Repository: colbymchenry/codegraph (2026-06-29T02:54:20Z)

Issue:
Bug: `codegraph node -f <file>` rejects valid usage with "missing required argument 'name'"
## Bug: `codegraph node -f <file>` rejects valid usage with "missing required argument 'name'"

The `--file` flag on the `node` CLI command is supposed to enable file-read mode (read a file with line numbers + dependents, same as the MCP `codegraph_node` tool's file mode). But the CLI requires a `name` positional argument even when `--file` is passed, making file-read mode unusable.

### Reproduction

```bash
codegraph init .   # any project
codegraph node -f src/main/java/com/example/MyClass.java
```

### Expected

File-read mode returns the file's source with line numbers and dependent files (matching the MCP `codegraph_node` tool behavior described in `tools.ts`):

> "READ A FILE — use INSTEAD of the Read tool: pass `file` (a path or basename) with no `symbol` and it returns that file's current on-disk source with line numbers"

### Actual

```
error: missing required argument 'name'
```

The `name` positional argument is required by commander.js regardless of `--file`. In file mode, there is no symbol name to pass — the whole point is reading a file, not looking up a symbol.

### Root cause

In the CLI command definition for `node`, `name` is defined as a required positional argument. The `--file` flag changes the semantics (file path instead of symbol name) but doesn't make `name` optional. The commander.js definition likely needs `<name>` changed to `[name]` (optional) with validation that either `name` or `--file` is provided.

### Impact

File-read mode — which the MCP tool description positions as a drop-in replacement for the Read tool — is completely inaccessible from the CLI. Users who test CodeGraph via CLI before wiring up MCP will conclude the feature doesn't work.

### Environment

- CodeGraph v1.1.2 (linux-x64, bundled install)
- Tested on a Java/Android project (636 Java files, 27k total files)



Discussion:
Fixed in #1051 (merged to `main`) — thanks for the clear writeup and spot-on root-cause analysis, that made this a quick one.

Exactly as you diagnosed: `node` was declared with a required `<name>` positional, so commander.js rejected file mode before the command even ran. The symbol name is now optional, so all of these work:

- `codegraph node -f src/main/java/com/example/MyClass.java` — file-read mode (source with line numbers + dependents)
- `codegraph node src/main/java/com/example/MyClass.java` — a path-like positional routes to file mode too
- `codegraph node MyClass` — symbol lookup, unchanged

Running `codegraph node` with neither now prints a short usage hint instead of the cryptic `missing required argument 'name'`. Added an end-to-end CLI regression test across all four paths.

This is on `main` now and will go out in the next release (it's not in v1.1.2). I'll update here if you'd like a heads-up when it ships.



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3b7c0b9bb..831dd4208 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 - C++ classes that inherit from a templated base — `class Widget : public Base<int>`, a CRTP base like `class App : public CRTPBase<App>`, or a struct inheriting a template — are now linked to that base class in the graph. Previously the template arguments (`<int>`) made the inheritance go unrecognized, so these classes looked like they inherited from nothing and impact/callers analysis stopped at the boundary; the connection is now followed like any other base class. Thanks @ryancu7 for the report. (#1043)
 - C++ objects constructed on the stack — `Calculator calc(0)` or `Widget w{1, 2}` — now record that the enclosing function instantiates that class, the same as heap construction (`new Calculator(0)`) already did. Previously only the `new` form was tracked, so a function that built objects with the ordinary stack syntax looked like it didn't construct them and the dependency was missing from impact/callers. Thanks @Dshuishui for the report. (#1035)
 - The graph no longer stores duplicate copies of the same relationship. The same dependency between the same two symbols at the same spot could be recorded more than once, which inflated edge counts and let callers/impact results list a relationship twice. Each relationship is now stored exactly once, and existing projects are de-duplicated automatically the next time CodeGraph opens them. Thanks @inth3shadows for the detailed report. (#1034)
+- `codegraph node` can now read a file from the command line. File-read mode — pass `-f`/`--file` to get a file's source with line numbers plus the files that depend on it, the same output as the `codegraph_node` MCP tool — was rejected with "missing required argument 'name'", because the command always demanded a symbol name even though file mode has none, leaving the feature unreachable from the CLI. The symbol name is now optional: `codegraph node -f src/auth.ts` (or `codegraph node src/auth.ts`) reads the file, `codegraph node parseToken` looks up a symbol, and running it with neither prints a short usage hint instead of a cryptic error. Thanks @jcrabapple for the report. (#1044)
 
 
 ## [1.1.2] - 2026-06-28
diff --git a/src/bin/codegraph.ts b/src/bin/codegraph.ts
index c93a65678..04801e401 100644
--- a/src/bin/codegraph.ts
+++ b/src/bin/codegraph.ts
@@ -1140,21 +1140,32 @@ program
   });
 
 /**
- * codegraph node <name>
+ * codegraph node [name]
  *
  * The CLI face of the MCP codegraph_node tool: one symbol's source +
  * caller/callee trail, or a whole file with line numbers + dependents
  * (Read-parity). Same subagent/non-MCP rationale as `explore`.
+ *
+ * `name` is OPTIONAL because `--file` (file-read mode) carries no symbol —
+ * a required `<name>` made `codegraph node -f <file>` unreachable (#1044).
  */
 program
-  .command('node <name>')
+  .command('node [name]')
   .description('One symbol\'s source + caller/callee trail, or read a file with line numbers + dependents (same output as the codegraph_node MCP tool)')
   .option('-p, --path <path>', 'Project path')
   .option('-f, --file <file>', 'Treat as file mode (or disambiguate a symbol to this file)')
   .option('--offset <number>', 'File mode: 1-based start line')
   .option('--limit <number>', 'File mode: maximum lines')
   .option('--symbols-only', 'File mode: just the symbol map + dependents')
-  .action(async (name: string, options: { path?: string; file?: string; offset?: string; limit?: string; symbolsOnly?: boolean }) => {
+  .action(async (name: string | undefined, options: { path?: string; file?: string; offset?: string; limit?: string; symbolsOnly?: boolean }) => {
+    // Need a symbol (positional) OR a file (--file / a path-like positional).
+    // With [name] optional, a bare `codegraph node` reaches here with neither
+    // and must be told what to pass, rather than crashing downstream.
+    if (!name && !options.file) {
+      error("Pass a symbol name (e.g. 'codegraph node parseToken') or a file (e.g. 'codegraph node -f src/auth.ts', or 'codegraph node src/auth.ts').");
+      process.exit(1);
+    }
+
     const projectPath = resolveProjectPath(options.path);
 
     try {
@@ -1177,9 +1188,9 @@ program
       if (options.file) {
         args.file = options.file;
         if (name && name !== options.file) args.symbol = name;
-      } else if (name.includes('/') || name.includes('\\')) {
+      } else if (name && (name.includes('/') || name.includes('\\'))) {
         args.file = name.replace(/\\/g, '/');
-      } else {
+      } else if (name) {
         args.symbol = name;
         args.includeCode = true;
       }


Test patch:
diff --git a/__tests__/cli-node-command.test.ts b/__tests__/cli-node-command.test.ts
new file mode 100644
index 000000000..4d3485031
--- /dev/null
+++ b/__tests__/cli-node-command.test.ts
@@ -0,0 +1,80 @@
+/**
+ * `codegraph node` argument handling (#1044).
+ *
+ * File-read mode (`codegraph node -f <file>`) carries no symbol name, but the
+ * command was defined with a REQUIRED `<name>` positional, so commander.js
+ * rejected the call with "missing required argument 'name'" before the action
+ * ever ran — making file mode unreachable from the CLI. `name` is now optional
+ * (`[name]`); the action validates that a symbol OR a file is supplied.
+ *
+ * Exercised end-to-end against the built binary.
+ */
+
+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');
+
+function runNode(cwd: string, extraArgs: string[]): { stdout: string; stderr: string; code: number } {
+  try {
+    const stdout = execFileSync(process.execPath, [BIN, 'node', ...extraArgs, '-p', cwd], {
+      encoding: 'utf-8',
+      env: { ...process.env, CODEGRAPH_NO_DAEMON: '1', CODEGRAPH_WASM_RELAUNCHED: '1' },
+      stdio: ['ignore', 'pipe', 'pipe'],
+    });
+    return { stdout, stderr: '', code: 0 };
+  } catch (err: any) {
+    return { stdout: err.stdout ?? '', stderr: err.stderr ?? '', code: err.status ?? 1 };
+  }
+}
+
+describe('codegraph node — argument handling (#1044)', () => {
+  let tempDir: string;
+
+  beforeEach(async () => {
+    tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-node-cmd-'));
+    fs.mkdirSync(path.join(tempDir, 'src'));
+    fs.writeFileSync(path.join(tempDir, 'src/util.ts'), 'export function util(x: number){ return x + 1; }\n');
+    const cg = CodeGraph.initSync(tempDir);
+    await cg.indexAll();
+    cg.close();
+  });
+
+  afterEach(() => {
+    fs.rmSync(tempDir, { recursive: true, force: true });
+  });
+
+  it('file mode via -f reads the file (was rejected as "missing required argument")', () => {
+    const { stdout, code } = runNode(tempDir, ['-f', 'src/util.ts']);
+    expect(code).toBe(0);
+    expect(stdout).toContain('src/util.ts');
+    expect(stdout).toContain('export function util');
+    // The line-numbered Read-parity shape.
+    expect(stdout).toMatch(/1\s+export function util/);
+  });
+
+  it('a path-like positional still routes to file mode', () => {
+    const { stdout, code } = runNode(tempDir, ['src/util.ts']);
+    expect(code).toBe(0);
+    expect(stdout).toContain('src/util.ts');
+    expect(stdout).toContain('export function util');
+  });
+
+  it('a bare symbol positional still routes to symbol mode', () => {
+    const { stdout, code } = runNode(tempDir, ['util']);
+    expect(code).toBe(0);
+    expect(stdout).toContain('util');
+    expect(stdout).toContain('Location:');
+  });
+
+  it('neither symbol nor file gives a usage error, not commander\'s cryptic one', () => {
+    const { stderr, code } = runNode(tempDir, []);
+    expect(code).not.toBe(0);
+    expect(stderr).toMatch(/symbol name|file/i);
+    expect(stderr).not.toMatch(/missing required argument/);
+  });
+});