@sdsrs/code-graph 0.8.1 → 0.8.3
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/README.md +17 -0
- package/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/claude-plugin/hooks/hooks.json +52 -3
- package/claude-plugin/scripts/adopt.js +4 -1
- package/claude-plugin/scripts/adopt.test.js +18 -0
- package/claude-plugin/scripts/doctor.js +36 -15
- package/claude-plugin/scripts/lifecycle.js +24 -163
- package/claude-plugin/scripts/lifecycle.test.js +80 -58
- package/claude-plugin/scripts/session-init.js +1 -32
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -146,6 +146,23 @@ Then reconnect the MCP server in Claude Code with `/mcp`.
|
|
|
146
146
|
|
|
147
147
|
> **Note:** Auto-update is disabled in the source repo directory (dev mode). Use manual update when developing the plugin itself.
|
|
148
148
|
|
|
149
|
+
#### Invited-memory mode (quieter prompts)
|
|
150
|
+
|
|
151
|
+
By default, every user prompt the plugin deems code-related gets a small context injection from `code-graph` CLI output. If you'd rather rely on MEMORY.md + explicit tool calls, opt into invited-memory mode:
|
|
152
|
+
|
|
153
|
+
1. Adopt the plugin contract into your project's memory index (idempotent, self-heals):
|
|
154
|
+
```bash
|
|
155
|
+
code-graph-mcp adopt
|
|
156
|
+
```
|
|
157
|
+
This writes `plugin_code_graph_mcp.md` (decision rules) into `~/.claude/projects/<slug>/memory/` and links it from `MEMORY.md` inside a sentinel block. Run `code-graph-mcp unadopt` to remove.
|
|
158
|
+
2. Set the activation env var in `~/.claude/settings.json`:
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"env": { "CODE_GRAPH_QUIET_HOOKS": "1" }
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
3. Restart Claude Code. Session startup skips the project-map injection, UserPromptSubmit stops auto-injecting context, and the MCP `instructions` become a short pointer to the MEMORY.md file.
|
|
165
|
+
|
|
149
166
|
### Option 2: Claude Code MCP Server Only
|
|
150
167
|
|
|
151
168
|
Register as an MCP server without the plugin features:
|
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "code-graph-mcp hooks",
|
|
3
|
-
"_note": "
|
|
4
|
-
"hooks": {
|
|
2
|
+
"description": "code-graph-mcp hooks — loaded directly by Claude Code from the plugin cache.",
|
|
3
|
+
"_note": "Authoritative source. settings.json is no longer used for hook registration as of v0.8.3 — session-init.js actively removes any legacy code-graph entries it finds there. Paths use ${CLAUDE_PLUGIN_ROOT} so they follow version directory updates automatically.",
|
|
4
|
+
"hooks": {
|
|
5
|
+
"SessionStart": [
|
|
6
|
+
{
|
|
7
|
+
"matcher": "startup|clear|compact",
|
|
8
|
+
"hooks": [
|
|
9
|
+
{
|
|
10
|
+
"type": "command",
|
|
11
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/session-init.js\"",
|
|
12
|
+
"timeout": 5
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"PreToolUse": [
|
|
18
|
+
{
|
|
19
|
+
"matcher": "tool == \"Edit\"",
|
|
20
|
+
"hooks": [
|
|
21
|
+
{
|
|
22
|
+
"type": "command",
|
|
23
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pre-edit-guide.js\"",
|
|
24
|
+
"timeout": 4
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
"PostToolUse": [
|
|
30
|
+
{
|
|
31
|
+
"matcher": "tool == \"Write\" || tool == \"Edit\"",
|
|
32
|
+
"hooks": [
|
|
33
|
+
{
|
|
34
|
+
"type": "command",
|
|
35
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/incremental-index.js\"",
|
|
36
|
+
"timeout": 10
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"UserPromptSubmit": [
|
|
42
|
+
{
|
|
43
|
+
"matcher": "",
|
|
44
|
+
"hooks": [
|
|
45
|
+
{
|
|
46
|
+
"type": "command",
|
|
47
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/user-prompt-context.js\"",
|
|
48
|
+
"timeout": 5
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
}
|
|
5
54
|
}
|
|
@@ -13,8 +13,11 @@ const INDEX_LINE = '- [code-graph-mcp](plugin_code_graph_mcp.md) — "谁调 X /
|
|
|
13
13
|
const TEMPLATE_PATH = path.resolve(__dirname, '..', 'templates', 'plugin_code_graph_mcp.md');
|
|
14
14
|
const TARGET_NAME = 'plugin_code_graph_mcp.md';
|
|
15
15
|
|
|
16
|
+
// Claude Code slug convention: every non-alphanumeric-non-hyphen char → `-`.
|
|
17
|
+
// `/mnt/data_ssd/dev/proj` → `-mnt-data-ssd-dev-proj`
|
|
18
|
+
// `/home/sds/.claude/x` → `-home-sds--claude-x` (double-dash from `/.`)
|
|
16
19
|
function memoryDir(cwd = process.cwd(), home = os.homedir()) {
|
|
17
|
-
const slug = cwd.replace(
|
|
20
|
+
const slug = cwd.replace(/[^a-zA-Z0-9-]/g, '-');
|
|
18
21
|
return path.join(home, '.claude', 'projects', slug, 'memory');
|
|
19
22
|
}
|
|
20
23
|
|
|
@@ -26,6 +26,24 @@ test('memoryDir slugifies cwd path', () => {
|
|
|
26
26
|
assert.strictEqual(dir, '/home/alice/.claude/projects/-home-alice-proj/memory');
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
+
test('memoryDir replaces underscores and dots (Claude Code slug convention)', () => {
|
|
30
|
+
// Real-world bug: /mnt/data_ssd/... needs data-ssd slug, not data_ssd
|
|
31
|
+
assert.strictEqual(
|
|
32
|
+
memoryDir('/mnt/data_ssd/dev/projects/code-graph-mcp', '/home/u'),
|
|
33
|
+
'/home/u/.claude/projects/-mnt-data-ssd-dev-projects-code-graph-mcp/memory'
|
|
34
|
+
);
|
|
35
|
+
// Hidden dirs: /home/sds/.claude/x → -home-sds--claude-x (double-dash)
|
|
36
|
+
assert.strictEqual(
|
|
37
|
+
memoryDir('/home/sds/.claude/x', '/home/sds'),
|
|
38
|
+
'/home/sds/.claude/projects/-home-sds--claude-x/memory'
|
|
39
|
+
);
|
|
40
|
+
// Preserves case and hyphens
|
|
41
|
+
assert.strictEqual(
|
|
42
|
+
memoryDir('/Users/Alice/my-Project_v2.1', '/'),
|
|
43
|
+
'/.claude/projects/-Users-Alice-my-Project-v2-1/memory'
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
29
47
|
test('adopt writes template and appends sentinel block when index absent', () => {
|
|
30
48
|
const sb = makeSandbox();
|
|
31
49
|
try {
|
|
@@ -7,7 +7,7 @@ const os = require('os');
|
|
|
7
7
|
const { readBinaryVersion, isDevMode, getNewestMtime } = require('./version-utils');
|
|
8
8
|
const {
|
|
9
9
|
getPluginVersion, readJson, healthCheck, CACHE_DIR,
|
|
10
|
-
|
|
10
|
+
removeHooksFromSettings, isOurHookEntry, writeJsonAtomic,
|
|
11
11
|
} = require('./lifecycle');
|
|
12
12
|
const { findBinary, clearCache: clearBinaryCache } = require('./find-binary');
|
|
13
13
|
|
|
@@ -166,24 +166,40 @@ function runDiagnostics() {
|
|
|
166
166
|
});
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
// 7.
|
|
169
|
+
// 7. Legacy hooks in settings.json — v0.8.2 and earlier wrote hooks there;
|
|
170
|
+
// cache/<ver>/hooks/hooks.json is now authoritative. Duplicates cause
|
|
171
|
+
// every hook to fire twice until settings.json is cleaned.
|
|
170
172
|
try {
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
173
|
+
const SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
|
|
174
|
+
const settings = readJson(SETTINGS_PATH) || {};
|
|
175
|
+
const legacyCount = countLegacyHookEntries(settings);
|
|
176
|
+
if (legacyCount === 0) {
|
|
177
|
+
results.push({ name: 'Legacy hooks', status: 'ok', detail: 'settings.json is clean' });
|
|
174
178
|
} else {
|
|
175
179
|
results.push({
|
|
176
|
-
name: '
|
|
180
|
+
name: 'Legacy hooks',
|
|
177
181
|
status: 'warn',
|
|
178
|
-
detail: `${
|
|
179
|
-
fixId: 'hooks-
|
|
182
|
+
detail: `${legacyCount} entries in settings.json (fire twice per event)`,
|
|
183
|
+
fixId: 'legacy-hooks-in-settings',
|
|
180
184
|
});
|
|
181
185
|
}
|
|
182
|
-
} catch { /*
|
|
186
|
+
} catch { /* probe failed — skip */ }
|
|
183
187
|
|
|
184
188
|
return results;
|
|
185
189
|
}
|
|
186
190
|
|
|
191
|
+
function countLegacyHookEntries(settings) {
|
|
192
|
+
if (!settings || !settings.hooks) return 0;
|
|
193
|
+
let count = 0;
|
|
194
|
+
for (const entries of Object.values(settings.hooks)) {
|
|
195
|
+
if (!Array.isArray(entries)) continue;
|
|
196
|
+
for (const entry of entries) {
|
|
197
|
+
if (isOurHookEntry(entry)) count++;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return count;
|
|
201
|
+
}
|
|
202
|
+
|
|
187
203
|
// ── Report Formatting ─────────────────────────────────────
|
|
188
204
|
|
|
189
205
|
const STATUS_ICONS = { ok: '\u2705', warn: '\u26a0\ufe0f', error: '\u274c', skip: '\u2796' };
|
|
@@ -339,12 +355,17 @@ function runRepairs(results) {
|
|
|
339
355
|
break;
|
|
340
356
|
}
|
|
341
357
|
|
|
342
|
-
case 'hooks-
|
|
343
|
-
console.log('\n
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
358
|
+
case 'legacy-hooks-in-settings': {
|
|
359
|
+
console.log('\n Removing legacy code-graph hooks from settings.json...');
|
|
360
|
+
const SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
|
|
361
|
+
const settings = readJson(SETTINGS_PATH) || {};
|
|
362
|
+
if (removeHooksFromSettings(settings)) {
|
|
363
|
+
writeJsonAtomic(SETTINGS_PATH, settings);
|
|
364
|
+
console.log(' \u2705 settings.json cleaned — restart Claude Code to apply');
|
|
365
|
+
fixed++;
|
|
366
|
+
} else {
|
|
367
|
+
console.log(' \u2796 No legacy entries found');
|
|
368
|
+
}
|
|
348
369
|
break;
|
|
349
370
|
}
|
|
350
371
|
|
|
@@ -220,92 +220,12 @@ function migrateOldPluginIds(settings) {
|
|
|
220
220
|
return changed;
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
// ---
|
|
224
|
-
// Claude Code loads hooks from
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
// every SessionStart (via session-init.js) as a second layer of defense.
|
|
230
|
-
|
|
231
|
-
const EMPTY_HOOKS_STUB = Object.freeze({
|
|
232
|
-
description: 'code-graph-mcp hooks',
|
|
233
|
-
_note: 'Hooks are registered to ~/.claude/settings.json by lifecycle.js. Cleared automatically to prevent double-firing.',
|
|
234
|
-
hooks: {},
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
function isOurPluginMarketplace(mpDir) {
|
|
238
|
-
try {
|
|
239
|
-
const meta = readJson(path.join(mpDir, '.claude-plugin', 'marketplace.json'));
|
|
240
|
-
if (meta && meta.name === MARKETPLACE_NAME) return true;
|
|
241
|
-
} catch { /* fallthrough */ }
|
|
242
|
-
return path.basename(mpDir) === MARKETPLACE_NAME;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function scanPluginHooksJsonCopies() {
|
|
246
|
-
const HOME = os.homedir();
|
|
247
|
-
const paths = [];
|
|
248
|
-
|
|
249
|
-
// Marketplace source (git-cloned by Claude Code on install)
|
|
250
|
-
const mpRoot = path.join(HOME, '.claude', 'plugins', 'marketplaces');
|
|
251
|
-
try {
|
|
252
|
-
for (const name of fs.readdirSync(mpRoot)) {
|
|
253
|
-
const mpDir = path.join(mpRoot, name);
|
|
254
|
-
try { if (!fs.statSync(mpDir).isDirectory()) continue; } catch { continue; }
|
|
255
|
-
if (!isOurPluginMarketplace(mpDir)) continue;
|
|
256
|
-
const p = path.join(mpDir, 'claude-plugin', 'hooks', 'hooks.json');
|
|
257
|
-
if (fs.existsSync(p)) paths.push(p);
|
|
258
|
-
}
|
|
259
|
-
} catch { /* no marketplaces dir */ }
|
|
260
|
-
|
|
261
|
-
// Cache (what Claude Code actually loads at runtime), per plugin + per version
|
|
262
|
-
const cacheRoot = path.join(HOME, '.claude', 'plugins', 'cache', MARKETPLACE_NAME);
|
|
263
|
-
try {
|
|
264
|
-
for (const pluginName of fs.readdirSync(cacheRoot)) {
|
|
265
|
-
const pluginDir = path.join(cacheRoot, pluginName);
|
|
266
|
-
try { if (!fs.statSync(pluginDir).isDirectory()) continue; } catch { continue; }
|
|
267
|
-
for (const ver of fs.readdirSync(pluginDir)) {
|
|
268
|
-
const verDir = path.join(pluginDir, ver);
|
|
269
|
-
try { if (!fs.statSync(verDir).isDirectory()) continue; } catch { continue; }
|
|
270
|
-
const p = path.join(verDir, 'hooks', 'hooks.json');
|
|
271
|
-
if (fs.existsSync(p)) paths.push(p);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
} catch { /* no cache dir */ }
|
|
275
|
-
|
|
276
|
-
return paths;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function findStalePluginHooksJson() {
|
|
280
|
-
const stale = [];
|
|
281
|
-
for (const p of scanPluginHooksJsonCopies()) {
|
|
282
|
-
try {
|
|
283
|
-
const cur = readJson(p);
|
|
284
|
-
if (cur && cur.hooks && typeof cur.hooks === 'object' && Object.keys(cur.hooks).length > 0) {
|
|
285
|
-
stale.push(p);
|
|
286
|
-
}
|
|
287
|
-
} catch { /* unreadable — skip */ }
|
|
288
|
-
}
|
|
289
|
-
return stale;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function clearStalePluginCacheHooks() {
|
|
293
|
-
const cleared = [];
|
|
294
|
-
const stamp = new Date().toISOString();
|
|
295
|
-
for (const p of findStalePluginHooksJson()) {
|
|
296
|
-
try {
|
|
297
|
-
const stub = { ...EMPTY_HOOKS_STUB, _note: `${EMPTY_HOOKS_STUB._note} (cleared ${stamp})` };
|
|
298
|
-
writeJsonAtomic(p, stub);
|
|
299
|
-
cleared.push(p);
|
|
300
|
-
} catch { /* write failure — skip */ }
|
|
301
|
-
}
|
|
302
|
-
return cleared;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// --- Hook Registration ---
|
|
306
|
-
// Plugin system's hooks.json auto-loading is unreliable (observed across GSD,
|
|
307
|
-
// superpowers, code-graph-mcp). Write hooks directly to settings.json instead.
|
|
308
|
-
// Same strategy as claude-mem-lite. hooks.json is kept empty to prevent double-firing.
|
|
223
|
+
// --- Hook identity ---
|
|
224
|
+
// Claude Code loads hooks from cache/<mp>/<plugin>/<ver>/hooks/hooks.json —
|
|
225
|
+
// that file is the authoritative source. Any entries matching our hooks
|
|
226
|
+
// inside settings.json are legacy migration debris (v0.8.2 and earlier wrote
|
|
227
|
+
// there) and must be stripped on every install/update/session-init so events
|
|
228
|
+
// don't fire twice.
|
|
309
229
|
|
|
310
230
|
const OUR_HOOK_SCRIPTS = ['session-init.js', 'incremental-index.js', 'user-prompt-context.js', 'pre-edit-guide.js'];
|
|
311
231
|
const OUR_DESCRIPTIONS = [
|
|
@@ -317,69 +237,15 @@ const OUR_DESCRIPTIONS = [
|
|
|
317
237
|
|
|
318
238
|
function isOurHookEntry(entry) {
|
|
319
239
|
if (!entry || !entry.hooks) return false;
|
|
320
|
-
// Primary: match by description (
|
|
240
|
+
// Primary: match by description (legacy v0.7.x/0.8.x registrations).
|
|
321
241
|
if (entry.description && OUR_DESCRIPTIONS.includes(entry.description)) return true;
|
|
322
|
-
// Fallback: match by script name + 'code-graph' in path
|
|
242
|
+
// Fallback: match by script name + 'code-graph' in path.
|
|
323
243
|
return entry.hooks.some(h =>
|
|
324
244
|
h.command && OUR_HOOK_SCRIPTS.some(s => h.command.includes(s)) &&
|
|
325
245
|
h.command.includes('code-graph')
|
|
326
246
|
);
|
|
327
247
|
}
|
|
328
248
|
|
|
329
|
-
function hookCommand(scriptName) {
|
|
330
|
-
return `node ${JSON.stringify(path.join(PLUGIN_ROOT, 'scripts', scriptName))}`;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
function getHookDefinitions() {
|
|
334
|
-
return {
|
|
335
|
-
SessionStart: [{
|
|
336
|
-
matcher: 'startup|clear|compact',
|
|
337
|
-
hooks: [{ type: 'command', command: hookCommand('session-init.js'), timeout: 5 }],
|
|
338
|
-
description: 'StatusLine self-heal, lifecycle sync, project map injection',
|
|
339
|
-
}],
|
|
340
|
-
PreToolUse: [{
|
|
341
|
-
matcher: 'tool == "Edit"',
|
|
342
|
-
hooks: [{ type: 'command', command: hookCommand('pre-edit-guide.js'), timeout: 4 }],
|
|
343
|
-
description: 'Auto-inject impact analysis when editing functions with 2+ callers',
|
|
344
|
-
}],
|
|
345
|
-
PostToolUse: [{
|
|
346
|
-
matcher: 'tool == "Write" || tool == "Edit"',
|
|
347
|
-
hooks: [{ type: 'command', command: hookCommand('incremental-index.js'), timeout: 10 }],
|
|
348
|
-
description: 'Auto-update code graph index after file edits',
|
|
349
|
-
}],
|
|
350
|
-
UserPromptSubmit: [{
|
|
351
|
-
matcher: '',
|
|
352
|
-
hooks: [{ type: 'command', command: hookCommand('user-prompt-context.js'), timeout: 5 }],
|
|
353
|
-
description: 'Inject code-graph structural context based on user intent',
|
|
354
|
-
}],
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function registerHooksToSettings(settings) {
|
|
359
|
-
if (!settings.hooks) settings.hooks = {};
|
|
360
|
-
const defs = getHookDefinitions();
|
|
361
|
-
let changed = false;
|
|
362
|
-
|
|
363
|
-
for (const [event, newEntries] of Object.entries(defs)) {
|
|
364
|
-
if (!settings.hooks[event]) settings.hooks[event] = [];
|
|
365
|
-
|
|
366
|
-
// First, remove ALL existing entries that match ours (cleans up duplicates
|
|
367
|
-
// from prior PLUGIN_ROOT pollution where isOurHookEntry couldn't match,
|
|
368
|
-
// causing infinite re-adds each session).
|
|
369
|
-
const beforeLen = settings.hooks[event].length;
|
|
370
|
-
settings.hooks[event] = settings.hooks[event].filter(e => !isOurHookEntry(e));
|
|
371
|
-
if (settings.hooks[event].length !== beforeLen) changed = true;
|
|
372
|
-
|
|
373
|
-
// Then add our entries fresh with correct paths
|
|
374
|
-
for (const newEntry of newEntries) {
|
|
375
|
-
settings.hooks[event].push(newEntry);
|
|
376
|
-
changed = true;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
return changed;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
249
|
function removeHooksFromSettings(settings) {
|
|
384
250
|
if (!settings.hooks) return false;
|
|
385
251
|
let changed = false;
|
|
@@ -434,10 +300,11 @@ function install() {
|
|
|
434
300
|
// Register code-graph provider
|
|
435
301
|
registerStatuslineProvider('code-graph', codeGraphStatuslineCommand(), false);
|
|
436
302
|
|
|
437
|
-
// 2. Hooks —
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
303
|
+
// 2. Hooks — cache/<ver>/hooks/hooks.json is authoritative. Strip any legacy
|
|
304
|
+
// entries from settings.json that v0.8.2 or earlier registered, so events
|
|
305
|
+
// don't fire twice.
|
|
306
|
+
const legacyHooksRemoved = removeHooksFromSettings(settings);
|
|
307
|
+
if (legacyHooksRemoved) settingsChanged = true;
|
|
441
308
|
|
|
442
309
|
// NOTE: enabledPlugins is managed by Claude Code's plugin system, not by lifecycle.
|
|
443
310
|
// Do NOT add enabledPlugins entries here — it causes phantom plugin entries
|
|
@@ -448,17 +315,13 @@ function install() {
|
|
|
448
315
|
writeJsonAtomic(SETTINGS_PATH, settings);
|
|
449
316
|
}
|
|
450
317
|
|
|
451
|
-
// 3b. Clear cache/marketplace hooks.json copies after settings.json is authoritative,
|
|
452
|
-
// so next session only fires hooks from settings.json (no double-firing).
|
|
453
|
-
const clearedHookCopies = clearStalePluginCacheHooks();
|
|
454
|
-
|
|
455
318
|
// 4. Write manifest with version
|
|
456
319
|
manifest.version = version;
|
|
457
320
|
manifest.installedAt = manifest.installedAt || new Date().toISOString();
|
|
458
321
|
manifest.updatedAt = new Date().toISOString();
|
|
459
322
|
writeManifest(manifest);
|
|
460
323
|
|
|
461
|
-
return { version, settingsChanged, statusLineClaimed: manifest.config.statusLine,
|
|
324
|
+
return { version, settingsChanged, statusLineClaimed: manifest.config.statusLine, legacyHooksRemoved };
|
|
462
325
|
}
|
|
463
326
|
|
|
464
327
|
// --- Uninstall (clean all config) ---
|
|
@@ -549,10 +412,10 @@ function update() {
|
|
|
549
412
|
// 2. Update code-graph provider in registry
|
|
550
413
|
registerStatuslineProvider('code-graph', codeGraphStatuslineCommand(), false);
|
|
551
414
|
|
|
552
|
-
// 3. Hooks —
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
415
|
+
// 3. Hooks — strip any legacy entries from settings.json. cache hooks.json
|
|
416
|
+
// is the new authoritative source and always has the up-to-date paths.
|
|
417
|
+
const legacyHooksRemoved = removeHooksFromSettings(settings);
|
|
418
|
+
if (legacyHooksRemoved) settingsChanged = true;
|
|
556
419
|
|
|
557
420
|
// NOTE: enabledPlugins is managed by Claude Code's plugin system, not by lifecycle.
|
|
558
421
|
|
|
@@ -561,10 +424,6 @@ function update() {
|
|
|
561
424
|
writeJsonAtomic(SETTINGS_PATH, settings);
|
|
562
425
|
}
|
|
563
426
|
|
|
564
|
-
// 4b. Clear cache/marketplace hooks.json copies after settings.json is updated.
|
|
565
|
-
// Auto-update can re-populate cache from marketplace source; stamp it out.
|
|
566
|
-
const clearedHookCopies = clearStalePluginCacheHooks();
|
|
567
|
-
|
|
568
427
|
// 5. Clear update-check cache (force re-check after update)
|
|
569
428
|
const updateCache = path.join(CACHE_DIR, 'update-check');
|
|
570
429
|
try { fs.unlinkSync(updateCache); } catch { /* ok */ }
|
|
@@ -574,10 +433,12 @@ function update() {
|
|
|
574
433
|
manifest.updatedAt = new Date().toISOString();
|
|
575
434
|
writeManifest(manifest);
|
|
576
435
|
|
|
577
|
-
// 7. Clean up old cached versions (keep latest 3)
|
|
436
|
+
// 7. Clean up old cached versions (keep latest 3). Claude Code only fires
|
|
437
|
+
// hooks from the active version (per installed_plugins.json), so older
|
|
438
|
+
// cache dirs are inert disk clutter, not correctness risks.
|
|
578
439
|
cleanupOldCacheVersions(3);
|
|
579
440
|
|
|
580
|
-
return { oldVersion, version, settingsChanged,
|
|
441
|
+
return { oldVersion, version, settingsChanged, legacyHooksRemoved };
|
|
581
442
|
}
|
|
582
443
|
|
|
583
444
|
/**
|
|
@@ -673,8 +534,7 @@ module.exports = {
|
|
|
673
534
|
readManifest, readJson, writeJsonAtomic,
|
|
674
535
|
readRegistry, writeRegistry,
|
|
675
536
|
getPluginVersion, cleanupOldCacheVersions,
|
|
676
|
-
|
|
677
|
-
scanPluginHooksJsonCopies, findStalePluginHooksJson, clearStalePluginCacheHooks,
|
|
537
|
+
removeHooksFromSettings, isOurHookEntry,
|
|
678
538
|
PLUGIN_ID, OLD_PLUGIN_IDS, MARKETPLACE_NAME, CACHE_DIR, REGISTRY_FILE,
|
|
679
539
|
};
|
|
680
540
|
|
|
@@ -687,6 +547,7 @@ if (require.main === module) {
|
|
|
687
547
|
} else if (cmd === 'uninstall') {
|
|
688
548
|
const r = uninstall();
|
|
689
549
|
console.log(`Uninstalled | settings cleaned=${r.settingsChanged}`);
|
|
550
|
+
console.log(' Note: also run `/plugin uninstall code-graph-mcp` inside Claude Code to sync its UI state.');
|
|
690
551
|
} else if (cmd === 'update') {
|
|
691
552
|
const r = update();
|
|
692
553
|
console.log(`Updated ${r.oldVersion} → ${r.version} | settings=${r.settingsChanged}`);
|
|
@@ -96,80 +96,102 @@ test('cleanupDisabledStatusline also heals orphaned statusline after uninstall',
|
|
|
96
96
|
assert.equal(fs.existsSync(registryPath), false);
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
-
function
|
|
99
|
+
function legacyHooksFromPlugin() {
|
|
100
100
|
return {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
101
|
+
SessionStart: [{
|
|
102
|
+
matcher: 'startup|clear|compact',
|
|
103
|
+
description: 'StatusLine self-heal, lifecycle sync, project map injection',
|
|
104
|
+
hooks: [{ type: 'command', command: 'node "/stale/cache/0.8.2/claude-plugin/scripts/session-init.js"', timeout: 5 }],
|
|
105
|
+
}],
|
|
106
|
+
PostToolUse: [{
|
|
107
|
+
matcher: 'tool == "Write" || tool == "Edit"',
|
|
108
|
+
description: 'Auto-update code graph index after file edits',
|
|
109
|
+
hooks: [{ type: 'command', command: 'node "/stale/code-graph/incremental-index.js"', timeout: 10 }],
|
|
110
|
+
}],
|
|
107
111
|
};
|
|
108
112
|
}
|
|
109
113
|
|
|
110
|
-
test('
|
|
111
|
-
const
|
|
112
|
-
const mpHooks = path.join(homeDir, '.claude', 'plugins', 'marketplaces', 'code-graph-mcp', 'claude-plugin', 'hooks', 'hooks.json');
|
|
113
|
-
const mpManifest = path.join(homeDir, '.claude', 'plugins', 'marketplaces', 'code-graph-mcp', '.claude-plugin', 'marketplace.json');
|
|
114
|
-
const cacheHooks = path.join(homeDir, '.claude', 'plugins', 'cache', 'code-graph-mcp', 'code-graph-mcp', '0.7.17', 'hooks', 'hooks.json');
|
|
115
|
-
|
|
116
|
-
writeJson(mpManifest, { name: 'code-graph-mcp' });
|
|
117
|
-
writeJson(mpHooks, nonEmptyHooksJson());
|
|
118
|
-
writeJson(cacheHooks, nonEmptyHooksJson());
|
|
119
|
-
|
|
114
|
+
test('isOurHookEntry matches legacy description-tagged entries', () => {
|
|
115
|
+
const entry = legacyHooksFromPlugin().SessionStart[0];
|
|
120
116
|
const out = execFileSync(process.execPath, ['-e', `
|
|
121
|
-
const {
|
|
122
|
-
process.stdout.write(JSON.stringify(
|
|
123
|
-
`]
|
|
124
|
-
|
|
125
|
-
const stale = JSON.parse(out).sort();
|
|
126
|
-
assert.equal(stale.length, 2);
|
|
127
|
-
assert.ok(stale.some(p => p === mpHooks));
|
|
128
|
-
assert.ok(stale.some(p => p === cacheHooks));
|
|
117
|
+
const { isOurHookEntry } = require(${JSON.stringify(lifecyclePath)});
|
|
118
|
+
process.stdout.write(JSON.stringify(isOurHookEntry(${JSON.stringify(entry)})));
|
|
119
|
+
`]).toString();
|
|
120
|
+
assert.equal(JSON.parse(out), true);
|
|
129
121
|
});
|
|
130
122
|
|
|
131
|
-
test('
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
123
|
+
test('isOurHookEntry matches script-name + path fallback (missing description)', () => {
|
|
124
|
+
const entry = {
|
|
125
|
+
matcher: 'tool == "Edit"',
|
|
126
|
+
hooks: [{ type: 'command', command: 'node "/cache/code-graph-mcp/scripts/pre-edit-guide.js"' }],
|
|
127
|
+
};
|
|
136
128
|
const out = execFileSync(process.execPath, ['-e', `
|
|
137
|
-
const {
|
|
138
|
-
process.stdout.write(JSON.stringify(
|
|
139
|
-
`]
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
assert.deepEqual(cleared, [cacheHooks]);
|
|
129
|
+
const { isOurHookEntry } = require(${JSON.stringify(lifecyclePath)});
|
|
130
|
+
process.stdout.write(JSON.stringify(isOurHookEntry(${JSON.stringify(entry)})));
|
|
131
|
+
`]).toString();
|
|
132
|
+
assert.equal(JSON.parse(out), true);
|
|
133
|
+
});
|
|
143
134
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
135
|
+
test('isOurHookEntry leaves unrelated entries alone', () => {
|
|
136
|
+
const entry = {
|
|
137
|
+
matcher: 'startup',
|
|
138
|
+
description: 'some other plugin hook',
|
|
139
|
+
hooks: [{ type: 'command', command: 'node /some/other/script.js' }],
|
|
140
|
+
};
|
|
141
|
+
const out = execFileSync(process.execPath, ['-e', `
|
|
142
|
+
const { isOurHookEntry } = require(${JSON.stringify(lifecyclePath)});
|
|
143
|
+
process.stdout.write(JSON.stringify(isOurHookEntry(${JSON.stringify(entry)})));
|
|
144
|
+
`]).toString();
|
|
145
|
+
assert.equal(JSON.parse(out), false);
|
|
147
146
|
});
|
|
148
147
|
|
|
149
|
-
test('
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
148
|
+
test('removeHooksFromSettings strips our entries but keeps unrelated hooks', () => {
|
|
149
|
+
const settings = {
|
|
150
|
+
hooks: {
|
|
151
|
+
SessionStart: [
|
|
152
|
+
legacyHooksFromPlugin().SessionStart[0],
|
|
153
|
+
{
|
|
154
|
+
matcher: 'startup',
|
|
155
|
+
description: 'some other plugin hook',
|
|
156
|
+
hooks: [{ type: 'command', command: 'node /some/other/script.js' }],
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
PostToolUse: [legacyHooksFromPlugin().PostToolUse[0]],
|
|
160
|
+
},
|
|
161
|
+
};
|
|
153
162
|
|
|
154
163
|
const out = execFileSync(process.execPath, ['-e', `
|
|
155
|
-
const {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
164
|
+
const { removeHooksFromSettings } = require(${JSON.stringify(lifecyclePath)});
|
|
165
|
+
const s = ${JSON.stringify(settings)};
|
|
166
|
+
const changed = removeHooksFromSettings(s);
|
|
167
|
+
process.stdout.write(JSON.stringify({ changed, s }));
|
|
168
|
+
`]).toString();
|
|
169
|
+
|
|
170
|
+
const { changed, s } = JSON.parse(out);
|
|
171
|
+
assert.equal(changed, true);
|
|
172
|
+
// Only the unrelated SessionStart entry remains; PostToolUse removed entirely.
|
|
173
|
+
assert.equal(s.hooks.SessionStart.length, 1);
|
|
174
|
+
assert.equal(s.hooks.SessionStart[0].description, 'some other plugin hook');
|
|
175
|
+
assert.ok(!s.hooks.PostToolUse, 'empty event key should be deleted');
|
|
160
176
|
});
|
|
161
177
|
|
|
162
|
-
test('
|
|
178
|
+
test('install() removes legacy code-graph hooks from settings.json without re-registering', () => {
|
|
163
179
|
const homeDir = mkHome();
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
180
|
+
const settingsPath = path.join(homeDir, '.claude', 'settings.json');
|
|
181
|
+
writeJson(settingsPath, {
|
|
182
|
+
statusLine: { type: 'command', command: 'echo previous-status' },
|
|
183
|
+
hooks: legacyHooksFromPlugin(),
|
|
184
|
+
});
|
|
168
185
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
`], { env: { ...process.env, HOME: homeDir } }).toString();
|
|
186
|
+
execFileSync(process.execPath, [lifecyclePath, 'install'], {
|
|
187
|
+
env: { ...process.env, HOME: homeDir },
|
|
188
|
+
});
|
|
173
189
|
|
|
174
|
-
|
|
190
|
+
const after = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
191
|
+
// No code-graph hook entries should remain — cache hooks.json is authoritative now.
|
|
192
|
+
const serialized = JSON.stringify(after.hooks || {});
|
|
193
|
+
assert.ok(!serialized.includes('code-graph'), 'settings.json must not retain code-graph hook entries');
|
|
194
|
+
assert.ok(!serialized.includes('session-init.js'), 'settings.json must not retain session-init.js paths');
|
|
195
|
+
// StatusLine composite is still registered (only channel available).
|
|
196
|
+
assert.match(after.statusLine.command, /statusline-composite/);
|
|
175
197
|
});
|
|
@@ -7,7 +7,6 @@ const fs = require('fs');
|
|
|
7
7
|
const {
|
|
8
8
|
install, update, readManifest, getPluginVersion, checkScopeConflict,
|
|
9
9
|
cleanupDisabledStatusline, isPluginInactive, readJson, CACHE_DIR,
|
|
10
|
-
clearStalePluginCacheHooks, findStalePluginHooksJson,
|
|
11
10
|
} = require('./lifecycle');
|
|
12
11
|
const { readBinaryVersion, isDevMode, getNewestMtime } = require('./version-utils');
|
|
13
12
|
|
|
@@ -227,30 +226,6 @@ function consistencyCheck(binary) {
|
|
|
227
226
|
return issues;
|
|
228
227
|
}
|
|
229
228
|
|
|
230
|
-
/**
|
|
231
|
-
* Self-heal: Claude Code auto-update can re-populate cache hooks.json from the
|
|
232
|
-
* marketplace source, which would double-fire every hook we registered to
|
|
233
|
-
* settings.json. If our hooks are already in settings.json (install has run),
|
|
234
|
-
* any non-empty cache/marketplace hooks.json is stale — clear it.
|
|
235
|
-
* Gated on settings.json registration so pure plugin-only users (no install
|
|
236
|
-
* script run; cache hooks.json is their only registration) are not broken.
|
|
237
|
-
*/
|
|
238
|
-
function healStaleCacheHooks() {
|
|
239
|
-
try {
|
|
240
|
-
const settings = readJson(path.join(os.homedir(), '.claude', 'settings.json')) || {};
|
|
241
|
-
const manifest = readManifest();
|
|
242
|
-
if (!manifest || !manifest.version) return { checked: false, cleared: 0 };
|
|
243
|
-
const serialized = JSON.stringify(settings.hooks || {});
|
|
244
|
-
if (!serialized.includes('code-graph')) return { checked: false, cleared: 0 };
|
|
245
|
-
const stale = findStalePluginHooksJson();
|
|
246
|
-
if (stale.length === 0) return { checked: true, cleared: 0 };
|
|
247
|
-
const cleared = clearStalePluginCacheHooks();
|
|
248
|
-
return { checked: true, cleared: cleared.length };
|
|
249
|
-
} catch {
|
|
250
|
-
return { checked: false, cleared: 0 };
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
229
|
function runSessionInit() {
|
|
255
230
|
if (isPluginInactive()) {
|
|
256
231
|
cleanupDisabledStatusline();
|
|
@@ -267,11 +242,6 @@ function runSessionInit() {
|
|
|
267
242
|
|
|
268
243
|
const lifecycle = syncLifecycleConfig();
|
|
269
244
|
|
|
270
|
-
// Self-heal stale plugin cache hooks.json (prevents double-firing after auto-update).
|
|
271
|
-
// syncLifecycleConfig's install/update path already clears; this catches the
|
|
272
|
-
// 'noop' case where version matches but cache was re-populated externally.
|
|
273
|
-
const cacheHookHeal = healStaleCacheHooks();
|
|
274
|
-
|
|
275
245
|
// Verify binary availability — catch issues early with actionable diagnostics
|
|
276
246
|
const binaryCheck = verifyBinary();
|
|
277
247
|
|
|
@@ -285,7 +255,7 @@ function runSessionInit() {
|
|
|
285
255
|
? consistencyCheck(binaryCheck.binary)
|
|
286
256
|
: [];
|
|
287
257
|
return {
|
|
288
|
-
inactive: false, lifecycle,
|
|
258
|
+
inactive: false, lifecycle,
|
|
289
259
|
autoUpdateLaunched, indexFreshness, mapInjected, binaryCheck, consistencyIssues,
|
|
290
260
|
quietHooks,
|
|
291
261
|
};
|
|
@@ -327,7 +297,6 @@ module.exports = {
|
|
|
327
297
|
injectProjectMap,
|
|
328
298
|
verifyBinary,
|
|
329
299
|
consistencyCheck,
|
|
330
|
-
healStaleCacheHooks,
|
|
331
300
|
runSessionInit,
|
|
332
301
|
};
|
|
333
302
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdsrs/code-graph",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
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": {
|
|
@@ -34,10 +34,10 @@
|
|
|
34
34
|
"node": ">=16"
|
|
35
35
|
},
|
|
36
36
|
"optionalDependencies": {
|
|
37
|
-
"@sdsrs/code-graph-linux-x64": "0.8.
|
|
38
|
-
"@sdsrs/code-graph-linux-arm64": "0.8.
|
|
39
|
-
"@sdsrs/code-graph-darwin-x64": "0.8.
|
|
40
|
-
"@sdsrs/code-graph-darwin-arm64": "0.8.
|
|
41
|
-
"@sdsrs/code-graph-win32-x64": "0.8.
|
|
37
|
+
"@sdsrs/code-graph-linux-x64": "0.8.3",
|
|
38
|
+
"@sdsrs/code-graph-linux-arm64": "0.8.3",
|
|
39
|
+
"@sdsrs/code-graph-darwin-x64": "0.8.3",
|
|
40
|
+
"@sdsrs/code-graph-darwin-arm64": "0.8.3",
|
|
41
|
+
"@sdsrs/code-graph-win32-x64": "0.8.3"
|
|
42
42
|
}
|
|
43
43
|
}
|