Fixes #1611. ## What `codegraph context <task>` has been advertised in the CLI usage header since the first commit, and the ContextBuilder behind the public `buildContext` API has always shipped in the package — but the command was never registered with commander (verified via `git log -S`: this is drift present from day one, not a removal). Invoking it errored with `unknown command 'context'`, which broke external integrations built against the documented contract — Memorix 1.8.1 invokes `codegraph context --path <project-root> --format json --max-nodes 8 --no-code <task>` and silently falls back to its own heuristic index when the command is missing. ## How Registers `context <task...>` next to the other read commands (`query`/`explore` pattern), mapping flags 1:1 onto `BuildContextOptions`: - `-p, --path <path>` — resolved exactly like every sibling command (nearest initialized project) - `-f, --format <format>` — `markdown` (default) or `json`, unknown values rejected with exit 1 - `-n, --max-nodes <number>` — positive integer, validated - `--no-code` — structure only (`includeCode: false`) JSON output is clean, machine-parseable stdout — `error()` and warnings go to stderr — and the uninitialized-project path matches the sibling commands' error text and exit code. The usage-header line needed no change; the registered syntax matches what it has always advertised. ## Tested New `__tests__/cli-context-command.test.ts` (modeled on `cli-query-command.test.ts`, spawning the built binary against a temp fixture): JSON parseability + shape, `--max-nodes` bounding, the exact Memorix invocation shape (`--format json --max-nodes 8 --no-code`), markdown default, uninitialized-project failure, unknown-format rejection. `npx vitest run __tests__/cli-context-command.test.ts __tests__/context.test.ts __tests__/context-ranking.test.ts __tests__/cli-query-command.test.ts` → 4 files, 39 tests, all green. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01LxZj6W6Y1SHXwvpT3uwJpK
This commit is contained in:
@@ -84,6 +84,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
- Methods implemented in a generic or lifetime-parameterized `impl` block (`impl<T> Source for BufSource<T>`, `impl<'a> Iterator for Parents<'a>`) are now recorded under the implementing type instead of the trait. Previously such a method could not be found by its type — "who calls `BufSource::read`" had no answer — and it collided with the trait's own declaration, which could even invent a call-graph edge out of an impl body that contains no call at all. Impls on a reference (`impl Trait for &Foo`) and on a module-qualified type (`impl Trait for m::Foo`) are attributed to their type too. Re-index after upgrading. Thanks @Dshuishui. (#1588) (Rust)
|
||||
- A method call on a struct field — `self.inner.run()` with `inner: Inner` — now resolves to the method on the field's declared type. Previously the call was reduced to the bare method name and matched whichever same-named method was nearest, which was often the calling method itself, recording recursion that isn't in the source (a few hundred such self-edges in ripgrep alone), or a method of an unrelated type. References and `Box`/`Rc`/`Arc` fields are looked through, as Rust's own method calls are; a field whose type is external (a std or third-party type), a generic parameter, or a container like `Option`/`Vec` is left unresolved rather than guessed. Re-index after upgrading. Thanks @Dshuishui. (#1585) (Rust)
|
||||
|
||||
- The `codegraph context <task>` command documented in the CLI help now actually exists — it builds a ready-to-inject context pack for a task (relevant symbols, their relationships, and code) in markdown or JSON, restoring the contract external integrations like Memorix rely on (`--path`, `--format json`, `--max-nodes`, `--no-code`). (#1611)
|
||||
|
||||
## [1.5.0] - 2026-07-21
|
||||
|
||||
# ⚡ The Rust engine release — with near-instant sync
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* `codegraph context` CLI command (#1611).
|
||||
*
|
||||
* The usage header has advertised `codegraph context <task> Build context for
|
||||
* a task` since the first release, and the ContextBuilder behind the public
|
||||
* `buildContext` API has always shipped in the package — but the command was
|
||||
* never registered with commander, so external integrations built against the
|
||||
* documented contract (`codegraph context --path <root> --format json
|
||||
* --max-nodes 8 --no-code <task>`, e.g. Memorix) got `unknown command
|
||||
* 'context'` and fell back to their own heuristics.
|
||||
*
|
||||
* Exercised end-to-end against the built binary, mirroring
|
||||
* cli-query-command.test.ts.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { execFileSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { CodeGraph } from '../src';
|
||||
|
||||
const BIN = path.resolve(__dirname, '../dist/bin/codegraph.js');
|
||||
|
||||
const ENV = { ...process.env, CODEGRAPH_NO_DAEMON: '1', CODEGRAPH_WASM_RELAUNCHED: '1' };
|
||||
|
||||
function runContext(cwd: string, extraArgs: string[], taskParts: string[] = ['parseToken', 'expiry', 'handling']): string {
|
||||
return execFileSync(process.execPath, [BIN, 'context', ...extraArgs, '-p', cwd, ...taskParts], {
|
||||
encoding: 'utf-8',
|
||||
env: ENV,
|
||||
stdio: ['ignore', 'pipe', 'ignore'], // drop stderr (SQLite experimental warning)
|
||||
});
|
||||
}
|
||||
|
||||
describe('codegraph context — registered CLI command (#1611)', () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-context-cmd-'));
|
||||
fs.mkdirSync(path.join(tempDir, 'src'));
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'src/auth.ts'),
|
||||
'export function parseToken(t: string){ return parseTokenExpiry(t) + t.trim().length; }\n' +
|
||||
'export function parseTokenExpiry(t: string){ return Date.parse(t); }\n',
|
||||
);
|
||||
const cg = CodeGraph.initSync(tempDir);
|
||||
await cg.indexAll();
|
||||
cg.close();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('--format json emits clean machine-parseable JSON on stdout', () => {
|
||||
const parsed = JSON.parse(runContext(tempDir, ['--format', 'json']));
|
||||
expect(parsed.query).toBe('parseToken expiry handling');
|
||||
expect(Array.isArray(parsed.nodes)).toBe(true);
|
||||
expect(parsed.nodes.length).toBeGreaterThan(0);
|
||||
expect(Array.isArray(parsed.codeBlocks)).toBe(true);
|
||||
expect(parsed.codeBlocks.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('--max-nodes bounds the returned symbol set', () => {
|
||||
const parsed = JSON.parse(runContext(tempDir, ['--format', 'json', '--max-nodes', '1']));
|
||||
expect(parsed.nodes.length).toBeLessThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('--no-code omits code blocks (the Memorix contract shape)', () => {
|
||||
// The exact documented invocation: --format json --max-nodes 8 --no-code
|
||||
const parsed = JSON.parse(
|
||||
runContext(tempDir, ['--format', 'json', '--max-nodes', '8', '--no-code']),
|
||||
);
|
||||
expect(parsed.codeBlocks).toEqual([]);
|
||||
expect(parsed.nodes.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('defaults to markdown output', () => {
|
||||
const out = runContext(tempDir, []);
|
||||
expect(out).toContain('## Code Context');
|
||||
expect(out).toContain('**Query:** parseToken expiry handling');
|
||||
});
|
||||
|
||||
it('fails cleanly on an uninitialized project', () => {
|
||||
const empty = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-context-empty-'));
|
||||
try {
|
||||
execFileSync(process.execPath, [BIN, 'context', '-p', empty, 'some', 'task'], {
|
||||
encoding: 'utf-8',
|
||||
env: ENV,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
throw new Error('expected non-zero exit');
|
||||
} catch (err: any) {
|
||||
expect(err.status).toBe(1);
|
||||
expect(String(err.stderr)).toContain('not initialized');
|
||||
} finally {
|
||||
fs.rmSync(empty, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects an unknown --format value', () => {
|
||||
try {
|
||||
execFileSync(process.execPath, [BIN, 'context', '--format', 'yaml', '-p', tempDir, 'task'], {
|
||||
encoding: 'utf-8',
|
||||
env: ENV,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
throw new Error('expected non-zero exit');
|
||||
} catch (err: any) {
|
||||
expect(err.status).toBe(1);
|
||||
expect(String(err.stderr)).toContain('Unknown format');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1250,6 +1250,65 @@ program
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* codegraph context <task...>
|
||||
*
|
||||
* The CLI face of the public `buildContext` API (ContextBuilder): FTS entry
|
||||
* points + graph expansion + code blocks, formatted as markdown or JSON.
|
||||
* Advertised in the usage header since the first release but never actually
|
||||
* registered (#1611); external integrations (e.g. Memorix) invoke it as
|
||||
* `codegraph context --path <root> --format json --max-nodes 8 --no-code <task>`.
|
||||
*/
|
||||
program
|
||||
.command('context <task...>')
|
||||
.description('Build context for a task: relevant symbols, relationships, and code blocks')
|
||||
.option('-p, --path <path>', 'Project path')
|
||||
.option('-f, --format <format>', 'Output format: markdown or json', 'markdown')
|
||||
.option('-n, --max-nodes <number>', 'Maximum number of symbols to include')
|
||||
.option('--no-code', 'Omit code blocks (structure only)')
|
||||
.action(async (taskParts: string[], options: { path?: string; format?: string; maxNodes?: string; code?: boolean }) => {
|
||||
const projectPath = resolveProjectPath(options.path);
|
||||
|
||||
const format = options.format ?? 'markdown';
|
||||
if (format !== 'markdown' && format !== 'json') {
|
||||
error(`Unknown format "${options.format}" — use "markdown" or "json".`);
|
||||
process.exit(1);
|
||||
}
|
||||
let maxNodes: number | undefined;
|
||||
if (options.maxNodes !== undefined) {
|
||||
maxNodes = parseInt(options.maxNodes, 10);
|
||||
if (Number.isNaN(maxNodes) || maxNodes < 1) {
|
||||
error(`--max-nodes expects a positive integer, got "${options.maxNodes}".`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
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 result = await cg.buildContext(taskParts.join(' '), {
|
||||
format,
|
||||
includeCode: options.code !== false,
|
||||
...(maxNodes !== undefined ? { maxNodes } : {}),
|
||||
});
|
||||
|
||||
// Both supported formats return a formatted string; print it verbatim so
|
||||
// `--format json` stays machine-parseable on stdout (error()/warnings go
|
||||
// to stderr only).
|
||||
console.log(typeof result === 'string' ? result : JSON.stringify(result, null, 2));
|
||||
cg.destroy();
|
||||
} catch (err) {
|
||||
error(`Context build failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* codegraph prompt-hook (hidden)
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user