UNPKG

@kya-os/cli

Version:

CLI for KYA-OS MCP-I setup and management

685 lines (684 loc) • 33.2 kB
import { enableMCPIdentityCLI, } from "@kya-os/mcp-i/cli-internal"; import chalk from "chalk"; import ora from "ora"; import { detectPlatform, getPlatformSpecificConfig, } from "../utils/platform-detector.js"; import { EnvManager } from "../utils/env-manager.js"; import { promptForInit, showError, showWarning, confirmMcpVariableReplacement, } from "../utils/prompts.js"; import { existsSync } from "fs"; import { join } from "path"; import { cliEffects } from "../effects/cli-integration.js"; import { createAgentTable, createClaimTable, createWelcomeBanner, createGradientBox, createSuccessBox, } from "../components/index.js"; import { showScreen } from "../utils/screen-manager.js"; import { validateInput, initOptionsSchema } from "../utils/validation.js"; async function checkRegistryHealth(verbose = false, endpoint = "https://knowthat.ai") { const healthSpinner = ora(`Checking registry status at ${endpoint}...`).start(); try { // Check the new health endpoint const healthResponse = await fetch(`${endpoint}/api/agents/auto-register/health`, { method: "GET", headers: { "User-Agent": "kya-os-cli", }, signal: AbortSignal.timeout(10000), // 10 second timeout }); if (healthResponse.ok) { const healthData = await healthResponse.json(); healthSpinner.succeed("Registry is operational"); if (verbose && healthData) { console.log(chalk.gray(` Service status: ${healthData.status || "healthy"}`)); } return { available: true, shouldProceed: true }; } else if (healthResponse.status === 503) { healthSpinner.fail("Registry is temporarily unavailable (maintenance mode)"); console.log(chalk.yellow("\nāš ļø The registry is undergoing maintenance.")); console.log(chalk.yellow(" This usually takes 5-15 minutes.")); console.log(chalk.yellow("\nšŸ”§ Options:")); console.log(" 1. Wait and try again in a few minutes"); console.log(" 2. Register manually at: https://knowthat.ai/submit-agent"); console.log(" 3. Create local config only (skip registration for now)"); return { available: false, shouldProceed: false }; } else { healthSpinner.warn("Registry status unknown, proceeding with caution..."); return { available: false, shouldProceed: true }; } } catch (error) { if (error.name === "TimeoutError") { healthSpinner.fail("Registry health check timed out"); console.log(chalk.yellow("\nāš ļø Could not reach the registry (network timeout).")); console.log(chalk.yellow(" Check your internet connection.")); } else if (error.code === "ENOTFOUND" || error.code === "ECONNREFUSED") { healthSpinner.fail("Registry is unreachable"); console.log(chalk.yellow("\nāš ļø Cannot connect to knowthat.ai registry.")); console.log(chalk.yellow(" The service may be down or you may have network issues.")); } else { healthSpinner.warn("Could not check registry status, proceeding anyway..."); if (verbose) { console.log(chalk.gray(` Error: ${error.message}`)); } } console.log(chalk.yellow("\nšŸ”§ Alternatives:")); console.log(" 1. Try again in a few minutes"); console.log(" 2. Register manually at: https://knowthat.ai/submit-agent"); console.log(" 3. Create local config only (we'll attempt registration anyway)"); return { available: false, shouldProceed: true }; } } async function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } async function attemptRegistrationWithRetry(config, verbose = false, maxRetries = 3, onProgress) { const baseDelay = 2000; // Start with 2 seconds for (let attempt = 1; attempt <= maxRetries; attempt++) { const isLastAttempt = attempt === maxRetries; const spinner = ora(`Connecting to KYA-OS network... (attempt ${attempt}/${maxRetries})`).start(); try { const result = await enableMCPIdentityCLI({ ...config, onProgress: async (event) => { // Update spinner based on progress switch (event.stage) { case "checking_existing": spinner.text = "Checking for existing identity..."; break; case "generating_keys": spinner.text = "Generating cryptographic keys..."; break; case "registering": spinner.text = "Registering with KYA-OS network..."; break; case "saving": spinner.text = "Saving identity..."; break; case "complete": spinner.succeed("Agent registered successfully!"); break; } // Call the provided callback if any if (onProgress) { await onProgress(event); } }, }); // The mcp-i package now handles async registration internally // We just get the final result return { success: true, identity: result.identity, metadata: result.metadata, }; } catch (error) { spinner.fail(`Registration attempt ${attempt} failed`); const errorInfo = categorizeError(error, verbose); if (errorInfo.category === "CRITICAL" || isLastAttempt) { // Don't retry for critical errors, or if this was the last attempt handleRegistrationError(error, errorInfo, verbose, isLastAttempt); return { success: false, error }; } else if (errorInfo.category === "RETRYABLE") { // Calculate exponential backoff delay const delay = baseDelay * Math.pow(2, attempt - 1); console.log(chalk.yellow(` ${errorInfo.message}`)); console.log(chalk.gray(` Retrying in ${delay / 1000} seconds...`)); if (attempt < maxRetries) { await sleep(delay); } } else { // Unknown error - try once more but show the error if (verbose) { console.log(chalk.gray(` ${errorInfo.debugInfo}`)); } console.log(chalk.yellow(` ${errorInfo.message}`)); if (attempt < maxRetries) { console.log(chalk.gray(` Trying again...`)); await sleep(baseDelay); } } } } return { success: false }; } function categorizeError(error, verbose = false) { const errorMessage = error.message || ""; const status = error.response?.status || error.status; const responseData = error.response?.data; // Build debug info let debugInfo = ""; if (verbose) { debugInfo = `Error type: ${error.constructor.name}, Status: ${status}`; if (responseData) { debugInfo += `, Response: ${JSON.stringify(responseData, null, 2)}`; } } // Rate limiting if (status === 429 || errorMessage.includes("429") || errorMessage.includes("rate limit")) { return { category: "RETRYABLE", message: "Rate limit exceeded. The registry is busy.", debugInfo, }; } // Service temporarily unavailable if (status === 503 || errorMessage.includes("503")) { return { category: "RETRYABLE", message: "Registry is temporarily unavailable (maintenance or overload).", debugInfo, }; } // Server errors that might be transient if (status === 500 || status === 502 || errorMessage.includes("500") || errorMessage.includes("502")) { return { category: "RETRYABLE", message: "Registry server error. This may be temporary.", debugInfo, }; } // Timeout errors if (error.name === "TimeoutError" || errorMessage.includes("timeout")) { return { category: "RETRYABLE", message: "Request timed out. Network or server may be slow.", debugInfo, }; } // Network errors if (error.code === "ENOTFOUND" || error.code === "ECONNREFUSED" || errorMessage.includes("ENOTFOUND")) { return { category: "RETRYABLE", message: "Cannot reach the registry. Check your internet connection.", debugInfo, }; } // Client errors that shouldn't be retried if (status >= 400 && status < 500 && status !== 429) { return { category: "CRITICAL", message: `Registration was rejected by the registry (${status}).`, debugInfo, }; } // MCP-I specific errors if (errorMessage.includes("Failed to auto-register agent")) { return { category: "CRITICAL", message: "The registry rejected the registration request.", debugInfo, }; } // Unknown error return { category: "UNKNOWN", message: errorMessage || "An unexpected error occurred during registration.", debugInfo, }; } function handleRegistrationError(_error, errorInfo, verbose, isFinalAttempt) { // Show main error message showError(errorInfo.message); // Show debug info if verbose if (verbose && errorInfo.debugInfo) { console.log(chalk.gray("\nDebug information:")); console.log(chalk.gray(errorInfo.debugInfo)); } // Show context-specific help if (errorInfo.category === "CRITICAL") { console.log(chalk.yellow("\nšŸ”§ This error cannot be resolved by retrying.")); console.log(chalk.yellow(" Possible solutions:")); console.log(" 1. Register manually at: https://knowthat.ai/submit-agent"); console.log(" 2. Check if your agent name is already taken"); console.log(" 3. Verify your repository URL is accessible"); console.log(" 4. Contact support if the problem persists"); } else if (isFinalAttempt) { console.log(chalk.yellow("\nšŸ”§ Registration failed after multiple attempts.")); console.log(chalk.yellow(" Next steps:")); console.log(" 1. Wait 5-10 minutes and try again"); console.log(" 2. Check knowthat.ai status page (if available)"); console.log(" 3. Register manually at: https://knowthat.ai/submit-agent"); console.log(" 4. Create local config only: npx kya-os init --skip-registration"); } // Show alternative registration method console.log(chalk.cyan("\nšŸ“ Manual registration process:")); console.log(" 1. Visit: https://knowthat.ai/submit-agent"); console.log(" 2. Fill in your agent details"); console.log(" 3. Get your DID and run: npx kya-os claim <your-agent-id>"); } async function pollForClaimStatus(agentName, did, endpoint = "https://knowthat.ai", options) { const spinner = ora({ text: "Waiting for you to claim your agent...", color: "yellow", }).start(); const maxAttempts = 60; // 5 minutes (60 * 5 seconds) let attempts = 0; // Add ability to skip polling with Ctrl+C let skipPolling = false; const handleInterrupt = () => { skipPolling = true; spinner.warn(chalk.yellow("ā­ļø Skipping claim check...")); }; process.on('SIGINT', handleInterrupt); while (attempts < maxAttempts && !skipPolling) { try { // Check if agent is claimed by checking the agent's profile endpoint // The API uses the agent name as the slug in the URL const response = await fetch(`${endpoint}/api/agents/${encodeURIComponent(agentName)}`, { method: "GET", headers: { "User-Agent": "kya-os-cli", Accept: "application/json", }, }); if (response.ok) { const data = await response.json(); // Debug logging if (process.env.DEBUG || options?.verbose) { console.log(chalk.gray(`\n[DEBUG] Agent status response:`)); console.log(chalk.gray(JSON.stringify(data, null, 2))); } // Check various indicators that the agent has been claimed // The agent is claimed if: // 1. It has a userId (owner) // 2. Its status is 'active' (not 'draft') // 3. It's not marked as a draft if (data && (data.userId || data.status === 'active' || data.owner || data.claimedAt || (data.isDraft === false) || (!data.hasOwnProperty('isDraft') && data.id) // Some agents don't have isDraft field )) { spinner.succeed(chalk.green("āœ… Agent claimed successfully!")); console.log(chalk.gray(`\n You can now manage your agent at:`)); console.log(chalk.cyan(` ${endpoint.replace('http://localhost:3000', 'https://knowthat.ai')}/agents/${agentName}`)); return true; } } else if (response.status === 404) { // 404 might mean the agent hasn't been indexed yet after creation // Continue polling } // Wait 5 seconds before next check await sleep(5000); attempts++; // Update spinner text with countdown const remainingTime = (maxAttempts - attempts) * 5; const minutes = Math.floor(remainingTime / 60); const seconds = remainingTime % 60; spinner.text = `Waiting for you to claim your agent... (${minutes}:${seconds .toString() .padStart(2, "0")} remaining)`; } catch (error) { // Ignore network errors and keep polling } } // Clean up interrupt handler process.removeListener('SIGINT', handleInterrupt); if (skipPolling) { console.log(chalk.gray("\n You can complete the claim process later.")); console.log(chalk.gray(" The claim URL remains valid.")); return false; } spinner.warn(chalk.yellow("ā±ļø Claim check timed out")); console.log(chalk.gray("\n Don't worry! You can still claim your agent.")); console.log(chalk.gray(" The claim URL remains valid.")); console.log(chalk.cyan(`\n Visit: ${endpoint.replace('http://localhost:3000', 'https://knowthat.ai')}/agents/${agentName}`)); return false; } export async function init(options) { try { // Validate options const validatedOptions = validateInput(initOptionsSchema, options, "init options"); // Show welcome banner try { const banner = await createWelcomeBanner("KYA-OS"); console.log(banner); } catch (err) { // Fallback if figlet fails console.log(chalk.cyan.bold("\nšŸš€ MCP-I Initialization\n")); } // Detect platform const platformInfo = detectPlatform(); console.log(chalk.cyan("\nšŸ” Detected environment:")); console.log(` Platform: ${chalk.bold(platformInfo.platform)}`); if (platformInfo.framework) { console.log(` Framework: ${chalk.bold(platformInfo.framework)}`); } console.log(` Package Manager: ${chalk.bold(platformInfo.packageManager)}`); // Check for existing configuration const envManager = new EnvManager(); const existingIdentityFile = existsSync(join(process.cwd(), ".mcp-identity.json")); const existingEnvVars = envManager.getFromProcess(); if ((existingIdentityFile || existingEnvVars.MCP_IDENTITY_AGENT_DID) && !validatedOptions.force) { showWarning("MCP-I is already configured in this project."); console.log("Use --force to reconfigure."); // Show current configuration if (existingEnvVars.MCP_IDENTITY_AGENT_DID) { console.log(`\nCurrent DID: ${chalk.green(existingEnvVars.MCP_IDENTITY_AGENT_DID)}`); } return; } // Prompt for configuration const answers = await promptForInit({ name: validatedOptions.name, description: validatedOptions.description, repository: validatedOptions.repository, }); // Show blackhole animation immediately after repository URL input console.log("\n"); const didGenerationText = `šŸ” "Generating Decentralized Identity"\n` + `\n` + `Creating cryptographic keys...`; await cliEffects.showCustomEffect("blackhole", didGenerationText, { config: { duration: 3000, // Shorter initial duration blackholeColor: "ffffff", starColors: [ "ffcc0d", "ff7326", "ff194d", "bf2669", "702a8c", "049dbf", ], finalColor: "00ff00", useGradient: false, gradientDirection: "diagonal", blackholeSize: 0.3, }, persistent: false, skipExitPrompt: true, }); // Determine directories configuration let directories; if (answers.directories === "verified") { directories = "verified"; } else if (answers.directories === "none") { directories = "none"; } else { directories = answers.specificDirectories || []; } // Get platform-specific config const platformConfig = getPlatformSpecificConfig(platformInfo.platform); // Determine API endpoint const apiEndpoint = validatedOptions.endpoint || (validatedOptions.local ? 'http://localhost:3000' : 'https://knowthat.ai'); if (apiEndpoint !== 'https://knowthat.ai') { console.log(chalk.yellow(`\nāš ļø Using custom endpoint: ${apiEndpoint}`)); } let identity; let envVars = null; if (!validatedOptions.skipRegistration && answers.confirmRegistration) { // Check registry health first const healthCheck = await checkRegistryHealth(validatedOptions.verbose, apiEndpoint); if (!healthCheck.available && !healthCheck.shouldProceed) { console.log("\nYou can still create the configuration files and register later."); answers.confirmEnvCreation = await promptForInit({ confirmEnvCreation: true, }).then((a) => a.confirmEnvCreation); } else { // Register agent with retry logic and enhanced error handling let registrationResult; try { registrationResult = await attemptRegistrationWithRetry({ name: answers.name, description: answers.description, repository: answers.repository, directories: directories, storage: platformConfig.storage, transport: platformConfig.transport, mode: "production", logLevel: "silent", apiEndpoint: apiEndpoint, registryEndpoint: "cli", // Use CLI endpoint for better CLI experience }, validatedOptions.verbose, 3, // maxRetries async (event) => { // Handle progress events for spinner updates // The blackhole effect will be shown after successful registration }); } catch (registrationError) { // Enhanced error handling for common scenarios if (registrationError.message?.includes("already exists") || registrationError.message?.includes("name taken") || registrationError.response?.status === 409) { showError("Agent name already exists in the registry."); console.log(chalk.yellow("\nšŸ” This usually means:")); console.log(" 1. You previously registered this agent name"); console.log(" 2. Your .env file was deleted/missing"); console.log(" 3. Someone else is using this name\n"); console.log(chalk.cyan("šŸ’” Solutions:")); console.log(" 1. If this is YOUR agent, restore your .env file:"); console.log(` ${chalk.green('MCP_IDENTITY_AGENT_DID="did:web:knowthat.ai:agents:' + answers.name + '"')}`); console.log(` ${chalk.green('MCP_IDENTITY_AGENT_PUBLIC_KEY="your-public-key"')}`); console.log(` ${chalk.green('MCP_IDENTITY_AGENT_PRIVATE_KEY="your-private-key"')}`); console.log("\n 2. Or choose a different agent name"); console.log(` 3. Or visit: ${chalk.blue("https://knowthat.ai/agents/claim")}\n`); process.exit(1); } // For other errors, show them and continue with config creation option showError(`Registration failed: ${registrationError.message}`); console.log("\nYou can still create the configuration files and register later."); answers.confirmEnvCreation = await promptForInit({ confirmEnvCreation: true, }).then((a) => a.confirmEnvCreation); registrationResult = { success: false }; } if (registrationResult.success && registrationResult.identity) { identity = registrationResult.identity; const metadata = registrationResult.metadata; // Extract environment variables envVars = { MCP_IDENTITY_AGENT_DID: identity.did, MCP_IDENTITY_AGENT_PUBLIC_KEY: identity.publicKey, MCP_IDENTITY_AGENT_PRIVATE_KEY: identity.privateKey || "", MCP_IDENTITY_AGENT_ID: identity.agentId || "", MCP_IDENTITY_AGENT_SLUG: identity.agentSlug || "", }; // Show blackhole effect for successful registrations if (validatedOptions.verbose) { console.log(chalk.gray(`[DEBUG] metadata.isNewIdentity: ${metadata?.isNewIdentity}`)); console.log(chalk.gray(`[DEBUG] Full metadata: ${JSON.stringify(metadata)}`)); } // Show the actual DID now that we have it if (metadata?.did) { // Update the display with the actual DID console.log("\n"); console.log(chalk.green(`āœ… Identity created: ${metadata.did}`)); console.log(); // 1. Claim Your Agent screen (FIRST) if (metadata?.claimUrl) { await showScreen(createClaimTable({ claimUrl: metadata.claimUrl }), { requireInput: true, clearBefore: true, prompt: "", // The table already has the prompt }); // Dynamically import and open the claim URL in the browser try { const open = (await import("open")).default; await open(metadata.claimUrl); } catch (error) { console.log(chalk.yellow("\nāš ļø Could not open browser automatically")); console.log(chalk.cyan(` Please visit: ${metadata.claimUrl}`)); } // Start polling for claim status (unless skipped) if (!validatedOptions.skipClaimCheck) { const claimed = await pollForClaimStatus(answers.name, metadata.did, apiEndpoint, { verbose: validatedOptions.verbose }); } else { console.log(chalk.gray("\n Skipping claim check...")); console.log(chalk.gray(" You can claim your agent anytime at:")); console.log(chalk.cyan(` ${metadata.claimUrl}`)); } // Clear screen after polling for clean transition console.clear(); } // 2. Agent Identity Created screen (SECOND) const agentTable = createAgentTable({ name: answers.name, description: answers.description, did: metadata?.did || envVars?.MCP_IDENTITY_AGENT_DID || "", privateKey: envVars?.MCP_IDENTITY_AGENT_PRIVATE_KEY || "", agentSlug: answers.name, profileUrl: `https://knowthat.ai/agents/${answers.name}`, }); await showScreen(agentTable, { requireInput: true, clearBefore: true, prompt: "Press Enter to continue...", }); } } } } // Create environment files if (answers.confirmEnvCreation && envVars) { console.log(`\nšŸ“„ ${chalk.bold("Creating environment files...")}`); try { // Check for existing MCP variables in env files const envFiles = envManager.scanEnvFiles(); let hasExistingMcpVars = false; let existingDid = ""; for (const envFile of envFiles) { if (envFile.exists && envFile.hasMcpIdentity && envFile.mcpVariables?.MCP_IDENTITY_AGENT_DID) { hasExistingMcpVars = true; existingDid = envFile.mcpVariables.MCP_IDENTITY_AGENT_DID; break; } } // If existing MCP variables found and not forcing, prompt for confirmation if (hasExistingMcpVars && !validatedOptions.force) { const shouldReplace = await confirmMcpVariableReplacement(existingDid); if (!shouldReplace) { console.log(chalk.yellow("\n⚠ Skipping environment file update.")); console.log(" Your existing MCP identity has been preserved."); return; } } // Use smart insertion with best practices const result = await envManager.smartInsertMcpVariables(envVars, { force: validatedOptions.force, }); if (result.success) { // Show operation result const operation = result.operation === "created" ? "Created" : result.operation === "updated" ? "Updated" : "Replaced variables in"; console.log(` āœ“ ${operation} ${chalk.green(result.targetFile)}`); // Show warnings if any if (result.warnings.length > 0) { result.warnings.forEach((warning) => { console.log(` ${chalk.yellow("⚠")} ${warning}`); }); } // Show conflicts if any if (result.conflicts && result.conflicts.length > 0) { console.log(` ${chalk.yellow("⚠")} Found conflicting values in other files:`); result.conflicts.forEach((conflict) => { console.log(` ${conflict.file}: ${conflict.variable}="${conflict.value.substring(0, 20)}..."`); }); console.log(` ${chalk.cyan("ℹ")} Using ${result.targetFile} as authoritative source`); } // Check gitignore const filesToIgnore = [ result.targetFile, ".mcp-identity.json", // Also add common .env files to gitignore ".env", ".env.local", ".env.development", ".env.dev", ]; const { missing } = envManager.checkGitignore(filesToIgnore); if (missing.length > 0) { envManager.addToGitignore(missing); console.log(` āœ“ Added to ${chalk.green(".gitignore")}: ${missing.join(", ")}`); } } else { showError(`Environment file setup failed: ${result.warnings.join(", ")}`); } } catch (error) { showError(`Failed to create environment files: ${error.message}`); } } // 3. Combined Environment Configuration & Success screen const envConfigContent = [ "šŸ“ Created files:", "", " • .env.local (for local development)", " • Added to .gitignore automatically", ]; const combinedScreen = `${createGradientBox(envConfigContent, { title: "Environment Configuration", gradientStyle: "cool", })} ${createSuccessBox("MCP-I initialization complete!", "Your AI agent now has a verifiable decentralized identity on the KYA-OS network.")}`; await showScreen(combinedScreen, { requireInput: true, clearBefore: true, prompt: "Press Enter to continue...", }); // 4. Integration Guide (no rainbow colors) const integrationContent = [ "Next Steps: ", "", "1. Install the MCP-I package:", ` ${chalk.cyan(`${platformInfo.packageManager} install @kya-os/mcp-i`)}`, "", "2. Add to your code:", ` ${chalk.cyan('import "@kya-os/mcp-i/auto"')}`, "", ]; if (platformInfo.platform === "vercel" || platformInfo.platform === "nextjs") { integrationContent.push("3. For production deployment:", " • Go to Vercel Dashboard → Settings → Environment Variables", " • Add all MCP_IDENTITY_* variables from .env.local", ` • Run: ${chalk.cyan("npx kya-os env copy")} to copy them to clipboard`, ""); } // Add platform-specific instructions if any if (platformConfig.instructions.length > 0) { integrationContent.push(`${chalk.bold("Platform-specific notes:")}`, ...platformConfig.instructions.map((instruction, i) => `${i + 1}. ${instruction}`)); } await showScreen(createGradientBox(integrationContent, { title: "Integration Guide", gradientStyle: "cool", // Changed from rainbow to cool }), { requireInput: true, clearBefore: true, prompt: "Press Enter to exit...", }); // Explicitly exit after showing the final screen process.exit(0); } catch (error) { showError(error.message || "Initialization failed"); process.exit(1); } } //# sourceMappingURL=init.js.map