@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.
|
@@ -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
|
|
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(
|
|
266
|
+
if (isOnCooldown(rawCmd)) return;
|
|
249
267
|
|
|
250
|
-
markCooldown(
|
|
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.
|
|
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.
|
|
39
|
-
"@sdsrs/code-graph-linux-arm64": "0.47.
|
|
40
|
-
"@sdsrs/code-graph-darwin-x64": "0.47.
|
|
41
|
-
"@sdsrs/code-graph-darwin-arm64": "0.47.
|
|
42
|
-
"@sdsrs/code-graph-win32-x64": "0.47.
|
|
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
|
}
|