@sdsrs/code-graph 0.162.0 → 0.164.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -6
- package/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/claude-plugin/agents/code-explorer.md +15 -12
- package/claude-plugin/hooks/hooks.json +91 -1
- package/claude-plugin/scripts/adopt.js +49 -37
- package/claude-plugin/scripts/cg-answer.js +7 -0
- package/claude-plugin/scripts/covering-tests.js +1 -2
- package/claude-plugin/scripts/doctor.js +40 -1
- package/claude-plugin/scripts/incremental-index.js +4 -1
- package/claude-plugin/scripts/lifecycle.js +49 -13
- package/claude-plugin/scripts/post-grep-inject.js +58 -3
- package/claude-plugin/scripts/pre-edit-guide.js +122 -83
- package/claude-plugin/scripts/pre-grep-guide.js +243 -11
- package/claude-plugin/scripts/pre-read-guide.js +42 -28
- package/claude-plugin/scripts/project-root.js +23 -1
- package/claude-plugin/scripts/session-edits.js +67 -5
- package/claude-plugin/scripts/session-init.js +101 -348
- package/claude-plugin/scripts/stop-impact.js +244 -18
- package/claude-plugin/scripts/subagent-start.js +8 -0
- package/claude-plugin/scripts/user-prompt-context.js +48 -16
- package/claude-plugin/skills/explore/SKILL.md +10 -7
- package/claude-plugin/templates/code-graph-snapshot.yml +1 -1
- package/claude-plugin/templates/plugin_code_graph_mcp.md +58 -169
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -231,7 +231,7 @@ and `code-graph-mcp doctor` prints the manual update command.
|
|
|
231
231
|
|
|
232
232
|
#### Invited-memory mode (quieter prompts)
|
|
233
233
|
|
|
234
|
-
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
|
|
234
|
+
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 explicit tool calls, opt into invited-memory mode:
|
|
235
235
|
|
|
236
236
|
1. Adopt the plugin contract into your project (idempotent, self-heals):
|
|
237
237
|
```bash
|
|
@@ -244,7 +244,7 @@ By default, every user prompt the plugin deems code-related gets a small context
|
|
|
244
244
|
"env": { "CODE_GRAPH_QUIET_HOOKS": "1" }
|
|
245
245
|
}
|
|
246
246
|
```
|
|
247
|
-
3. Restart Claude Code. Session startup skips the project-map injection, UserPromptSubmit stops auto-injecting context, and the MCP `instructions`
|
|
247
|
+
3. Restart Claude Code. Session startup skips the project-map injection, UserPromptSubmit stops auto-injecting context, and the MCP `instructions` shrink to a one-line command pointer.
|
|
248
248
|
|
|
249
249
|
### Option 2: Claude Code MCP Server Only
|
|
250
250
|
|
|
@@ -545,14 +545,13 @@ defaults are what you get by doing nothing.
|
|
|
545
545
|
| Variable | Effect |
|
|
546
546
|
|---|---|
|
|
547
547
|
| `CODE_GRAPH_NO_AUTO_UPDATE=1` | Never check GitHub for a new release. |
|
|
548
|
-
| `CODE_GRAPH_NO_AUTO_ADOPT=1` |
|
|
549
|
-
| `CODE_GRAPH_NO_TEMPLATE_REFRESH=1` |
|
|
550
|
-
| `CODE_GRAPH_NO_GITIGNORE=1` |
|
|
548
|
+
| `CODE_GRAPH_NO_AUTO_ADOPT=1` | Skip SessionStart's adoption check: no cleanup of this plugin's legacy memory-dir files and no notice about an out-of-date steering block. SessionStart never writes `CLAUDE.md` itself; `code-graph-mcp adopt` does. |
|
|
549
|
+
| `CODE_GRAPH_NO_TEMPLATE_REFRESH=1` | Do not report a steering block that has drifted from the current template (SessionStart only reports it; `code-graph-mcp adopt` refreshes it). |
|
|
550
|
+
| `CODE_GRAPH_NO_GITIGNORE=1` | Do not write the `.code-graph/` ignore rule. It goes to the repository's local `.git/info/exclude`, never to the tracked `.gitignore`, and only when neither file names the directory already. |
|
|
551
551
|
| `CODE_GRAPH_QUIET_HOOKS=1` | Hooks inject a one-line pointer instead of the full decision table. |
|
|
552
552
|
| `CODE_GRAPH_VERBOSE_HOOKS=1` | The opposite: opt into the noisy form. |
|
|
553
553
|
| `CODE_GRAPH_NO_BLOCK_GREP=1` | Never turn a `grep` hint into a block — prefix a single command with it to get past one. |
|
|
554
554
|
| `CODE_GRAPH_NO_INJECT=1` | No post-tool AST context injection. |
|
|
555
|
-
| `CODE_GRAPH_NO_RECENT_IMPACT=1` | Skip the recent-impact section of the SessionStart briefing. |
|
|
556
555
|
| `CODE_GRAPH_HOOK_INDEX=on\|off` | Force the incremental-index hook on or off instead of letting it decide. |
|
|
557
556
|
| `CODE_GRAPH_MODEL_DIR=<dir>` | Load the embedding model from here (air-gapped installs). |
|
|
558
557
|
| `CODE_GRAPH_DISABLE_MODEL_DOWNLOAD=1` | Never fetch the model; fail instead. |
|
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: code-explorer
|
|
3
3
|
description: Deep code understanding expert using AST knowledge graph. Use when exploring unfamiliar code, tracing complex relationships, or understanding module architecture.
|
|
4
|
-
tools: ["Read", "Grep", "Glob", "Bash", "
|
|
4
|
+
tools: ["Read", "Grep", "Glob", "Bash", "mcp__plugin_code-graph-mcp_code-graph__semantic_code_search", "mcp__plugin_code-graph-mcp_code-graph__get_call_graph", "mcp__plugin_code-graph-mcp_code-graph__get_ast_node", "mcp__plugin_code-graph-mcp_code-graph__project_map", "mcp__plugin_code-graph-mcp_code-graph__module_overview", "mcp__plugin_code-graph-mcp_code-graph__ast_search", "mcp__plugin_code-graph-mcp_code-graph__find_references"]
|
|
5
5
|
model: sonnet
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
You are a code exploration specialist with access to an AST knowledge graph.
|
|
9
9
|
|
|
10
|
-
<!-- The tool allowlist
|
|
11
|
-
|
|
10
|
+
<!-- The tool allowlist names only the plugin-hosted MCP spelling
|
|
11
|
+
(`mcp__plugin_code-graph-mcp_code-graph__*`): that is what a live plugin
|
|
12
|
+
install exposes (checked 2026-09-28 on Claude Code 2.1.284). The bare
|
|
13
|
+
`mcp__code-graph__*` half DOC-07 kept as a bet matched no tool. -->
|
|
12
14
|
|
|
13
15
|
## Strategy
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
The CLI answers from Bash with no tool loading; the MCP tools return the same data.
|
|
18
|
+
|
|
19
|
+
1. **Map first**: `code-graph-mcp overview <dir>` (MCP `module_overview`) or `code-graph-mcp map --compact` (`project_map`) for an unfamiliar area.
|
|
20
|
+
2. **Locate**: `code-graph-mcp show X` for a named symbol; `code-graph-mcp search "words"` or MCP `semantic_code_search` when you only know what the code does.
|
|
21
|
+
3. **Relate**: `code-graph-mcp callgraph X` (`get_call_graph`; `route_path='GET /api/x'` traces an HTTP handler) and `code-graph-mcp impact X` (`get_ast_node include_impact=true`).
|
|
22
|
+
4. **Audit uses**: `code-graph-mcp refs X` (`find_references`), then grep for what the graph cannot see; `ast_search` enumerates symbols by type / return / params.
|
|
23
|
+
5. **Read** the files the answer points to; use Grep for literal strings and non-code files.
|
|
20
24
|
|
|
21
25
|
## Rules
|
|
22
26
|
|
|
23
|
-
-
|
|
24
|
-
- Return structured findings: name, file, line, relationships
|
|
25
|
-
- When reporting call chains, include depth and direction
|
|
26
|
-
- Estimate token cost: if Read would require >3 files, prefer code-graph tools
|
|
27
|
+
- Calls through dynamic dispatch, reflection or unresolved imports are not in the graph: an empty caller list is not proof of no callers.
|
|
28
|
+
- Return structured findings: name, file, line, relationships.
|
|
29
|
+
- When reporting call chains, include depth and direction.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "code-graph-mcp hooks
|
|
2
|
+
"description": "code-graph-mcp hooks, loaded by Claude Code from the plugin: SessionStart plus the tool, prompt, subagent and stop hooks. Since 0.164 they all live here; SessionStart removes the copies earlier versions wrote into ~/.claude/settings.json (a plugin's hooks.json is loaded for every event by current Claude Code). Keep this file to the keys Claude Code's schema allows (description, hooks, modules, surface); maintainer notes go in hooks.test.js.",
|
|
3
3
|
"hooks": {
|
|
4
4
|
"SessionStart": [
|
|
5
5
|
{
|
|
@@ -12,6 +12,96 @@
|
|
|
12
12
|
}
|
|
13
13
|
]
|
|
14
14
|
}
|
|
15
|
+
],
|
|
16
|
+
"PreToolUse": [
|
|
17
|
+
{
|
|
18
|
+
"matcher": "Edit|Write",
|
|
19
|
+
"hooks": [
|
|
20
|
+
{
|
|
21
|
+
"type": "command",
|
|
22
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pre-edit-guide.js\"",
|
|
23
|
+
"timeout": 4
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"matcher": "Bash",
|
|
29
|
+
"hooks": [
|
|
30
|
+
{
|
|
31
|
+
"type": "command",
|
|
32
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pre-grep-guide.js\"",
|
|
33
|
+
"timeout": 3
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"matcher": "Read",
|
|
39
|
+
"hooks": [
|
|
40
|
+
{
|
|
41
|
+
"type": "command",
|
|
42
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pre-read-guide.js\"",
|
|
43
|
+
"timeout": 3
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
"PostToolUse": [
|
|
49
|
+
{
|
|
50
|
+
"matcher": "Write|Edit",
|
|
51
|
+
"hooks": [
|
|
52
|
+
{
|
|
53
|
+
"type": "command",
|
|
54
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/incremental-index.js\"",
|
|
55
|
+
"timeout": 10
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"matcher": "Bash",
|
|
61
|
+
"hooks": [
|
|
62
|
+
{
|
|
63
|
+
"type": "command",
|
|
64
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/post-grep-inject.js\"",
|
|
65
|
+
"timeout": 5
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
|
+
"UserPromptSubmit": [
|
|
71
|
+
{
|
|
72
|
+
"matcher": "",
|
|
73
|
+
"hooks": [
|
|
74
|
+
{
|
|
75
|
+
"type": "command",
|
|
76
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/user-prompt-context.js\"",
|
|
77
|
+
"timeout": 5
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
],
|
|
82
|
+
"SubagentStart": [
|
|
83
|
+
{
|
|
84
|
+
"matcher": "Explore|Plan|general-purpose",
|
|
85
|
+
"hooks": [
|
|
86
|
+
{
|
|
87
|
+
"type": "command",
|
|
88
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/subagent-start.js\"",
|
|
89
|
+
"timeout": 3
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
"Stop": [
|
|
95
|
+
{
|
|
96
|
+
"matcher": "",
|
|
97
|
+
"hooks": [
|
|
98
|
+
{
|
|
99
|
+
"type": "command",
|
|
100
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/stop-impact.js\"",
|
|
101
|
+
"timeout": 5
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
}
|
|
15
105
|
]
|
|
16
106
|
}
|
|
17
107
|
}
|
|
@@ -69,29 +69,26 @@ function writeFileAtomic(filePath, data, { followLink = false } = {}) {
|
|
|
69
69
|
// The managed block written into <cwd>/CLAUDE.md. Concise + always-loaded: a
|
|
70
70
|
// scannable trigger table that primes the right tool, ending with a pointer to
|
|
71
71
|
// the full table at .claude/plugin_code_graph_mcp.md (opened on demand, never
|
|
72
|
-
// auto-loaded). Project-type tailoring
|
|
73
|
-
// tracing
|
|
72
|
+
// auto-loaded). Project-type tailoring adds a row for web projects (HTTP-route
|
|
73
|
+
// tracing) — body of the detail doc is unchanged.
|
|
74
74
|
//
|
|
75
|
-
// Every row spends the bare name `code-graph-mcp
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
// answers with "command not found". Hence the fallback line below the table.
|
|
75
|
+
// Every row spends the bare name `code-graph-mcp`. Issue #41 was a plugin-only
|
|
76
|
+
// install whose shell answered it with "command not found"; Claude Code now puts
|
|
77
|
+
// the plugin's `bin/` launcher on the Bash PATH, so the `~/.cache/code-graph/bin`
|
|
78
|
+
// fallback line this block used to carry is gone (it also could not run
|
|
79
|
+
// `adopt`/`doctor`, which are JS-dispatched).
|
|
81
80
|
//
|
|
82
|
-
//
|
|
83
|
-
// CLAUDE.md
|
|
84
|
-
// /home/<user>/…
|
|
85
|
-
// buildBlock's byte-determinism is exactly what needsRefresh diffs, so
|
|
86
|
-
// machine-varying content would rewrite the block on every clone's next
|
|
87
|
-
// SessionStart. No `.exe` variant for the same reason it is safe not to have one:
|
|
88
|
-
// platformGuard() refuses adopt on win32, so this block never exists there.
|
|
81
|
+
// The block is byte-deterministic and machine-independent: it goes into a
|
|
82
|
+
// CLAUDE.md that may be git-tracked, and needsRefresh diffs it bytewise, so a
|
|
83
|
+
// resolved /home/<user>/… path would be right on one machine only.
|
|
89
84
|
const BLOCK_HEADING = '## Code Graph (repo-wide AST index)';
|
|
90
85
|
|
|
91
86
|
function buildTriggerRows(projectType = 'generic') {
|
|
92
87
|
const base = [
|
|
93
88
|
['Who calls X / what X calls', '`code-graph-mcp callgraph X`'],
|
|
94
|
-
['Impact before
|
|
89
|
+
['Impact before changing a signature', '`code-graph-mcp impact X`'],
|
|
90
|
+
['Tests to re-run after changing files', '`code-graph-mcp affected <files>`'],
|
|
91
|
+
['Rename / remove audit', '`code-graph-mcp refs X`, then `grep -w X`'],
|
|
95
92
|
['Unfamiliar dir / module', '`code-graph-mcp overview <dir>`'],
|
|
96
93
|
['Symbol source / signature', '`code-graph-mcp show X`'],
|
|
97
94
|
['Concept search (no exact name)', '`code-graph-mcp search "…"` (vector: MCP `semantic_code_search`)'],
|
|
@@ -106,11 +103,6 @@ function buildTriggerRows(projectType = 'generic') {
|
|
|
106
103
|
return [base[0], base[1],
|
|
107
104
|
['HTTP route → handler chain', '`code-graph-mcp trace "GET /api/x"`'],
|
|
108
105
|
...base.slice(2)];
|
|
109
|
-
case 'frontend':
|
|
110
|
-
// Rename/refactor audits dominate; surface find-references explicitly.
|
|
111
|
-
return [base[0],
|
|
112
|
-
['Rename / refactor audit (refs)', '`code-graph-mcp refs X`'],
|
|
113
|
-
...base.slice(1)];
|
|
114
106
|
default:
|
|
115
107
|
return base;
|
|
116
108
|
}
|
|
@@ -126,14 +118,13 @@ function buildBlock(projectType = 'generic') {
|
|
|
126
118
|
const body = [
|
|
127
119
|
BLOCK_HEADING,
|
|
128
120
|
'',
|
|
129
|
-
'
|
|
130
|
-
'
|
|
121
|
+
'Parsed index of the whole repo. For structural questions — who calls X, what a',
|
|
122
|
+
'change breaks, every use of a symbol — one call replaces rounds of Grep + Read:',
|
|
131
123
|
'',
|
|
132
124
|
table,
|
|
133
125
|
'',
|
|
134
|
-
'
|
|
135
|
-
'
|
|
136
|
-
'',
|
|
126
|
+
'Unresolved calls (dynamic dispatch, reflection, unresolved imports) leave no edge, so',
|
|
127
|
+
'an empty answer is not proof: confirm with grep before deleting or renaming.',
|
|
137
128
|
"Still use Grep for literal strings/regex in non-code files; still Read files you'll edit.",
|
|
138
129
|
'Full command + MCP-tool table: `.claude/plugin_code_graph_mcp.md`',
|
|
139
130
|
].join('\n');
|
|
@@ -491,6 +482,17 @@ function readAdoptedProjects(home) {
|
|
|
491
482
|
return readAdoptedResult(home).list;
|
|
492
483
|
}
|
|
493
484
|
|
|
485
|
+
// A8 (2026-09-29 usage evaluation): the registry only ever grew — 7 of 16
|
|
486
|
+
// entries on the evaluation machine named deleted temp dirs. A directory that
|
|
487
|
+
// no longer exists holds no managed block, so each write drops those; an entry
|
|
488
|
+
// whose check fails any other way (EACCES, a stale mount) stays, since its
|
|
489
|
+
// block may still be there.
|
|
490
|
+
function withoutGoneDirs(list) {
|
|
491
|
+
return list.filter((p) => {
|
|
492
|
+
try { fs.statSync(p); return true; } catch (err) { return !(err && err.code === 'ENOENT'); }
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
494
496
|
/** @returns {boolean} true when the project is recorded (or already was). */
|
|
495
497
|
function recordAdopted(projectDir, home) {
|
|
496
498
|
const res = readAdoptedResult(home);
|
|
@@ -498,9 +500,11 @@ function recordAdopted(projectDir, home) {
|
|
|
498
500
|
try {
|
|
499
501
|
const file = adoptedRegistryFile(home);
|
|
500
502
|
const abs = path.resolve(projectDir);
|
|
501
|
-
|
|
503
|
+
const kept = withoutGoneDirs(res.list);
|
|
504
|
+
const next = kept.includes(abs) ? kept : [...kept, abs];
|
|
505
|
+
if (next.length === res.list.length && next.every((p, i) => p === res.list[i])) return true;
|
|
502
506
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
503
|
-
writeFileAtomic(file, JSON.stringify(
|
|
507
|
+
writeFileAtomic(file, JSON.stringify(next, null, 2) + '\n');
|
|
504
508
|
return true;
|
|
505
509
|
} catch { return false; } // best-effort: registry loss only degrades guidance
|
|
506
510
|
}
|
|
@@ -511,7 +515,7 @@ function removeAdopted(projectDir, home) {
|
|
|
511
515
|
if (res.unusable) return false;
|
|
512
516
|
try {
|
|
513
517
|
const abs = path.resolve(projectDir);
|
|
514
|
-
const next = res.list.filter((p) => p !== abs);
|
|
518
|
+
const next = withoutGoneDirs(res.list).filter((p) => p !== abs);
|
|
515
519
|
if (next.length === res.list.length) return true;
|
|
516
520
|
writeFileAtomic(adoptedRegistryFile(home), JSON.stringify(next, null, 2) + '\n');
|
|
517
521
|
return true;
|
|
@@ -736,19 +740,27 @@ function maybeAutoAdopt({ cwd, home, env, scriptPath } = {}) {
|
|
|
736
740
|
if (!isPluginModeInstall(scriptPath || __dirname)) {
|
|
737
741
|
return { attempted: false, reason: 'not-plugin-mode', migrated: noMigration };
|
|
738
742
|
}
|
|
739
|
-
// Clean legacy memory-dir artifacts
|
|
743
|
+
// Clean up this plugin's own legacy memory-dir artifacts.
|
|
740
744
|
const migrated = migrateLegacyMemoryDir({ cwd, home });
|
|
745
|
+
// Never writes CLAUDE.md or .claude/ (decision D4, 2026-09-28 usage
|
|
746
|
+
// evaluation). Creating them on the first session made Claude stop to
|
|
747
|
+
// explain the unexpected changes in 12 of 15 coding runs (0 of 15 without
|
|
748
|
+
// the plugin), and the MCP instructions already carry 10 of the block's 11
|
|
749
|
+
// guidance points. An adopted project keeps its block as it is; one that has
|
|
750
|
+
// drifted from the shipped template is reported as `stale`, so SessionStart
|
|
751
|
+
// can tell the user how to refresh (`adopt`) or remove (`unadopt`) it. The
|
|
752
|
+
// name is kept: SessionStart, tests and the uninstall paths all call it.
|
|
741
753
|
if (isAdopted({ cwd })) {
|
|
742
|
-
// shipped template / 管理块 漂移时重跑 adopt 对齐。
|
|
743
|
-
// opt-out: CODE_GRAPH_NO_TEMPLATE_REFRESH=1(锁定手动编辑)。
|
|
744
754
|
if (env.CODE_GRAPH_NO_TEMPLATE_REFRESH !== '1' && needsRefresh({ cwd })) {
|
|
745
|
-
|
|
746
|
-
return { attempted: true, reason: 'refreshed', result, migrated };
|
|
755
|
+
return { attempted: false, reason: 'stale', migrated };
|
|
747
756
|
}
|
|
748
757
|
return { attempted: false, reason: 'already-adopted', migrated };
|
|
749
758
|
}
|
|
750
|
-
|
|
751
|
-
|
|
759
|
+
return { attempted: false, reason: 'not-adopted', migrated };
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function adoptCommand() {
|
|
763
|
+
return `node ${shellQuote(__filename)} adopt`;
|
|
752
764
|
}
|
|
753
765
|
|
|
754
766
|
function unadopt({ cwd, home } = {}) {
|
|
@@ -978,7 +990,7 @@ if (require.main === module) {
|
|
|
978
990
|
module.exports = {
|
|
979
991
|
adopt, unadopt, memoryDir, formatResult, unadoptCommand, shellQuote, stripSentinelBlock,
|
|
980
992
|
readAdoptedProjects, readAdoptedResult, recordAdopted, removeAdopted, adoptedRegistryFile,
|
|
981
|
-
isAdopted, isPluginModeInstall, maybeAutoAdopt, needsRefresh, isProjectRoot,
|
|
993
|
+
isAdopted, isPluginModeInstall, maybeAutoAdopt, needsRefresh, isProjectRoot, adoptCommand,
|
|
982
994
|
detectProjectType, buildBlock, buildTriggerRows, migrateLegacyMemoryDir,
|
|
983
995
|
claudeMdPath, detailDir, detailPath,
|
|
984
996
|
extractCargoRuntimeDeps, extractPyRuntimeDeps, extractGoDirectRequires,
|
|
@@ -545,6 +545,13 @@ function runCallgraphAnswer(opts = {}) {
|
|
|
545
545
|
!(out.includes('← called by') || out.includes('→ calls'))) {
|
|
546
546
|
return { status: 'no-hits' };
|
|
547
547
|
}
|
|
548
|
+
// `callgraph` promotes a unique fuzzy match when the name has no edges of
|
|
549
|
+
// its own (`task` → `run_startup_tasks`) and says so only on stderr, which
|
|
550
|
+
// this run discards. A tree rooted at another symbol is not an answer about
|
|
551
|
+
// the grepped one: real sessions got up to 27 of 98 injections this way.
|
|
552
|
+
if (out.split('\n', 1)[0].split(' ', 1)[0] !== symbol) {
|
|
553
|
+
return { status: 'no-hits' };
|
|
554
|
+
}
|
|
548
555
|
const { text, truncated } = truncateAtLine(out, maxBytes);
|
|
549
556
|
return { status: 'hits', text, truncated };
|
|
550
557
|
} catch {
|
|
@@ -56,8 +56,7 @@ function formatCoveringTests(testCallers, editedFile) {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
// High fan-out: editing a widely-tested symbol. A long targeted command is noise
|
|
59
|
-
// — point at the suite instead
|
|
60
|
-
// session-init.js formatRecentImpact).
|
|
59
|
+
// — point at the suite instead.
|
|
61
60
|
let out = ` Covering tests: ${n} — editing a widely-tested symbol\n`;
|
|
62
61
|
const suite = suiteCommand(runner);
|
|
63
62
|
if (suite) out += ` → Run the suite after editing: ${suite}\n`;
|
|
@@ -7,7 +7,7 @@ const os = require('os');
|
|
|
7
7
|
const { readBinaryVersion, isDevMode, getNewestMtime } = require('./version-utils');
|
|
8
8
|
const {
|
|
9
9
|
getPluginVersion, readJson, readJsonResult, healthCheck, scanForBrokenPaths,
|
|
10
|
-
settingsPath, surveyHookCoverage,
|
|
10
|
+
settingsPath, surveyHookCoverage, hooksFromPluginManifest,
|
|
11
11
|
installedGlobalPkgs, GLOBAL_INSTALL_MARKER, SHELL_PKG,
|
|
12
12
|
} = require('./lifecycle');
|
|
13
13
|
const { UPDATE_STATE_FILE, uninstallTombstoneActive } = require('./cache-paths');
|
|
@@ -537,6 +537,25 @@ function runDiagnostics({ checkOnly = false } = {}) {
|
|
|
537
537
|
status: 'warn',
|
|
538
538
|
detail: 'not determinable — settings.json could not be read or parsed',
|
|
539
539
|
});
|
|
540
|
+
} else if (hooksFromPluginManifest(settings)) {
|
|
541
|
+
// Decision D2: the installed plugin's hooks.json carries every hook, so an
|
|
542
|
+
// empty settings.json is the healthy state and our entries in it — left
|
|
543
|
+
// by a pre-0.164 install — fire each hook a second time.
|
|
544
|
+
if (cov.present.length === 0) {
|
|
545
|
+
results.push({
|
|
546
|
+
name: 'Hook coverage',
|
|
547
|
+
status: 'ok',
|
|
548
|
+
detail: "hooks come from the plugin's hooks.json; settings.json holds none of ours",
|
|
549
|
+
});
|
|
550
|
+
} else {
|
|
551
|
+
results.push({
|
|
552
|
+
name: 'Hook coverage',
|
|
553
|
+
status: 'warn',
|
|
554
|
+
detail: `${cov.present.length} code-graph hook entr${cov.present.length === 1 ? 'y' : 'ies'} in settings.json ` +
|
|
555
|
+
`run next to the plugin's hooks.json, so each fires twice: ${cov.present.join(', ')}`,
|
|
556
|
+
fixId: 'legacy-hooks-in-settings',
|
|
557
|
+
});
|
|
558
|
+
}
|
|
540
559
|
} else if (cov.missing.length === 0 && cov.stale.length === 0) {
|
|
541
560
|
results.push({
|
|
542
561
|
name: 'Hook coverage',
|
|
@@ -1247,6 +1266,26 @@ function runRepairs(results, {
|
|
|
1247
1266
|
break;
|
|
1248
1267
|
}
|
|
1249
1268
|
|
|
1269
|
+
case 'legacy-hooks-in-settings': {
|
|
1270
|
+
console.log("\n Removing code-graph hooks from settings.json (the plugin's hooks.json carries them)...");
|
|
1271
|
+
if (relicRepairGuard()) break;
|
|
1272
|
+
if (teardownRepairGuard()) break;
|
|
1273
|
+
const { install } = require('./lifecycle');
|
|
1274
|
+
const r = install({ clearTombstone: true });
|
|
1275
|
+
if (r.settingsUnwritable) {
|
|
1276
|
+
console.log(' \u274c settings.json is not writable \u2014 entries NOT removed');
|
|
1277
|
+
console.log(' Fix the permissions on it (or on ~/.claude) and re-run; see the error above.');
|
|
1278
|
+
} else if (r.settingsUnreadable) {
|
|
1279
|
+
console.log(' \u274c settings.json could not be read or parsed \u2014 entries NOT removed');
|
|
1280
|
+
} else if (r.hooksRegistered) {
|
|
1281
|
+
console.log(' \u2705 settings.json updated — restart Claude Code to apply');
|
|
1282
|
+
fixed++;
|
|
1283
|
+
} else {
|
|
1284
|
+
console.log(' \u2796 install reported no change');
|
|
1285
|
+
}
|
|
1286
|
+
break;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1250
1289
|
case 'missing-hooks-in-settings': {
|
|
1251
1290
|
console.log('\n Registering code-graph hooks in settings.json...');
|
|
1252
1291
|
if (relicRepairGuard()) break;
|
|
@@ -60,8 +60,11 @@ function runMain() {
|
|
|
60
60
|
const budget = childBudgetMs();
|
|
61
61
|
if (budget === null) return; // exhausted — the index waits for the next edit
|
|
62
62
|
|
|
63
|
+
// --no-embed: structure is what the next query needs, and the vector
|
|
64
|
+
// backfill made every edit wait out the whole budget (8,048 ms per edit,
|
|
65
|
+
// hook audit 2026-09-28). Vectors are filled in by the MCP server.
|
|
63
66
|
try {
|
|
64
|
-
execFileSync(bin, ['incremental-index', '--quiet'], hidden({
|
|
67
|
+
execFileSync(bin, ['incremental-index', '--quiet', '--no-embed'], hidden({
|
|
65
68
|
timeout: budget,
|
|
66
69
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
67
70
|
}));
|
|
@@ -348,7 +348,7 @@ function tryWriteSettings(settings) {
|
|
|
348
348
|
} catch (err) {
|
|
349
349
|
console.error(
|
|
350
350
|
`[code-graph] cannot write ${settingsPath()} (${err.code || err.name}: ${err.message}). ` +
|
|
351
|
-
`Nothing was changed
|
|
351
|
+
`Nothing was changed; run \`code-graph-mcp doctor\` once the file is writable.`
|
|
352
352
|
);
|
|
353
353
|
return err;
|
|
354
354
|
}
|
|
@@ -1019,7 +1019,7 @@ function buildSettingsHookEntries() {
|
|
|
1019
1019
|
|
|
1020
1020
|
return {
|
|
1021
1021
|
PreToolUse: [
|
|
1022
|
-
{ description: SETTINGS_HOOK_DESC.preToolUse, matcher: 'Edit', hooks: [scriptCmd('pre-edit-guide.js')] },
|
|
1022
|
+
{ description: SETTINGS_HOOK_DESC.preToolUse, matcher: 'Edit|Write', hooks: [scriptCmd('pre-edit-guide.js')] },
|
|
1023
1023
|
{ description: SETTINGS_HOOK_DESC.preToolUse, matcher: 'Bash', hooks: [scriptCmd('pre-grep-guide.js')] },
|
|
1024
1024
|
{ description: SETTINGS_HOOK_DESC.preToolUse, matcher: 'Read', hooks: [scriptCmd('pre-read-guide.js')] },
|
|
1025
1025
|
],
|
|
@@ -1101,6 +1101,42 @@ function registerHooksToSettings(settings) {
|
|
|
1101
1101
|
return before !== JSON.stringify(settings.hooks);
|
|
1102
1102
|
}
|
|
1103
1103
|
|
|
1104
|
+
// --- Where the hooks live (decision D2, 2026-09-28 usage evaluation) ---
|
|
1105
|
+
//
|
|
1106
|
+
// A plugin install declares its hooks in the plugin's own hooks/hooks.json, and
|
|
1107
|
+
// current Claude Code loads that file for every event — verified on 2.1.284:
|
|
1108
|
+
// PreToolUse / PostToolUse / UserPromptSubmit / Stop entries in a plugin's
|
|
1109
|
+
// hooks.json all fired, and the marketplace-installed claude-mem-lite runs every
|
|
1110
|
+
// hook it has that way. The 2026-05-24 observation behind the v0.32.0 re-route
|
|
1111
|
+
// (only SessionStart loaded from there) no longer holds, and writing into the
|
|
1112
|
+
// user-global settings.json cost a doctor that re-added hooks after uninstall,
|
|
1113
|
+
// version-pinned paths and the ping-pong between delivery surfaces.
|
|
1114
|
+
//
|
|
1115
|
+
// settings.json registration stays for the one surface hooks.json does not
|
|
1116
|
+
// reach: an npm-global or dev CLI with no plugin installed. It never runs next
|
|
1117
|
+
// to an installed, enabled plugin — both copies would fire every hook twice.
|
|
1118
|
+
function hooksFromPluginManifest(settings = readJson(settingsPath()) || {}, { pluginRoot = PLUGIN_ROOT, env = process.env } = {}) {
|
|
1119
|
+
// Claude Code is running this copy as a plugin right now: SessionStart comes
|
|
1120
|
+
// from its hooks.json with CLAUDE_PLUGIN_ROOT set to its root. Compared, not
|
|
1121
|
+
// merely present — the variable can carry another plugin's root.
|
|
1122
|
+
if (env.CLAUDE_PLUGIN_ROOT && path.resolve(env.CLAUDE_PLUGIN_ROOT) === path.resolve(pluginRoot)) return true;
|
|
1123
|
+
// A marketplace install's copy, reached outside a session (doctor, auto-update).
|
|
1124
|
+
const rel = path.relative(pluginsCacheDir(), path.resolve(pluginRoot));
|
|
1125
|
+
if (rel && !rel.startsWith('..') && !path.isAbsolute(rel)) return true;
|
|
1126
|
+
// Another surface (npm global, dev checkout) while the plugin is installed and
|
|
1127
|
+
// enabled: the plugin's hooks.json already covers every session.
|
|
1128
|
+
return hasInstalledPluginRecord() && !isPluginExplicitlyDisabled(settings);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// Put settings.json in the state the surface needs: our entries registered when
|
|
1132
|
+
// nothing else carries the hooks, removed when the plugin's hooks.json does.
|
|
1133
|
+
// Returns whether `settings` changed.
|
|
1134
|
+
function syncSettingsHooks(settings, opts = {}) {
|
|
1135
|
+
return hooksFromPluginManifest(settings, opts)
|
|
1136
|
+
? removeHooksFromSettings(settings)
|
|
1137
|
+
: registerHooksToSettings(settings);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1104
1140
|
// Extract the .js script path a hook command invokes — bare (`node "…"`) or
|
|
1105
1141
|
// existence-guarded (`if [ -f "…" ]; then node "…"; fi`).
|
|
1106
1142
|
// Is the composite command currently in the statusLine slot one we should
|
|
@@ -1251,6 +1287,7 @@ function hookFirePayload(matcher, event = '') {
|
|
|
1251
1287
|
case 'Read':
|
|
1252
1288
|
return { tool_name: 'Read', tool_input: { file_path: 'src/example.rs' } };
|
|
1253
1289
|
case 'Edit':
|
|
1290
|
+
case 'Edit|Write':
|
|
1254
1291
|
case 'Write|Edit':
|
|
1255
1292
|
return { tool_name: 'Edit', tool_input: { file_path: 'src/example.rs', old_string: 'a', new_string: 'b' } };
|
|
1256
1293
|
case '': // UserPromptSubmit
|
|
@@ -1269,9 +1306,9 @@ function hookFirePayload(matcher, event = '') {
|
|
|
1269
1306
|
}
|
|
1270
1307
|
}
|
|
1271
1308
|
|
|
1272
|
-
//
|
|
1273
|
-
//
|
|
1274
|
-
// liveness proof → excluded here.
|
|
1309
|
+
// Every hook besides SessionStart, from the one list both registration surfaces
|
|
1310
|
+
// use (hooks.json for a plugin, settings.json otherwise). SessionStart runs
|
|
1311
|
+
// every session → its own liveness proof → excluded here.
|
|
1275
1312
|
function defaultHookFireProbes() {
|
|
1276
1313
|
const probes = [];
|
|
1277
1314
|
for (const [event, entries] of Object.entries(buildSettingsHookEntries())) {
|
|
@@ -1485,11 +1522,9 @@ function install({ reclaimStatusline = false, clearTombstone = false } = {}) {
|
|
|
1485
1522
|
// Register code-graph provider
|
|
1486
1523
|
registerStatuslineProvider('code-graph', codeGraphStatuslineCommand(), false);
|
|
1487
1524
|
|
|
1488
|
-
// 2. Hooks —
|
|
1489
|
-
//
|
|
1490
|
-
|
|
1491
|
-
// registerHooksToSettings is idempotent: strips priors then appends fresh.
|
|
1492
|
-
const hooksRegistered = registerHooksToSettings(settings);
|
|
1525
|
+
// 2. Hooks — registered in settings.json only where the plugin's hooks.json
|
|
1526
|
+
// does not reach; removed from it where it does (syncSettingsHooks).
|
|
1527
|
+
const hooksRegistered = syncSettingsHooks(settings);
|
|
1493
1528
|
if (hooksRegistered) settingsChanged = true;
|
|
1494
1529
|
|
|
1495
1530
|
// NOTE: enabledPlugins is managed by Claude Code's plugin system, not by lifecycle.
|
|
@@ -1765,9 +1800,9 @@ function update() {
|
|
|
1765
1800
|
// 2. Update code-graph provider in registry
|
|
1766
1801
|
registerStatuslineProvider('code-graph', codeGraphStatuslineCommand(), false);
|
|
1767
1802
|
|
|
1768
|
-
// 3. Hooks —
|
|
1769
|
-
//
|
|
1770
|
-
const hooksRegistered =
|
|
1803
|
+
// 3. Hooks — as in install(): settings.json carries them only where the
|
|
1804
|
+
// plugin's hooks.json does not (idempotent; paths re-anchor on update).
|
|
1805
|
+
const hooksRegistered = syncSettingsHooks(settings);
|
|
1771
1806
|
if (hooksRegistered) settingsChanged = true;
|
|
1772
1807
|
|
|
1773
1808
|
// NOTE: enabledPlugins is managed by Claude Code's plugin system, not by lifecycle.
|
|
@@ -2192,6 +2227,7 @@ module.exports = {
|
|
|
2192
2227
|
cacheDirVersion, // exported for the separator-agnostic test
|
|
2193
2228
|
|
|
2194
2229
|
verifyHooksFire, defaultHookFireProbes, // v0.67.0 — firing self-test
|
|
2230
|
+
hooksFromPluginManifest, syncSettingsHooks, // D2 — where the hooks live
|
|
2195
2231
|
activeInstallPath, isStaleRelicContext, // v0.49.1 — stale-relic downgrade guard
|
|
2196
2232
|
SETTINGS_HOOK_DESC, OUR_HOOK_SCRIPTS, OUR_DESCRIPTIONS, // v0.32.0 — for tests
|
|
2197
2233
|
PLUGIN_ROOT, // v0.32.1 — for tests / consumers
|