@kya-os/cli
Version:
CLI for KYA-OS MCP-I setup and management
685 lines (684 loc) ⢠33.2 kB
JavaScript
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