@sdsrs/code-graph 0.73.1 → 0.74.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/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/claude-plugin/scripts/adopt.js +263 -221
- package/claude-plugin/scripts/adopt.test.js +355 -648
- package/claude-plugin/scripts/session-init.js +13 -5
- package/claude-plugin/templates/plugin_code_graph_mcp.md +12 -9
- package/package.json +6 -6
|
@@ -1,29 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
|
-
// adopt / unadopt —
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// adopt / unadopt — installs the code-graph steering into this project:
|
|
4
|
+
// <cwd>/CLAUDE.md — a concise, sentinel-bracketed managed block
|
|
5
|
+
// (always-loaded; the router/decision summary)
|
|
6
|
+
// <cwd>/.claude/plugin_code_graph_mcp.md — the full decision table
|
|
7
|
+
// (on-demand; NOT auto-loaded each session)
|
|
8
|
+
// Idempotent. Replaces the pre-v0.74 "adopt into the auto-memory dir" scheme,
|
|
9
|
+
// which wrote into ~/.claude/projects/<slug>/memory/ (MEMORY.md sentinel +
|
|
10
|
+
// detail file) — equal weight to CLAUDE.md but polluting claude-mem-lite's
|
|
11
|
+
// index. migrateLegacyMemoryDir() cleans those legacy artifacts on upgrade.
|
|
8
12
|
const fs = require('fs');
|
|
9
13
|
const path = require('path');
|
|
10
14
|
const os = require('os');
|
|
11
15
|
const { PROJECT_MARKERS, isProjectRoot, isNonProjectCwd } = require('./project-detect');
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
// Managed-block sentinels. Bumped to v2 when the steering target moved from the
|
|
18
|
+
// auto-memory dir's MEMORY.md (v1) to the project's CLAUDE.md (v2). The strip
|
|
19
|
+
// regex matches ANY version so migration can remove the legacy v1 MEMORY.md block.
|
|
20
|
+
const SENTINEL_VERSION = 'v2';
|
|
21
|
+
const SENTINEL_BEGIN = `<!-- code-graph-mcp:begin ${SENTINEL_VERSION} -->`;
|
|
14
22
|
const SENTINEL_END = '<!-- code-graph-mcp:end -->';
|
|
15
|
-
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const m = first.match(/^<!-- adopted-by: (.+?) -->/);
|
|
24
|
-
return m ? m[1] : null;
|
|
25
|
-
} catch { return null; }
|
|
26
|
-
}
|
|
23
|
+
const SENTINEL_BEGIN_SRC = '<!-- code-graph-mcp:begin[^>]*-->';
|
|
24
|
+
// Marker on the first line of the installed .claude/plugin_code_graph_mcp.md so
|
|
25
|
+
// unadopt/needsRefresh can distinguish our generated copy from a user's own file
|
|
26
|
+
// of the same name (and so needsRefresh strips it before the bytewise compare).
|
|
27
|
+
const MANAGED_BY = '<!-- managed-by: code-graph-mcp -->';
|
|
28
|
+
// Legacy first-line marker on the old memory-dir detail file; migration deletes
|
|
29
|
+
// only files carrying it (never a user file that happens to share the name).
|
|
30
|
+
const LEGACY_ADOPTED_BY = '<!-- adopted-by:';
|
|
27
31
|
// Atomic write (tmp in same dir → rename) so a crash mid-write can't leave a
|
|
28
32
|
// half-written MEMORY.md / detail file — the dir is shared with claude-mem-lite,
|
|
29
33
|
// which reads MEMORY.md on every keyword match. Mirrors lifecycle.js
|
|
@@ -41,40 +45,68 @@ function writeFileAtomic(filePath, data) {
|
|
|
41
45
|
throw e;
|
|
42
46
|
}
|
|
43
47
|
}
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
48
|
+
// The managed block written into <cwd>/CLAUDE.md. Concise + always-loaded: a
|
|
49
|
+
// scannable trigger table that primes the right tool, ending with a pointer to
|
|
50
|
+
// the full table at .claude/plugin_code_graph_mcp.md (opened on demand, never
|
|
51
|
+
// auto-loaded). Project-type tailoring swaps a couple of rows (web → HTTP-route
|
|
52
|
+
// tracing; frontend → reference audits) — body of the detail doc is unchanged.
|
|
53
|
+
const BLOCK_HEADING = '## Code Graph (repo-wide AST index)';
|
|
54
|
+
|
|
55
|
+
function buildTriggerRows(projectType = 'generic') {
|
|
56
|
+
const base = [
|
|
57
|
+
['Who calls X / what X calls', '`code-graph-mcp callgraph X`'],
|
|
58
|
+
['Impact before editing a fn', '`code-graph-mcp impact X`'],
|
|
59
|
+
['Unfamiliar dir / module', '`code-graph-mcp overview <dir>`'],
|
|
60
|
+
['Symbol source / signature', '`code-graph-mcp show X`'],
|
|
61
|
+
['Concept search (no exact name)', '`code-graph-mcp search "…"` (vector: MCP `semantic_code_search`)'],
|
|
62
|
+
['grep + AST context', '`code-graph-mcp grep "pat" [paths]`'],
|
|
63
|
+
];
|
|
64
|
+
switch (projectType) {
|
|
65
|
+
case 'web-rs':
|
|
66
|
+
case 'web-node':
|
|
67
|
+
case 'web-py':
|
|
68
|
+
case 'web-go':
|
|
69
|
+
// HTTP route → handler chain matters; insert after the impact row.
|
|
70
|
+
return [base[0], base[1],
|
|
71
|
+
['HTTP route → handler chain', '`code-graph-mcp trace "GET /api/x"`'],
|
|
72
|
+
...base.slice(2)];
|
|
73
|
+
case 'frontend':
|
|
74
|
+
// Rename/refactor audits dominate; surface find-references explicitly.
|
|
75
|
+
return [base[0],
|
|
76
|
+
['Rename / refactor audit (refs)', '`code-graph-mcp refs X`'],
|
|
77
|
+
...base.slice(1)];
|
|
78
|
+
default:
|
|
79
|
+
return base;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Build the full sentinel-wrapped managed block for a project type. Deterministic
|
|
84
|
+
// (same type → byte-identical) so needsRefresh can bytewise-detect drift.
|
|
85
|
+
function buildBlock(projectType = 'generic') {
|
|
86
|
+
const rows = buildTriggerRows(projectType);
|
|
87
|
+
const table = ['| Intent | Command |', '|--------|---------|']
|
|
88
|
+
.concat(rows.map(([intent, cmd]) => `| ${intent} | ${cmd} |`))
|
|
89
|
+
.join('\n');
|
|
90
|
+
const body = [
|
|
91
|
+
BLOCK_HEADING,
|
|
92
|
+
'',
|
|
93
|
+
'AST + FTS + vector index of the whole repo — prefer over multi-round Grep/Read for',
|
|
94
|
+
'structural queries (LSP only sees open files; this sees everything). Fastest path = Bash CLI:',
|
|
95
|
+
'',
|
|
96
|
+
table,
|
|
97
|
+
'',
|
|
98
|
+
"Still use Grep for literal strings/regex in non-code files; still Read files you'll edit.",
|
|
99
|
+
'Full command + MCP-tool table: `.claude/plugin_code_graph_mcp.md`',
|
|
100
|
+
].join('\n');
|
|
101
|
+
return `${SENTINEL_BEGIN}\n${body}\n${SENTINEL_END}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Project-type detection tailors the CLAUDE.md block's trigger rows (buildBlock):
|
|
105
|
+
// a Rust CLI never benefits from HTTP-route tracing, a React frontend cares more
|
|
106
|
+
// about find-references for rename audits. Detection is cheap substring-on-marker
|
|
107
|
+
// (no AST, no graph): one fs.readFileSync per cwd. Failure mode is silent
|
|
108
|
+
// fall-back to 'generic' — false-negatives are strictly safer than false-positives
|
|
109
|
+
// that promote the wrong tool.
|
|
78
110
|
function readFileQuiet(p) {
|
|
79
111
|
try { return fs.readFileSync(p, 'utf8'); } catch { return ''; }
|
|
80
112
|
}
|
|
@@ -241,53 +273,12 @@ function detectProjectType(cwd = process.cwd(), env = process.env) {
|
|
|
241
273
|
return 'generic';
|
|
242
274
|
}
|
|
243
275
|
|
|
244
|
-
// Build the MEMORY.md index line for a project type. The 'generic' bucket
|
|
245
|
-
// returns the canonical INDEX_LINE so untyped projects (and the existing
|
|
246
|
-
// adopt.test.js fixtures, which use empty tmp dirs) stay byte-identical.
|
|
247
|
-
//
|
|
248
|
-
// For typed projects, the difference from generic is the tag set + the lead
|
|
249
|
-
// sentence — body of plugin_code_graph_mcp.md is unchanged. Decision table
|
|
250
|
-
// stays one source of truth; the index line just primes which subset matters
|
|
251
|
-
// most for THIS project.
|
|
252
|
-
function buildIndexLine(projectType = 'generic') {
|
|
253
|
-
const prefix = '- [code-graph-mcp](plugin_code_graph_mcp.md) ';
|
|
254
|
-
// v0.49 — CLI form leads: in Claude Code the MCP tools are deferred (need a
|
|
255
|
-
// ToolSearch load before first call) while Bash is always live; the only
|
|
256
|
-
// conversions observed in real coding nights were CLI invocations.
|
|
257
|
-
const coreSuffix =
|
|
258
|
-
'Bash 直呼 CLI 最快(零加载):`code-graph-mcp callgraph X / show X / overview <dir> / grep "pat" / impact X`;' +
|
|
259
|
-
'MCP 核心 7(get_call_graph/module_overview/semantic_code_search/ast_search/find_references/get_ast_node/project_map),决策表见全文';
|
|
260
|
-
switch (projectType) {
|
|
261
|
-
case 'web-rs':
|
|
262
|
-
case 'web-node':
|
|
263
|
-
case 'web-py':
|
|
264
|
-
case 'web-go':
|
|
265
|
-
return prefix +
|
|
266
|
-
'[trace-http-chain, http-route, callgraph, impact-analysis, find-references, module-overview, semantic-search, dependency-graph] — ' +
|
|
267
|
-
'HTTP 路由→handler 链路用 trace_http_chain(或 get_call_graph route_path=);改 handler 影响面用 impact;' +
|
|
268
|
-
'其他结构化查询同上 优先于 Grep。' + coreSuffix;
|
|
269
|
-
case 'frontend':
|
|
270
|
-
return prefix +
|
|
271
|
-
'[find-references, module-overview, semantic-search, callgraph, impact-analysis, ast-search] — ' +
|
|
272
|
-
'组件重命名/重构用 find_references(含 imports/inherits);模块层级用 module_overview;' +
|
|
273
|
-
'改 props/接口前用 impact 看下游;HTTP route 通常不适用。' + coreSuffix;
|
|
274
|
-
case 'rust':
|
|
275
|
-
case 'go':
|
|
276
|
-
case 'python':
|
|
277
|
-
case 'node':
|
|
278
|
-
return prefix +
|
|
279
|
-
'[callgraph, impact-analysis, find-references, module-overview, semantic-search, ast-search, dead-code, dependency-graph] — ' +
|
|
280
|
-
'改 X 影响面/谁调用 X/Y 模块 优先于 Grep;HTTP route 追踪通常不适用(无 web 框架);' +
|
|
281
|
-
'字面匹配走 Grep。' + coreSuffix;
|
|
282
|
-
case 'generic':
|
|
283
|
-
default:
|
|
284
|
-
return INDEX_LINE;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
276
|
const TEMPLATE_PATH = path.resolve(__dirname, '..', 'templates', 'plugin_code_graph_mcp.md');
|
|
288
277
|
const TARGET_NAME = 'plugin_code_graph_mcp.md';
|
|
289
278
|
|
|
290
|
-
//
|
|
279
|
+
// LEGACY (pre-v0.74) memory-dir path — now used ONLY by migrateLegacyMemoryDir
|
|
280
|
+
// to locate and remove the old MEMORY.md sentinel + detail file. Claude Code slug
|
|
281
|
+
// convention: every non-alphanumeric-non-hyphen char → `-`.
|
|
291
282
|
// `/mnt/data_ssd/dev/proj` → `-mnt-data-ssd-dev-proj`
|
|
292
283
|
// `/home/sds/.claude/x` → `-home-sds--claude-x` (double-dash from `/.`)
|
|
293
284
|
//
|
|
@@ -300,6 +291,13 @@ function memoryDir(cwd = process.cwd(), home = os.homedir()) {
|
|
|
300
291
|
return path.join(claudeDir, 'projects', slug, 'memory');
|
|
301
292
|
}
|
|
302
293
|
|
|
294
|
+
// New-scheme targets: steering lives in the project tree, not the memory dir.
|
|
295
|
+
// CLAUDE.md is auto-loaded each session (concise block); .claude/<detail> is
|
|
296
|
+
// opened on demand. Both keyed off the project cwd — no lossy slug, no collision.
|
|
297
|
+
function claudeMdPath(cwd = process.cwd()) { return path.join(cwd, 'CLAUDE.md'); }
|
|
298
|
+
function detailDir(cwd = process.cwd()) { return path.join(cwd, '.claude'); }
|
|
299
|
+
function detailPath(cwd = process.cwd()) { return path.join(detailDir(cwd), TARGET_NAME); }
|
|
300
|
+
|
|
303
301
|
function escapeRegex(s) {
|
|
304
302
|
return s.replace(/[\\/[\]^$.*+?()|{}]/g, '\\$&');
|
|
305
303
|
}
|
|
@@ -307,16 +305,18 @@ function escapeRegex(s) {
|
|
|
307
305
|
// Strip our sentinel block — well-formed first, then self-heal orphan begin/end.
|
|
308
306
|
// Shared by adopt (so re-adopt rewrites a stale/malformed block) and unadopt.
|
|
309
307
|
function stripSentinelBlock(text) {
|
|
308
|
+
// Match ANY begin version (v1 legacy MEMORY.md block, v2 CLAUDE.md block) so a
|
|
309
|
+
// single strip handles both the new target and the legacy migration cleanup.
|
|
310
310
|
const wellFormed = new RegExp(
|
|
311
|
-
`${
|
|
311
|
+
`${SENTINEL_BEGIN_SRC}[\\s\\S]*?${escapeRegex(SENTINEL_END)}\\n?`, 'g'
|
|
312
312
|
);
|
|
313
313
|
let out = text.replace(wellFormed, '');
|
|
314
314
|
// Orphan BEGIN with no matching END (truncation / partial edit).
|
|
315
|
-
// Strip from BEGIN to the next blank line or EOF — the file
|
|
316
|
-
// claude-mem-lite, so we must not eat past a blank-line boundary.
|
|
317
|
-
if (
|
|
315
|
+
// Strip from BEGIN to the next blank line or EOF — the file may be shared with
|
|
316
|
+
// claude-mem-lite (legacy MEMORY.md), so we must not eat past a blank-line boundary.
|
|
317
|
+
if (new RegExp(SENTINEL_BEGIN_SRC).test(out)) {
|
|
318
318
|
out = out.replace(
|
|
319
|
-
new RegExp(`${
|
|
319
|
+
new RegExp(`${SENTINEL_BEGIN_SRC}[\\s\\S]*?(?=\\n\\n|$)`, 'g'),
|
|
320
320
|
''
|
|
321
321
|
);
|
|
322
322
|
}
|
|
@@ -339,103 +339,89 @@ function platformGuard() {
|
|
|
339
339
|
// now lives in project-detect.js — the single activation gate shared with
|
|
340
340
|
// mcp-launcher.js and session-init.js. Imported above and re-exported below.
|
|
341
341
|
|
|
342
|
-
function adopt({ cwd,
|
|
342
|
+
function adopt({ cwd, templatePath } = {}) {
|
|
343
343
|
const blocked = platformGuard();
|
|
344
344
|
if (blocked) return blocked;
|
|
345
345
|
|
|
346
346
|
const effectiveCwd = cwd || process.cwd();
|
|
347
|
-
// Gate
|
|
348
|
-
//
|
|
349
|
-
// pre-creates ~/.claude/projects/<slug>/memory for every session (including
|
|
350
|
-
// the ~2035 headless /tmp mem-lite calls), and the old guard — nested inside
|
|
351
|
-
// `if (!fs.existsSync(dir))` — was bypassed in exactly that case, letting
|
|
352
|
-
// /tmp get adopted (sentinel written into its MEMORY.md). See project-detect.js.
|
|
347
|
+
// Gate on a real-project cwd BEFORE touching the filesystem — Claude Code also
|
|
348
|
+
// spawns headless /tmp sessions (claude-mem-lite); we must leave those alone.
|
|
353
349
|
if (isNonProjectCwd(effectiveCwd)) {
|
|
354
|
-
return { ok: false, reason: 'not-a-project',
|
|
350
|
+
return { ok: false, reason: 'not-a-project', cwd: effectiveCwd };
|
|
355
351
|
}
|
|
356
|
-
const dir = memoryDir(cwd, home);
|
|
357
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
358
|
-
const target = path.join(dir, TARGET_NAME);
|
|
359
352
|
const tpl = templatePath || TEMPLATE_PATH;
|
|
360
353
|
if (!fs.existsSync(tpl)) {
|
|
361
354
|
return { ok: false, reason: 'no-template', template: tpl };
|
|
362
355
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const indexPath = path.join(dir, 'MEMORY.md');
|
|
377
|
-
const index = fs.existsSync(indexPath) ? fs.readFileSync(indexPath, 'utf8') : '# Memory Index\n';
|
|
378
|
-
// Per-project index line: tagged tools + lead sentence tailored to the
|
|
379
|
-
// detected project type. Falls back to the canonical INDEX_LINE for
|
|
380
|
-
// generic / untyped cwds (preserves byte-identity with prior versions).
|
|
381
|
-
const indexLine = buildIndexLine(detectProjectType(effectiveCwd));
|
|
382
|
-
const desiredBlock = `${SENTINEL_BEGIN}\n${indexLine}\n${SENTINEL_END}`;
|
|
383
|
-
|
|
384
|
-
// Already-adopted-and-well-formed: skip the write entirely.
|
|
385
|
-
if (index.includes(desiredBlock)) {
|
|
386
|
-
return { ok: true, target, indexPath, indexed: false, healed: false, collisionWith };
|
|
356
|
+
|
|
357
|
+
// 1. Install the detail doc at <cwd>/.claude/plugin_code_graph_mcp.md.
|
|
358
|
+
// First line is the MANAGED_BY marker (HTML comment → invisible in rendered
|
|
359
|
+
// markdown) so unadopt/needsRefresh can tell our generated copy from a user
|
|
360
|
+
// file of the same name. needsRefresh strips it before the bytewise compare.
|
|
361
|
+
const dDir = detailDir(effectiveCwd);
|
|
362
|
+
if (!fs.existsSync(dDir)) fs.mkdirSync(dDir, { recursive: true });
|
|
363
|
+
const dPath = detailPath(effectiveCwd);
|
|
364
|
+
const desiredDetail = Buffer.concat([Buffer.from(`${MANAGED_BY}\n`), fs.readFileSync(tpl)]);
|
|
365
|
+
let detailWritten = false;
|
|
366
|
+
if (!fs.existsSync(dPath) || !fs.readFileSync(dPath).equals(desiredDetail)) {
|
|
367
|
+
writeFileAtomic(dPath, desiredDetail);
|
|
368
|
+
detailWritten = true;
|
|
387
369
|
}
|
|
388
370
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
371
|
+
// 2. Ensure the managed block in <cwd>/CLAUDE.md. Create-if-missing, else
|
|
372
|
+
// inject — only our sentinel block is managed; the user's own prose is never
|
|
373
|
+
// touched. Per-project trigger rows tailored to the detected project type.
|
|
374
|
+
const cPath = claudeMdPath(effectiveCwd);
|
|
375
|
+
const block = buildBlock(detectProjectType(effectiveCwd));
|
|
376
|
+
const exists = fs.existsSync(cPath);
|
|
377
|
+
const current = exists ? fs.readFileSync(cPath, 'utf8') : '';
|
|
378
|
+
if (current.includes(block)) {
|
|
379
|
+
return { ok: true, detailPath: dPath, claudeMdPath: cPath, detailWritten, claudeMdWritten: false, created: false, healed: false };
|
|
380
|
+
}
|
|
381
|
+
const cleaned = exists ? stripSentinelBlock(current) : '';
|
|
382
|
+
const healed = exists && cleaned !== current;
|
|
383
|
+
const base = cleaned.replace(/\n+$/, '');
|
|
384
|
+
const prefix = base ? base + '\n\n' : '';
|
|
385
|
+
writeFileAtomic(cPath, prefix + block + '\n');
|
|
386
|
+
return { ok: true, detailPath: dPath, claudeMdPath: cPath, detailWritten, claudeMdWritten: true, created: !exists, healed };
|
|
394
387
|
}
|
|
395
388
|
|
|
396
|
-
//
|
|
389
|
+
// "已 install" 判定:detail 文件在 + CLAUDE.md 内有我们的 sentinel 块(任意版本)。
|
|
397
390
|
// 用在 maybeAutoAdopt 里做幂等门,也用在 session-init 里推导 quietHooks。
|
|
398
|
-
function isAdopted({ cwd
|
|
399
|
-
const
|
|
400
|
-
const
|
|
401
|
-
const
|
|
402
|
-
if (!fs.existsSync(
|
|
403
|
-
const
|
|
404
|
-
return
|
|
391
|
+
function isAdopted({ cwd } = {}) {
|
|
392
|
+
const effectiveCwd = cwd || process.cwd();
|
|
393
|
+
const cPath = claudeMdPath(effectiveCwd);
|
|
394
|
+
const dPath = detailPath(effectiveCwd);
|
|
395
|
+
if (!fs.existsSync(dPath) || !fs.existsSync(cPath)) return false;
|
|
396
|
+
const c = fs.readFileSync(cPath, 'utf8');
|
|
397
|
+
return new RegExp(SENTINEL_BEGIN_SRC).test(c) && c.includes(SENTINEL_END);
|
|
405
398
|
}
|
|
406
399
|
|
|
407
|
-
//
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
const
|
|
413
|
-
const
|
|
414
|
-
const indexPath = path.join(dir, 'MEMORY.md');
|
|
400
|
+
// shipped template / 管理块 与已落地版本出现漂移时返回 true。让已 install 的项目
|
|
401
|
+
// 在下次 SessionStart 自动对齐到插件最新决策表(含 v1→v2 升级、项目类型变更)。
|
|
402
|
+
// 手动编辑会被覆盖——锁定方式:CODE_GRAPH_NO_TEMPLATE_REFRESH=1。
|
|
403
|
+
function needsRefresh({ cwd, templatePath } = {}) {
|
|
404
|
+
const effectiveCwd = cwd || process.cwd();
|
|
405
|
+
const cPath = claudeMdPath(effectiveCwd);
|
|
406
|
+
const dPath = detailPath(effectiveCwd);
|
|
415
407
|
const tpl = templatePath || TEMPLATE_PATH;
|
|
416
|
-
if (!fs.existsSync(
|
|
408
|
+
if (!fs.existsSync(dPath) || !fs.existsSync(cPath) || !fs.existsSync(tpl)) {
|
|
417
409
|
return false;
|
|
418
410
|
}
|
|
411
|
+
// Detail-doc body drift — strip the leading MANAGED_BY marker line first.
|
|
419
412
|
const shipped = fs.readFileSync(tpl);
|
|
420
|
-
const current = fs.readFileSync(
|
|
421
|
-
// Strip the leading "<!-- adopted-by: ... -->\n" collision marker (D fix)
|
|
422
|
-
// before bytewise comparing — its presence/path naturally diverges from
|
|
423
|
-
// the shipped template.
|
|
413
|
+
const current = fs.readFileSync(dPath);
|
|
424
414
|
let body = current;
|
|
425
415
|
const nl = current.indexOf(0x0a);
|
|
426
|
-
if (nl > 0 &&
|
|
416
|
+
if (nl > 0 && current.subarray(0, nl).toString().includes('managed-by: code-graph-mcp')) {
|
|
427
417
|
body = current.subarray(nl + 1);
|
|
428
418
|
}
|
|
429
419
|
if (!shipped.equals(body)) return true;
|
|
430
|
-
|
|
431
|
-
//
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
const effectiveCwd = cwd || process.cwd();
|
|
436
|
-
const indexLine = buildIndexLine(detectProjectType(effectiveCwd));
|
|
437
|
-
const desiredBlock = `${SENTINEL_BEGIN}\n${indexLine}\n${SENTINEL_END}`;
|
|
438
|
-
return !index.includes(desiredBlock);
|
|
420
|
+
// CLAUDE.md managed-block drift. Detection is deterministic so adopt and
|
|
421
|
+
// needsRefresh always agree on the variant — including when a project gains a
|
|
422
|
+
// web-framework dep and switches type bucket, or on a sentinel version bump.
|
|
423
|
+
const block = buildBlock(detectProjectType(effectiveCwd));
|
|
424
|
+
return !fs.readFileSync(cPath, 'utf8').includes(block);
|
|
439
425
|
}
|
|
440
426
|
|
|
441
427
|
// 检测脚本是否从 Claude Code 插件 cache 运行。
|
|
@@ -454,9 +440,44 @@ function isPluginModeInstall(scriptPath = __dirname) {
|
|
|
454
440
|
return false;
|
|
455
441
|
}
|
|
456
442
|
|
|
457
|
-
//
|
|
458
|
-
//
|
|
459
|
-
//
|
|
443
|
+
// One-time per-project cleanup of the legacy memory-dir adoption (pre-v0.74):
|
|
444
|
+
// the v1 sentinel block in MEMORY.md + the adopted-by-marked detail file under
|
|
445
|
+
// ~/.claude/projects/<slug>/memory/. Touches only the CURRENT project's memory
|
|
446
|
+
// dir (a single known path derived from cwd) — no ~/.claude traversal (§8 SAFETY).
|
|
447
|
+
// Idempotent + guarded: only strips OUR sentinel, only deletes a file carrying
|
|
448
|
+
// the adopted-by marker. Safe to run every SessionStart.
|
|
449
|
+
function migrateLegacyMemoryDir({ cwd, home } = {}) {
|
|
450
|
+
const result = { memoryIndexPruned: false, legacyDetailRemoved: false };
|
|
451
|
+
if (platformGuard()) return result;
|
|
452
|
+
const dir = memoryDir(cwd, home);
|
|
453
|
+
const legacyDetail = path.join(dir, TARGET_NAME);
|
|
454
|
+
if (fs.existsSync(legacyDetail)) {
|
|
455
|
+
try {
|
|
456
|
+
const head = fs.readFileSync(legacyDetail, 'utf8').split('\n', 1)[0];
|
|
457
|
+
if (head.startsWith(LEGACY_ADOPTED_BY)) {
|
|
458
|
+
fs.unlinkSync(legacyDetail);
|
|
459
|
+
result.legacyDetailRemoved = true;
|
|
460
|
+
}
|
|
461
|
+
} catch { /* unreadable → leave it */ }
|
|
462
|
+
}
|
|
463
|
+
const legacyIndex = path.join(dir, 'MEMORY.md');
|
|
464
|
+
if (fs.existsSync(legacyIndex)) {
|
|
465
|
+
try {
|
|
466
|
+
const before = fs.readFileSync(legacyIndex, 'utf8');
|
|
467
|
+
const after = stripSentinelBlock(before);
|
|
468
|
+
if (after !== before) {
|
|
469
|
+
writeFileAtomic(legacyIndex, after);
|
|
470
|
+
result.memoryIndexPruned = true;
|
|
471
|
+
}
|
|
472
|
+
} catch { /* unreadable → leave it */ }
|
|
473
|
+
}
|
|
474
|
+
return result;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// 上下文感知默认:插件模式下首次 SessionStart 静默安装(创建/注入 CLAUDE.md 块 +
|
|
478
|
+
// .claude/ detail 文件)。/plugin install 本身已构成知情同意;npm / npx / 裸 checkout
|
|
479
|
+
// 保持 opt-in。退出:CODE_GRAPH_NO_AUTO_ADOPT=1。每次 SessionStart 先清理旧 memory-dir
|
|
480
|
+
// 制品(升级自动迁移),再安装/刷新。
|
|
460
481
|
function maybeAutoAdopt({ cwd, home, env, scriptPath } = {}) {
|
|
461
482
|
env = env || process.env;
|
|
462
483
|
if (env.CODE_GRAPH_NO_AUTO_ADOPT === '1') {
|
|
@@ -465,55 +486,71 @@ function maybeAutoAdopt({ cwd, home, env, scriptPath } = {}) {
|
|
|
465
486
|
if (!isPluginModeInstall(scriptPath || __dirname)) {
|
|
466
487
|
return { attempted: false, reason: 'not-plugin-mode' };
|
|
467
488
|
}
|
|
468
|
-
|
|
469
|
-
|
|
489
|
+
// Clean legacy memory-dir artifacts before installing the new CLAUDE.md scheme.
|
|
490
|
+
const migrated = migrateLegacyMemoryDir({ cwd, home });
|
|
491
|
+
if (isAdopted({ cwd })) {
|
|
492
|
+
// shipped template / 管理块 漂移时重跑 adopt 对齐。
|
|
470
493
|
// opt-out: CODE_GRAPH_NO_TEMPLATE_REFRESH=1(锁定手动编辑)。
|
|
471
|
-
if (env.CODE_GRAPH_NO_TEMPLATE_REFRESH !== '1' && needsRefresh({ cwd
|
|
472
|
-
const result = adopt({ cwd
|
|
473
|
-
return { attempted: true, reason: 'refreshed', result };
|
|
494
|
+
if (env.CODE_GRAPH_NO_TEMPLATE_REFRESH !== '1' && needsRefresh({ cwd })) {
|
|
495
|
+
const result = adopt({ cwd });
|
|
496
|
+
return { attempted: true, reason: 'refreshed', result, migrated };
|
|
474
497
|
}
|
|
475
|
-
return { attempted: false, reason: 'already-adopted' };
|
|
498
|
+
return { attempted: false, reason: 'already-adopted', migrated };
|
|
476
499
|
}
|
|
477
|
-
const result = adopt({ cwd
|
|
478
|
-
return { attempted: true, reason: 'adopted', result };
|
|
500
|
+
const result = adopt({ cwd });
|
|
501
|
+
return { attempted: true, reason: 'adopted', result, migrated };
|
|
479
502
|
}
|
|
480
503
|
|
|
481
504
|
function unadopt({ cwd, home } = {}) {
|
|
482
505
|
const blocked = platformGuard();
|
|
483
506
|
if (blocked) return blocked;
|
|
484
507
|
|
|
485
|
-
const
|
|
486
|
-
const
|
|
487
|
-
const
|
|
508
|
+
const effectiveCwd = cwd || process.cwd();
|
|
509
|
+
const cPath = claudeMdPath(effectiveCwd);
|
|
510
|
+
const dPath = detailPath(effectiveCwd);
|
|
488
511
|
let fileRemoved = false;
|
|
489
|
-
let
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
512
|
+
let blockPruned = false;
|
|
513
|
+
let claudeMdRemoved = false;
|
|
514
|
+
|
|
515
|
+
// Detail file — guard on our marker so a user's same-named file is never deleted.
|
|
516
|
+
if (fs.existsSync(dPath)) {
|
|
517
|
+
let mine = false;
|
|
518
|
+
try {
|
|
519
|
+
const head = fs.readFileSync(dPath, 'utf8').split('\n', 1)[0];
|
|
520
|
+
mine = head.includes(MANAGED_BY) || head.startsWith(LEGACY_ADOPTED_BY);
|
|
521
|
+
} catch { mine = false; }
|
|
522
|
+
if (mine) { fs.unlinkSync(dPath); fileRemoved = true; }
|
|
494
523
|
}
|
|
495
|
-
|
|
496
|
-
|
|
524
|
+
|
|
525
|
+
// CLAUDE.md — strip only our block. If nothing else remains, remove the file
|
|
526
|
+
// we created; otherwise preserve the user's prose.
|
|
527
|
+
if (fs.existsSync(cPath)) {
|
|
528
|
+
const before = fs.readFileSync(cPath, 'utf8');
|
|
497
529
|
const after = stripSentinelBlock(before);
|
|
498
530
|
if (after !== before) {
|
|
499
|
-
|
|
500
|
-
|
|
531
|
+
blockPruned = true;
|
|
532
|
+
if (after.trim() === '') {
|
|
533
|
+
fs.unlinkSync(cPath);
|
|
534
|
+
claudeMdRemoved = true;
|
|
535
|
+
} else {
|
|
536
|
+
writeFileAtomic(cPath, after);
|
|
537
|
+
}
|
|
501
538
|
}
|
|
502
539
|
}
|
|
503
|
-
|
|
540
|
+
|
|
541
|
+
// Also sweep any legacy memory-dir remnants (uninstall before auto-migration ran).
|
|
542
|
+
const migrated = migrateLegacyMemoryDir({ cwd, home });
|
|
543
|
+
|
|
544
|
+
return { ok: true, fileRemoved, blockPruned, claudeMdRemoved, target: dPath, claudeMdPath: cPath, migrated };
|
|
504
545
|
}
|
|
505
546
|
|
|
506
547
|
function formatResult(action, result) {
|
|
507
548
|
if (!result.ok && result.reason === 'windows-not-supported') {
|
|
508
|
-
return '[code-graph] adopt/unadopt are POSIX-only
|
|
509
|
-
'
|
|
549
|
+
return '[code-graph] adopt/unadopt are POSIX-only on this build. ' +
|
|
550
|
+
'Edit CLAUDE.md manually to opt in.';
|
|
510
551
|
}
|
|
511
552
|
if (action === 'adopt') {
|
|
512
553
|
if (!result.ok) {
|
|
513
|
-
if (result.reason === 'no-memory-dir') {
|
|
514
|
-
return `[code-graph] Memory dir not found: ${result.dir}\n` +
|
|
515
|
-
' Run \`claude\` at least once in this project to create it.';
|
|
516
|
-
}
|
|
517
554
|
if (result.reason === 'not-a-project') {
|
|
518
555
|
return `[code-graph] Not a project root: ${result.cwd}\n` +
|
|
519
556
|
' No project marker (.git, Cargo.toml, package.json, pyproject.toml, ...).\n' +
|
|
@@ -524,28 +561,31 @@ function formatResult(action, result) {
|
|
|
524
561
|
}
|
|
525
562
|
return `[code-graph] adopt failed: ${result.reason || 'unknown'}`;
|
|
526
563
|
}
|
|
527
|
-
const lines = [
|
|
528
|
-
if (result.
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
// of adoption). Adoption now only governs MEMORY.md sentinel + decision-table
|
|
538
|
-
// refresh; the noisy hook needs an explicit opt-in.
|
|
539
|
-
lines.push('[code-graph] Active. SessionStart project_map injection: OFF (default).');
|
|
540
|
-
lines.push('[code-graph] Opt in to map dump: CODE_GRAPH_VERBOSE_HOOKS=1');
|
|
541
|
-
lines.push('[code-graph] Legacy override: CODE_GRAPH_QUIET_HOOKS=0 (force noisy) / =1 (force quiet)');
|
|
564
|
+
const lines = [];
|
|
565
|
+
if (result.created) lines.push(`[code-graph] Created → ${result.claudeMdPath} (code-graph block)`);
|
|
566
|
+
else if (result.claudeMdWritten) lines.push(`[code-graph] Updated code-graph block → ${result.claudeMdPath}`);
|
|
567
|
+
else lines.push('[code-graph] CLAUDE.md block already up-to-date — no write');
|
|
568
|
+
if (result.healed) lines.push('[code-graph] Healed a malformed prior block.');
|
|
569
|
+
lines.push(`[code-graph] Detail table → ${result.detailPath}`);
|
|
570
|
+
lines.push('[code-graph] CLAUDE.md is git-tracked — commit the block, or gitignore');
|
|
571
|
+
lines.push('[code-graph] .claude/plugin_code_graph_mcp.md (a generated copy) as you prefer.');
|
|
572
|
+
lines.push('[code-graph] Reverse: code-graph-mcp unadopt');
|
|
573
|
+
lines.push('[code-graph] Opt out: CODE_GRAPH_NO_AUTO_ADOPT=1 in ~/.claude/settings.json env');
|
|
542
574
|
return lines.join('\n');
|
|
543
575
|
}
|
|
544
576
|
if (action === 'unadopt') {
|
|
545
577
|
const lines = [];
|
|
578
|
+
if (result.claudeMdRemoved) lines.push(`[code-graph] Removed → ${result.claudeMdPath} (was code-graph-only)`);
|
|
579
|
+
else if (result.blockPruned) lines.push(`[code-graph] De-blocked → ${result.claudeMdPath}`);
|
|
546
580
|
if (result.fileRemoved) lines.push(`[code-graph] Removed → ${result.target}`);
|
|
547
|
-
|
|
548
|
-
if (
|
|
581
|
+
const m = result.migrated || {};
|
|
582
|
+
if (m.memoryIndexPruned || m.legacyDetailRemoved) {
|
|
583
|
+
lines.push('[code-graph] Cleaned legacy memory-dir artifacts.');
|
|
584
|
+
}
|
|
585
|
+
if (!result.blockPruned && !result.fileRemoved && !result.claudeMdRemoved &&
|
|
586
|
+
!(m.memoryIndexPruned || m.legacyDetailRemoved)) {
|
|
587
|
+
lines.push('[code-graph] Nothing to unadopt');
|
|
588
|
+
}
|
|
549
589
|
return lines.join('\n');
|
|
550
590
|
}
|
|
551
591
|
return '';
|
|
@@ -561,8 +601,10 @@ if (require.main === module) {
|
|
|
561
601
|
module.exports = {
|
|
562
602
|
adopt, unadopt, memoryDir, formatResult, stripSentinelBlock,
|
|
563
603
|
isAdopted, isPluginModeInstall, maybeAutoAdopt, needsRefresh, isProjectRoot,
|
|
564
|
-
detectProjectType,
|
|
604
|
+
detectProjectType, buildBlock, buildTriggerRows, migrateLegacyMemoryDir,
|
|
605
|
+
claudeMdPath, detailDir, detailPath,
|
|
565
606
|
extractCargoRuntimeDeps, extractPyRuntimeDeps, extractGoDirectRequires,
|
|
566
|
-
SENTINEL_BEGIN, SENTINEL_END,
|
|
607
|
+
SENTINEL_BEGIN, SENTINEL_END, SENTINEL_BEGIN_SRC, SENTINEL_VERSION,
|
|
608
|
+
MANAGED_BY, TEMPLATE_PATH, TARGET_NAME,
|
|
567
609
|
PROJECT_MARKERS, PROJECT_TYPES, isNonProjectCwd,
|
|
568
610
|
};
|