@sdsrs/code-graph 0.20.0 → 0.21.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/incremental-index.js +38 -9
- package/claude-plugin/scripts/incremental-index.test.js +55 -0
- package/claude-plugin/scripts/user-prompt-context.js +270 -60
- package/claude-plugin/scripts/user-prompt-context.test.js +91 -0
- package/package.json +6 -6
|
@@ -3,12 +3,41 @@
|
|
|
3
3
|
const { execFileSync } = require('child_process');
|
|
4
4
|
const { findBinary } = require('./find-binary');
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
6
|
+
// v0.21 — gated default-off. v0.18.0 added query-time freshness
|
|
7
|
+
// (ensure_file_indexed) inside MCP tools that take a file_path arg, so a
|
|
8
|
+
// PostToolUse hook spawning a fresh process on every Edit/Write was redundant
|
|
9
|
+
// for the MCP-driven workflow and just burnt ~80ms cold-start per edit.
|
|
10
|
+
//
|
|
11
|
+
// CLI-only workflows (running `code-graph-mcp search` after Bash-side edits
|
|
12
|
+
// without going through MCP) need the hook to keep the DB fresh, so the knob
|
|
13
|
+
// lets users opt back in.
|
|
14
|
+
//
|
|
15
|
+
// Priority (high → low):
|
|
16
|
+
// 1. CODE_GRAPH_HOOK_INDEX=on → run the hook (opt-in)
|
|
17
|
+
// 2. CODE_GRAPH_HOOK_INDEX=off → skip
|
|
18
|
+
// 3. default → skip (v0.21 flip)
|
|
19
|
+
function shouldRun(env = process.env) {
|
|
20
|
+
const v = (env.CODE_GRAPH_HOOK_INDEX || '').toLowerCase();
|
|
21
|
+
if (v === 'on' || v === '1' || v === 'true') return true;
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function runMain() {
|
|
26
|
+
if (!shouldRun()) return;
|
|
27
|
+
|
|
28
|
+
const bin = findBinary();
|
|
29
|
+
if (!bin) return; // silent — binary not installed yet
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
execFileSync(bin, ['incremental-index', '--quiet'], {
|
|
33
|
+
timeout: 8000,
|
|
34
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
35
|
+
});
|
|
36
|
+
} catch { /* timeout or error — silent for hook */ }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (require.main === module) {
|
|
40
|
+
runMain();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { shouldRun };
|
|
@@ -7,6 +7,61 @@ const os = require('os');
|
|
|
7
7
|
const { spawnSync } = require('child_process');
|
|
8
8
|
|
|
9
9
|
const { findBinary } = require('./find-binary');
|
|
10
|
+
const { shouldRun } = require('./incremental-index');
|
|
11
|
+
|
|
12
|
+
// ── shouldRun gate (v0.21 opt-in flip) ──────────────────
|
|
13
|
+
|
|
14
|
+
test('shouldRun: default (no env) is OFF', () => {
|
|
15
|
+
// v0.21: hook-driven incremental-index is opt-in. Rely on
|
|
16
|
+
// ensure_file_indexed query-time freshness for MCP workflows.
|
|
17
|
+
assert.equal(shouldRun({}), false);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('shouldRun: CODE_GRAPH_HOOK_INDEX=on enables hook (opt-in)', () => {
|
|
21
|
+
assert.equal(shouldRun({ CODE_GRAPH_HOOK_INDEX: 'on' }), true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('shouldRun: CODE_GRAPH_HOOK_INDEX=1 enables hook (truthy alias)', () => {
|
|
25
|
+
assert.equal(shouldRun({ CODE_GRAPH_HOOK_INDEX: '1' }), true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('shouldRun: CODE_GRAPH_HOOK_INDEX=true enables hook (truthy alias)', () => {
|
|
29
|
+
assert.equal(shouldRun({ CODE_GRAPH_HOOK_INDEX: 'true' }), true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('shouldRun: CODE_GRAPH_HOOK_INDEX=off keeps hook off (explicit)', () => {
|
|
33
|
+
assert.equal(shouldRun({ CODE_GRAPH_HOOK_INDEX: 'off' }), false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('shouldRun: CODE_GRAPH_HOOK_INDEX with empty string is OFF', () => {
|
|
37
|
+
assert.equal(shouldRun({ CODE_GRAPH_HOOK_INDEX: '' }), false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('shouldRun: CODE_GRAPH_HOOK_INDEX is case-insensitive', () => {
|
|
41
|
+
assert.equal(shouldRun({ CODE_GRAPH_HOOK_INDEX: 'ON' }), true);
|
|
42
|
+
assert.equal(shouldRun({ CODE_GRAPH_HOOK_INDEX: 'On' }), true);
|
|
43
|
+
assert.equal(shouldRun({ CODE_GRAPH_HOOK_INDEX: 'TRUE' }), true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('hook script default-env spawn does not invoke the binary (default OFF)', () => {
|
|
47
|
+
// End-to-end check: with the v0.21 opt-in flip, a default-env spawn of
|
|
48
|
+
// incremental-index.js exits 0 immediately without touching the binary.
|
|
49
|
+
const script = path.join(__dirname, 'incremental-index.js');
|
|
50
|
+
const cleanEnv = { ...process.env };
|
|
51
|
+
delete cleanEnv.CODE_GRAPH_HOOK_INDEX;
|
|
52
|
+
const t0 = Date.now();
|
|
53
|
+
const proc = spawnSync(process.execPath, [script], {
|
|
54
|
+
env: cleanEnv,
|
|
55
|
+
encoding: 'utf8',
|
|
56
|
+
timeout: 2000,
|
|
57
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
58
|
+
});
|
|
59
|
+
// Should be much faster than the 80ms+ cold-start of running the binary.
|
|
60
|
+
// 500ms is generous — actual is ~30-50ms node startup.
|
|
61
|
+
assert.equal(proc.status, 0, `expected exit 0, got ${proc.status}; stderr: ${proc.stderr}`);
|
|
62
|
+
assert.equal(proc.stdout, '', 'stdout must be empty when default-OFF');
|
|
63
|
+
assert.ok(Date.now() - t0 < 500, 'default-OFF must short-circuit fast (< 500ms)');
|
|
64
|
+
});
|
|
10
65
|
|
|
11
66
|
test('incremental-index bails silently when cwd is not a git repo', (t) => {
|
|
12
67
|
const bin = findBinary();
|
|
@@ -8,28 +8,8 @@ const fs = require('fs');
|
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const os = require('os');
|
|
10
10
|
|
|
11
|
-
//
|
|
12
|
-
// If hooks are running but lifecycle install() hasn't executed yet (no manifest),
|
|
13
|
-
// the plugin was installed mid-session and the MCP server isn't connected.
|
|
14
|
-
// Claude Code only starts MCP servers at session startup; /mcp reconnect cannot
|
|
15
|
-
// start servers that were never initialized.
|
|
11
|
+
// Mid-session install detection: hook fires but no manifest yet.
|
|
16
12
|
const MANIFEST_PATH = path.join(os.homedir(), '.cache', 'code-graph', 'install-manifest.json');
|
|
17
|
-
if (!fs.existsSync(MANIFEST_PATH)) {
|
|
18
|
-
const noticeFile = path.join(os.tmpdir(), '.code-graph-mcp-restart-notice');
|
|
19
|
-
try {
|
|
20
|
-
// Show once per hour to avoid spam
|
|
21
|
-
if (Date.now() - fs.statSync(noticeFile).mtimeMs < 3600000) process.exit(0);
|
|
22
|
-
} catch { /* first notice */ }
|
|
23
|
-
try { fs.writeFileSync(noticeFile, ''); } catch { /* ok */ }
|
|
24
|
-
process.stdout.write(
|
|
25
|
-
'[code-graph] Plugin installed — MCP server requires a session restart to connect.\n' +
|
|
26
|
-
'MCP servers are only initialized at session startup. To activate:\n' +
|
|
27
|
-
' 1. Press Ctrl+C to exit the current session\n' +
|
|
28
|
-
' 2. Re-run `claude` to start a new session\n' +
|
|
29
|
-
'Meanwhile, CLI tools work directly: code-graph-mcp search <query>, code-graph-mcp map, etc.\n'
|
|
30
|
-
);
|
|
31
|
-
process.exit(0);
|
|
32
|
-
}
|
|
33
13
|
|
|
34
14
|
// --- Per-type rate limiting (replaces single global cooldown) ---
|
|
35
15
|
const COOLDOWNS = {
|
|
@@ -53,25 +33,24 @@ function markCooldown(type) {
|
|
|
53
33
|
} catch { /* ok */ }
|
|
54
34
|
}
|
|
55
35
|
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
36
|
+
// v0.21 — flipped to opt-in default. Routing-bench backend P@1=100% (v0.20.0)
|
|
37
|
+
// proves Sonnet 4.5 picks tools correctly without push injection; per-prompt
|
|
38
|
+
// CLI exec was costing 200-500 tokens/turn across N turns to repeat what the
|
|
39
|
+
// agent would have called itself. Mirrors session-init.js computeQuietHooks
|
|
40
|
+
// priority chain so a single env knob covers both hooks.
|
|
41
|
+
//
|
|
42
|
+
// Priority (high → low):
|
|
43
|
+
// 1. CODE_GRAPH_QUIET_HOOKS=0 → forced noisy (legacy back-compat)
|
|
44
|
+
// 2. CODE_GRAPH_QUIET_HOOKS=1 → forced quiet (legacy back-compat)
|
|
45
|
+
// 3. CODE_GRAPH_VERBOSE_HOOKS=1 → opt-in noisy (new, recommended)
|
|
46
|
+
// 4. default → quiet
|
|
47
|
+
function computeQuietHooks(env = process.env) {
|
|
48
|
+
const envQuiet = env.CODE_GRAPH_QUIET_HOOKS;
|
|
49
|
+
if (envQuiet === '0') return false;
|
|
50
|
+
if (envQuiet === '1') return true;
|
|
51
|
+
if (env.CODE_GRAPH_VERBOSE_HOOKS === '1') return false;
|
|
52
|
+
return true;
|
|
67
53
|
}
|
|
68
|
-
// Chinese chars are ~3 bytes but 1 char; "看看 fts5_search" is only 16 chars
|
|
69
|
-
if (!message || message.length < 8) process.exit(0);
|
|
70
|
-
|
|
71
|
-
// --- Check index ---
|
|
72
|
-
const cwd = process.cwd();
|
|
73
|
-
const dbPath = path.join(cwd, '.code-graph', 'index.db');
|
|
74
|
-
if (!fs.existsSync(dbPath)) process.exit(0);
|
|
75
54
|
|
|
76
55
|
// --- Pure logic (exported for testing) ---
|
|
77
56
|
|
|
@@ -121,14 +100,203 @@ function extractSymbols(msg) {
|
|
|
121
100
|
return { symbols: candidates, lowConfidence };
|
|
122
101
|
}
|
|
123
102
|
|
|
103
|
+
// v0.21 — replaced 6 mixed-language regex piles with per-keyword weighted
|
|
104
|
+
// patterns. Each row is testable in isolation, weights ready for tuning when
|
|
105
|
+
// false-positive data accumulates. Threshold 0.5 + uniform weight 1.0
|
|
106
|
+
// preserves the original OR-of-alternatives behavior 1:1; future tuning can
|
|
107
|
+
// downweight noisy short keywords like "bug" or "什么" that currently fire
|
|
108
|
+
// too eagerly. Maintenance cost: ~150 lines of table vs 6 × 200-char regex —
|
|
109
|
+
// regression history (#5754, #7713) shows the regex form was the higher
|
|
110
|
+
// silent-bug surface.
|
|
111
|
+
const INTENT_PATTERNS = {
|
|
112
|
+
impact: [
|
|
113
|
+
[/impact/i, 1.0],
|
|
114
|
+
[/影响/, 1.0],
|
|
115
|
+
[/修改前/, 1.0],
|
|
116
|
+
[/改之前/, 1.0],
|
|
117
|
+
[/blast radius/i, 1.0],
|
|
118
|
+
[/before (?:edit|chang|modif)/i, 1.0],
|
|
119
|
+
[/risk/i, 1.0],
|
|
120
|
+
[/风险/, 1.0],
|
|
121
|
+
[/改动范围/, 1.0],
|
|
122
|
+
[/波及/, 1.0],
|
|
123
|
+
[/问题在/, 1.0],
|
|
124
|
+
[/bug/i, 1.0],
|
|
125
|
+
[/干扰/, 1.0],
|
|
126
|
+
[/冲突/, 1.0],
|
|
127
|
+
[/卡/, 1.0],
|
|
128
|
+
],
|
|
129
|
+
modify: [
|
|
130
|
+
[/改(?!变)/, 1.0],
|
|
131
|
+
[/修改/, 1.0],
|
|
132
|
+
[/修复/, 1.0],
|
|
133
|
+
[/重构/, 1.0],
|
|
134
|
+
[/优化/, 1.0],
|
|
135
|
+
[/简化/, 1.0],
|
|
136
|
+
[/精简/, 1.0],
|
|
137
|
+
[/适配/, 1.0],
|
|
138
|
+
[/统一/, 1.0],
|
|
139
|
+
[/修正/, 1.0],
|
|
140
|
+
[/调整/, 1.0],
|
|
141
|
+
[/去掉/, 1.0],
|
|
142
|
+
[/整理/, 1.0],
|
|
143
|
+
[/清理/, 1.0],
|
|
144
|
+
[/解耦/, 1.0],
|
|
145
|
+
[/更新/, 1.0],
|
|
146
|
+
[/\brefactor\b/i, 1.0],
|
|
147
|
+
[/\bchange\b/i, 1.0],
|
|
148
|
+
[/\brename\b/i, 1.0],
|
|
149
|
+
[/\bfix\b/i, 1.0],
|
|
150
|
+
[/移动/, 1.0],
|
|
151
|
+
[/\bmove\b/i, 1.0],
|
|
152
|
+
[/删(?!除文件)/, 1.0],
|
|
153
|
+
[/\bremove\b/i, 1.0],
|
|
154
|
+
[/替换/, 1.0],
|
|
155
|
+
[/\breplace\b/i, 1.0],
|
|
156
|
+
[/\bupdate\b/i, 1.0],
|
|
157
|
+
[/升级/, 1.0],
|
|
158
|
+
[/\bmigrate\b/i, 1.0],
|
|
159
|
+
[/迁移/, 1.0],
|
|
160
|
+
[/拆分/, 1.0],
|
|
161
|
+
[/\bsplit\b/i, 1.0],
|
|
162
|
+
[/合并/, 1.0],
|
|
163
|
+
[/\bmerge\b/i, 1.0],
|
|
164
|
+
[/提取/, 1.0],
|
|
165
|
+
[/\bextract\b/i, 1.0],
|
|
166
|
+
[/改成/, 1.0],
|
|
167
|
+
[/改为/, 1.0],
|
|
168
|
+
[/换成/, 1.0],
|
|
169
|
+
[/转为/, 1.0],
|
|
170
|
+
[/异步/, 1.0],
|
|
171
|
+
[/同步/, 1.0],
|
|
172
|
+
],
|
|
173
|
+
implement: [
|
|
174
|
+
[/\badd\b/i, 1.0],
|
|
175
|
+
[/\bimplement\b/i, 1.0],
|
|
176
|
+
[/\bcreate\b/i, 1.0],
|
|
177
|
+
[/\bbuild\b/i, 1.0],
|
|
178
|
+
[/\bwrite\b/i, 1.0],
|
|
179
|
+
[/新增/, 1.0],
|
|
180
|
+
[/添加/, 1.0],
|
|
181
|
+
[/实现/, 1.0],
|
|
182
|
+
[/创建/, 1.0],
|
|
183
|
+
[/编写/, 1.0],
|
|
184
|
+
[/开发/, 1.0],
|
|
185
|
+
[/增加/, 1.0],
|
|
186
|
+
[/加上/, 1.0],
|
|
187
|
+
[/加个/, 1.0],
|
|
188
|
+
[/写/, 1.0],
|
|
189
|
+
[/做个/, 1.0],
|
|
190
|
+
[/搭建/, 1.0],
|
|
191
|
+
[/补充/, 1.0],
|
|
192
|
+
[/引入/, 1.0],
|
|
193
|
+
[/支持/, 1.0],
|
|
194
|
+
[/封装/, 1.0],
|
|
195
|
+
[/接入/, 1.0],
|
|
196
|
+
[/对接/, 1.0],
|
|
197
|
+
[/配置/, 1.0],
|
|
198
|
+
],
|
|
199
|
+
understand: [
|
|
200
|
+
[/how does/i, 1.0],
|
|
201
|
+
[/怎么工作/, 1.0],
|
|
202
|
+
[/怎么实现/, 1.0],
|
|
203
|
+
[/怎么做/, 1.0],
|
|
204
|
+
[/什么/, 1.0],
|
|
205
|
+
[/理解/, 1.0],
|
|
206
|
+
[/看看/, 1.0],
|
|
207
|
+
[/看一下/, 1.0],
|
|
208
|
+
[/了解/, 1.0],
|
|
209
|
+
[/分析/, 1.0],
|
|
210
|
+
[/explain/i, 1.0],
|
|
211
|
+
[/understand/i, 1.0],
|
|
212
|
+
[/架构/, 1.0],
|
|
213
|
+
[/architecture/i, 1.0],
|
|
214
|
+
[/structure/i, 1.0],
|
|
215
|
+
[/overview/i, 1.0],
|
|
216
|
+
[/模块/, 1.0],
|
|
217
|
+
[/概览/, 1.0],
|
|
218
|
+
[/干什么/, 1.0],
|
|
219
|
+
[/做什么/, 1.0],
|
|
220
|
+
[/工作原理/, 1.0],
|
|
221
|
+
[/逻辑/, 1.0],
|
|
222
|
+
[/机制/, 1.0],
|
|
223
|
+
[/流程/, 1.0],
|
|
224
|
+
[/功能/, 1.0],
|
|
225
|
+
[/结合度/, 1.0],
|
|
226
|
+
[/效率/, 1.0],
|
|
227
|
+
[/评估/, 1.0],
|
|
228
|
+
[/调研/, 1.0],
|
|
229
|
+
[/是什么/, 1.0],
|
|
230
|
+
[/有什么/, 1.0],
|
|
231
|
+
[/能用不/, 1.0],
|
|
232
|
+
[/高效不/, 1.0],
|
|
233
|
+
[/达标/, 1.0],
|
|
234
|
+
[/起作用/, 1.0],
|
|
235
|
+
[/科学/, 1.0],
|
|
236
|
+
[/深入思考/, 1.0],
|
|
237
|
+
[/源码/, 1.0],
|
|
238
|
+
[/检查/, 1.0],
|
|
239
|
+
[/审核/, 1.0],
|
|
240
|
+
[/审查/, 1.0],
|
|
241
|
+
[/验证/, 1.0],
|
|
242
|
+
[/诊断/, 1.0],
|
|
243
|
+
],
|
|
244
|
+
callgraph: [
|
|
245
|
+
[/who calls/i, 1.0],
|
|
246
|
+
[/what calls/i, 1.0],
|
|
247
|
+
[/调用/, 1.0],
|
|
248
|
+
[/call(?:graph|er|ee)/i, 1.0],
|
|
249
|
+
[/trace/i, 1.0],
|
|
250
|
+
[/链路/, 1.0],
|
|
251
|
+
[/追踪/, 1.0],
|
|
252
|
+
[/谁调/, 1.0],
|
|
253
|
+
[/被谁调/, 1.0],
|
|
254
|
+
[/调了谁/, 1.0],
|
|
255
|
+
[/上下游/, 1.0],
|
|
256
|
+
[/依赖关系/, 1.0],
|
|
257
|
+
[/触发/, 1.0],
|
|
258
|
+
[/路径/, 1.0],
|
|
259
|
+
[/覆盖/, 1.0],
|
|
260
|
+
[/介入/, 1.0],
|
|
261
|
+
],
|
|
262
|
+
search: [
|
|
263
|
+
[/where is/i, 1.0],
|
|
264
|
+
[/在哪/, 1.0],
|
|
265
|
+
[/find/i, 1.0],
|
|
266
|
+
[/search/i, 1.0],
|
|
267
|
+
[/搜索/, 1.0],
|
|
268
|
+
[/找/, 1.0],
|
|
269
|
+
[/locate/i, 1.0],
|
|
270
|
+
[/哪里用/, 1.0],
|
|
271
|
+
[/哪里定义/, 1.0],
|
|
272
|
+
[/定义在/, 1.0],
|
|
273
|
+
[/实现在/, 1.0],
|
|
274
|
+
[/处理没/, 1.0],
|
|
275
|
+
[/在源码/, 1.0],
|
|
276
|
+
[/加不加/, 1.0],
|
|
277
|
+
],
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const INTENT_THRESHOLD = 0.5;
|
|
281
|
+
|
|
282
|
+
function scoreIntent(msg, intent) {
|
|
283
|
+
const patterns = INTENT_PATTERNS[intent];
|
|
284
|
+
if (!patterns) return 0;
|
|
285
|
+
let max = 0;
|
|
286
|
+
for (const [pattern, weight] of patterns) {
|
|
287
|
+
if (pattern.test(msg) && weight > max) max = weight;
|
|
288
|
+
}
|
|
289
|
+
return max;
|
|
290
|
+
}
|
|
291
|
+
|
|
124
292
|
function detectIntents(msg) {
|
|
125
293
|
return {
|
|
126
|
-
impact:
|
|
127
|
-
modify:
|
|
128
|
-
implement:
|
|
129
|
-
understand:
|
|
130
|
-
callgraph:
|
|
131
|
-
search:
|
|
294
|
+
impact: scoreIntent(msg, 'impact') >= INTENT_THRESHOLD,
|
|
295
|
+
modify: scoreIntent(msg, 'modify') >= INTENT_THRESHOLD,
|
|
296
|
+
implement: scoreIntent(msg, 'implement') >= INTENT_THRESHOLD,
|
|
297
|
+
understand: scoreIntent(msg, 'understand') >= INTENT_THRESHOLD,
|
|
298
|
+
callgraph: scoreIntent(msg, 'callgraph') >= INTENT_THRESHOLD,
|
|
299
|
+
search: scoreIntent(msg, 'search') >= INTENT_THRESHOLD,
|
|
132
300
|
};
|
|
133
301
|
}
|
|
134
302
|
|
|
@@ -152,15 +320,55 @@ function determineQueryType(intents, symbols, filePaths, isCoolingDownFn) {
|
|
|
152
320
|
}
|
|
153
321
|
|
|
154
322
|
// --- Main execution (only when run directly) ---
|
|
155
|
-
|
|
156
|
-
|
|
323
|
+
// All exit-on-condition checks (manifest, computeQuietHooks, message length,
|
|
324
|
+
// db presence) live INSIDE this guard so `require()` from tests doesn't
|
|
325
|
+
// terminate the test process on module load.
|
|
326
|
+
function runMain() {
|
|
327
|
+
// Mid-session install: lifecycle.js install() hasn't run yet (no manifest).
|
|
328
|
+
// MCP server only starts at session startup — tell the user to restart.
|
|
329
|
+
if (!fs.existsSync(MANIFEST_PATH)) {
|
|
330
|
+
const noticeFile = path.join(os.tmpdir(), '.code-graph-mcp-restart-notice');
|
|
331
|
+
try {
|
|
332
|
+
// Show once per hour to avoid spam
|
|
333
|
+
if (Date.now() - fs.statSync(noticeFile).mtimeMs < 3600000) return;
|
|
334
|
+
} catch { /* first notice */ }
|
|
335
|
+
try { fs.writeFileSync(noticeFile, ''); } catch { /* ok */ }
|
|
336
|
+
process.stdout.write(
|
|
337
|
+
'[code-graph] Plugin installed — MCP server requires a session restart to connect.\n' +
|
|
338
|
+
'MCP servers are only initialized at session startup. To activate:\n' +
|
|
339
|
+
' 1. Press Ctrl+C to exit the current session\n' +
|
|
340
|
+
' 2. Re-run `claude` to start a new session\n' +
|
|
341
|
+
'Meanwhile, CLI tools work directly: code-graph-mcp search <query>, code-graph-mcp map, etc.\n'
|
|
342
|
+
);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (computeQuietHooks()) return;
|
|
347
|
+
|
|
348
|
+
// --- Read user message ---
|
|
349
|
+
let message;
|
|
350
|
+
try {
|
|
351
|
+
const input = JSON.parse(fs.readFileSync('/dev/stdin', 'utf8'));
|
|
352
|
+
message = (input && input.message) || '';
|
|
353
|
+
} catch {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
// Chinese chars are ~3 bytes but 1 char; "看看 fts5_search" is only 16 chars
|
|
357
|
+
if (!message || message.length < 8) return;
|
|
358
|
+
|
|
359
|
+
// --- Check index ---
|
|
360
|
+
const cwd = process.cwd();
|
|
361
|
+
const dbPath = path.join(cwd, '.code-graph', 'index.db');
|
|
362
|
+
if (!fs.existsSync(dbPath)) return;
|
|
363
|
+
|
|
364
|
+
if (shouldSkip(message)) return;
|
|
157
365
|
|
|
158
366
|
const filePaths = extractFilePaths(message);
|
|
159
367
|
const symbols = extractSymbols(message);
|
|
160
368
|
const intents = detectIntents(message);
|
|
161
369
|
const query = determineQueryType(intents, symbols, filePaths, isCoolingDown);
|
|
162
370
|
|
|
163
|
-
if (!query)
|
|
371
|
+
if (!query) return;
|
|
164
372
|
|
|
165
373
|
const PREFIXES = {
|
|
166
374
|
impact: '[code-graph:impact] Blast radius — review before editing:',
|
|
@@ -169,6 +377,15 @@ if (require.main === module) {
|
|
|
169
377
|
search: '[code-graph:search] Relevant code:',
|
|
170
378
|
};
|
|
171
379
|
|
|
380
|
+
function run(cmd, args) {
|
|
381
|
+
return execFileSync(cmd, args, {
|
|
382
|
+
cwd,
|
|
383
|
+
timeout: 3000,
|
|
384
|
+
encoding: 'utf8',
|
|
385
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
172
389
|
try {
|
|
173
390
|
let result = '';
|
|
174
391
|
if (query.type === 'impact') result = run('code-graph-mcp', ['impact', query.symbol]);
|
|
@@ -181,19 +398,12 @@ if (require.main === module) {
|
|
|
181
398
|
process.stdout.write(`${PREFIXES[query.type]}\n${result.trim()}\n`);
|
|
182
399
|
}
|
|
183
400
|
} catch {
|
|
184
|
-
|
|
401
|
+
/* return silently */
|
|
185
402
|
}
|
|
186
403
|
}
|
|
187
404
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
// --- Helpers ---
|
|
191
|
-
|
|
192
|
-
function run(cmd, args) {
|
|
193
|
-
return execFileSync(cmd, args, {
|
|
194
|
-
cwd,
|
|
195
|
-
timeout: 3000,
|
|
196
|
-
encoding: 'utf8',
|
|
197
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
198
|
-
});
|
|
405
|
+
if (require.main === module) {
|
|
406
|
+
runMain();
|
|
199
407
|
}
|
|
408
|
+
|
|
409
|
+
module.exports = { shouldSkip, extractFilePaths, extractSymbols, detectIntents, scoreIntent, INTENT_PATTERNS, INTENT_THRESHOLD, determineQueryType, computeQuietHooks, STOP_WORDS, PLAIN_WORD_EXCLUDE };
|
|
@@ -9,7 +9,11 @@ const {
|
|
|
9
9
|
extractFilePaths,
|
|
10
10
|
extractSymbols,
|
|
11
11
|
detectIntents,
|
|
12
|
+
scoreIntent,
|
|
13
|
+
INTENT_PATTERNS,
|
|
14
|
+
INTENT_THRESHOLD,
|
|
12
15
|
determineQueryType,
|
|
16
|
+
computeQuietHooks,
|
|
13
17
|
} = require('./user-prompt-context');
|
|
14
18
|
|
|
15
19
|
// ── shouldSkip ──────────────────────────────────────────
|
|
@@ -250,6 +254,48 @@ test('detectIntents: search (ZH)', () => {
|
|
|
250
254
|
assert.ok(detectIntents('在哪里用了这个常量').search);
|
|
251
255
|
});
|
|
252
256
|
|
|
257
|
+
// --- Per-keyword scoring (v0.21 weighted-scorer refactor) ---
|
|
258
|
+
test('scoreIntent: matched keyword returns its weight, unmatched returns 0', () => {
|
|
259
|
+
// Each pattern in INTENT_PATTERNS is testable in isolation now.
|
|
260
|
+
assert.equal(scoreIntent('this bug is critical', 'impact'), 1.0);
|
|
261
|
+
assert.equal(scoreIntent('hello world', 'impact'), 0);
|
|
262
|
+
assert.equal(scoreIntent('refactor this module', 'modify'), 1.0);
|
|
263
|
+
assert.equal(scoreIntent('refactor this module', 'implement'), 0);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('scoreIntent: max weight wins when multiple patterns match', () => {
|
|
267
|
+
// "this bug needs a fix and impact analysis" matches `impact`, `bug`,
|
|
268
|
+
// `risk`-no, all three impact rows are weight 1.0 currently — score is 1.0.
|
|
269
|
+
// Spec: scoreIntent returns max(weight) of matching patterns, never sum.
|
|
270
|
+
const score = scoreIntent('this bug needs impact analysis', 'impact');
|
|
271
|
+
assert.equal(score, 1.0);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test('scoreIntent: unknown intent returns 0 (no throw)', () => {
|
|
275
|
+
assert.equal(scoreIntent('anything', 'nonexistent_intent'), 0);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test('INTENT_PATTERNS: every intent has at least 5 patterns and uniform weights', () => {
|
|
279
|
+
// v0.21 starts with uniform weights; future tuning can vary them per-pattern.
|
|
280
|
+
// This test guards against regression to the giant single-regex form.
|
|
281
|
+
const intents = ['impact', 'modify', 'implement', 'understand', 'callgraph', 'search'];
|
|
282
|
+
for (const intent of intents) {
|
|
283
|
+
const patterns = INTENT_PATTERNS[intent];
|
|
284
|
+
assert.ok(Array.isArray(patterns), `${intent} must have patterns array`);
|
|
285
|
+
assert.ok(patterns.length >= 5, `${intent} must have >=5 patterns, got ${patterns.length}`);
|
|
286
|
+
for (const [pattern, weight] of patterns) {
|
|
287
|
+
assert.ok(pattern instanceof RegExp, `${intent} pattern must be RegExp`);
|
|
288
|
+
assert.ok(typeof weight === 'number' && weight > 0 && weight <= 1, `${intent} weight must be (0, 1]`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test('INTENT_THRESHOLD is 0.5 — single weight-1.0 match fires the intent', () => {
|
|
294
|
+
// Threshold contract: any pattern @ weight >= 0.5 → intent fires.
|
|
295
|
+
// If we lower a pattern to weight 0.4, it must NOT fire alone.
|
|
296
|
+
assert.equal(INTENT_THRESHOLD, 0.5);
|
|
297
|
+
});
|
|
298
|
+
|
|
253
299
|
// --- No false positives ---
|
|
254
300
|
test('detectIntents: simple confirmations have no code intent', () => {
|
|
255
301
|
const r = detectIntents('好的');
|
|
@@ -479,3 +525,48 @@ test('CODE_GRAPH_QUIET_HOOKS=1 short-circuits silently on stdout, stderr, exit 0
|
|
|
479
525
|
assert.equal(proc.stderr, '', 'stderr must be empty');
|
|
480
526
|
assert.equal(proc.status, 0, 'must exit 0');
|
|
481
527
|
});
|
|
528
|
+
|
|
529
|
+
// ── computeQuietHooks priority chain (v0.21 opt-in flip) ────────
|
|
530
|
+
|
|
531
|
+
test('computeQuietHooks: default (no env) is QUIET', () => {
|
|
532
|
+
// v0.21: flipped from opt-out to opt-in. Routing-bench P@1=100% earned
|
|
533
|
+
// the right to stop pushing context the agent would have requested.
|
|
534
|
+
assert.equal(computeQuietHooks({}), true);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
test('computeQuietHooks: CODE_GRAPH_VERBOSE_HOOKS=1 enables push (opt-in)', () => {
|
|
538
|
+
assert.equal(computeQuietHooks({ CODE_GRAPH_VERBOSE_HOOKS: '1' }), false);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
test('computeQuietHooks: legacy CODE_GRAPH_QUIET_HOOKS=0 forces noisy (back-compat)', () => {
|
|
542
|
+
assert.equal(computeQuietHooks({ CODE_GRAPH_QUIET_HOOKS: '0' }), false);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
test('computeQuietHooks: legacy CODE_GRAPH_QUIET_HOOKS=1 forces quiet (back-compat)', () => {
|
|
546
|
+
assert.equal(computeQuietHooks({ CODE_GRAPH_QUIET_HOOKS: '1' }), true);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
test('computeQuietHooks: legacy QUIET_HOOKS=0 wins over VERBOSE_HOOKS=1 (priority chain)', () => {
|
|
550
|
+
// Priority order: CODE_GRAPH_QUIET_HOOKS=0/1 > CODE_GRAPH_VERBOSE_HOOKS > default.
|
|
551
|
+
assert.equal(computeQuietHooks({ CODE_GRAPH_QUIET_HOOKS: '0', CODE_GRAPH_VERBOSE_HOOKS: '0' }), false);
|
|
552
|
+
assert.equal(computeQuietHooks({ CODE_GRAPH_QUIET_HOOKS: '1', CODE_GRAPH_VERBOSE_HOOKS: '1' }), true);
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
test('default env (no flags) short-circuits silently — opt-in flip', () => {
|
|
556
|
+
// End-to-end check: with the opt-in flip, a default-env spawn produces
|
|
557
|
+
// no stdout/stderr even on a message that previously would have injected.
|
|
558
|
+
const { spawnSync } = require('node:child_process');
|
|
559
|
+
const script = path.join(__dirname, 'user-prompt-context.js');
|
|
560
|
+
const cleanEnv = { ...process.env };
|
|
561
|
+
delete cleanEnv.CODE_GRAPH_QUIET_HOOKS;
|
|
562
|
+
delete cleanEnv.CODE_GRAPH_VERBOSE_HOOKS;
|
|
563
|
+
const proc = spawnSync(process.execPath, [script], {
|
|
564
|
+
input: JSON.stringify({ message: 'impact of refactoring parse_code function' }),
|
|
565
|
+
env: cleanEnv,
|
|
566
|
+
encoding: 'utf8',
|
|
567
|
+
timeout: 2000,
|
|
568
|
+
});
|
|
569
|
+
assert.equal(proc.stdout, '', 'default must be silent on stdout');
|
|
570
|
+
assert.equal(proc.stderr, '', 'default must be silent on stderr');
|
|
571
|
+
assert.equal(proc.status, 0, 'default must exit 0');
|
|
572
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdsrs/code-graph",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.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.21.0",
|
|
39
|
+
"@sdsrs/code-graph-linux-arm64": "0.21.0",
|
|
40
|
+
"@sdsrs/code-graph-darwin-x64": "0.21.0",
|
|
41
|
+
"@sdsrs/code-graph-darwin-arm64": "0.21.0",
|
|
42
|
+
"@sdsrs/code-graph-win32-x64": "0.21.0"
|
|
43
43
|
}
|
|
44
44
|
}
|