@sdsrs/code-graph 0.54.2 → 0.56.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 +8 -2
- package/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/claude-plugin/scripts/doctor.js +36 -12
- package/claude-plugin/scripts/doctor.test.js +29 -0
- package/claude-plugin/scripts/find-binary.js +40 -0
- package/claude-plugin/scripts/find-binary.test.js +26 -1
- package/claude-plugin/scripts/mcp-launcher.js +13 -1
- package/claude-plugin/templates/plugin_code_graph_mcp.md +6 -0
- package/package.json +6 -6
package/bin/cli.js
CHANGED
|
@@ -18,17 +18,19 @@ if (sub === "adopt" || sub === "unadopt") {
|
|
|
18
18
|
process.exit(result.ok === false ? 1 : 0);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
const { findBinary } = require("../claude-plugin/scripts/find-binary");
|
|
21
|
+
const { findBinary, unsupportedPlatformHint } = require("../claude-plugin/scripts/find-binary");
|
|
22
22
|
|
|
23
23
|
const binary = findBinary();
|
|
24
24
|
|
|
25
25
|
if (!binary) {
|
|
26
|
+
const hint = unsupportedPlatformHint();
|
|
26
27
|
console.error(
|
|
27
28
|
"Error: code-graph-mcp binary not found.\n\n" +
|
|
29
|
+
(hint ? hint + "\n\n" : "") +
|
|
28
30
|
"To install:\n" +
|
|
29
31
|
" npm install -g @sdsrs/code-graph\n\n" +
|
|
30
32
|
"To build from source:\n" +
|
|
31
|
-
" cargo
|
|
33
|
+
" cargo install code-graph-mcp --features embed-model\n"
|
|
32
34
|
);
|
|
33
35
|
process.exit(1);
|
|
34
36
|
}
|
|
@@ -41,6 +43,10 @@ const child = spawn(binary, process.argv.slice(2), {
|
|
|
41
43
|
|
|
42
44
|
child.on("error", (err) => {
|
|
43
45
|
console.error(`Failed to start code-graph-mcp: ${err.message}`);
|
|
46
|
+
// A glibc binary installed on musl (older npm ignores the `libc` field) is present
|
|
47
|
+
// but fails to exec — surface the actionable platform hint instead of a bare error.
|
|
48
|
+
const hint = unsupportedPlatformHint();
|
|
49
|
+
if (hint) console.error("\n" + hint);
|
|
44
50
|
process.exit(1);
|
|
45
51
|
});
|
|
46
52
|
|
|
@@ -13,6 +13,38 @@ const { findBinary, clearCache: clearBinaryCache } = require('./find-binary');
|
|
|
13
13
|
|
|
14
14
|
// ── Diagnostics ───────────────────────────────────────────
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Classify embedding/vector availability from a `health-check --json` payload.
|
|
18
|
+
* Pure (no I/O) so it is unit-testable. Surfaces a silent FTS5-only degradation
|
|
19
|
+
* that the prior embedding_progress-only check false-greened as "no embeddable
|
|
20
|
+
* nodes": when the binary is embed-capable and the index HAS embeddable nodes but
|
|
21
|
+
* none are embedded, the vector channel is inactive and semantic search runs
|
|
22
|
+
* FTS5-only — that is a 'warn', not 'ok'.
|
|
23
|
+
* @returns {{name:string, status:'ok'|'warn', detail:string}}
|
|
24
|
+
*/
|
|
25
|
+
function classifyEmbeddings(hc) {
|
|
26
|
+
const ep = (hc && hc.embedding_progress) || '0/0';
|
|
27
|
+
const [done, total] = ep.split('/').map(Number);
|
|
28
|
+
if (hc && hc.model_available === false) {
|
|
29
|
+
return { name: 'Embeddings', status: 'warn',
|
|
30
|
+
detail: 'binary built without embed-model — semantic search is FTS5-only; reinstall via npm/plugin for the hybrid binary' };
|
|
31
|
+
}
|
|
32
|
+
if (!total) {
|
|
33
|
+
return { name: 'Embeddings', status: 'ok', detail: 'no embeddable nodes' };
|
|
34
|
+
}
|
|
35
|
+
if (!done) {
|
|
36
|
+
const why = (hc && hc.embedding_status === 'pending')
|
|
37
|
+
? 'model not downloaded/loaded yet — auto-downloads in background on first search; retry shortly or restart the MCP server'
|
|
38
|
+
: `embedding_status=${(hc && hc.embedding_status) || 'unknown'}`;
|
|
39
|
+
return { name: 'Embeddings', status: 'warn',
|
|
40
|
+
detail: `vector INACTIVE — ${total} embeddable nodes, 0 embedded; semantic search is FTS5-only (${why})` };
|
|
41
|
+
}
|
|
42
|
+
if (done < total) {
|
|
43
|
+
return { name: 'Embeddings', status: 'ok', detail: `hybrid — ${Math.round((done / total) * 100)}% embedded (${done}/${total})` };
|
|
44
|
+
}
|
|
45
|
+
return { name: 'Embeddings', status: 'ok', detail: `hybrid — embeddings complete (${done}/${total})` };
|
|
46
|
+
}
|
|
47
|
+
|
|
16
48
|
/**
|
|
17
49
|
* Run all diagnostic checks. Returns an array of:
|
|
18
50
|
* { name: string, status: 'ok'|'warn'|'error'|'skip', detail: string, fixId?: string }
|
|
@@ -119,17 +151,9 @@ function runDiagnostics() {
|
|
|
119
151
|
});
|
|
120
152
|
}
|
|
121
153
|
|
|
122
|
-
// Embeddings
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (total > 0 && done < total) {
|
|
126
|
-
const pct = Math.round((done / total) * 100);
|
|
127
|
-
results.push({ name: 'Embeddings', status: 'ok', detail: `${pct}% (${done}/${total})` });
|
|
128
|
-
} else if (total === 0) {
|
|
129
|
-
results.push({ name: 'Embeddings', status: 'ok', detail: 'no embeddable nodes' });
|
|
130
|
-
} else {
|
|
131
|
-
results.push({ name: 'Embeddings', status: 'ok', detail: `100% (${done}/${total})` });
|
|
132
|
-
}
|
|
154
|
+
// Embeddings / vector availability — pure classifier; warns on FTS5-only
|
|
155
|
+
// degradation (model missing/not loaded) instead of false-greening it.
|
|
156
|
+
results.push(classifyEmbeddings(hc));
|
|
133
157
|
}
|
|
134
158
|
} catch (e) {
|
|
135
159
|
const rawStderr = e.stderr ? e.stderr.toString() : '';
|
|
@@ -453,7 +477,7 @@ function runDoctor(opts = {}) {
|
|
|
453
477
|
return { results, issueCount: issues.length };
|
|
454
478
|
}
|
|
455
479
|
|
|
456
|
-
module.exports = { runDiagnostics, formatReport, runRepairs, runDoctor, surveyHookCoverage, relicRepairGuard };
|
|
480
|
+
module.exports = { runDiagnostics, formatReport, runRepairs, runDoctor, surveyHookCoverage, relicRepairGuard, classifyEmbeddings };
|
|
457
481
|
|
|
458
482
|
if (require.main === module) {
|
|
459
483
|
const args = process.argv.slice(2);
|
|
@@ -93,3 +93,32 @@ test('relicRepairGuard blocks settings repair from a relic copy and redirects',
|
|
|
93
93
|
// Active (or dev/npm) context → repair proceeds.
|
|
94
94
|
assert.equal(relicRepairGuard({ relic: false, log: () => {} }), false);
|
|
95
95
|
});
|
|
96
|
+
|
|
97
|
+
// ── classifyEmbeddings (vector-availability — warns on silent FTS5-only) ──
|
|
98
|
+
|
|
99
|
+
test('classifyEmbeddings WARNS when embed-capable but nothing embedded (vector inactive)', () => {
|
|
100
|
+
const { classifyEmbeddings } = require('./doctor');
|
|
101
|
+
// The exact silent-FTS5 gap: model_available compile-flag true, real embeddable
|
|
102
|
+
// nodes exist, but 0 embedded (model never downloaded/loaded).
|
|
103
|
+
const r = classifyEmbeddings({ model_available: true, embedding_progress: '0/2745',
|
|
104
|
+
embedding_status: 'pending', search_mode: 'fts_only' });
|
|
105
|
+
assert.equal(r.status, 'warn', 'must not false-green a vector-inactive index');
|
|
106
|
+
assert.match(r.detail, /FTS5-only|vector INACTIVE/);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('classifyEmbeddings WARNS when binary lacks embed-model feature', () => {
|
|
110
|
+
const { classifyEmbeddings } = require('./doctor');
|
|
111
|
+
const r = classifyEmbeddings({ model_available: false, embedding_progress: '0/0' });
|
|
112
|
+
assert.equal(r.status, 'warn');
|
|
113
|
+
assert.match(r.detail, /without embed-model/);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('classifyEmbeddings OK for hybrid (partial + complete) and no-embeddable', () => {
|
|
117
|
+
const { classifyEmbeddings } = require('./doctor');
|
|
118
|
+
assert.equal(classifyEmbeddings({ model_available: true, embedding_progress: '900/2745' }).status, 'ok');
|
|
119
|
+
assert.equal(classifyEmbeddings({ model_available: true, embedding_progress: '2745/2745' }).status, 'ok');
|
|
120
|
+
// total === 0 is a non-code index, genuinely nothing to embed → ok, not a false warn.
|
|
121
|
+
const none = classifyEmbeddings({ model_available: true, embedding_progress: '0/0' });
|
|
122
|
+
assert.equal(none.status, 'ok');
|
|
123
|
+
assert.match(none.detail, /no embeddable nodes/);
|
|
124
|
+
});
|
|
@@ -12,6 +12,45 @@ const CACHE_FILE = path.join(os.homedir(), '.cache', 'code-graph', 'binary-path'
|
|
|
12
12
|
const BINARY_NAME = PLATFORM === 'win32' ? 'code-graph-mcp.exe' : 'code-graph-mcp';
|
|
13
13
|
const PLATFORM_PKG = `@sdsrs/code-graph-${PLATFORM}-${ARCH}`;
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* libc flavor of the current Linux runtime: 'glibc' or 'musl'. musl (Alpine) has
|
|
17
|
+
* no `glibcVersionRuntime` in the Node process-report header. Best-effort; returns
|
|
18
|
+
* 'glibc' off Linux or when detection is unavailable.
|
|
19
|
+
*/
|
|
20
|
+
function detectLibc() {
|
|
21
|
+
if (PLATFORM !== 'linux') return 'glibc';
|
|
22
|
+
try {
|
|
23
|
+
const header = process.report.getReport().header;
|
|
24
|
+
if (header && header.glibcVersionRuntime) return 'glibc';
|
|
25
|
+
return 'musl';
|
|
26
|
+
} catch {
|
|
27
|
+
try { if (fs.existsSync('/etc/alpine-release')) return 'musl'; } catch { /* ignore */ }
|
|
28
|
+
return 'glibc';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Actionable install hint for a platform that has NO published prebuilt binary
|
|
34
|
+
* (Alpine/musl, or native Windows-on-ARM), or null when the platform is supported
|
|
35
|
+
* (generic messaging applies). Pure in (platform, arch, libc) so it is unit-testable
|
|
36
|
+
* without running on each OS. Prevents the misleading "npm install
|
|
37
|
+
* @sdsrs/code-graph-<plat>-<arch>" suggestion for a package that does not exist.
|
|
38
|
+
*/
|
|
39
|
+
function unsupportedPlatformHint(platform = PLATFORM, arch = ARCH, libc = null) {
|
|
40
|
+
const lc = libc || (platform === 'linux' ? detectLibc() : 'glibc');
|
|
41
|
+
if (platform === 'linux' && lc === 'musl') {
|
|
42
|
+
return 'Detected Alpine/musl libc, which has no prebuilt binary. Install from source:\n'
|
|
43
|
+
+ ' cargo install code-graph-mcp --features embed-model\n'
|
|
44
|
+
+ 'or use a glibc-based image (e.g. node:20-slim / debian, not node:20-alpine).';
|
|
45
|
+
}
|
|
46
|
+
if (platform === 'win32' && arch === 'arm64') {
|
|
47
|
+
return 'Detected Windows on ARM (arm64), which has no native build. Either use an x64 '
|
|
48
|
+
+ 'build of Node.js (the published x64 binary runs under Windows ARM emulation), '
|
|
49
|
+
+ 'or install from source:\n cargo install code-graph-mcp --features embed-model';
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
15
54
|
/** Read the npm pkg version from this script's package.json (claude-plugin/../package.json). */
|
|
16
55
|
function getPackageVersion() {
|
|
17
56
|
try { return require('../../package.json').version; }
|
|
@@ -263,6 +302,7 @@ module.exports = {
|
|
|
263
302
|
findBinary, findBinaryUncached, clearCache,
|
|
264
303
|
globalNodeModulesCandidates, findPlatformBinary,
|
|
265
304
|
getPackageVersion, compareVersions, isCachedBinaryFresh,
|
|
305
|
+
detectLibc, unsupportedPlatformHint,
|
|
266
306
|
CACHE_FILE, BINARY_NAME, PLATFORM_PKG,
|
|
267
307
|
};
|
|
268
308
|
|
|
@@ -6,7 +6,8 @@ const os = require('os');
|
|
|
6
6
|
const path = require('path');
|
|
7
7
|
|
|
8
8
|
const { globalNodeModulesCandidates, findPlatformBinary, BINARY_NAME,
|
|
9
|
-
compareVersions, getPackageVersion, isCachedBinaryFresh
|
|
9
|
+
compareVersions, getPackageVersion, isCachedBinaryFresh,
|
|
10
|
+
unsupportedPlatformHint } = require('./find-binary');
|
|
10
11
|
|
|
11
12
|
function mkDir(t, prefix) {
|
|
12
13
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
@@ -219,3 +220,27 @@ test('isCachedBinaryFresh: file basename mismatch → not fresh', (t) => {
|
|
|
219
220
|
if (process.platform !== 'win32') fs.chmodSync(wrongName, 0o755);
|
|
220
221
|
assert.equal(isCachedBinaryFresh(wrongName, '0.25.0'), false);
|
|
221
222
|
});
|
|
223
|
+
|
|
224
|
+
// ── unsupportedPlatformHint (actionable message for tails with no prebuilt binary) ──
|
|
225
|
+
|
|
226
|
+
test('unsupportedPlatformHint flags Alpine/musl with a source/glibc-image hint', () => {
|
|
227
|
+
const hint = unsupportedPlatformHint('linux', 'x64', 'musl');
|
|
228
|
+
assert.ok(hint, 'musl should produce a hint');
|
|
229
|
+
assert.match(hint, /musl|Alpine/);
|
|
230
|
+
assert.match(hint, /cargo install/);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('unsupportedPlatformHint flags native Windows-on-ARM with emulation/source hint', () => {
|
|
234
|
+
const hint = unsupportedPlatformHint('win32', 'arm64', 'glibc');
|
|
235
|
+
assert.ok(hint, 'win32-arm64 should produce a hint');
|
|
236
|
+
assert.match(hint, /Windows on ARM|arm64/);
|
|
237
|
+
assert.match(hint, /x64|cargo install/);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('unsupportedPlatformHint returns null for supported platforms', () => {
|
|
241
|
+
assert.equal(unsupportedPlatformHint('linux', 'x64', 'glibc'), null);
|
|
242
|
+
assert.equal(unsupportedPlatformHint('linux', 'arm64', 'glibc'), null);
|
|
243
|
+
assert.equal(unsupportedPlatformHint('darwin', 'arm64', 'glibc'), null);
|
|
244
|
+
assert.equal(unsupportedPlatformHint('darwin', 'x64', 'glibc'), null);
|
|
245
|
+
assert.equal(unsupportedPlatformHint('win32', 'x64', 'glibc'), null);
|
|
246
|
+
});
|
|
@@ -105,7 +105,7 @@ if (process.env.CODE_GRAPH_FORCE_PLUGIN_MCP !== '1' && isNonProjectCwd(process.c
|
|
|
105
105
|
return;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
const { findBinary, clearCache } = require('./find-binary');
|
|
108
|
+
const { findBinary, clearCache, unsupportedPlatformHint } = require('./find-binary');
|
|
109
109
|
|
|
110
110
|
let binary = findBinary();
|
|
111
111
|
|
|
@@ -169,6 +169,14 @@ if (!binary) {
|
|
|
169
169
|
const installedViaMarketplace = fs.existsSync(
|
|
170
170
|
path.join(__dirname, '..', '.claude-plugin', 'plugin.json')
|
|
171
171
|
);
|
|
172
|
+
const platformHint = unsupportedPlatformHint();
|
|
173
|
+
if (platformHint) {
|
|
174
|
+
// Unsupported platform (Alpine/musl or native Windows-on-ARM): the per-platform
|
|
175
|
+
// npm package does not exist, so the generic "npm install @sdsrs/code-graph-<plat>-<arch>"
|
|
176
|
+
// suggestion below would point at a nonexistent package. Show the source/emulation hint.
|
|
177
|
+
process.stderr.write('[code-graph] Binary not found.\n' + platformHint + '\n');
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
172
180
|
process.stderr.write('[code-graph] Binary not found. Install manually:\n');
|
|
173
181
|
if (installedViaMarketplace) {
|
|
174
182
|
process.stderr.write(
|
|
@@ -217,6 +225,10 @@ child.on('error', (err) => {
|
|
|
217
225
|
` xattr -d com.apple.quarantine "${binary}"\n`
|
|
218
226
|
);
|
|
219
227
|
}
|
|
228
|
+
// A glibc binary installed on musl (older npm ignores the `libc` field) is present
|
|
229
|
+
// but execs into a loader error — surface the actionable platform hint.
|
|
230
|
+
const platformHint = unsupportedPlatformHint();
|
|
231
|
+
if (platformHint) process.stderr.write(platformHint + '\n');
|
|
220
232
|
process.exit(1);
|
|
221
233
|
});
|
|
222
234
|
|
|
@@ -82,6 +82,9 @@ hidden 5 加载 schema —— 实操中走新 flag。CLI 子命令保持原样
|
|
|
82
82
|
| "相似/重复函数"(需 embedding) | `code-graph-mcp similar X` | `get_ast_node symbol_name=X include_similar=true` |
|
|
83
83
|
| "未使用的代码" | `code-graph-mcp dead-code [path]` | `module_overview path=<path> include_dead=true` |
|
|
84
84
|
| "架构咽喉/桥节点是谁?" | `code-graph-mcp centrality` | —(CLI-only;betweenness 中心性,补 `map` 的 caller_count 度中心性) |
|
|
85
|
+
| "循环导入依赖(哪些文件互相 import)?" | `code-graph-mcp cycles` | —(CLI-only;文件级 import 环 = SCC;JS/TS/Py/Go 是坏味,Rust 内部环常良性) |
|
|
86
|
+
| "可疑/意外的跨模块耦合?" | `code-graph-mcp surprising` | —(CLI-only;跨文件 calls/refs 按 低置信(ambiguous>inferred)+跨模块+sole-bridge 打分) |
|
|
87
|
+
| "代码健康总览(想要一份报告)?" | `code-graph-mcp report` | —(CLI-only;汇总 summary+置信度 / hot / chokepoints / cycles / surprising / dead-code) |
|
|
85
88
|
|
|
86
89
|
**dead-code 的 `ignore_paths`**:CLI 默认豁免 `["claude-plugin/", "benches/"]`
|
|
87
90
|
(macro/shell 入口点);`--no-ignore` 关闭。MCP 端也接同名参数。
|
|
@@ -117,6 +120,9 @@ code-graph-mcp show SYMBOL # 节点详情
|
|
|
117
120
|
code-graph-mcp refs SYMBOL --relation calls # 引用筛选
|
|
118
121
|
code-graph-mcp refs SYMBOL --min-confidence extracted # 只看精确边(滤跨文件裸名 inferred/ambiguous)
|
|
119
122
|
code-graph-mcp centrality # 架构咽喉(betweenness 桥节点;补 map 的 caller_count)
|
|
123
|
+
code-graph-mcp cycles # 循环导入依赖(文件级 import 环 / SCC)
|
|
124
|
+
code-graph-mcp surprising # 可疑跨模块耦合(低置信 + 跨模块 + sole-bridge 打分)
|
|
125
|
+
code-graph-mcp report # 代码健康总览(汇总 hot/chokepoints/cycles/surprising/dead-code)
|
|
120
126
|
code-graph-mcp dead-code [path] # 未使用代码(默认豁免 claude-plugin/)
|
|
121
127
|
code-graph-mcp dead-code --ignore tmp/ --ignore scripts/bin/ # 自定义豁免前缀
|
|
122
128
|
code-graph-mcp dead-code --no-ignore # 关掉默认豁免,看完整列表
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdsrs/code-graph",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.56.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": {
|
|
@@ -35,10 +35,10 @@
|
|
|
35
35
|
"node": ">=16"
|
|
36
36
|
},
|
|
37
37
|
"optionalDependencies": {
|
|
38
|
-
"@sdsrs/code-graph-linux-x64": "0.
|
|
39
|
-
"@sdsrs/code-graph-linux-arm64": "0.
|
|
40
|
-
"@sdsrs/code-graph-darwin-x64": "0.
|
|
41
|
-
"@sdsrs/code-graph-darwin-arm64": "0.
|
|
42
|
-
"@sdsrs/code-graph-win32-x64": "0.
|
|
38
|
+
"@sdsrs/code-graph-linux-x64": "0.56.0",
|
|
39
|
+
"@sdsrs/code-graph-linux-arm64": "0.56.0",
|
|
40
|
+
"@sdsrs/code-graph-darwin-x64": "0.56.0",
|
|
41
|
+
"@sdsrs/code-graph-darwin-arm64": "0.56.0",
|
|
42
|
+
"@sdsrs/code-graph-win32-x64": "0.56.0"
|
|
43
43
|
}
|
|
44
44
|
}
|