@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.
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "sdsrs"
6
6
  },
7
- "version": "0.7.12",
7
+ "version": "0.7.14",
8
8
  "keywords": [
9
9
  "code-graph",
10
10
  "ast",
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Analyze impact scope before modifying a symbol
2
+ description: Analyze blast radius before modifying a symbol. Use when about to edit/rename/remove a function, or asked about change risk and affected callers.
3
3
  argument-hint: <symbol_name>
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Force a full code-graph index rebuild
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 status
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: Trace call flow from a handler or route
2
+ description: Trace call flow from a handler or route. Use when debugging API behavior, understanding request processing flow, or asked how an endpoint works.
3
3
  argument-hint: <handler_or_route>
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Deep dive into a module or file's architecture
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
- "hooks": {
3
- "PreToolUse": [
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
- const settingsChanged = detachStatuslineIntegration(settings);
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
- // 2. Write settings atomically if changed
345
+ // 3. Write settings atomically if changed
255
346
  if (settingsChanged) {
256
347
  writeJsonAtomic(SETTINGS_PATH, settings);
257
348
  }
258
349
 
259
- // 3. Write manifest with version
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. Remove all known IDs from enabledPlugins
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
- // 3. Write settings if changed
386
+ // 4. Write settings if changed
291
387
  if (settingsChanged) {
292
388
  writeJsonAtomic(SETTINGS_PATH, settings);
293
389
  }
294
390
  }
295
391
 
296
- // 4. Remove all known IDs from installed_plugins.json
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
- // 5. Remove cache directory
405
+ // 6. Remove cache directory
310
406
  try { fs.rmSync(CACHE_DIR, { recursive: true, force: true }); } catch { /* ok */ }
311
407
 
312
- // 6. Remove plugin files from cache (all known paths, including parent dirs)
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
- // 3. Write settings if changed
454
+ // 4. Write settings if changed
354
455
  if (settingsChanged) {
355
456
  writeJsonAtomic(SETTINGS_PATH, settings);
356
457
  }
357
458
 
358
- // 4. Clear update-check cache (force re-check after update)
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
- // 5. Update manifest
463
+ // 6. Update manifest
363
464
  manifest.version = version;
364
465
  manifest.updatedAt = new Date().toISOString();
365
466
  writeManifest(manifest);
366
467
 
367
- // 6. Clean up old cached versions (keep latest 3)
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*(?:::\w+)+|[a-z]\w*(?:_\w+){1,}|[a-z]\w*(?:[A-Z]\w*)+|[A-Z][a-z]+(?:[A-Z][a-z]+)+)\b/g) || [])
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.12",
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.12",
37
- "@sdsrs/code-graph-linux-arm64": "0.7.12",
38
- "@sdsrs/code-graph-darwin-x64": "0.7.12",
39
- "@sdsrs/code-graph-darwin-arm64": "0.7.12",
40
- "@sdsrs/code-graph-win32-x64": "0.7.12"
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
  }