@sdsrs/code-graph 0.101.0 → 0.102.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/LICENSE +21 -0
- package/bin/cli.js +18 -5
- package/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/claude-plugin/scripts/adopt.js +46 -3
- package/claude-plugin/scripts/auto-update.js +139 -8
- package/claude-plugin/scripts/doctor.js +24 -0
- package/claude-plugin/scripts/find-binary.js +101 -35
- package/claude-plugin/scripts/install-lock.js +48 -0
- package/claude-plugin/scripts/launcher-install.js +146 -0
- package/claude-plugin/scripts/lifecycle.js +80 -6
- package/claude-plugin/scripts/mcp-launcher.js +71 -62
- package/claude-plugin/scripts/npm-exec.js +15 -0
- package/claude-plugin/scripts/session-init.js +7 -4
- package/claude-plugin/scripts/statusline.js +29 -5
- package/claude-plugin/scripts/version-utils.js +10 -2
- package/package.json +6 -6
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Used by .mcp.json so the plugin controls binary discovery instead of
|
|
8
8
|
* relying on the binary being in PATH.
|
|
9
9
|
*/
|
|
10
|
-
const { spawn
|
|
10
|
+
const { spawn } = require('child_process');
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const fs = require('fs');
|
|
13
13
|
const { isNonProjectCwd } = require('./project-detect');
|
|
@@ -86,66 +86,15 @@ if (process.env.CODE_GRAPH_FORCE_PLUGIN_MCP !== '1' && isNonProjectCwd(process.c
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
const { findBinary, clearCache, unsupportedPlatformHint } = require('./find-binary');
|
|
89
|
+
const { installBinaryInBackground } = require('./launcher-install');
|
|
89
90
|
|
|
90
|
-
|
|
91
|
+
const binary = findBinary();
|
|
91
92
|
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
version = JSON.parse(fs.readFileSync(pj, 'utf8')).version || 'latest';
|
|
98
|
-
} catch { /* use latest */ }
|
|
99
|
-
|
|
100
|
-
process.stderr.write(`[code-graph] Binary not found, installing @sdsrs/code-graph@${version}...\n`);
|
|
101
|
-
const npmResult = spawnSync('npm', ['install', '-g', `@sdsrs/code-graph@${version}`], {
|
|
102
|
-
timeout: 60000, stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8',
|
|
103
|
-
});
|
|
104
|
-
if (npmResult.error || npmResult.status !== 0) {
|
|
105
|
-
process.stderr.write('[code-graph] npm install failed.\n');
|
|
106
|
-
if (npmResult.stderr) {
|
|
107
|
-
process.stderr.write(npmResult.stderr.trim().split('\n').map(l => `[code-graph][npm] ${l}\n`).join(''));
|
|
108
|
-
}
|
|
109
|
-
} else {
|
|
110
|
-
clearCache();
|
|
111
|
-
binary = findBinary();
|
|
112
|
-
if (binary) {
|
|
113
|
-
process.stderr.write(`[code-graph] Installed at ${binary}\n`);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Fallback: npm install may have succeeded but optionalDependencies for the
|
|
119
|
-
// platform binary can fail silently (npm tolerates OS-mismatch + flaky
|
|
120
|
-
// registry). Pull the platform binary directly from the GitHub release.
|
|
121
|
-
//
|
|
122
|
-
// --install-missing bypasses auto-update.js's isDevMode() short-circuit. The
|
|
123
|
-
// marketplace ships the full repo (including Cargo.toml at the workspace root),
|
|
124
|
-
// so dev-mode heuristics that look for Cargo.toml were misclassifying every
|
|
125
|
-
// marketplace install as dev mode and skipping this fallback (issue #12).
|
|
126
|
-
if (!binary) {
|
|
127
|
-
process.stderr.write('[code-graph] Falling back to GitHub release download...\n');
|
|
128
|
-
const result = spawnSync(
|
|
129
|
-
process.execPath,
|
|
130
|
-
[path.join(__dirname, 'auto-update.js'), '--silent', '--install-missing'],
|
|
131
|
-
{ timeout: 90000, stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' }
|
|
132
|
-
);
|
|
133
|
-
if (result.stderr && result.stderr.trim()) {
|
|
134
|
-
process.stderr.write(result.stderr.trim().split('\n').map(l => `[code-graph][auto-update] ${l}\n`).join(''));
|
|
135
|
-
}
|
|
136
|
-
if (result.error) {
|
|
137
|
-
process.stderr.write(`[code-graph] auto-update spawn failed: ${result.error.message}\n`);
|
|
138
|
-
} else if (result.status !== 0) {
|
|
139
|
-
process.stderr.write(`[code-graph] auto-update exited with status ${result.status}\n`);
|
|
140
|
-
}
|
|
141
|
-
clearCache();
|
|
142
|
-
binary = findBinary();
|
|
143
|
-
if (binary) {
|
|
144
|
-
process.stderr.write(`[code-graph] Installed at ${binary}\n`);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (!binary) {
|
|
93
|
+
// Manual-install guidance, printed when the background install chain exhausts
|
|
94
|
+
// both steps without producing a binary. Unlike the old sync path this does NOT
|
|
95
|
+
// exit: the upgradeable stub stays connected (0 tools), so a manual
|
|
96
|
+
// `npm install -g` mid-session still upgrades the live connection.
|
|
97
|
+
function printManualInstallHints() {
|
|
149
98
|
const installedViaMarketplace = fs.existsSync(
|
|
150
99
|
path.join(__dirname, '..', '.claude-plugin', 'plugin.json')
|
|
151
100
|
);
|
|
@@ -155,9 +104,9 @@ if (!binary) {
|
|
|
155
104
|
// npm package does not exist, so the generic "npm install @sdsrs/code-graph-<plat>-<arch>"
|
|
156
105
|
// suggestion below would point at a nonexistent package. Show the source/emulation hint.
|
|
157
106
|
process.stderr.write('[code-graph] Binary not found.\n' + platformHint + '\n');
|
|
158
|
-
|
|
107
|
+
return;
|
|
159
108
|
}
|
|
160
|
-
process.stderr.write('[code-graph] Binary
|
|
109
|
+
process.stderr.write('[code-graph] Binary install failed. Install manually:\n');
|
|
161
110
|
if (installedViaMarketplace) {
|
|
162
111
|
process.stderr.write(
|
|
163
112
|
' # Re-install the plugin via Claude Code marketplace:\n' +
|
|
@@ -171,7 +120,67 @@ if (!binary) {
|
|
|
171
120
|
' npm install -g @sdsrs/code-graph\n' +
|
|
172
121
|
' npm install -g @sdsrs/code-graph-' + process.platform + '-' + process.arch + '\n'
|
|
173
122
|
);
|
|
174
|
-
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// --- Missing binary: answer the handshake NOW, install in the background ----
|
|
126
|
+
// The old chain ran `npm install -g` (60s timeout) and the GitHub-release
|
|
127
|
+
// fallback (90s) SYNCHRONOUSLY before answering any MCP JSON-RPC. Claude
|
|
128
|
+
// Code's connect timeout is 30s, so a cold install always presented as
|
|
129
|
+
// "MCP server connection timed out after 30000ms" and the tools only appeared
|
|
130
|
+
// on a later reconnect. Serve the upgradeable 0-tool stub first (initialize is
|
|
131
|
+
// answered instantly), run the same install chain in the background, and hand
|
|
132
|
+
// the live connection to the real binary via the same upgrade mechanism the
|
|
133
|
+
// non-project gate uses — no reconnect, no restart.
|
|
134
|
+
//
|
|
135
|
+
// --install-missing bypasses auto-update.js's isDevMode() short-circuit. The
|
|
136
|
+
// marketplace ships the full repo (including Cargo.toml at the workspace root),
|
|
137
|
+
// so dev-mode heuristics that look for Cargo.toml were misclassifying every
|
|
138
|
+
// marketplace install as dev mode and skipping this fallback (issue #12).
|
|
139
|
+
if (!binary) {
|
|
140
|
+
let version = 'latest';
|
|
141
|
+
try {
|
|
142
|
+
const pj = path.join(__dirname, '..', '.claude-plugin', 'plugin.json');
|
|
143
|
+
version = JSON.parse(fs.readFileSync(pj, 'utf8')).version || 'latest';
|
|
144
|
+
} catch { /* use latest */ }
|
|
145
|
+
|
|
146
|
+
process.stderr.write(
|
|
147
|
+
`[code-graph] Binary not found — serving 0-tool stub while installing ` +
|
|
148
|
+
`@sdsrs/code-graph@${version} in the background (tools appear when it lands)...\n`
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const stub = serveEmptyMcpStub({
|
|
152
|
+
upgrade: {
|
|
153
|
+
shouldUpgrade: () => !!findBinary(),
|
|
154
|
+
spawnReal: () => {
|
|
155
|
+
const bin = findBinary();
|
|
156
|
+
if (!bin) return null;
|
|
157
|
+
process.stderr.write(`[code-graph] binary ready at ${bin} — upgrading plugin MCP to real tools (restart Claude Code for full tool steering)\n`);
|
|
158
|
+
return spawn(bin, ['serve'], { stdio: ['pipe', 'pipe', 'inherit'], env: process.env });
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const { GLOBAL_INSTALL_MARKER, INSTALL_LOCK_FILE } = require('./lifecycle');
|
|
164
|
+
installBinaryInBackground({
|
|
165
|
+
version,
|
|
166
|
+
findBinary,
|
|
167
|
+
clearCache,
|
|
168
|
+
// Nudge the handover immediately instead of waiting for the stub's next poll.
|
|
169
|
+
onInstalled: () => stub.attemptUpgrade(),
|
|
170
|
+
onFailed: () => printManualInstallHints(),
|
|
171
|
+
// Marker: this npm install was OURS, so lifecycle.js uninstall knows it
|
|
172
|
+
// owns removing the global packages (never yanks a user's own install).
|
|
173
|
+
recordGlobalInstall: () => {
|
|
174
|
+
fs.mkdirSync(path.dirname(GLOBAL_INSTALL_MARKER), { recursive: true });
|
|
175
|
+
fs.writeFileSync(GLOBAL_INSTALL_MARKER, JSON.stringify({
|
|
176
|
+
installedBy: 'code-graph-mcp launcher', version, at: new Date().toISOString(),
|
|
177
|
+
}, null, 2) + '\n');
|
|
178
|
+
},
|
|
179
|
+
// Serialize against other cold sessions + auto-update (parallel global npm
|
|
180
|
+
// installs corrupt the shared prefix).
|
|
181
|
+
lockPath: INSTALL_LOCK_FILE,
|
|
182
|
+
});
|
|
183
|
+
return; // top-level function scope of mcp-launcher.js
|
|
175
184
|
}
|
|
176
185
|
|
|
177
186
|
// Pre-spawn: verify binary is executable (catches macOS quarantine, permission issues)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// npm is `npm.cmd` on Windows: child_process spawn/execFileSync cannot exec a
|
|
3
|
+
// .cmd without a shell (and Node >= 18.20 throws EINVAL spawning .cmd directly
|
|
4
|
+
// as a CVE-2024-27980 mitigation). Every bare `spawn('npm', ...)` in the
|
|
5
|
+
// install/update flow therefore silently ENOENT'd on Windows while
|
|
6
|
+
// commandExists('npm') (via `where`) said npm was present. All args routed
|
|
7
|
+
// through here are fixed flags / package specs — shell-quoting-safe.
|
|
8
|
+
const NPM_NEEDS_SHELL = process.platform === 'win32';
|
|
9
|
+
|
|
10
|
+
/** Merge shell:true into spawn/exec options when the platform needs it. */
|
|
11
|
+
function npmSpawnOpts(opts = {}) {
|
|
12
|
+
return NPM_NEEDS_SHELL ? { ...opts, shell: true } : opts;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = { npmSpawnOpts, NPM_NEEDS_SHELL };
|
|
@@ -480,10 +480,13 @@ function runSessionInit({ source } = {}) {
|
|
|
480
480
|
cleanupDisabledStatusline();
|
|
481
481
|
// Genuine uninstall (not a temporary disable) leaves residue the settings-only
|
|
482
482
|
// self-heal can't reach: ~/.cache/code-graph (the ~40MB binary + state) and the
|
|
483
|
-
// current project's CLAUDE.md adoption block. CC fires no uninstall hook,
|
|
484
|
-
//
|
|
485
|
-
//
|
|
486
|
-
//
|
|
483
|
+
// current project's CLAUDE.md adoption block. CC fires no uninstall hook, AND it
|
|
484
|
+
// stops loading this plugin's hooks.json the moment the install record is gone —
|
|
485
|
+
// so after a real `/plugin uninstall` this SessionStart usually never runs again.
|
|
486
|
+
// The reachable teardown is cleanupDisabledStatusline() via the composite
|
|
487
|
+
// statusline (still wired in settings.json); it removes the cache residue too.
|
|
488
|
+
// This branch remains for the disable→uninstall-while-running edge and as the
|
|
489
|
+
// only place project unadoption can happen automatically.
|
|
487
490
|
let teardown = null;
|
|
488
491
|
if (uninstalled) {
|
|
489
492
|
const cacheRemoved = removeCacheResidue();
|
|
@@ -57,13 +57,31 @@ const codeGraphDir = path.join(root, '.code-graph');
|
|
|
57
57
|
// Check for background indexing progress file first
|
|
58
58
|
const progressFile = path.join(codeGraphDir, 'indexing-status.json');
|
|
59
59
|
try {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
// Staleness gate: the file is normally deleted by the server's IndexGuard, but
|
|
61
|
+
// a killed process (session exit, SIGKILL, the 30s MCP connect-timeout kill)
|
|
62
|
+
// skips Drop, and the orphan would pin "indexing N/M" here forever. A LIVE
|
|
63
|
+
// indexer heartbeats the file at least once per batch and per finalize phase,
|
|
64
|
+
// so an old mtime proves no indexer is writing it: ignore the file and fall
|
|
65
|
+
// through to the health check. (Mirrors INDEXING_STATUS_STALE_SECS in
|
|
66
|
+
// src/indexer/pipeline/mod.rs, which drives server/CLI-side stale cleanup.)
|
|
67
|
+
const INDEXING_STALE_MS = 120000;
|
|
68
|
+
const fresh = (Date.now() - fs.statSync(progressFile).mtimeMs) < INDEXING_STALE_MS;
|
|
69
|
+
const p = fresh ? JSON.parse(fs.readFileSync(progressFile, 'utf8')) : null;
|
|
70
|
+
if (p && p.s === 'indexing' && p.t > 0) {
|
|
71
|
+
// floor, not round: skipped files (parse errors, oversized) keep d below t
|
|
72
|
+
// even in the terminal progress write, and rounding displayed that state as
|
|
73
|
+
// a confusing stuck "100%".
|
|
74
|
+
const pct = Math.floor((p.d / p.t) * 100);
|
|
64
75
|
process.stdout.write(`code-graph: \u21BB indexing ${p.d}/${p.t} (${pct}%)`);
|
|
65
76
|
process.exit(0);
|
|
66
77
|
}
|
|
78
|
+
if (p && p.s === 'finalizing' && p.t > 0) {
|
|
79
|
+
// Post-batch full-graph phases (context strings, import bind/prune, ANALYZE):
|
|
80
|
+
// the file count no longer moves, so show an explicit phase label instead of
|
|
81
|
+
// a frozen-looking counter.
|
|
82
|
+
process.stdout.write(`code-graph: ↻ finalizing ${p.d}/${p.t}`);
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
67
85
|
} catch { /* no progress file or parse error — continue to health check */ }
|
|
68
86
|
|
|
69
87
|
// No indexing in progress — show normal health status
|
|
@@ -131,8 +149,14 @@ function statusUnavailable(errText) {
|
|
|
131
149
|
let report = null;
|
|
132
150
|
let errText = '';
|
|
133
151
|
try {
|
|
152
|
+
// 1500ms, NOT 3000ms: the composite wrapper kills this whole provider at
|
|
153
|
+
// 3000ms (statusline-composite.js runProvider), so an inner budget equal to
|
|
154
|
+
// the outer one guaranteed the OUTER timeout fired first on a slow
|
|
155
|
+
// health-check (e.g. CPU saturated by the embedding backfill) and the segment
|
|
156
|
+
// silently vanished. Keeping the inner budget well under the outer one turns
|
|
157
|
+
// "slow health-check" into a rendered "offline"/"updating" instead of a blank.
|
|
134
158
|
report = parseReport(execFileSync(bin, ['health-check', '--format', 'json'], {
|
|
135
|
-
timeout:
|
|
159
|
+
timeout: 1500,
|
|
136
160
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
137
161
|
// Run the binary FROM the resolved root so its own project-root resolution
|
|
138
162
|
// lands on the same DB the gate above picked (a subdir cwd would otherwise
|
|
@@ -3,12 +3,20 @@ const { execFileSync } = require('child_process');
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
// Tolerant match: the version line anywhere in the output (m flag), optional
|
|
7
|
+
// "v" prefix, and anything after the numeric triple (build-metadata suffixes
|
|
8
|
+
// like "1.2.3 (abc123)" or "-dev"). The old fully-anchored /^...$/ turned ANY
|
|
9
|
+
// deviation into null, which upstream reads as "broken binary" → judged
|
|
10
|
+
// permanently stale → re-download every session, with the fresh download
|
|
11
|
+
// rejected by the same parse: a self-sustaining loop.
|
|
12
|
+
const VERSION_OUTPUT_RE = /^code-graph-mcp\s+v?(\d+\.\d+\.\d+)/m;
|
|
7
13
|
|
|
8
14
|
function readBinaryVersion(binaryPath) {
|
|
9
15
|
try {
|
|
10
16
|
const out = execFileSync(binaryPath, ['--version'], {
|
|
11
|
-
|
|
17
|
+
// 5s: a cold exec of a freshly-written ~40MB binary (page-in, Windows AV
|
|
18
|
+
// scan) regularly exceeded the old 2s, misclassifying a good binary.
|
|
19
|
+
timeout: 5000,
|
|
12
20
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
13
21
|
}).toString().trim();
|
|
14
22
|
const match = out.match(VERSION_OUTPUT_RE);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdsrs/code-graph",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.102.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": {
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
"node": ">=16"
|
|
37
37
|
},
|
|
38
38
|
"optionalDependencies": {
|
|
39
|
-
"@sdsrs/code-graph-linux-x64": "0.
|
|
40
|
-
"@sdsrs/code-graph-linux-arm64": "0.
|
|
41
|
-
"@sdsrs/code-graph-darwin-x64": "0.
|
|
42
|
-
"@sdsrs/code-graph-darwin-arm64": "0.
|
|
43
|
-
"@sdsrs/code-graph-win32-x64": "0.
|
|
39
|
+
"@sdsrs/code-graph-linux-x64": "0.102.0",
|
|
40
|
+
"@sdsrs/code-graph-linux-arm64": "0.102.0",
|
|
41
|
+
"@sdsrs/code-graph-darwin-x64": "0.102.0",
|
|
42
|
+
"@sdsrs/code-graph-darwin-arm64": "0.102.0",
|
|
43
|
+
"@sdsrs/code-graph-win32-x64": "0.102.0"
|
|
44
44
|
}
|
|
45
45
|
}
|