@sdsrs/code-graph 0.24.0 → 0.25.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/hooks/hooks.json +10 -0
- package/claude-plugin/scripts/adopt.js +11 -4
- package/claude-plugin/scripts/adopt.test.js +52 -1
- package/claude-plugin/scripts/pre-grep-guide.js +118 -0
- package/claude-plugin/scripts/pre-grep-guide.test.js +195 -0
- package/package.json +6 -6
|
@@ -33,9 +33,16 @@ function readAdoptedBy(filePath) {
|
|
|
33
33
|
// scratch dirs, mixed repos). Per-type variants live in `buildIndexLine` and
|
|
34
34
|
// are computed per-cwd at adopt + needsRefresh time. Adopted-project receives
|
|
35
35
|
// the typed variant; everyone else falls back to this canonical line.
|
|
36
|
+
// Tags MUST be ≥4 chars and topic-specific (per claudemd §11-EXT Tag-specificity).
|
|
37
|
+
// Generic single-word English tags (impact / refs / overview / semantic / deps /
|
|
38
|
+
// trace / route / similar) substring-match release-notes / commit-message prose
|
|
39
|
+
// via the §11 read-the-file hook regex (word-boundary + 0–2 declension chars),
|
|
40
|
+
// producing false-positive denies. Each tag below aligns with its MCP tool name
|
|
41
|
+
// (impact_analysis / find_references / module_overview / …) so hyphenated literals
|
|
42
|
+
// never collide with natural prose.
|
|
36
43
|
const INDEX_LINE =
|
|
37
44
|
'- [code-graph-mcp](plugin_code_graph_mcp.md) ' +
|
|
38
|
-
'[impact, callgraph,
|
|
45
|
+
'[impact-analysis, callgraph, find-references, module-overview, semantic-search, ast-search, dead-code, find-similar-code, dependency-graph, trace-http-chain] — ' +
|
|
39
46
|
'改 X 影响面/谁调用 X/X 被谁用/看 X 源码/Y 模块长啥样/概念查询 优先于 Grep;字面匹配走 Grep。' +
|
|
40
47
|
'核心 7(get_call_graph/module_overview/semantic_code_search/ast_search/find_references/get_ast_node/project_map)' +
|
|
41
48
|
'+ 进阶 5(impact_analysis/trace_http_chain/dependency_graph/find_similar_code/find_dead_code),决策表见全文';
|
|
@@ -235,12 +242,12 @@ function buildIndexLine(projectType = 'generic') {
|
|
|
235
242
|
case 'web-py':
|
|
236
243
|
case 'web-go':
|
|
237
244
|
return prefix +
|
|
238
|
-
'[trace, route, callgraph, impact,
|
|
245
|
+
'[trace-http-chain, http-route, callgraph, impact-analysis, find-references, module-overview, semantic-search, dependency-graph] — ' +
|
|
239
246
|
'HTTP 路由→handler 链路用 trace_http_chain(或 get_call_graph route_path=);改 handler 影响面用 impact;' +
|
|
240
247
|
'其他结构化查询同上 优先于 Grep。' + coreSuffix;
|
|
241
248
|
case 'frontend':
|
|
242
249
|
return prefix +
|
|
243
|
-
'[
|
|
250
|
+
'[find-references, module-overview, semantic-search, callgraph, impact-analysis, ast-search] — ' +
|
|
244
251
|
'组件重命名/重构用 find_references(含 imports/inherits);模块层级用 module_overview;' +
|
|
245
252
|
'改 props/接口前用 impact 看下游;HTTP route 通常不适用。' + coreSuffix;
|
|
246
253
|
case 'rust':
|
|
@@ -248,7 +255,7 @@ function buildIndexLine(projectType = 'generic') {
|
|
|
248
255
|
case 'python':
|
|
249
256
|
case 'node':
|
|
250
257
|
return prefix +
|
|
251
|
-
'[callgraph, impact,
|
|
258
|
+
'[callgraph, impact-analysis, find-references, module-overview, semantic-search, ast-search, dead-code, dependency-graph] — ' +
|
|
252
259
|
'改 X 影响面/谁调用 X/Y 模块 优先于 Grep;HTTP route 追踪通常不适用(无 web 框架);' +
|
|
253
260
|
'字面匹配走 Grep。' + coreSuffix;
|
|
254
261
|
case 'generic':
|
|
@@ -648,7 +648,7 @@ test('buildIndexLine generic returns the canonical INDEX_LINE byte-for-byte', ()
|
|
|
648
648
|
|
|
649
649
|
test('buildIndexLine web-rs prepends route/trace tags + handler-focused lead', () => {
|
|
650
650
|
const line = buildIndexLine('web-rs');
|
|
651
|
-
assert.match(line, /\[trace, route,/, 'web-rs index line should lead with trace/route tags');
|
|
651
|
+
assert.match(line, /\[trace-http-chain, http-route,/, 'web-rs index line should lead with trace-http-chain/http-route tags');
|
|
652
652
|
assert.match(line, /HTTP 路由/, 'lead sentence should mention HTTP routes');
|
|
653
653
|
});
|
|
654
654
|
|
|
@@ -675,6 +675,57 @@ test('adopt + needsRefresh agree on typed INDEX_LINE — no spurious refresh in
|
|
|
675
675
|
} finally { sb.cleanup(); }
|
|
676
676
|
});
|
|
677
677
|
|
|
678
|
+
test('stale INDEX_LINE → adopt rewrites in place without duplicating sentinel blocks', () => {
|
|
679
|
+
// Regression for the v0.24+ tag-rename fix (feedback_adoption_tag_specificity).
|
|
680
|
+
// If a user's MEMORY.md was written by an older code-graph-mcp (pre-rename),
|
|
681
|
+
// SessionStart → maybeAutoAdopt → needsRefresh must detect the drift,
|
|
682
|
+
// stripSentinelBlock must locate the old v1 block by sentinel marker, and
|
|
683
|
+
// the rewrite must end with exactly one BEGIN/END pair. Breaks if anyone
|
|
684
|
+
// bumps SENTINEL_BEGIN without teaching stripSentinelBlock to also match
|
|
685
|
+
// the prior version — would leave an orphan v1 block plus a new v2 block.
|
|
686
|
+
const sb = makeSandbox();
|
|
687
|
+
try {
|
|
688
|
+
fs.writeFileSync(path.join(sb.cwd, 'Cargo.toml'), '[package]\nname="x"\n');
|
|
689
|
+
// Plant pre-rename MEMORY.md: well-formed v1 sentinel + an obsolete tag
|
|
690
|
+
// line that no current buildIndexLine variant produces. Exact prior bytes
|
|
691
|
+
// don't matter — only that it differs from today's desiredBlock.
|
|
692
|
+
const stalePayload =
|
|
693
|
+
'- [code-graph-mcp](plugin_code_graph_mcp.md) [obsolete-tag, another-stale] — stale lead sentence kept for drift detection.';
|
|
694
|
+
const indexPath = path.join(sb.dir, 'MEMORY.md');
|
|
695
|
+
const neighbor = '- [user_profile.md](user_profile.md) — neighbor (must survive)';
|
|
696
|
+
fs.writeFileSync(
|
|
697
|
+
indexPath,
|
|
698
|
+
`# Memory Index\n\n${neighbor}\n\n${SENTINEL_BEGIN}\n${stalePayload}\n${SENTINEL_END}\n`
|
|
699
|
+
);
|
|
700
|
+
// Plant target file so isAdopted/needsRefresh treat this as a real prior
|
|
701
|
+
// adoption (body bytewise-identical to shipped template — drift is only
|
|
702
|
+
// in MEMORY.md, not the template body).
|
|
703
|
+
const tplBody = fs.readFileSync(TEMPLATE_PATH);
|
|
704
|
+
const marker = Buffer.from(`<!-- adopted-by: ${sb.cwd} -->\n`);
|
|
705
|
+
fs.writeFileSync(path.join(sb.dir, TARGET_NAME), Buffer.concat([marker, tplBody]));
|
|
706
|
+
|
|
707
|
+
assert.strictEqual(isAdopted({ cwd: sb.cwd, home: sb.home }), true,
|
|
708
|
+
'precondition: planted state should look adopted');
|
|
709
|
+
assert.strictEqual(needsRefresh({ cwd: sb.cwd, home: sb.home }), true,
|
|
710
|
+
'tag-list drift must trigger refresh');
|
|
711
|
+
|
|
712
|
+
adopt({ cwd: sb.cwd, home: sb.home });
|
|
713
|
+
|
|
714
|
+
const after = fs.readFileSync(indexPath, 'utf8');
|
|
715
|
+
const escBegin = SENTINEL_BEGIN.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
716
|
+
const escEnd = SENTINEL_END.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
717
|
+
const beginHits = (after.match(new RegExp(escBegin, 'g')) || []).length;
|
|
718
|
+
const endHits = (after.match(new RegExp(escEnd, 'g')) || []).length;
|
|
719
|
+
assert.strictEqual(beginHits, 1, 'exactly one sentinel BEGIN after rewrite (no duplicate blocks)');
|
|
720
|
+
assert.strictEqual(endHits, 1, 'exactly one sentinel END after rewrite');
|
|
721
|
+
assert.ok(!after.includes('obsolete-tag'), 'stale tag list must be gone');
|
|
722
|
+
assert.ok(!after.includes('another-stale'), 'stale tag list must be gone');
|
|
723
|
+
assert.ok(after.includes(neighbor), 'neighbor entry preserved');
|
|
724
|
+
assert.strictEqual(needsRefresh({ cwd: sb.cwd, home: sb.home }), false,
|
|
725
|
+
'post-refresh needsRefresh must be false (no SessionStart refresh loop)');
|
|
726
|
+
} finally { sb.cleanup(); }
|
|
727
|
+
});
|
|
728
|
+
|
|
678
729
|
// 2A — false-positive hardening: comment-strip + section-aware scan.
|
|
679
730
|
|
|
680
731
|
test('detectProjectType ignores commented-out web-framework deps in Cargo.toml', () => {
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// PreToolUse(Bash) hook: detect raw `grep`/`rg`/`ag` on the indexed source tree
|
|
4
|
+
// and suggest code-graph CLI alternatives. Closes the "Bash comfort zone" leak —
|
|
5
|
+
// pre-training bias has Claude reach for `grep -rn` ~13× more than the indexed
|
|
6
|
+
// CLI on bash-heavy days (15-day baseline: 429 raw grep vs 191 functional CLI).
|
|
7
|
+
//
|
|
8
|
+
// Fires when ALL conditions met:
|
|
9
|
+
// 1. Command HEAD is grep/rg/ag (NOT piped — pipe-greps are output filters)
|
|
10
|
+
// 2. Args include an indexed source-tree path (src/ tests/ lib/ scripts/ ...)
|
|
11
|
+
// 3. Not searching only a config/lockfile (Cargo.toml/.gitignore/*.md/*.json)
|
|
12
|
+
// 4. Command doesn't already invoke code-graph-mcp (no double-suggest)
|
|
13
|
+
// 5. .code-graph/index.db exists in CWD
|
|
14
|
+
// 6. Same command-hash not hinted within last 60s (per-command cooldown)
|
|
15
|
+
//
|
|
16
|
+
// Exits silently otherwise — zero noise for build greps, log filters, config
|
|
17
|
+
// lookups, or the rare legitimate use of raw grep on indexed source.
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const os = require('os');
|
|
22
|
+
const crypto = require('crypto');
|
|
23
|
+
|
|
24
|
+
// --- Pure logic (testable) ---
|
|
25
|
+
|
|
26
|
+
const GREP_HEAD = /^\s*(?:env\s+\S+=\S+\s+)*(grep|rg|ag)\b/;
|
|
27
|
+
const SRC_PATH = /(?:^|\s|["'])(src|tests|lib|scripts|claude-plugin|tools|pkg|cmd|internal|app|components?|server|client|crates|packages)\//;
|
|
28
|
+
const PIPE_INTO_GREP = /\|\s*(?:grep|rg|ag)\b/;
|
|
29
|
+
const CG_INVOKED = /\bcode-graph-mcp\b/;
|
|
30
|
+
// A file argument that ends in a config/lockfile extension AND no source-tree
|
|
31
|
+
// path appears elsewhere → grep is searching config, not code.
|
|
32
|
+
const CONFIG_TARGET_ONLY =
|
|
33
|
+
/(?:^|\s)[^\s|<>]*\.(toml|md|json|yml|yaml|lock|txt|cfg|env|gitignore|properties)(?:\s|$)/i;
|
|
34
|
+
|
|
35
|
+
function shouldHint(cmd) {
|
|
36
|
+
if (!cmd || typeof cmd !== 'string') return false;
|
|
37
|
+
if (cmd.length > 1000) return false; // sanity — oversize commands are noise
|
|
38
|
+
if (CG_INVOKED.test(cmd)) return false; // already using cg
|
|
39
|
+
if (PIPE_INTO_GREP.test(cmd)) return false; // `cargo test | grep FAILED` is output filter
|
|
40
|
+
if (!GREP_HEAD.test(cmd)) return false; // not a search command
|
|
41
|
+
if (!SRC_PATH.test(cmd)) return false; // not against indexed source tree
|
|
42
|
+
// If a config file appears AND no source path remains after stripping it, skip.
|
|
43
|
+
if (CONFIG_TARGET_ONLY.test(cmd)) {
|
|
44
|
+
const stripped = cmd.replace(CONFIG_TARGET_ONLY, ' ');
|
|
45
|
+
if (!SRC_PATH.test(stripped)) return false;
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function commandHash(cmd) {
|
|
51
|
+
return crypto.createHash('sha1').update(cmd).digest('hex').slice(0, 12);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isOnCooldown(cmd, now = Date.now(), windowMs = 60000) {
|
|
55
|
+
const flag = path.join(os.tmpdir(), `.code-graph-bash-${commandHash(cmd)}`);
|
|
56
|
+
try {
|
|
57
|
+
return now - fs.statSync(flag).mtimeMs < windowMs;
|
|
58
|
+
} catch { return false; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function markCooldown(cmd) {
|
|
62
|
+
const flag = path.join(os.tmpdir(), `.code-graph-bash-${commandHash(cmd)}`);
|
|
63
|
+
try { fs.writeFileSync(flag, ''); } catch { /* ok */ }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function buildHint() {
|
|
67
|
+
// Terse, no banner spam. Single message budget ~600 bytes.
|
|
68
|
+
return [
|
|
69
|
+
'[code-graph] Raw `grep`/`rg` on indexed source — consider AST-aware equivalents:',
|
|
70
|
+
' • code-graph-mcp grep "<pat>" <path> # grep + containing fn/module per hit',
|
|
71
|
+
' • code-graph-mcp ast-search "<pat>" --type fn # filter by type/returns/params',
|
|
72
|
+
' • code-graph-mcp callgraph SYMBOL # callers + callees, repo-wide',
|
|
73
|
+
' • code-graph-mcp show SYMBOL # one symbol: signature + source',
|
|
74
|
+
'Repo-wide index (LSP only sees open files). Skip this hint if you specifically need raw-text regex.',
|
|
75
|
+
].join('\n');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// --- Main execution (only when run directly) ---
|
|
79
|
+
|
|
80
|
+
// Kill switch: matches user-prompt-context.js convention. =1 forces silence
|
|
81
|
+
// even when the rest of the hook tier is noisy. Default (unset) is noisy here
|
|
82
|
+
// — this hook only fires on raw grep against the source tree, which is the
|
|
83
|
+
// exact comfort-zone leak it was designed to catch.
|
|
84
|
+
function isSilenced(env = process.env) {
|
|
85
|
+
return env.CODE_GRAPH_QUIET_HOOKS === '1';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function runMain() {
|
|
89
|
+
if (isSilenced()) return;
|
|
90
|
+
const cwd = process.cwd();
|
|
91
|
+
const dbPath = path.join(cwd, '.code-graph', 'index.db');
|
|
92
|
+
if (!fs.existsSync(dbPath)) return; // no index — no hint
|
|
93
|
+
|
|
94
|
+
let input;
|
|
95
|
+
try {
|
|
96
|
+
input = JSON.parse(fs.readFileSync('/dev/stdin', 'utf8'));
|
|
97
|
+
} catch { return; }
|
|
98
|
+
|
|
99
|
+
const cmd = (input.tool_input && input.tool_input.command) || '';
|
|
100
|
+
if (!shouldHint(cmd)) return;
|
|
101
|
+
if (isOnCooldown(cmd)) return;
|
|
102
|
+
|
|
103
|
+
markCooldown(cmd);
|
|
104
|
+
process.stdout.write(buildHint() + '\n');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (require.main === module) {
|
|
108
|
+
runMain();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = {
|
|
112
|
+
shouldHint,
|
|
113
|
+
buildHint,
|
|
114
|
+
commandHash,
|
|
115
|
+
isOnCooldown,
|
|
116
|
+
markCooldown,
|
|
117
|
+
isSilenced,
|
|
118
|
+
};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const test = require('node:test');
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const { shouldHint, buildHint, commandHash, isSilenced } = require('./pre-grep-guide');
|
|
5
|
+
|
|
6
|
+
// ── Should fire: bare grep/rg/ag on indexed source tree ─────────────
|
|
7
|
+
|
|
8
|
+
test('shouldHint: grep -rn on src/', () => {
|
|
9
|
+
assert.equal(shouldHint('grep -rn "fn fts5_search" src/storage/'), true);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('shouldHint: rg on tests/', () => {
|
|
13
|
+
assert.equal(shouldHint('rg "expand_acronym" tests/'), true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('shouldHint: grep -n on single file in src/', () => {
|
|
17
|
+
assert.equal(shouldHint('grep -n "fn split_identifier" src/search/tokenizer.rs'), true);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('shouldHint: grep -rn on claude-plugin/', () => {
|
|
21
|
+
assert.equal(shouldHint('grep -rn "computeQuietHooks" claude-plugin/scripts/'), true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('shouldHint: grep with alternation against src/', () => {
|
|
25
|
+
assert.equal(shouldHint('grep -rn "set_hook\\|panic_handler" src/main.rs src/lib.rs'), true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('shouldHint: grep with stderr redirect + head pipe (still a source search)', () => {
|
|
29
|
+
// head/tail/sort pipes don't disqualify — the SEARCH operation is grep on src/
|
|
30
|
+
assert.equal(shouldHint('grep -rn "fn fts5_search\\|MATCH" src/storage/ 2>&1 | head -10'), true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('shouldHint: ag on lib/', () => {
|
|
34
|
+
assert.equal(shouldHint('ag "TODO" lib/'), true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('shouldHint: env-prefixed grep on src/', () => {
|
|
38
|
+
assert.equal(shouldHint('env LANG=C grep -rn "Foo" src/'), true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// ── Should NOT fire: pipe-grep (output filter, not search) ──────────
|
|
42
|
+
|
|
43
|
+
test('shouldHint: pipe-grep on cargo test output', () => {
|
|
44
|
+
assert.equal(shouldHint('cargo test 2>&1 | grep "test result"'), false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('shouldHint: pipe-grep with -E flag', () => {
|
|
48
|
+
assert.equal(shouldHint("cargo test --no-default-features 2>&1 | grep -E 'test result|FAILED'"), false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('shouldHint: pipe-rg', () => {
|
|
52
|
+
assert.equal(shouldHint("cargo build 2>&1 | rg 'warning|error'"), false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('shouldHint: pipe-grep with src/ in pattern (still output filter)', () => {
|
|
56
|
+
assert.equal(shouldHint("cargo build 2>&1 | grep 'src/main.rs'"), false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// ── Should NOT fire: already using code-graph-mcp ───────────────────
|
|
60
|
+
|
|
61
|
+
test('shouldHint: code-graph-mcp grep itself', () => {
|
|
62
|
+
assert.equal(shouldHint('code-graph-mcp grep "fn parse" src/'), false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('shouldHint: pipe through code-graph-mcp', () => {
|
|
66
|
+
assert.equal(shouldHint('code-graph-mcp show foo | grep src/'), false);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// ── Should NOT fire: not source-tree paths ──────────────────────────
|
|
70
|
+
|
|
71
|
+
test('shouldHint: grep on Cargo.toml only', () => {
|
|
72
|
+
assert.equal(shouldHint('grep "^version" Cargo.toml'), false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('shouldHint: grep -i docs on .gitignore', () => {
|
|
76
|
+
assert.equal(shouldHint('grep -i docs .gitignore'), false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('shouldHint: grep on package.json', () => {
|
|
80
|
+
assert.equal(shouldHint('grep "version" package.json'), false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('shouldHint: grep on a markdown changelog', () => {
|
|
84
|
+
assert.equal(shouldHint('grep "v0.24" CHANGELOG.md'), false);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// ── Should NOT fire: not search tools ───────────────────────────────
|
|
88
|
+
|
|
89
|
+
test('shouldHint: ls src/', () => {
|
|
90
|
+
assert.equal(shouldHint('ls src/storage/'), false);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('shouldHint: cat src/main.rs', () => {
|
|
94
|
+
assert.equal(shouldHint('cat src/main.rs'), false);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('shouldHint: git log on src/', () => {
|
|
98
|
+
assert.equal(shouldHint('git log --oneline -10 src/'), false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('shouldHint: find on src/ (file path tool, not content search)', () => {
|
|
102
|
+
// find is path-based, not pattern-based. Out of scope for this hook.
|
|
103
|
+
assert.equal(shouldHint('find src/ -name "*.rs"'), false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ── Edge cases ──────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
test('shouldHint: empty command', () => {
|
|
109
|
+
assert.equal(shouldHint(''), false);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('shouldHint: non-string input', () => {
|
|
113
|
+
assert.equal(shouldHint(null), false);
|
|
114
|
+
assert.equal(shouldHint(undefined), false);
|
|
115
|
+
assert.equal(shouldHint(42), false);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('shouldHint: oversize command (>1000 chars)', () => {
|
|
119
|
+
assert.equal(shouldHint('grep -rn "x" src/ ' + 'y'.repeat(1100)), false);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ── Hint content ────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
test('buildHint: includes all four code-graph subcommands', () => {
|
|
125
|
+
const out = buildHint();
|
|
126
|
+
assert.match(out, /code-graph-mcp grep/);
|
|
127
|
+
assert.match(out, /code-graph-mcp ast-search/);
|
|
128
|
+
assert.match(out, /code-graph-mcp callgraph/);
|
|
129
|
+
assert.match(out, /code-graph-mcp show/);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('buildHint: stays under 700-byte budget (~175 tokens)', () => {
|
|
133
|
+
const out = buildHint();
|
|
134
|
+
assert.ok(out.length < 700, `hint length ${out.length} exceeds budget`);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('buildHint: mentions repo-wide / LSP boundary', () => {
|
|
138
|
+
assert.match(buildHint(), /Repo-wide index|LSP/);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// ── Cooldown hash ───────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
test('commandHash: deterministic + 12-char', () => {
|
|
144
|
+
const h1 = commandHash('grep -rn "foo" src/');
|
|
145
|
+
const h2 = commandHash('grep -rn "foo" src/');
|
|
146
|
+
assert.equal(h1, h2);
|
|
147
|
+
assert.equal(h1.length, 12);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('commandHash: different commands → different hashes', () => {
|
|
151
|
+
assert.notEqual(commandHash('grep -rn "foo" src/'), commandHash('grep -rn "bar" src/'));
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ── Kill switch ─────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
test('isSilenced: default (no env) → not silenced (noisy)', () => {
|
|
157
|
+
assert.equal(isSilenced({}), false);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('isSilenced: CODE_GRAPH_QUIET_HOOKS=1 → silenced', () => {
|
|
161
|
+
assert.equal(isSilenced({ CODE_GRAPH_QUIET_HOOKS: '1' }), true);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('isSilenced: CODE_GRAPH_QUIET_HOOKS=0 → not silenced', () => {
|
|
165
|
+
assert.equal(isSilenced({ CODE_GRAPH_QUIET_HOOKS: '0' }), false);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('isSilenced: VERBOSE_HOOKS=1 alone → not silenced (noisy by default already)', () => {
|
|
169
|
+
// pre-grep-guide is noisy-by-default; VERBOSE is irrelevant here.
|
|
170
|
+
assert.equal(isSilenced({ CODE_GRAPH_VERBOSE_HOOKS: '1' }), false);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// ── Regression cases from real session telemetry (2026-05-11) ───────
|
|
174
|
+
|
|
175
|
+
test('regression: grep -n "Error\\|anyhow" src/main.rs (sess 5052e2a1)', () => {
|
|
176
|
+
assert.equal(shouldHint('grep -n "Error\\|anyhow\\|context" src/main.rs'), true);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('regression: grep -rn "fn fts5_search" src/storage/ (sess 25fa8050)', () => {
|
|
180
|
+
assert.equal(shouldHint('grep -rn "fn fts5_search\\|MATCH\\|fts.*tokenize" src/storage/'), true);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('regression: grep multi-extension MEMORY.md tag search (sess 5052e2a1)', () => {
|
|
184
|
+
// This one targets MEMORY.md files — should NOT fire because the --include flags
|
|
185
|
+
// are for non-source extensions and there's no `src/` etc. in the args.
|
|
186
|
+
assert.equal(shouldHint("grep -rn 'callgraph, impact' --include='*.md'"), false);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('regression: cargo test pipe filter NOT fires (sess 45691293)', () => {
|
|
190
|
+
assert.equal(shouldHint('cargo test --no-default-features 2>&1 | grep -E "test result|FAILED|error\\[" | tail -15'), false);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('regression: grep -m1 "^version" Cargo.toml NOT fires', () => {
|
|
194
|
+
assert.equal(shouldHint('grep -m1 "^version" Cargo.toml'), false);
|
|
195
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdsrs/code-graph",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.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.25.0",
|
|
39
|
+
"@sdsrs/code-graph-linux-arm64": "0.25.0",
|
|
40
|
+
"@sdsrs/code-graph-darwin-x64": "0.25.0",
|
|
41
|
+
"@sdsrs/code-graph-darwin-arm64": "0.25.0",
|
|
42
|
+
"@sdsrs/code-graph-win32-x64": "0.25.0"
|
|
43
43
|
}
|
|
44
44
|
}
|