@sdsrs/code-graph 0.55.0 → 0.56.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.
- 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/lifecycle.e2e.test.js +36 -0
- package/claude-plugin/scripts/mcp-launcher.js +13 -1
- package/claude-plugin/scripts/statusline-composite.js +34 -11
- 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
|
+
});
|
|
@@ -141,3 +141,39 @@ test('lifecycle install writes to CLAUDE_CONFIG_DIR instead of ~/.claude when se
|
|
|
141
141
|
'default ~/.claude/settings.json must not be written when override is set');
|
|
142
142
|
});
|
|
143
143
|
|
|
144
|
+
test('composite expands a leading ~ in a _previous command instead of dropping it (issue #24)', (t) => {
|
|
145
|
+
// A user whose prior statusline used a leading ~ (valid in settings.json, which
|
|
146
|
+
// Claude Code runs through a shell). install() captures it verbatim as _previous.
|
|
147
|
+
// The composite runs providers via execFileSync (no shell), so without tilde
|
|
148
|
+
// expansion the command throws ENOENT and is silently swallowed — the user's
|
|
149
|
+
// original statusline vanishes.
|
|
150
|
+
const homeDir = mkHome(t);
|
|
151
|
+
const prevScript = path.join(homeDir, '.claude', 'utils', 'statusline.sh');
|
|
152
|
+
fs.mkdirSync(path.dirname(prevScript), { recursive: true });
|
|
153
|
+
fs.writeFileSync(prevScript, '#!/bin/sh\necho "PREV-STATUSLINE-OK"\n');
|
|
154
|
+
fs.chmodSync(prevScript, 0o755);
|
|
155
|
+
|
|
156
|
+
const registryPath = path.join(homeDir, '.cache', 'code-graph', 'statusline-registry.json');
|
|
157
|
+
writeJson(registryPath, [
|
|
158
|
+
{ id: '_previous', command: '~/.claude/utils/statusline.sh', needsStdin: true },
|
|
159
|
+
]);
|
|
160
|
+
|
|
161
|
+
const out = runScript(homeDir, compositeCli, [], { input: '{}' });
|
|
162
|
+
assert.match(out, /PREV-STATUSLINE-OK/,
|
|
163
|
+
'a _previous command using a leading ~ must be tilde-expanded, not silently dropped');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('expandTilde mirrors shell tilde expansion (only a leading ~ / ~/)', () => {
|
|
167
|
+
const composite = require('./statusline-composite');
|
|
168
|
+
const home = os.homedir();
|
|
169
|
+
assert.equal(composite.expandTilde('~'), home);
|
|
170
|
+
assert.equal(composite.expandTilde('~/.claude/utils/statusline.sh'),
|
|
171
|
+
path.join(home, '.claude', 'utils', 'statusline.sh'));
|
|
172
|
+
assert.equal(composite.expandTilde('/abs/path/script.sh'), '/abs/path/script.sh');
|
|
173
|
+
assert.equal(composite.expandTilde('node'), 'node');
|
|
174
|
+
assert.equal(composite.expandTilde('~user/script.sh'), '~user/script.sh',
|
|
175
|
+
'other-user home dirs are not resolved');
|
|
176
|
+
assert.equal(composite.expandTilde('a~/b'), 'a~/b',
|
|
177
|
+
'only a leading ~ expands, not a mid-string ~');
|
|
178
|
+
});
|
|
179
|
+
|
|
@@ -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
|
|
|
@@ -7,22 +7,28 @@
|
|
|
7
7
|
*/
|
|
8
8
|
const { execFileSync } = require('child_process');
|
|
9
9
|
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
10
11
|
const lifecycle = require('./lifecycle');
|
|
11
12
|
const { readRegistry } = lifecycle;
|
|
12
13
|
const cleanupDisabledStatusline = lifecycle.cleanupDisabledStatusline || (() => ({ cleaned: false }));
|
|
13
14
|
|
|
14
15
|
const SEPARATOR = ' \x1b[2m|\x1b[0m ';
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
function main() {
|
|
18
|
+
const disabledCleanup = cleanupDisabledStatusline();
|
|
19
|
+
if (disabledCleanup.cleaned) process.exit(0);
|
|
18
20
|
|
|
19
|
-
// Collect stdin (Claude Code pipes JSON context)
|
|
20
|
-
let stdinData = '';
|
|
21
|
-
let ran = false;
|
|
22
|
-
const stdinTimeout = setTimeout(() => { if (!ran) { ran = true; run(''); } }, 2000);
|
|
23
|
-
process.stdin.setEncoding('utf8');
|
|
24
|
-
process.stdin.on('data', (chunk) => { stdinData += chunk; });
|
|
25
|
-
process.stdin.on('end', () => { clearTimeout(stdinTimeout); if (!ran) { ran = true; run(stdinData); } });
|
|
21
|
+
// Collect stdin (Claude Code pipes JSON context)
|
|
22
|
+
let stdinData = '';
|
|
23
|
+
let ran = false;
|
|
24
|
+
const stdinTimeout = setTimeout(() => { if (!ran) { ran = true; run(''); } }, 2000);
|
|
25
|
+
process.stdin.setEncoding('utf8');
|
|
26
|
+
process.stdin.on('data', (chunk) => { stdinData += chunk; });
|
|
27
|
+
process.stdin.on('end', () => { clearTimeout(stdinTimeout); if (!ran) { ran = true; run(stdinData); } });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Only run the statusline when invoked as a CLI; `require()` (tests) just imports helpers.
|
|
31
|
+
if (require.main === module) main();
|
|
26
32
|
|
|
27
33
|
function run(stdin) {
|
|
28
34
|
const registry = readRegistry();
|
|
@@ -58,7 +64,14 @@ function runProvider(command, needsStdin, stdin) {
|
|
|
58
64
|
const parts = parseCommand(command);
|
|
59
65
|
if (!parts) return null;
|
|
60
66
|
|
|
61
|
-
|
|
67
|
+
// Claude Code runs statusLine.command through a shell, so a leading `~`
|
|
68
|
+
// (e.g. `~/.claude/utils/statusline.sh`) is expanded natively. execFileSync
|
|
69
|
+
// does NOT use a shell, so we must expand `~/` ourselves on every word —
|
|
70
|
+
// otherwise a `_previous` command captured verbatim throws ENOENT and gets
|
|
71
|
+
// swallowed below, silently dropping the user's original statusline.
|
|
72
|
+
const argv = parts.map(expandTilde);
|
|
73
|
+
|
|
74
|
+
const out = execFileSync(argv[0], argv.slice(1), {
|
|
62
75
|
timeout: 3000,
|
|
63
76
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
64
77
|
input: needsStdin ? stdin : '',
|
|
@@ -81,9 +94,19 @@ function parseCommand(cmd) {
|
|
|
81
94
|
return parts.length > 0 ? parts : null;
|
|
82
95
|
}
|
|
83
96
|
|
|
97
|
+
// Expand a leading `~` / `~/` to the home directory, mirroring shell tilde
|
|
98
|
+
// expansion (which Claude Code applies when it runs statusLine.command, but
|
|
99
|
+
// execFileSync does not). Only a bare `~` or a `~/`-prefixed word is expanded;
|
|
100
|
+
// `~user` and mid-string `~` are left untouched (we don't resolve other users).
|
|
101
|
+
function expandTilde(p) {
|
|
102
|
+
if (p === '~') return os.homedir();
|
|
103
|
+
if (p.startsWith('~/')) return path.join(os.homedir(), p.slice(2));
|
|
104
|
+
return p;
|
|
105
|
+
}
|
|
106
|
+
|
|
84
107
|
function codeGraphCommand() {
|
|
85
108
|
// Always derive from __dirname — CLAUDE_PLUGIN_ROOT can leak from other plugins
|
|
86
109
|
return `node "${path.join(__dirname, 'statusline.js')}"`;
|
|
87
110
|
}
|
|
88
111
|
|
|
89
|
-
module.exports = { run };
|
|
112
|
+
module.exports = { run, runProvider, parseCommand, expandTilde };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdsrs/code-graph",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.56.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": {
|
|
@@ -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.1",
|
|
39
|
+
"@sdsrs/code-graph-linux-arm64": "0.56.1",
|
|
40
|
+
"@sdsrs/code-graph-darwin-x64": "0.56.1",
|
|
41
|
+
"@sdsrs/code-graph-darwin-arm64": "0.56.1",
|
|
42
|
+
"@sdsrs/code-graph-win32-x64": "0.56.1"
|
|
43
43
|
}
|
|
44
44
|
}
|