@sdsrs/code-graph 0.111.0 → 0.111.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/README.md +5 -3
- package/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/claude-plugin/scripts/auto-update.js +41 -3
- package/claude-plugin/scripts/doctor.js +1 -1
- package/claude-plugin/scripts/launcher-install.js +14 -1
- package/claude-plugin/scripts/npm-exec.js +7 -1
- package/claude-plugin/scripts/statusline.js +27 -8
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -160,9 +160,11 @@ Then reconnect the MCP server in Claude Code with `/mcp`.
|
|
|
160
160
|
Set it in the `env` block of `~/.claude/settings.json` (the environment of the
|
|
161
161
|
process hosting the MCP server), then reconnect with `/mcp`.
|
|
162
162
|
|
|
163
|
-
An update that keeps failing also stops
|
|
164
|
-
|
|
165
|
-
release is published
|
|
163
|
+
An update that keeps failing also stops hammering: after 5 failed attempts at
|
|
164
|
+
the *same* release the updater drops to one retry per day (and retries
|
|
165
|
+
immediately when a newer release is published), instead of re-downloading on
|
|
166
|
+
every session. While it is in that state the statusline shows `⚠ update stuck`
|
|
167
|
+
and `code-graph-mcp doctor` prints the manual update command.
|
|
166
168
|
|
|
167
169
|
#### Invited-memory mode (quieter prompts)
|
|
168
170
|
|
|
@@ -57,6 +57,20 @@ const FETCH_TIMEOUT_MS = 3000;
|
|
|
57
57
|
// STUCK_UPDATE_ATTEMPTS (drift-guarded in statusline.test.js) so the moment the
|
|
58
58
|
// updater gives up is the moment the statusline stops promising "↻ updating".
|
|
59
59
|
const MAX_UPDATE_ATTEMPTS = 5;
|
|
60
|
+
// ...but suspension is not permanent. The cap alone assumed every repeated
|
|
61
|
+
// failure is permanent, and the causes are not distinguishable at the failure
|
|
62
|
+
// site: a briefly-missing `.sha256` sidecar, a captive portal, a temporarily
|
|
63
|
+
// full disk burn the budget just as fast as a broken tar — and SessionStart
|
|
64
|
+
// forces a check with only a 2-minute floor, so ~5 Claude Code restarts in ~10
|
|
65
|
+
// minutes exhaust it. Before this, recovery required a NEWER release (or
|
|
66
|
+
// hand-deleting update-state.json, which nothing tells the user to do), so a
|
|
67
|
+
// ten-minute outage could park the updater for days. One retry per day keeps
|
|
68
|
+
// the per-session treadmill dead while guaranteeing self-heal.
|
|
69
|
+
//
|
|
70
|
+
// Note the retry can NOT be keyed to `--force`: session-init passes --force on
|
|
71
|
+
// every session start, so re-arming there would restore the exact treadmill
|
|
72
|
+
// this cap exists to stop.
|
|
73
|
+
const SUSPENSION_RETRY_MS = 24 * 60 * 60 * 1000;
|
|
60
74
|
|
|
61
75
|
function isSilentMode(argv = process.argv.slice(2), env = process.env) {
|
|
62
76
|
return argv.includes('--silent') || env.CODE_GRAPH_AUTO_UPDATE_SILENT === '1';
|
|
@@ -880,8 +894,17 @@ async function checkForUpdate({ installMissing = false, force = false, requestJs
|
|
|
880
894
|
// always starts with a full budget. The counter used to be unscoped, which
|
|
881
895
|
// only mattered because nothing ever read it: the download chain re-ran on
|
|
882
896
|
// every single session no matter how many times it had already failed.
|
|
883
|
-
const
|
|
884
|
-
|
|
897
|
+
const sameTarget = state.latestVersion === latest.version;
|
|
898
|
+
const attempts = sameTarget ? (state.updateAttempts || 0) : 0;
|
|
899
|
+
// A suspended release gets one retry per day (SUSPENSION_RETRY_MS), so a
|
|
900
|
+
// transient cause — sidecar blip, captive portal, briefly-full disk —
|
|
901
|
+
// heals itself instead of parking the updater until the next release.
|
|
902
|
+
// Spend the retry by entering the attempt path with the budget one short:
|
|
903
|
+
// if it fails again it re-suspends immediately (and re-stamps the clock),
|
|
904
|
+
// costing at most one download per day rather than one per session.
|
|
905
|
+
const suspendedAt = sameTarget && state.suspendedAt ? Date.parse(state.suspendedAt) : NaN;
|
|
906
|
+
const retryDue = Number.isFinite(suspendedAt) && (Date.now() - suspendedAt) >= SUSPENSION_RETRY_MS;
|
|
907
|
+
if (attempts >= MAX_UPDATE_ATTEMPTS && !retryDue) {
|
|
885
908
|
// Suspended — check-only from here until a newer release moves the
|
|
886
909
|
// target. The one thing still worth attempting is a MISSING cached
|
|
887
910
|
// binary: without it the MCP server has no engine at all, so that
|
|
@@ -895,18 +918,32 @@ async function checkForUpdate({ installMissing = false, force = false, requestJs
|
|
|
895
918
|
latestVersion: latest.version,
|
|
896
919
|
updateAvailable: true,
|
|
897
920
|
updateAttempts: attempts,
|
|
921
|
+
// Stamp on ENTRY to suspension, then leave it alone: the retry clock
|
|
922
|
+
// must measure time since we gave up, not time since the last check
|
|
923
|
+
// (which every session would reset, making the retry unreachable).
|
|
924
|
+
suspendedAt: (sameTarget && state.suspendedAt) || new Date().toISOString(),
|
|
898
925
|
rateLimited: false,
|
|
899
926
|
binaryUpdated: healedMissing || state.binaryUpdated,
|
|
900
927
|
});
|
|
901
928
|
console.error(
|
|
902
929
|
`[code-graph] Auto-update to v${latest.version} suspended after ${attempts} failed attempts on this machine. ` +
|
|
903
930
|
'Update manually (`/plugin update code-graph-mcp`, or `npm install -g @sdsrs/code-graph`) or run `code-graph-mcp doctor`. ' +
|
|
904
|
-
'
|
|
931
|
+
'Retried automatically once a day, and immediately when a newer release is published.'
|
|
905
932
|
);
|
|
906
933
|
return { updateAvailable: true, suspended: true, from: installedVersion, to: latest.version };
|
|
907
934
|
}
|
|
908
935
|
const result = await downloadAndInstall(latest);
|
|
909
936
|
const success = result.pluginUpdated;
|
|
937
|
+
// Suspension clock. It restarts when the daily retry is spent and fails,
|
|
938
|
+
// which is what keeps `retryDue` from staying true and turning the retry
|
|
939
|
+
// back into a per-session treadmill; it clears on success and on a new
|
|
940
|
+
// target version, so a stale stamp from the previous release cannot make
|
|
941
|
+
// the next one look instantly retry-due.
|
|
942
|
+
let nextSuspendedAt;
|
|
943
|
+
if (success) nextSuspendedAt = null;
|
|
944
|
+
else if (retryDue) nextSuspendedAt = new Date().toISOString();
|
|
945
|
+
else if (!sameTarget) nextSuspendedAt = null;
|
|
946
|
+
else nextSuspendedAt = state.suspendedAt || null;
|
|
910
947
|
const newState = {
|
|
911
948
|
lastCheck: new Date().toISOString(),
|
|
912
949
|
installedVersion: success ? latest.version : installedVersion,
|
|
@@ -918,6 +955,7 @@ async function checkForUpdate({ installMissing = false, force = false, requestJs
|
|
|
918
955
|
// forever, asserting a self-heal that never happens. The statusline stops
|
|
919
956
|
// trusting it past STUCK_UPDATE_ATTEMPTS; success resets to 0.
|
|
920
957
|
updateAttempts: success ? 0 : attempts + 1,
|
|
958
|
+
suspendedAt: nextSuspendedAt,
|
|
921
959
|
lastUpdate: success ? new Date().toISOString() : state.lastUpdate,
|
|
922
960
|
rateLimited: false,
|
|
923
961
|
binaryUpdated: result.binaryUpdated,
|
|
@@ -214,7 +214,7 @@ function runDiagnostics({ checkOnly = false } = {}) {
|
|
|
214
214
|
results.push({
|
|
215
215
|
name: 'Auto-update',
|
|
216
216
|
status: 'warn',
|
|
217
|
-
detail: `v${state.latestVersion} failed to install ${attempts}× — auto-retry
|
|
217
|
+
detail: `v${state.latestVersion} failed to install ${attempts}× — auto-retry throttled to once a day. `
|
|
218
218
|
+ 'Update manually: `npm install -g @sdsrs/code-graph` (or `/plugin update code-graph-mcp`)',
|
|
219
219
|
});
|
|
220
220
|
} else if (state && state.updateAvailable && state.binaryUpdated === false) {
|
|
@@ -129,7 +129,20 @@ function installBinaryInBackground({
|
|
|
129
129
|
// `npm.cmd` (needs a shell) and passing `args` alongside `shell: true` is
|
|
130
130
|
// DEP0190 — runtime-deprecated in Node 24, and unescaped. It pre-quotes the
|
|
131
131
|
// whole command into `file` with empty `args`, and carries windowsHide.
|
|
132
|
-
|
|
132
|
+
// try/catch because quoteCmdArg THROWS on an argument it cannot quote, and
|
|
133
|
+
// `version` is read from plugin.json — our own file, but a parse of somebody
|
|
134
|
+
// else's disk. Uncaught, the throw escapes mcp-launcher.js and kills the MCP
|
|
135
|
+
// server AFTER the 0-tool stub is already serving, turning a bad version
|
|
136
|
+
// string into a dead server instead of a failed install. Every sibling call
|
|
137
|
+
// in auto-update.js is already inside a try.
|
|
138
|
+
let npm;
|
|
139
|
+
try {
|
|
140
|
+
npm = npmInvocation(['install', '-g', `@sdsrs/code-graph@${version}`]);
|
|
141
|
+
} catch (e) {
|
|
142
|
+
process.stderr.write(`[code-graph] cannot build the npm install command: ${e.message}\n`);
|
|
143
|
+
finish(onFailed);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
133
146
|
runStep(npm.file, npm.args, npmTimeoutMs, '[code-graph][npm]', spawnFn, (npmExit) => {
|
|
134
147
|
if (resolved()) {
|
|
135
148
|
if (npmExit === 0 && recordGlobalInstall) {
|
|
@@ -30,7 +30,13 @@ function quoteCmdArg(arg) {
|
|
|
30
30
|
if (/["%!\r\n]/.test(s)) {
|
|
31
31
|
throw new Error(`npm argument cannot be safely quoted for cmd.exe: ${JSON.stringify(s)}`);
|
|
32
32
|
}
|
|
33
|
-
|
|
33
|
+
// Double a trailing run of backslashes. cmd.exe itself does not treat `\` as
|
|
34
|
+
// an escape, but the RECEIVING program's MSVCRT argv parser does: `"C:\x\"`
|
|
35
|
+
// reads the final `\"` as an escaped quote and swallows the rest of the
|
|
36
|
+
// command line into that argument. Only reachable for path-shaped args, which
|
|
37
|
+
// no current call site passes — but this helper reads as general-purpose.
|
|
38
|
+
const trailing = /\\+$/.exec(s);
|
|
39
|
+
return `"${trailing ? s + trailing[0] : s}"`;
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
/**
|
|
@@ -19,16 +19,29 @@ const cleanupDisabledStatusline = lifecycle.cleanupDisabledStatusline || (() =>
|
|
|
19
19
|
// real status instead. Without this, a persistently-failing update (missing
|
|
20
20
|
// tar/curl, full disk, blocked network) pins "updating" forever.
|
|
21
21
|
const STUCK_UPDATE_ATTEMPTS = 5;
|
|
22
|
-
function
|
|
22
|
+
function readUpdateState() {
|
|
23
23
|
try {
|
|
24
|
-
|
|
24
|
+
return JSON.parse(fs.readFileSync(
|
|
25
25
|
path.join(os.homedir(), '.cache', 'code-graph', 'update-state.json'), 'utf8'));
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
} catch { return null; /* no state file or unreadable */ }
|
|
27
|
+
}
|
|
28
|
+
function updatePending(st = readUpdateState()) {
|
|
29
|
+
if (!st) return false;
|
|
30
|
+
if ((st.updateAttempts || 0) >= STUCK_UPDATE_ATTEMPTS) return false;
|
|
31
|
+
if (st.updateAvailable) return true;
|
|
32
|
+
if (st.latestVersion && st.installedVersion && st.latestVersion !== st.installedVersion) return true;
|
|
30
33
|
return false;
|
|
31
34
|
}
|
|
35
|
+
// The updater has given up on this release (auto-update.js MAX_UPDATE_ATTEMPTS).
|
|
36
|
+
// This has to be SHOWN, not merely not-lied-about: the updater's own stderr
|
|
37
|
+
// notice is written by a process session-init spawns `detached` with
|
|
38
|
+
// `stdio: 'ignore'`, so nobody ever sees it, and updatePending() going quiet
|
|
39
|
+
// above means the only remaining signal was running `doctor` by hand. A user
|
|
40
|
+
// who never runs doctor would sit on a permanently parked updater with no way
|
|
41
|
+
// to know (found by pre-release review of v0.111.0, fixed in v0.111.1).
|
|
42
|
+
function updateStuck(st = readUpdateState()) {
|
|
43
|
+
return !!(st && st.updateAvailable && (st.updateAttempts || 0) >= STUCK_UPDATE_ATTEMPTS);
|
|
44
|
+
}
|
|
32
45
|
|
|
33
46
|
const disabledCleanup = cleanupDisabledStatusline();
|
|
34
47
|
if (disabledCleanup.cleaned) process.exit(0);
|
|
@@ -94,7 +107,10 @@ const bin = findBinary();
|
|
|
94
107
|
if (!bin) {
|
|
95
108
|
// No usable binary yet. If an update is queued, the background downloader is
|
|
96
109
|
// still fetching it \u2014 that is "updating", not a broken "offline" state.
|
|
97
|
-
process.stdout.write(
|
|
110
|
+
process.stdout.write(
|
|
111
|
+
updateStuck() ? 'code-graph: \u26a0 update stuck'
|
|
112
|
+
: updatePending() ? 'code-graph: \u21bb updating'
|
|
113
|
+
: 'code-graph: offline');
|
|
98
114
|
process.exit(0);
|
|
99
115
|
}
|
|
100
116
|
|
|
@@ -121,6 +137,8 @@ function renderHealth(s) {
|
|
|
121
137
|
// index doesn't masquerade as fully current.
|
|
122
138
|
if (s.index_version_stale) line += ' | \u21bb rebuilding';
|
|
123
139
|
if (s.watching) line += ' | watching';
|
|
140
|
+
// A parked updater is otherwise invisible in normal use — see updateStuck().
|
|
141
|
+
if (updateStuck()) line += ' | \u26a0 update stuck';
|
|
124
142
|
return line;
|
|
125
143
|
}
|
|
126
144
|
|
|
@@ -144,7 +162,8 @@ function statusUnavailable(errText) {
|
|
|
144
162
|
// phrase so a cached binary predating the marker still reads as "updating".
|
|
145
163
|
const errStr = errText || '';
|
|
146
164
|
const binaryOutdated = errStr.includes('code-graph:schema-too-new') || /schema version/i.test(errStr);
|
|
147
|
-
|
|
165
|
+
if (binaryOutdated || updatePending()) return 'code-graph: \u21bb updating';
|
|
166
|
+
return updateStuck() ? 'code-graph: \u26a0 update stuck' : 'code-graph: offline';
|
|
148
167
|
}
|
|
149
168
|
|
|
150
169
|
let report = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdsrs/code-graph",
|
|
3
|
-
"version": "0.111.
|
|
3
|
+
"version": "0.111.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.111.
|
|
39
|
-
"@sdsrs/code-graph-linux-arm64": "0.111.
|
|
40
|
-
"@sdsrs/code-graph-darwin-x64": "0.111.
|
|
41
|
-
"@sdsrs/code-graph-darwin-arm64": "0.111.
|
|
42
|
-
"@sdsrs/code-graph-win32-x64": "0.111.
|
|
38
|
+
"@sdsrs/code-graph-linux-x64": "0.111.1",
|
|
39
|
+
"@sdsrs/code-graph-linux-arm64": "0.111.1",
|
|
40
|
+
"@sdsrs/code-graph-darwin-x64": "0.111.1",
|
|
41
|
+
"@sdsrs/code-graph-darwin-arm64": "0.111.1",
|
|
42
|
+
"@sdsrs/code-graph-win32-x64": "0.111.1"
|
|
43
43
|
}
|
|
44
44
|
}
|