@sdsrs/code-graph 0.47.0 → 0.47.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.47.0",
7
+ "version": "0.47.1",
8
8
  "keywords": [
9
9
  "code-graph",
10
10
  "ast",
@@ -117,6 +117,20 @@ function shouldBlock(cmd) {
117
117
  return patterns.some(p => IDENTIFIER_LIKE.test(p));
118
118
  }
119
119
 
120
+ // v0.47.1 — CC harness steers Bash toward ABSOLUTE paths (cd in compound
121
+ // commands triggers permission prompts), so `grep -rn "X" /abs/root/backend/…`
122
+ // is the dominant real shape — and SRC_PATH's lookbehind (^|\s|quote) never
123
+ // matched it (daagu 2026-06-11 replay: 42/42 head-greps absolute → 1 hint /
124
+ // 0 block as-is vs 30 / 16 after this strip). Strip `<cwd>/` everywhere before
125
+ // matching: the hook's cwd IS the project root, so this is exact — paths
126
+ // outside the project stay absolute and keep not firing (conservative edge).
127
+ // split/join, not regex: cwd may contain regex metacharacters.
128
+ function normalizeCommandPaths(cmd, cwd) {
129
+ if (!cmd || typeof cmd !== 'string') return cmd;
130
+ if (!cwd || typeof cwd !== 'string' || cwd === '/') return cmd;
131
+ return cmd.split(cwd.endsWith('/') ? cwd : cwd + '/').join('');
132
+ }
133
+
120
134
  // v0.47.0 — pull the first source-tree path token out of the denied command so
121
135
  // the inline answer can scope its search the same way the raw grep would have.
122
136
  function extractSearchPath(cmd) {
@@ -243,11 +257,15 @@ function runMain() {
243
257
  input = JSON.parse(fs.readFileSync(0, 'utf8'));
244
258
  } catch { return; }
245
259
 
246
- const cmd = (input.tool_input && input.tool_input.command) || '';
260
+ const rawCmd = (input.tool_input && input.tool_input.command) || '';
261
+ // v0.47.1 — match against the cwd-stripped form so absolute paths under the
262
+ // project root behave exactly like their relative spelling. Cooldown stays
263
+ // keyed on the raw command (what Claude actually sent).
264
+ const cmd = normalizeCommandPaths(rawCmd, cwd);
247
265
  if (!shouldHint(cmd)) return;
248
- if (isOnCooldown(cmd)) return;
266
+ if (isOnCooldown(rawCmd)) return;
249
267
 
250
- markCooldown(cmd);
268
+ markCooldown(rawCmd);
251
269
 
252
270
  if (!isBlockDisabled() && shouldBlock(cmd)) {
253
271
  // v0.47.0 — run the AST-aware equivalent inside the hook and embed the
@@ -299,6 +317,7 @@ module.exports = {
299
317
  shouldBlock,
300
318
  extractPatterns, // v0.32.1 — exposed for tests
301
319
  extractSearchPath, // v0.47.0 — deny-with-answer
320
+ normalizeCommandPaths, // v0.47.1 — abs-path matcher fix
302
321
  pickBlockPattern,
303
322
  buildHint,
304
323
  buildBlockReason,
@@ -6,6 +6,7 @@ const {
6
6
  shouldBlock,
7
7
  extractPatterns,
8
8
  extractSearchPath,
9
+ normalizeCommandPaths,
9
10
  pickBlockPattern,
10
11
  buildHint,
11
12
  buildBlockReason,
@@ -530,6 +531,74 @@ test('I4: grep -rn "fn render" src/ → BLOCK (decl anchor at start)', () => {
530
531
  assert.equal(shouldBlock('grep -rn "fn render" src/'), true);
531
532
  });
532
533
 
534
+ // ── v0.47.1 abs-path matcher fix: normalizeCommandPaths ─────────────
535
+ // CC harness steers Bash toward ABSOLUTE paths (cd in compound commands
536
+ // triggers permission prompts), so `grep -rn "X" /abs/root/backend/...` is
537
+ // the dominant real-world shape. SRC_PATH's lookbehind (^|\s|quote) never
538
+ // matched it: daagu 2026-06-11 replay — 42/42 head-greps absolute, 1 hint /
539
+ // 0 block as-is vs 30 hint / 16 block after cwd-strip.
540
+
541
+ test('normalizeCommandPaths: strips cwd prefix from path args', () => {
542
+ assert.equal(
543
+ normalizeCommandPaths('grep -rn "X" /proj/root/src/storage/', '/proj/root'),
544
+ 'grep -rn "X" src/storage/');
545
+ });
546
+
547
+ test('normalizeCommandPaths: strips every occurrence', () => {
548
+ assert.equal(
549
+ normalizeCommandPaths('grep -rn "X" /proj/root/src/a.rs /proj/root/tests/', '/proj/root'),
550
+ 'grep -rn "X" src/a.rs tests/');
551
+ });
552
+
553
+ test('normalizeCommandPaths: strips inside quotes', () => {
554
+ assert.equal(
555
+ normalizeCommandPaths('grep -rn "X" "/proj/root/backend/app/"', '/proj/root'),
556
+ 'grep -rn "X" "backend/app/"');
557
+ });
558
+
559
+ test('normalizeCommandPaths: leaves foreign absolute paths alone', () => {
560
+ assert.equal(
561
+ normalizeCommandPaths('grep -rn "X" /other/place/src/', '/proj/root'),
562
+ 'grep -rn "X" /other/place/src/');
563
+ });
564
+
565
+ test('normalizeCommandPaths: no-op when cwd absent / falsy inputs', () => {
566
+ assert.equal(normalizeCommandPaths('grep -rn "X" src/', '/proj/root'), 'grep -rn "X" src/');
567
+ assert.equal(normalizeCommandPaths('', '/proj/root'), '');
568
+ assert.equal(normalizeCommandPaths('grep "X" src/', ''), 'grep "X" src/');
569
+ });
570
+
571
+ // Real daagu transcript commands (2026-06-11 session 23f149f0…), the exact
572
+ // shape that was invisible to v0.47.0. Replay must fire post-normalization.
573
+ const DAAGU = '/mnt/data_ssd/dev/projects/daagu';
574
+
575
+ test('replay: real abs-path symbol grep → BLOCK after normalization', () => {
576
+ const cmd = `grep -n "_parse_finish_reason\\|_last_finish_reason\\|class OpenRouterProvider" ${DAAGU}/backend/app/services/llm_engine/openrouter.py`;
577
+ assert.equal(shouldHint(cmd), false); // documents the v0.47.0 blindspot
578
+ const norm = normalizeCommandPaths(cmd, DAAGU);
579
+ assert.equal(shouldHint(norm), true);
580
+ assert.equal(shouldBlock(norm), true);
581
+ });
582
+
583
+ test('replay: real abs-path -rln grep → HINT only after normalization (precision flag)', () => {
584
+ const cmd = `grep -rln "load_active_config_standalone" ${DAAGU}/backend/tests/ | head -5`;
585
+ const norm = normalizeCommandPaths(cmd, DAAGU);
586
+ assert.equal(shouldHint(norm), true);
587
+ assert.equal(shouldBlock(norm), false); // -l cluster disqualifies block
588
+ });
589
+
590
+ test('replay: abs-path config-only grep stays silent after normalization', () => {
591
+ const cmd = `grep -n '"typecheck"\\|"type-check"\\|vue-tsc' ${DAAGU}/frontend/package.json`;
592
+ assert.equal(shouldHint(normalizeCommandPaths(cmd, DAAGU)), false);
593
+ });
594
+
595
+ test('replay: extractSearchPath gets relative path from normalized abs command', () => {
596
+ const cmd = `grep -rn "config_version" ${DAAGU}/backend/app/services/stock_picker/data_providers.py 2>/dev/null | head -5`;
597
+ assert.equal(
598
+ extractSearchPath(normalizeCommandPaths(cmd, DAAGU)),
599
+ 'backend/app/services/stock_picker/data_providers.py');
600
+ });
601
+
533
602
  // ── v0.47.0 deny-with-answer: extractSearchPath / pickBlockPattern ──
534
603
 
535
604
  test('extractSearchPath: dir path after pattern', () => {
@@ -724,6 +793,26 @@ test('e2e: stub fails → static deny (v0.46 fallback) + records answered:false'
724
793
  }
725
794
  });
726
795
 
796
+ test('e2e: ABS-path grep under fixture root → deny fires, CLI argv gets relative path', () => {
797
+ const uniq = `StubAbs${Date.now()}`;
798
+ const fixture = e2eFixture(
799
+ `process.stdout.write('args=' + JSON.stringify(process.argv.slice(2)) + '\\n');`);
800
+ // fs.realpathSync: on macOS/Linux tmpdir may be a symlink; the hook sees the
801
+ // resolved cwd, so build the command from the same resolved form.
802
+ const realDir = fsE2e.realpathSync(fixture.dir);
803
+ const cmd = `grep -rn "${uniq}" ${realDir}/src/storage/`;
804
+ try {
805
+ const res = runHook(cmd, fixture);
806
+ assert.equal(res.status, 0);
807
+ const out = JSON.parse(res.stdout);
808
+ assert.equal(out.hookSpecificOutput.permissionDecision, 'deny');
809
+ assert.match(out.hookSpecificOutput.permissionDecisionReason,
810
+ /args=\["grep","StubAbs\d+","src\/storage\/"\]/);
811
+ } finally {
812
+ cleanupFixture(fixture, cmd);
813
+ }
814
+ });
815
+
727
816
  test('e2e: CODE_GRAPH_NO_ANSWER_IN_DENY=1 → static deny even when stub would hit', () => {
728
817
  const uniq = `StubOptout${Date.now()}`;
729
818
  const fixture = e2eFixture(`process.stdout.write('src/foo.rs:7 hit\\n');`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdsrs/code-graph",
3
- "version": "0.47.0",
3
+ "version": "0.47.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.47.0",
39
- "@sdsrs/code-graph-linux-arm64": "0.47.0",
40
- "@sdsrs/code-graph-darwin-x64": "0.47.0",
41
- "@sdsrs/code-graph-darwin-arm64": "0.47.0",
42
- "@sdsrs/code-graph-win32-x64": "0.47.0"
38
+ "@sdsrs/code-graph-linux-x64": "0.47.1",
39
+ "@sdsrs/code-graph-linux-arm64": "0.47.1",
40
+ "@sdsrs/code-graph-darwin-x64": "0.47.1",
41
+ "@sdsrs/code-graph-darwin-arm64": "0.47.1",
42
+ "@sdsrs/code-graph-win32-x64": "0.47.1"
43
43
  }
44
44
  }