UNPKG

@kya-os/cli

Version:

CLI for MCP-I setup and management

315 lines • 15.7 kB
import chalk from "chalk"; import ora from "ora"; import { existsSync, readFileSync } from "fs"; import { join } from "path"; import { EnvManager } from "../utils/env-manager.js"; import { showSuccess, showError, showInfo } from "../utils/prompts.js"; import contractsRegistry from "@kya-os/contracts/registry"; const { RECEIPT_SCHEMA_ID, CLAIM_TOKEN_TTL_HOURS, } = contractsRegistry; import { status } from "./status.js"; /** * Claim an agent with a token or retrieve management URLs * * This command serves two purposes: * 1. With token: Attempts to claim ownership of an agent using a claim token * 2. Without token: Retrieves management URLs for the current agent * * The claim process requires authentication through a web browser as it uses * NextAuth for user verification. The CLI generates the claim URL which must * be opened in a browser to complete the claim process. * * @param token - Optional claim token from registration * @param options - Command options */ export async function claim(token, options = {}) { const { json = false, saveReceipt = true, receiptPath = ".mcpi/receipts", } = options; if (!json) { console.log(chalk.cyan(token ? "\n🎯 Claiming Agent with Token\n" : "\n🎯 Agent Management URLs\n")); } // Identity Loading Strategy: // 1. First, load identity from environment variables (highest priority) // 2. Then, check for .mcpi/identity.json file as fallback // 3. Only use file values for fields not already set in environment // This prevents overwriting env vars with potentially stale file data const envManager = new EnvManager(); let processVars = envManager.getFromProcess(); // Check for identity file as secondary source let identityFromFile = null; const identityPath = join(process.cwd(), ".mcpi", "identity.json"); if (existsSync(identityPath)) { try { const identityContent = readFileSync(identityPath, "utf-8"); identityFromFile = JSON.parse(identityContent); // Validate required fields exist if (identityFromFile && typeof identityFromFile === 'object') { const hasRequiredFields = identityFromFile.did && identityFromFile.publicKey; if (!hasRequiredFields) { console.warn(chalk.yellow('⚠️ Identity file is missing required fields')); identityFromFile = null; } } } catch (error) { // Log parse error for debugging but continue if (!json && process.env.DEBUG) { console.warn(chalk.yellow(`⚠️ Failed to parse identity file: ${error instanceof Error ? error.message : 'Unknown error'}`)); } identityFromFile = null; } } // Merge identity from file ONLY for missing environment variables // This ensures environment variables take precedence over file if (identityFromFile) { if (!processVars.AGENT_DID && identityFromFile.did) { processVars.AGENT_DID = identityFromFile.did; } if (!processVars.AGENT_KEY_ID && identityFromFile.kid) { processVars.AGENT_KEY_ID = identityFromFile.kid; } if (!processVars.AGENT_PUBLIC_KEY && identityFromFile.publicKey) { processVars.AGENT_PUBLIC_KEY = identityFromFile.publicKey; } } if (!processVars.AGENT_DID) { const error = 'No XMCP-I identity found. Run "mcpi init" first.'; if (json) { console.log(JSON.stringify({ success: false, error }, null, 2)); } else { showError(error); } return; } const spinner = json ? null : ora(token ? "Processing claim token..." : "Retrieving management URLs...").start(); try { if (token) { // Token-Based Claim Flow: // The claim process requires web authentication because: // 1. KTA uses NextAuth for user authentication // 2. Claims must be associated with authenticated user accounts // 3. This ensures only legitimate users can claim agents // // Flow: // 1. Validate token format and TTL locally // 2. Print browser URL for user authentication // 3. Poll API to confirm claim completion // 4. Report success only after server verification // Step 1: Validate token format // Token must follow format: claim-token:timestamp_uuid if (!token?.startsWith("claim-token:")) { const error = new Error("Invalid claim token format. Expected format: claim-token:timestamp_uuid"); error.code = "XMCP_I_ECLAIM"; throw error; } // Step 2: Validate token TTL // Extract timestamp from token and check if expired const tsPart = token.split(":")[1]?.split("_")[0]; const issuedMs = Number(tsPart); if (!Number.isFinite(issuedMs)) { const error = new Error("Token missing or invalid timestamp"); error.code = "XMCP_I_ECLAIM"; throw error; } const maxAgeMs = CLAIM_TOKEN_TTL_HOURS * 60 * 60 * 1000; const ageMs = Date.now() - issuedMs; if (ageMs > maxAgeMs) { const error = new Error(`Claim token expired (${CLAIM_TOKEN_TTL_HOURS}h TTL). Token age: ${Math.floor(ageMs / (60 * 60 * 1000))}h. Please register again to get a new token.`); error.code = "XMCP_I_ECLAIM"; throw error; } // Step 3: Generate claim URL for browser authentication const profileBaseUrl = process.env.KTA_BASE_URL || "https://knowthat.ai"; const claimPageUrl = new URL(`/claim/by-did/${encodeURIComponent(processVars.AGENT_DID)}`, profileBaseUrl); claimPageUrl.searchParams.set('token', token); spinner?.stop(); if (!json) { console.log(chalk.yellow("\n⚠️ Browser authentication required")); console.log(chalk.gray("\nThe claim process requires you to authenticate in a web browser.")); console.log(chalk.gray("Please visit the URL below to complete the claim:\n")); console.log(chalk.cyan(` ${claimPageUrl.toString()}\n`)); console.log(chalk.gray("Waiting for claim to complete...")); } // Step 4: Poll API to check claim status const apiBaseUrl = process.env.KTA_API_URL || process.env.KTA_BASE_URL || "https://knowthat.ai"; const statusCheckUrl = new URL("/api/agents/claim", apiBaseUrl); statusCheckUrl.searchParams.set('did', processVars.AGENT_DID); const maxAttempts = 60; // 5 minutes (5 second intervals) const pollInterval = 5000; // 5 seconds let claimed = false; let attempts = 0; spinner?.start("Checking claim status..."); while (!claimed && attempts < maxAttempts) { try { const response = await fetch(statusCheckUrl.toString()); if (response.ok) { const statusData = await response.json(); if (statusData.claimed === true || (statusData.status === 'active' && statusData.isDraft === false)) { claimed = true; break; } } else if (response.status === 404) { // Agent not found - token might be invalid throw new Error("Agent not found. The claim token may be invalid or expired."); } } catch (fetchError) { // Log error but continue polling (might be transient network issue) if (process.env.DEBUG && !json) { console.warn(chalk.yellow(`Poll attempt ${attempts + 1} failed: ${fetchError instanceof Error ? fetchError.message : 'Unknown error'}`)); } } attempts++; if (!claimed && attempts < maxAttempts) { await new Promise(resolve => setTimeout(resolve, pollInterval)); } } if (!claimed) { spinner?.fail("Claim verification timeout"); const error = new Error(`Claim not confirmed after ${maxAttempts * pollInterval / 1000} seconds. ` + `The token may have expired (${CLAIM_TOKEN_TTL_HOURS}h TTL) or you may not have completed authentication in the browser.`); error.code = "XMCP_I_ECLAIM"; throw error; } // Step 5: Fetch agent details after successful claim let agent; try { const agentResponse = await fetch(statusCheckUrl.toString()); if (agentResponse.ok) { const agentData = await agentResponse.json(); agent = agentData; } } catch (fetchError) { // Non-fatal - we know claim succeeded if (process.env.DEBUG && !json) { console.warn(chalk.yellow('Could not fetch updated agent details')); } } spinner?.succeed("Agent claimed successfully!"); const agentURL = new URL(`/agents/by-did/${encodeURIComponent(processVars.AGENT_DID)}`, profileBaseUrl).toString(); const editURL = new URL(`/agents/by-did/${encodeURIComponent(processVars.AGENT_DID)}/edit`, profileBaseUrl).toString(); if (json) { const output = { success: true, claim: { agentURL, editURL, claimedAt: agent?.claimedAt || new Date().toISOString(), }, }; console.log(JSON.stringify(output, null, 2)); } else { console.log(`\n${chalk.bold("✅ Agent Claimed Successfully!")}`); console.log(` Agent URL: ${chalk.cyan(agentURL)}`); console.log(` Edit URL: ${chalk.cyan(editURL)}`); if (agent?.claimedAt) { console.log(` Claimed At: ${chalk.gray(agent.claimedAt)}`); } } // Re-run status after successful claim (R2.12) if (!json) { console.log(`\n${chalk.gray("Running status check...")}`); try { await status({ json: false }); } catch (error) { console.warn(chalk.yellow(`⚠️ Status check failed: ${error instanceof Error ? error.message : "Unknown error"}`)); } } } else { // URL retrieval flow const baseUrl = process.env.KTA_BASE_URL || "https://knowthat.ai"; const encodedDid = encodeURIComponent(processVars.AGENT_DID); const editUrl = new URL(`/agents/by-did/${encodedDid}/edit`, baseUrl).toString(); const claimUrl = new URL(`/claim/by-did/${encodedDid}`, baseUrl).toString(); spinner?.succeed("URLs retrieved successfully!"); if (json) { const output = { success: true, urls: { editUrl, claimUrl, profileUrl: new URL(`/agents/by-did/${encodedDid}`, baseUrl).toString(), }, agent: { did: processVars.AGENT_DID, kid: processVars.AGENT_KEY_ID, }, }; console.log(JSON.stringify(output, null, 2)); } else { console.log(`\n${chalk.bold("📝 Agent Information:")}`); console.log(` DID: ${chalk.gray(processVars.AGENT_DID)}`); console.log(` Key ID: ${chalk.gray(processVars.AGENT_KEY_ID)}`); console.log(` Profile: ${chalk.cyan(new URL(`/agents/by-did/${encodedDid}`, baseUrl).toString())}`); if (claimUrl) { console.log(`\n${chalk.bold("🎯 Claim Your Agent:")}`); console.log(` ${chalk.cyan(claimUrl)}`); console.log(`\n ${chalk.gray("Use this link to:")}`); console.log(` ${chalk.gray("• Set up your account")}`); console.log(` ${chalk.gray("• Edit agent details")}`); console.log(` ${chalk.gray("• Add logos and descriptions")}`); console.log(` ${chalk.gray("• Manage directory listings")}`); console.log(` ${chalk.gray("• View analytics")}`); showInfo("This is a one-time claim link. Once claimed, use the edit URL."); } console.log(`\n${chalk.bold("✏️ Edit Your Agent:")}`); console.log(` ${chalk.cyan(editUrl)}`); console.log(` ${chalk.gray("(Requires authentication after claiming)")}`); // Check if agent is draft if (processVars.AGENT_DID?.includes("localhost")) { showInfo("This is a development/draft agent. It won't appear in public directories."); } showSuccess("Management URLs ready!"); } } } catch (error) { spinner?.fail(token ? "Claim failed" : "Failed to retrieve URLs"); const errorMessage = error.message || (token ? "Failed to process claim token" : "Failed to get management URLs"); if (json) { console.log(JSON.stringify({ success: false, error: errorMessage, code: error.code || (token ? "XMCP_I_ECLAIM" : "XMCP_I_EURLS"), }, null, 2)); } else { if (error.code === "XMCP_I_ECLAIM" || error.message?.includes("XMCP_I_ECLAIM")) { showError(error.message || `Claim failed. Token may be invalid or expired (${CLAIM_TOKEN_TTL_HOURS}h TTL)`); console.log(`\n${chalk.gray("To get a new claim token:")}`); console.log(`${chalk.cyan("mcpi register")}`); } else if (error.message?.includes("429")) { showError("Rate limit exceeded. Please try again in a few minutes."); } else if (error.message?.includes("Network")) { showError("Network error. Check your internet connection."); } else { showError(errorMessage); } // Provide fallback URL const fallbackBaseUrl = process.env.KTA_BASE_URL || "https://knowthat.ai"; const fallbackProfileUrl = new URL(`/agents/by-did/${encodeURIComponent(processVars.AGENT_DID)}`, fallbackBaseUrl).toString(); console.log(`\n${chalk.gray("You can try visiting directly:")}`); console.log(`${chalk.cyan(fallbackProfileUrl)}`); } if (token && (error.code === "XMCP_I_ECLAIM" || error.message?.includes("XMCP_I_ECLAIM"))) { process.exit(24); // XMCP_I_ECLAIM exit code } } } //# sourceMappingURL=claim.js.map