#!/usr/bin/env node /** * code-intel.mjs — Semantic Code Intelligence CLI for AI Agents * * Single entry point for all code analysis commands. * Built on ts-morph (TypeScript Compiler API). * * Usage: * node tools/code-intel.mjs [args] [options] * * Commands: * refs Find all references to a symbol * type-info Show module structure (exports, imports, declarations) * structure File outline with line numbers * errors [path] TypeScript compilation errors * exports All exports with types * implementors Find all implementations of an interface * call-graph Call graph for a function * deps Import dependency tree * * Options: * --json Output as JSON (for programmatic use) * --quiet Only summary counts * --project client | server | all (default: all) * --help, -h Show help * * Exit codes: * 0 = success with results * 1 = error * 2 = success but no results found * * Examples: * node tools/code-intel.mjs refs createRow * node tools/code-intel.mjs type-info src/shared/utils/apiClient.ts * node tools/code-intel.mjs errors --project client --json * node tools/code-intel.mjs refs createRow | grep service * node tools/code-intel.mjs exports src/features/auth/ | wc -l */ import { loadProject, getSourceFiles, getSourceFile, getProjectRoot, getScopes } from './lib/project-loader.mjs'; import { resolve, relative } from 'path'; // ── Argument Parsing ────────────────────────────────────────────── function parseArgs(argv) { const args = argv.slice(2); const options = { command: null, args: [], json: false, quiet: false, project: 'all', help: false, }; let i = 0; while (i < args.length) { const arg = args[i]; if (arg === '--json') { options.json = true; } else if (arg === '--quiet' || arg === '-q') { options.quiet = true; } else if (arg === '--project' || arg === '-p') { i++; options.project = args[i] || 'all'; } else if (arg === '--help' || arg === '-h') { options.help = true; } else if (arg.startsWith('--')) { // Unknown option — ignore } else if (!options.command) { options.command = arg; } else { options.args.push(arg); } i++; } return options; } // ── Output Formatting ───────────────────────────────────────────── /** * Format and print results. * @param {Array>} rows - Array of result objects * @param {object} meta - { command, query, count, duration_ms } * @param {object} options - { json, quiet } */ function output(rows, meta, options) { if (options.json) { const result = { command: meta.command, query: meta.query || null, count: rows.length, duration_ms: meta.duration_ms || 0, results: rows, }; process.stdout.write(JSON.stringify(result, null, 2) + '\n'); return; } if (options.quiet) { process.stdout.write(`${rows.length}\n`); return; } // TSV output — pipe-friendly if (rows.length === 0) { process.stderr.write(`No results found.\n`); return; } // Print header from first row keys const keys = Object.keys(rows[0]); process.stdout.write(keys.join('\t') + '\n'); // Print rows for (const row of rows) { const values = keys.map(k => String(row[k] ?? '')); process.stdout.write(values.join('\t') + '\n'); } } /** * Print error and exit. */ function exitError(message, code = 1) { process.stderr.write(`Error: ${message}\n`); process.exit(code); } // ── Command Registry ────────────────────────────────────────────── const COMMANDS = { refs: { description: 'Find all references to a symbol', usage: 'refs [--project scope]', examples: [ 'refs createRow', 'refs TableService --project server', 'refs useAuth --project client | grep features', ], handler: cmdRefs, }, 'type-info': { description: 'Show module structure (exports, imports, declarations)', usage: 'type-info ', examples: [ 'type-info src/shared/utils/apiClient.ts', 'type-info backend/services/tableService.js', ], handler: cmdTypeInfo, }, structure: { description: 'File outline with line numbers', usage: 'structure ', examples: [ 'structure src/shared/utils/apiClient.ts', 'structure backend/routes/v3/tables.js', ], handler: cmdStructure, }, errors: { description: 'TypeScript/JavaScript compilation errors', usage: 'errors [path] [--project scope]', examples: [ 'errors', 'errors src/features/auth/', 'errors --project client --json', ], handler: cmdErrors, }, exports: { description: 'All exports from a module with types', usage: 'exports ', examples: [ 'exports src/shared/utils/apiClient.ts', 'exports backend/services/ | sort', ], handler: cmdExports, }, implementors: { description: 'Find all implementations of an interface/type', usage: 'implementors ', examples: [ 'implementors BaseWidget', 'implementors RouteHandler --project server', ], handler: cmdImplementors, }, 'call-graph': { description: 'Show call graph for a function', usage: 'call-graph ', examples: [ 'call-graph createRow', 'call-graph handleAuth --json', ], handler: cmdCallGraph, }, deps: { description: 'Import dependency tree for a file', usage: 'deps [--depth N]', examples: [ 'deps src/shared/utils/apiClient.ts', 'deps backend/server.js --depth 2', ], handler: cmdDeps, }, }; // ── Command Implementations ─────────────────────────────────────── function cmdRefs(project, args, options) { const symbol = args[0]; if (!symbol) exitError('Usage: refs \nExample: refs createRow'); const start = Date.now(); const root = getProjectRoot(); const results = []; for (const sourceFile of getSourceFiles(project)) { const filePath = relative(root, sourceFile.getFilePath()); // Search all identifiers in the file sourceFile.forEachDescendant((node) => { if (node.getKindName() === 'Identifier' && node.getText() === symbol) { const line = node.getStartLineNumber(); const col = node.getStartLinePos ? node.getStart() - node.getStartLinePos() : 0; const parent = node.getParent(); const context = parent ? parent.getKindName() : 'unknown'; results.push({ file: filePath, line, col, context, text: parent ? parent.getText().substring(0, 120).replace(/\n/g, ' ') : '', }); } }); } const duration = Date.now() - start; output(results, { command: 'refs', query: symbol, duration_ms: duration }, options); return results.length === 0 ? 2 : 0; } function cmdTypeInfo(project, args, options) { const filePath = args[0]; if (!filePath) exitError('Usage: type-info \nExample: type-info src/shared/utils/apiClient.ts'); const start = Date.now(); const sourceFile = getSourceFile(project, filePath); if (!sourceFile) exitError(`File not found: ${filePath}`); const results = []; const root = getProjectRoot(); const relPath = relative(root, sourceFile.getFilePath()); // Imports for (const imp of sourceFile.getImportDeclarations()) { const moduleSpec = imp.getModuleSpecifierValue(); const namedImports = imp.getNamedImports().map(n => n.getName()); const defaultImport = imp.getDefaultImport()?.getText(); results.push({ kind: 'import', name: defaultImport || namedImports.join(', '), type: 'module', from: moduleSpec, line: imp.getStartLineNumber(), }); } // Exports for (const [name, declarations] of sourceFile.getExportedDeclarations()) { for (const decl of declarations) { results.push({ kind: 'export', name, type: decl.getKindName(), from: relPath, line: decl.getStartLineNumber(), }); } } // Functions (non-exported) for (const fn of sourceFile.getFunctions()) { if (!fn.isExported()) { results.push({ kind: 'function', name: fn.getName() || '', type: `(${fn.getParameters().map(p => p.getName()).join(', ')})`, from: relPath, line: fn.getStartLineNumber(), }); } } // Interfaces for (const iface of sourceFile.getInterfaces()) { if (!iface.isExported()) { results.push({ kind: 'interface', name: iface.getName(), type: `{${iface.getProperties().length} props}`, from: relPath, line: iface.getStartLineNumber(), }); } } // Type aliases for (const ta of sourceFile.getTypeAliases()) { if (!ta.isExported()) { results.push({ kind: 'type', name: ta.getName(), type: ta.getTypeNode()?.getText().substring(0, 80) || '?', from: relPath, line: ta.getStartLineNumber(), }); } } const duration = Date.now() - start; output(results, { command: 'type-info', query: filePath, duration_ms: duration }, options); return results.length === 0 ? 2 : 0; } function cmdStructure(project, args, options) { const filePath = args[0]; if (!filePath) exitError('Usage: structure \nExample: structure src/shared/utils/apiClient.ts'); const start = Date.now(); const sourceFile = getSourceFile(project, filePath); if (!sourceFile) exitError(`File not found: ${filePath}`); const results = []; // Classes for (const cls of sourceFile.getClasses()) { results.push({ kind: 'class', name: cls.getName() || '', line: cls.getStartLineNumber(), end_line: cls.getEndLineNumber(), exported: cls.isExported(), }); for (const method of cls.getMethods()) { results.push({ kind: ' method', name: `${cls.getName()}.${method.getName()}`, line: method.getStartLineNumber(), end_line: method.getEndLineNumber(), exported: false, }); } for (const prop of cls.getProperties()) { results.push({ kind: ' property', name: `${cls.getName()}.${prop.getName()}`, line: prop.getStartLineNumber(), end_line: prop.getStartLineNumber(), exported: false, }); } } // Functions for (const fn of sourceFile.getFunctions()) { const params = fn.getParameters().map(p => p.getName()).join(', '); results.push({ kind: 'function', name: `${fn.getName() || ''}(${params})`, line: fn.getStartLineNumber(), end_line: fn.getEndLineNumber(), exported: fn.isExported(), }); } // Variable declarations (arrow functions, consts) for (const varStmt of sourceFile.getVariableStatements()) { for (const decl of varStmt.getDeclarations()) { const init = decl.getInitializer(); const isArrowFn = init && init.getKindName() === 'ArrowFunction'; results.push({ kind: isArrowFn ? 'arrow-fn' : 'const', name: decl.getName(), line: decl.getStartLineNumber(), end_line: init ? init.getEndLineNumber() : decl.getStartLineNumber(), exported: varStmt.isExported(), }); } } // Interfaces for (const iface of sourceFile.getInterfaces()) { results.push({ kind: 'interface', name: iface.getName(), line: iface.getStartLineNumber(), end_line: iface.getEndLineNumber(), exported: iface.isExported(), }); } // Type aliases for (const ta of sourceFile.getTypeAliases()) { results.push({ kind: 'type', name: ta.getName(), line: ta.getStartLineNumber(), end_line: ta.getEndLineNumber(), exported: ta.isExported(), }); } // Enums for (const en of sourceFile.getEnums()) { results.push({ kind: 'enum', name: en.getName(), line: en.getStartLineNumber(), end_line: en.getEndLineNumber(), exported: en.isExported(), }); } // Sort by line number results.sort((a, b) => a.line - b.line); const duration = Date.now() - start; output(results, { command: 'structure', query: filePath, duration_ms: duration }, options); return results.length === 0 ? 2 : 0; } function cmdErrors(project, args, options) { const filterPath = args[0] || null; const start = Date.now(); const root = getProjectRoot(); const results = []; const files = filterPath ? getSourceFiles(project).filter(f => relative(root, f.getFilePath()).startsWith(filterPath)) : getSourceFiles(project); for (const sourceFile of files) { const diagnostics = sourceFile.getPreEmitDiagnostics(); for (const diag of diagnostics) { const file = diag.getSourceFile(); const filePath = file ? relative(root, file.getFilePath()) : '?'; const line = diag.getLineNumber() || 0; results.push({ file: filePath, line, severity: getSeverityText(diag.getCategory()), code: `TS${diag.getCode()}`, message: diag.getMessageText().toString().substring(0, 200).replace(/\n/g, ' '), }); } } const duration = Date.now() - start; output(results, { command: 'errors', query: filterPath || 'all', duration_ms: duration }, options); return results.length === 0 ? 0 : 1; // errors = exit 1 } function getSeverityText(category) { // ts.DiagnosticCategory: 0=Warning, 1=Error, 2=Suggestion, 3=Message switch (category) { case 0: return 'warning'; case 1: return 'error'; case 2: return 'suggestion'; case 3: return 'message'; default: return 'unknown'; } } function cmdExports(project, args, options) { const filePath = args[0]; if (!filePath) exitError('Usage: exports \nExample: exports src/shared/utils/apiClient.ts'); const start = Date.now(); const root = getProjectRoot(); const results = []; // If path is a directory, scan all files in it const files = filePath.endsWith('/') ? getSourceFiles(project).filter(f => relative(root, f.getFilePath()).startsWith(filePath)) : [getSourceFile(project, filePath)].filter(Boolean); if (files.length === 0) exitError(`No files found: ${filePath}`); for (const sourceFile of files) { const relPath = relative(root, sourceFile.getFilePath()); for (const [name, declarations] of sourceFile.getExportedDeclarations()) { for (const decl of declarations) { let typeText = ''; try { typeText = decl.getType().getText(decl).substring(0, 100); } catch { typeText = decl.getKindName(); } results.push({ file: relPath, name, kind: decl.getKindName(), type: typeText, line: decl.getStartLineNumber(), }); } } } const duration = Date.now() - start; output(results, { command: 'exports', query: filePath, duration_ms: duration }, options); return results.length === 0 ? 2 : 0; } function cmdImplementors(project, args, options) { const interfaceName = args[0]; if (!interfaceName) exitError('Usage: implementors \nExample: implementors BaseWidget'); const start = Date.now(); const root = getProjectRoot(); const results = []; // First, find the interface declaration let targetInterface = null; for (const sourceFile of getSourceFiles(project)) { const iface = sourceFile.getInterface(interfaceName); if (iface) { targetInterface = iface; break; } // Also check type aliases const typeAlias = sourceFile.getTypeAlias(interfaceName); if (typeAlias) { targetInterface = typeAlias; break; } } if (!targetInterface) { exitError(`Interface/type "${interfaceName}" not found`); } // Find all classes/types that implement/extend it for (const sourceFile of getSourceFiles(project)) { const relPath = relative(root, sourceFile.getFilePath()); // Check classes for (const cls of sourceFile.getClasses()) { const impls = cls.getImplements(); for (const impl of impls) { if (impl.getText().includes(interfaceName)) { results.push({ file: relPath, name: cls.getName() || '', kind: 'class implements', line: cls.getStartLineNumber(), }); } } // Check extends const baseClass = cls.getExtends(); if (baseClass && baseClass.getText().includes(interfaceName)) { results.push({ file: relPath, name: cls.getName() || '', kind: 'class extends', line: cls.getStartLineNumber(), }); } } // Check interfaces that extend target for (const iface of sourceFile.getInterfaces()) { if (iface.getName() === interfaceName) continue; const exts = iface.getExtends(); for (const ext of exts) { if (ext.getText().includes(interfaceName)) { results.push({ file: relPath, name: iface.getName(), kind: 'interface extends', line: iface.getStartLineNumber(), }); } } } } const duration = Date.now() - start; output(results, { command: 'implementors', query: interfaceName, duration_ms: duration }, options); return results.length === 0 ? 2 : 0; } function cmdCallGraph(project, args, options) { const functionName = args[0]; if (!functionName) exitError('Usage: call-graph \nExample: call-graph createRow'); const start = Date.now(); const root = getProjectRoot(); const results = []; for (const sourceFile of getSourceFiles(project)) { const relPath = relative(root, sourceFile.getFilePath()); sourceFile.forEachDescendant((node) => { // Find function/method declarations matching the name const kindName = node.getKindName(); if ( (kindName === 'FunctionDeclaration' || kindName === 'MethodDeclaration') && node.getName && node.getName() === functionName ) { // Find all call expressions inside this function node.forEachDescendant((child) => { if (child.getKindName() === 'CallExpression') { const expr = child.getExpression(); const calledName = expr.getText().substring(0, 80); results.push({ from: `${relPath}:${functionName}`, calls: calledName, line: child.getStartLineNumber(), type: 'outgoing', }); } }); } }); } // Also find who calls this function (incoming) for (const sourceFile of getSourceFiles(project)) { const relPath = relative(root, sourceFile.getFilePath()); sourceFile.forEachDescendant((node) => { if (node.getKindName() === 'CallExpression') { const expr = node.getExpression(); const text = expr.getText(); // Match direct calls or method calls ending with the function name if (text === functionName || text.endsWith(`.${functionName}`)) { // Find enclosing function let parent = node.getParent(); let enclosing = ''; while (parent) { const pk = parent.getKindName(); if (pk === 'FunctionDeclaration' || pk === 'MethodDeclaration' || pk === 'ArrowFunction') { enclosing = parent.getName ? (parent.getName() || '') : ''; break; } parent = parent.getParent(); } results.push({ from: `${relPath}:${enclosing}`, calls: functionName, line: node.getStartLineNumber(), type: 'incoming', }); } } }); } const duration = Date.now() - start; output(results, { command: 'call-graph', query: functionName, duration_ms: duration }, options); return results.length === 0 ? 2 : 0; } function cmdDeps(project, args, options) { const filePath = args[0]; if (!filePath) exitError('Usage: deps \nExample: deps src/shared/utils/apiClient.ts'); const start = Date.now(); const sourceFile = getSourceFile(project, filePath); if (!sourceFile) exitError(`File not found: ${filePath}`); const root = getProjectRoot(); const results = []; const visited = new Set(); function walk(sf, depth) { const relPath = relative(root, sf.getFilePath()); if (visited.has(relPath)) return; visited.add(relPath); for (const imp of sf.getImportDeclarations()) { const moduleSpec = imp.getModuleSpecifierValue(); const resolvedFile = imp.getModuleSpecifierSourceFile(); const resolvedPath = resolvedFile ? relative(root, resolvedFile.getFilePath()) : moduleSpec; const isExternal = !resolvedFile || resolvedPath.startsWith('node_modules'); results.push({ file: relPath, imports: moduleSpec, resolved: isExternal ? `[external] ${moduleSpec}` : resolvedPath, depth, external: isExternal, }); // Recurse into local deps (max depth 5) if (!isExternal && resolvedFile && depth < 5) { walk(resolvedFile, depth + 1); } } } walk(sourceFile, 0); const duration = Date.now() - start; output(results, { command: 'deps', query: filePath, duration_ms: duration }, options); return results.length === 0 ? 2 : 0; } // ── Help ────────────────────────────────────────────────────────── function showHelp(command) { if (command && COMMANDS[command]) { const cmd = COMMANDS[command]; process.stdout.write(`\n ${command} — ${cmd.description}\n\n`); process.stdout.write(` Usage: node tools/code-intel.mjs ${cmd.usage}\n\n`); process.stdout.write(` Examples:\n`); for (const ex of cmd.examples) { process.stdout.write(` node tools/code-intel.mjs ${ex}\n`); } process.stdout.write('\n'); process.stdout.write(` Options:\n`); process.stdout.write(` --json Output as JSON\n`); process.stdout.write(` --quiet, -q Only print count\n`); process.stdout.write(` --project, -p Scope: client | server | all (default: all)\n`); process.stdout.write('\n'); } else { process.stdout.write(` code-intel — Semantic Code Intelligence for AI Agents Built on ts-morph (TypeScript Compiler API) Usage: node tools/code-intel.mjs [args] [options] Commands: `); for (const [name, cmd] of Object.entries(COMMANDS)) { process.stdout.write(` ${name.padEnd(16)} ${cmd.description}\n`); } process.stdout.write(` Options: --json Output as JSON (for programmatic use) --quiet, -q Only print count --project, -p Scope: client | server | all (default: all) --help, -h Show help Exit codes: 0 = success with results 1 = error 2 = no results found Examples: node tools/code-intel.mjs refs createRow node tools/code-intel.mjs refs createRow --json | jq '.results[].file' node tools/code-intel.mjs errors --project client node tools/code-intel.mjs structure src/app/App.tsx node tools/code-intel.mjs deps backend/server.js | grep -v external `); } } // ── Main ────────────────────────────────────────────────────────── async function main() { const options = parseArgs(process.argv); // Help if (options.help || !options.command) { showHelp(options.command); process.exit(options.command ? 0 : 1); } // Validate command const cmd = COMMANDS[options.command]; if (!cmd) { process.stderr.write(`Unknown command: ${options.command}\n`); process.stderr.write(`Run: node tools/code-intel.mjs --help\n`); process.exit(1); } // Load project const start = Date.now(); let project; try { project = loadProject(options.project); } catch (err) { exitError(`Failed to load project (scope: ${options.project}): ${err.message}`); } const loadTime = Date.now() - start; if (!options.quiet && !options.json) { process.stderr.write(`Project loaded (${options.project}) in ${loadTime}ms — ${getSourceFiles(project).length} files\n`); } // Execute command const exitCode = cmd.handler(project, options.args, options); process.exit(exitCode || 0); } main().catch((err) => { process.stderr.write(`Fatal: ${err.message}\n`); process.exit(1); });