aios-core
Version:
Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework
344 lines (302 loc) • 10.5 kB
JavaScript
/**
* 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();