@kya-os/cli
Version:
CLI for MCP-I setup and management
530 lines ⢠20.4 kB
JavaScript
import chalk from "chalk";
import ora from "ora";
import { existsSync, readFileSync } from "fs";
import { join } from "path";
import { exec } from "child_process";
import { promisify } from "util";
import { EnvManager } from "../utils/env-manager.js";
import { detectPlatform } from "../utils/platform-detector.js";
import { ktaApi, isKTAAvailable } from "../utils/kta-api.js";
import { showSuccess, showError, showWarning, } from "../utils/prompts.js";
const execAsync = promisify(exec);
/**
* Comprehensive system health check for XMCP-I
* Implements Requirements 16.2, 16.3, 16.4, 16.5, 16.6
*/
export async function doctor(options = {}) {
const { json = false, verbose = false } = options;
if (!json) {
console.log(chalk.cyan("\n𩺠XMCP-I Doctor - System Health Check\n"));
}
const spinner = json ? null : ora("Running diagnostics...").start();
try {
// Run all diagnostic checks
const [packages, xmcpUpstream, environment, kta, cache] = await Promise.all([
checkPackageVersions(verbose),
checkXMCPUpstreamCompatibility(verbose),
checkEnvironmentConfiguration(verbose),
checkKTAConnectivity(verbose),
checkNonceCacheConfiguration(verbose),
]);
if (spinner) {
spinner.succeed("Diagnostics complete");
}
const result = {
packages,
xmcpUpstream,
environment,
kta,
cache,
};
if (json) {
const output = {
success: true,
result,
schemaId: "https://schemas.kya-os.ai/mcpi/doctor-result/v1.0.0.json",
timestamp: new Date().toISOString(),
};
console.log(JSON.stringify(output, null, 2));
}
else {
displayDoctorResults(result, verbose);
}
// Determine exit code based on critical issues
const hasCriticalIssues = !environment.valid ||
packages.some((p) => !p.compatible) ||
!xmcpUpstream.compatible;
if (hasCriticalIssues && process.env.NODE_ENV !== "test") {
process.exit(1);
}
}
catch (error) {
if (spinner) {
spinner.fail("Diagnostics failed");
}
const errorMessage = error.message || "Failed to run diagnostics";
if (json) {
console.log(JSON.stringify({
success: false,
error: errorMessage,
code: "XMCP_I_EDOCTOR",
}, null, 2));
}
else {
showError(`Doctor check failed: ${errorMessage}`);
}
if (process.env.NODE_ENV !== "test") {
process.exit(1);
}
}
}
/**
* Check all package versions for compatibility (R16.2)
*/
async function checkPackageVersions(verbose) {
const packages = [];
const packageJsonPath = join(process.cwd(), "package.json");
if (!existsSync(packageJsonPath)) {
return [
{
name: "package.json",
version: "not found",
compatible: false,
issues: ["No package.json found in current directory"],
},
];
}
try {
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
const allDeps = {
...packageJson.dependencies,
...packageJson.devDependencies,
...packageJson.peerDependencies,
};
// Check XMCP-I related packages
const xmcpPackages = [
"mcpi",
"@kya-os/cli",
"@kya-os/contracts",
"@kya-os/create-mcpi-app",
"create-mcpi-app",
"@kya-os/mcp-i", // deprecated
];
for (const pkgName of xmcpPackages) {
const version = allDeps[pkgName];
if (version) {
const compatible = await checkPackageCompatibility(pkgName, version, verbose);
packages.push({
name: pkgName,
version,
compatible: compatible.compatible,
issues: compatible.issues,
});
}
}
// Check for deprecated packages
if (allDeps["@kya-os/mcp-i"]) {
const mcpIPackage = packages.find((p) => p.name === "@kya-os/mcp-i");
if (mcpIPackage) {
mcpIPackage.issues = mcpIPackage.issues || [];
mcpIPackage.issues.push("This package is deprecated. Migrate to 'mcpi'");
}
}
}
catch (error) {
packages.push({
name: "package.json",
version: "error",
compatible: false,
issues: [`Failed to parse package.json: ${error.message}`],
});
}
return packages;
}
/**
* Check individual package compatibility
*/
async function checkPackageCompatibility(name, version, verbose) {
const issues = [];
let compatible = true;
try {
// Check if package is installed
const { stdout } = await execAsync(`npm list ${name} --depth=0 --json`, {
cwd: process.cwd(),
});
const listResult = JSON.parse(stdout);
const installedVersion = listResult.dependencies?.[name]?.version;
if (!installedVersion) {
compatible = false;
issues.push(`Package ${name} is not installed`);
return { compatible, issues };
}
// Version compatibility checks
if (name === "mcpi" ||
name === "@kya-os/cli" ||
name === "@kya-os/contracts") {
// These should be on compatible versions (1.x.x)
if (!installedVersion.startsWith("1.")) {
compatible = false;
issues.push(`Expected version 1.x.x, found ${installedVersion}`);
}
}
if (verbose) {
issues.push(`Installed version: ${installedVersion}`);
}
}
catch (error) {
if (verbose) {
issues.push(`Could not verify installation: ${error.message}`);
}
}
return { compatible, issues };
}
/**
* Verify XMCP upstream version compatibility (R16.3)
*/
async function checkXMCPUpstreamCompatibility(verbose) {
const issues = [];
let compatible = true;
let version = "unknown";
try {
// Check if XMCP is installed
const { stdout } = await execAsync("npm list xmcp --depth=0 --json", {
cwd: process.cwd(),
});
const listResult = JSON.parse(stdout);
const installedVersion = listResult.dependencies?.xmcp?.version;
if (installedVersion) {
version = installedVersion;
// Check compatibility with known versions
// Based on design doc: default caret ^0.3.1
const majorVersion = installedVersion.split(".")[0];
const minorVersion = installedVersion.split(".")[1];
if (majorVersion !== "0" || parseInt(minorVersion) < 3) {
compatible = false;
issues.push(`XMCP version ${installedVersion} may not be compatible. Expected ^0.3.1`);
}
if (verbose) {
issues.push(`Found XMCP version: ${installedVersion}`);
}
}
else {
compatible = false;
issues.push("XMCP package not found. This is required for XMCP-I runtime");
}
// Check for latest version availability
if (verbose) {
try {
const { stdout: viewOutput } = await execAsync("npm view xmcp version");
const latestVersion = viewOutput.trim();
if (latestVersion !== installedVersion) {
issues.push(`Latest available version: ${latestVersion}`);
}
}
catch (error) {
if (verbose) {
issues.push("Could not check latest version");
}
}
}
}
catch (error) {
compatible = false;
version = "not found";
issues.push("XMCP package not installed");
if (verbose) {
issues.push(`Error: ${error.message}`);
}
}
return { version, compatible, issues };
}
/**
* Validate platform environment variables and identity configuration (R16.4)
*/
async function checkEnvironmentConfiguration(verbose) {
const envManager = new EnvManager();
const issues = [];
// Check process environment
const processVars = envManager.getFromProcess();
const { valid, missing } = envManager.validateVariables(processVars);
// Check environment files
const envFiles = envManager.scanEnvFiles();
const hasValidEnvFile = envFiles.some((f) => f.hasmcpidentity && f.exists);
// Platform-specific checks
const platform = detectPlatform();
if (verbose) {
issues.push(`Detected platform: ${platform.platform}`);
issues.push(`Package manager: ${platform.packageManager}`);
}
// Environment validation
if (!valid && !hasValidEnvFile) {
issues.push("No valid environment configuration found");
issues.push("Run 'mcpi init' to set up identity");
}
if (missing.length > 0) {
issues.push(`Missing environment variables: ${missing.join(", ")}`);
}
// Platform-specific environment checks
if (platform.platform === "vercel" && process.env.VERCEL) {
if (!valid) {
issues.push("Running on Vercel but environment variables not configured");
issues.push("Add variables to Vercel Dashboard ā Settings ā Environment Variables");
}
}
// Check .gitignore for security
const sensitiveFiles = [".env", ".env.local", ".mcpi/identity.json"];
const { missing: notIgnored } = envManager.checkGitignore(sensitiveFiles);
if (notIgnored.length > 0) {
issues.push(`Sensitive files not in .gitignore: ${notIgnored.join(", ")}`);
}
// Check identity file in development
const identityPath = join(process.cwd(), ".mcpi", "identity.json");
if (existsSync(identityPath)) {
if (verbose) {
issues.push("Development identity file found");
}
try {
const identity = JSON.parse(readFileSync(identityPath, "utf-8"));
if (!identity.did || !identity.kid || !identity.privateKey) {
issues.push("Identity file is incomplete or corrupted");
}
}
catch (error) {
issues.push("Identity file is corrupted or unreadable");
}
}
return {
valid: valid || hasValidEnvFile,
missing,
issues,
};
}
/**
* Test KTA connectivity and authentication (R16.5)
*/
async function checkKTAConnectivity(verbose) {
const issues = [];
let reachable = false;
let authenticated = false;
try {
// Test basic connectivity
reachable = await isKTAAvailable();
if (reachable) {
if (verbose) {
issues.push("KTA endpoint is reachable");
}
// Test authentication if we have an API key
const apiKey = process.env.KYA_VOUCHED_API_KEY || process.env.VOUCHED_API_KEY;
if (apiKey) {
try {
// Try to make an authenticated request
const envManager = new EnvManager();
const processVars = envManager.getFromProcess();
if (processVars.AGENT_DID) {
await ktaApi.getAgentStatus(processVars.AGENT_DID);
authenticated = true;
if (verbose) {
issues.push("KTA authentication successful");
}
}
else {
issues.push("Cannot test authentication without AGENT_DID");
}
}
catch (error) {
if (error.message.includes("404")) {
// Agent not found is OK for auth test
authenticated = true;
if (verbose) {
issues.push("KTA authentication successful (agent not registered)");
}
}
else if (error.message.includes("401") ||
error.message.includes("403")) {
issues.push("KTA authentication failed - check API key");
}
else {
issues.push(`KTA authentication test failed: ${error.message}`);
}
}
}
else {
issues.push("No KTA API key found - cannot test authentication");
}
}
else {
issues.push("KTA endpoint is not reachable");
issues.push("Check internet connection and firewall settings");
}
}
catch (error) {
issues.push(`KTA connectivity check failed: ${error.message}`);
}
return { reachable, authenticated, issues };
}
/**
* Check nonce cache configuration (R16.5)
*/
async function checkNonceCacheConfiguration(verbose) {
const issues = [];
let type = "memory";
let functional = true;
try {
// Detect cache type based on environment
if (process.env.REDIS_URL) {
type = "redis";
// Test Redis connectivity if available
try {
// This is a basic check - in a real implementation we'd test the actual connection
if (verbose) {
issues.push("Redis URL detected");
}
}
catch (error) {
functional = false;
issues.push("Redis connection failed");
}
}
else if (process.env.AWS_REGION) {
type = "dynamodb";
if (verbose) {
issues.push("AWS region detected - will use DynamoDB cache");
}
}
else if (typeof globalThis !== "undefined" && "caches" in globalThis) {
type = "cloudflare-kv";
if (verbose) {
issues.push("Cloudflare Workers environment detected");
}
}
else {
type = "memory";
// Memory cache warnings for production
if (process.env.NODE_ENV === "production") {
issues.push("Using memory cache in production - not suitable for multi-instance deployments");
issues.push("Consider configuring Redis, DynamoDB, or Cloudflare KV");
}
if (verbose) {
issues.push("Using in-memory cache (suitable for single-instance only)");
}
}
// Platform-specific cache recommendations
const platform = detectPlatform();
if (platform.platform === "vercel" && type === "memory") {
issues.push("Vercel deployment detected - consider using external cache for multi-region");
}
if (platform.platform === "lambda" && type === "memory") {
issues.push("Lambda deployment detected - consider DynamoDB for persistence");
}
}
catch (error) {
functional = false;
issues.push(`Cache configuration check failed: ${error.message}`);
}
return { type, functional, issues };
}
/**
* Display doctor results in human-readable format
*/
function displayDoctorResults(result, verbose) {
console.log(chalk.bold("š¦ Package Compatibility:"));
if (result.packages.length === 0) {
console.log(` ${chalk.yellow("ā ļø No XMCP-I packages found")}`);
}
else {
for (const pkg of result.packages) {
const status = pkg.compatible ? chalk.green("ā") : chalk.red("ā");
console.log(` ${status} ${pkg.name}@${pkg.version}`);
if (pkg.issues && (verbose || !pkg.compatible)) {
pkg.issues.forEach((issue) => {
const color = pkg.compatible ? chalk.gray : chalk.red;
console.log(` ${color(`⢠${issue}`)}`);
});
}
}
}
console.log(chalk.bold("\nš XMCP Upstream:"));
const xmcpStatus = result.xmcpUpstream.compatible
? chalk.green("ā")
: chalk.red("ā");
console.log(` ${xmcpStatus} XMCP ${result.xmcpUpstream.version}`);
if (result.xmcpUpstream.issues &&
(verbose || !result.xmcpUpstream.compatible)) {
result.xmcpUpstream.issues.forEach((issue) => {
const color = result.xmcpUpstream.compatible ? chalk.gray : chalk.red;
console.log(` ${color(`⢠${issue}`)}`);
});
}
console.log(chalk.bold("\nš Environment Configuration:"));
const envStatus = result.environment.valid
? chalk.green("ā")
: chalk.red("ā");
console.log(` ${envStatus} Environment variables`);
if (result.environment.missing.length > 0) {
console.log(` ${chalk.red(`⢠Missing: ${result.environment.missing.join(", ")}`)}`);
}
if (result.environment.issues && (verbose || !result.environment.valid)) {
result.environment.issues.forEach((issue) => {
const color = result.environment.valid ? chalk.gray : chalk.yellow;
console.log(` ${color(`⢠${issue}`)}`);
});
}
console.log(chalk.bold("\nš KTA Connectivity:"));
const ktaStatus = result.kta.reachable ? chalk.green("ā") : chalk.red("ā");
const authStatus = result.kta.authenticated
? chalk.green("authenticated")
: chalk.yellow("not authenticated");
console.log(` ${ktaStatus} KTA reachable (${authStatus})`);
if (result.kta.issues && (verbose || !result.kta.reachable)) {
result.kta.issues.forEach((issue) => {
const color = result.kta.reachable ? chalk.gray : chalk.red;
console.log(` ${color(`⢠${issue}`)}`);
});
}
console.log(chalk.bold("\nš¾ Nonce Cache:"));
const cacheStatus = result.cache.functional
? chalk.green("ā")
: chalk.red("ā");
console.log(` ${cacheStatus} ${result.cache.type} cache`);
if (result.cache.issues && (verbose || !result.cache.functional)) {
result.cache.issues.forEach((issue) => {
const color = result.cache.functional ? chalk.gray : chalk.yellow;
console.log(` ${color(`⢠${issue}`)}`);
});
}
// Overall summary
const hasIssues = !result.environment.valid ||
result.packages.some((p) => !p.compatible) ||
!result.xmcpUpstream.compatible ||
!result.kta.reachable;
console.log(chalk.bold("\nš Summary:"));
if (!hasIssues) {
showSuccess("All systems operational! š");
}
else {
showWarning("Issues detected that may affect functionality");
console.log(chalk.bold("\nš” Recommended Actions:"));
if (!result.environment.valid) {
console.log(` ⢠Run ${chalk.cyan("mcpi init")} to set up identity`);
}
if (result.packages.some((p) => !p.compatible)) {
console.log(` ⢠Update incompatible packages to latest versions`);
}
if (!result.xmcpUpstream.compatible) {
console.log(` ⢠Update XMCP to version ^0.3.1 or later`);
}
if (!result.kta.reachable) {
console.log(` ⢠Check internet connection and firewall settings`);
}
if (result.cache.type === "memory" &&
process.env.NODE_ENV === "production") {
console.log(` ⢠Configure external cache (Redis/DynamoDB/KV) for production`);
}
}
console.log(chalk.bold("\nš§ Additional Commands:"));
console.log(` ${chalk.cyan("mcpi status")} - Check agent registration status`);
console.log(` ${chalk.cyan("mcpi env verify")} - Verify environment configuration`);
console.log(` ${chalk.cyan("mcpi keys rotate")} - Rotate identity keys`);
console.log(` ${chalk.cyan("mcpi --help")} - Show all available commands`);
}
//# sourceMappingURL=doctor.js.map