@sdsrs/code-graph 0.74.8 → 0.75.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.
- package/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/claude-plugin/scripts/cg-answer.js +62 -1
- package/claude-plugin/scripts/cg-answer.test.js +57 -1
- package/claude-plugin/scripts/post-grep-inject.js +41 -7
- package/claude-plugin/scripts/post-grep-inject.test.js +13 -0
- package/claude-plugin/scripts/pre-grep-guide.test.js +32 -0
- package/claude-plugin/scripts/project-root.js +43 -4
- package/claude-plugin/scripts/statusline.js +17 -7
- package/package.json +6 -6
|
@@ -243,6 +243,67 @@ function runOverviewAnswer(opts = {}) {
|
|
|
243
243
|
}
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
+
/**
|
|
247
|
+
* v0.75 — Run `code-graph-mcp callgraph <symbol>` for the cross-file caller/callee
|
|
248
|
+
* tree. This is the ONE thing a raw grep CANNOT return: a symbol-targeted grep
|
|
249
|
+
* hands the model the definition + same-file usages it already scoped to, but NOT
|
|
250
|
+
* "who calls this across the repo". The 2026-06-26 inject audit (13 events, 0
|
|
251
|
+
* CONSUMED) found the grep-echo payload redundant precisely because it re-stated
|
|
252
|
+
* the model's own hits; the caller tree is the marginal signal grep can't give.
|
|
253
|
+
*
|
|
254
|
+
* "hits" requires an actual EDGE line (`← called by` / `→ calls`) — a bare symbol
|
|
255
|
+
* header with no edges (leaf symbol, or name not in the graph) carries no marginal
|
|
256
|
+
* value over the grep the model already ran, so it degrades to `no-hits` and the
|
|
257
|
+
* caller falls back to the grep/show echo. Same bounded/best-effort posture as the
|
|
258
|
+
* sibling runners; any failure → `unavailable` / `no-binary`, never a new failure.
|
|
259
|
+
* @returns {{status: 'hits', text: string, truncated: boolean}
|
|
260
|
+
* | {status: 'no-hits'}
|
|
261
|
+
* | {status: 'no-binary'}
|
|
262
|
+
* | {status: 'unavailable'}}
|
|
263
|
+
*/
|
|
264
|
+
function runCallgraphAnswer(opts = {}) {
|
|
265
|
+
const {
|
|
266
|
+
cwd,
|
|
267
|
+
symbol,
|
|
268
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
269
|
+
maxBytes = DEFAULT_MAX_BYTES,
|
|
270
|
+
} = opts;
|
|
271
|
+
try {
|
|
272
|
+
if (typeof symbol !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(symbol)) {
|
|
273
|
+
return { status: 'unavailable' };
|
|
274
|
+
}
|
|
275
|
+
let binary = opts.binary;
|
|
276
|
+
if (binary === undefined) {
|
|
277
|
+
binary = process.env._CG_ANSWER_BINARY || require('./find-binary').findBinary();
|
|
278
|
+
}
|
|
279
|
+
if (!binary) return { status: 'no-binary' };
|
|
280
|
+
|
|
281
|
+
const res = spawnSync(binary, ['callgraph', symbol], {
|
|
282
|
+
cwd,
|
|
283
|
+
timeout: timeoutMs,
|
|
284
|
+
encoding: 'utf8',
|
|
285
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
286
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
287
|
+
env: { ...process.env, CODE_GRAPH_INTERNAL: '1' },
|
|
288
|
+
});
|
|
289
|
+
if (res.error || res.signal) return { status: 'unavailable' };
|
|
290
|
+
// grep-parity exit codes: 1 = symbol not found (no graph node).
|
|
291
|
+
if (res.status === 1) return { status: 'no-hits' };
|
|
292
|
+
if (res.status !== 0) return { status: 'unavailable' };
|
|
293
|
+
const out = (res.stdout || '').trim();
|
|
294
|
+
// Only an edge-bearing tree is marginal over the grep the model already ran.
|
|
295
|
+
if (!out || out.startsWith(NO_MATCH_PREFIX) ||
|
|
296
|
+
!(out.includes('← called by') || out.includes('→ calls'))) {
|
|
297
|
+
return { status: 'no-hits' };
|
|
298
|
+
}
|
|
299
|
+
const { text, truncated } = truncateAtLine(out, maxBytes);
|
|
300
|
+
return { status: 'hits', text, truncated };
|
|
301
|
+
} catch {
|
|
302
|
+
return { status: 'unavailable' };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
246
306
|
module.exports = {
|
|
247
|
-
runGrepAnswer, runShowAnswer, runOverviewAnswer,
|
|
307
|
+
runGrepAnswer, runShowAnswer, runOverviewAnswer, runCallgraphAnswer,
|
|
308
|
+
truncateAtLine, sanitizeSearchPath,
|
|
248
309
|
};
|
|
@@ -4,7 +4,7 @@ const assert = require('node:assert/strict');
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const path = require('path');
|
|
7
|
-
const { runGrepAnswer, runShowAnswer, runOverviewAnswer, truncateAtLine } = require('./cg-answer');
|
|
7
|
+
const { runGrepAnswer, runShowAnswer, runOverviewAnswer, runCallgraphAnswer, truncateAtLine } = require('./cg-answer');
|
|
8
8
|
|
|
9
9
|
// Stub "binary": a node script that reacts to its first real arg so one stub
|
|
10
10
|
// covers hits / no-hits / error / timeout cases.
|
|
@@ -24,6 +24,15 @@ else if (pattern === 'NothingHere') {
|
|
|
24
24
|
} else if (pattern === 'NothingHereExit1') {
|
|
25
25
|
// v0.50 grep-parity binary: no match → empty stdout + exit 1
|
|
26
26
|
process.exit(1);
|
|
27
|
+
} else if (pattern === 'HasCallers') {
|
|
28
|
+
// callgraph with real edges → runCallgraphAnswer 'hits'
|
|
29
|
+
process.stdout.write(
|
|
30
|
+
'HasCallers (src/a.rs)\\n' +
|
|
31
|
+
' \\u2190 called by: alpha (src/b.rs)\\n' +
|
|
32
|
+
' \\u2192 calls: beta (src/c.rs)\\n');
|
|
33
|
+
} else if (pattern === 'LeafSymbol') {
|
|
34
|
+
// callgraph with a bare header, no edge lines → 'no-hits' (no marginal value)
|
|
35
|
+
process.stdout.write('LeafSymbol (src/a.rs)\\n');
|
|
27
36
|
} else {
|
|
28
37
|
process.stdout.write(
|
|
29
38
|
'src/storage/db.rs:42 fn ' + pattern + '() {\\n' +
|
|
@@ -251,3 +260,50 @@ test('runOverviewAnswer: empty/oversized dir → unavailable (never spawns)', ()
|
|
|
251
260
|
runOverviewAnswer({ cwd: stubDir, dir: 'a'.repeat(301), binary: stubBinary() }).status,
|
|
252
261
|
'unavailable');
|
|
253
262
|
});
|
|
263
|
+
|
|
264
|
+
// ── runCallgraphAnswer (v0.75) — cross-file caller/callee tree ────────
|
|
265
|
+
|
|
266
|
+
test('runCallgraphAnswer: edge-bearing tree → hits with caller/callee lines', () => {
|
|
267
|
+
const r = runCallgraphAnswer({ cwd: stubDir, symbol: 'HasCallers', binary: stubBinary() });
|
|
268
|
+
assert.equal(r.status, 'hits');
|
|
269
|
+
assert.match(r.text, /called by: alpha/);
|
|
270
|
+
assert.match(r.text, /calls: beta/);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('runCallgraphAnswer: passes callgraph subcommand + symbol as argv', () => {
|
|
274
|
+
// 'HasCallers' is the only stub branch that emits edges; assert it reached it.
|
|
275
|
+
const r = runCallgraphAnswer({ cwd: stubDir, symbol: 'HasCallers', binary: stubBinary() });
|
|
276
|
+
assert.equal(r.status, 'hits');
|
|
277
|
+
assert.match(r.text, /← called by/);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('runCallgraphAnswer: bare header with no edges → no-hits (no marginal value)', () => {
|
|
281
|
+
const r = runCallgraphAnswer({ cwd: stubDir, symbol: 'LeafSymbol', binary: stubBinary() });
|
|
282
|
+
assert.equal(r.status, 'no-hits');
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test('runCallgraphAnswer: symbol not in graph (exit 1) → no-hits', () => {
|
|
286
|
+
const r = runCallgraphAnswer({ cwd: stubDir, symbol: 'NothingHereExit1', binary: stubBinary() });
|
|
287
|
+
assert.equal(r.status, 'no-hits');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test('runCallgraphAnswer: failing binary → unavailable', () => {
|
|
291
|
+
const r = runCallgraphAnswer({ cwd: stubDir, symbol: 'ExplodePlease', binary: stubBinary() });
|
|
292
|
+
assert.equal(r.status, 'unavailable');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('runCallgraphAnswer: missing binary → no-binary (distinct from runtime unavailable)', () => {
|
|
296
|
+
const r = runCallgraphAnswer({ cwd: stubDir, symbol: 'HasCallers', binary: null });
|
|
297
|
+
assert.equal(r.status, 'no-binary');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test('runCallgraphAnswer: non-identifier symbol → unavailable (never spawns)', () => {
|
|
301
|
+
assert.equal(runCallgraphAnswer({ cwd: stubDir, symbol: 'a|b', binary: stubBinary() }).status, 'unavailable');
|
|
302
|
+
assert.equal(runCallgraphAnswer({ cwd: stubDir, symbol: '', binary: stubBinary() }).status, 'unavailable');
|
|
303
|
+
assert.equal(runCallgraphAnswer({ cwd: stubDir, symbol: 'def foo', binary: stubBinary() }).status, 'unavailable');
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test('runCallgraphAnswer: timeout → unavailable', () => {
|
|
307
|
+
const r = runCallgraphAnswer({ cwd: stubDir, symbol: 'HangForever', binary: stubBinary(), timeoutMs: 300 });
|
|
308
|
+
assert.equal(r.status, 'unavailable');
|
|
309
|
+
});
|
|
@@ -25,7 +25,7 @@ const path = require('path');
|
|
|
25
25
|
const crypto = require('crypto');
|
|
26
26
|
const { cgTmpDir } = require('./tmp-dir');
|
|
27
27
|
const { recordRecommendation } = require('./recommendation-log');
|
|
28
|
-
const { runGrepAnswer, runShowAnswer, sanitizeSearchPath } = require('./cg-answer');
|
|
28
|
+
const { runGrepAnswer, runShowAnswer, runCallgraphAnswer, sanitizeSearchPath } = require('./cg-answer');
|
|
29
29
|
const { emitPostToolContext } = require('./hook-emit');
|
|
30
30
|
const {
|
|
31
31
|
splitTopLevelSegments,
|
|
@@ -65,8 +65,21 @@ function findFoldableGrepSegment(cmd) {
|
|
|
65
65
|
// Short header so the model recognizes this as cg's parallel structural view of
|
|
66
66
|
// the grep it just ran (the grep already executed; this is additive context).
|
|
67
67
|
const INJECT_HEADER = '[code-graph] AST-aware view of your grep (ran alongside):';
|
|
68
|
+
// callgraph mode carries the cross-file caller/callee tree — the marginal signal
|
|
69
|
+
// the grep CANNOT return (2026-06-26 audit: the grep-echo above was redundant
|
|
70
|
+
// with the model's own hits; the caller tree is what moves behavior).
|
|
71
|
+
const CALLGRAPH_HEADER =
|
|
72
|
+
'[code-graph] Cross-file call graph for the symbol you grepped (grep can\'t show this):';
|
|
68
73
|
|
|
69
74
|
function buildInjectText(answer, mode) {
|
|
75
|
+
if (mode === 'callgraph') {
|
|
76
|
+
const lines = [CALLGRAPH_HEADER, answer.text];
|
|
77
|
+
if (answer.truncated) {
|
|
78
|
+
lines.push('(truncated — run `code-graph-mcp callgraph <symbol>` yourself for the full tree)');
|
|
79
|
+
}
|
|
80
|
+
lines.push('`← called by` = callers, `→ calls` = callees, across all files — use these directly.');
|
|
81
|
+
return lines.join('\n');
|
|
82
|
+
}
|
|
70
83
|
const lines = [INJECT_HEADER, answer.text];
|
|
71
84
|
if (answer.truncated) {
|
|
72
85
|
lines.push(mode === 'show'
|
|
@@ -142,18 +155,39 @@ function runMain() {
|
|
|
142
155
|
|
|
143
156
|
const { segment, block } = found;
|
|
144
157
|
// Run the answer exactly like the deny path.
|
|
145
|
-
const
|
|
158
|
+
const rawPattern = pickBlockPattern(segment);
|
|
159
|
+
const pattern = translateBreToRg(segment, rawPattern);
|
|
146
160
|
const searchPath = sanitizeSearchPath(extractSearchPath(segment));
|
|
147
161
|
let answer = { status: 'unavailable' };
|
|
148
162
|
let answeredMode = block.mode;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
163
|
+
|
|
164
|
+
// PREFER the cross-file caller/callee tree when the grep targets a single clean
|
|
165
|
+
// identifier — that is the marginal signal a raw grep can't return (2026-06-26
|
|
166
|
+
// inject audit: 13 events / 0 CONSUMED because the grep-echo just re-stated the
|
|
167
|
+
// model's own hits). runCallgraphAnswer returns `hits` ONLY when the symbol has
|
|
168
|
+
// real edges; a leaf/absent symbol degrades to the show/grep echo below.
|
|
169
|
+
const symbol = (typeof rawPattern === 'string' && /^[A-Za-z_][A-Za-z0-9_]*$/.test(rawPattern))
|
|
170
|
+
? rawPattern : null;
|
|
171
|
+
if (symbol) {
|
|
172
|
+
const cg = runCallgraphAnswer({ cwd: root, symbol });
|
|
173
|
+
if (cg.status === 'hits') {
|
|
174
|
+
answer = cg;
|
|
175
|
+
answeredMode = 'callgraph';
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (answer.status !== 'hits') {
|
|
180
|
+
if (block.mode === 'show') {
|
|
181
|
+
answeredMode = 'show';
|
|
182
|
+
answer = runShowAnswer({ cwd: root, symbols: block.symbols });
|
|
183
|
+
if (answer.status !== 'hits' && pattern) {
|
|
184
|
+
answeredMode = 'grep';
|
|
185
|
+
answer = runGrepAnswer({ cwd: root, pattern, searchPath });
|
|
186
|
+
}
|
|
187
|
+
} else if (pattern) {
|
|
152
188
|
answeredMode = 'grep';
|
|
153
189
|
answer = runGrepAnswer({ cwd: root, pattern, searchPath });
|
|
154
190
|
}
|
|
155
|
-
} else if (pattern) {
|
|
156
|
-
answer = runGrepAnswer({ cwd: root, pattern, searchPath });
|
|
157
191
|
}
|
|
158
192
|
|
|
159
193
|
// Only inject on hits — no-hits / unavailable / no-binary stay silent (the grep
|
|
@@ -95,6 +95,19 @@ test('buildInjectText: no truncation note when not truncated', () => {
|
|
|
95
95
|
assert.doesNotMatch(out, /truncated/);
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
+
test('buildInjectText: callgraph mode uses the cross-file header (not the grep-echo header)', () => {
|
|
99
|
+
const out = buildInjectText({ text: ' ← called by: alpha (src/b.rs)', truncated: false }, 'callgraph');
|
|
100
|
+
assert.match(out, /Cross-file call graph/);
|
|
101
|
+
assert.match(out, /grep can't show this/);
|
|
102
|
+
assert.doesNotMatch(out, /AST-aware view of your grep/);
|
|
103
|
+
assert.match(out, /← called by` = callers/);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('buildInjectText: callgraph truncation note points at the callgraph command', () => {
|
|
107
|
+
const out = buildInjectText({ text: 'tree', truncated: true }, 'callgraph');
|
|
108
|
+
assert.match(out, /code-graph-mcp callgraph <symbol>/);
|
|
109
|
+
});
|
|
110
|
+
|
|
98
111
|
// ── opt-out / kill switch ───────────────────────────────────────────
|
|
99
112
|
|
|
100
113
|
test('isSilenced: CODE_GRAPH_QUIET_HOOKS=1 → silenced; default not', () => {
|
|
@@ -1359,6 +1359,38 @@ test('resolveProjectRoot: no index up to $HOME → null (home itself still check
|
|
|
1359
1359
|
} finally { fsE2e.rmSync(base, { recursive: true, force: true }); }
|
|
1360
1360
|
});
|
|
1361
1361
|
|
|
1362
|
+
test('resolveProjectRoot: skips a STRAY nested subdir index, prefers the .git root', () => {
|
|
1363
|
+
// monorepo (daagu shape): root has .git + index; a subdir carries a stray
|
|
1364
|
+
// index relic but no .git. Resolving from the subdir must climb to the root,
|
|
1365
|
+
// not pin the stray nested index (the statusline "oscillation" root cause).
|
|
1366
|
+
const base = fsE2e.mkdtempSync(pathE2e.join(osE2e.tmpdir(), 'cg-root-'));
|
|
1367
|
+
try {
|
|
1368
|
+
const proj = pathE2e.join(base, 'proj');
|
|
1369
|
+
fsE2e.mkdirSync(pathE2e.join(proj, '.git'), { recursive: true });
|
|
1370
|
+
fsE2e.mkdirSync(pathE2e.join(proj, '.code-graph'), { recursive: true });
|
|
1371
|
+
fsE2e.writeFileSync(pathE2e.join(proj, '.code-graph', 'index.db'), '');
|
|
1372
|
+
const sub = pathE2e.join(proj, 'backend');
|
|
1373
|
+
fsE2e.mkdirSync(pathE2e.join(sub, '.code-graph'), { recursive: true });
|
|
1374
|
+
fsE2e.writeFileSync(pathE2e.join(sub, '.code-graph', 'index.db'), '');
|
|
1375
|
+
assert.equal(resolveProjectRoot(sub, { home: base }), proj);
|
|
1376
|
+
} finally { fsE2e.rmSync(base, { recursive: true, force: true }); }
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
test('resolveProjectRoot: a nested index with its OWN .git (submodule) still wins', () => {
|
|
1380
|
+
const base = fsE2e.mkdtempSync(pathE2e.join(osE2e.tmpdir(), 'cg-root-'));
|
|
1381
|
+
try {
|
|
1382
|
+
const proj = pathE2e.join(base, 'proj');
|
|
1383
|
+
fsE2e.mkdirSync(pathE2e.join(proj, '.git'), { recursive: true });
|
|
1384
|
+
fsE2e.mkdirSync(pathE2e.join(proj, '.code-graph'), { recursive: true });
|
|
1385
|
+
fsE2e.writeFileSync(pathE2e.join(proj, '.code-graph', 'index.db'), '');
|
|
1386
|
+
const sub = pathE2e.join(proj, 'vendored');
|
|
1387
|
+
fsE2e.mkdirSync(pathE2e.join(sub, '.git'), { recursive: true });
|
|
1388
|
+
fsE2e.mkdirSync(pathE2e.join(sub, '.code-graph'), { recursive: true });
|
|
1389
|
+
fsE2e.writeFileSync(pathE2e.join(sub, '.code-graph', 'index.db'), '');
|
|
1390
|
+
assert.equal(resolveProjectRoot(sub, { home: base }), sub);
|
|
1391
|
+
} finally { fsE2e.rmSync(base, { recursive: true, force: true }); }
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1362
1394
|
test('rebaseRelativePaths: daagu shape — bare `app` from backend/ cwd', () => {
|
|
1363
1395
|
const exists = (p) => p.endsWith(pathE2e.join('backend', 'app'));
|
|
1364
1396
|
const cmd = 'grep -rn "rr_source\\|max_retries" app --include=*.py';
|
|
@@ -14,16 +14,55 @@ const fs = require('fs');
|
|
|
14
14
|
const os = require('os');
|
|
15
15
|
const path = require('path');
|
|
16
16
|
|
|
17
|
+
// Resolves to the project's CANONICAL index dir, skipping STRAY nested indexes.
|
|
18
|
+
// A monorepo subdir (`daagu/backend`, `daagu/frontend`) can carry its own
|
|
19
|
+
// `.code-graph/index.db` — a relic an older binary created — nested under the
|
|
20
|
+
// real root's index. Returning the nearest such index made every consumer
|
|
21
|
+
// (statusline gate, hooks) read a different DB per cwd (statusline "oscillation",
|
|
22
|
+
// `✗ 0 nodes` in an empty subdir index). Mirror the Rust resolver: the start's
|
|
23
|
+
// own index wins only if it is NOT a stray nested index (no indexed ancestor) OR
|
|
24
|
+
// start is itself a project boundary (`.git`, i.e. a real submodule). Otherwise
|
|
25
|
+
// prefer the project root: the nearest indexed `.git` root, else the outermost
|
|
26
|
+
// indexed dir on the chain. `null` when nothing on start→…→home is indexed.
|
|
17
27
|
function resolveProjectRoot(startDir, opts = {}) {
|
|
18
28
|
const home = opts.home !== undefined ? opts.home : os.homedir();
|
|
19
29
|
const exists = opts.exists || fs.existsSync;
|
|
20
|
-
|
|
30
|
+
const hasIndex = (d) => exists(path.join(d, '.code-graph', 'index.db'));
|
|
31
|
+
const hasGit = (d) => exists(path.join(d, '.git'));
|
|
32
|
+
const start = path.resolve(startDir || '.');
|
|
33
|
+
|
|
34
|
+
// Detect whether `start` is a STRAY nested index: walk STRICT ancestors up to
|
|
35
|
+
// the nearest `.git` root (project boundary), bounded at home. An indexed
|
|
36
|
+
// ancestor within that boundary means start's own index is a monorepo-subdir
|
|
37
|
+
// relic. Stop AT the git root — an index above it (e.g. `~/.code-graph`) is an
|
|
38
|
+
// unrelated outer project and must not poison this one.
|
|
39
|
+
let gitRootIndexed = null;
|
|
40
|
+
let ancestorIndexed = false;
|
|
41
|
+
let dir = start;
|
|
21
42
|
for (;;) {
|
|
22
|
-
if (exists(path.join(dir, '.code-graph', 'index.db'))) return dir;
|
|
23
|
-
if (dir === home) return null;
|
|
24
43
|
const parent = path.dirname(dir);
|
|
25
|
-
|
|
44
|
+
// Stop BEFORE home (and fs root): an index at/above home (e.g. ~/.code-graph
|
|
45
|
+
// from indexing a home dir) is an unrelated outer project, never a parent
|
|
46
|
+
// that makes `start` stray.
|
|
47
|
+
if (parent === dir || parent === home) break;
|
|
26
48
|
dir = parent;
|
|
49
|
+
if (hasIndex(dir)) ancestorIndexed = true;
|
|
50
|
+
if (hasGit(dir)) { if (hasIndex(dir)) gitRootIndexed = dir; break; }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// start's own index wins unless it is stray (indexed ancestor within the git
|
|
54
|
+
// boundary) and start is not itself a boundary (`.git`, a real submodule).
|
|
55
|
+
if (hasIndex(start) && (!ancestorIndexed || hasGit(start))) return start;
|
|
56
|
+
if (gitRootIndexed) return gitRootIndexed;
|
|
57
|
+
// Otherwise the nearest indexed ancestor (skipping a stray start), bounded at
|
|
58
|
+
// home; null if nothing on the chain is indexed. Mirrors the original walk.
|
|
59
|
+
let d = hasIndex(start) ? path.dirname(start) : start;
|
|
60
|
+
for (;;) {
|
|
61
|
+
if (hasIndex(d)) return d;
|
|
62
|
+
if (d === home) return null;
|
|
63
|
+
const parent = path.dirname(d);
|
|
64
|
+
if (parent === d) return null;
|
|
65
|
+
d = parent;
|
|
27
66
|
}
|
|
28
67
|
}
|
|
29
68
|
|
|
@@ -5,6 +5,7 @@ const fs = require('fs');
|
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const { findBinary } = require('./find-binary');
|
|
8
|
+
const { resolveProjectRoot } = require('./project-root');
|
|
8
9
|
const lifecycle = require('./lifecycle');
|
|
9
10
|
const cleanupDisabledStatusline = lifecycle.cleanupDisabledStatusline || (() => ({ cleaned: false }));
|
|
10
11
|
|
|
@@ -24,14 +25,19 @@ function updatePending() {
|
|
|
24
25
|
const disabledCleanup = cleanupDisabledStatusline();
|
|
25
26
|
if (disabledCleanup.cleaned) process.exit(0);
|
|
26
27
|
|
|
27
|
-
// Only show status in projects that have a code-graph directory.
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
// Only show status in projects that have a code-graph directory. The statusLine
|
|
29
|
+
// config is global, so we must exit silently for non-code-graph directories.
|
|
30
|
+
// Walk UP to the canonical project root (resolveProjectRoot) rather than keying
|
|
31
|
+
// on the bare process.cwd(): when the shell sits in a subdir, the bare-cwd gate
|
|
32
|
+
// either showed a STRAY nested subdir index (monorepo relic — the statusline
|
|
33
|
+
// "oscillating" between root/backend/frontend node counts) or, in a clean subdir
|
|
34
|
+
// with no local index, showed nothing at all. The resolver skips stray nested
|
|
35
|
+
// indexes, so the statusline tracks one DB — the project root — from any subdir.
|
|
36
|
+
const root = resolveProjectRoot(process.cwd());
|
|
37
|
+
if (!root) {
|
|
33
38
|
process.exit(0);
|
|
34
39
|
}
|
|
40
|
+
const codeGraphDir = path.join(root, '.code-graph');
|
|
35
41
|
|
|
36
42
|
// Check for background indexing progress file first
|
|
37
43
|
const progressFile = path.join(codeGraphDir, 'indexing-status.json');
|
|
@@ -108,7 +114,11 @@ let errText = '';
|
|
|
108
114
|
try {
|
|
109
115
|
report = parseReport(execFileSync(bin, ['health-check', '--format', 'json'], {
|
|
110
116
|
timeout: 3000,
|
|
111
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
117
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
118
|
+
// Run the binary FROM the resolved root so its own project-root resolution
|
|
119
|
+
// lands on the same DB the gate above picked (a subdir cwd would otherwise
|
|
120
|
+
// re-resolve to a stray nested index inside the binary).
|
|
121
|
+
cwd: root
|
|
112
122
|
}).toString());
|
|
113
123
|
} catch (e) {
|
|
114
124
|
// health-check exits NON-ZERO on an unhealthy/empty index but still writes the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdsrs/code-graph",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.75.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.
|
|
39
|
-
"@sdsrs/code-graph-linux-arm64": "0.
|
|
40
|
-
"@sdsrs/code-graph-darwin-x64": "0.
|
|
41
|
-
"@sdsrs/code-graph-darwin-arm64": "0.
|
|
42
|
-
"@sdsrs/code-graph-win32-x64": "0.
|
|
38
|
+
"@sdsrs/code-graph-linux-x64": "0.75.1",
|
|
39
|
+
"@sdsrs/code-graph-linux-arm64": "0.75.1",
|
|
40
|
+
"@sdsrs/code-graph-darwin-x64": "0.75.1",
|
|
41
|
+
"@sdsrs/code-graph-darwin-arm64": "0.75.1",
|
|
42
|
+
"@sdsrs/code-graph-win32-x64": "0.75.1"
|
|
43
43
|
}
|
|
44
44
|
}
|