@sdsrs/code-graph 0.159.0 → 0.160.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.
|
@@ -47,6 +47,9 @@ const {
|
|
|
47
47
|
cgFlagSet,
|
|
48
48
|
extractSearchPath,
|
|
49
49
|
bareSourceTarget,
|
|
50
|
+
segmentCwd,
|
|
51
|
+
segmentSeparators,
|
|
52
|
+
operandMatches,
|
|
50
53
|
firstShellClause,
|
|
51
54
|
normalizeCommandPaths,
|
|
52
55
|
rebaseRelativePaths,
|
|
@@ -273,6 +276,18 @@ function runMain() {
|
|
|
273
276
|
// closes, so a bare dir is answered only when the grep is the FIRST segment.
|
|
274
277
|
if (bareSourceTarget(firstShellClause(segment))
|
|
275
278
|
&& (relPrefix || splitTopLevelSegments(cmd)[0] !== segment)) return;
|
|
279
|
+
// D#76 — the answer searches `extractSearchPath(segment)` from the root. The
|
|
280
|
+
// segment ran where the shell was by then: its own cwd, moved by any `cd`
|
|
281
|
+
// before it. Segments are read from the RAW command (normalizing strips the
|
|
282
|
+
// root from `cd <root>/x` too), which splits into the same segments.
|
|
283
|
+
const segs = splitTopLevelSegments(cmd);
|
|
284
|
+
const rawSegs = splitTopLevelSegments(rawCmd);
|
|
285
|
+
const idx = segs.indexOf(segment);
|
|
286
|
+
if (rawSegs.length !== segs.length) return;
|
|
287
|
+
const segCwd = segmentCwd(rawSegs, idx, shellCwd, { seps: segmentSeparators(rawCmd) });
|
|
288
|
+
if (segCwd === null) return;
|
|
289
|
+
if ((segCwd !== root || relPrefix)
|
|
290
|
+
&& !operandMatches(firstShellClause(rawSegs[idx]), extractSearchPath(segment), root, segCwd)) return;
|
|
276
291
|
// Run the answer exactly like the deny path.
|
|
277
292
|
const rawPattern = pickBlockPattern(segment);
|
|
278
293
|
// Grep-response gate (2026-07-03 audit: 18/18 injects were 0 CONSUMED because they
|
|
@@ -255,8 +255,18 @@ function extractPatterns(cmd) {
|
|
|
255
255
|
const stripped = firstShellClause(cmd).replace(VERB_STRIP, '');
|
|
256
256
|
// Collect every quoted argument — first one is the pattern in standard grep
|
|
257
257
|
// usage; subsequent ones (e.g. `-e "second"`) are also patterns or filter
|
|
258
|
-
// expressions and worth screening too.
|
|
259
|
-
|
|
258
|
+
// expressions and worth screening too. Whole words, as the shell passes them
|
|
259
|
+
// (D#63): `"Foo"'BarBaz'` is `FooBarBaz`, not two patterns; a flag's attached
|
|
260
|
+
// quoted value (`-e"Foo"`, `--include='*.rs'`) is the value. A clause the
|
|
261
|
+
// shell would reject, or whose quoted words it expands (`"$MAX"`, D#64), has
|
|
262
|
+
// no pattern we can read.
|
|
263
|
+
const words = clauseWords(stripped);
|
|
264
|
+
if (!words) return [];
|
|
265
|
+
const quoted = words.filter((w) => w.quotedFrom !== -1);
|
|
266
|
+
if (quoted.some((w) => w.expands)) return [];
|
|
267
|
+
return quoted
|
|
268
|
+
.map((w) => (!w.startsQuoted && w.text[0] === '-' ? w.text.slice(w.quotedFrom) : w.text))
|
|
269
|
+
.filter(Boolean);
|
|
260
270
|
}
|
|
261
271
|
|
|
262
272
|
/**
|
|
@@ -299,6 +309,69 @@ function quotedSpans(s) {
|
|
|
299
309
|
return spans;
|
|
300
310
|
}
|
|
301
311
|
|
|
312
|
+
/**
|
|
313
|
+
* The words of a shell clause, split and unquoted the way quotedSpans reads
|
|
314
|
+
* quotes: whitespace separates words only outside quotes, and adjacent quoted
|
|
315
|
+
* and bare parts join into one word (`-r"l"` is `-rl`, `"Foo"'Bar'` is
|
|
316
|
+
* `FooBar`). Per word:
|
|
317
|
+
* - `startsQuoted`: the first character was a quote — an argument, never a
|
|
318
|
+
* flag, so `"-l"` searches for the string `-l`;
|
|
319
|
+
* - `quotedFrom`: where in `text` the first quoted part begins (-1 if none),
|
|
320
|
+
* so `-e"Foo"` yields the value `Foo`;
|
|
321
|
+
* - `expands`: the shell substitutes something into it — `$NAME`, `${…}`,
|
|
322
|
+
* `$(…)`, a backtick, or `$'…'`/`$"…"` quoting — outside single quotes,
|
|
323
|
+
* so `text` is not what the command received.
|
|
324
|
+
* Returns null for an unterminated quote: the shell would not run it at all.
|
|
325
|
+
*
|
|
326
|
+
* A whitespace split read `-l` out of `grep -rn "FooBar -l x" src/` and the
|
|
327
|
+
* rewrite answered with a file list (D#62).
|
|
328
|
+
* @returns {{text: string, startsQuoted: boolean, quotedFrom: number, expands: boolean}[]|null}
|
|
329
|
+
*/
|
|
330
|
+
function clauseWords(s) {
|
|
331
|
+
const words = [];
|
|
332
|
+
if (typeof s !== 'string') return words;
|
|
333
|
+
let cur = null;
|
|
334
|
+
// Inside double quotes `$"` is a literal `$` before the closing quote
|
|
335
|
+
// (`"FooBar$"`); bare, `$'…'` and `$"…"` are quoting forms that translate.
|
|
336
|
+
const expandsAt = (k, inDouble) => s[k] === '`'
|
|
337
|
+
|| (s[k] === '$' && k + 1 < s.length
|
|
338
|
+
&& (inDouble ? /[A-Za-z0-9_{(@*#?$!-]/ : /[A-Za-z0-9_{(@*#?$!'"-]/).test(s[k + 1]));
|
|
339
|
+
for (let i = 0; i < s.length; i++) {
|
|
340
|
+
const c = s[i];
|
|
341
|
+
if (c === ' ' || c === '\t' || c === '\n') {
|
|
342
|
+
if (cur) words.push(cur);
|
|
343
|
+
cur = null;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (!cur) cur = { text: '', startsQuoted: c === '"' || c === "'", quotedFrom: -1, expands: false };
|
|
347
|
+
if (c === '\\') {
|
|
348
|
+
if (i + 1 < s.length && s[i + 1] !== '\n') cur.text += s[i + 1];
|
|
349
|
+
i++;
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (c !== '"' && c !== "'") {
|
|
353
|
+
if (expandsAt(i, false)) cur.expands = true;
|
|
354
|
+
cur.text += c;
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (cur.quotedFrom === -1) cur.quotedFrom = cur.text.length;
|
|
358
|
+
let j = i + 1;
|
|
359
|
+
for (; j < s.length && s[j] !== c; j++) {
|
|
360
|
+
if (c === '"' && s[j] === '\\' && j + 1 < s.length && '"\\$`\n'.includes(s[j + 1])) {
|
|
361
|
+
j++;
|
|
362
|
+
if (s[j] === '\n') continue;
|
|
363
|
+
} else if (c === '"' && expandsAt(j, true)) {
|
|
364
|
+
cur.expands = true;
|
|
365
|
+
}
|
|
366
|
+
cur.text += s[j];
|
|
367
|
+
}
|
|
368
|
+
if (j >= s.length) return null;
|
|
369
|
+
i = j;
|
|
370
|
+
}
|
|
371
|
+
if (cur) words.push(cur);
|
|
372
|
+
return words;
|
|
373
|
+
}
|
|
374
|
+
|
|
302
375
|
// Declaration anchors inside a pattern (`def cascade_failure|class TaskState`)
|
|
303
376
|
// name the exact symbols the model wants to READ — extract them for `show`.
|
|
304
377
|
const DECL_SYMBOL = /(?:fn|def|class|function|struct|impl|trait)\s+([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
@@ -475,17 +548,21 @@ const CG_FLAG_ORDER = ['-i', '-w', '-F', '-l', '-c'];
|
|
|
475
548
|
* file list was asked for (field report 2026-09-08).
|
|
476
549
|
*
|
|
477
550
|
* Scanning rules that matter: only the grep's OWN clause (a tail command's
|
|
478
|
-
* `-l` is not this grep's),
|
|
479
|
-
*
|
|
551
|
+
* `-l` is not this grep's), words as the shell splits them (clauseWords — a
|
|
552
|
+
* quoted pattern holding ` -l ` is one word), and a word that STARTS quoted is
|
|
553
|
+
* an argument, not a flag — `grep "-l" src/` searches for the string `-l`.
|
|
480
554
|
*/
|
|
481
555
|
function extractCgFlags(cmd) {
|
|
482
556
|
if (!cmd || typeof cmd !== 'string') return [];
|
|
483
557
|
const clause = firstShellClause(cmd);
|
|
484
558
|
const isRg = RG_VERB.test((clause.match(GREP_HEAD) || [])[1] || '');
|
|
485
|
-
const
|
|
559
|
+
const words = clauseWords(clause.replace(VERB_STRIP, ''));
|
|
560
|
+
if (!words) return [];
|
|
561
|
+
const toks = words.map((w) => (w.startsQuoted ? '' : w.text));
|
|
486
562
|
const found = new Set();
|
|
487
563
|
const filters = []; // [cgFlag, value] pairs, in the order the user wrote them
|
|
488
|
-
|
|
564
|
+
// A value is read from the word, whatever its quoting: `--include '*.rs'`.
|
|
565
|
+
const valueAt = (k) => (words[k] ? words[k].text : '');
|
|
489
566
|
const addFilter = (flag, value) => {
|
|
490
567
|
if (!value) return;
|
|
491
568
|
// `-g '*.rs' -g '*.rs'` is one filter written twice; cg would honour both
|
|
@@ -494,12 +571,12 @@ function extractCgFlags(cmd) {
|
|
|
494
571
|
};
|
|
495
572
|
for (let i = 0; i < toks.length; i++) {
|
|
496
573
|
const tok = toks[i];
|
|
497
|
-
if (!tok || tok[0]
|
|
574
|
+
if (!tok || tok[0] !== '-') continue;
|
|
498
575
|
if (tok.startsWith('--')) {
|
|
499
576
|
const eq = tok.indexOf('=');
|
|
500
577
|
const name = eq === -1 ? tok : tok.slice(0, eq);
|
|
501
578
|
if (CG_VALUE_FLAGS[name]) {
|
|
502
|
-
addFilter(CG_VALUE_FLAGS[name],
|
|
579
|
+
addFilter(CG_VALUE_FLAGS[name], eq === -1 ? valueAt(i + 1) : tok.slice(eq + 1));
|
|
503
580
|
if (eq === -1) i++; // the value was a separate token — do not rescan it
|
|
504
581
|
continue;
|
|
505
582
|
}
|
|
@@ -515,8 +592,8 @@ function extractCgFlags(cmd) {
|
|
|
515
592
|
const ch = letters[k];
|
|
516
593
|
if (isRg && CG_VALUE_SHORT[ch]) {
|
|
517
594
|
const attached = letters.slice(k + 1);
|
|
518
|
-
addFilter(CG_VALUE_SHORT[ch], attached
|
|
519
|
-
// Consume the value
|
|
595
|
+
addFilter(CG_VALUE_SHORT[ch], attached || valueAt(i + 1));
|
|
596
|
+
// Consume the value word so the next pass cannot read it as a flag
|
|
520
597
|
// cluster: `rg -g -i 'sym' src/` must be `-g` with value `-i`, not `-g`
|
|
521
598
|
// plus a phantom `-i` (round 2 of pre-ship review — the test that named
|
|
522
599
|
// this property used a quoted value, which the quote guard already
|
|
@@ -611,7 +688,10 @@ function rebaseRelativePaths(cmd, relPrefix, rootDir, exists = fs.existsSync) {
|
|
|
611
688
|
// Shell sits outside any known source dir (docs/, target/, …) — don't guess.
|
|
612
689
|
if (!SRC_PATH_TOKEN.test(prefix + '/')) return cmd;
|
|
613
690
|
let verbSeen = false;
|
|
614
|
-
|
|
691
|
+
// Split on whitespace outside quotes only: a quoted pattern is one token and
|
|
692
|
+
// its inner words are never paths (D#63 — `"fn main utils here"` from src/
|
|
693
|
+
// became `"fn main src/utils here"`).
|
|
694
|
+
return splitKeepingQuotes(cmd).map((tok) => {
|
|
615
695
|
if (!tok || /^\s+$/.test(tok)) return tok;
|
|
616
696
|
if (!verbSeen) {
|
|
617
697
|
if (/^(?:env|[A-Za-z_][A-Za-z0-9_]*=\S*)$/.test(tok)) return tok;
|
|
@@ -635,6 +715,36 @@ function rebaseRelativePaths(cmd, relPrefix, rootDir, exists = fs.existsSync) {
|
|
|
635
715
|
}).join('');
|
|
636
716
|
}
|
|
637
717
|
|
|
718
|
+
// Words and the whitespace between them, text unchanged, splitting on
|
|
719
|
+
// whitespace outside quotes only (the quotedSpans rules).
|
|
720
|
+
function splitKeepingQuotes(s) {
|
|
721
|
+
const out = [];
|
|
722
|
+
let cur = '';
|
|
723
|
+
let quote = null;
|
|
724
|
+
for (let i = 0; i < s.length; i++) {
|
|
725
|
+
const c = s[i];
|
|
726
|
+
if (quote) {
|
|
727
|
+
cur += c;
|
|
728
|
+
if (quote === '"' && c === '\\' && i + 1 < s.length) { cur += s[++i]; continue; }
|
|
729
|
+
if (c === quote) quote = null;
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
if (c === '\\' && i + 1 < s.length) { cur += c + s[++i]; continue; }
|
|
733
|
+
if (c === '"' || c === "'") { quote = c; cur += c; continue; }
|
|
734
|
+
if (/\s/.test(c)) {
|
|
735
|
+
if (cur) out.push(cur);
|
|
736
|
+
cur = c;
|
|
737
|
+
while (i + 1 < s.length && /\s/.test(s[i + 1])) cur += s[++i];
|
|
738
|
+
out.push(cur);
|
|
739
|
+
cur = '';
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
cur += c;
|
|
743
|
+
}
|
|
744
|
+
if (cur) out.push(cur);
|
|
745
|
+
return out;
|
|
746
|
+
}
|
|
747
|
+
|
|
638
748
|
// v0.48 — bypass detection on the RAW command. (The deny copy stopped teaching
|
|
639
749
|
// the escape in v0.49, but models that already know it — or learned it from a
|
|
640
750
|
// session summary — must stay visible to the funnel.)
|
|
@@ -763,22 +873,117 @@ function pickBlockPattern(cmd) {
|
|
|
763
873
|
// inside single/double quotes are literal command text, never split points.
|
|
764
874
|
// Returns trimmed, non-empty segments. Shared by post-grep-inject so the
|
|
765
875
|
// PostToolUse path reuses this splitter instead of copying it.
|
|
766
|
-
|
|
876
|
+
//
|
|
877
|
+
// A heredoc body is skipped (D#66): it is data on the reading command's stdin,
|
|
878
|
+
// not commands. Lexed as shell, a Python `\'` in a `<<'PY'` body flipped the
|
|
879
|
+
// quote parity for the rest of the command, and a `grep` line in a script body
|
|
880
|
+
// was folded as if the shell had run it. The `<<WORD` stays in its segment;
|
|
881
|
+
// the lines after that segment's newline, through the terminator line, go.
|
|
882
|
+
//
|
|
883
|
+
// Also read as the shell reads them, because each misreading drops or invents
|
|
884
|
+
// commands: a comment (`#` at the start of a word) runs to the end of its line;
|
|
885
|
+
// `<<<` is a here-string and `<<` inside `((…))` a shift, neither a heredoc;
|
|
886
|
+
// the heredoc delimiter is a whole word with its quoting removed (`E"O"F`,
|
|
887
|
+
// `$'EOF'`, `\EOF` all end at a line `EOF`).
|
|
888
|
+
function readHeredocDelim(cmd, i) {
|
|
889
|
+
let j = i;
|
|
890
|
+
let delim = '';
|
|
891
|
+
while (j < cmd.length && !/[\s;&|<>()]/.test(cmd[j])) {
|
|
892
|
+
const c = cmd[j];
|
|
893
|
+
if (c === '$' && (cmd[j + 1] === "'" || cmd[j + 1] === '"')) { j++; continue; }
|
|
894
|
+
if (c === "'" || c === '"') {
|
|
895
|
+
const close = cmd.indexOf(c, j + 1);
|
|
896
|
+
if (close === -1) return null;
|
|
897
|
+
delim += cmd.slice(j + 1, close);
|
|
898
|
+
j = close + 1;
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
if (c === '\\' && j + 1 < cmd.length) { delim += cmd[j + 1]; j += 2; continue; }
|
|
902
|
+
delim += c;
|
|
903
|
+
j++;
|
|
904
|
+
}
|
|
905
|
+
return delim ? { delim, end: j } : null;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// Stronger of two separators joining the same pair of segments (an empty
|
|
909
|
+
// segment between them collapses): anything conditional beats `;`/newline.
|
|
910
|
+
const SEP_RANK = { '': 0, ';': 1, '\n': 1, '&&': 2, '||': 2, ctrl: 3 };
|
|
911
|
+
function joinSep(a, b) {
|
|
912
|
+
if (a === undefined) return b;
|
|
913
|
+
if (SEP_RANK[a] === 2 && SEP_RANK[b] === 2 && a !== b) return 'ctrl';
|
|
914
|
+
return SEP_RANK[b] > SEP_RANK[a] ? b : a;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Top-level segments with the separator before each: '' (the first), ';',
|
|
919
|
+
* '\n', '&&', '||', or 'ctrl' (after a `do`/`then`/… control word, where
|
|
920
|
+
* whether the segment runs is not a matter of the previous one).
|
|
921
|
+
* @returns {{text: string, sep: string}[]}
|
|
922
|
+
*/
|
|
923
|
+
function splitTopLevelSegmentsWithSeps(cmd) {
|
|
767
924
|
if (!cmd || typeof cmd !== 'string') return [];
|
|
768
925
|
const segs = [];
|
|
769
926
|
let cur = '';
|
|
927
|
+
let sep = '';
|
|
770
928
|
let quote = null;
|
|
929
|
+
let arith = 0; // depth of `((` … `))`
|
|
930
|
+
let heredocs = []; // [{delim, stripTabs}] started on the current line
|
|
931
|
+
// The last char appended to `cur`, kept apart: reading `cur` itself flattens
|
|
932
|
+
// the string `+=` built, once per `#` — a 300 KB `echo a#a#…` took 7.9 s in
|
|
933
|
+
// a 5 s hook (pre-release review #11).
|
|
934
|
+
let last = '';
|
|
935
|
+
const add = (s) => { cur += s; last = s[s.length - 1]; };
|
|
936
|
+
const cut = (next) => { segs.push({ text: cur, sep }); cur = ''; last = ''; sep = next; };
|
|
771
937
|
for (let i = 0; i < cmd.length; i++) {
|
|
772
938
|
const c = cmd[i];
|
|
939
|
+
if (!quote) {
|
|
940
|
+
if (c === '#' && (last === '' || /[\s|(]/.test(last))) {
|
|
941
|
+
const nl = cmd.indexOf('\n', i);
|
|
942
|
+
i = (nl === -1 ? cmd.length : nl) - 1;
|
|
943
|
+
continue;
|
|
944
|
+
}
|
|
945
|
+
if (c === '(' && cmd[i + 1] === '(') { arith++; add('(('); i++; continue; }
|
|
946
|
+
if (c === ')' && cmd[i + 1] === ')' && arith > 0) { arith--; add('))'); i++; continue; }
|
|
947
|
+
if (c === '<' && cmd[i + 1] === '<' && cmd[i + 2] === '<') { add('<<<'); i += 2; continue; }
|
|
948
|
+
if (c === '<' && cmd[i + 1] === '<' && arith === 0) {
|
|
949
|
+
let j = i + 2;
|
|
950
|
+
const stripTabs = cmd[j] === '-';
|
|
951
|
+
if (stripTabs) j++;
|
|
952
|
+
while (cmd[j] === ' ' || cmd[j] === '\t') j++;
|
|
953
|
+
const d = readHeredocDelim(cmd, j);
|
|
954
|
+
if (d) {
|
|
955
|
+
heredocs.push({ delim: d.delim, stripTabs });
|
|
956
|
+
add(cmd.slice(i, d.end));
|
|
957
|
+
i = d.end - 1;
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
if (c === '\n' && heredocs.length > 0) {
|
|
962
|
+
cut('\n');
|
|
963
|
+
let j = i + 1;
|
|
964
|
+
for (const { delim, stripTabs } of heredocs) {
|
|
965
|
+
while (j < cmd.length) {
|
|
966
|
+
const nl = cmd.indexOf('\n', j);
|
|
967
|
+
const end = nl === -1 ? cmd.length : nl;
|
|
968
|
+
const line = cmd.slice(j, end);
|
|
969
|
+
j = end + 1;
|
|
970
|
+
if ((stripTabs ? line.replace(/^\t+/, '') : line) === delim) break;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
heredocs = [];
|
|
974
|
+
i = j - 1;
|
|
975
|
+
continue;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
773
978
|
if (quote) {
|
|
774
|
-
|
|
979
|
+
add(c);
|
|
775
980
|
// Inside DOUBLE quotes a backslash escapes the next char, so `\"` does NOT
|
|
776
981
|
// close the quote (POSIX). Single quotes do no escaping — `\` is literal
|
|
777
982
|
// and `'` always closes — so this only applies to `"`. Without it,
|
|
778
983
|
// `echo "x\" && grep \"Y\" src/"` (one literal echo arg) mis-closes at
|
|
779
984
|
// `\"`, splits on `&&`, and yields a phantom foldable grep segment.
|
|
780
985
|
if (quote === '"' && c === '\\' && i + 1 < cmd.length) {
|
|
781
|
-
|
|
986
|
+
add(cmd[i + 1]);
|
|
782
987
|
i++;
|
|
783
988
|
continue;
|
|
784
989
|
}
|
|
@@ -790,46 +995,58 @@ function splitTopLevelSegments(cmd) {
|
|
|
790
995
|
// removes both. Keeping them left `&& \⏎ grep …` a segment that starts
|
|
791
996
|
// with `\`, which GREP_HEAD rejects (pre-ship review round 2 F1).
|
|
792
997
|
if (c === '\\' && i + 1 < cmd.length) {
|
|
793
|
-
if (cmd[i + 1] !== '\n')
|
|
998
|
+
if (cmd[i + 1] !== '\n') add(c + cmd[i + 1]);
|
|
794
999
|
i++;
|
|
795
1000
|
continue;
|
|
796
1001
|
}
|
|
797
|
-
if (c === '"' || c === "'") { quote = c;
|
|
1002
|
+
if (c === '"' || c === "'") { quote = c; add(c); continue; }
|
|
798
1003
|
// `&&` and `||` (a single `&`/`|` is NOT a split — `|` is an output-filter
|
|
799
1004
|
// pipe, lone `&` is background and rare in tool calls).
|
|
800
1005
|
if ((c === '&' && cmd[i + 1] === '&') || (c === '|' && cmd[i + 1] === '|')) {
|
|
801
|
-
|
|
1006
|
+
cut(c + c); i++; continue;
|
|
802
1007
|
}
|
|
803
|
-
if (c === ';' || c === '\n') {
|
|
804
|
-
|
|
1008
|
+
if (c === ';' || c === '\n') { cut(c); continue; }
|
|
1009
|
+
add(c);
|
|
805
1010
|
}
|
|
806
|
-
|
|
1011
|
+
cut(undefined);
|
|
807
1012
|
// Split out `for … in` / `do` / `done` control words as their own boundaries
|
|
808
1013
|
// so a loop body grep is isolated (the head of `for s in …; do grep …` is the
|
|
809
1014
|
// `for` keyword, which would otherwise mask the grep). Quote-safety already
|
|
810
1015
|
// handled above — these run per already-split segment on whitespace-delimited
|
|
811
|
-
// control words only.
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
1016
|
+
// control words only. A `for` header takes its word list with it: the list
|
|
1017
|
+
// is data, and left behind as a segment it read as a command that segmentCwd
|
|
1018
|
+
// cannot place (D#76).
|
|
1019
|
+
const pieces = [];
|
|
1020
|
+
const CTRL = /(?:^|\s)(for\s+\S+\s+in\b[\s\S]*$|do\b|done\b|then\b|fi\b)(?=\s|$)/g;
|
|
1021
|
+
for (const { text: raw, sep: rawSep } of segs) {
|
|
815
1022
|
let last = 0;
|
|
816
1023
|
let m;
|
|
1024
|
+
let pieceSep = rawSep;
|
|
817
1025
|
CTRL.lastIndex = 0;
|
|
818
|
-
let pushed = false;
|
|
819
1026
|
while ((m = CTRL.exec(raw)) !== null) {
|
|
820
|
-
|
|
821
|
-
if (before.trim()) out.push(before);
|
|
1027
|
+
pieces.push({ text: raw.slice(last, m.index), sep: pieceSep });
|
|
822
1028
|
last = CTRL.lastIndex;
|
|
823
|
-
|
|
824
|
-
}
|
|
825
|
-
if (pushed) {
|
|
826
|
-
const tail = raw.slice(last);
|
|
827
|
-
if (tail.trim()) out.push(tail);
|
|
828
|
-
} else {
|
|
829
|
-
out.push(raw);
|
|
1029
|
+
pieceSep = 'ctrl';
|
|
830
1030
|
}
|
|
1031
|
+
pieces.push({ text: raw.slice(last), sep: pieceSep });
|
|
831
1032
|
}
|
|
832
|
-
|
|
1033
|
+
const out = [];
|
|
1034
|
+
let carried;
|
|
1035
|
+
for (const { text, sep: s } of pieces) {
|
|
1036
|
+
carried = joinSep(carried, s);
|
|
1037
|
+
if (!text.trim()) continue;
|
|
1038
|
+
out.push({ text: text.trim(), sep: out.length === 0 ? '' : carried });
|
|
1039
|
+
carried = undefined;
|
|
1040
|
+
}
|
|
1041
|
+
return out;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function splitTopLevelSegments(cmd) {
|
|
1045
|
+
return splitTopLevelSegmentsWithSeps(cmd).map((s) => s.text);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function segmentSeparators(cmd) {
|
|
1049
|
+
return splitTopLevelSegmentsWithSeps(cmd).map((s) => s.sep);
|
|
833
1050
|
}
|
|
834
1051
|
|
|
835
1052
|
// One implementation of the cooldown quartet, in tmp-dir.js (ARC-02/ARC-06);
|
|
@@ -937,6 +1154,9 @@ function buildRewriteContext(mode, cmdShown) {
|
|
|
937
1154
|
// `-`, and anything the tokenizer does not know — newline, `;`, `&`, `\`, `$`,
|
|
938
1155
|
// a backtick, parentheses, braces, `~`. Returns a plan or null.
|
|
939
1156
|
const WORD_CHAR = /[A-Za-z0-9_.,:@%+=/*?<>&-]/;
|
|
1157
|
+
function isDirectory(p) {
|
|
1158
|
+
try { return fs.statSync(p).isDirectory(); } catch { return false; }
|
|
1159
|
+
}
|
|
940
1160
|
function shellWords(s) {
|
|
941
1161
|
const words = [];
|
|
942
1162
|
let cur = null;
|
|
@@ -1000,10 +1220,15 @@ function valueWordOk(w, isCount) {
|
|
|
1000
1220
|
return !(w.bareSpecial);
|
|
1001
1221
|
}
|
|
1002
1222
|
|
|
1003
|
-
|
|
1223
|
+
// `isDir(target)` answers whether the path operand is a directory. runMain asks
|
|
1224
|
+
// the filesystem, resolved against the root its command is relative to; the
|
|
1225
|
+
// default is the spelling alone (a trailing `/`, or a last segment with no
|
|
1226
|
+
// extension), which keeps the classifiers that call this pure.
|
|
1227
|
+
const looksLikeDir = (t) => t.endsWith('/') || !/\.[A-Za-z0-9]{1,6}$/.test(t);
|
|
1228
|
+
function rewritePlan(cmd, { isDir = looksLikeDir } = {}) {
|
|
1004
1229
|
if (!cmd || typeof cmd !== 'string' || cmd.length > 1000) return null;
|
|
1005
1230
|
const words = shellWords(cmd);
|
|
1006
|
-
if (!words || words.some((w) => w.op)) return null;
|
|
1231
|
+
if (!words || words.length === 0 || words.some((w) => w.op)) return null;
|
|
1007
1232
|
let i = 0;
|
|
1008
1233
|
let verb = !words[0].anyQuoted ? words[0].text : '';
|
|
1009
1234
|
if (verb === 'git') {
|
|
@@ -1019,6 +1244,7 @@ function rewritePlan(cmd) {
|
|
|
1019
1244
|
let caseFlag = false;
|
|
1020
1245
|
let context = false;
|
|
1021
1246
|
let endOfFlags = false;
|
|
1247
|
+
let include = false;
|
|
1022
1248
|
for (; i < words.length; i++) {
|
|
1023
1249
|
const w = words[i];
|
|
1024
1250
|
if (!w.anyQuoted && (w.text === '2>&1' || w.text === '2>/dev/null')) continue;
|
|
@@ -1031,17 +1257,19 @@ function rewritePlan(cmd) {
|
|
|
1031
1257
|
// (`--include='*.rs'`, rg `-g'*.rs'`); an unquoted `<>&` is shell syntax.
|
|
1032
1258
|
const attachedFilter = /^--(?:include|glob|type)=/.test(w.text) || (verb === 'rg' && /^-[gt]./.test(w.text));
|
|
1033
1259
|
if (!attachedFilter || w.bareOp) return null;
|
|
1034
|
-
// GNU grep's --include has no `!` negation
|
|
1035
|
-
|
|
1260
|
+
// GNU grep's --include has no `!` negation and no `{a,b}` alternatives;
|
|
1261
|
+
// cg's -g has both (round 4 L2; review of D#78).
|
|
1262
|
+
if (/^--include=(?:!|.*\{)/.test(w.text)) return null;
|
|
1036
1263
|
}
|
|
1037
1264
|
if (w.text === '--') { endOfFlags = true; continue; }
|
|
1038
1265
|
if (w.text.startsWith('--')) {
|
|
1039
1266
|
const eq = w.text.indexOf('=');
|
|
1040
1267
|
const name = w.text.slice(2, eq === -1 ? undefined : eq);
|
|
1041
1268
|
if (!ALLOWED_LONG[verb].has(name)) return null;
|
|
1269
|
+
if (name === 'include') include = true;
|
|
1042
1270
|
if (VALUE_LONG.has(name) && eq === -1) {
|
|
1043
1271
|
const v = words[++i];
|
|
1044
|
-
if (!valueWordOk(v, false) || (name === 'include' && v.text
|
|
1272
|
+
if (!valueWordOk(v, false) || (name === 'include' && /^!|\{/.test(v.text))) return null;
|
|
1045
1273
|
}
|
|
1046
1274
|
if (name === 'ignore-case' || name === 'case-sensitive') caseFlag = true;
|
|
1047
1275
|
continue;
|
|
@@ -1075,12 +1303,85 @@ function rewritePlan(cmd) {
|
|
|
1075
1303
|
// A `..` segment: extractSearchPath refuses to scope it, so the answer would
|
|
1076
1304
|
// search the whole repo (round 4 M2).
|
|
1077
1305
|
if (target && /(?:^|\/)\.\.(?:\/|$)/.test(target.text)) return null;
|
|
1306
|
+
// GNU grep applies --include to a file named on the command line too, so
|
|
1307
|
+
// `grep --include='*.py' X src/a.rs` finds nothing; rg — and so cg — searches
|
|
1308
|
+
// a named file whatever its -g says (D#78). Equivalent only for a directory.
|
|
1309
|
+
if (include && target && !isDir(target.text)) return null;
|
|
1078
1310
|
// ag is smart-case by default: an all-lowercase pattern matches any case,
|
|
1079
1311
|
// and cg's search is case-sensitive, so the answer would find less.
|
|
1080
1312
|
if (verb === 'ag' && !caseFlag && !/[A-Z]/.test(pattern.text)) return null;
|
|
1081
1313
|
return { pattern: pattern.text, target: target ? target.text : undefined, context };
|
|
1082
1314
|
}
|
|
1083
1315
|
|
|
1316
|
+
// Does `rootTarget`, run from the root, name the path the grep clause's own
|
|
1317
|
+
// operand names from `cwd` — the directory the clause ran in? The raw clause is
|
|
1318
|
+
// parsed with the rewrite grammar; an absolute operand resolves the same from
|
|
1319
|
+
// anywhere. A clause the grammar cannot read proves nothing.
|
|
1320
|
+
function operandMatches(rawClause, rootTarget, root, cwd) {
|
|
1321
|
+
const raw = rewritePlan(rawClause, { isDir: (t) => isDirectory(path.resolve(cwd, t)) });
|
|
1322
|
+
if (!raw || raw.target === undefined || rootTarget === undefined) return false;
|
|
1323
|
+
return path.resolve(cwd, raw.target) === path.resolve(root, rootTarget);
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// Commands that cannot move the shell: external programs run in a child, and
|
|
1327
|
+
// these builtins leave the cwd alone. An allowlist, because the commands that
|
|
1328
|
+
// DO move it are open-ended — `builtin cd`, `if cd`, `{ cd …; }`, `eval`,
|
|
1329
|
+
// `source`, `popd`, any function (D#73 round 3 reproduced 19 forms).
|
|
1330
|
+
const CWD_NEUTRAL = new Set([
|
|
1331
|
+
'echo', 'printf', 'grep', 'rg', 'ag', 'git', 'sed', 'awk', 'cat', 'head', 'tail', 'wc',
|
|
1332
|
+
'ls', 'find', 'sort', 'uniq', 'cut', 'tr', 'diff', 'cmp', 'test', '[', 'true', 'false',
|
|
1333
|
+
':', 'node', 'python', 'python3', 'cargo', 'npm', 'npx', 'jq', 'xargs', 'tee', 'stat',
|
|
1334
|
+
'file', 'du', 'df', 'date', 'sleep', 'which', 'env', 'export', 'set', 'unset', 'nl',
|
|
1335
|
+
'rm', 'mkdir', 'cp', 'mv', 'touch', 'chmod', 'ln', 'readlink', 'realpath', 'basename',
|
|
1336
|
+
'dirname', 'perl', 'gh', 'curl', 'make', 'bash', 'sh', 'timeout', 'tar', 'sqlite3',
|
|
1337
|
+
'code-graph-mcp', 'nproc', 'rustc', 'go', 'wait', 'column', 'md5sum', 'sha256sum',
|
|
1338
|
+
'od', 'xxd', 'seq', 'paste', 'comm', 'uname', 'id', 'printenv',
|
|
1339
|
+
// If an `exit` before the grep had run, the grep would not have.
|
|
1340
|
+
'exit', 'return',
|
|
1341
|
+
]);
|
|
1342
|
+
// `X=1 Y=$(mktemp -d)`: a command substitution runs in a subshell.
|
|
1343
|
+
const PURE_ASSIGNMENTS =
|
|
1344
|
+
/^\s*(?:[A-Za-z_][A-Za-z0-9_]*=(?:\$\([^()]*\)|"[^"]*"|'[^']*'|[^\s;|&<>()'"]*)\s*)+$/;
|
|
1345
|
+
const CD_LITERAL = /^\s*(?:[A-Za-z_][A-Za-z0-9_]*=\S*\s+)*cd\s+('[^']*'|"[^"$`\\]*"|[^\s'"$`\\~*?[{|;&<>()]+)\s*$/;
|
|
1346
|
+
|
|
1347
|
+
/**
|
|
1348
|
+
* The directory `segments[idx]` runs in, or null when the segments before it
|
|
1349
|
+
* could have moved the shell somewhere we cannot name (D#76). Follows
|
|
1350
|
+
* cwd-neutral commands and `cd` to one literal path that exists; a relative
|
|
1351
|
+
* `cd` under a set CDPATH is not literal. A `cd` is followed only when it is
|
|
1352
|
+
* certain to have run if the grep did: it is unconditional (its `seps` entry,
|
|
1353
|
+
* from segmentSeparators, is '', `;` or a newline) or every separator from it
|
|
1354
|
+
* to the grep is `&&`, the one before it included: `true || cd x && grep` is
|
|
1355
|
+
* `(true || cd x) && grep`, which runs the grep without the cd. `false && cd x;
|
|
1356
|
+
* grep` and `cd x | cat` did not move the shell. Without `seps` every segment
|
|
1357
|
+
* counts as unconditional.
|
|
1358
|
+
*/
|
|
1359
|
+
function segmentCwd(segments, idx, shellCwd, { isDir = isDirectory, seps } = {}) {
|
|
1360
|
+
let cwd = shellCwd;
|
|
1361
|
+
const ran = (k) => !seps || SEP_RANK[seps[k]] <= 1
|
|
1362
|
+
|| seps.slice(k, idx + 1).every((x) => x === '&&');
|
|
1363
|
+
for (let k = 0; k < idx; k++) {
|
|
1364
|
+
const seg = segments[k];
|
|
1365
|
+
if (/^\s*#/.test(seg) || PURE_ASSIGNMENTS.test(seg)) continue;
|
|
1366
|
+
const clause = firstShellClause(seg);
|
|
1367
|
+
const cd = CD_LITERAL.exec(seg);
|
|
1368
|
+
if (cd) {
|
|
1369
|
+
if (!ran(k)) return null;
|
|
1370
|
+
const arg = /^['"]/.test(cd[1]) ? cd[1].slice(1, -1) : cd[1];
|
|
1371
|
+
if (!arg || arg === '-' || (!path.isAbsolute(arg) && process.env.CDPATH)) return null;
|
|
1372
|
+
const next = path.resolve(cwd, arg);
|
|
1373
|
+
if (!isDir(next)) return null; // `cd x; grep` runs the grep where it was
|
|
1374
|
+
try { cwd = fs.realpathSync(next); } catch { cwd = next; }
|
|
1375
|
+
continue;
|
|
1376
|
+
}
|
|
1377
|
+
const words = clauseWords(clause);
|
|
1378
|
+
if (!words) return null;
|
|
1379
|
+
const head = words.find((w) => !/^[A-Za-z_][A-Za-z0-9_]*=/.test(w.text));
|
|
1380
|
+
if (head && !(head.quotedFrom === -1 && CWD_NEUTRAL.has(head.text))) return null;
|
|
1381
|
+
}
|
|
1382
|
+
return cwd;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1084
1385
|
// The plan must describe the same search classifyBlock decided on. The pattern
|
|
1085
1386
|
// the answer ran is picked from quoted spans (pickBlockPattern); the grammar's
|
|
1086
1387
|
// pattern operand is the one the shell passes — `"is"'Foo'` or an unquoted
|
|
@@ -1124,11 +1425,70 @@ function translateBreToRg(cmd, pattern) {
|
|
|
1124
1425
|
// the two hooks, because post-grep-inject passes a SEGMENT here while the deny
|
|
1125
1426
|
// path passes the whole command — the same pattern then filed under two
|
|
1126
1427
|
// spellings and the funnel scored a verbatim re-grep as neutral.
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1428
|
+
// Read from the clause's words, like extractCgFlags: a pattern holding ` -E `
|
|
1429
|
+
// is not the flag (review of D#62).
|
|
1430
|
+
const words = clauseWords(firstShellClause(cmd).replace(VERB_STRIP, '')) || [];
|
|
1431
|
+
const extended = words.some((w) => w.quotedFrom === -1
|
|
1432
|
+
&& (/^-[a-zA-Z]*[EP][a-zA-Z]*(?:=|\d|$)/.test(w.text) || /^--(?:extended-regexp|perl-regexp)$/.test(w.text)));
|
|
1433
|
+
if (extended) return pattern;
|
|
1434
|
+
return breToRustRegex(pattern);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// The two dialects swap these characters' roles: in a basic regex `( ) { } + ?
|
|
1438
|
+
// |` are literals and `\(` … `\|` the operators, in rust regex the reverse. Only
|
|
1439
|
+
// unescaping the operators (the translation until D#65) left the literals as
|
|
1440
|
+
// operators, so `tombstoneActive()` searched `tombstoneActive` and the rewrite
|
|
1441
|
+
// returned a superset of grep's lines. Checked against the grep Claude Code's
|
|
1442
|
+
// shell runs (ugrep -G): `f()`, `a+b`, `a|b`, `{x}`, `a?` match only literally.
|
|
1443
|
+
//
|
|
1444
|
+
// A bracket expression is copied as written. Inside one, BRE has no escapes and
|
|
1445
|
+
// rust has escapes, `[` nesting and the `&&` `--` `~~` set operators, so a
|
|
1446
|
+
// bracket holding any of those means different things in the two; that pattern
|
|
1447
|
+
// is untranslatable and the result is null — the caller lets the grep run.
|
|
1448
|
+
// (`[:alpha:]` is a POSIX class in both and is kept.)
|
|
1449
|
+
//
|
|
1450
|
+
// Escapes: only the ones both read alike pass — the swapped operators, the
|
|
1451
|
+
// word/space/boundary classes, and an escaped metacharacter. `\1`, `\d`, `\z`,
|
|
1452
|
+
// `\_`, `\=` mean something else (or are an error) in one of the two, and so
|
|
1453
|
+
// do an empty group `\(\)`, an unknown `[:class:]` and a leading `]` in a
|
|
1454
|
+
// bracket (review of D#65).
|
|
1455
|
+
const BRE_SWAPPED = '(){}+?|';
|
|
1456
|
+
const BRE_SAME_ESCAPE = 'wWsSbB<>.*[]^$\\/';
|
|
1457
|
+
const POSIX_CLASSES = new Set(['alpha', 'digit', 'alnum', 'upper', 'lower', 'space',
|
|
1458
|
+
'punct', 'xdigit', 'blank', 'cntrl', 'graph', 'print']);
|
|
1459
|
+
function breToRustRegex(pattern) {
|
|
1460
|
+
let out = '';
|
|
1461
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
1462
|
+
const c = pattern[i];
|
|
1463
|
+
if (c === '\\' && i + 1 < pattern.length) {
|
|
1464
|
+
const n = pattern[++i];
|
|
1465
|
+
if (n === '(' && pattern.startsWith('\\)', i + 1)) return null;
|
|
1466
|
+
if (BRE_SWAPPED.includes(n)) out += n;
|
|
1467
|
+
else if (BRE_SAME_ESCAPE.includes(n)) out += c + n;
|
|
1468
|
+
else return null;
|
|
1469
|
+
continue;
|
|
1470
|
+
}
|
|
1471
|
+
if (BRE_SWAPPED.includes(c)) { out += '\\' + c; continue; }
|
|
1472
|
+
if (c !== '[') { out += c; continue; }
|
|
1473
|
+
// `]` right after `[` or `[^` is a member, not the end.
|
|
1474
|
+
let j = i + 1;
|
|
1475
|
+
if (pattern[j] === '^') j++;
|
|
1476
|
+
if (pattern[j] === ']') return null;
|
|
1477
|
+
for (; j < pattern.length && pattern[j] !== ']'; j++) {
|
|
1478
|
+
if (pattern[j] === '[' && pattern[j + 1] === ':') {
|
|
1479
|
+
const close = pattern.indexOf(':]', j + 2);
|
|
1480
|
+
if (close === -1 || !POSIX_CLASSES.has(pattern.slice(j + 2, close))) return null;
|
|
1481
|
+
j = close + 1;
|
|
1482
|
+
continue;
|
|
1483
|
+
}
|
|
1484
|
+
if (pattern[j] === '\\' || pattern[j] === '[') return null;
|
|
1485
|
+
if ('&-~'.includes(pattern[j]) && pattern[j + 1] === pattern[j]) return null;
|
|
1486
|
+
}
|
|
1487
|
+
if (j >= pattern.length) return null; // unterminated: grep reports an error
|
|
1488
|
+
out += pattern.slice(i, j + 1);
|
|
1489
|
+
i = j;
|
|
1130
1490
|
}
|
|
1131
|
-
return
|
|
1491
|
+
return out;
|
|
1132
1492
|
}
|
|
1133
1493
|
|
|
1134
1494
|
// v0.47.0 — cg grep found nothing. Regex-dialect differences (BRE `\|` vs
|
|
@@ -1334,8 +1694,17 @@ function runMain() {
|
|
|
1334
1694
|
// The rewrite replaces the WHOLE command. One it cannot reproduce runs as
|
|
1335
1695
|
// typed (see rewritePlan) — decided before the answer is spent on it. The
|
|
1336
1696
|
// opt-in static deny keeps its own, older scope.
|
|
1337
|
-
const plan = block && !isAnswerDisabled()
|
|
1697
|
+
const plan = block && !isAnswerDisabled()
|
|
1698
|
+
? rewritePlan(cmd, { isDir: (t) => isDirectory(path.resolve(root, t)) })
|
|
1699
|
+
: null;
|
|
1338
1700
|
if (block && !isAnswerDisabled() && !rewriteMatchesBlock(plan, block, cmd, rawGrepPattern)) return;
|
|
1701
|
+
// A pattern the dialect bridge cannot translate (null) has no equivalent search.
|
|
1702
|
+
if (block && !isAnswerDisabled() && block.mode === 'grep' && grepPattern === null) return;
|
|
1703
|
+
// D#76 — the rewrite searches its root-relative target from the root; the
|
|
1704
|
+
// command searched its own operand from the shell's cwd. From a subdirectory
|
|
1705
|
+
// the rebase moves only operands that exist there, so an operand it left
|
|
1706
|
+
// alone (`src/` from `xtask/`, `tests/` from `src/`) names another directory.
|
|
1707
|
+
if (plan && relPrefix && !operandMatches(rawCmd, plan.target, root, shellCwd)) return;
|
|
1339
1708
|
if (block) {
|
|
1340
1709
|
// v0.47.0 — run the AST-aware equivalent inside the hook and embed the
|
|
1341
1710
|
// results in the deny reason ("answer in the deny"). Degrades to the
|
|
@@ -1481,6 +1850,9 @@ module.exports = {
|
|
|
1481
1850
|
shellWords, // rewrite — the tokenizer rewritePlan's grammar reads
|
|
1482
1851
|
rewritePlan, // rewrite — does the whole command parse as one the rewrite reproduces
|
|
1483
1852
|
rewriteMatchesBlock, // rewrite — does that plan describe the search classifyBlock chose
|
|
1853
|
+
operandMatches, // D#76 — does a root-relative target name the path the command searched
|
|
1854
|
+
segmentCwd, // D#76 — the directory a compound command's segment runs in
|
|
1855
|
+
segmentSeparators, // D#76 — the separator before each top-level segment
|
|
1484
1856
|
extractSedReadTargets, // v0.49 — sed-range reads feed the read-fanout state
|
|
1485
1857
|
extractUnansweredTail, // v0.50 — compound-tail honesty in answered denies
|
|
1486
1858
|
extractPatterns, // v0.32.1 — exposed for tests
|
|
@@ -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.160.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.160.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.160.0",
|
|
39
|
+
"@sdsrs/code-graph-linux-arm64": "0.160.0",
|
|
40
|
+
"@sdsrs/code-graph-darwin-x64": "0.160.0",
|
|
41
|
+
"@sdsrs/code-graph-darwin-arm64": "0.160.0",
|
|
42
|
+
"@sdsrs/code-graph-win32-x64": "0.160.0"
|
|
43
43
|
}
|
|
44
44
|
}
|