Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,10 @@ At the start of a session, ask the user if they'd like to initialize CodeGraph:

## CLI Reference

> **Why CLI parity with MCP matters**
>
> MCP is a great transport for agent runtimes, but it couples capabilities too tightly to the agent process. Developers working in scripts, CI pipelines, git hooks, or editor-agnostic workflows shouldn't need a running MCP server to access the same graph intelligence. The commands below are designed to match MCP feature parity so the CLI is a first-class citizen alongside the MCP server.

```bash
codegraph # Run interactive installer
codegraph install # Run installer (explicit)
Expand All @@ -334,6 +338,9 @@ codegraph status [path] # Show statistics
codegraph query <search> # Search symbols (--kind, --limit, --json)
codegraph files [path] # Show file structure (--format, --filter, --max-depth, --json)
codegraph context <task> # Build context for AI (--format, --max-nodes)
codegraph callers <symbol> # Find what calls a function/method (--limit, --json)
codegraph callees <symbol> # Find what a function/method calls (--limit, --json)
codegraph impact <symbol> # Analyze what code is affected by changing a symbol (--depth, --json)
codegraph affected [files...] # Find test files affected by changes (see below)
codegraph serve --mcp # Start MCP server
```
Expand Down
263 changes: 263 additions & 0 deletions src/bin/codegraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
* codegraph query <search> Search for symbols
* codegraph files [options] Show project file structure
* codegraph context <task> Build context for a task
* codegraph callers <symbol> Find what calls a function/method
* codegraph callees <symbol> Find what a function/method calls
* codegraph impact <symbol> Analyze what code is affected by changing a symbol
* codegraph affected [files] Find test files affected by changes
*/

Expand Down Expand Up @@ -1157,6 +1160,266 @@ program
}
});

/**
* codegraph callers <symbol>
*
* Motivation: MCP is powerful but it couples the intelligence layer too tightly
* to the agent runtime — every new capability requires the agent to be running.
* We believe the CLI should be a first-class citizen that matches MCP feature
* parity, so developers can use codegraph in scripts, CI pipelines, git hooks,
* and any other context where spinning up an MCP server is impractical.
*/
program
.command('callers <symbol>')
.description('Find all functions/methods that call a specific symbol')
.option('-p, --path <path>', 'Project path')
.option('-l, --limit <number>', 'Maximum results', '20')
.option('-j, --json', 'Output as JSON')
.action(async (symbol: string, options: { path?: string; limit?: string; json?: boolean }) => {
const projectPath = resolveProjectPath(options.path);

try {
if (!isInitialized(projectPath)) {
error(`CodeGraph not initialized in ${projectPath}`);
process.exit(1);
}

const { default: CodeGraph } = await loadCodeGraph();
const cg = await CodeGraph.open(projectPath);
const limit = parseInt(options.limit || '20', 10);

const matches = cg.searchNodes(symbol, { limit: 50 });
if (matches.length === 0) {
info(`Symbol "${symbol}" not found`);
cg.destroy();
return;
}

const seen = new Set<string>();
const allCallers: Array<{ name: string; kind: string; filePath: string; startLine?: number }> = [];

for (const match of matches) {
const exactMatch = match.node.name === symbol || match.node.name.endsWith(`.${symbol}`) || match.node.name.endsWith(`::${symbol}`);
if (!exactMatch && matches.length > 1) continue;
for (const c of cg.getCallers(match.node.id)) {
if (!seen.has(c.node.id)) {
seen.add(c.node.id);
allCallers.push({ name: c.node.name, kind: c.node.kind, filePath: c.node.filePath, startLine: c.node.startLine });
}
}
}

// Fallback: if exact filter removed everything, use the top match
if (allCallers.length === 0 && matches[0]) {
for (const c of cg.getCallers(matches[0].node.id)) {
if (!seen.has(c.node.id)) {
seen.add(c.node.id);
allCallers.push({ name: c.node.name, kind: c.node.kind, filePath: c.node.filePath, startLine: c.node.startLine });
}
}
}

const limited = allCallers.slice(0, limit);

if (options.json) {
console.log(JSON.stringify({ symbol, callers: limited }, null, 2));
} else if (limited.length === 0) {
info(`No callers found for "${symbol}"`);
} else {
console.log(chalk.bold(`\nCallers of "${symbol}" (${limited.length}):\n`));
for (const node of limited) {
const loc = node.startLine ? `:${node.startLine}` : '';
console.log(
chalk.cyan(node.kind.padEnd(12)) +
chalk.white(node.name)
);
console.log(chalk.dim(` ${node.filePath}${loc}`));
console.log();
}
}

cg.destroy();
} catch (err) {
error(`callers failed: ${err instanceof Error ? err.message : String(err)}`);
process.exit(1);
}
});

/**
* codegraph callees <symbol>
*/
program
.command('callees <symbol>')
.description('Find all functions/methods that a specific symbol calls')
.option('-p, --path <path>', 'Project path')
.option('-l, --limit <number>', 'Maximum results', '20')
.option('-j, --json', 'Output as JSON')
.action(async (symbol: string, options: { path?: string; limit?: string; json?: boolean }) => {
const projectPath = resolveProjectPath(options.path);

try {
if (!isInitialized(projectPath)) {
error(`CodeGraph not initialized in ${projectPath}`);
process.exit(1);
}

const { default: CodeGraph } = await loadCodeGraph();
const cg = await CodeGraph.open(projectPath);
const limit = parseInt(options.limit || '20', 10);

const matches = cg.searchNodes(symbol, { limit: 50 });
if (matches.length === 0) {
info(`Symbol "${symbol}" not found`);
cg.destroy();
return;
}

const seen = new Set<string>();
const allCallees: Array<{ name: string; kind: string; filePath: string; startLine?: number }> = [];

for (const match of matches) {
const exactMatch = match.node.name === symbol || match.node.name.endsWith(`.${symbol}`) || match.node.name.endsWith(`::${symbol}`);
if (!exactMatch && matches.length > 1) continue;
for (const c of cg.getCallees(match.node.id)) {
if (!seen.has(c.node.id)) {
seen.add(c.node.id);
allCallees.push({ name: c.node.name, kind: c.node.kind, filePath: c.node.filePath, startLine: c.node.startLine });
}
}
}

if (allCallees.length === 0 && matches[0]) {
for (const c of cg.getCallees(matches[0].node.id)) {
if (!seen.has(c.node.id)) {
seen.add(c.node.id);
allCallees.push({ name: c.node.name, kind: c.node.kind, filePath: c.node.filePath, startLine: c.node.startLine });
}
}
}

const limited = allCallees.slice(0, limit);

if (options.json) {
console.log(JSON.stringify({ symbol, callees: limited }, null, 2));
} else if (limited.length === 0) {
info(`No callees found for "${symbol}"`);
} else {
console.log(chalk.bold(`\nCallees of "${symbol}" (${limited.length}):\n`));
for (const node of limited) {
const loc = node.startLine ? `:${node.startLine}` : '';
console.log(
chalk.cyan(node.kind.padEnd(12)) +
chalk.white(node.name)
);
console.log(chalk.dim(` ${node.filePath}${loc}`));
console.log();
}
}

cg.destroy();
} catch (err) {
error(`callees failed: ${err instanceof Error ? err.message : String(err)}`);
process.exit(1);
}
});

/**
* codegraph impact <symbol>
*/
program
.command('impact <symbol>')
.description('Analyze what code is affected by changing a symbol')
.option('-p, --path <path>', 'Project path')
.option('-d, --depth <number>', 'Traversal depth', '2')
.option('-j, --json', 'Output as JSON')
.action(async (symbol: string, options: { path?: string; depth?: string; json?: boolean }) => {
const projectPath = resolveProjectPath(options.path);

try {
if (!isInitialized(projectPath)) {
error(`CodeGraph not initialized in ${projectPath}`);
process.exit(1);
}

const { default: CodeGraph } = await loadCodeGraph();
const cg = await CodeGraph.open(projectPath);
const depth = Math.min(Math.max(parseInt(options.depth || '2', 10), 1), 10);

const matches = cg.searchNodes(symbol, { limit: 50 });
if (matches.length === 0) {
info(`Symbol "${symbol}" not found`);
cg.destroy();
return;
}

// Merge impact subgraphs across all exact-matching symbols
const mergedNodes = new Map<string, { name: string; kind: string; filePath: string; startLine?: number }>();
const seenEdges = new Set<string>();
let edgeCount = 0;

for (const match of matches) {
const exactMatch = match.node.name === symbol || match.node.name.endsWith(`.${symbol}`) || match.node.name.endsWith(`::${symbol}`);
if (!exactMatch && matches.length > 1) continue;
const impact = cg.getImpactRadius(match.node.id, depth);
for (const [id, n] of impact.nodes) {
mergedNodes.set(id, { name: n.name, kind: n.kind, filePath: n.filePath, startLine: n.startLine });
}
for (const e of impact.edges) {
const key = `${e.source}->${e.target}:${e.kind}`;
if (!seenEdges.has(key)) {
seenEdges.add(key);
edgeCount++;
}
}
}

// Fallback to top match if exact filter removed everything
if (mergedNodes.size === 0 && matches[0]) {
const impact = cg.getImpactRadius(matches[0].node.id, depth);
for (const [id, n] of impact.nodes) {
mergedNodes.set(id, { name: n.name, kind: n.kind, filePath: n.filePath, startLine: n.startLine });
}
edgeCount = impact.edges.length;
}

if (options.json) {
console.log(JSON.stringify({
symbol,
depth,
nodeCount: mergedNodes.size,
edgeCount,
affected: Array.from(mergedNodes.values()),
}, null, 2));
} else if (mergedNodes.size === 0) {
info(`No affected symbols found for "${symbol}"`);
} else {
console.log(chalk.bold(`\nImpact of changing "${symbol}" — ${mergedNodes.size} affected symbols:\n`));

// Group by file
const byFile = new Map<string, Array<{ name: string; kind: string; startLine?: number }>>();
for (const node of mergedNodes.values()) {
const list = byFile.get(node.filePath) || [];
list.push({ name: node.name, kind: node.kind, startLine: node.startLine });
byFile.set(node.filePath, list);
}

for (const [file, nodes] of byFile) {
console.log(chalk.cyan(file));
for (const node of nodes) {
const loc = node.startLine ? `:${node.startLine}` : '';
console.log(` ${chalk.dim(node.kind.padEnd(12))}${node.name}${chalk.dim(loc)}`);
}
console.log();
}
}

cg.destroy();
} catch (err) {
error(`impact failed: ${err instanceof Error ? err.message : String(err)}`);
process.exit(1);
}
});

/**
* codegraph affected [files...]
*
Expand Down