@sdsrs/code-graph 0.16.7 → 0.16.9

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.16.7",
7
+ "version": "0.16.9",
8
8
  "keywords": [
9
9
  "code-graph",
10
10
  "ast",
@@ -11,6 +11,18 @@ const os = require('os');
11
11
 
12
12
  const SENTINEL_BEGIN = '<!-- code-graph-mcp:begin v1 -->';
13
13
  const SENTINEL_END = '<!-- code-graph-mcp:end -->';
14
+ // Collision-detection marker. Slug encoding `[^a-zA-Z0-9-]→'-'` is lossy,
15
+ // so two cwds (e.g. /foo/bar and /foo bar) can resolve to the same memory
16
+ // dir. Adopt records its absolute cwd as the file's first-line HTML comment;
17
+ // re-adopt from a different cwd surfaces a warning.
18
+ const ADOPTED_BY_RE = /^<!-- adopted-by: (.+?) -->\r?\n?/;
19
+ function readAdoptedBy(filePath) {
20
+ try {
21
+ const first = fs.readFileSync(filePath, 'utf8').split('\n', 1)[0];
22
+ const m = first.match(/^<!-- adopted-by: (.+?) -->/);
23
+ return m ? m[1] : null;
24
+ } catch { return null; }
25
+ }
14
26
  const INDEX_LINE = [
15
27
  '- [code-graph-mcp](plugin_code_graph_mcp.md) — v0.10.0 起 tools/list 默认 7 核心 + 5 隐藏可调(省启动 token)',
16
28
  ' - 核心 7(默认暴露):`get_call_graph`/`module_overview`/`semantic_code_search`/`ast_search`/`find_references`/`get_ast_node`/`project_map`',
@@ -68,20 +80,49 @@ function platformGuard() {
68
80
  return null;
69
81
  }
70
82
 
83
+ // Project-marker check: cwd looks like a real project (not /tmp / $HOME).
84
+ // Used to gate auto-mkdir of the auto-memory dir so adopt doesn't pollute
85
+ // random directories. Mirrors the markers Claude Code itself recognizes.
86
+ const PROJECT_MARKERS = [
87
+ '.git', '.code-graph', 'package.json', 'Cargo.toml',
88
+ 'pyproject.toml', 'go.mod', 'pom.xml', 'build.gradle',
89
+ ];
90
+ function isProjectRoot(cwd) {
91
+ return PROJECT_MARKERS.some(m => fs.existsSync(path.join(cwd, m)));
92
+ }
93
+
71
94
  function adopt({ cwd, home, templatePath } = {}) {
72
95
  const blocked = platformGuard();
73
96
  if (blocked) return blocked;
74
97
 
98
+ const effectiveCwd = cwd || process.cwd();
75
99
  const dir = memoryDir(cwd, home);
76
100
  if (!fs.existsSync(dir)) {
77
- return { ok: false, reason: 'no-memory-dir', dir };
101
+ // Auto-create only when cwd has a project marker. Without markers the
102
+ // user is likely in /tmp or $HOME, where adopt would litter
103
+ // ~/.claude/projects/ with bogus slugs.
104
+ if (!isProjectRoot(effectiveCwd)) {
105
+ return { ok: false, reason: 'not-a-project', dir, cwd: effectiveCwd };
106
+ }
107
+ fs.mkdirSync(dir, { recursive: true });
78
108
  }
79
109
  const target = path.join(dir, TARGET_NAME);
80
110
  const tpl = templatePath || TEMPLATE_PATH;
81
111
  if (!fs.existsSync(tpl)) {
82
112
  return { ok: false, reason: 'no-template', template: tpl };
83
113
  }
84
- fs.copyFileSync(tpl, target);
114
+ // Slug-collision detection: read prior adopted-by marker before overwrite.
115
+ let collisionWith = null;
116
+ if (fs.existsSync(target)) {
117
+ const prevCwd = readAdoptedBy(target);
118
+ if (prevCwd && prevCwd !== effectiveCwd) collisionWith = prevCwd;
119
+ }
120
+ // Write marker + template. Marker is HTML comment → invisible in rendered
121
+ // markdown but preserved by needsRefresh's bytewise compare (skipped via
122
+ // ADOPTED_BY_RE strip below).
123
+ const tplBody = fs.readFileSync(tpl);
124
+ const marker = Buffer.from(`<!-- adopted-by: ${effectiveCwd} -->\n`);
125
+ fs.writeFileSync(target, Buffer.concat([marker, tplBody]));
85
126
 
86
127
  const indexPath = path.join(dir, 'MEMORY.md');
87
128
  const index = fs.existsSync(indexPath) ? fs.readFileSync(indexPath, 'utf8') : '# Memory Index\n';
@@ -89,14 +130,14 @@ function adopt({ cwd, home, templatePath } = {}) {
89
130
 
90
131
  // Already-adopted-and-well-formed: skip the write entirely.
91
132
  if (index.includes(desiredBlock)) {
92
- return { ok: true, target, indexPath, indexed: false, healed: false };
133
+ return { ok: true, target, indexPath, indexed: false, healed: false, collisionWith };
93
134
  }
94
135
 
95
136
  const cleaned = stripSentinelBlock(index);
96
137
  const healed = cleaned !== index;
97
138
  const base = cleaned.endsWith('\n') ? cleaned : cleaned + '\n';
98
139
  fs.writeFileSync(indexPath, base + desiredBlock + '\n');
99
- return { ok: true, target, indexPath, indexed: true, healed };
140
+ return { ok: true, target, indexPath, indexed: true, healed, collisionWith };
100
141
  }
101
142
 
102
143
  // v0.9.0 — "已 adopt" 判定:template 文件在 + MEMORY.md 内有我们的 sentinel 块。
@@ -124,7 +165,15 @@ function needsRefresh({ cwd, home, templatePath } = {}) {
124
165
  }
125
166
  const shipped = fs.readFileSync(tpl);
126
167
  const current = fs.readFileSync(target);
127
- if (!shipped.equals(current)) return true;
168
+ // Strip the leading "<!-- adopted-by: ... -->\n" collision marker (D fix)
169
+ // before bytewise comparing — its presence/path naturally diverges from
170
+ // the shipped template.
171
+ let body = current;
172
+ const nl = current.indexOf(0x0a);
173
+ if (nl > 0 && ADOPTED_BY_RE.test(current.subarray(0, nl + 1).toString())) {
174
+ body = current.subarray(nl + 1);
175
+ }
176
+ if (!shipped.equals(body)) return true;
128
177
  const index = fs.readFileSync(indexPath, 'utf8');
129
178
  const desiredBlock = `${SENTINEL_BEGIN}\n${INDEX_LINE}\n${SENTINEL_END}`;
130
179
  return !index.includes(desiredBlock);
@@ -198,12 +247,22 @@ function formatResult(action, result) {
198
247
  return `[code-graph] Memory dir not found: ${result.dir}\n` +
199
248
  ' Run \`claude\` at least once in this project to create it.';
200
249
  }
250
+ if (result.reason === 'not-a-project') {
251
+ return `[code-graph] Not a project root: ${result.cwd}\n` +
252
+ ' No project marker (.git, Cargo.toml, package.json, pyproject.toml, ...).\n' +
253
+ ' cd into a real project before running adopt.';
254
+ }
201
255
  if (result.reason === 'no-template') {
202
256
  return `[code-graph] Template missing: ${result.template}`;
203
257
  }
204
258
  return `[code-graph] adopt failed: ${result.reason || 'unknown'}`;
205
259
  }
206
260
  const lines = [`[code-graph] Adopted → ${result.target}`];
261
+ if (result.collisionWith) {
262
+ lines.push(`[code-graph] ⚠ slug collision: this dir was previously adopted by ${result.collisionWith}.`);
263
+ lines.push('[code-graph] Memory dir is shared — sentinels overwritten. ' +
264
+ 'Investigate path encoding clash (Claude Code slug = path with non-[a-zA-Z0-9-] → "-").');
265
+ }
207
266
  if (result.healed) lines.push(`[code-graph] Healed malformed sentinel block → ${result.indexPath}`);
208
267
  else if (result.indexed) lines.push(`[code-graph] Indexed → ${result.indexPath}`);
209
268
  else lines.push(`[code-graph] Index already up-to-date — no write`);
@@ -231,6 +290,7 @@ if (require.main === module) {
231
290
 
232
291
  module.exports = {
233
292
  adopt, unadopt, memoryDir, formatResult, stripSentinelBlock,
234
- isAdopted, isPluginModeInstall, maybeAutoAdopt, needsRefresh,
293
+ isAdopted, isPluginModeInstall, maybeAutoAdopt, needsRefresh, isProjectRoot,
235
294
  SENTINEL_BEGIN, SENTINEL_END, INDEX_LINE, TEMPLATE_PATH, TARGET_NAME,
295
+ PROJECT_MARKERS,
236
296
  };
@@ -6,8 +6,9 @@ const path = require('path');
6
6
  const os = require('os');
7
7
  const {
8
8
  adopt, unadopt, memoryDir, stripSentinelBlock,
9
- isAdopted, isPluginModeInstall, maybeAutoAdopt, needsRefresh,
9
+ isAdopted, isPluginModeInstall, maybeAutoAdopt, needsRefresh, isProjectRoot,
10
10
  SENTINEL_BEGIN, SENTINEL_END, INDEX_LINE, TEMPLATE_PATH, TARGET_NAME,
11
+ PROJECT_MARKERS,
11
12
  } = require('./adopt');
12
13
 
13
14
  function makeSandbox() {
@@ -84,13 +85,17 @@ test('adopt preserves existing MEMORY.md content and appends', () => {
84
85
  } finally { sb.cleanup(); }
85
86
  });
86
87
 
87
- test('adopt fails gracefully when memory dir missing', () => {
88
+ test('adopt fails gracefully when cwd is not a project root', () => {
89
+ // v0.16.9: behavior change — adopt now mkdir's the memory dir when cwd has
90
+ // a project marker (.git / Cargo.toml / package.json / ...). Bare mkdtemp
91
+ // without markers still fails with the more specific 'not-a-project' reason
92
+ // to prevent /tmp pollution.
88
93
  const home = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-adopt-home-'));
89
94
  const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-adopt-cwd-'));
90
95
  try {
91
96
  const res = adopt({ cwd, home });
92
97
  assert.strictEqual(res.ok, false);
93
- assert.strictEqual(res.reason, 'no-memory-dir');
98
+ assert.strictEqual(res.reason, 'not-a-project');
94
99
  } finally {
95
100
  fs.rmSync(home, { recursive: true, force: true });
96
101
  fs.rmSync(cwd, { recursive: true, force: true });
@@ -329,7 +334,10 @@ test('maybeAutoAdopt runs adopt when plugin-mode + unadopted + no opt-out', () =
329
334
  } finally { sb.cleanup(); }
330
335
  });
331
336
 
332
- test('maybeAutoAdopt returns no-memory-dir when project memory missing', () => {
337
+ test('maybeAutoAdopt fails with not-a-project when cwd has no project marker', () => {
338
+ // v0.16.9: bare mkdtemp cwd without .git/Cargo.toml/etc. surfaces
339
+ // 'not-a-project' so plugin-mode auto-adopt doesn't litter ~/.claude/projects/
340
+ // with bogus slugs from non-project working directories.
333
341
  const home = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-adopt-home-'));
334
342
  const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-adopt-cwd-'));
335
343
  try {
@@ -338,10 +346,9 @@ test('maybeAutoAdopt returns no-memory-dir when project memory missing', () => {
338
346
  scriptPath: '/home/u/.claude/plugins/cache/code-graph-mcp/scripts',
339
347
  env: {},
340
348
  });
341
- // Plugin-mode + not adopted + no opt-out → attempt runs, but adopt() fails gracefully
342
349
  assert.strictEqual(res.attempted, true);
343
350
  assert.strictEqual(res.result.ok, false);
344
- assert.strictEqual(res.result.reason, 'no-memory-dir');
351
+ assert.strictEqual(res.result.reason, 'not-a-project');
345
352
  } finally {
346
353
  fs.rmSync(home, { recursive: true, force: true });
347
354
  fs.rmSync(cwd, { recursive: true, force: true });
@@ -400,10 +407,14 @@ test('maybeAutoAdopt refreshes drifted target on re-run (reason=refreshed)', ()
400
407
  assert.strictEqual(res.attempted, true);
401
408
  assert.strictEqual(res.reason, 'refreshed');
402
409
  assert.strictEqual(res.result.ok, true);
403
- // Target now matches shipped template
410
+ // Target now matches shipped template (after stripping the leading
411
+ // "<!-- adopted-by: ... -->\n" collision marker added by adopt v0.16.9).
404
412
  const shipped = fs.readFileSync(TEMPLATE_PATH);
405
413
  const current = fs.readFileSync(target);
406
- assert.ok(shipped.equals(current), 'target re-synced to shipped template');
414
+ const nl = current.indexOf(0x0a);
415
+ const body = nl > 0 && /^<!-- adopted-by: /.test(current.subarray(0, nl).toString())
416
+ ? current.subarray(nl + 1) : current;
417
+ assert.ok(shipped.equals(body), 'target re-synced to shipped template');
407
418
  // Sentinel preserved in MEMORY.md
408
419
  assert.strictEqual(isAdopted({ cwd: sb.cwd, home: sb.home }), true);
409
420
  } finally { sb.cleanup(); }
@@ -482,3 +493,95 @@ test('Windows platform is rejected with clear reason', { skip: process.platform
482
493
  Object.defineProperty(process, 'platform', { value: orig, configurable: true });
483
494
  }
484
495
  });
496
+
497
+ // ─── C fix: project-marker mkdir ─────────────────────────────────────────
498
+
499
+ test('isProjectRoot detects each marker', () => {
500
+ for (const marker of PROJECT_MARKERS) {
501
+ const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-marker-'));
502
+ try {
503
+ assert.strictEqual(isProjectRoot(cwd), false, `bare cwd should not be a project`);
504
+ const markerPath = path.join(cwd, marker);
505
+ // Some markers are directories (.git, .code-graph), others are files.
506
+ if (marker.startsWith('.')) fs.mkdirSync(markerPath);
507
+ else fs.writeFileSync(markerPath, '');
508
+ assert.strictEqual(isProjectRoot(cwd), true, `${marker} should make cwd a project`);
509
+ } finally {
510
+ fs.rmSync(cwd, { recursive: true, force: true });
511
+ }
512
+ }
513
+ });
514
+
515
+ test('adopt auto-creates memory dir when cwd has a project marker', () => {
516
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-adopt-home-'));
517
+ const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-adopt-cwd-'));
518
+ try {
519
+ // Add a project marker so adopt is allowed to create the memory dir.
520
+ fs.writeFileSync(path.join(cwd, 'package.json'), '{}');
521
+ // The memory dir does NOT exist yet — pre-fix behavior errored 'no-memory-dir'.
522
+ const dir = memoryDir(cwd, home);
523
+ assert.strictEqual(fs.existsSync(dir), false);
524
+
525
+ const res = adopt({ cwd, home });
526
+ assert.strictEqual(res.ok, true, `expected ok, got ${JSON.stringify(res)}`);
527
+ assert.strictEqual(fs.existsSync(dir), true, 'memory dir auto-created');
528
+ assert.strictEqual(fs.existsSync(path.join(dir, TARGET_NAME)), true, 'plugin file written');
529
+ } finally {
530
+ fs.rmSync(home, { recursive: true, force: true });
531
+ fs.rmSync(cwd, { recursive: true, force: true });
532
+ }
533
+ });
534
+
535
+ // ─── D fix: slug collision marker ────────────────────────────────────────
536
+
537
+ test('adopt writes adopted-by marker as first line of plugin file', () => {
538
+ const sb = makeSandbox();
539
+ try {
540
+ adopt({ cwd: sb.cwd, home: sb.home });
541
+ const target = path.join(sb.dir, TARGET_NAME);
542
+ const firstLine = fs.readFileSync(target, 'utf8').split('\n', 1)[0];
543
+ assert.match(firstLine, /^<!-- adopted-by: .* -->$/, `expected adopted-by marker, got: ${firstLine}`);
544
+ assert.ok(firstLine.includes(sb.cwd), `marker should embed absolute cwd: ${firstLine}`);
545
+ } finally { sb.cleanup(); }
546
+ });
547
+
548
+ test('adopt detects slug collision when same memory dir is re-adopted from a different cwd', () => {
549
+ // Simulate two real cwds whose paths slugify to the same string. Here we
550
+ // skip real path encoding and just write a file pretending it came from a
551
+ // different cwd, then re-adopt — collision detection reads the prior marker.
552
+ const sb = makeSandbox();
553
+ try {
554
+ adopt({ cwd: sb.cwd, home: sb.home });
555
+ const target = path.join(sb.dir, TARGET_NAME);
556
+ // Tamper: rewrite the marker to look like a different cwd adopted first.
557
+ const body = fs.readFileSync(target, 'utf8').split('\n').slice(1).join('\n');
558
+ fs.writeFileSync(target, '<!-- adopted-by: /imaginary/other-project -->\n' + body);
559
+
560
+ const res = adopt({ cwd: sb.cwd, home: sb.home });
561
+ assert.strictEqual(res.ok, true);
562
+ assert.strictEqual(res.collisionWith, '/imaginary/other-project',
563
+ `expected collisionWith to surface prior cwd, got ${res.collisionWith}`);
564
+ } finally { sb.cleanup(); }
565
+ });
566
+
567
+ test('adopt collisionWith is null when re-adopting from same cwd (idempotent)', () => {
568
+ const sb = makeSandbox();
569
+ try {
570
+ adopt({ cwd: sb.cwd, home: sb.home });
571
+ const res = adopt({ cwd: sb.cwd, home: sb.home });
572
+ assert.strictEqual(res.ok, true);
573
+ assert.strictEqual(res.collisionWith, null);
574
+ } finally { sb.cleanup(); }
575
+ });
576
+
577
+ test('needsRefresh ignores the adopted-by marker when bytewise comparing', () => {
578
+ // Critical: the marker we add to target makes target ≠ template byte-for-byte.
579
+ // needsRefresh must skip the leading marker line before compare; otherwise
580
+ // every SessionStart would re-write the file and burn IO on a no-op.
581
+ const sb = makeSandbox();
582
+ try {
583
+ adopt({ cwd: sb.cwd, home: sb.home });
584
+ assert.strictEqual(needsRefresh({ cwd: sb.cwd, home: sb.home }), false,
585
+ 'needsRefresh should be false right after adopt — marker must not trigger drift');
586
+ } finally { sb.cleanup(); }
587
+ });
@@ -4,6 +4,7 @@ const { execFileSync } = require('child_process');
4
4
  const path = require('path');
5
5
  const fs = require('fs');
6
6
  const os = require('os');
7
+ const { readBinaryVersion } = require('./version-utils');
7
8
 
8
9
  const PLATFORM = os.platform();
9
10
  const ARCH = os.arch();
@@ -11,6 +12,24 @@ const CACHE_FILE = path.join(os.homedir(), '.cache', 'code-graph', 'binary-path'
11
12
  const BINARY_NAME = PLATFORM === 'win32' ? 'code-graph-mcp.exe' : 'code-graph-mcp';
12
13
  const PLATFORM_PKG = `@sdsrs/code-graph-${PLATFORM}-${ARCH}`;
13
14
 
15
+ /** Read the npm pkg version from this script's package.json (claude-plugin/../package.json). */
16
+ function getPackageVersion() {
17
+ try { return require('../../package.json').version; }
18
+ catch { return null; }
19
+ }
20
+
21
+ /** Compare semver-ish "M.m.p" strings; returns -1, 0, or 1. Non-numeric parts → 0. */
22
+ function compareVersions(a, b) {
23
+ const pa = String(a).split('.').map(s => parseInt(s, 10));
24
+ const pb = String(b).split('.').map(s => parseInt(s, 10));
25
+ for (let i = 0; i < 3; i++) {
26
+ const x = Number.isFinite(pa[i]) ? pa[i] : 0;
27
+ const y = Number.isFinite(pb[i]) ? pb[i] : 0;
28
+ if (x !== y) return x < y ? -1 : 1;
29
+ }
30
+ return 0;
31
+ }
32
+
14
33
  /**
15
34
  * Candidate paths for npm global `node_modules`.
16
35
  *
@@ -157,8 +176,19 @@ function findBinaryUncached() {
157
176
  }
158
177
 
159
178
  // --- Auto-update cache (binary downloaded directly from GitHub release) ---
179
+ // Cache wins when its version >= the npm pkg version. After `npm update`
180
+ // refreshes the platform-pkg, an older auto-update cache binary must NOT
181
+ // shadow the freshly-installed one; this version check prevents the
182
+ // upgrade-race where users keep running stale binary until auto-update fires.
160
183
  const autoUpdateBin = path.join(os.homedir(), '.cache', 'code-graph', 'bin', BINARY_NAME);
161
- if (isNativeBinary(autoUpdateBin)) return autoUpdateBin;
184
+ if (isNativeBinary(autoUpdateBin)) {
185
+ const cacheVer = readBinaryVersion(autoUpdateBin);
186
+ const pkgVer = getPackageVersion();
187
+ if (!pkgVer || !cacheVer || compareVersions(cacheVer, pkgVer) >= 0) {
188
+ return autoUpdateBin;
189
+ }
190
+ // Cache is older than npm pkg — fall through to platform-pkg.
191
+ }
162
192
 
163
193
  // --- Platform-specific npm package (@sdsrs/code-graph-{os}-{arch}) ---
164
194
  const platformBin = findPlatformBinary();
@@ -212,6 +242,7 @@ function clearCache() {
212
242
  module.exports = {
213
243
  findBinary, findBinaryUncached, clearCache,
214
244
  globalNodeModulesCandidates, findPlatformBinary,
245
+ getPackageVersion, compareVersions,
215
246
  CACHE_FILE, BINARY_NAME, PLATFORM_PKG,
216
247
  };
217
248
 
@@ -5,7 +5,8 @@ const fs = require('fs');
5
5
  const os = require('os');
6
6
  const path = require('path');
7
7
 
8
- const { globalNodeModulesCandidates, findPlatformBinary, BINARY_NAME } = require('./find-binary');
8
+ const { globalNodeModulesCandidates, findPlatformBinary, BINARY_NAME,
9
+ compareVersions, getPackageVersion } = require('./find-binary');
9
10
 
10
11
  function mkDir(t, prefix) {
11
12
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
@@ -108,3 +109,39 @@ test('findPlatformBinary returns null when no platform pkg installed anywhere re
108
109
  }
109
110
  assert.equal(real, null);
110
111
  });
112
+
113
+ // ─── compareVersions (B fix: cache version invalidation helper) ───────────
114
+
115
+ test('compareVersions: equal', () => {
116
+ assert.equal(compareVersions('1.2.3', '1.2.3'), 0);
117
+ });
118
+
119
+ test('compareVersions: cache older than pkg', () => {
120
+ // After `npm update` to 0.16.8, an auto-update cache from 0.16.7 must NOT
121
+ // shadow the freshly-installed platform-pkg binary. Returns -1 here so
122
+ // findBinaryUncached falls through to platform-pkg.
123
+ assert.equal(compareVersions('0.16.7', '0.16.8'), -1);
124
+ });
125
+
126
+ test('compareVersions: cache newer than pkg', () => {
127
+ // Auto-update may legitimately be ahead of npm pkg (cache fetched 0.17.0
128
+ // before npm shipped it). Returns 1 → cache wins.
129
+ assert.equal(compareVersions('0.17.0', '0.16.8'), 1);
130
+ });
131
+
132
+ test('compareVersions: minor and patch boundaries', () => {
133
+ assert.equal(compareVersions('1.0.0', '0.999.999'), 1);
134
+ assert.equal(compareVersions('1.10.0', '1.9.99'), 1); // numeric, not lexical
135
+ assert.equal(compareVersions('1.0.10', '1.0.9'), 1);
136
+ });
137
+
138
+ test('compareVersions: tolerates non-numeric / short input', () => {
139
+ // Non-numeric → treated as 0; shorter strings padded with 0.
140
+ assert.equal(compareVersions('1.2', '1.2.0'), 0);
141
+ assert.equal(compareVersions('foo', '0.0.0'), 0);
142
+ });
143
+
144
+ test('getPackageVersion reads root package.json', () => {
145
+ const v = getPackageVersion();
146
+ assert.match(v, /^\d+\.\d+\.\d+$/, `expected semver-ish, got: ${v}`);
147
+ });
@@ -63,7 +63,7 @@ type: reference
63
63
 
64
64
  ## 工作流惯例
65
65
 
66
- 1. 起手 `project_map --compact` 看架构
66
+ 1. 起手 `project_map`(或 Bash 调 `code-graph-mcp map --compact`)看架构
67
67
  2. `semantic_code_search` 默认带 `compact=true`,省 token
68
68
  3. 展开节点:`get_ast_node node_id=N compact=true` 看签名 / 不带 compact 看全文
69
69
  4. 改前评估影响:`get_ast_node symbol_name=X include_impact=true`(核心 7 内,首选)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdsrs/code-graph",
3
- "version": "0.16.7",
3
+ "version": "0.16.9",
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": {
@@ -28,16 +28,17 @@
28
28
  ],
29
29
  "scripts": {
30
30
  "build": "cargo build --release --no-default-features && node scripts/copy-binary.js",
31
- "prepare": "git rev-parse --git-dir > /dev/null 2>&1 && ln -sf ../../scripts/pre-commit.sh .git/hooks/pre-commit || true"
31
+ "prepare": "git rev-parse --git-dir > /dev/null 2>&1 && ln -sf ../../scripts/pre-commit.sh .git/hooks/pre-commit || true",
32
+ "preuninstall": "node claude-plugin/scripts/lifecycle.js uninstall || true"
32
33
  },
33
34
  "engines": {
34
35
  "node": ">=16"
35
36
  },
36
37
  "optionalDependencies": {
37
- "@sdsrs/code-graph-linux-x64": "0.16.7",
38
- "@sdsrs/code-graph-linux-arm64": "0.16.7",
39
- "@sdsrs/code-graph-darwin-x64": "0.16.7",
40
- "@sdsrs/code-graph-darwin-arm64": "0.16.7",
41
- "@sdsrs/code-graph-win32-x64": "0.16.7"
38
+ "@sdsrs/code-graph-linux-x64": "0.16.9",
39
+ "@sdsrs/code-graph-linux-arm64": "0.16.9",
40
+ "@sdsrs/code-graph-darwin-x64": "0.16.9",
41
+ "@sdsrs/code-graph-darwin-arm64": "0.16.9",
42
+ "@sdsrs/code-graph-win32-x64": "0.16.9"
42
43
  }
43
44
  }