@sdsrs/code-graph 0.7.18 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "sdsrs"
6
6
  },
7
- "version": "0.7.18",
7
+ "version": "0.8.1",
8
8
  "keywords": [
9
9
  "code-graph",
10
10
  "ast",
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // adopt / unadopt — writes plugin_code_graph_mcp.md into this project's
4
+ // claude-mem memory dir and maintains a sentinel-bracketed index entry in
5
+ // MEMORY.md. Idempotent. Used by invited-memory pattern with CODE_GRAPH_QUIET_HOOKS=1.
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const os = require('os');
9
+
10
+ const SENTINEL_BEGIN = '<!-- code-graph-mcp:begin v1 -->';
11
+ const SENTINEL_END = '<!-- code-graph-mcp:end -->';
12
+ const INDEX_LINE = '- [code-graph-mcp](plugin_code_graph_mcp.md) — "谁调 X / 改 X 炸啥 / 模块结构" → `get_call_graph` / `impact_analysis` / `module_overview`,替代多步 Grep/Read';
13
+ const TEMPLATE_PATH = path.resolve(__dirname, '..', 'templates', 'plugin_code_graph_mcp.md');
14
+ const TARGET_NAME = 'plugin_code_graph_mcp.md';
15
+
16
+ function memoryDir(cwd = process.cwd(), home = os.homedir()) {
17
+ const slug = cwd.replace(/\//g, '-');
18
+ return path.join(home, '.claude', 'projects', slug, 'memory');
19
+ }
20
+
21
+ function escapeRegex(s) {
22
+ return s.replace(/[\\/[\]^$.*+?()|{}]/g, '\\$&');
23
+ }
24
+
25
+ // Strip our sentinel block — well-formed first, then self-heal orphan begin/end.
26
+ // Shared by adopt (so re-adopt rewrites a stale/malformed block) and unadopt.
27
+ function stripSentinelBlock(text) {
28
+ const wellFormed = new RegExp(
29
+ `${escapeRegex(SENTINEL_BEGIN)}[\\s\\S]*?${escapeRegex(SENTINEL_END)}\\n?`, 'g'
30
+ );
31
+ let out = text.replace(wellFormed, '');
32
+ // Orphan BEGIN with no matching END (truncation / partial edit).
33
+ // Strip from BEGIN to the next blank line or EOF — the file is shared with
34
+ // claude-mem-lite, so we must not eat past a blank-line boundary.
35
+ if (out.includes(SENTINEL_BEGIN)) {
36
+ out = out.replace(
37
+ new RegExp(`${escapeRegex(SENTINEL_BEGIN)}[\\s\\S]*?(?=\\n\\n|$)`, 'g'),
38
+ ''
39
+ );
40
+ }
41
+ // Orphan END line by itself.
42
+ if (out.includes(SENTINEL_END)) {
43
+ out = out.split('\n').filter(l => l.trim() !== SENTINEL_END).join('\n');
44
+ }
45
+ // Collapse blank-line runs introduced by stripping mid-paragraph blocks.
46
+ return out.replace(/\n{3,}/g, '\n\n');
47
+ }
48
+
49
+ function platformGuard() {
50
+ if (process.platform === 'win32') {
51
+ return { ok: false, reason: 'windows-not-supported' };
52
+ }
53
+ return null;
54
+ }
55
+
56
+ function adopt({ cwd, home, templatePath } = {}) {
57
+ const blocked = platformGuard();
58
+ if (blocked) return blocked;
59
+
60
+ const dir = memoryDir(cwd, home);
61
+ if (!fs.existsSync(dir)) {
62
+ return { ok: false, reason: 'no-memory-dir', dir };
63
+ }
64
+ const target = path.join(dir, TARGET_NAME);
65
+ const tpl = templatePath || TEMPLATE_PATH;
66
+ if (!fs.existsSync(tpl)) {
67
+ return { ok: false, reason: 'no-template', template: tpl };
68
+ }
69
+ fs.copyFileSync(tpl, target);
70
+
71
+ const indexPath = path.join(dir, 'MEMORY.md');
72
+ const index = fs.existsSync(indexPath) ? fs.readFileSync(indexPath, 'utf8') : '# Memory Index\n';
73
+ const desiredBlock = `${SENTINEL_BEGIN}\n${INDEX_LINE}\n${SENTINEL_END}`;
74
+
75
+ // Already-adopted-and-well-formed: skip the write entirely.
76
+ if (index.includes(desiredBlock)) {
77
+ return { ok: true, target, indexPath, indexed: false, healed: false };
78
+ }
79
+
80
+ const cleaned = stripSentinelBlock(index);
81
+ const healed = cleaned !== index;
82
+ const base = cleaned.endsWith('\n') ? cleaned : cleaned + '\n';
83
+ fs.writeFileSync(indexPath, base + desiredBlock + '\n');
84
+ return { ok: true, target, indexPath, indexed: true, healed };
85
+ }
86
+
87
+ function unadopt({ cwd, home } = {}) {
88
+ const blocked = platformGuard();
89
+ if (blocked) return blocked;
90
+
91
+ const dir = memoryDir(cwd, home);
92
+ const target = path.join(dir, TARGET_NAME);
93
+ const indexPath = path.join(dir, 'MEMORY.md');
94
+ let fileRemoved = false;
95
+ let indexPruned = false;
96
+
97
+ if (fs.existsSync(target)) {
98
+ fs.unlinkSync(target);
99
+ fileRemoved = true;
100
+ }
101
+ if (fs.existsSync(indexPath)) {
102
+ const before = fs.readFileSync(indexPath, 'utf8');
103
+ const after = stripSentinelBlock(before);
104
+ if (after !== before) {
105
+ fs.writeFileSync(indexPath, after);
106
+ indexPruned = true;
107
+ }
108
+ }
109
+ return { ok: true, fileRemoved, indexPruned, target, indexPath };
110
+ }
111
+
112
+ function formatResult(action, result) {
113
+ if (!result.ok && result.reason === 'windows-not-supported') {
114
+ return '[code-graph] adopt/unadopt are POSIX-only — claude-mem-lite slug ' +
115
+ 'convention on Windows is unverified. Edit MEMORY.md manually to opt in.';
116
+ }
117
+ if (action === 'adopt') {
118
+ if (!result.ok) {
119
+ if (result.reason === 'no-memory-dir') {
120
+ return `[code-graph] Memory dir not found: ${result.dir}\n` +
121
+ ' Run \`claude\` at least once in this project to create it.';
122
+ }
123
+ if (result.reason === 'no-template') {
124
+ return `[code-graph] Template missing: ${result.template}`;
125
+ }
126
+ return `[code-graph] adopt failed: ${result.reason || 'unknown'}`;
127
+ }
128
+ const lines = [`[code-graph] Adopted → ${result.target}`];
129
+ if (result.healed) lines.push(`[code-graph] Healed malformed sentinel block → ${result.indexPath}`);
130
+ else if (result.indexed) lines.push(`[code-graph] Indexed → ${result.indexPath}`);
131
+ else lines.push(`[code-graph] Index already up-to-date — no write`);
132
+ lines.push('[code-graph] Activate: set CODE_GRAPH_QUIET_HOOKS=1 in ~/.claude/settings.json env');
133
+ return lines.join('\n');
134
+ }
135
+ if (action === 'unadopt') {
136
+ const lines = [];
137
+ if (result.fileRemoved) lines.push(`[code-graph] Removed → ${result.target}`);
138
+ if (result.indexPruned) lines.push(`[code-graph] De-indexed → ${result.indexPath}`);
139
+ if (!result.fileRemoved && !result.indexPruned) lines.push('[code-graph] Nothing to unadopt');
140
+ return lines.join('\n');
141
+ }
142
+ return '';
143
+ }
144
+
145
+ if (require.main === module) {
146
+ const action = process.argv[2] === 'unadopt' ? 'unadopt' : 'adopt';
147
+ const result = action === 'unadopt' ? unadopt() : adopt();
148
+ process.stdout.write(formatResult(action, result) + '\n');
149
+ process.exit(result.ok === false ? 1 : 0);
150
+ }
151
+
152
+ module.exports = {
153
+ adopt, unadopt, memoryDir, formatResult, stripSentinelBlock,
154
+ SENTINEL_BEGIN, SENTINEL_END, INDEX_LINE, TEMPLATE_PATH, TARGET_NAME,
155
+ };
@@ -0,0 +1,214 @@
1
+ 'use strict';
2
+ const test = require('node:test');
3
+ const assert = require('node:assert');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const os = require('os');
7
+ const {
8
+ adopt, unadopt, memoryDir, stripSentinelBlock,
9
+ SENTINEL_BEGIN, SENTINEL_END, INDEX_LINE, TEMPLATE_PATH,
10
+ } = require('./adopt');
11
+
12
+ function makeSandbox() {
13
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-adopt-home-'));
14
+ const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-adopt-cwd-'));
15
+ // Pre-create the memory dir (claude-mem convention — we don't create it).
16
+ const dir = memoryDir(cwd, home);
17
+ fs.mkdirSync(dir, { recursive: true });
18
+ return { home, cwd, dir, cleanup: () => {
19
+ fs.rmSync(home, { recursive: true, force: true });
20
+ fs.rmSync(cwd, { recursive: true, force: true });
21
+ }};
22
+ }
23
+
24
+ test('memoryDir slugifies cwd path', () => {
25
+ const dir = memoryDir('/home/alice/proj', '/home/alice');
26
+ assert.strictEqual(dir, '/home/alice/.claude/projects/-home-alice-proj/memory');
27
+ });
28
+
29
+ test('adopt writes template and appends sentinel block when index absent', () => {
30
+ const sb = makeSandbox();
31
+ try {
32
+ const res = adopt({ cwd: sb.cwd, home: sb.home });
33
+ assert.strictEqual(res.ok, true);
34
+ assert.strictEqual(res.indexed, true);
35
+ assert.ok(fs.existsSync(res.target), 'plugin file written');
36
+ const index = fs.readFileSync(res.indexPath, 'utf8');
37
+ assert.match(index, /^# Memory Index/);
38
+ assert.ok(index.includes(SENTINEL_BEGIN));
39
+ assert.ok(index.includes(SENTINEL_END));
40
+ assert.ok(index.includes(INDEX_LINE));
41
+ } finally { sb.cleanup(); }
42
+ });
43
+
44
+ test('adopt is idempotent — no duplicate sentinel on re-run', () => {
45
+ const sb = makeSandbox();
46
+ try {
47
+ adopt({ cwd: sb.cwd, home: sb.home });
48
+ const res2 = adopt({ cwd: sb.cwd, home: sb.home });
49
+ assert.strictEqual(res2.indexed, false, 'second run leaves index alone');
50
+ const index = fs.readFileSync(res2.indexPath, 'utf8');
51
+ const matches = index.match(new RegExp(SENTINEL_BEGIN.replace(/[\\/[\]^$.*+?()|{}]/g, '\\$&'), 'g'));
52
+ assert.strictEqual(matches.length, 1, 'sentinel appears exactly once');
53
+ } finally { sb.cleanup(); }
54
+ });
55
+
56
+ test('adopt preserves existing MEMORY.md content and appends', () => {
57
+ const sb = makeSandbox();
58
+ try {
59
+ const indexPath = path.join(sb.dir, 'MEMORY.md');
60
+ fs.writeFileSync(indexPath, '# Memory Index\n\n- [foo.md](foo.md) — existing entry\n');
61
+ adopt({ cwd: sb.cwd, home: sb.home });
62
+ const index = fs.readFileSync(indexPath, 'utf8');
63
+ assert.ok(index.includes('existing entry'), 'preserves prior entries');
64
+ assert.ok(index.includes(SENTINEL_BEGIN), 'appends sentinel');
65
+ } finally { sb.cleanup(); }
66
+ });
67
+
68
+ test('adopt fails gracefully when memory dir missing', () => {
69
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-adopt-home-'));
70
+ const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-adopt-cwd-'));
71
+ try {
72
+ const res = adopt({ cwd, home });
73
+ assert.strictEqual(res.ok, false);
74
+ assert.strictEqual(res.reason, 'no-memory-dir');
75
+ } finally {
76
+ fs.rmSync(home, { recursive: true, force: true });
77
+ fs.rmSync(cwd, { recursive: true, force: true });
78
+ }
79
+ });
80
+
81
+ test('unadopt removes file and sentinel block, preserves other entries', () => {
82
+ const sb = makeSandbox();
83
+ try {
84
+ adopt({ cwd: sb.cwd, home: sb.home });
85
+ // add a neighboring entry
86
+ const indexPath = path.join(sb.dir, 'MEMORY.md');
87
+ const withNeighbor = fs.readFileSync(indexPath, 'utf8') + '- [bar.md](bar.md) — neighbor\n';
88
+ fs.writeFileSync(indexPath, withNeighbor);
89
+
90
+ const res = unadopt({ cwd: sb.cwd, home: sb.home });
91
+ assert.strictEqual(res.fileRemoved, true);
92
+ assert.strictEqual(res.indexPruned, true);
93
+ assert.ok(!fs.existsSync(res.target), 'plugin file gone');
94
+ const final = fs.readFileSync(indexPath, 'utf8');
95
+ assert.ok(!final.includes(SENTINEL_BEGIN), 'sentinel removed');
96
+ assert.ok(final.includes('neighbor'), 'neighbor preserved');
97
+ } finally { sb.cleanup(); }
98
+ });
99
+
100
+ test('unadopt is a no-op when never adopted', () => {
101
+ const sb = makeSandbox();
102
+ try {
103
+ const res = unadopt({ cwd: sb.cwd, home: sb.home });
104
+ assert.strictEqual(res.fileRemoved, false);
105
+ assert.strictEqual(res.indexPruned, false);
106
+ } finally { sb.cleanup(); }
107
+ });
108
+
109
+ test('template file exists and contains decision table', () => {
110
+ assert.ok(fs.existsSync(TEMPLATE_PATH), `template at ${TEMPLATE_PATH}`);
111
+ const content = fs.readFileSync(TEMPLATE_PATH, 'utf8');
112
+ assert.ok(content.includes('get_call_graph'), 'mentions get_call_graph');
113
+ assert.ok(content.includes('impact_analysis'), 'mentions impact_analysis');
114
+ assert.ok(content.includes('CODE_GRAPH_QUIET_HOOKS'), 'mentions env gate');
115
+ });
116
+
117
+ test('stripSentinelBlock removes well-formed block', () => {
118
+ const before = `# Index\n${SENTINEL_BEGIN}\n${INDEX_LINE}\n${SENTINEL_END}\n- [x.md](x.md)\n`;
119
+ const after = stripSentinelBlock(before);
120
+ assert.ok(!after.includes(SENTINEL_BEGIN));
121
+ assert.ok(!after.includes(SENTINEL_END));
122
+ assert.ok(after.includes('- [x.md](x.md)'), 'preserves neighbors');
123
+ });
124
+
125
+ test('stripSentinelBlock self-heals orphan BEGIN without END', () => {
126
+ // Truncation / partial edit scenario
127
+ const before = `# Index\n- [a.md](a.md) — entry\n${SENTINEL_BEGIN}\n${INDEX_LINE}\n\n- [b.md](b.md) — survivor\n`;
128
+ const after = stripSentinelBlock(before);
129
+ assert.ok(!after.includes(SENTINEL_BEGIN), 'orphan BEGIN removed');
130
+ assert.ok(after.includes('survivor'), 'content past blank-line boundary preserved');
131
+ assert.ok(after.includes('entry'), 'content before BEGIN preserved');
132
+ });
133
+
134
+ test('stripSentinelBlock self-heals orphan END line', () => {
135
+ const before = `# Index\n- [a.md](a.md)\n${SENTINEL_END}\n- [b.md](b.md)\n`;
136
+ const after = stripSentinelBlock(before);
137
+ assert.ok(!after.includes(SENTINEL_END));
138
+ assert.ok(after.includes('- [a.md](a.md)') && after.includes('- [b.md](b.md)'));
139
+ });
140
+
141
+ test('adopt heals malformed sentinel (orphan BEGIN) on re-run', () => {
142
+ const sb = makeSandbox();
143
+ try {
144
+ const indexPath = path.join(sb.dir, 'MEMORY.md');
145
+ // Simulate truncated prior adopt — BEGIN line + stale entry, no END
146
+ fs.writeFileSync(
147
+ indexPath,
148
+ `# Memory Index\n- [old.md](old.md) — preserved\n${SENTINEL_BEGIN}\n- [stale](stale.md) — wrong entry\n\n- [neighbor.md](neighbor.md) — survives\n`
149
+ );
150
+ const res = adopt({ cwd: sb.cwd, home: sb.home });
151
+ assert.strictEqual(res.ok, true);
152
+ assert.strictEqual(res.healed, true, 'reports healed');
153
+ const final = fs.readFileSync(indexPath, 'utf8');
154
+ // Exactly one well-formed block now
155
+ const beginCount = (final.match(new RegExp(SENTINEL_BEGIN.replace(/[\\/[\]^$.*+?()|{}]/g, '\\$&'), 'g')) || []).length;
156
+ const endCount = (final.match(new RegExp(SENTINEL_END.replace(/[\\/[\]^$.*+?()|{}]/g, '\\$&'), 'g')) || []).length;
157
+ assert.strictEqual(beginCount, 1, 'one BEGIN');
158
+ assert.strictEqual(endCount, 1, 'one END');
159
+ assert.ok(final.includes('preserved'), 'preserves pre-BEGIN content');
160
+ assert.ok(final.includes('neighbor.md'), 'preserves post-malformed-block content');
161
+ assert.ok(!final.includes('stale.md'), 'old wrong entry purged');
162
+ assert.ok(final.includes(INDEX_LINE), 'fresh canonical line written');
163
+ } finally { sb.cleanup(); }
164
+ });
165
+
166
+ test('adopt is a true no-op when desired block is already present verbatim', () => {
167
+ const sb = makeSandbox();
168
+ try {
169
+ adopt({ cwd: sb.cwd, home: sb.home });
170
+ const indexPath = path.join(sb.dir, 'MEMORY.md');
171
+ const before = fs.readFileSync(indexPath, 'utf8');
172
+ const beforeMtime = fs.statSync(indexPath).mtimeMs;
173
+ const res2 = adopt({ cwd: sb.cwd, home: sb.home });
174
+ assert.strictEqual(res2.indexed, false);
175
+ assert.strictEqual(res2.healed, false);
176
+ assert.strictEqual(fs.readFileSync(indexPath, 'utf8'), before, 'file content identical');
177
+ // mtime may equal beforeMtime since we skipped the write
178
+ assert.strictEqual(fs.statSync(indexPath).mtimeMs, beforeMtime, 'no write occurred');
179
+ } finally { sb.cleanup(); }
180
+ });
181
+
182
+ test('unadopt heals malformed sentinel (orphan BEGIN)', () => {
183
+ const sb = makeSandbox();
184
+ try {
185
+ const indexPath = path.join(sb.dir, 'MEMORY.md');
186
+ fs.writeFileSync(
187
+ indexPath,
188
+ `# Index\n${SENTINEL_BEGIN}\n${INDEX_LINE}\n\n- [keep.md](keep.md) — survives\n`
189
+ );
190
+ const res = unadopt({ cwd: sb.cwd, home: sb.home });
191
+ assert.strictEqual(res.indexPruned, true);
192
+ const final = fs.readFileSync(indexPath, 'utf8');
193
+ assert.ok(!final.includes(SENTINEL_BEGIN), 'orphan BEGIN purged');
194
+ assert.ok(final.includes('keep.md'), 'content past blank-line preserved');
195
+ } finally { sb.cleanup(); }
196
+ });
197
+
198
+ test('Windows platform is rejected with clear reason', { skip: process.platform === 'win32' }, () => {
199
+ const orig = process.platform;
200
+ Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
201
+ try {
202
+ const sb = makeSandbox();
203
+ try {
204
+ const adoptRes = adopt({ cwd: sb.cwd, home: sb.home });
205
+ assert.strictEqual(adoptRes.ok, false);
206
+ assert.strictEqual(adoptRes.reason, 'windows-not-supported');
207
+ const unadoptRes = unadopt({ cwd: sb.cwd, home: sb.home });
208
+ assert.strictEqual(unadoptRes.ok, false);
209
+ assert.strictEqual(unadoptRes.reason, 'windows-not-supported');
210
+ } finally { sb.cleanup(); }
211
+ } finally {
212
+ Object.defineProperty(process, 'platform', { value: orig, configurable: true });
213
+ }
214
+ });
@@ -277,13 +277,17 @@ function runSessionInit() {
277
277
 
278
278
  const autoUpdateLaunched = launchBackgroundAutoUpdate();
279
279
  const indexFreshness = binaryCheck.available ? ensureIndexFresh() : 'skipped';
280
- const mapInjected = binaryCheck.available ? injectProjectMap() : false;
280
+ // CODE_GRAPH_QUIET_HOOKS=1 skip the 60-line project-map injection; rely
281
+ // on MEMORY.md pointer + on-demand `project_map` tool call instead.
282
+ const quietHooks = process.env.CODE_GRAPH_QUIET_HOOKS === '1';
283
+ const mapInjected = binaryCheck.available && !quietHooks ? injectProjectMap() : false;
281
284
  const consistencyIssues = binaryCheck.available
282
285
  ? consistencyCheck(binaryCheck.binary)
283
286
  : [];
284
287
  return {
285
288
  inactive: false, lifecycle, cacheHookHeal,
286
289
  autoUpdateLaunched, indexFreshness, mapInjected, binaryCheck, consistencyIssues,
290
+ quietHooks,
287
291
  };
288
292
  }
289
293
 
@@ -53,6 +53,10 @@ function markCooldown(type) {
53
53
  } catch { /* ok */ }
54
54
  }
55
55
 
56
+ // CODE_GRAPH_QUIET_HOOKS=1 → skip passive per-prompt injection entirely.
57
+ // Users opt in to this mode when MEMORY.md + explicit tool calls cover their needs.
58
+ if (process.env.CODE_GRAPH_QUIET_HOOKS === '1') process.exit(0);
59
+
56
60
  // --- Read user message ---
57
61
  let message;
58
62
  try {
@@ -464,3 +464,18 @@ test('skills: only expected skills exist', () => {
464
464
  const files = fs.readdirSync(skillsDir).filter(f => f.endsWith('.md')).sort();
465
465
  assert.deepEqual(files, ['explore.md', 'index.md']);
466
466
  });
467
+
468
+ test('CODE_GRAPH_QUIET_HOOKS=1 short-circuits silently on stdout, stderr, exit 0', () => {
469
+ const { spawnSync } = require('node:child_process');
470
+ const script = path.join(__dirname, 'user-prompt-context.js');
471
+ const proc = spawnSync(process.execPath, [script], {
472
+ input: JSON.stringify({ message: 'impact analysis for fn_that_would_trigger_search' }),
473
+ env: { ...process.env, CODE_GRAPH_QUIET_HOOKS: '1' },
474
+ encoding: 'utf8',
475
+ timeout: 2000,
476
+ });
477
+ // Quiet mode must be fully silent — any stderr leaks into Claude's display.
478
+ assert.equal(proc.stdout, '', 'stdout must be empty');
479
+ assert.equal(proc.stderr, '', 'stderr must be empty');
480
+ assert.equal(proc.status, 0, 'must exit 0');
481
+ });
@@ -0,0 +1,70 @@
1
+ ---
2
+ name: code-graph-mcp 插件契约
3
+ description: code-graph-mcp 工具调度规则 — 何时用 MCP/CLI 替代 Grep/Read,invited-memory 模式
4
+ type: reference
5
+ ---
6
+ # code-graph-mcp 插件契约
7
+
8
+ > Invited-memory 模式:MCP `instructions` 仅留指针,决策细则集中在此。
9
+ > 启用条件:`CODE_GRAPH_QUIET_HOOKS=1`(在 `~/.claude/settings.json` 的 `env` 中设置)。
10
+
11
+ ## 何时调用 MCP/CLI(替代多步 Grep/Read)
12
+
13
+ | 意图 | 工具 | 关键参数 / 例子 |
14
+ |------|------|----------------|
15
+ | "谁调用 X?" / "X 调了啥?" | `get_call_graph` / `callgraph X` | 替代 `grep "X("` |
16
+ | "改 X 会炸啥?" | `impact_analysis` / `impact X` | 修改函数签名前必跑 |
17
+ | "Y 模块长啥样?" | `module_overview` / `overview Y/` | 替代逐文件 Read |
18
+ | "找做 Z 的代码"(概念) | `semantic_code_search` / `search "Z"` | 不知道精确名 |
19
+ | "返回 T 类型的函数" | `ast_search --returns T` | 结构化筛选 |
20
+ | "X 在哪被引用?" | `find_references` / `refs X` | 含 callers/importers |
21
+ | "未使用的代码" | `find_dead_code` / `dead-code [path]` | 清理 exports |
22
+ | "相似/重复函数" | `find_similar_code` / `similar X` | 需 embedding |
23
+ | "X 文件依赖谁?" | `dependency_graph` / `deps X` | file 级别 |
24
+ | "看 X 的源码 / 签名" | `get_ast_node` / `show X` | `--include-impact` 含影响面 |
25
+ | "项目结构总览" | `project_map` / `map` | 起手势用 `--compact` |
26
+ | HTTP 路由 → handler 链路 | `trace_http_chain` / `trace ROUTE` | API 调试 |
27
+
28
+ ## 不要替代
29
+
30
+ - 精确字符串 / 常量 / 正则 → 仍用 `Grep`
31
+ - 非代码文件(README/JSON/log) → 仍用 `Grep`
32
+ - 即将编辑的具体文件 → 仍用 `Read`
33
+
34
+ ## 工作流惯例
35
+
36
+ 1. 起手 `project_map --compact` 看架构
37
+ 2. `semantic_code_search` 默认带 `compact=true`,省 token
38
+ 3. 展开节点:`get_ast_node node_id=N compact=true` 看签名 / 不带 compact 看全文
39
+ 4. 改前必跑 `impact_analysis`
40
+ 5. 搜不到结果 → `code-graph-mcp health-check` 检查索引与 embedding 覆盖率
41
+
42
+ 可用 prompts:`impact-analysis`、`understand-module`、`trace-request`
43
+
44
+ ## CLI 速查(替 Bash)
45
+
46
+ ```
47
+ code-graph-mcp grep "pattern" [path] # ripgrep + AST 上下文
48
+ code-graph-mcp search "concept" # FTS5 语义搜索
49
+ code-graph-mcp ast-search "q" --type fn # 结构化筛选
50
+ code-graph-mcp map # 项目架构
51
+ code-graph-mcp overview src/mcp/ # 模块总览
52
+ code-graph-mcp callgraph SYMBOL # 调用图
53
+ code-graph-mcp impact SYMBOL # 影响面
54
+ code-graph-mcp show SYMBOL # 节点详情
55
+ code-graph-mcp refs SYMBOL --relation calls # 引用筛选
56
+ code-graph-mcp dead-code [path] # 未使用代码
57
+ code-graph-mcp health-check # 索引健康
58
+ ```
59
+
60
+ 完整列表:`code-graph-mcp --help`。
61
+
62
+ ## 质量门槛
63
+
64
+ - `compact=true` 一般够用;要看完整代码再去掉
65
+ - `impact` 在 `--change-type signature` 时返回最严格的破坏面
66
+ - 索引陈旧 → SessionStart 自带 `ensureIndexFresh`;手动跑 `incremental-index`
67
+
68
+ ## 卸载
69
+
70
+ `code-graph-mcp unadopt` 精确移除 sentinel 段 + 本文件;或取消 `CODE_GRAPH_QUIET_HOOKS` 即恢复原注入。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdsrs/code-graph",
3
- "version": "0.7.18",
3
+ "version": "0.8.1",
4
4
  "description": "MCP server that indexes codebases into an AST knowledge graph with semantic search, call graph traversal, and HTTP route tracing",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -34,10 +34,10 @@
34
34
  "node": ">=16"
35
35
  },
36
36
  "optionalDependencies": {
37
- "@sdsrs/code-graph-linux-x64": "0.7.18",
38
- "@sdsrs/code-graph-linux-arm64": "0.7.18",
39
- "@sdsrs/code-graph-darwin-x64": "0.7.18",
40
- "@sdsrs/code-graph-darwin-arm64": "0.7.18",
41
- "@sdsrs/code-graph-win32-x64": "0.7.18"
37
+ "@sdsrs/code-graph-linux-x64": "0.8.1",
38
+ "@sdsrs/code-graph-linux-arm64": "0.8.1",
39
+ "@sdsrs/code-graph-darwin-x64": "0.8.1",
40
+ "@sdsrs/code-graph-darwin-arm64": "0.8.1",
41
+ "@sdsrs/code-graph-win32-x64": "0.8.1"
42
42
  }
43
43
  }