@sdsrs/code-graph 0.31.0 → 0.32.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/claude-plugin/hooks/hooks.json +2 -58
- package/claude-plugin/scripts/auto-update.js +2 -1
- package/claude-plugin/scripts/doctor.js +64 -33
- package/claude-plugin/scripts/hooks.test.js +151 -0
- package/claude-plugin/scripts/lifecycle.js +145 -30
- package/claude-plugin/scripts/lifecycle.test.js +381 -6
- package/claude-plugin/scripts/mcp-launcher.js +73 -0
- package/claude-plugin/scripts/mcp-launcher.test.js +23 -3
- package/claude-plugin/scripts/pre-edit-guide.js +24 -4
- package/claude-plugin/scripts/pre-grep-guide.js +107 -9
- package/claude-plugin/scripts/pre-grep-guide.test.js +263 -1
- package/claude-plugin/scripts/pre-read-guide.js +2 -2
- package/claude-plugin/scripts/session-init.js +17 -0
- package/claude-plugin/scripts/tmp-dir.js +32 -0
- package/claude-plugin/scripts/tmp-dir.test.js +50 -0
- package/package.json +6 -6
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "code-graph-mcp hooks — loaded
|
|
3
|
-
"_note": "
|
|
2
|
+
"description": "code-graph-mcp hooks — only SessionStart is loaded by Claude Code from plugin-cache. All other event types are registered by lifecycle.js into ~/.claude/settings.json (v0.32.0+).",
|
|
3
|
+
"_note": "Empirical 2026-05-24: current Claude Code only loads SessionStart entries from plugin-cache hooks.json. PreToolUse / PostToolUse / UserPromptSubmit / Stop / SessionEnd entries here are SILENTLY IGNORED. lifecycle.js install/update writes those to ~/.claude/settings.json instead (pattern from claude-mem-lite). Re-adding non-SessionStart entries here would NOT make them fire — they would just be dead config. See feedback_pretooluse_dark_under_green_health.md for the jsonl evidence chain.",
|
|
4
4
|
"hooks": {
|
|
5
5
|
"SessionStart": [
|
|
6
6
|
{
|
|
@@ -13,62 +13,6 @@
|
|
|
13
13
|
}
|
|
14
14
|
]
|
|
15
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
|
-
"matcher": "tool == \"Bash\"",
|
|
30
|
-
"hooks": [
|
|
31
|
-
{
|
|
32
|
-
"type": "command",
|
|
33
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pre-grep-guide.js\"",
|
|
34
|
-
"timeout": 3
|
|
35
|
-
}
|
|
36
|
-
]
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
"matcher": "tool == \"Read\"",
|
|
40
|
-
"hooks": [
|
|
41
|
-
{
|
|
42
|
-
"type": "command",
|
|
43
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pre-read-guide.js\"",
|
|
44
|
-
"timeout": 3
|
|
45
|
-
}
|
|
46
|
-
]
|
|
47
|
-
}
|
|
48
|
-
],
|
|
49
|
-
"PostToolUse": [
|
|
50
|
-
{
|
|
51
|
-
"matcher": "tool == \"Write\" || tool == \"Edit\"",
|
|
52
|
-
"hooks": [
|
|
53
|
-
{
|
|
54
|
-
"type": "command",
|
|
55
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/incremental-index.js\"",
|
|
56
|
-
"timeout": 10
|
|
57
|
-
}
|
|
58
|
-
]
|
|
59
|
-
}
|
|
60
|
-
],
|
|
61
|
-
"UserPromptSubmit": [
|
|
62
|
-
{
|
|
63
|
-
"matcher": "",
|
|
64
|
-
"hooks": [
|
|
65
|
-
{
|
|
66
|
-
"type": "command",
|
|
67
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/user-prompt-context.js\"",
|
|
68
|
-
"timeout": 5
|
|
69
|
-
}
|
|
70
|
-
]
|
|
71
|
-
}
|
|
72
16
|
]
|
|
73
17
|
}
|
|
74
18
|
}
|
|
@@ -8,6 +8,7 @@ const os = require('os');
|
|
|
8
8
|
const { CACHE_DIR, PLUGIN_ID, MARKETPLACE_NAME, readManifest, readJson, writeJsonAtomic, installedPluginsPath, pluginsCacheDir } = require('./lifecycle');
|
|
9
9
|
const { clearCache: clearBinaryCache } = require('./find-binary');
|
|
10
10
|
const { readBinaryVersion, isDevMode } = require('./version-utils');
|
|
11
|
+
const { cgTmpDir } = require('./tmp-dir');
|
|
11
12
|
|
|
12
13
|
// ── Environment Checks ────────────────────────────────────
|
|
13
14
|
|
|
@@ -251,7 +252,7 @@ async function downloadAndInstall(latest) {
|
|
|
251
252
|
return { pluginUpdated: false, binaryUpdated: false };
|
|
252
253
|
}
|
|
253
254
|
|
|
254
|
-
const tmpDir = path.join(
|
|
255
|
+
const tmpDir = path.join(cgTmpDir(), `update-${Date.now()}`);
|
|
255
256
|
let pluginUpdated = false;
|
|
256
257
|
let binaryUpdated = false;
|
|
257
258
|
|
|
@@ -7,8 +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
|
-
|
|
11
|
-
settingsPath,
|
|
10
|
+
isOurHookEntry, settingsPath, buildSettingsHookEntries,
|
|
12
11
|
} = require('./lifecycle');
|
|
13
12
|
const { findBinary, clearCache: clearBinaryCache } = require('./find-binary');
|
|
14
13
|
|
|
@@ -173,34 +172,52 @@ function runDiagnostics() {
|
|
|
173
172
|
}
|
|
174
173
|
|
|
175
174
|
// 6. Hook paths validity
|
|
175
|
+
// healthCheck() auto-attempts install() when broken paths are detected and
|
|
176
|
+
// re-scans to verify; repaired:true is now contingent on the re-scan
|
|
177
|
+
// returning clean. If repaired:false despite install() running, the
|
|
178
|
+
// re-scan still found broken paths — surfacing 'remaining' makes that
|
|
179
|
+
// honest instead of telling the user we fixed nothing.
|
|
176
180
|
const hookResult = healthCheck();
|
|
177
181
|
if (hookResult.healthy) {
|
|
178
182
|
results.push({ name: 'Hooks', status: 'ok', detail: 'all paths valid' });
|
|
183
|
+
} else if (hookResult.repaired) {
|
|
184
|
+
results.push({
|
|
185
|
+
name: 'Hooks',
|
|
186
|
+
status: 'ok',
|
|
187
|
+
detail: `${hookResult.issues.length} issue(s) auto-repaired`,
|
|
188
|
+
});
|
|
179
189
|
} else {
|
|
190
|
+
const remainingCount = Array.isArray(hookResult.remaining)
|
|
191
|
+
? hookResult.remaining.length
|
|
192
|
+
: hookResult.issues.length;
|
|
180
193
|
results.push({
|
|
181
194
|
name: 'Hooks',
|
|
182
|
-
status:
|
|
183
|
-
detail:
|
|
184
|
-
|
|
185
|
-
: `${hookResult.issues.length} invalid path(s)`,
|
|
186
|
-
fixId: hookResult.repaired ? undefined : 'hooks-invalid',
|
|
195
|
+
status: 'warn',
|
|
196
|
+
detail: `${remainingCount} invalid path(s) — auto-repair did not resolve`,
|
|
197
|
+
fixId: 'hooks-invalid',
|
|
187
198
|
});
|
|
188
199
|
}
|
|
189
200
|
|
|
190
|
-
// 7.
|
|
191
|
-
// cache
|
|
192
|
-
//
|
|
201
|
+
// 7. settings.json hook coverage — v0.32.0 inversion. Current Claude Code
|
|
202
|
+
// silently ignores plugin-cache hooks.json for PreToolUse/PostToolUse/
|
|
203
|
+
// UserPromptSubmit. lifecycle.js install/update is responsible for
|
|
204
|
+
// registering them in settings.json. "Missing" is the bug (previously
|
|
205
|
+
// "present" was treated as legacy debris — that was wrong).
|
|
193
206
|
try {
|
|
194
207
|
const settings = readJson(settingsPath()) || {};
|
|
195
|
-
const
|
|
196
|
-
if (
|
|
197
|
-
results.push({
|
|
208
|
+
const cov = surveyHookCoverage(settings);
|
|
209
|
+
if (cov.missing.length === 0) {
|
|
210
|
+
results.push({
|
|
211
|
+
name: 'Hook coverage',
|
|
212
|
+
status: 'ok',
|
|
213
|
+
detail: `settings.json has all ${cov.expected.length} expected entries`,
|
|
214
|
+
});
|
|
198
215
|
} else {
|
|
199
216
|
results.push({
|
|
200
|
-
name: '
|
|
217
|
+
name: 'Hook coverage',
|
|
201
218
|
status: 'warn',
|
|
202
|
-
detail:
|
|
203
|
-
fixId: '
|
|
219
|
+
detail: `missing ${cov.missing.length}/${cov.expected.length} settings.json entries: ${cov.missing.join(', ')}`,
|
|
220
|
+
fixId: 'missing-hooks-in-settings',
|
|
204
221
|
});
|
|
205
222
|
}
|
|
206
223
|
} catch { /* probe failed — skip */ }
|
|
@@ -208,16 +225,31 @@ function runDiagnostics() {
|
|
|
208
225
|
return results;
|
|
209
226
|
}
|
|
210
227
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
228
|
+
// Inventory of (event, matcher) tuples we expect to find in settings.json after
|
|
229
|
+
// install. Used by doctor to detect missing entries.
|
|
230
|
+
function surveyHookCoverage(settings) {
|
|
231
|
+
const desired = buildSettingsHookEntries();
|
|
232
|
+
const expected = [];
|
|
233
|
+
for (const [event, entries] of Object.entries(desired)) {
|
|
234
|
+
for (const e of entries) {
|
|
235
|
+
expected.push(`${event}:${e.matcher || '*'}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const present = new Set();
|
|
240
|
+
if (settings && settings.hooks) {
|
|
241
|
+
for (const [event, entries] of Object.entries(settings.hooks)) {
|
|
242
|
+
if (!Array.isArray(entries)) continue;
|
|
243
|
+
for (const entry of entries) {
|
|
244
|
+
if (isOurHookEntry(entry)) {
|
|
245
|
+
present.add(`${event}:${entry.matcher || '*'}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
218
248
|
}
|
|
219
249
|
}
|
|
220
|
-
|
|
250
|
+
|
|
251
|
+
const missing = expected.filter(k => !present.has(k));
|
|
252
|
+
return { expected, present: [...present], missing };
|
|
221
253
|
}
|
|
222
254
|
|
|
223
255
|
// ── Report Formatting ─────────────────────────────────────
|
|
@@ -370,21 +402,20 @@ function runRepairs(results) {
|
|
|
370
402
|
console.log('\n Repairing hooks...');
|
|
371
403
|
const { install } = require('./lifecycle');
|
|
372
404
|
install();
|
|
373
|
-
console.log(' \u2705 Hooks repaired');
|
|
405
|
+
console.log(' \u2705 Hooks repaired \u2014 restart Claude Code to apply');
|
|
374
406
|
fixed++;
|
|
375
407
|
break;
|
|
376
408
|
}
|
|
377
409
|
|
|
378
|
-
case '
|
|
379
|
-
console.log('\n
|
|
380
|
-
const
|
|
381
|
-
const
|
|
382
|
-
if (
|
|
383
|
-
|
|
384
|
-
console.log(' \u2705 settings.json cleaned — restart Claude Code to apply');
|
|
410
|
+
case 'missing-hooks-in-settings': {
|
|
411
|
+
console.log('\n Registering code-graph hooks in settings.json...');
|
|
412
|
+
const { install } = require('./lifecycle');
|
|
413
|
+
const r = install();
|
|
414
|
+
if (r.hooksRegistered) {
|
|
415
|
+
console.log(' \u2705 settings.json updated — restart Claude Code to apply');
|
|
385
416
|
fixed++;
|
|
386
417
|
} else {
|
|
387
|
-
console.log(' \u2796
|
|
418
|
+
console.log(' \u2796 install reported no change (settings already had entries)');
|
|
388
419
|
}
|
|
389
420
|
break;
|
|
390
421
|
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const test = require('node:test');
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
// Regression gate for v0.31.1: hooks.json matchers must be Claude Code's
|
|
8
|
+
// literal/regex form, NOT the expression DSL `tool == "X"`. The earlier
|
|
9
|
+
// matchers parsed as regex against tool names, never matched anything,
|
|
10
|
+
// and left every PreToolUse hook silently inert from v0.25.0 through
|
|
11
|
+
// v0.31.0. The bug was invisible to the existing unit tests because they
|
|
12
|
+
// spawn the hook scripts directly via stdin, bypassing Claude Code's
|
|
13
|
+
// matcher dispatch.
|
|
14
|
+
|
|
15
|
+
const HOOKS_JSON = path.resolve(__dirname, '..', 'hooks', 'hooks.json');
|
|
16
|
+
|
|
17
|
+
function loadHooks() {
|
|
18
|
+
const raw = fs.readFileSync(HOOKS_JSON, 'utf8');
|
|
19
|
+
return JSON.parse(raw);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function* iterMatchers(hooksByEvent) {
|
|
23
|
+
for (const [event, entries] of Object.entries(hooksByEvent || {})) {
|
|
24
|
+
if (!Array.isArray(entries)) continue;
|
|
25
|
+
for (let i = 0; i < entries.length; i++) {
|
|
26
|
+
const e = entries[i];
|
|
27
|
+
yield { event, idx: i, matcher: e && e.matcher };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
test('hooks.json: file parses as JSON', () => {
|
|
33
|
+
assert.doesNotThrow(loadHooks);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('hooks.json: every entry has a string matcher', () => {
|
|
37
|
+
const cfg = loadHooks();
|
|
38
|
+
let count = 0;
|
|
39
|
+
for (const { event, idx, matcher } of iterMatchers(cfg.hooks)) {
|
|
40
|
+
assert.equal(typeof matcher, 'string',
|
|
41
|
+
`hooks.${event}[${idx}].matcher should be a string, got ${typeof matcher}`);
|
|
42
|
+
count++;
|
|
43
|
+
}
|
|
44
|
+
assert.ok(count > 0, 'expected at least one matcher in hooks.json');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// The actual regression gate. Each banned token reflects a specific
|
|
48
|
+
// failure mode we hit and want to keep out forever.
|
|
49
|
+
const BANNED_TOKENS = [
|
|
50
|
+
// The original v0.25.0 → v0.31.0 bug: expression-style matcher treated
|
|
51
|
+
// as regex against tool name → never matched.
|
|
52
|
+
{ token: '==', why: 'expression DSL (e.g. `tool == "Edit"`) is not supported; use literal tool name' },
|
|
53
|
+
// `tool ==` or `tool name == "X"` — same family, different spelling.
|
|
54
|
+
{ token: 'tool ', why: 'expression DSL with `tool` variable is not supported' },
|
|
55
|
+
// Boolean ORs as expression operators (regex uses `|`, not `||`).
|
|
56
|
+
{ token: '||', why: 'use `|` for pipe-list (e.g. `Write|Edit`), not `||`' },
|
|
57
|
+
// Boolean AND has no meaning in tool-name matching.
|
|
58
|
+
{ token: '&&', why: '`&&` has no meaning in matchers' },
|
|
59
|
+
// Double-quotes inside the matcher are a strong hint of expression DSL
|
|
60
|
+
// (the broken syntax was `"tool == \"Edit\""`).
|
|
61
|
+
{ token: '"', why: 'literal double-quote in matcher is almost always a copy-paste of expression DSL' },
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
test('hooks.json: matchers avoid banned expression-DSL tokens', () => {
|
|
65
|
+
const cfg = loadHooks();
|
|
66
|
+
const offenders = [];
|
|
67
|
+
for (const { event, idx, matcher } of iterMatchers(cfg.hooks)) {
|
|
68
|
+
for (const { token, why } of BANNED_TOKENS) {
|
|
69
|
+
if (matcher.includes(token)) {
|
|
70
|
+
offenders.push(`hooks.${event}[${idx}].matcher = ${JSON.stringify(matcher)} — contains banned ${JSON.stringify(token)} (${why})`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
assert.deepEqual(offenders, [],
|
|
75
|
+
'hooks.json matcher syntax regression — see v0.31.1 CHANGELOG:\n ' + offenders.join('\n '));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// v0.32.0 architecture: plugin-cache hooks.json ONLY carries SessionStart.
|
|
79
|
+
// PreToolUse / PostToolUse / UserPromptSubmit are registered into
|
|
80
|
+
// ~/.claude/settings.json by lifecycle.js (current Claude Code silently
|
|
81
|
+
// ignores plugin-cache hooks.json entries for those events — confirmed
|
|
82
|
+
// 2026-05-24 via session jsonl, see feedback_pretooluse_dark_under_green_health.md).
|
|
83
|
+
test('hooks.json: contains SessionStart only (v0.32.0)', () => {
|
|
84
|
+
const cfg = loadHooks();
|
|
85
|
+
assert.deepEqual(Object.keys(cfg.hooks || {}), ['SessionStart'],
|
|
86
|
+
'plugin-cache hooks.json must contain only SessionStart; other events go via settings.json. ' +
|
|
87
|
+
'Adding entries here for PreToolUse/PostToolUse/UserPromptSubmit would be dead config — CC does not load them.');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('hooks.json: SessionStart wires session-init.js', () => {
|
|
91
|
+
const cfg = loadHooks();
|
|
92
|
+
const entries = (cfg.hooks && cfg.hooks.SessionStart) || [];
|
|
93
|
+
assert.ok(entries.length > 0, 'SessionStart entry missing');
|
|
94
|
+
const cmd = entries[0].hooks && entries[0].hooks[0] && entries[0].hooks[0].command;
|
|
95
|
+
assert.match(cmd || '', /session-init\.js/);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Cross-validate that lifecycle.js's buildSettingsHookEntries covers the
|
|
99
|
+
// matchers we removed from hooks.json — keeps the migration whole. If a
|
|
100
|
+
// future refactor accidentally drops a matcher in one place, this fails.
|
|
101
|
+
test('lifecycle.buildSettingsHookEntries covers PreToolUse Edit/Bash/Read', () => {
|
|
102
|
+
const { buildSettingsHookEntries } = require('./lifecycle');
|
|
103
|
+
const desired = buildSettingsHookEntries();
|
|
104
|
+
const ptu = (desired.PreToolUse || []).map(e => e.matcher);
|
|
105
|
+
for (const tool of ['Edit', 'Bash', 'Read']) {
|
|
106
|
+
assert.ok(ptu.includes(tool), `lifecycle.js PreToolUse missing matcher: ${tool}; got ${JSON.stringify(ptu)}`);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('lifecycle.buildSettingsHookEntries covers PostToolUse Write|Edit + UserPromptSubmit', () => {
|
|
111
|
+
const { buildSettingsHookEntries } = require('./lifecycle');
|
|
112
|
+
const desired = buildSettingsHookEntries();
|
|
113
|
+
const postMatchers = (desired.PostToolUse || []).map(e => e.matcher);
|
|
114
|
+
assert.ok(postMatchers.some(m => m === 'Write|Edit'),
|
|
115
|
+
`PostToolUse must have 'Write|Edit' matcher; got ${JSON.stringify(postMatchers)}`);
|
|
116
|
+
const upsMatchers = (desired.UserPromptSubmit || []).map(e => e.matcher);
|
|
117
|
+
assert.ok(upsMatchers.length > 0, 'UserPromptSubmit must have at least one matcher');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('lifecycle.buildSettingsHookEntries: every entry carries description marker', () => {
|
|
121
|
+
// Description marker is the primary cleanup discriminator (immune to
|
|
122
|
+
// path/env pollution per feedback_plugin_env_isolation.md). If an entry
|
|
123
|
+
// lacks a description, isOurHookEntry falls back to path-fragment match
|
|
124
|
+
// which is less reliable. Force every entry to have one.
|
|
125
|
+
const { buildSettingsHookEntries } = require('./lifecycle');
|
|
126
|
+
const desired = buildSettingsHookEntries();
|
|
127
|
+
for (const [event, entries] of Object.entries(desired)) {
|
|
128
|
+
for (let i = 0; i < entries.length; i++) {
|
|
129
|
+
assert.ok(entries[i].description && entries[i].description.includes('[code-graph-mcp'),
|
|
130
|
+
`${event}[${i}] missing or malformed description marker`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('lifecycle.buildSettingsHookEntries: hook commands use absolute paths (no env vars)', () => {
|
|
136
|
+
// settings.json hook commands run with env pollution risk
|
|
137
|
+
// (feedback_plugin_env_isolation.md). Paths MUST be absolute, derived
|
|
138
|
+
// from __dirname, never from ${CLAUDE_PLUGIN_ROOT}.
|
|
139
|
+
const { buildSettingsHookEntries } = require('./lifecycle');
|
|
140
|
+
const desired = buildSettingsHookEntries();
|
|
141
|
+
for (const entries of Object.values(desired)) {
|
|
142
|
+
for (const e of entries) {
|
|
143
|
+
for (const h of e.hooks) {
|
|
144
|
+
assert.ok(!h.command.includes('${CLAUDE_PLUGIN_ROOT}'),
|
|
145
|
+
`command must not use \${CLAUDE_PLUGIN_ROOT}: ${h.command}`);
|
|
146
|
+
assert.ok(h.command.startsWith('node "/') || h.command.match(/node "[A-Z]:\\/),
|
|
147
|
+
`command path must be absolute: ${h.command}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
});
|
|
@@ -245,28 +245,63 @@ function migrateOldPluginIds(settings) {
|
|
|
245
245
|
}
|
|
246
246
|
|
|
247
247
|
// --- Hook identity ---
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
//
|
|
248
|
+
//
|
|
249
|
+
// v0.32.0 ARCHITECTURE CORRECTION (see project_hooks_settings.md / feedback_pretooluse_dark_under_green_health.md):
|
|
250
|
+
//
|
|
251
|
+
// Empirical finding 2026-05-24: current Claude Code only loads SessionStart
|
|
252
|
+
// hooks from cache/<mp>/<plugin>/<ver>/hooks/hooks.json. PreToolUse, PostToolUse,
|
|
253
|
+
// UserPromptSubmit, Stop, SessionEnd entries in plugin-cache hooks.json are
|
|
254
|
+
// SILENTLY IGNORED. Only ~/.claude/settings.json entries reach CC for those events.
|
|
255
|
+
//
|
|
256
|
+
// Therefore lifecycle.js now ACTIVELY WRITES non-SessionStart hook entries to
|
|
257
|
+
// settings.json (with description markers for cleanup), and the shipped
|
|
258
|
+
// claude-plugin/hooks/hooks.json carries only SessionStart. SessionStart entries
|
|
259
|
+
// in claude-plugin/hooks/hooks.json continue to be CC-loaded as before.
|
|
260
|
+
//
|
|
261
|
+
// Pattern mirrors claude-mem-lite's install.mjs (cache hooks.json cleared
|
|
262
|
+
// to prevent duplicate registration).
|
|
263
|
+
|
|
264
|
+
const OUR_HOOK_SCRIPTS = [
|
|
265
|
+
'session-init.js',
|
|
266
|
+
'incremental-index.js',
|
|
267
|
+
'user-prompt-context.js',
|
|
268
|
+
'pre-edit-guide.js',
|
|
269
|
+
'pre-grep-guide.js', // v0.32.0 — was in plugin-cache only, never fired
|
|
270
|
+
'pre-read-guide.js', // v0.32.0 — was in plugin-cache only, never fired
|
|
271
|
+
];
|
|
272
|
+
|
|
273
|
+
// Description markers — primary cleanup discriminator (immune to env/path
|
|
274
|
+
// pollution per feedback_plugin_env_isolation.md). New v0.32.0 markers carry
|
|
275
|
+
// the version so older lifecycle.js still recognizes them as ours.
|
|
276
|
+
const SETTINGS_HOOK_DESC = {
|
|
277
|
+
preToolUse: '[code-graph-mcp v0.32+] PreToolUse re-routed via settings.json (cache hooks.json silently ignored for this event by current CC)',
|
|
278
|
+
postToolUseEdit: '[code-graph-mcp v0.32+] PostToolUse Write|Edit incremental-index update',
|
|
279
|
+
userPromptSubmit: '[code-graph-mcp v0.32+] UserPromptSubmit context push',
|
|
280
|
+
};
|
|
253
281
|
|
|
254
|
-
const OUR_HOOK_SCRIPTS = ['session-init.js', 'incremental-index.js', 'user-prompt-context.js', 'pre-edit-guide.js'];
|
|
255
282
|
const OUR_DESCRIPTIONS = [
|
|
283
|
+
// Legacy v0.7.x / 0.8.x descriptions — kept so very-old installs still get cleaned up.
|
|
256
284
|
'StatusLine self-heal, lifecycle sync, project map injection',
|
|
257
285
|
'Auto-inject impact analysis when editing functions with 2+ callers',
|
|
258
286
|
'Auto-update code graph index after file edits',
|
|
259
287
|
'Inject code-graph structural context based on user intent',
|
|
288
|
+
// v0.32.0 — new re-route markers
|
|
289
|
+
SETTINGS_HOOK_DESC.preToolUse,
|
|
290
|
+
SETTINGS_HOOK_DESC.postToolUseEdit,
|
|
291
|
+
SETTINGS_HOOK_DESC.userPromptSubmit,
|
|
260
292
|
];
|
|
261
293
|
|
|
262
294
|
function isOurHookEntry(entry) {
|
|
263
295
|
if (!entry || !entry.hooks) return false;
|
|
264
|
-
// Primary: match by description (
|
|
296
|
+
// Primary: match by description (immune to path pollution).
|
|
265
297
|
if (entry.description && OUR_DESCRIPTIONS.includes(entry.description)) return true;
|
|
266
|
-
// Fallback:
|
|
298
|
+
// Fallback: script name + MARKETPLACE_NAME in path. v0.32.1: tightened from
|
|
299
|
+
// bare 'code-graph' (which would claim a user's own ~/code-graph/foo.js) to
|
|
300
|
+
// the actual marketplace dir name 'code-graph-mcp' — Requirement 3 says
|
|
301
|
+
// foreign-entry strip is unacceptable, so be conservative.
|
|
267
302
|
return entry.hooks.some(h =>
|
|
268
303
|
h.command && OUR_HOOK_SCRIPTS.some(s => h.command.includes(s)) &&
|
|
269
|
-
h.command.includes(
|
|
304
|
+
h.command.includes(MARKETPLACE_NAME)
|
|
270
305
|
);
|
|
271
306
|
}
|
|
272
307
|
|
|
@@ -286,6 +321,62 @@ function removeHooksFromSettings(settings) {
|
|
|
286
321
|
return changed;
|
|
287
322
|
}
|
|
288
323
|
|
|
324
|
+
// --- v0.32.0: settings.json hook registration ---
|
|
325
|
+
|
|
326
|
+
// PLUGIN_ROOT (module-level, line 18) is the canonical __dirname-derived
|
|
327
|
+
// absolute path — never CLAUDE_PLUGIN_ROOT env (env leaks across plugins
|
|
328
|
+
// in settings.json hook execution context per feedback_plugin_env_isolation.md).
|
|
329
|
+
|
|
330
|
+
function buildSettingsHookEntries() {
|
|
331
|
+
const root = PLUGIN_ROOT;
|
|
332
|
+
const scriptCmd = (name, timeout) => ({
|
|
333
|
+
type: 'command',
|
|
334
|
+
command: `node "${path.join(root, 'scripts', name)}"`,
|
|
335
|
+
timeout,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
PreToolUse: [
|
|
340
|
+
{ description: SETTINGS_HOOK_DESC.preToolUse, matcher: 'Edit', hooks: [scriptCmd('pre-edit-guide.js', 4)] },
|
|
341
|
+
{ description: SETTINGS_HOOK_DESC.preToolUse, matcher: 'Bash', hooks: [scriptCmd('pre-grep-guide.js', 3)] },
|
|
342
|
+
{ description: SETTINGS_HOOK_DESC.preToolUse, matcher: 'Read', hooks: [scriptCmd('pre-read-guide.js', 3)] },
|
|
343
|
+
],
|
|
344
|
+
PostToolUse: [
|
|
345
|
+
{ description: SETTINGS_HOOK_DESC.postToolUseEdit, matcher: 'Write|Edit', hooks: [scriptCmd('incremental-index.js', 10)] },
|
|
346
|
+
],
|
|
347
|
+
UserPromptSubmit: [
|
|
348
|
+
{ description: SETTINGS_HOOK_DESC.userPromptSubmit, matcher: '', hooks: [scriptCmd('user-prompt-context.js', 5)] },
|
|
349
|
+
],
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Idempotent two-pass: (1) evict ALL our entries (legacy v0.7+/0.8+ markers
|
|
354
|
+
// AND v0.32+ markers) across EVERY event — catches legacy SessionStart/
|
|
355
|
+
// PostToolUse entries in settings.json pointing to stale plugin-cache paths;
|
|
356
|
+
// (2) write fresh v0.32+ entries for the events we own. SessionStart stays
|
|
357
|
+
// in plugin-cache hooks.json (it's still loaded from there), so we don't
|
|
358
|
+
// re-write it to settings.json.
|
|
359
|
+
function registerHooksToSettings(settings) {
|
|
360
|
+
settings.hooks = settings.hooks || {};
|
|
361
|
+
const before = JSON.stringify(settings.hooks);
|
|
362
|
+
|
|
363
|
+
// Pass 1: evict our entries across every event.
|
|
364
|
+
for (const event of Object.keys(settings.hooks)) {
|
|
365
|
+
if (!Array.isArray(settings.hooks[event])) continue;
|
|
366
|
+
settings.hooks[event] = settings.hooks[event].filter(e => !isOurHookEntry(e));
|
|
367
|
+
if (settings.hooks[event].length === 0) delete settings.hooks[event];
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Pass 2: write fresh entries for our desired events.
|
|
371
|
+
const desired = buildSettingsHookEntries();
|
|
372
|
+
for (const [event, desiredEntries] of Object.entries(desired)) {
|
|
373
|
+
const existing = Array.isArray(settings.hooks[event]) ? settings.hooks[event] : [];
|
|
374
|
+
settings.hooks[event] = [...existing, ...desiredEntries];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return before !== JSON.stringify(settings.hooks);
|
|
378
|
+
}
|
|
379
|
+
|
|
289
380
|
// --- Install (idempotent) ---
|
|
290
381
|
|
|
291
382
|
function install() {
|
|
@@ -324,11 +415,12 @@ function install() {
|
|
|
324
415
|
// Register code-graph provider
|
|
325
416
|
registerStatuslineProvider('code-graph', codeGraphStatuslineCommand(), false);
|
|
326
417
|
|
|
327
|
-
// 2. Hooks —
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
|
|
418
|
+
// 2. Hooks — v0.32.0: actively write PreToolUse/PostToolUse/UserPromptSubmit
|
|
419
|
+
// to settings.json. Plugin-cache hooks.json is silently ignored by current
|
|
420
|
+
// Claude Code for these events (SessionStart still loads from cache).
|
|
421
|
+
// registerHooksToSettings is idempotent: strips priors then appends fresh.
|
|
422
|
+
const hooksRegistered = registerHooksToSettings(settings);
|
|
423
|
+
if (hooksRegistered) settingsChanged = true;
|
|
332
424
|
|
|
333
425
|
// NOTE: enabledPlugins is managed by Claude Code's plugin system, not by lifecycle.
|
|
334
426
|
// Do NOT add enabledPlugins entries here — it causes phantom plugin entries
|
|
@@ -345,7 +437,7 @@ function install() {
|
|
|
345
437
|
manifest.updatedAt = new Date().toISOString();
|
|
346
438
|
writeManifest(manifest);
|
|
347
439
|
|
|
348
|
-
return { version, settingsChanged, statusLineClaimed: manifest.config.statusLine,
|
|
440
|
+
return { version, settingsChanged, statusLineClaimed: manifest.config.statusLine, hooksRegistered };
|
|
349
441
|
}
|
|
350
442
|
|
|
351
443
|
// --- Uninstall (clean all config) ---
|
|
@@ -437,10 +529,10 @@ function update() {
|
|
|
437
529
|
// 2. Update code-graph provider in registry
|
|
438
530
|
registerStatuslineProvider('code-graph', codeGraphStatuslineCommand(), false);
|
|
439
531
|
|
|
440
|
-
// 3. Hooks —
|
|
441
|
-
//
|
|
442
|
-
const
|
|
443
|
-
if (
|
|
532
|
+
// 3. Hooks — v0.32.0: register PreToolUse/PostToolUse/UserPromptSubmit in
|
|
533
|
+
// settings.json (idempotent; absolute paths re-anchor on every update).
|
|
534
|
+
const hooksRegistered = registerHooksToSettings(settings);
|
|
535
|
+
if (hooksRegistered) settingsChanged = true;
|
|
444
536
|
|
|
445
537
|
// NOTE: enabledPlugins is managed by Claude Code's plugin system, not by lifecycle.
|
|
446
538
|
|
|
@@ -463,7 +555,7 @@ function update() {
|
|
|
463
555
|
// cache dirs are inert disk clutter, not correctness risks.
|
|
464
556
|
cleanupOldCacheVersions(3);
|
|
465
557
|
|
|
466
|
-
return { oldVersion, version, settingsChanged,
|
|
558
|
+
return { oldVersion, version, settingsChanged, hooksRegistered };
|
|
467
559
|
}
|
|
468
560
|
|
|
469
561
|
/**
|
|
@@ -503,9 +595,15 @@ function cleanupOldCacheVersions(keep = 3) {
|
|
|
503
595
|
|
|
504
596
|
// --- Health Check ---
|
|
505
597
|
// Validates all registered paths in settings.json point to existing scripts.
|
|
506
|
-
// Returns { healthy, issues, repaired }.
|
|
507
|
-
|
|
508
|
-
|
|
598
|
+
// Returns { healthy, issues, repaired, remaining }.
|
|
599
|
+
// issues: pre-repair detection list (what was wrong on entry)
|
|
600
|
+
// repaired: true only after a post-repair re-scan returned zero issues
|
|
601
|
+
// (was previously set blindly to true whenever install() ran,
|
|
602
|
+
// which would lie if install() couldn't actually fix something)
|
|
603
|
+
// remaining: post-repair detection list — present iff install() was invoked;
|
|
604
|
+
// empty array means repair succeeded
|
|
605
|
+
|
|
606
|
+
function scanForBrokenPaths() {
|
|
509
607
|
const settings = readJson(settingsPath()) || {};
|
|
510
608
|
const issues = [];
|
|
511
609
|
|
|
@@ -543,23 +641,40 @@ function healthCheck() {
|
|
|
543
641
|
}
|
|
544
642
|
}
|
|
545
643
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
644
|
+
return issues;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function healthCheck() {
|
|
648
|
+
const issues = scanForBrokenPaths();
|
|
649
|
+
|
|
650
|
+
if (issues.length === 0) {
|
|
651
|
+
return { healthy: true, issues, repaired: false };
|
|
551
652
|
}
|
|
552
653
|
|
|
553
|
-
|
|
654
|
+
// Attempt auto-repair, then re-scan to confirm the issues actually went
|
|
655
|
+
// away. install() may legitimately fail to resolve a problem (binary path
|
|
656
|
+
// permanently gone, registry corrupted, etc.) and the previous code lied
|
|
657
|
+
// by always returning repaired:true.
|
|
658
|
+
install();
|
|
659
|
+
const remaining = scanForBrokenPaths();
|
|
660
|
+
return {
|
|
661
|
+
healthy: false,
|
|
662
|
+
issues,
|
|
663
|
+
repaired: remaining.length === 0,
|
|
664
|
+
remaining,
|
|
665
|
+
};
|
|
554
666
|
}
|
|
555
667
|
|
|
556
668
|
module.exports = {
|
|
557
|
-
install, uninstall, update, healthCheck, checkScopeConflict,
|
|
669
|
+
install, uninstall, update, healthCheck, scanForBrokenPaths, checkScopeConflict,
|
|
558
670
|
isPluginExplicitlyDisabled, isPluginInactive, cleanupDisabledStatusline,
|
|
559
671
|
readManifest, readJson, writeJsonAtomic,
|
|
560
672
|
readRegistry, writeRegistry,
|
|
561
673
|
getPluginVersion, cleanupOldCacheVersions,
|
|
562
674
|
removeHooksFromSettings, isOurHookEntry,
|
|
675
|
+
registerHooksToSettings, buildSettingsHookEntries, // v0.32.0
|
|
676
|
+
SETTINGS_HOOK_DESC, OUR_HOOK_SCRIPTS, OUR_DESCRIPTIONS, // v0.32.0 — for tests
|
|
677
|
+
PLUGIN_ROOT, // v0.32.1 — for tests / consumers
|
|
563
678
|
registerStatuslineProvider, unregisterStatuslineProvider,
|
|
564
679
|
PLUGIN_ID, OLD_PLUGIN_IDS, MARKETPLACE_NAME, CACHE_DIR, REGISTRY_FILE,
|
|
565
680
|
settingsPath, installedPluginsPath, providersBackupFile, pluginsCacheDir,
|