@sdsrs/code-graph 0.85.6 → 0.85.8
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/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/claude-plugin/scripts/auto-update.js +33 -8
- package/claude-plugin/scripts/auto-update.test.js +40 -0
- package/claude-plugin/scripts/session-init.js +17 -3
- package/claude-plugin/scripts/session-init.test.js +23 -1
- package/package.json +6 -6
|
@@ -33,7 +33,9 @@ function commandExists(cmd) {
|
|
|
33
33
|
const GITHUB_REPO = 'sdsrss/code-graph-mcp';
|
|
34
34
|
const STATE_FILE = path.join(CACHE_DIR, 'update-state.json');
|
|
35
35
|
const BINARY_CACHE_DIR = path.join(CACHE_DIR, 'bin');
|
|
36
|
-
const CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6h
|
|
36
|
+
const CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6h — steady-state re-check
|
|
37
|
+
const UP_TO_DATE_RECHECK_MS = 30 * 60 * 1000; // 30min — re-verify an "up to date" result (release-race guard)
|
|
38
|
+
const SESSION_START_MIN_GAP_MS = 2 * 60 * 1000; // 2min — anti-hammer floor for forced (session-start) checks
|
|
37
39
|
const RATE_LIMIT_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24h if rate-limited
|
|
38
40
|
const FETCH_TIMEOUT_MS = 3000;
|
|
39
41
|
|
|
@@ -45,6 +47,13 @@ function isInstallMissingMode(argv = process.argv.slice(2)) {
|
|
|
45
47
|
return argv.includes('--install-missing');
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
// High-intent trigger (session start / explicit reload) → bypass the soft
|
|
51
|
+
// throttle so an available update is picked up immediately, not on the next
|
|
52
|
+
// 6h/30min tick. Passed by session-init's launchBackgroundAutoUpdate.
|
|
53
|
+
function isForceMode(argv = process.argv.slice(2)) {
|
|
54
|
+
return argv.includes('--force');
|
|
55
|
+
}
|
|
56
|
+
|
|
48
57
|
// ── Platform → GitHub release asset name mapping ──────────
|
|
49
58
|
function getPlatformAssetName() {
|
|
50
59
|
const platform = os.platform();
|
|
@@ -74,10 +83,25 @@ function saveState(state) {
|
|
|
74
83
|
|
|
75
84
|
// ── Throttle ───────────────────────────────────────────────
|
|
76
85
|
|
|
77
|
-
|
|
86
|
+
// Whether to hit GitHub now. Keyed to the previous check's outcome, with a force
|
|
87
|
+
// override for high-intent triggers (session start / explicit reload). Ordering:
|
|
88
|
+
// 1. rate-limit backoff (24h) wins over everything — never push more requests
|
|
89
|
+
// into a GitHub 403.
|
|
90
|
+
// 2. force → only the short SESSION_START_MIN_GAP_MS floor applies, so opening
|
|
91
|
+
// a new session re-checks immediately while a crash/reopen loop still can't
|
|
92
|
+
// hammer the API.
|
|
93
|
+
// 3. otherwise → an "up to date" result is re-verified on a short cadence
|
|
94
|
+
// (UP_TO_DATE_RECHECK_MS). This is the release-publish race guard: a version
|
|
95
|
+
// can go live seconds AFTER a check that said "up to date", and the plain 6h
|
|
96
|
+
// interval left it invisible for the full 6h (observed live — v0.85.7
|
|
97
|
+
// published 8s after a check pinned v0.85.6). A pending-but-unfinished update
|
|
98
|
+
// keeps the 6h steady-state interval.
|
|
99
|
+
function shouldCheck(state, { force = false } = {}) {
|
|
78
100
|
if (!state.lastCheck) return true;
|
|
79
101
|
const elapsed = Date.now() - new Date(state.lastCheck).getTime();
|
|
80
|
-
|
|
102
|
+
if (state.rateLimited) return elapsed >= RATE_LIMIT_INTERVAL_MS;
|
|
103
|
+
if (force) return elapsed >= SESSION_START_MIN_GAP_MS;
|
|
104
|
+
const interval = state.updateAvailable === false ? UP_TO_DATE_RECHECK_MS : CHECK_INTERVAL_MS;
|
|
81
105
|
return elapsed >= interval;
|
|
82
106
|
}
|
|
83
107
|
|
|
@@ -472,7 +496,7 @@ async function selfHealStaleBinary(latest, { needsUpdate = cachedBinaryNeedsUpda
|
|
|
472
496
|
return await download(latest);
|
|
473
497
|
}
|
|
474
498
|
|
|
475
|
-
async function checkForUpdate({ installMissing = false } = {}) {
|
|
499
|
+
async function checkForUpdate({ installMissing = false, force = false } = {}) {
|
|
476
500
|
try {
|
|
477
501
|
// Skip in dev mode — unless the launcher explicitly requested a missing-
|
|
478
502
|
// binary install, in which case we MUST proceed regardless of mode (the
|
|
@@ -491,7 +515,7 @@ async function checkForUpdate({ installMissing = false } = {}) {
|
|
|
491
515
|
// fetch + self-heal path below.
|
|
492
516
|
const binaryMissing = !fs.existsSync(cachedBinaryPath());
|
|
493
517
|
const binaryStale = cachedBinaryStaleVsState(state);
|
|
494
|
-
if (!binaryMissing && !binaryStale && !shouldCheck(state)) {
|
|
518
|
+
if (!binaryMissing && !binaryStale && !shouldCheck(state, { force })) {
|
|
495
519
|
if (state.installedVersion !== installedVersion) {
|
|
496
520
|
saveState({ ...state, installedVersion });
|
|
497
521
|
}
|
|
@@ -567,9 +591,9 @@ async function checkForUpdate({ installMissing = false } = {}) {
|
|
|
567
591
|
}
|
|
568
592
|
|
|
569
593
|
module.exports = {
|
|
570
|
-
checkForUpdate, commandExists, isDevMode, readState, compareVersions,
|
|
594
|
+
checkForUpdate, commandExists, isDevMode, readState, compareVersions, shouldCheck,
|
|
571
595
|
getExtractedPluginVersion, readBinaryVersion, promoteVerifiedBinary,
|
|
572
|
-
isSilentMode, isInstallMissingMode,
|
|
596
|
+
isSilentMode, isInstallMissingMode, isForceMode,
|
|
573
597
|
requestJson, parseLatestRelease, fetchLatestRelease,
|
|
574
598
|
downloadBinary, cachedBinaryPath, cachedBinaryNeedsUpdate, cachedBinaryStaleVsState,
|
|
575
599
|
selfHealStaleBinary,
|
|
@@ -583,12 +607,13 @@ if (require.main === module) {
|
|
|
583
607
|
const cmd = argv.find(arg => !arg.startsWith('--')) || 'check';
|
|
584
608
|
const silent = isSilentMode(argv);
|
|
585
609
|
const installMissing = isInstallMissingMode(argv);
|
|
610
|
+
const force = isForceMode(argv);
|
|
586
611
|
if (cmd === 'status') {
|
|
587
612
|
const state = readState();
|
|
588
613
|
console.log(JSON.stringify(state, null, 2));
|
|
589
614
|
} else {
|
|
590
615
|
if (!silent) console.log('Checking for updates...');
|
|
591
|
-
const result = await checkForUpdate({ installMissing });
|
|
616
|
+
const result = await checkForUpdate({ installMissing, force });
|
|
592
617
|
if (silent) return;
|
|
593
618
|
if (result && result.updated) {
|
|
594
619
|
console.log(`Updated: v${result.from} → v${result.to} (binary: ${result.binaryUpdated ? 'yes' : 'no'})`);
|
|
@@ -20,6 +20,7 @@ const {
|
|
|
20
20
|
selfHealStaleBinary,
|
|
21
21
|
isInstallMissingMode,
|
|
22
22
|
isSilentMode,
|
|
23
|
+
shouldCheck,
|
|
23
24
|
} = require('./auto-update');
|
|
24
25
|
|
|
25
26
|
function mkDir(t, prefix) {
|
|
@@ -180,6 +181,45 @@ test('cachedBinaryStaleVsState bypasses throttle only for a present-but-stale bi
|
|
|
180
181
|
assert.equal(cachedBinaryStaleVsState({ latestVersion: '0.45.1' }, { binaryPath }), false);
|
|
181
182
|
});
|
|
182
183
|
|
|
184
|
+
test('shouldCheck re-verifies an up-to-date state on a short cadence (release-publish race)', () => {
|
|
185
|
+
const minsAgo = (m) => new Date(Date.now() - m * 60 * 1000).toISOString();
|
|
186
|
+
|
|
187
|
+
// never checked → always check
|
|
188
|
+
assert.equal(shouldCheck({}), true);
|
|
189
|
+
|
|
190
|
+
// Bug repro: the last check reported "up to date" (updateAvailable:false) and a
|
|
191
|
+
// release published moments later. 45min on, the plain 6h throttle kept the
|
|
192
|
+
// stale answer latched (every session reopen re-reported up-to-date); the short
|
|
193
|
+
// up-to-date cadence must allow a re-check so the new release is discovered.
|
|
194
|
+
assert.equal(shouldCheck({ lastCheck: minsAgo(45), updateAvailable: false }), true);
|
|
195
|
+
|
|
196
|
+
// within the short window → still throttled (don't hammer the API every call)
|
|
197
|
+
assert.equal(shouldCheck({ lastCheck: minsAgo(10), updateAvailable: false }), false);
|
|
198
|
+
|
|
199
|
+
// a pending-but-unfinished update keeps the 6h steady-state interval
|
|
200
|
+
assert.equal(shouldCheck({ lastCheck: minsAgo(45), updateAvailable: true }), false);
|
|
201
|
+
|
|
202
|
+
// rate-limit backoff (24h) wins even over the up-to-date short cadence
|
|
203
|
+
assert.equal(shouldCheck({ lastCheck: minsAgo(120), updateAvailable: false, rateLimited: true }), false);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('shouldCheck lets a forced (session-start) check bypass the soft throttle', () => {
|
|
207
|
+
const minsAgo = (m) => new Date(Date.now() - m * 60 * 1000).toISOString();
|
|
208
|
+
|
|
209
|
+
// A new session / explicit reload is a strong "get me latest" signal: a forced
|
|
210
|
+
// check runs even inside the 30min up-to-date window (contrast the non-forced
|
|
211
|
+
// call on the same state, which stays throttled).
|
|
212
|
+
assert.equal(shouldCheck({ lastCheck: minsAgo(10), updateAvailable: false }, { force: true }), true);
|
|
213
|
+
assert.equal(shouldCheck({ lastCheck: minsAgo(10), updateAvailable: false }), false);
|
|
214
|
+
|
|
215
|
+
// ...but a short anti-hammer floor still applies, so a crash/reopen loop can't
|
|
216
|
+
// pound the GitHub API on every restart.
|
|
217
|
+
assert.equal(shouldCheck({ lastCheck: minsAgo(0.5), updateAvailable: false }, { force: true }), false);
|
|
218
|
+
|
|
219
|
+
// Rate-limit backoff wins even over force — never push more requests into a 403.
|
|
220
|
+
assert.equal(shouldCheck({ lastCheck: minsAgo(60), updateAvailable: false, rateLimited: true }, { force: true }), false);
|
|
221
|
+
});
|
|
222
|
+
|
|
183
223
|
test('selfHealStaleBinary wires the stale-binary check to a download (the v0.45.x glue)', async () => {
|
|
184
224
|
const latest = { version: '0.45.2', binaryUrl: 'https://example/bin' };
|
|
185
225
|
|
|
@@ -166,9 +166,14 @@ function formatRecentImpact(changed, affected, dependentCap = 6) {
|
|
|
166
166
|
return lines.join('\n');
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
function launchBackgroundAutoUpdate(spawnFn = spawn, env = process.env) {
|
|
169
|
+
function launchBackgroundAutoUpdate(spawnFn = spawn, env = process.env, { force = false } = {}) {
|
|
170
170
|
try {
|
|
171
|
-
const
|
|
171
|
+
const args = [path.join(__dirname, 'auto-update.js'), 'check', '--silent'];
|
|
172
|
+
// A session start / reload forces an immediate check (bypasses the soft
|
|
173
|
+
// throttle down to auto-update.js's short anti-hammer floor + rate-limit
|
|
174
|
+
// backoff), so an available update is picked up now rather than on the next tick.
|
|
175
|
+
if (force) args.push('--force');
|
|
176
|
+
const child = spawnFn(process.execPath, args, {
|
|
172
177
|
detached: true,
|
|
173
178
|
stdio: 'ignore',
|
|
174
179
|
env: { ...env, CODE_GRAPH_AUTO_UPDATE_SILENT: '1' },
|
|
@@ -180,6 +185,14 @@ function launchBackgroundAutoUpdate(spawnFn = spawn, env = process.env) {
|
|
|
180
185
|
}
|
|
181
186
|
}
|
|
182
187
|
|
|
188
|
+
// A session start / resume / clear / explicit reload is a strong "I'm here, get
|
|
189
|
+
// me the latest" signal → force an immediate update check. Automatic mid-session
|
|
190
|
+
// compaction is not high-intent, so it keeps auto-update.js's gentle background
|
|
191
|
+
// cadence. Unknown source (direct calls / tests) is treated as high-intent.
|
|
192
|
+
function isHighIntentSource(source) {
|
|
193
|
+
return source !== 'compact';
|
|
194
|
+
}
|
|
195
|
+
|
|
183
196
|
function syncLifecycleConfig() {
|
|
184
197
|
// v0.49.1: stale-relic guard. A still-running Claude Code process fires
|
|
185
198
|
// SessionStart from the plugin-cache dir it loaded at startup; after
|
|
@@ -509,7 +522,7 @@ function runSessionInit({ source } = {}) {
|
|
|
509
522
|
// Verify binary availability — catch issues early with actionable diagnostics
|
|
510
523
|
const binaryCheck = verifyBinary();
|
|
511
524
|
|
|
512
|
-
const autoUpdateLaunched = launchBackgroundAutoUpdate();
|
|
525
|
+
const autoUpdateLaunched = launchBackgroundAutoUpdate(spawn, process.env, { force: isHighIntentSource(source) });
|
|
513
526
|
const indexFreshness = binaryCheck.available ? ensureIndexFresh() : 'skipped';
|
|
514
527
|
|
|
515
528
|
// 上下文感知默认:插件模式下首次 SessionStart 自动安装(创建/注入 CLAUDE.md 块 +
|
|
@@ -752,6 +765,7 @@ function detectHookDark() {
|
|
|
752
765
|
|
|
753
766
|
module.exports = {
|
|
754
767
|
launchBackgroundAutoUpdate,
|
|
768
|
+
isHighIntentSource,
|
|
755
769
|
syncLifecycleConfig,
|
|
756
770
|
ensureIndexFresh,
|
|
757
771
|
indexNeedsRevalidation,
|
|
@@ -5,7 +5,7 @@ const fs = require('fs');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
|
|
7
7
|
const os = require('os');
|
|
8
|
-
const { launchBackgroundAutoUpdate, syncLifecycleConfig, ensureIndexFresh, indexNeedsRevalidation, verifyBinary, computeQuietHooks, shouldInjectMap, shouldInjectRecentImpact, recentImpactWorthShowing, filterSourceFiles, parseGitStatusPaths, formatRecentImpact } = require('./session-init');
|
|
8
|
+
const { launchBackgroundAutoUpdate, isHighIntentSource, syncLifecycleConfig, ensureIndexFresh, indexNeedsRevalidation, verifyBinary, computeQuietHooks, shouldInjectMap, shouldInjectRecentImpact, recentImpactWorthShowing, filterSourceFiles, parseGitStatusPaths, formatRecentImpact } = require('./session-init');
|
|
9
9
|
|
|
10
10
|
// Write an executable stub named `code-graph-mcp` that emits `json` to stdout on
|
|
11
11
|
// `health-check` and exits with `exitCode`. Mirrors how the real binary behaves:
|
|
@@ -121,6 +121,28 @@ test('launchBackgroundAutoUpdate spawns detached silent updater', () => {
|
|
|
121
121
|
assert.equal(calls[0].unrefCalled, true);
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
+
test('launchBackgroundAutoUpdate forwards --force only when asked (session-start bypass)', () => {
|
|
125
|
+
const calls = [];
|
|
126
|
+
const capture = (_command, args) => {
|
|
127
|
+
calls.push({ args });
|
|
128
|
+
return { unref() {} };
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
launchBackgroundAutoUpdate(capture, {}, { force: true });
|
|
132
|
+
assert.deepEqual(calls[0].args.slice(1), ['check', '--silent', '--force']);
|
|
133
|
+
|
|
134
|
+
launchBackgroundAutoUpdate(capture, {}); // default → no --force
|
|
135
|
+
assert.deepEqual(calls[1].args.slice(1), ['check', '--silent']);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('isHighIntentSource forces on session start/resume/clear but not automatic compaction', () => {
|
|
139
|
+
assert.equal(isHighIntentSource('startup'), true);
|
|
140
|
+
assert.equal(isHighIntentSource('resume'), true);
|
|
141
|
+
assert.equal(isHighIntentSource('clear'), true);
|
|
142
|
+
assert.equal(isHighIntentSource(undefined), true); // direct call / unknown → high intent
|
|
143
|
+
assert.equal(isHighIntentSource('compact'), false); // frequent + automatic → gentle cadence
|
|
144
|
+
});
|
|
145
|
+
|
|
124
146
|
const { consistencyCheck, runSessionInit } = require('./session-init');
|
|
125
147
|
|
|
126
148
|
test('consistencyCheck is exported as a function', () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdsrs/code-graph",
|
|
3
|
-
"version": "0.85.
|
|
3
|
+
"version": "0.85.8",
|
|
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.85.
|
|
39
|
-
"@sdsrs/code-graph-linux-arm64": "0.85.
|
|
40
|
-
"@sdsrs/code-graph-darwin-x64": "0.85.
|
|
41
|
-
"@sdsrs/code-graph-darwin-arm64": "0.85.
|
|
42
|
-
"@sdsrs/code-graph-win32-x64": "0.85.
|
|
38
|
+
"@sdsrs/code-graph-linux-x64": "0.85.8",
|
|
39
|
+
"@sdsrs/code-graph-linux-arm64": "0.85.8",
|
|
40
|
+
"@sdsrs/code-graph-darwin-x64": "0.85.8",
|
|
41
|
+
"@sdsrs/code-graph-darwin-arm64": "0.85.8",
|
|
42
|
+
"@sdsrs/code-graph-win32-x64": "0.85.8"
|
|
43
43
|
}
|
|
44
44
|
}
|