@sdsrs/code-graph 0.12.1 → 0.14.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.
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "sdsrs"
6
6
  },
7
- "version": "0.12.1",
7
+ "version": "0.14.0",
8
8
  "keywords": [
9
9
  "code-graph",
10
10
  "ast",
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
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.
4
+ // Claude Code auto-memory dir (~/.claude/projects/<slug>/memory/, also
5
+ // read/written by claude-mem-lite) and maintains a sentinel-bracketed index
6
+ // entry in MEMORY.md. Idempotent. Used by invited-memory pattern with
7
+ // CODE_GRAPH_QUIET_HOOKS=1.
6
8
  const fs = require('fs');
7
9
  const path = require('path');
8
10
  const os = require('os');
@@ -19,6 +19,10 @@ const MANIFEST_FILE = path.join(CACHE_DIR, 'install-manifest.json');
19
19
  const SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
20
20
  const INSTALLED_PLUGINS_PATH = path.join(os.homedir(), '.claude', 'plugins', 'installed_plugins.json');
21
21
  const REGISTRY_FILE = path.join(CACHE_DIR, 'statusline-registry.json');
22
+ // Durable mirror outside ~/.cache/ — survives cache cleanup. Captures the
23
+ // `_previous` snapshot (pre-install statusline) and any third-party providers
24
+ // (GSD, etc.). readRegistry() self-heals from this file when primary is missing.
25
+ const PROVIDERS_BACKUP_FILE = path.join(os.homedir(), '.claude', 'statusline-providers.json');
22
26
 
23
27
  // --- Helpers ---
24
28
 
@@ -75,15 +79,28 @@ function isOurComposite(settings) {
75
79
  // Multiple providers can register. The composite script runs them all.
76
80
 
77
81
  function readRegistry() {
78
- return readJson(REGISTRY_FILE) || [];
82
+ const primary = readJson(REGISTRY_FILE);
83
+ if (primary && Array.isArray(primary) && primary.length > 0) return primary;
84
+ // Self-heal: primary missing or empty (e.g. user cleaned ~/.cache/code-graph/).
85
+ // Durable backup in ~/.claude/ retains `_previous` + third-party providers.
86
+ const backup = readJson(PROVIDERS_BACKUP_FILE);
87
+ if (backup && Array.isArray(backup) && backup.length > 0) {
88
+ try { writeJsonAtomic(REGISTRY_FILE, backup); } catch { /* ok */ }
89
+ return backup;
90
+ }
91
+ return [];
79
92
  }
80
93
 
81
94
  function writeRegistry(registry) {
82
95
  if (!registry || registry.length === 0) {
83
96
  try { fs.unlinkSync(REGISTRY_FILE); } catch { /* ok */ }
97
+ try { fs.unlinkSync(PROVIDERS_BACKUP_FILE); } catch { /* ok */ }
84
98
  return;
85
99
  }
86
100
  writeJsonAtomic(REGISTRY_FILE, registry);
101
+ // Mirror to durable location so cache cleanup doesn't strand `_previous`
102
+ // or third-party provider entries.
103
+ try { writeJsonAtomic(PROVIDERS_BACKUP_FILE, registry); } catch { /* ok */ }
87
104
  }
88
105
 
89
106
  function registerStatuslineProvider(id, command, needsStdin) {
@@ -535,7 +552,9 @@ module.exports = {
535
552
  readRegistry, writeRegistry,
536
553
  getPluginVersion, cleanupOldCacheVersions,
537
554
  removeHooksFromSettings, isOurHookEntry,
555
+ registerStatuslineProvider, unregisterStatuslineProvider,
538
556
  PLUGIN_ID, OLD_PLUGIN_IDS, MARKETPLACE_NAME, CACHE_DIR, REGISTRY_FILE,
557
+ PROVIDERS_BACKUP_FILE,
539
558
  };
540
559
 
541
560
  // CLI: node lifecycle.js <install|uninstall|update|health>
@@ -178,6 +178,94 @@ test('removeHooksFromSettings strips our entries but keeps unrelated hooks', ()
178
178
  assert.ok(!s.hooks.PostToolUse, 'empty event key should be deleted');
179
179
  });
180
180
 
181
+ test('writeRegistry mirrors entries to durable backup outside ~/.cache/', (t) => {
182
+ const homeDir = mkHome(t);
183
+ const registryPath = path.join(homeDir, '.cache', 'code-graph', 'statusline-registry.json');
184
+ const backupPath = path.join(homeDir, '.claude', 'statusline-providers.json');
185
+
186
+ execFileSync(process.execPath, ['-e', `
187
+ const { registerStatuslineProvider } = require(${JSON.stringify(lifecyclePath)});
188
+ registerStatuslineProvider('_previous', 'echo prev', true);
189
+ registerStatuslineProvider('code-graph', 'node /cg.js', false);
190
+ `], { env: { ...process.env, HOME: homeDir } });
191
+
192
+ const primary = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
193
+ const backup = JSON.parse(fs.readFileSync(backupPath, 'utf8'));
194
+ assert.deepEqual(primary, backup);
195
+ assert.equal(primary.length, 2);
196
+ });
197
+
198
+ test('readRegistry self-heals primary from durable backup after cache wipe', (t) => {
199
+ const homeDir = mkHome(t);
200
+ const cacheDir = path.join(homeDir, '.cache', 'code-graph');
201
+ const registryPath = path.join(cacheDir, 'statusline-registry.json');
202
+ const backupPath = path.join(homeDir, '.claude', 'statusline-providers.json');
203
+
204
+ // Seed both files, then simulate user wiping ~/.cache/code-graph/
205
+ writeJson(registryPath, [
206
+ { id: '_previous', command: 'echo gsd', needsStdin: true },
207
+ { id: 'code-graph', command: 'node /cg.js', needsStdin: false },
208
+ ]);
209
+ writeJson(backupPath, [
210
+ { id: '_previous', command: 'echo gsd', needsStdin: true },
211
+ { id: 'code-graph', command: 'node /cg.js', needsStdin: false },
212
+ ]);
213
+ fs.rmSync(cacheDir, { recursive: true, force: true });
214
+ assert.equal(fs.existsSync(registryPath), false);
215
+
216
+ const out = execFileSync(process.execPath, ['-e', `
217
+ const { readRegistry } = require(${JSON.stringify(lifecyclePath)});
218
+ process.stdout.write(JSON.stringify(readRegistry()));
219
+ `], { env: { ...process.env, HOME: homeDir } }).toString();
220
+
221
+ const restored = JSON.parse(out);
222
+ assert.equal(restored.length, 2);
223
+ assert.equal(restored[0].id, '_previous');
224
+ // Primary file rebuilt from backup
225
+ assert.equal(fs.existsSync(registryPath), true);
226
+ });
227
+
228
+ test('writeRegistry([]) clears both primary and backup', (t) => {
229
+ const homeDir = mkHome(t);
230
+ const registryPath = path.join(homeDir, '.cache', 'code-graph', 'statusline-registry.json');
231
+ const backupPath = path.join(homeDir, '.claude', 'statusline-providers.json');
232
+
233
+ execFileSync(process.execPath, ['-e', `
234
+ const { registerStatuslineProvider, unregisterStatuslineProvider } = require(${JSON.stringify(lifecyclePath)});
235
+ registerStatuslineProvider('code-graph', 'node /cg.js', false);
236
+ unregisterStatuslineProvider('code-graph');
237
+ `], { env: { ...process.env, HOME: homeDir } });
238
+
239
+ assert.equal(fs.existsSync(registryPath), false);
240
+ assert.equal(fs.existsSync(backupPath), false);
241
+ });
242
+
243
+ test('statusline-chain CLI register/unregister/list + reserved-id guard', (t) => {
244
+ const homeDir = mkHome(t);
245
+ const chainPath = path.join(__dirname, 'statusline-chain.js');
246
+ const env = { ...process.env, HOME: homeDir };
247
+
248
+ const reg = execFileSync(process.execPath, [chainPath, 'register', 'gsd', 'node /gsd.cjs', '--stdin'], { env }).toString();
249
+ assert.match(reg, /registered gsd/);
250
+
251
+ const reRun = execFileSync(process.execPath, [chainPath, 'register', 'gsd', 'node /gsd.cjs', '--stdin'], { env }).toString();
252
+ assert.match(reRun, /unchanged gsd/);
253
+
254
+ const list = execFileSync(process.execPath, [chainPath, 'list'], { env }).toString();
255
+ assert.match(list, /gsd \[stdin\]: node \/gsd\.cjs/);
256
+
257
+ // Reserved ids rejected — both should exit 2 with stderr "reserved"
258
+ const { spawnSync } = require('child_process');
259
+ for (const rid of ['_previous', 'code-graph']) {
260
+ const r = spawnSync(process.execPath, [chainPath, 'register', rid, 'x'], { env });
261
+ assert.equal(r.status, 2, `${rid} should exit 2`);
262
+ assert.match(r.stderr.toString(), /reserved/);
263
+ }
264
+
265
+ const un = execFileSync(process.execPath, [chainPath, 'unregister', 'gsd'], { env }).toString();
266
+ assert.match(un, /unregistered gsd/);
267
+ });
268
+
181
269
  test('install() removes legacy code-graph hooks from settings.json without re-registering', (t) => {
182
270
  const homeDir = mkHome(t);
183
271
  const settingsPath = path.join(homeDir, '.claude', 'settings.json');
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // Public CLI for third-party plugins (GSD, etc.) to register a statusline
4
+ // provider into code-graph's composite chain.
5
+ //
6
+ // Usage:
7
+ // node statusline-chain.js register <id> <command> [--stdin]
8
+ // node statusline-chain.js unregister <id>
9
+ // node statusline-chain.js list
10
+ //
11
+ // Writes to ~/.cache/code-graph/statusline-registry.json (working copy) and
12
+ // mirrors to ~/.claude/statusline-providers.json (durable backup). The
13
+ // composite script reads both.
14
+ //
15
+ // Reserved ids: "_previous" (captures pre-install statusline), "code-graph"
16
+ // (this plugin's own provider). Third parties should use stable ids like
17
+ // "gsd", "claude-mem", etc.
18
+
19
+ const { readRegistry, registerStatuslineProvider, unregisterStatuslineProvider } = require('./lifecycle');
20
+
21
+ function usage(code = 1) {
22
+ process.stderr.write(
23
+ 'Usage:\n' +
24
+ ' node statusline-chain.js register <id> <command> [--stdin]\n' +
25
+ ' node statusline-chain.js unregister <id>\n' +
26
+ ' node statusline-chain.js list\n'
27
+ );
28
+ process.exit(code);
29
+ }
30
+
31
+ function runRegister(id, command, needsStdin) {
32
+ if (id === 'code-graph' || id === '_previous') {
33
+ process.stderr.write(`error: id "${id}" is reserved\n`);
34
+ process.exit(2);
35
+ }
36
+ if (!id || !command) usage();
37
+ const changed = registerStatuslineProvider(id, command, needsStdin);
38
+ process.stdout.write(changed ? `registered ${id}\n` : `unchanged ${id}\n`);
39
+ }
40
+
41
+ function runUnregister(id) {
42
+ if (!id) usage();
43
+ const changed = unregisterStatuslineProvider(id);
44
+ process.stdout.write(changed ? `unregistered ${id}\n` : `not-found ${id}\n`);
45
+ }
46
+
47
+ function runList() {
48
+ const registry = readRegistry();
49
+ if (registry.length === 0) {
50
+ process.stdout.write('(empty)\n');
51
+ return;
52
+ }
53
+ for (const entry of registry) {
54
+ const stdin = entry.needsStdin ? ' [stdin]' : '';
55
+ process.stdout.write(`${entry.id}${stdin}: ${entry.command}\n`);
56
+ }
57
+ }
58
+
59
+ if (require.main === module) {
60
+ const [, , cmd, ...rest] = process.argv;
61
+ if (cmd === 'register') {
62
+ const needsStdin = rest.includes('--stdin');
63
+ const args = rest.filter((a) => a !== '--stdin');
64
+ runRegister(args[0], args[1], needsStdin);
65
+ } else if (cmd === 'unregister') {
66
+ runUnregister(rest[0]);
67
+ } else if (cmd === 'list') {
68
+ runList();
69
+ } else {
70
+ usage();
71
+ }
72
+ }
73
+
74
+ module.exports = { runRegister, runUnregister, runList };
@@ -30,7 +30,7 @@ type: reference
30
30
  |------|------|----------------|
31
31
  | "谁调用 X?" / "X 调了啥?" | `get_call_graph` / `callgraph X` | 替代 `grep "X("` |
32
32
  | "Y 模块长啥样?" | `module_overview` / `overview Y/` | 替代逐文件 Read |
33
- | "找做 Z 的代码"(概念) | `semantic_code_search` / `search "Z"` | 不知道精确名 |
33
+ | "找做 Z 的代码"(概念) | MCP `semantic_code_search`(RRF 混合);CLI `search`(纯 FTS5) | 不知道精确名;要向量召回走 MCP |
34
34
  | "返回 T 类型的函数" | `ast_search --returns T` | 结构化筛选 |
35
35
  | "X 在哪被引用?" | `find_references` / `refs X` | 含 callers/importers |
36
36
  | "看 X 的源码 / 签名" | `get_ast_node` / `show X` | `include_impact=true` 含影响面(替代 impact_analysis) |
@@ -76,7 +76,7 @@ type: reference
76
76
 
77
77
  ```
78
78
  code-graph-mcp grep "pattern" [path] # ripgrep + AST 上下文
79
- code-graph-mcp search "concept" # FTS5 语义搜索
79
+ code-graph-mcp search "concept" # FTS5(要混合检索走 MCP semantic_code_search)
80
80
  code-graph-mcp ast-search "q" --type fn # 结构化筛选
81
81
  code-graph-mcp map # 项目架构
82
82
  code-graph-mcp overview src/mcp/ # 模块总览
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdsrs/code-graph",
3
- "version": "0.12.1",
3
+ "version": "0.14.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.12.1",
38
- "@sdsrs/code-graph-linux-arm64": "0.12.1",
39
- "@sdsrs/code-graph-darwin-x64": "0.12.1",
40
- "@sdsrs/code-graph-darwin-arm64": "0.12.1",
41
- "@sdsrs/code-graph-win32-x64": "0.12.1"
37
+ "@sdsrs/code-graph-linux-x64": "0.14.0",
38
+ "@sdsrs/code-graph-linux-arm64": "0.14.0",
39
+ "@sdsrs/code-graph-darwin-x64": "0.14.0",
40
+ "@sdsrs/code-graph-darwin-arm64": "0.14.0",
41
+ "@sdsrs/code-graph-win32-x64": "0.14.0"
42
42
  }
43
43
  }