@sdsrs/code-graph 0.77.0 → 0.77.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "sdsrs"
6
6
  },
7
- "version": "0.77.0",
7
+ "version": "0.77.2",
8
8
  "keywords": [
9
9
  "code-graph",
10
10
  "ast",
@@ -71,16 +71,41 @@ function runProvider(command, needsStdin, stdin) {
71
71
  // swallowed below, silently dropping the user's original statusline.
72
72
  const argv = parts.map(expandTilde);
73
73
 
74
+ // Forward Claude Code's authoritative current dir (from the stdin payload) as
75
+ // a plugin-scoped env var. The code-graph provider gates on it instead of its
76
+ // own process.cwd(), which need not track the session's working dir. Harmless
77
+ // to `_previous`/third-party providers, which ignore the unknown var. The
78
+ // CODE_GRAPH_ prefix (not CLAUDE_) keeps it out of Claude Code's own namespace.
79
+ const cwd = cwdFromStdin(stdin);
80
+ const env = cwd ? { ...process.env, CODE_GRAPH_STATUSLINE_CWD: cwd } : process.env;
81
+
74
82
  const out = execFileSync(argv[0], argv.slice(1), {
75
83
  timeout: 3000,
76
84
  stdio: ['pipe', 'pipe', 'pipe'],
77
85
  input: needsStdin ? stdin : '',
86
+ env,
78
87
  }).toString().trim();
79
88
 
80
89
  return out || null;
81
90
  } catch { return null; }
82
91
  }
83
92
 
93
+ // Extract Claude Code's current working directory from the stdin JSON context.
94
+ // Prefer the top-level `cwd`, then `workspace.current_dir`; both track the
95
+ // session's working dir (after the model runs `cd`). Returns null for empty,
96
+ // non-JSON, or cwd-less payloads (e.g. the stdin-timeout fallback passes '').
97
+ // Only a non-empty STRING is accepted: a malformed `cwd` (number/object) would
98
+ // otherwise be coerced to a bogus env path that resolves nowhere and silently
99
+ // blanks the segment — null keeps the gate on the safe process.cwd() fallback.
100
+ function cwdFromStdin(stdin) {
101
+ if (!stdin) return null;
102
+ try {
103
+ const ctx = JSON.parse(stdin);
104
+ const v = ctx && (ctx.cwd || (ctx.workspace && ctx.workspace.current_dir));
105
+ return typeof v === 'string' && v ? v : null;
106
+ } catch { return null; }
107
+ }
108
+
84
109
  function parseCommand(cmd) {
85
110
  // Handle: node "/path/to/script.js"
86
111
  const match = cmd.match(/^(\S+)\s+"([^"]+)"(.*)$/);
@@ -109,4 +134,4 @@ function codeGraphCommand() {
109
134
  return `node "${path.join(__dirname, 'statusline.js')}"`;
110
135
  }
111
136
 
112
- module.exports = { run, runProvider, parseCommand, expandTilde };
137
+ module.exports = { run, runProvider, parseCommand, expandTilde, cwdFromStdin };
@@ -0,0 +1,65 @@
1
+ 'use strict';
2
+ // The composite is the registered statusLine command: it receives Claude Code's
3
+ // JSON context on stdin and fans out to each provider. This pins the cwd bridge:
4
+ // the code-graph provider keys its gate on process.cwd(), but Claude Code may
5
+ // spawn the statusline from a cwd unrelated to the session. The composite must
6
+ // extract the authoritative cwd from stdin and forward it (CODE_GRAPH_STATUSLINE_CWD)
7
+ // so the provider resolves the right project regardless of the spawn's cwd.
8
+ const test = require('node:test');
9
+ const assert = require('node:assert/strict');
10
+ const fs = require('fs');
11
+ const os = require('os');
12
+ const path = require('path');
13
+ const { cwdFromStdin, runProvider } = require('./statusline-composite');
14
+
15
+ test('cwdFromStdin reads the top-level cwd field', () => {
16
+ assert.equal(cwdFromStdin('{"cwd":"/a/b"}'), '/a/b');
17
+ });
18
+
19
+ test('cwdFromStdin falls back to workspace.current_dir', () => {
20
+ assert.equal(cwdFromStdin('{"workspace":{"current_dir":"/c/d"}}'), '/c/d');
21
+ });
22
+
23
+ test('cwdFromStdin prefers top-level cwd over workspace.current_dir', () => {
24
+ assert.equal(cwdFromStdin('{"cwd":"/a","workspace":{"current_dir":"/c"}}'), '/a');
25
+ });
26
+
27
+ test('cwdFromStdin returns null for empty / non-JSON / cwd-less payloads', () => {
28
+ assert.equal(cwdFromStdin(''), null);
29
+ assert.equal(cwdFromStdin('not json'), null);
30
+ assert.equal(cwdFromStdin('{}'), null);
31
+ assert.equal(cwdFromStdin('{"workspace":{}}'), null);
32
+ });
33
+
34
+ test('cwdFromStdin returns null for a non-string cwd (no bogus env path)', () => {
35
+ // A malformed payload must not coerce a number/object into an env path that
36
+ // resolves to nowhere and silently blanks the segment. Only a real string wins.
37
+ assert.equal(cwdFromStdin('{"cwd":123}'), null);
38
+ assert.equal(cwdFromStdin('{"cwd":{"x":1}}'), null);
39
+ assert.equal(cwdFromStdin('{"cwd":""}'), null);
40
+ assert.equal(cwdFromStdin('{"workspace":{"current_dir":42}}'), null);
41
+ });
42
+
43
+ test('runProvider forwards the stdin cwd to the provider as CODE_GRAPH_STATUSLINE_CWD', (t) => {
44
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-composite-'));
45
+ t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
46
+ const fixture = path.join(dir, 'echo-cwd.js');
47
+ fs.writeFileSync(fixture, "process.stdout.write('CWD='+(process.env.CODE_GRAPH_STATUSLINE_CWD||'NONE'));");
48
+ const out = runProvider(`node ${JSON.stringify(fixture)}`, false, '{"cwd":"/x/y"}');
49
+ assert.equal(out, 'CWD=/x/y');
50
+ });
51
+
52
+ test('runProvider leaves CODE_GRAPH_STATUSLINE_CWD unset when stdin carries no cwd', (t) => {
53
+ // Hermetic against an ambient var: with no stdin cwd, runProvider passes
54
+ // process.env through unchanged, so a value inherited by the test runner would
55
+ // leak into the child. Clear it for this case, restore after.
56
+ const saved = process.env.CODE_GRAPH_STATUSLINE_CWD;
57
+ delete process.env.CODE_GRAPH_STATUSLINE_CWD;
58
+ t.after(() => { if (saved !== undefined) process.env.CODE_GRAPH_STATUSLINE_CWD = saved; });
59
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-composite-'));
60
+ t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
61
+ const fixture = path.join(dir, 'echo-cwd.js');
62
+ fs.writeFileSync(fixture, "process.stdout.write('CWD='+(process.env.CODE_GRAPH_STATUSLINE_CWD||'NONE'));");
63
+ const out = runProvider(`node ${JSON.stringify(fixture)}`, false, '');
64
+ assert.equal(out, 'CWD=NONE');
65
+ });
@@ -33,7 +33,15 @@ if (disabledCleanup.cleaned) process.exit(0);
33
33
  // "oscillating" between root/backend/frontend node counts) or, in a clean subdir
34
34
  // with no local index, showed nothing at all. The resolver skips stray nested
35
35
  // indexes, so the statusline tracks one DB — the project root — from any subdir.
36
- const root = resolveProjectRoot(process.cwd());
36
+ //
37
+ // Start from Claude Code's AUTHORITATIVE current dir (CODE_GRAPH_STATUSLINE_CWD,
38
+ // forwarded by the composite from its stdin payload) rather than process.cwd().
39
+ // The spawned statusline's process.cwd() is an implementation detail of how
40
+ // Claude Code launches the command and need not track the session's working dir;
41
+ // the stdin `cwd` always does. Fall back to process.cwd() when unset (direct
42
+ // invocation, tests).
43
+ const startDir = process.env.CODE_GRAPH_STATUSLINE_CWD || process.cwd();
44
+ const root = resolveProjectRoot(startDir);
37
45
  if (!root) {
38
46
  process.exit(0);
39
47
  }
@@ -69,6 +69,17 @@ function runStatusline(home, projectDir) {
69
69
  }).trim();
70
70
  }
71
71
 
72
+ // Run statusline.js from an arbitrary process.cwd() with extra env vars. Used to
73
+ // prove the gate keys on Claude Code's authoritative cwd (CODE_GRAPH_STATUSLINE_CWD,
74
+ // forwarded by the composite from the stdin payload), NOT the spawn's cwd.
75
+ function runStatuslineIn(home, processCwd, extraEnv) {
76
+ return execFileSync('node', [STATUSLINE], {
77
+ cwd: processCwd,
78
+ env: { ...process.env, HOME: home, ...extraEnv },
79
+ encoding: 'utf8',
80
+ }).trim();
81
+ }
82
+
72
83
  function mkProject(home) {
73
84
  const dir = path.join(home, 'project');
74
85
  const cg = path.join(dir, '.code-graph');
@@ -164,3 +175,36 @@ test('version-stale index shows rebuilding marker', (t) => {
164
175
  });
165
176
  assert.equal(runStatusline(home, project), 'code-graph: ✓ 14119 nodes | 922 files | ↻ rebuilding');
166
177
  });
178
+
179
+ // CODE_GRAPH_STATUSLINE_CWD is Claude Code's authoritative current dir, forwarded by
180
+ // the composite from its stdin payload. The gate must trust it over process.cwd():
181
+ // Claude Code may spawn the statusline from a cwd unrelated to the session (the
182
+ // classic regression — the segment vanished when the shell sat in a subdir whose
183
+ // process.cwd() didn't resolve to the project root).
184
+ test('CODE_GRAPH_STATUSLINE_CWD overrides process.cwd() for the gate', (t) => {
185
+ const home = mkHome(t);
186
+ const project = mkProject(home);
187
+ installStubBinary(home, {
188
+ report: { healthy: true, nodes: 3145, files: 205 },
189
+ exitCode: 0,
190
+ });
191
+ // process.cwd() = home (no .code-graph → resolves null → would be blank), but
192
+ // the authoritative cwd points at the project → must render the health line.
193
+ const out = runStatuslineIn(home, home, { CODE_GRAPH_STATUSLINE_CWD: project });
194
+ assert.equal(out, 'code-graph: ✓ 3145 nodes | 205 files');
195
+ });
196
+
197
+ test('CODE_GRAPH_STATUSLINE_CWD in a subdir walks up to the project root', (t) => {
198
+ const home = mkHome(t);
199
+ const project = mkProject(home);
200
+ const subdir = path.join(project, 'claude-plugin', 'scripts');
201
+ fs.mkdirSync(subdir, { recursive: true });
202
+ installStubBinary(home, {
203
+ report: { healthy: true, nodes: 3145, files: 205 },
204
+ exitCode: 0,
205
+ });
206
+ // The reported symptom: shell in <root>/claude-plugin/scripts. The subdir has
207
+ // no .code-graph of its own; resolveProjectRoot must walk up to <root>.
208
+ const out = runStatuslineIn(home, home, { CODE_GRAPH_STATUSLINE_CWD: subdir });
209
+ assert.equal(out, 'code-graph: ✓ 3145 nodes | 205 files');
210
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdsrs/code-graph",
3
- "version": "0.77.0",
3
+ "version": "0.77.2",
4
4
  "description": "MCP server that indexes codebases into an AST knowledge graph with semantic search, call graph traversal, and HTTP route tracing",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -35,10 +35,10 @@
35
35
  "node": ">=16"
36
36
  },
37
37
  "optionalDependencies": {
38
- "@sdsrs/code-graph-linux-x64": "0.77.0",
39
- "@sdsrs/code-graph-linux-arm64": "0.77.0",
40
- "@sdsrs/code-graph-darwin-x64": "0.77.0",
41
- "@sdsrs/code-graph-darwin-arm64": "0.77.0",
42
- "@sdsrs/code-graph-win32-x64": "0.77.0"
38
+ "@sdsrs/code-graph-linux-x64": "0.77.2",
39
+ "@sdsrs/code-graph-linux-arm64": "0.77.2",
40
+ "@sdsrs/code-graph-darwin-x64": "0.77.2",
41
+ "@sdsrs/code-graph-darwin-arm64": "0.77.2",
42
+ "@sdsrs/code-graph-win32-x64": "0.77.2"
43
43
  }
44
44
  }