@sdsrs/code-graph 0.8.3 → 0.9.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/bin/cli.js CHANGED
@@ -7,6 +7,17 @@ const path = require("path");
7
7
  // and detect dev mode from bin/ → repo root (one level up)
8
8
  process.env._FIND_BINARY_ROOT = path.resolve(__dirname, "..");
9
9
 
10
+ // Intercept adopt / unadopt before forwarding — they're node-only concerns
11
+ // (write to ~/.claude/projects/<slug>/memory/) and have no Rust counterpart.
12
+ // Lets `code-graph-mcp adopt` / `unadopt` work uniformly across plugin / npm / npx.
13
+ const sub = process.argv[2];
14
+ if (sub === "adopt" || sub === "unadopt") {
15
+ const { adopt, unadopt, formatResult } = require("../claude-plugin/scripts/adopt");
16
+ const result = sub === "unadopt" ? unadopt() : adopt();
17
+ process.stdout.write(formatResult(sub, result) + "\n");
18
+ process.exit(result.ok === false ? 1 : 0);
19
+ }
20
+
10
21
  const { findBinary } = require("../claude-plugin/scripts/find-binary");
11
22
 
12
23
  const binary = findBinary();
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "sdsrs"
6
6
  },
7
- "version": "0.8.3",
7
+ "version": "0.9.0",
8
8
  "keywords": [
9
9
  "code-graph",
10
10
  "ast",
@@ -9,7 +9,11 @@ const os = require('os');
9
9
 
10
10
  const SENTINEL_BEGIN = '<!-- code-graph-mcp:begin v1 -->';
11
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';
12
+ const INDEX_LINE = [
13
+ '- [code-graph-mcp](plugin_code_graph_mcp.md) — 代码理解 12 工具(替代 Grep/Read 多步):',
14
+ ' - 问"谁调/改动/模块/概念/HTTP/相似" → `get_call_graph`/`impact_analysis`/`module_overview`/`semantic_code_search`/`trace_http_chain`/`find_similar_code`',
15
+ ' - 问"返回型/引用/死码/依赖/架构/看签名" → `ast_search`/`find_references`/`find_dead_code`/`dependency_graph`/`project_map`/`get_ast_node`',
16
+ ].join('\n');
13
17
  const TEMPLATE_PATH = path.resolve(__dirname, '..', 'templates', 'plugin_code_graph_mcp.md');
14
18
  const TARGET_NAME = 'plugin_code_graph_mcp.md';
15
19
 
@@ -87,6 +91,43 @@ function adopt({ cwd, home, templatePath } = {}) {
87
91
  return { ok: true, target, indexPath, indexed: true, healed };
88
92
  }
89
93
 
94
+ // v0.9.0 — "已 adopt" 判定:template 文件在 + MEMORY.md 内有我们的 sentinel 块。
95
+ // 用在 maybeAutoAdopt 里做幂等门,也用在 session-init 里推导 quietHooks。
96
+ function isAdopted({ cwd, home } = {}) {
97
+ const dir = memoryDir(cwd, home);
98
+ const target = path.join(dir, TARGET_NAME);
99
+ const indexPath = path.join(dir, 'MEMORY.md');
100
+ if (!fs.existsSync(target) || !fs.existsSync(indexPath)) return false;
101
+ const index = fs.readFileSync(indexPath, 'utf8');
102
+ return index.includes(SENTINEL_BEGIN) && index.includes(SENTINEL_END);
103
+ }
104
+
105
+ // 检测脚本是否从 Claude Code 插件 cache 运行。
106
+ // 走 __dirname 而非 CLAUDE_PLUGIN_ROOT — 后者在多插件共存时会互相污染
107
+ // (见 feedback_plugin_env_isolation.md)。
108
+ function isPluginModeInstall(scriptPath = __dirname) {
109
+ const sep = path.sep;
110
+ return scriptPath.includes(`${sep}.claude${sep}plugins${sep}`);
111
+ }
112
+
113
+ // C' 上下文感知默认(v0.9.0):插件模式下首次 SessionStart 静默 adopt。
114
+ // /plugin install 本身已构成知情同意;npm / npx / 裸 checkout 保持 opt-in。
115
+ // 退出:CODE_GRAPH_NO_AUTO_ADOPT=1。
116
+ function maybeAutoAdopt({ cwd, home, env, scriptPath } = {}) {
117
+ env = env || process.env;
118
+ if (env.CODE_GRAPH_NO_AUTO_ADOPT === '1') {
119
+ return { attempted: false, reason: 'opted-out' };
120
+ }
121
+ if (!isPluginModeInstall(scriptPath || __dirname)) {
122
+ return { attempted: false, reason: 'not-plugin-mode' };
123
+ }
124
+ if (isAdopted({ cwd, home })) {
125
+ return { attempted: false, reason: 'already-adopted' };
126
+ }
127
+ const result = adopt({ cwd, home });
128
+ return { attempted: true, reason: 'adopted', result };
129
+ }
130
+
90
131
  function unadopt({ cwd, home } = {}) {
91
132
  const blocked = platformGuard();
92
133
  if (blocked) return blocked;
@@ -132,7 +173,9 @@ function formatResult(action, result) {
132
173
  if (result.healed) lines.push(`[code-graph] Healed malformed sentinel block → ${result.indexPath}`);
133
174
  else if (result.indexed) lines.push(`[code-graph] Indexed → ${result.indexPath}`);
134
175
  else lines.push(`[code-graph] Index already up-to-date — no write`);
135
- lines.push('[code-graph] Activate: set CODE_GRAPH_QUIET_HOOKS=1 in ~/.claude/settings.json env');
176
+ // v0.9.0: adoption auto-implies quietHooks; no env var needed for the common case.
177
+ lines.push('[code-graph] Active — quietHooks auto-enabled via adopted state.');
178
+ lines.push('[code-graph] Force inject: CODE_GRAPH_QUIET_HOOKS=0 Force silent: =1');
136
179
  return lines.join('\n');
137
180
  }
138
181
  if (action === 'unadopt') {
@@ -154,5 +197,6 @@ if (require.main === module) {
154
197
 
155
198
  module.exports = {
156
199
  adopt, unadopt, memoryDir, formatResult, stripSentinelBlock,
200
+ isAdopted, isPluginModeInstall, maybeAutoAdopt,
157
201
  SENTINEL_BEGIN, SENTINEL_END, INDEX_LINE, TEMPLATE_PATH, TARGET_NAME,
158
202
  };
@@ -6,6 +6,7 @@ const path = require('path');
6
6
  const os = require('os');
7
7
  const {
8
8
  adopt, unadopt, memoryDir, stripSentinelBlock,
9
+ isAdopted, isPluginModeInstall, maybeAutoAdopt,
9
10
  SENTINEL_BEGIN, SENTINEL_END, INDEX_LINE, TEMPLATE_PATH,
10
11
  } = require('./adopt');
11
12
 
@@ -213,6 +214,140 @@ test('unadopt heals malformed sentinel (orphan BEGIN)', () => {
213
214
  } finally { sb.cleanup(); }
214
215
  });
215
216
 
217
+ // ──────────────────────────────────────────────────────────────────────────
218
+ // v0.9.0 — C' context-aware auto-adopt
219
+ // ──────────────────────────────────────────────────────────────────────────
220
+
221
+ test('isAdopted returns false on fresh project (no files)', () => {
222
+ const sb = makeSandbox();
223
+ try {
224
+ assert.strictEqual(isAdopted({ cwd: sb.cwd, home: sb.home }), false);
225
+ } finally { sb.cleanup(); }
226
+ });
227
+
228
+ test('isAdopted returns true after adopt()', () => {
229
+ const sb = makeSandbox();
230
+ try {
231
+ adopt({ cwd: sb.cwd, home: sb.home });
232
+ assert.strictEqual(isAdopted({ cwd: sb.cwd, home: sb.home }), true);
233
+ } finally { sb.cleanup(); }
234
+ });
235
+
236
+ test('isAdopted returns false after unadopt()', () => {
237
+ const sb = makeSandbox();
238
+ try {
239
+ adopt({ cwd: sb.cwd, home: sb.home });
240
+ unadopt({ cwd: sb.cwd, home: sb.home });
241
+ assert.strictEqual(isAdopted({ cwd: sb.cwd, home: sb.home }), false);
242
+ } finally { sb.cleanup(); }
243
+ });
244
+
245
+ test('isAdopted returns false when target file exists but index has no sentinel', () => {
246
+ const sb = makeSandbox();
247
+ try {
248
+ const indexPath = path.join(sb.dir, 'MEMORY.md');
249
+ fs.writeFileSync(indexPath, '# Memory Index\n- [foo.md](foo.md) — unrelated\n');
250
+ fs.writeFileSync(path.join(sb.dir, 'plugin_code_graph_mcp.md'), 'stale copy');
251
+ assert.strictEqual(isAdopted({ cwd: sb.cwd, home: sb.home }), false);
252
+ } finally { sb.cleanup(); }
253
+ });
254
+
255
+ test('isPluginModeInstall recognizes ~/.claude/plugins/... paths', () => {
256
+ const pluginPath = '/home/user/.claude/plugins/cache/code-graph-mcp@0.9.0/scripts';
257
+ assert.strictEqual(isPluginModeInstall(pluginPath), true);
258
+ });
259
+
260
+ test('isPluginModeInstall rejects npm global install paths', () => {
261
+ const npmPath = '/usr/local/lib/node_modules/@sdsrs/code-graph/claude-plugin/scripts';
262
+ assert.strictEqual(isPluginModeInstall(npmPath), false);
263
+ });
264
+
265
+ test('isPluginModeInstall rejects dev-checkout paths', () => {
266
+ const devPath = '/mnt/data_ssd/dev/projects/code-graph-mcp/claude-plugin/scripts';
267
+ assert.strictEqual(isPluginModeInstall(devPath), false);
268
+ });
269
+
270
+ test('isPluginModeInstall rejects npx cache paths', () => {
271
+ const npxPath = '/tmp/npx-abc123/node_modules/@sdsrs/code-graph/claude-plugin/scripts';
272
+ assert.strictEqual(isPluginModeInstall(npxPath), false);
273
+ });
274
+
275
+ test('maybeAutoAdopt skips when CODE_GRAPH_NO_AUTO_ADOPT=1', () => {
276
+ const sb = makeSandbox();
277
+ try {
278
+ const res = maybeAutoAdopt({
279
+ cwd: sb.cwd, home: sb.home,
280
+ scriptPath: '/home/u/.claude/plugins/cache/code-graph-mcp/scripts',
281
+ env: { CODE_GRAPH_NO_AUTO_ADOPT: '1' },
282
+ });
283
+ assert.strictEqual(res.attempted, false);
284
+ assert.strictEqual(res.reason, 'opted-out');
285
+ assert.strictEqual(isAdopted({ cwd: sb.cwd, home: sb.home }), false);
286
+ } finally { sb.cleanup(); }
287
+ });
288
+
289
+ test('maybeAutoAdopt skips when not plugin-mode (npm install)', () => {
290
+ const sb = makeSandbox();
291
+ try {
292
+ const res = maybeAutoAdopt({
293
+ cwd: sb.cwd, home: sb.home,
294
+ scriptPath: '/usr/local/lib/node_modules/@sdsrs/code-graph/claude-plugin/scripts',
295
+ env: {},
296
+ });
297
+ assert.strictEqual(res.attempted, false);
298
+ assert.strictEqual(res.reason, 'not-plugin-mode');
299
+ assert.strictEqual(isAdopted({ cwd: sb.cwd, home: sb.home }), false);
300
+ } finally { sb.cleanup(); }
301
+ });
302
+
303
+ test('maybeAutoAdopt skips when already adopted (idempotent)', () => {
304
+ const sb = makeSandbox();
305
+ try {
306
+ adopt({ cwd: sb.cwd, home: sb.home });
307
+ const res = maybeAutoAdopt({
308
+ cwd: sb.cwd, home: sb.home,
309
+ scriptPath: '/home/u/.claude/plugins/cache/code-graph-mcp/scripts',
310
+ env: {},
311
+ });
312
+ assert.strictEqual(res.attempted, false);
313
+ assert.strictEqual(res.reason, 'already-adopted');
314
+ } finally { sb.cleanup(); }
315
+ });
316
+
317
+ test('maybeAutoAdopt runs adopt when plugin-mode + unadopted + no opt-out', () => {
318
+ const sb = makeSandbox();
319
+ try {
320
+ const res = maybeAutoAdopt({
321
+ cwd: sb.cwd, home: sb.home,
322
+ scriptPath: '/home/u/.claude/plugins/cache/code-graph-mcp/scripts',
323
+ env: {},
324
+ });
325
+ assert.strictEqual(res.attempted, true);
326
+ assert.strictEqual(res.result.ok, true);
327
+ assert.strictEqual(res.result.indexed, true);
328
+ assert.strictEqual(isAdopted({ cwd: sb.cwd, home: sb.home }), true);
329
+ } finally { sb.cleanup(); }
330
+ });
331
+
332
+ test('maybeAutoAdopt returns no-memory-dir when project memory missing', () => {
333
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-adopt-home-'));
334
+ const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-adopt-cwd-'));
335
+ try {
336
+ const res = maybeAutoAdopt({
337
+ cwd, home,
338
+ scriptPath: '/home/u/.claude/plugins/cache/code-graph-mcp/scripts',
339
+ env: {},
340
+ });
341
+ // Plugin-mode + not adopted + no opt-out → attempt runs, but adopt() fails gracefully
342
+ assert.strictEqual(res.attempted, true);
343
+ assert.strictEqual(res.result.ok, false);
344
+ assert.strictEqual(res.result.reason, 'no-memory-dir');
345
+ } finally {
346
+ fs.rmSync(home, { recursive: true, force: true });
347
+ fs.rmSync(cwd, { recursive: true, force: true });
348
+ }
349
+ });
350
+
216
351
  test('Windows platform is rejected with clear reason', { skip: process.platform === 'win32' }, () => {
217
352
  const orig = process.platform;
218
353
  Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
@@ -310,11 +310,18 @@ async function checkForUpdate() {
310
310
  if (isDevMode()) return null;
311
311
 
312
312
  const state = readState();
313
+ // manifest.version is authoritative — /plugin update writes it directly and
314
+ // bypasses auto-update.js, so re-sync state.installedVersion every call.
315
+ const installedVersion = readManifest().version || '0.0.0';
313
316
 
314
317
  // Time-based throttle
315
318
  if (!shouldCheck(state)) {
316
- if (state.updateAvailable && state.latestVersion) {
317
- return { updateAvailable: true, from: state.installedVersion, to: state.latestVersion };
319
+ if (state.installedVersion !== installedVersion) {
320
+ saveState({ ...state, installedVersion });
321
+ }
322
+ if (state.updateAvailable && state.latestVersion
323
+ && compareVersions(state.latestVersion, installedVersion) > 0) {
324
+ return { updateAvailable: true, from: installedVersion, to: state.latestVersion };
318
325
  }
319
326
  return null;
320
327
  }
@@ -322,21 +329,19 @@ async function checkForUpdate() {
322
329
  // Check GitHub for latest release
323
330
  const latest = await fetchLatestRelease();
324
331
  if (!latest) {
325
- saveState({ ...state, lastCheck: new Date().toISOString() });
332
+ saveState({ ...state, installedVersion, lastCheck: new Date().toISOString() });
326
333
  return null;
327
334
  }
328
335
 
329
336
  // Compare versions
330
- const manifest = readManifest();
331
- const currentVersion = manifest.version || '0.0.0';
332
- const hasUpdate = compareVersions(latest.version, currentVersion) > 0;
337
+ const hasUpdate = compareVersions(latest.version, installedVersion) > 0;
333
338
 
334
339
  if (hasUpdate) {
335
340
  const result = await downloadAndInstall(latest);
336
341
  const success = result.pluginUpdated;
337
342
  const newState = {
338
343
  lastCheck: new Date().toISOString(),
339
- installedVersion: success ? latest.version : currentVersion,
344
+ installedVersion: success ? latest.version : installedVersion,
340
345
  latestVersion: latest.version,
341
346
  updateAvailable: !success,
342
347
  lastUpdate: success ? new Date().toISOString() : state.lastUpdate,
@@ -349,7 +354,7 @@ async function checkForUpdate() {
349
354
  updateAvailable: !success,
350
355
  updated: success,
351
356
  binaryUpdated: result.binaryUpdated,
352
- from: currentVersion,
357
+ from: installedVersion,
353
358
  to: latest.version,
354
359
  };
355
360
  }
@@ -357,6 +362,7 @@ async function checkForUpdate() {
357
362
  // No update needed
358
363
  saveState({
359
364
  ...state,
365
+ installedVersion,
360
366
  lastCheck: new Date().toISOString(),
361
367
  latestVersion: latest.version,
362
368
  updateAvailable: false,
@@ -14,12 +14,14 @@ const {
14
14
  promoteVerifiedBinary,
15
15
  } = require('./auto-update');
16
16
 
17
- function mkDir(prefix) {
18
- return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
17
+ function mkDir(t, prefix) {
18
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
19
+ t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
20
+ return dir;
19
21
  }
20
22
 
21
- test('getExtractedPluginVersion reads extracted plugin manifest version', () => {
22
- const root = mkDir('code-graph-plugin-');
23
+ test('getExtractedPluginVersion reads extracted plugin manifest version', (t) => {
24
+ const root = mkDir(t, 'code-graph-plugin-');
23
25
  const manifest = path.join(root, '.claude-plugin', 'plugin.json');
24
26
  fs.mkdirSync(path.dirname(manifest), { recursive: true });
25
27
  fs.writeFileSync(manifest, JSON.stringify({ version: '1.2.3' }, null, 2));
@@ -41,8 +43,8 @@ function writeFakeBinary(filePath, version) {
41
43
  fs.chmodSync(filePath, 0o755);
42
44
  }
43
45
 
44
- test('promoteVerifiedBinary accepts a runnable binary with the expected version', () => {
45
- const dir = mkDir('code-graph-bin-');
46
+ test('promoteVerifiedBinary accepts a runnable binary with the expected version', (t) => {
47
+ const dir = mkDir(t, 'code-graph-bin-');
46
48
  const tmp = path.join(dir, 'code-graph-mcp.tmp');
47
49
  const dst = path.join(dir, 'code-graph-mcp');
48
50
  writeFakeBinary(tmp, '1.2.3');
@@ -53,8 +55,8 @@ test('promoteVerifiedBinary accepts a runnable binary with the expected version'
53
55
  assert.equal(fs.existsSync(dst), true);
54
56
  });
55
57
 
56
- test('promoteVerifiedBinary rejects binaries with mismatched version', () => {
57
- const dir = mkDir('code-graph-bin-');
58
+ test('promoteVerifiedBinary rejects binaries with mismatched version', (t) => {
59
+ const dir = mkDir(t, 'code-graph-bin-');
58
60
  const tmp = path.join(dir, 'code-graph-mcp.tmp');
59
61
  const dst = path.join(dir, 'code-graph-mcp');
60
62
  writeFakeBinary(tmp, '1.2.2');
@@ -12,8 +12,10 @@ const lifecycleCli = path.join(__dirname, 'lifecycle.js');
12
12
  const compositeCli = path.join(__dirname, 'statusline-composite.js');
13
13
  const currentVersion = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')).version;
14
14
 
15
- function mkHome() {
16
- return fs.mkdtempSync(path.join(os.tmpdir(), 'code-graph-e2e-'));
15
+ function mkHome(t) {
16
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'code-graph-e2e-'));
17
+ t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
18
+ return dir;
17
19
  }
18
20
 
19
21
  function writeJson(filePath, value) {
@@ -38,8 +40,8 @@ function runScript(homeDir, scriptPath, args = [], options = {}) {
38
40
  }).toString();
39
41
  }
40
42
 
41
- test('lifecycle CLI handles install, disable self-heal, re-enable, and uninstall', () => {
42
- const homeDir = mkHome();
43
+ test('lifecycle CLI handles install, disable self-heal, re-enable, and uninstall', (t) => {
44
+ const homeDir = mkHome(t);
43
45
  const settingsPath = path.join(homeDir, '.claude', 'settings.json');
44
46
  const installedPath = path.join(homeDir, '.claude', 'plugins', 'installed_plugins.json');
45
47
  const registryPath = path.join(homeDir, '.cache', 'code-graph', 'statusline-registry.json');
@@ -9,8 +9,10 @@ const { execFileSync } = require('child_process');
9
9
  const lifecyclePath = path.join(__dirname, 'lifecycle.js');
10
10
  const statuslinePath = path.join(__dirname, 'statusline.js');
11
11
 
12
- function mkHome() {
13
- return fs.mkdtempSync(path.join(os.tmpdir(), 'code-graph-home-'));
12
+ function mkHome(t) {
13
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'code-graph-home-'));
14
+ t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
15
+ return dir;
14
16
  }
15
17
 
16
18
  function writeJson(filePath, value) {
@@ -48,8 +50,8 @@ function seedOrphanedComposite(homeDir) {
48
50
  return { settingsPath, registryPath };
49
51
  }
50
52
 
51
- test('cleanupDisabledStatusline restores previous statusline and removes registry', () => {
52
- const homeDir = mkHome();
53
+ test('cleanupDisabledStatusline restores previous statusline and removes registry', (t) => {
54
+ const homeDir = mkHome(t);
53
55
  const { settingsPath, registryPath } = seedDisabledComposite(homeDir);
54
56
 
55
57
  const out = execFileSync(process.execPath, ['-e', `
@@ -63,10 +65,11 @@ test('cleanupDisabledStatusline restores previous statusline and removes registr
63
65
  assert.equal(fs.existsSync(registryPath), false);
64
66
  });
65
67
 
66
- test('statusline exits cleanly and self-heals when plugin is disabled', () => {
67
- const homeDir = mkHome();
68
+ test('statusline exits cleanly and self-heals when plugin is disabled', (t) => {
69
+ const homeDir = mkHome(t);
68
70
  const { settingsPath, registryPath } = seedDisabledComposite(homeDir);
69
71
  const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'code-graph-project-'));
72
+ t.after(() => fs.rmSync(projectDir, { recursive: true, force: true }));
70
73
  fs.mkdirSync(path.join(projectDir, '.code-graph'), { recursive: true });
71
74
  fs.writeFileSync(path.join(projectDir, '.code-graph', 'index.db'), '');
72
75
 
@@ -81,8 +84,8 @@ test('statusline exits cleanly and self-heals when plugin is disabled', () => {
81
84
  assert.equal(fs.existsSync(registryPath), false);
82
85
  });
83
86
 
84
- test('cleanupDisabledStatusline also heals orphaned statusline after uninstall', () => {
85
- const homeDir = mkHome();
87
+ test('cleanupDisabledStatusline also heals orphaned statusline after uninstall', (t) => {
88
+ const homeDir = mkHome(t);
86
89
  const { settingsPath, registryPath } = seedOrphanedComposite(homeDir);
87
90
 
88
91
  const out = execFileSync(process.execPath, ['-e', `
@@ -175,8 +178,8 @@ test('removeHooksFromSettings strips our entries but keeps unrelated hooks', ()
175
178
  assert.ok(!s.hooks.PostToolUse, 'empty event key should be deleted');
176
179
  });
177
180
 
178
- test('install() removes legacy code-graph hooks from settings.json without re-registering', () => {
179
- const homeDir = mkHome();
181
+ test('install() removes legacy code-graph hooks from settings.json without re-registering', (t) => {
182
+ const homeDir = mkHome(t);
180
183
  const settingsPath = path.join(homeDir, '.claude', 'settings.json');
181
184
  writeJson(settingsPath, {
182
185
  statusLine: { type: 'command', command: 'echo previous-status' },
@@ -9,6 +9,16 @@ const {
9
9
  cleanupDisabledStatusline, isPluginInactive, readJson, CACHE_DIR,
10
10
  } = require('./lifecycle');
11
11
  const { readBinaryVersion, isDevMode, getNewestMtime } = require('./version-utils');
12
+ const { maybeAutoAdopt, isAdopted } = require('./adopt');
13
+
14
+ // v0.9.0 — quietHooks 推导:显式 env override > adopted 状态。
15
+ // CODE_GRAPH_QUIET_HOOKS='0' 强制 noisy;'1' 强制 quiet;未设 → 跟随 adopted。
16
+ function computeQuietHooks({ adopted, env = {} } = {}) {
17
+ const envQuiet = env.CODE_GRAPH_QUIET_HOOKS;
18
+ if (envQuiet === '0') return false;
19
+ if (envQuiet === '1') return true;
20
+ return !!adopted;
21
+ }
12
22
 
13
23
  function launchBackgroundAutoUpdate(spawnFn = spawn, env = process.env) {
14
24
  try {
@@ -247,9 +257,23 @@ function runSessionInit() {
247
257
 
248
258
  const autoUpdateLaunched = launchBackgroundAutoUpdate();
249
259
  const indexFreshness = binaryCheck.available ? ensureIndexFresh() : 'skipped';
250
- // CODE_GRAPH_QUIET_HOOKS=1 → skip the 60-line project-map injection; rely
251
- // on MEMORY.md pointer + on-demand `project_map` tool call instead.
252
- const quietHooks = process.env.CODE_GRAPH_QUIET_HOOKS === '1';
260
+
261
+ // v0.9.0 C' 上下文感知默认:插件模式下首次 SessionStart 自动 adopt。
262
+ // 仅一次 stderr 提示(采纳成功时),让用户知道发生了什么 + 如何回退。
263
+ const autoAdopt = maybeAutoAdopt({ scriptPath: __dirname });
264
+ if (autoAdopt.attempted && autoAdopt.result && autoAdopt.result.ok) {
265
+ process.stderr.write(
266
+ '[code-graph] Auto-adopted into project MEMORY.md (plugin install → knowing consent).\n' +
267
+ ' Opt out: CODE_GRAPH_NO_AUTO_ADOPT=1 in ~/.claude/settings.json env\n' +
268
+ ' Reverse: code-graph-mcp unadopt\n'
269
+ );
270
+ }
271
+
272
+ // quietHooks: adopted → quiet by default (rely on MEMORY.md pointer + on-demand
273
+ // project_map tool); env '1'/'0' overrides for explicit control.
274
+ const adopted = isAdopted();
275
+ const quietHooks = computeQuietHooks({ adopted, env: process.env });
276
+
253
277
  const mapInjected = binaryCheck.available && !quietHooks ? injectProjectMap() : false;
254
278
  const consistencyIssues = binaryCheck.available
255
279
  ? consistencyCheck(binaryCheck.binary)
@@ -257,7 +281,7 @@ function runSessionInit() {
257
281
  return {
258
282
  inactive: false, lifecycle,
259
283
  autoUpdateLaunched, indexFreshness, mapInjected, binaryCheck, consistencyIssues,
260
- quietHooks,
284
+ quietHooks, adopted, autoAdopted: autoAdopt.attempted,
261
285
  };
262
286
  }
263
287
 
@@ -298,6 +322,7 @@ module.exports = {
298
322
  verifyBinary,
299
323
  consistencyCheck,
300
324
  runSessionInit,
325
+ computeQuietHooks,
301
326
  };
302
327
 
303
328
  if (require.main === module) {
@@ -4,7 +4,7 @@ const assert = require('node:assert/strict');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
 
7
- const { launchBackgroundAutoUpdate, syncLifecycleConfig, ensureIndexFresh, verifyBinary } = require('./session-init');
7
+ const { launchBackgroundAutoUpdate, syncLifecycleConfig, ensureIndexFresh, verifyBinary, computeQuietHooks } = require('./session-init');
8
8
 
9
9
  test('syncLifecycleConfig is exported as a callable helper', () => {
10
10
  assert.equal(typeof syncLifecycleConfig, 'function');
@@ -83,9 +83,34 @@ test('consistencyCheck returns empty array when binary version matches plugin',
83
83
  assert.ok(Array.isArray(result));
84
84
  });
85
85
 
86
- test('consistencyCheck returns version-mismatch when versions differ', () => {
86
+ // ──────────────────────────────────────────────────────────────────────────
87
+ // v0.9.0 — quietHooks inference from adopted state
88
+ // ──────────────────────────────────────────────────────────────────────────
89
+
90
+ test('computeQuietHooks: env "0" forces noisy regardless of adoption', () => {
91
+ assert.equal(computeQuietHooks({ adopted: true, env: { CODE_GRAPH_QUIET_HOOKS: '0' } }), false);
92
+ assert.equal(computeQuietHooks({ adopted: false, env: { CODE_GRAPH_QUIET_HOOKS: '0' } }), false);
93
+ });
94
+
95
+ test('computeQuietHooks: env "1" forces quiet regardless of adoption', () => {
96
+ assert.equal(computeQuietHooks({ adopted: true, env: { CODE_GRAPH_QUIET_HOOKS: '1' } }), true);
97
+ assert.equal(computeQuietHooks({ adopted: false, env: { CODE_GRAPH_QUIET_HOOKS: '1' } }), true);
98
+ });
99
+
100
+ test('computeQuietHooks: env unset → follows adopted state', () => {
101
+ assert.equal(computeQuietHooks({ adopted: true, env: {} }), true);
102
+ assert.equal(computeQuietHooks({ adopted: false, env: {} }), false);
103
+ });
104
+
105
+ test('computeQuietHooks: env unset, no env arg → follows adopted state', () => {
106
+ assert.equal(computeQuietHooks({ adopted: true }), true);
107
+ assert.equal(computeQuietHooks({ adopted: false }), false);
108
+ });
109
+
110
+ test('consistencyCheck returns version-mismatch when versions differ', (t) => {
87
111
  const os = require('os');
88
112
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-'));
113
+ t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
89
114
  const bin = path.join(dir, 'code-graph-mcp');
90
115
  fs.writeFileSync(bin, [
91
116
  '#!/usr/bin/env bash',
@@ -5,15 +5,17 @@ const fs = require('fs');
5
5
  const os = require('os');
6
6
  const path = require('path');
7
7
 
8
- function mkDir(prefix) {
9
- return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
8
+ function mkDir(t, prefix) {
9
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
10
+ t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
11
+ return dir;
10
12
  }
11
13
 
12
14
  // ── readBinaryVersion ──
13
15
 
14
- test('readBinaryVersion returns version from valid binary', () => {
16
+ test('readBinaryVersion returns version from valid binary', (t) => {
15
17
  const { readBinaryVersion } = require('./version-utils');
16
- const dir = mkDir('vu-');
18
+ const dir = mkDir(t, 'vu-');
17
19
  const bin = path.join(dir, 'code-graph-mcp');
18
20
  fs.writeFileSync(bin, [
19
21
  '#!/usr/bin/env bash',
@@ -32,9 +34,9 @@ test('readBinaryVersion returns null for non-existent binary', () => {
32
34
  assert.equal(readBinaryVersion('/tmp/does-not-exist-binary'), null);
33
35
  });
34
36
 
35
- test('readBinaryVersion returns null for binary with unexpected output', () => {
37
+ test('readBinaryVersion returns null for binary with unexpected output', (t) => {
36
38
  const { readBinaryVersion } = require('./version-utils');
37
- const dir = mkDir('vu-');
39
+ const dir = mkDir(t, 'vu-');
38
40
  const bin = path.join(dir, 'code-graph-mcp');
39
41
  fs.writeFileSync(bin, '#!/usr/bin/env bash\necho "something else"');
40
42
  fs.chmodSync(bin, 0o755);
@@ -56,9 +58,9 @@ test('getNewestMtime returns 0 for non-existent directory', () => {
56
58
  assert.equal(getNewestMtime('/tmp/no-such-dir-xyz'), 0);
57
59
  });
58
60
 
59
- test('getNewestMtime finds newest .rs file mtime', () => {
61
+ test('getNewestMtime finds newest .rs file mtime', (t) => {
60
62
  const { getNewestMtime } = require('./version-utils');
61
- const dir = mkDir('vu-mtime-');
63
+ const dir = mkDir(t, 'vu-mtime-');
62
64
  const sub = path.join(dir, 'sub');
63
65
  fs.mkdirSync(sub);
64
66
 
@@ -75,9 +77,9 @@ test('getNewestMtime finds newest .rs file mtime', () => {
75
77
  assert.equal(result, newerMtime, 'should return exactly the newest file mtime');
76
78
  });
77
79
 
78
- test('getNewestMtime ignores non-matching extensions', () => {
80
+ test('getNewestMtime ignores non-matching extensions', (t) => {
79
81
  const { getNewestMtime } = require('./version-utils');
80
- const dir = mkDir('vu-ext-');
82
+ const dir = mkDir(t, 'vu-ext-');
81
83
  fs.writeFileSync(path.join(dir, 'file.js'), 'hello');
82
84
  assert.equal(getNewestMtime(dir, '.rs'), 0);
83
85
  });
@@ -6,7 +6,11 @@ type: reference
6
6
  # code-graph-mcp 插件契约
7
7
 
8
8
  > Invited-memory 模式:MCP `instructions` 仅留指针,决策细则集中在此。
9
- > 启用条件:`CODE_GRAPH_QUIET_HOOKS=1`(在 `~/.claude/settings.json` 的 `env` 中设置)。
9
+ >
10
+ > **v0.9.0 起**:插件(`/plugin install`)模式下首次 SessionStart 自动 adopt,
11
+ > 本文件自动写入,自动切换 quietHooks(跳过每次 project_map 注入)。
12
+ > 退出:`CODE_GRAPH_NO_AUTO_ADOPT=1` 阻止,`code-graph-mcp unadopt` 回退。
13
+ > 手动强控:`CODE_GRAPH_QUIET_HOOKS=0` 强制注入 / `=1` 强制静默(覆盖 adoption 推导)。
10
14
 
11
15
  ## 何时调用 MCP/CLI(替代多步 Grep/Read)
12
16
 
@@ -27,9 +31,9 @@ type: reference
27
31
 
28
32
  ## 不要替代
29
33
 
30
- - 精确字符串 / 常量 / 正则 仍用 `Grep`
31
- - 非代码文件(README/JSON/log) 仍用 `Grep`
32
- - 即将编辑的具体文件 → 仍用 `Read`
34
+ - 非代码文件(README/JSON/log)用内置 `Grep`
35
+ - 代码里查常量/函数名/字符串首选 `code-graph-mcp grep "pattern" [path]`(每个命中带 containing function/module 上下文,结构化);只做纯文本匹配且不关心上下文时用内置 `Grep`
36
+ - 即将编辑的具体文件 → `Read`(`overview <file>` 看概览,`show SYMBOL` 看某符号)
33
37
 
34
38
  ## 工作流惯例
35
39
 
@@ -65,6 +69,8 @@ code-graph-mcp health-check # 索引健康
65
69
  - `impact` 在 `--change-type signature` 时返回最严格的破坏面
66
70
  - 索引陈旧 → SessionStart 自带 `ensureIndexFresh`;手动跑 `incremental-index`
67
71
 
68
- ## 卸载
72
+ ## 卸载 / 回退
69
73
 
70
- `code-graph-mcp unadopt` 精确移除 sentinel 段 + 本文件;或取消 `CODE_GRAPH_QUIET_HOOKS` 即恢复原注入。
74
+ - `code-graph-mcp unadopt` 精确移除 sentinel 段 + 本文件,quietHooks 自动回到 false(下次 SessionStart 恢复 project_map 注入)。
75
+ - `CODE_GRAPH_NO_AUTO_ADOPT=1`(`~/.claude/settings.json` env) — 阻止未来自动 adopt,不影响已 adopted 状态。
76
+ - `CODE_GRAPH_QUIET_HOOKS=0` — 强制恢复 project_map 注入(即使已 adopted)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdsrs/code-graph",
3
- "version": "0.8.3",
3
+ "version": "0.9.0",
4
4
  "description": "MCP server that indexes codebases into an AST knowledge graph with semantic search, call graph traversal, and HTTP route tracing",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -34,10 +34,10 @@
34
34
  "node": ">=16"
35
35
  },
36
36
  "optionalDependencies": {
37
- "@sdsrs/code-graph-linux-x64": "0.8.3",
38
- "@sdsrs/code-graph-linux-arm64": "0.8.3",
39
- "@sdsrs/code-graph-darwin-x64": "0.8.3",
40
- "@sdsrs/code-graph-darwin-arm64": "0.8.3",
41
- "@sdsrs/code-graph-win32-x64": "0.8.3"
37
+ "@sdsrs/code-graph-linux-x64": "0.9.0",
38
+ "@sdsrs/code-graph-linux-arm64": "0.9.0",
39
+ "@sdsrs/code-graph-darwin-x64": "0.9.0",
40
+ "@sdsrs/code-graph-darwin-arm64": "0.9.0",
41
+ "@sdsrs/code-graph-win32-x64": "0.9.0"
42
42
  }
43
43
  }