@sdsrs/code-graph 0.160.0 → 0.161.0
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 +51 -2
- package/claude-plugin/scripts/post-grep-inject.js +18 -1
- package/claude-plugin/scripts/pre-grep-guide.js +120 -2
- package/claude-plugin/templates/code-graph-snapshot.yml +1 -1
- package/package.json +6 -6
|
@@ -347,10 +347,45 @@ function runGrepAnswer(opts = {}) {
|
|
|
347
347
|
* `no-binary` distinguishes a missing/unlocatable binary from a runtime
|
|
348
348
|
* `unavailable`, so the deny funnel can see a dark flagship answer-in-deny.
|
|
349
349
|
*/
|
|
350
|
+
/**
|
|
351
|
+
* The files of the definitions a `show` printed: each definition opens with an
|
|
352
|
+
* unindented header `<kind> <name> <path>:<start>-<end> <signature>`, its body
|
|
353
|
+
* indented below. null when an unindented line is not such a header, so a
|
|
354
|
+
* caller that must account for every definition can refuse.
|
|
355
|
+
*/
|
|
356
|
+
function showDefinitionFiles(out) {
|
|
357
|
+
const defs = [];
|
|
358
|
+
for (const line of out.split('\n')) {
|
|
359
|
+
if (line === '' || /^\s/.test(line)) continue;
|
|
360
|
+
const m = /^(\S+) \S+ {2}(\S+):\d+-\d+(?: |$)/.exec(line);
|
|
361
|
+
if (!m) return null;
|
|
362
|
+
defs.push({ kind: m[1], file: m[2] });
|
|
363
|
+
}
|
|
364
|
+
return defs.length ? defs : null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** Whether a root-relative file lies under a grep path ('' = the whole root). */
|
|
368
|
+
function pathWithin(file, within) {
|
|
369
|
+
const w = within.replace(/^(\.\/)+/, '').replace(/\/+$/, '');
|
|
370
|
+
return w === '' || w === '.' || file === w || file.startsWith(w + '/');
|
|
371
|
+
}
|
|
372
|
+
|
|
350
373
|
function runShowAnswer(opts = {}) {
|
|
351
374
|
const {
|
|
352
375
|
cwd,
|
|
353
376
|
symbols,
|
|
377
|
+
// A grep path the answer must stay inside (D#125 #2): `show` prints every
|
|
378
|
+
// same-named definition in the project. With `within`, a symbol is answered
|
|
379
|
+
// only when EVERY definition `show` prints lies inside the path and is one
|
|
380
|
+
// the grep's declaration keyword matches (`kinds`: symbol → the label and
|
|
381
|
+
// file-extension shapes it matches); a symbol with no definition inside is
|
|
382
|
+
// skipped (the grep finds none either), and any other mix refuses the whole
|
|
383
|
+
// answer. Narrowing with `show --file` was tried and removed: the label is
|
|
384
|
+
// coarser than the keyword, so a narrowed answer dropped real matches, and
|
|
385
|
+
// the inject printed the unnarrowed text (review of D#125, round 2).
|
|
386
|
+
// undefined: unscoped, as before.
|
|
387
|
+
within,
|
|
388
|
+
kinds,
|
|
354
389
|
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
355
390
|
maxBytes = DEFAULT_MAX_BYTES,
|
|
356
391
|
} = opts;
|
|
@@ -365,6 +400,8 @@ function runShowAnswer(opts = {}) {
|
|
|
365
400
|
// Which symbols actually resolved — the rewrite re-runs exactly these, so a
|
|
366
401
|
// symbol that printed nothing here cannot turn into an exit-1 in the Bash call.
|
|
367
402
|
const resolved = [];
|
|
403
|
+
// The argv that re-runs each resolved symbol, scoped when `within` is set.
|
|
404
|
+
const argvs = [];
|
|
368
405
|
for (const sym of symbols.slice(0, 3)) {
|
|
369
406
|
if (typeof sym !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(sym)) continue;
|
|
370
407
|
const res = runCg(binary, ['show', sym], { cwd, timeoutMs });
|
|
@@ -383,12 +420,24 @@ function runShowAnswer(opts = {}) {
|
|
|
383
420
|
if (classifyRun(res, { exitOneIsNoHits: false }) !== 'ok') continue;
|
|
384
421
|
const out = (res.stdout || '').trim();
|
|
385
422
|
if (isEmptyAnswer(out)) continue;
|
|
386
|
-
|
|
423
|
+
const argv = ['show', sym];
|
|
424
|
+
if (within !== undefined) {
|
|
425
|
+
const defs = showDefinitionFiles(out);
|
|
426
|
+
if (!defs) return { status: 'unavailable', reason: 'scope' };
|
|
427
|
+
if (!defs.some((d) => pathWithin(d.file, within))) continue;
|
|
428
|
+
const shapes = (kinds && kinds[sym]) || [];
|
|
429
|
+
const ext = (f) => (f.match(/\.([A-Za-z0-9]+)$/) || [])[1];
|
|
430
|
+
const matches = (d) => pathWithin(d.file, within)
|
|
431
|
+
&& shapes.some((k) => k.labels.includes(d.kind) && k.exts.includes(ext(d.file)));
|
|
432
|
+
if (!defs.every(matches)) return { status: 'unavailable', reason: 'scope' };
|
|
433
|
+
}
|
|
434
|
+
parts.push(`$ code-graph-mcp ${argv.join(' ')}\n${out}`);
|
|
387
435
|
resolved.push(sym);
|
|
436
|
+
argvs.push(argv);
|
|
388
437
|
}
|
|
389
438
|
if (parts.length === 0) return { status: 'no-hits' };
|
|
390
439
|
const { text, truncated } = truncateAtLine(parts.join('\n\n'), maxBytes);
|
|
391
|
-
return { status: 'hits', text, truncated, symbols: resolved };
|
|
440
|
+
return { status: 'hits', text, truncated, symbols: resolved, argvs };
|
|
392
441
|
} catch {
|
|
393
442
|
return { status: 'unavailable' };
|
|
394
443
|
}
|
|
@@ -52,6 +52,11 @@ const {
|
|
|
52
52
|
operandMatches,
|
|
53
53
|
firstShellClause,
|
|
54
54
|
normalizeCommandPaths,
|
|
55
|
+
rootOnlyInSearchPath,
|
|
56
|
+
grepSearchesNoDirectory,
|
|
57
|
+
isDirectory,
|
|
58
|
+
declKindsBySymbol,
|
|
59
|
+
extractPatterns,
|
|
55
60
|
rebaseRelativePaths,
|
|
56
61
|
resolveProjectRoot,
|
|
57
62
|
} = require('./pre-grep-guide');
|
|
@@ -288,6 +293,11 @@ function runMain() {
|
|
|
288
293
|
if (segCwd === null) return;
|
|
289
294
|
if ((segCwd !== root || relPrefix)
|
|
290
295
|
&& !operandMatches(firstShellClause(rawSegs[idx]), extractSearchPath(segment), root, segCwd)) return;
|
|
296
|
+
// D#125 #1 — the root strip rewrote a pattern that held the root.
|
|
297
|
+
if (!rootOnlyInSearchPath(rawSegs[idx], root, extractSearchPath(segment))) return;
|
|
298
|
+
// D#125 #3 — a plain grep without -r searched no directory.
|
|
299
|
+
if (grepSearchesNoDirectory(firstShellClause(segment), extractSearchPath(segment),
|
|
300
|
+
(t) => isDirectory(path.resolve(root, t)))) return;
|
|
291
301
|
// Run the answer exactly like the deny path.
|
|
292
302
|
const rawPattern = pickBlockPattern(segment);
|
|
293
303
|
// Grep-response gate (2026-07-03 audit: 18/18 injects were 0 CONSUMED because they
|
|
@@ -382,7 +392,14 @@ function runMain() {
|
|
|
382
392
|
if (answer.status !== 'hits') {
|
|
383
393
|
if (block.mode === 'show') {
|
|
384
394
|
answeredMode = 'show';
|
|
385
|
-
|
|
395
|
+
// Scoped like the rewrite: the grep's path and declaration kinds; a grep
|
|
396
|
+
// with a file filter gets the grep answer, which honors it.
|
|
397
|
+
answer = flags.includes('-g') || flags.includes('-t')
|
|
398
|
+
? { status: 'unavailable' }
|
|
399
|
+
: runShowAnswer({
|
|
400
|
+
cwd: root, symbols: block.symbols, within: searchPath ?? '',
|
|
401
|
+
kinds: declKindsBySymbol(extractPatterns(segment)),
|
|
402
|
+
});
|
|
386
403
|
if (answer.status !== 'hits' && pattern) {
|
|
387
404
|
answeredMode = 'grep';
|
|
388
405
|
answer = runGrepAnswer({ cwd: root, pattern, searchPath, flags });
|
|
@@ -376,6 +376,38 @@ function clauseWords(s) {
|
|
|
376
376
|
// name the exact symbols the model wants to READ — extract them for `show`.
|
|
377
377
|
const DECL_SYMBOL = /(?:fn|def|class|function|struct|impl|trait)\s+([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
378
378
|
|
|
379
|
+
// What a `show` definition must be for the grep's declaration keyword to match
|
|
380
|
+
// its line: the kind label `show` prints (src/cli/symbols.rs
|
|
381
|
+
// format_node_compact) AND a file of a language that spells the declaration
|
|
382
|
+
// with that keyword. The label alone is coarser than the keyword — `fn` is also
|
|
383
|
+
// a Python `def` and a TS method, and a Swift `struct` is labelled `class`
|
|
384
|
+
// (review of D#125, round 2). `impl` names no definition of its own.
|
|
385
|
+
const DECL_KEYWORD_SHAPES = {
|
|
386
|
+
fn: { labels: ['fn'], exts: ['rs'] },
|
|
387
|
+
def: { labels: ['fn'], exts: ['py', 'rb'] },
|
|
388
|
+
function: { labels: ['fn'], exts: ['js', 'mjs', 'cjs', 'jsx', 'ts', 'tsx', 'php', 'sh', 'bash'] },
|
|
389
|
+
class: {
|
|
390
|
+
labels: ['class'],
|
|
391
|
+
exts: ['py', 'js', 'mjs', 'cjs', 'jsx', 'ts', 'tsx', 'java', 'kt', 'cs', 'php', 'rb', 'dart',
|
|
392
|
+
'cpp', 'cc', 'hpp', 'h', 'scala'],
|
|
393
|
+
},
|
|
394
|
+
struct: { labels: ['struct'], exts: ['rs', 'go', 'c', 'h', 'cpp', 'cc', 'hpp', 'cs'] },
|
|
395
|
+
trait: { labels: ['trait', 'iface'], exts: ['rs', 'php', 'scala'] },
|
|
396
|
+
impl: { labels: [], exts: [] },
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
// Symbol → the (label, extension) shapes its declaration keywords match
|
|
400
|
+
// (`fn foo\|def foo` → foo: Rust fn, Python/Ruby def).
|
|
401
|
+
function declKindsBySymbol(patterns) {
|
|
402
|
+
const out = {};
|
|
403
|
+
for (const p of patterns) {
|
|
404
|
+
for (const m of p.matchAll(/\b(fn|def|class|function|struct|impl|trait)\s+([A-Za-z_][A-Za-z0-9_]*)/g)) {
|
|
405
|
+
(out[m[2]] = out[m[2]] || []).push(DECL_KEYWORD_SHAPES[m[1]]);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return out;
|
|
409
|
+
}
|
|
410
|
+
|
|
379
411
|
function extractDeclSymbols(patterns) {
|
|
380
412
|
const out = [];
|
|
381
413
|
for (const p of patterns) {
|
|
@@ -656,6 +688,30 @@ function normalizeCommandPaths(cmd, cwd) {
|
|
|
656
688
|
return cmd.split(cwd.endsWith('/') ? cwd : cwd + '/').join('');
|
|
657
689
|
}
|
|
658
690
|
|
|
691
|
+
// D#125 #1 — the strip above cannot tell a path operand from a PATTERN, and
|
|
692
|
+
// strips both: `grep -rn "<root>/Foo" src/` searches the literal text
|
|
693
|
+
// `<root>/Foo`, but was answered for `Foo`. Re-picking the pattern with the
|
|
694
|
+
// root marked was tried first and missed shapes where the mark changes which
|
|
695
|
+
// word is picked (`"<root>/def foo"`, `"abc_<root>/def"`: review of D#125). The
|
|
696
|
+
// narrow form: every shell word that holds `<root>/` must BE the search path
|
|
697
|
+
// the answer scopes to; anything else runs as typed.
|
|
698
|
+
function rootOnlyInSearchPath(cmd, cwd, target) {
|
|
699
|
+
if (!cmd || typeof cmd !== 'string') return true;
|
|
700
|
+
if (!cwd || typeof cwd !== 'string' || cwd === '/') return true;
|
|
701
|
+
const prefix = cwd.endsWith('/') ? cwd : cwd + '/';
|
|
702
|
+
if (!cmd.includes(prefix)) return true;
|
|
703
|
+
// Round 2: counting words was not enough — a lone root-holding PATTERN whose
|
|
704
|
+
// stripped text equals the path (`grep -rn "<root>/src/x" src/x`) passed. The
|
|
705
|
+
// rewrite grammar names which word is the path operand: accept only a command
|
|
706
|
+
// it reads, with the root once, in that operand, and nowhere in the pattern.
|
|
707
|
+
if (cmd.split(prefix).length !== 2) return false;
|
|
708
|
+
const plan = rewritePlan(firstShellClause(cmd), { isDir: () => true });
|
|
709
|
+
if (!plan || plan.target === undefined || plan.pattern.includes(prefix)) return false;
|
|
710
|
+
if (!plan.target.startsWith(prefix)) return false;
|
|
711
|
+
const norm = (p) => p.replace(/^(?:\.\/)+/, '').replace(/\/+$/, '');
|
|
712
|
+
return target !== undefined && norm(plan.target.slice(prefix.length)) === norm(target);
|
|
713
|
+
}
|
|
714
|
+
|
|
659
715
|
// v0.48 — subdir-cwd fix; v0.49 — extracted to project-root.js so the read
|
|
660
716
|
// hook shares it. Re-exported below for test/back-compat.
|
|
661
717
|
const { resolveProjectRoot } = require('./project-root');
|
|
@@ -1245,6 +1301,7 @@ function rewritePlan(cmd, { isDir = looksLikeDir } = {}) {
|
|
|
1245
1301
|
let context = false;
|
|
1246
1302
|
let endOfFlags = false;
|
|
1247
1303
|
let include = false;
|
|
1304
|
+
let recursive = false;
|
|
1248
1305
|
for (; i < words.length; i++) {
|
|
1249
1306
|
const w = words[i];
|
|
1250
1307
|
if (!w.anyQuoted && (w.text === '2>&1' || w.text === '2>/dev/null')) continue;
|
|
@@ -1267,6 +1324,7 @@ function rewritePlan(cmd, { isDir = looksLikeDir } = {}) {
|
|
|
1267
1324
|
const name = w.text.slice(2, eq === -1 ? undefined : eq);
|
|
1268
1325
|
if (!ALLOWED_LONG[verb].has(name)) return null;
|
|
1269
1326
|
if (name === 'include') include = true;
|
|
1327
|
+
if (name === 'recursive') recursive = true;
|
|
1270
1328
|
if (VALUE_LONG.has(name) && eq === -1) {
|
|
1271
1329
|
const v = words[++i];
|
|
1272
1330
|
if (!valueWordOk(v, false) || (name === 'include' && /^!|\{/.test(v.text))) return null;
|
|
@@ -1290,6 +1348,7 @@ function rewritePlan(cmd, { isDir = looksLikeDir } = {}) {
|
|
|
1290
1348
|
}
|
|
1291
1349
|
if (!allowed.includes(ch)) return null;
|
|
1292
1350
|
if (ch === 'i' || ch === 's') caseFlag = true;
|
|
1351
|
+
if (ch === 'r' || ch === 'R') recursive = true;
|
|
1293
1352
|
}
|
|
1294
1353
|
}
|
|
1295
1354
|
if (operands.length === 0 || operands.length > 2) return null;
|
|
@@ -1307,12 +1366,38 @@ function rewritePlan(cmd, { isDir = looksLikeDir } = {}) {
|
|
|
1307
1366
|
// `grep --include='*.py' X src/a.rs` finds nothing; rg — and so cg — searches
|
|
1308
1367
|
// a named file whatever its -g says (D#78). Equivalent only for a directory.
|
|
1309
1368
|
if (include && target && !isDir(target.text)) return null;
|
|
1369
|
+
// Plain grep without -r reads no directory ("Is a directory") and, with no
|
|
1370
|
+
// path, reads stdin; the answer would search the tree (D#125 #3). rg and ag
|
|
1371
|
+
// recurse by default, and git grep searches the tracked tree.
|
|
1372
|
+
if (verb === 'grep' && !recursive && (!target || isDir(target.text))) return null;
|
|
1310
1373
|
// ag is smart-case by default: an all-lowercase pattern matches any case,
|
|
1311
1374
|
// and cg's search is case-sensitive, so the answer would find less.
|
|
1312
1375
|
if (verb === 'ag' && !caseFlag && !/[A-Z]/.test(pattern.text)) return null;
|
|
1313
1376
|
return { pattern: pattern.text, target: target ? target.text : undefined, context };
|
|
1314
1377
|
}
|
|
1315
1378
|
|
|
1379
|
+
// D#125 #3 for the PostToolUse side, which has no rewrite grammar: a plain
|
|
1380
|
+
// `grep` with no recursion flag searches no directory ("Is a directory" under
|
|
1381
|
+
// GNU grep) and, with no path, reads its input.
|
|
1382
|
+
function grepSearchesNoDirectory(clause, target, isDir) {
|
|
1383
|
+
const words = shellWords(clause || '');
|
|
1384
|
+
if (!words || words.length === 0 || words[0].anyQuoted || words[0].text !== 'grep') return false;
|
|
1385
|
+
for (let k = 1; k < words.length; k++) {
|
|
1386
|
+
const w = words[k];
|
|
1387
|
+
if (w.op || w.startsQuoted) continue;
|
|
1388
|
+
if (w.text === '--') break;
|
|
1389
|
+
if (/^--(?:recursive|dereference-recursive|directories=recurse)$/.test(w.text)) return false;
|
|
1390
|
+
if (w.text === '-d' && words[k + 1] && words[k + 1].text === 'recurse') return false;
|
|
1391
|
+
if (/^-[^-]/.test(w.text)) {
|
|
1392
|
+
for (const ch of w.text.slice(1)) {
|
|
1393
|
+
if (ch === 'r' || ch === 'R') return false;
|
|
1394
|
+
if ('ABCefmdDg'.includes(ch)) break; // the rest is that flag's value
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
return target === undefined || isDir(target);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1316
1401
|
// Does `rootTarget`, run from the root, name the path the grep clause's own
|
|
1317
1402
|
// operand names from `cwd` — the directory the clause ran in? The raw clause is
|
|
1318
1403
|
// parsed with the rewrite grammar; an absolute operand resolves the same from
|
|
@@ -1401,6 +1486,10 @@ function rewriteMatchesBlock(plan, block, cmd, rawPattern) {
|
|
|
1401
1486
|
if (block.mode === 'show') {
|
|
1402
1487
|
const f = cgFlagSet(extractCgFlags(cmd));
|
|
1403
1488
|
if (f.has('-l') || f.has('-c')) return false;
|
|
1489
|
+
// `show` has no file filter: `--include`/`-g`/`-t` excluded definitions it
|
|
1490
|
+
// would print (review of D#125).
|
|
1491
|
+
const all = extractCgFlags(cmd);
|
|
1492
|
+
if (all.includes('-g') || all.includes('-t')) return false;
|
|
1404
1493
|
// show answers at most three symbols; a fourth would silently vanish.
|
|
1405
1494
|
if (extractDeclSymbols(extractPatterns(cmd)).length > 3) return false;
|
|
1406
1495
|
}
|
|
@@ -1456,18 +1545,37 @@ const BRE_SWAPPED = '(){}+?|';
|
|
|
1456
1545
|
const BRE_SAME_ESCAPE = 'wWsSbB<>.*[]^$\\/';
|
|
1457
1546
|
const POSIX_CLASSES = new Set(['alpha', 'digit', 'alnum', 'upper', 'lower', 'space',
|
|
1458
1547
|
'punct', 'xdigit', 'blank', 'cntrl', 'graph', 'print']);
|
|
1548
|
+
// An unescaped `^` is an anchor only where an alternative starts (the pattern's
|
|
1549
|
+
// start, after `\(` or `\|`), and `$` only where one ends (the pattern's end,
|
|
1550
|
+
// before `\)` or `\|`); elsewhere BRE reads them as literals and rust regex as
|
|
1551
|
+
// anchors — `getUser\|$user_id` searched the text `$user_id` (D#125 #6). No
|
|
1552
|
+
// translation: the grep runs.
|
|
1459
1553
|
function breToRustRegex(pattern) {
|
|
1460
1554
|
let out = '';
|
|
1555
|
+
let altStart = true;
|
|
1556
|
+
let anchor = false;
|
|
1461
1557
|
for (let i = 0; i < pattern.length; i++) {
|
|
1462
1558
|
const c = pattern[i];
|
|
1559
|
+
const wasAltStart = altStart;
|
|
1560
|
+
const wasAnchor = anchor;
|
|
1561
|
+
altStart = false;
|
|
1562
|
+
anchor = c === '^' && wasAltStart;
|
|
1463
1563
|
if (c === '\\' && i + 1 < pattern.length) {
|
|
1464
1564
|
const n = pattern[++i];
|
|
1465
1565
|
if (n === '(' && pattern.startsWith('\\)', i + 1)) return null;
|
|
1466
1566
|
if (BRE_SWAPPED.includes(n)) out += n;
|
|
1467
1567
|
else if (BRE_SAME_ESCAPE.includes(n)) out += c + n;
|
|
1468
1568
|
else return null;
|
|
1569
|
+
altStart = n === '(' || n === '|';
|
|
1469
1570
|
continue;
|
|
1470
1571
|
}
|
|
1572
|
+
if (c === '^' && !wasAltStart) return null;
|
|
1573
|
+
// A `*` where an alternative starts, or right after its `^`, repeats
|
|
1574
|
+
// nothing: BRE reads it as a literal, rust regex as an error or a repeat of
|
|
1575
|
+
// the anchor (review of D#125).
|
|
1576
|
+
if (c === '*' && (wasAltStart || (wasAnchor && out.endsWith('^')))) return null;
|
|
1577
|
+
if (c === '$' && i + 1 < pattern.length
|
|
1578
|
+
&& !pattern.startsWith('\\)', i + 1) && !pattern.startsWith('\\|', i + 1)) return null;
|
|
1471
1579
|
if (BRE_SWAPPED.includes(c)) { out += '\\' + c; continue; }
|
|
1472
1580
|
if (c !== '[') { out += c; continue; }
|
|
1473
1581
|
// `]` right after `[` or `[^` is a member, not the end.
|
|
@@ -1650,6 +1758,7 @@ function runMain() {
|
|
|
1650
1758
|
// forwarding `-F` without this guard would make it search the wrong text.
|
|
1651
1759
|
const cgFlags = extractCgFlags(cmd);
|
|
1652
1760
|
const rawGrepPattern = pickBlockPattern(cmd);
|
|
1761
|
+
if (!rootOnlyInSearchPath(rawCmd, root, extractSearchPath(cmd))) return;
|
|
1653
1762
|
const grepPattern = cgFlagSet(cgFlags).has('-F')
|
|
1654
1763
|
? rawGrepPattern
|
|
1655
1764
|
: translateBreToRg(cmd, rawGrepPattern);
|
|
@@ -1728,7 +1837,12 @@ function runMain() {
|
|
|
1728
1837
|
// No fallback to a grep answer: a context grep (`-A5`) asked for the
|
|
1729
1838
|
// body, and the grep rewrite would drop the context silently (pre-ship
|
|
1730
1839
|
// review round 2). A show miss lets the raw grep run.
|
|
1731
|
-
|
|
1840
|
+
// Scoped to the grep's path: `show` alone answers the whole project
|
|
1841
|
+
// (D#125 #2 — `grep -A3 "fn f" lib/` was answered from src/).
|
|
1842
|
+
answer = runShowAnswer({
|
|
1843
|
+
cwd: root, symbols: block.symbols, within: searchPath ?? '',
|
|
1844
|
+
kinds: declKindsBySymbol(extractPatterns(cmd)),
|
|
1845
|
+
});
|
|
1732
1846
|
} else if (pattern) {
|
|
1733
1847
|
answer = runGrepAnswer({ cwd: root, pattern, searchPath, flags });
|
|
1734
1848
|
}
|
|
@@ -1800,7 +1914,7 @@ function runMain() {
|
|
|
1800
1914
|
// stderr; the grep being replaced has no cap (round 3 M4). Only here, not
|
|
1801
1915
|
// in the in-hook answer, which is a has-hits probe.
|
|
1802
1916
|
const argvList = answeredMode === 'show'
|
|
1803
|
-
? answer.
|
|
1917
|
+
? answer.argvs
|
|
1804
1918
|
: [[...args.slice(0, 1), '-m', '0', ...args.slice(1)]];
|
|
1805
1919
|
const command = buildRewriteCommand(argvList, {
|
|
1806
1920
|
invocation: shellQuoteArg(resolveAnswerBinary({})),
|
|
@@ -1861,6 +1975,10 @@ module.exports = {
|
|
|
1861
1975
|
isRevisionScopedGitGrep, // v0.71 — git grep --cached/treeish exclusion
|
|
1862
1976
|
extractSearchPath, // v0.47.0 — deny-with-answer
|
|
1863
1977
|
normalizeCommandPaths, // v0.47.1 — abs-path matcher fix
|
|
1978
|
+
rootOnlyInSearchPath, // D#125 #1
|
|
1979
|
+
isDirectory,
|
|
1980
|
+
grepSearchesNoDirectory, // D#125 #3
|
|
1981
|
+
declKindsBySymbol, // D#125 #2 review
|
|
1864
1982
|
resolveProjectRoot, // v0.48 — subdir-cwd dark fix
|
|
1865
1983
|
rebaseRelativePaths, // v0.48 — subdir-cwd dark fix
|
|
1866
1984
|
commandHasBypass, // v0.48 — bypass funnel visibility
|
|
@@ -35,7 +35,7 @@ jobs:
|
|
|
35
35
|
node-version: '20'
|
|
36
36
|
- name: Build snapshot
|
|
37
37
|
run: |
|
|
38
|
-
npx -y -p @sdsrs/code-graph@0.
|
|
38
|
+
npx -y -p @sdsrs/code-graph@0.161.0 code-graph-mcp snapshot create --out snapshot.db
|
|
39
39
|
zstd -9 snapshot.db -o snapshot.db.zst
|
|
40
40
|
mv snapshot.db.zst "code-graph-snapshot-${GITHUB_SHA:0:7}.db.zst"
|
|
41
41
|
- name: Upload to release
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdsrs/code-graph",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.161.0",
|
|
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.161.0",
|
|
39
|
+
"@sdsrs/code-graph-linux-arm64": "0.161.0",
|
|
40
|
+
"@sdsrs/code-graph-darwin-x64": "0.161.0",
|
|
41
|
+
"@sdsrs/code-graph-darwin-arm64": "0.161.0",
|
|
42
|
+
"@sdsrs/code-graph-win32-x64": "0.161.0"
|
|
43
43
|
}
|
|
44
44
|
}
|