@sdsrs/code-graph 0.7.12 → 0.7.14
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/commands/impact.md +1 -1
- package/claude-plugin/commands/rebuild.md +1 -1
- package/claude-plugin/commands/status.md +1 -1
- package/claude-plugin/commands/trace.md +1 -1
- package/claude-plugin/commands/understand.md +1 -1
- package/claude-plugin/hooks/hooks.json +2 -54
- package/claude-plugin/scripts/lifecycle.js +114 -12
- package/claude-plugin/scripts/pre-edit-guide.js +39 -0
- package/claude-plugin/scripts/user-prompt-context.js +10 -2
- package/package.json +8 -7
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Force
|
|
2
|
+
description: Force code-graph index rebuild. Use when search results seem stale or wrong, after major codebase restructuring, or when index health check reports issues.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
Run via Bash: `code-graph-mcp incremental-index`
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Show code-graph index
|
|
2
|
+
description: Show code-graph index health and coverage. Use when search returns unexpected results, checking if index is current, or diagnosing code-graph issues.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
!`code-graph-mcp health-check --format json 2>/dev/null || echo '{"error":"No index found"}'`
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Deep dive into a module or
|
|
2
|
+
description: Deep dive into a module's architecture. Use when starting work in an unfamiliar area, asked to explain how code works, or before implementing changes in a module.
|
|
3
3
|
argument-hint: <file_or_dir_path>
|
|
4
4
|
---
|
|
5
5
|
|
|
@@ -1,56 +1,4 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
|
|
4
|
-
{
|
|
5
|
-
"matcher": "tool == \"Edit\"",
|
|
6
|
-
"hooks": [
|
|
7
|
-
{
|
|
8
|
-
"type": "command",
|
|
9
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pre-edit-guide.js\"",
|
|
10
|
-
"timeout": 4
|
|
11
|
-
}
|
|
12
|
-
],
|
|
13
|
-
"description": "Auto-inject impact analysis when editing function definitions with 2+ callers"
|
|
14
|
-
}
|
|
15
|
-
],
|
|
16
|
-
"PostToolUse": [
|
|
17
|
-
{
|
|
18
|
-
"matcher": "tool == \"Write\" || tool == \"Edit\"",
|
|
19
|
-
"hooks": [
|
|
20
|
-
{
|
|
21
|
-
"type": "command",
|
|
22
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/incremental-index.js\"",
|
|
23
|
-
"timeout": 10
|
|
24
|
-
}
|
|
25
|
-
],
|
|
26
|
-
"description": "Auto-update code graph index after file edits"
|
|
27
|
-
}
|
|
28
|
-
],
|
|
29
|
-
"UserPromptSubmit": [
|
|
30
|
-
{
|
|
31
|
-
"matcher": "",
|
|
32
|
-
"hooks": [
|
|
33
|
-
{
|
|
34
|
-
"type": "command",
|
|
35
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/user-prompt-context.js\"",
|
|
36
|
-
"timeout": 5
|
|
37
|
-
}
|
|
38
|
-
],
|
|
39
|
-
"description": "Inject code-graph structural context (impact, overview, callgraph) based on user intent"
|
|
40
|
-
}
|
|
41
|
-
],
|
|
42
|
-
"SessionStart": [
|
|
43
|
-
{
|
|
44
|
-
"matcher": "startup",
|
|
45
|
-
"hooks": [
|
|
46
|
-
{
|
|
47
|
-
"type": "command",
|
|
48
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/session-init.js\"",
|
|
49
|
-
"timeout": 5
|
|
50
|
-
}
|
|
51
|
-
],
|
|
52
|
-
"description": "StatusLine self-heal, lifecycle sync, project map injection at session start"
|
|
53
|
-
}
|
|
54
|
-
]
|
|
55
|
-
}
|
|
2
|
+
"_note": "Hooks registered to settings.json by lifecycle.js — hooks.json auto-loading is unreliable",
|
|
3
|
+
"hooks": {}
|
|
56
4
|
}
|
|
@@ -150,7 +150,8 @@ function cleanupDisabledStatusline() {
|
|
|
150
150
|
return { cleaned: false, settingsChanged: false };
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
|
|
153
|
+
let settingsChanged = detachStatuslineIntegration(settings);
|
|
154
|
+
if (removeHooksFromSettings(settings)) settingsChanged = true;
|
|
154
155
|
if (settingsChanged) {
|
|
155
156
|
writeJsonAtomic(SETTINGS_PATH, settings);
|
|
156
157
|
}
|
|
@@ -216,6 +217,91 @@ function migrateOldPluginIds(settings) {
|
|
|
216
217
|
return changed;
|
|
217
218
|
}
|
|
218
219
|
|
|
220
|
+
// --- Hook Registration ---
|
|
221
|
+
// Plugin system's hooks.json auto-loading is unreliable (observed across GSD,
|
|
222
|
+
// superpowers, code-graph-mcp). Write hooks directly to settings.json instead.
|
|
223
|
+
// Same strategy as claude-mem-lite. hooks.json is kept empty to prevent double-firing.
|
|
224
|
+
|
|
225
|
+
const OUR_HOOK_SCRIPTS = ['session-init.js', 'incremental-index.js', 'user-prompt-context.js', 'pre-edit-guide.js'];
|
|
226
|
+
|
|
227
|
+
function isOurHookEntry(entry) {
|
|
228
|
+
if (!entry || !entry.hooks) return false;
|
|
229
|
+
return entry.hooks.some(h =>
|
|
230
|
+
h.command && OUR_HOOK_SCRIPTS.some(s => h.command.includes(s)) &&
|
|
231
|
+
h.command.includes('code-graph')
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function hookCommand(scriptName) {
|
|
236
|
+
return `node ${JSON.stringify(path.join(PLUGIN_ROOT, 'scripts', scriptName))}`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function getHookDefinitions() {
|
|
240
|
+
return {
|
|
241
|
+
SessionStart: [{
|
|
242
|
+
matcher: 'startup|clear|compact',
|
|
243
|
+
hooks: [{ type: 'command', command: hookCommand('session-init.js'), timeout: 5 }],
|
|
244
|
+
description: 'StatusLine self-heal, lifecycle sync, project map injection',
|
|
245
|
+
}],
|
|
246
|
+
PreToolUse: [{
|
|
247
|
+
matcher: 'tool == "Edit"',
|
|
248
|
+
hooks: [{ type: 'command', command: hookCommand('pre-edit-guide.js'), timeout: 4 }],
|
|
249
|
+
description: 'Auto-inject impact analysis when editing functions with 2+ callers',
|
|
250
|
+
}],
|
|
251
|
+
PostToolUse: [{
|
|
252
|
+
matcher: 'tool == "Write" || tool == "Edit"',
|
|
253
|
+
hooks: [{ type: 'command', command: hookCommand('incremental-index.js'), timeout: 10 }],
|
|
254
|
+
description: 'Auto-update code graph index after file edits',
|
|
255
|
+
}],
|
|
256
|
+
UserPromptSubmit: [{
|
|
257
|
+
matcher: '',
|
|
258
|
+
hooks: [{ type: 'command', command: hookCommand('user-prompt-context.js'), timeout: 5 }],
|
|
259
|
+
description: 'Inject code-graph structural context based on user intent',
|
|
260
|
+
}],
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function registerHooksToSettings(settings) {
|
|
265
|
+
if (!settings.hooks) settings.hooks = {};
|
|
266
|
+
const defs = getHookDefinitions();
|
|
267
|
+
let changed = false;
|
|
268
|
+
|
|
269
|
+
for (const [event, newEntries] of Object.entries(defs)) {
|
|
270
|
+
if (!settings.hooks[event]) settings.hooks[event] = [];
|
|
271
|
+
|
|
272
|
+
for (const newEntry of newEntries) {
|
|
273
|
+
const existingIdx = settings.hooks[event].findIndex(e => isOurHookEntry(e));
|
|
274
|
+
if (existingIdx >= 0) {
|
|
275
|
+
if (JSON.stringify(settings.hooks[event][existingIdx]) !== JSON.stringify(newEntry)) {
|
|
276
|
+
settings.hooks[event][existingIdx] = newEntry;
|
|
277
|
+
changed = true;
|
|
278
|
+
}
|
|
279
|
+
} else {
|
|
280
|
+
settings.hooks[event].push(newEntry);
|
|
281
|
+
changed = true;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return changed;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function removeHooksFromSettings(settings) {
|
|
290
|
+
if (!settings.hooks) return false;
|
|
291
|
+
let changed = false;
|
|
292
|
+
|
|
293
|
+
for (const event of Object.keys(settings.hooks)) {
|
|
294
|
+
if (!Array.isArray(settings.hooks[event])) continue;
|
|
295
|
+
const before = settings.hooks[event].length;
|
|
296
|
+
settings.hooks[event] = settings.hooks[event].filter(e => !isOurHookEntry(e));
|
|
297
|
+
if (settings.hooks[event].length !== before) changed = true;
|
|
298
|
+
if (settings.hooks[event].length === 0) delete settings.hooks[event];
|
|
299
|
+
}
|
|
300
|
+
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
301
|
+
|
|
302
|
+
return changed;
|
|
303
|
+
}
|
|
304
|
+
|
|
219
305
|
// --- Install (idempotent) ---
|
|
220
306
|
|
|
221
307
|
function install() {
|
|
@@ -247,16 +333,21 @@ function install() {
|
|
|
247
333
|
// Register code-graph provider
|
|
248
334
|
registerStatuslineProvider('code-graph', codeGraphStatuslineCommand(), false);
|
|
249
335
|
|
|
336
|
+
// 2. Hooks — register to settings.json (hooks.json auto-loading unreliable)
|
|
337
|
+
if (registerHooksToSettings(settings)) {
|
|
338
|
+
settingsChanged = true;
|
|
339
|
+
}
|
|
340
|
+
|
|
250
341
|
// NOTE: enabledPlugins is managed by Claude Code's plugin system, not by lifecycle.
|
|
251
342
|
// Do NOT add enabledPlugins entries here — it causes phantom plugin entries
|
|
252
343
|
// when the ID doesn't match the marketplace name.
|
|
253
344
|
|
|
254
|
-
//
|
|
345
|
+
// 3. Write settings atomically if changed
|
|
255
346
|
if (settingsChanged) {
|
|
256
347
|
writeJsonAtomic(SETTINGS_PATH, settings);
|
|
257
348
|
}
|
|
258
349
|
|
|
259
|
-
//
|
|
350
|
+
// 4. Write manifest with version
|
|
260
351
|
manifest.version = version;
|
|
261
352
|
manifest.installedAt = manifest.installedAt || new Date().toISOString();
|
|
262
353
|
manifest.updatedAt = new Date().toISOString();
|
|
@@ -277,7 +368,12 @@ function uninstall() {
|
|
|
277
368
|
settingsChanged = true;
|
|
278
369
|
}
|
|
279
370
|
|
|
280
|
-
// 2.
|
|
371
|
+
// 2. Hooks: remove from settings.json
|
|
372
|
+
if (removeHooksFromSettings(settings)) {
|
|
373
|
+
settingsChanged = true;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// 3. Remove all known IDs from enabledPlugins
|
|
281
377
|
if (settings.enabledPlugins) {
|
|
282
378
|
for (const id of [PLUGIN_ID, ...OLD_PLUGIN_IDS]) {
|
|
283
379
|
if (id in settings.enabledPlugins) {
|
|
@@ -287,13 +383,13 @@ function uninstall() {
|
|
|
287
383
|
}
|
|
288
384
|
}
|
|
289
385
|
|
|
290
|
-
//
|
|
386
|
+
// 4. Write settings if changed
|
|
291
387
|
if (settingsChanged) {
|
|
292
388
|
writeJsonAtomic(SETTINGS_PATH, settings);
|
|
293
389
|
}
|
|
294
390
|
}
|
|
295
391
|
|
|
296
|
-
//
|
|
392
|
+
// 5. Remove all known IDs from installed_plugins.json
|
|
297
393
|
const installedPlugins = readJson(INSTALLED_PLUGINS_PATH);
|
|
298
394
|
if (installedPlugins && installedPlugins.plugins) {
|
|
299
395
|
let ipChanged = false;
|
|
@@ -306,10 +402,10 @@ function uninstall() {
|
|
|
306
402
|
if (ipChanged) writeJsonAtomic(INSTALLED_PLUGINS_PATH, installedPlugins);
|
|
307
403
|
}
|
|
308
404
|
|
|
309
|
-
//
|
|
405
|
+
// 6. Remove cache directory
|
|
310
406
|
try { fs.rmSync(CACHE_DIR, { recursive: true, force: true }); } catch { /* ok */ }
|
|
311
407
|
|
|
312
|
-
//
|
|
408
|
+
// 7. Remove plugin files from cache (all known paths, including parent dirs)
|
|
313
409
|
const pluginCacheDirs = [
|
|
314
410
|
path.join(os.homedir(), '.claude', 'plugins', 'cache', MARKETPLACE_NAME),
|
|
315
411
|
path.join(os.homedir(), '.claude', 'plugins', 'cache', 'sdsrss-code-graph'),
|
|
@@ -348,23 +444,28 @@ function update() {
|
|
|
348
444
|
// 2. Update code-graph provider in registry
|
|
349
445
|
registerStatuslineProvider('code-graph', codeGraphStatuslineCommand(), false);
|
|
350
446
|
|
|
447
|
+
// 3. Hooks — update command paths
|
|
448
|
+
if (registerHooksToSettings(settings)) {
|
|
449
|
+
settingsChanged = true;
|
|
450
|
+
}
|
|
451
|
+
|
|
351
452
|
// NOTE: enabledPlugins is managed by Claude Code's plugin system, not by lifecycle.
|
|
352
453
|
|
|
353
|
-
//
|
|
454
|
+
// 4. Write settings if changed
|
|
354
455
|
if (settingsChanged) {
|
|
355
456
|
writeJsonAtomic(SETTINGS_PATH, settings);
|
|
356
457
|
}
|
|
357
458
|
|
|
358
|
-
//
|
|
459
|
+
// 5. Clear update-check cache (force re-check after update)
|
|
359
460
|
const updateCache = path.join(CACHE_DIR, 'update-check');
|
|
360
461
|
try { fs.unlinkSync(updateCache); } catch { /* ok */ }
|
|
361
462
|
|
|
362
|
-
//
|
|
463
|
+
// 6. Update manifest
|
|
363
464
|
manifest.version = version;
|
|
364
465
|
manifest.updatedAt = new Date().toISOString();
|
|
365
466
|
writeManifest(manifest);
|
|
366
467
|
|
|
367
|
-
//
|
|
468
|
+
// 7. Clean up old cached versions (keep latest 3)
|
|
368
469
|
cleanupOldCacheVersions(3);
|
|
369
470
|
|
|
370
471
|
return { oldVersion, version, settingsChanged };
|
|
@@ -411,6 +512,7 @@ module.exports = {
|
|
|
411
512
|
readManifest, readJson, writeJsonAtomic,
|
|
412
513
|
readRegistry, writeRegistry,
|
|
413
514
|
getPluginVersion, cleanupOldCacheVersions,
|
|
515
|
+
registerHooksToSettings, removeHooksFromSettings, getHookDefinitions,
|
|
414
516
|
PLUGIN_ID, OLD_PLUGIN_IDS, MARKETPLACE_NAME, CACHE_DIR, REGISTRY_FILE,
|
|
415
517
|
};
|
|
416
518
|
|
|
@@ -47,6 +47,45 @@ for (const pat of fnPatterns) {
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
// Fallback: if old_string is inside a function body (not a definition),
|
|
51
|
+
// extract a unique identifier from the code and grep for it to find the containing function
|
|
52
|
+
if (!symbol || symbol.length < 3) {
|
|
53
|
+
const filePath = (input.tool_input && input.tool_input.file_path) || '';
|
|
54
|
+
if (filePath && oldStr.length >= 10) {
|
|
55
|
+
try {
|
|
56
|
+
// Extract identifiers from old_string, try the most specific one first
|
|
57
|
+
const identifiers = (oldStr.match(/\b([a-z]\w*(?:_\w+)+|[a-z]\w*(?:[A-Z]\w*)+|[A-Z]\w+\.\w+|[A-Z]\w+::\w+)\b/g) || [])
|
|
58
|
+
.filter(id => id.length >= 6);
|
|
59
|
+
const skipWords = new Set(['return', 'function', 'default', 'require', 'module', 'exports', 'import', 'console']);
|
|
60
|
+
// Sort by length descending (longer = more specific = fewer matches)
|
|
61
|
+
const candidates = [...new Set(identifiers)]
|
|
62
|
+
.filter(id => !skipWords.has(id.toLowerCase()))
|
|
63
|
+
.sort((a, b) => b.length - a.length);
|
|
64
|
+
for (const candidate of candidates.slice(0, 5)) {
|
|
65
|
+
try {
|
|
66
|
+
const raw = execFileSync('code-graph-mcp', ['grep', candidate, filePath, '--json'], {
|
|
67
|
+
cwd, timeout: 2000, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
68
|
+
});
|
|
69
|
+
const grepResult = JSON.parse(raw);
|
|
70
|
+
// Pick this candidate if it has few matches (precise location)
|
|
71
|
+
const withContainer = (grepResult || []).filter(m => m.container && m.container.name);
|
|
72
|
+
if (withContainer.length > 0 && withContainer.length <= 5) {
|
|
73
|
+
// If multiple containers, vote for the most common one
|
|
74
|
+
const votes = {};
|
|
75
|
+
for (const m of withContainer) {
|
|
76
|
+
const cn = m.container.name;
|
|
77
|
+
votes[cn] = (votes[cn] || 0) + 1;
|
|
78
|
+
}
|
|
79
|
+
const best = Object.entries(votes).sort((a, b) => b[1] - a[1])[0][0];
|
|
80
|
+
symbol = best.includes('.') ? best.split('.').pop() : best.includes('::') ? best.split('::').pop() : best;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
} catch { /* try next candidate */ }
|
|
84
|
+
}
|
|
85
|
+
} catch { /* grep failed or no match — fall through */ }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
50
89
|
if (!symbol || symbol.length < 3) process.exit(0);
|
|
51
90
|
|
|
52
91
|
// Skip common patterns that aren't real function names
|
|
@@ -95,12 +95,20 @@ if (/^(修复|优化|实施|执行|开始|按|实测|帮我|进入|用|重新)/.
|
|
|
95
95
|
const filePaths = (message.match(/(?:src|lib|test|pkg|cmd|internal|app|components?)\/[\w/.-]+/g) || [])
|
|
96
96
|
.slice(0, 2);
|
|
97
97
|
|
|
98
|
-
// Extract potential symbol names (camelCase, snake_case, PascalCase, qualified like Foo::bar)
|
|
99
|
-
const symbolCandidates = (message.match(/\b(?:[A-Z]\w*(
|
|
98
|
+
// Extract potential symbol names (camelCase, snake_case, PascalCase, qualified like Foo::bar, Foo.bar, Foo::bar::baz)
|
|
99
|
+
const symbolCandidates = (message.match(/\b(?:[A-Z]\w*(?:(?:::|\.)\w+)+|[a-z]\w*(?:_\w+){1,}|[a-z]\w*(?:[A-Z]\w*)+|[A-Z][a-z]+(?:[A-Z][a-z]+)+)\b/g) || [])
|
|
100
100
|
.filter(s => s.length > 4)
|
|
101
101
|
.filter(s => !STOP_WORDS.has(s.toLowerCase()))
|
|
102
102
|
.slice(0, 3);
|
|
103
103
|
|
|
104
|
+
// Fallback: extract backtick-quoted symbols (common in mixed Chinese+code: "修改 `parse_code` 函数")
|
|
105
|
+
if (symbolCandidates.length === 0) {
|
|
106
|
+
const backtickSymbols = (message.match(/`([a-zA-Z_]\w{2,})`/g) || [])
|
|
107
|
+
.map(s => s.replace(/`/g, ''))
|
|
108
|
+
.filter(s => s.length >= 3 && !STOP_WORDS.has(s.toLowerCase()));
|
|
109
|
+
symbolCandidates.push(...backtickSymbols.slice(0, 3));
|
|
110
|
+
}
|
|
111
|
+
|
|
104
112
|
// Fallback: plain lowercase words (8+ chars) likely to be function/type names.
|
|
105
113
|
// Only when strict patterns found nothing — avoids false positives from English prose.
|
|
106
114
|
// Minimum 8 chars filters most common English words while keeping technical terms
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdsrs/code-graph",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.14",
|
|
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": {
|
|
@@ -27,16 +27,17 @@
|
|
|
27
27
|
"claude-plugin"
|
|
28
28
|
],
|
|
29
29
|
"scripts": {
|
|
30
|
-
"build": "cargo build --release --no-default-features && node scripts/copy-binary.js"
|
|
30
|
+
"build": "cargo build --release --no-default-features && node scripts/copy-binary.js",
|
|
31
|
+
"prepare": "git rev-parse --git-dir > /dev/null 2>&1 && ln -sf ../../scripts/pre-commit.sh .git/hooks/pre-commit || true"
|
|
31
32
|
},
|
|
32
33
|
"engines": {
|
|
33
34
|
"node": ">=16"
|
|
34
35
|
},
|
|
35
36
|
"optionalDependencies": {
|
|
36
|
-
"@sdsrs/code-graph-linux-x64": "0.7.
|
|
37
|
-
"@sdsrs/code-graph-linux-arm64": "0.7.
|
|
38
|
-
"@sdsrs/code-graph-darwin-x64": "0.7.
|
|
39
|
-
"@sdsrs/code-graph-darwin-arm64": "0.7.
|
|
40
|
-
"@sdsrs/code-graph-win32-x64": "0.7.
|
|
37
|
+
"@sdsrs/code-graph-linux-x64": "0.7.14",
|
|
38
|
+
"@sdsrs/code-graph-linux-arm64": "0.7.14",
|
|
39
|
+
"@sdsrs/code-graph-darwin-x64": "0.7.14",
|
|
40
|
+
"@sdsrs/code-graph-darwin-arm64": "0.7.14",
|
|
41
|
+
"@sdsrs/code-graph-win32-x64": "0.7.14"
|
|
41
42
|
}
|
|
42
43
|
}
|