@sdsrs/code-graph 0.76.4 → 0.77.1

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.76.4",
7
+ "version": "0.77.1",
8
8
  "keywords": [
9
9
  "code-graph",
10
10
  "ast",
@@ -83,6 +83,10 @@ function shouldCheck(state) {
83
83
 
84
84
  // ── Version Comparison (semver) ────────────────────────────
85
85
 
86
+ // Assumes plain numeric "M.m.p" releases (the project's tag scheme). A pre-release
87
+ // tag (e.g. "1.2.4-rc1") is NOT semver-ordered: `Number("4-rc1")` is NaN → coerced
88
+ // to 0, dropping that segment's number (so "1.2.4-rc1" wrongly sorts below "1.2.3").
89
+ // Revisit with a real semver compare only if the release process adopts pre-releases.
86
90
  function compareVersions(a, b) {
87
91
  const pa = a.split('.').map(Number);
88
92
  const pb = b.split('.').map(Number);
@@ -57,7 +57,13 @@ function getPackageVersion() {
57
57
  catch { return null; }
58
58
  }
59
59
 
60
- /** Compare semver-ish "M.m.p" strings; returns -1, 0, or 1. Non-numeric parts → 0. */
60
+ /**
61
+ * Compare semver-ish "M.m.p" strings; returns -1, 0, or 1. Non-numeric parts → 0.
62
+ * Assumes plain numeric releases (the project's tag scheme); a pre-release tag
63
+ * (e.g. "1.2.3-rc1") is NOT semver-ordered — `parseInt("3-rc1", 10)` keeps the
64
+ * leading 3 and drops the suffix, so "1.2.3-rc1" compares EQUAL to "1.2.3".
65
+ * Revisit only if releases adopt pre-release tags.
66
+ */
61
67
  function compareVersions(a, b) {
62
68
  const pa = String(a).split('.').map(s => parseInt(s, 10));
63
69
  const pb = String(b).split('.').map(s => parseInt(s, 10));
@@ -71,16 +71,36 @@ 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
+ // an env var. The code-graph provider gates on it instead of its own
76
+ // process.cwd(), which need not track the session's working dir. Harmless to
77
+ // `_previous`/third-party providers, which ignore the unknown var.
78
+ const cwd = cwdFromStdin(stdin);
79
+ const env = cwd ? { ...process.env, CLAUDE_STATUSLINE_CWD: cwd } : process.env;
80
+
74
81
  const out = execFileSync(argv[0], argv.slice(1), {
75
82
  timeout: 3000,
76
83
  stdio: ['pipe', 'pipe', 'pipe'],
77
84
  input: needsStdin ? stdin : '',
85
+ env,
78
86
  }).toString().trim();
79
87
 
80
88
  return out || null;
81
89
  } catch { return null; }
82
90
  }
83
91
 
92
+ // Extract Claude Code's current working directory from the stdin JSON context.
93
+ // Prefer the top-level `cwd`, then `workspace.current_dir`; both track the
94
+ // session's working dir (after the model runs `cd`). Returns null for empty,
95
+ // non-JSON, or cwd-less payloads (e.g. the stdin-timeout fallback passes '').
96
+ function cwdFromStdin(stdin) {
97
+ if (!stdin) return null;
98
+ try {
99
+ const ctx = JSON.parse(stdin);
100
+ return (ctx && (ctx.cwd || (ctx.workspace && ctx.workspace.current_dir))) || null;
101
+ } catch { return null; }
102
+ }
103
+
84
104
  function parseCommand(cmd) {
85
105
  // Handle: node "/path/to/script.js"
86
106
  const match = cmd.match(/^(\S+)\s+"([^"]+)"(.*)$/);
@@ -109,4 +129,4 @@ function codeGraphCommand() {
109
129
  return `node "${path.join(__dirname, 'statusline.js')}"`;
110
130
  }
111
131
 
112
- module.exports = { run, runProvider, parseCommand, expandTilde };
132
+ module.exports = { run, runProvider, parseCommand, expandTilde, cwdFromStdin };
@@ -0,0 +1,50 @@
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 (CLAUDE_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('runProvider forwards the stdin cwd to the provider as CLAUDE_STATUSLINE_CWD', (t) => {
35
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-composite-'));
36
+ t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
37
+ const fixture = path.join(dir, 'echo-cwd.js');
38
+ fs.writeFileSync(fixture, "process.stdout.write('CWD='+(process.env.CLAUDE_STATUSLINE_CWD||'NONE'));");
39
+ const out = runProvider(`node ${JSON.stringify(fixture)}`, false, '{"cwd":"/x/y"}');
40
+ assert.equal(out, 'CWD=/x/y');
41
+ });
42
+
43
+ test('runProvider leaves CLAUDE_STATUSLINE_CWD unset when stdin carries no 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.CLAUDE_STATUSLINE_CWD||'NONE'));");
48
+ const out = runProvider(`node ${JSON.stringify(fixture)}`, false, '');
49
+ assert.equal(out, 'CWD=NONE');
50
+ });
@@ -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 (CLAUDE_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.CLAUDE_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 (CLAUDE_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
+ // CLAUDE_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('CLAUDE_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, { CLAUDE_STATUSLINE_CWD: project });
194
+ assert.equal(out, 'code-graph: ✓ 3145 nodes | 205 files');
195
+ });
196
+
197
+ test('CLAUDE_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, { CLAUDE_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.76.4",
3
+ "version": "0.77.1",
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.76.4",
39
- "@sdsrs/code-graph-linux-arm64": "0.76.4",
40
- "@sdsrs/code-graph-darwin-x64": "0.76.4",
41
- "@sdsrs/code-graph-darwin-arm64": "0.76.4",
42
- "@sdsrs/code-graph-win32-x64": "0.76.4"
38
+ "@sdsrs/code-graph-linux-x64": "0.77.1",
39
+ "@sdsrs/code-graph-linux-arm64": "0.77.1",
40
+ "@sdsrs/code-graph-darwin-x64": "0.77.1",
41
+ "@sdsrs/code-graph-darwin-arm64": "0.77.1",
42
+ "@sdsrs/code-graph-win32-x64": "0.77.1"
43
43
  }
44
44
  }