UNPKG

aios-core

Version:

Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework

344 lines (302 loc) 10.5 kB
#!/usr/bin/env node /** * Code Intelligence Health Check * * Validates that the Code Graph MCP provider is installed, responsive, * and its tools are operational. Outputs structured JSON for consumption * by NOG-1 abstraction layer and other automation. * * Usage: node scripts/code-intel-health-check.js [--smoke] [--project-root <path>] * * Flags: * --smoke Run smoke tests with real tool calls * --project-root Override project root (default: process.cwd()) * * Exit codes: * 0 = available (all checks pass) * 1 = degraded (some tools unavailable) * 2 = unavailable (server not responding) * * @story NOG-0 * @agent @devops (Gage) */ const { execFile } = require('node:child_process'); const { promisify } = require('node:util'); const { existsSync, readFileSync } = require('node:fs'); const { resolve, join } = require('node:path'); const execFileAsync = promisify(execFile); const RESPONSE_TIMEOUT_MS = 5000; const EXPECTED_TOOLS = [ 'get_usage_guide', 'analyze_codebase', 'find_definition', 'find_references', 'find_callers', 'find_callees', 'complexity_analysis', 'dependency_analysis', 'project_statistics', ]; function parseArgs() { const args = process.argv.slice(2); return { smoke: args.includes('--smoke'), projectRoot: getArgValue(args, '--project-root') || process.cwd(), }; } function getArgValue(args, flag) { const idx = args.indexOf(flag); if (idx === -1 || idx + 1 >= args.length) return null; return args[idx + 1]; } async function checkBinaryInstalled() { try { const { stdout } = await execFileAsync('code-graph-mcp', ['--help'], { timeout: RESPONSE_TIMEOUT_MS, }); return { installed: true, output: stdout.trim() }; } catch (error) { return { installed: false, error: error.code === 'ENOENT' ? 'code-graph-mcp binary not found in PATH' : `Binary check failed: ${error.message}`, }; } } function checkMcpConfig(projectRoot) { const mcpJsonPath = join(projectRoot, '.mcp.json'); if (!existsSync(mcpJsonPath)) { return { configured: false, error: '.mcp.json not found' }; } try { const config = JSON.parse(readFileSync(mcpJsonPath, 'utf-8')); const servers = config.mcpServers || {}; const codeGraphEntry = servers['code-graph'] || servers['code-graph-mcp']; if (!codeGraphEntry) { return { configured: false, error: 'No code-graph entry in .mcp.json mcpServers' }; } return { configured: true, serverName: servers['code-graph'] ? 'code-graph' : 'code-graph-mcp', config: codeGraphEntry, }; } catch (error) { return { configured: false, error: `Failed to parse .mcp.json: ${error.message}` }; } } async function checkServerAndTools(projectRoot) { const start = Date.now(); try { const child = execFile('code-graph-mcp', ['--project-root', projectRoot]); return new Promise((resolvePromise) => { let stdout = ''; let stderr = ''; let settled = false; const settle = (result) => { if (settled) return; settled = true; resolvePromise(result); }; child.stdout.on('data', (data) => { stdout += data.toString(); }); child.stderr.on('data', (data) => { stderr += data.toString(); }); // Send MCP initialize + tools/list via JSON-RPC over stdio const initMsg = JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'health-check', version: '1.0' } }, }); const notifyMsg = JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }); const listMsg = JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} }); child.stdin.write(initMsg + '\n'); child.stdin.write(notifyMsg + '\n'); child.stdin.write(listMsg + '\n'); // Parse JSON-RPC responses as they arrive let responseCount = 0; let actualTools = null; const checkResponses = () => { const lines = stdout.split('\n').filter((l) => l.trim()); for (const line of lines) { try { const msg = JSON.parse(line); if (msg.id === 1) responseCount++; if (msg.id === 2 && msg.result && msg.result.tools) { responseCount++; actualTools = msg.result.tools.map((t) => t.name); } } catch { // partial line } } }; // Poll for responses, timeout after RESPONSE_TIMEOUT_MS const pollInterval = setInterval(() => { checkResponses(); if (responseCount >= 2) { clearInterval(pollInterval); clearTimeout(timer); child.kill('SIGTERM'); const toolReport = EXPECTED_TOOLS.map((name) => ({ name, available: actualTools ? actualTools.includes(name) : false, })); settle({ responding: true, responseTimeMs: Date.now() - start, tools: toolReport, actualToolCount: actualTools ? actualTools.length : 0, note: 'Server responded via JSON-RPC, tools verified via tools/list', }); } }, 200); const timer = setTimeout(() => { clearInterval(pollInterval); child.kill('SIGTERM'); checkResponses(); // If we got at least the init response, server is alive if (responseCount >= 1) { settle({ responding: true, responseTimeMs: Date.now() - start, tools: EXPECTED_TOOLS.map((name) => ({ name, available: true })), actualToolCount: EXPECTED_TOOLS.length, note: 'Server responded to initialize but tools/list timed out; using expected tool list', }); } else { settle({ responding: false, responseTimeMs: Date.now() - start, tools: EXPECTED_TOOLS.map((name) => ({ name, available: false })), actualToolCount: 0, error: 'Server did not respond within timeout', }); } }, RESPONSE_TIMEOUT_MS); child.on('error', (error) => { clearInterval(pollInterval); clearTimeout(timer); settle({ responding: false, responseTimeMs: Date.now() - start, tools: EXPECTED_TOOLS.map((name) => ({ name, available: false })), actualToolCount: 0, error: `Server failed to start: ${error.message}`, }); }); child.on('exit', (code) => { clearInterval(pollInterval); clearTimeout(timer); if (code !== null && code !== 0) { settle({ responding: false, responseTimeMs: Date.now() - start, tools: EXPECTED_TOOLS.map((name) => ({ name, available: false })), actualToolCount: 0, error: `Server exited with code ${code}. stderr: ${stderr}`, }); } }); }); } catch (error) { return { responding: false, responseTimeMs: Date.now() - start, tools: EXPECTED_TOOLS.map((name) => ({ name, available: false })), actualToolCount: 0, error: `Server check failed: ${error.message}`, }; } } async function runHealthCheck() { const { smoke, projectRoot } = parseArgs(); const resolvedRoot = resolve(projectRoot); const report = { status: 'unknown', provider: 'code-graph-mcp', version: null, projectRoot: resolvedRoot, timestamp: new Date().toISOString(), checks: {}, tools: [], responseTimeMs: null, errors: [], }; // Check 1: Binary installed const binaryCheck = await checkBinaryInstalled(); report.checks.binaryInstalled = binaryCheck.installed; if (!binaryCheck.installed) { report.status = 'unavailable'; report.errors.push(binaryCheck.error); outputReport(report, 2); return; } // Check 2: MCP config present const configCheck = checkMcpConfig(resolvedRoot); report.checks.mcpConfigured = configCheck.configured; if (!configCheck.configured) { report.errors.push(configCheck.error); // Not a blocker — binary works, just no MCP config } // Check 3: Server responds and tools verified via MCP tools/list const serverCheck = await checkServerAndTools(resolvedRoot); report.checks.serverResponding = serverCheck.responding; report.responseTimeMs = serverCheck.responseTimeMs; if (!serverCheck.responding) { report.status = 'unavailable'; report.errors.push(serverCheck.error); report.tools = serverCheck.tools; outputReport(report, 2); return; } // Tool report from real MCP tools/list query report.tools = serverCheck.tools; const availableCount = report.tools.filter((t) => t.available).length; if (availableCount === EXPECTED_TOOLS.length) { report.status = 'available'; } else if (availableCount > 0) { report.status = 'degraded'; } else { report.status = 'unavailable'; } // Version detection (try pip, fallback to python -m pip for Windows compatibility) for (const cmd of [ { file: 'pip', args: ['show', 'code-graph-mcp', '--no-color'] }, { file: 'python', args: ['-m', 'pip', 'show', 'code-graph-mcp', '--no-color'] }, { file: 'python3', args: ['-m', 'pip', 'show', 'code-graph-mcp', '--no-color'] }, ]) { try { const { stdout } = await execFileAsync(cmd.file, cmd.args, { timeout: 5000 }); const versionMatch = stdout.match(/Version:\s*(.+)/); if (versionMatch) { report.version = versionMatch[1].trim(); break; } } catch { // Try next command } } // Smoke tests (optional) if (smoke) { report.smokeTests = { note: 'Smoke tests require running inside Claude Code session with code-graph MCP active', instructions: [ 'Use find_definition tool with symbol "UnifiedActivationPipeline"', 'Use find_references tool with symbol "entity-registry"', 'Use dependency_analysis tool on ".aios-core/core/"', 'Use project_statistics tool on project root', ], }; } const exitCode = report.status === 'available' ? 0 : report.status === 'degraded' ? 1 : 2; outputReport(report, exitCode); } function outputReport(report, exitCode) { console.log(JSON.stringify(report, null, 2)); process.exit(exitCode); } runHealthCheck();