Skip to content

Commit a14e721

Browse files
sdsrssclaude
andcommitted
feat(plugin): improve auto-update UX and reliability
- Reduce update check interval from 24h to 6h (within GitHub API limits) - Add post-update 1h fast check interval to verify binary install success - Record pendingBinaryUpdate state when npm install fails (CI race condition) - Retry pending binary install on next session start (step 0b) - Prompt user to run /mcp after binary update so MCP server uses new version - Add scripts/sync-versions.js for consistent version bumping across Cargo.toml, plugin.json, and marketplace.json Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4ddc27d commit a14e721

5 files changed

Lines changed: 128 additions & 8 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,15 @@
1919
"homepage": "https://github.com/sdsrss/code-graph-mcp",
2020
"repository": "https://github.com/sdsrss/code-graph-mcp",
2121
"license": "MIT",
22-
"keywords": ["code-graph", "ast", "navigation", "mcp", "knowledge-graph", "semantic-search", "call-graph"]
22+
"keywords": [
23+
"code-graph",
24+
"ast",
25+
"navigation",
26+
"mcp",
27+
"knowledge-graph",
28+
"semantic-search",
29+
"call-graph"
30+
]
2331
}
2432
]
2533
}

claude-plugin/.claude-plugin/plugin.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,11 @@
55
"name": "sdsrs"
66
},
77
"version": "0.5.26",
8-
"keywords": ["code-graph", "ast", "navigation", "mcp", "knowledge-graph"]
8+
"keywords": [
9+
"code-graph",
10+
"ast",
11+
"navigation",
12+
"mcp",
13+
"knowledge-graph"
14+
]
915
}

claude-plugin/scripts/auto-update.js

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ const { CACHE_DIR, PLUGIN_ID, MARKETPLACE_NAME, readManifest, readJson, writeJso
1010
const GITHUB_REPO = 'sdsrss/code-graph-mcp';
1111
const NPM_PACKAGE = '@sdsrs/code-graph';
1212
const STATE_FILE = path.join(CACHE_DIR, 'update-state.json');
13-
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24h
14-
const RATE_LIMIT_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6h if rate-limited
13+
const CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6h (GitHub allows 60 req/h unauthenticated)
14+
const RATE_LIMIT_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24h if rate-limited
15+
const POST_UPDATE_INTERVAL_MS = 1 * 60 * 60 * 1000; // 1h after update (verify success)
1516
const FETCH_TIMEOUT_MS = 3000;
1617

1718
// ── State Persistence ──────────────────────────────────────
@@ -42,7 +43,9 @@ function isDevMode() {
4243
function shouldCheck(state) {
4344
if (!state.lastCheck) return true;
4445
const elapsed = Date.now() - new Date(state.lastCheck).getTime();
45-
const interval = state.rateLimited ? RATE_LIMIT_INTERVAL_MS : CHECK_INTERVAL_MS;
46+
let interval = CHECK_INTERVAL_MS;
47+
if (state.rateLimited) interval = RATE_LIMIT_INTERVAL_MS;
48+
else if (state.pendingBinaryUpdate) interval = POST_UPDATE_INTERVAL_MS;
4649
return elapsed >= interval;
4750
}
4851

@@ -154,14 +157,18 @@ async function downloadAndInstall(latest) {
154157
} catch { /* manifest update failed — not fatal */ }
155158

156159
// 6. Update npm binary (non-blocking, best-effort)
160+
let binaryUpdated = false;
157161
try {
158162
execFileSync('npm', ['install', '-g', `${NPM_PACKAGE}@${latest.version}`], {
159163
timeout: 60000,
160164
stdio: 'pipe',
161165
});
166+
binaryUpdated = true;
167+
// Clear pending flag on success
168+
try { const s = readState(); delete s.pendingBinaryUpdate; saveState(s); } catch { /* ok */ }
162169
} catch {
163-
// npm install failed — plugin files still updated
164-
// User can manually update binary later
170+
// npm package may not be published yet (race with CI). Record for retry.
171+
try { const s = readState(); s.pendingBinaryUpdate = latest.version; saveState(s); } catch { /* ok */ }
165172
}
166173

167174
return true;

claude-plugin/scripts/session-init.js

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,31 @@ const path = require('path');
66
const os = require('os');
77
const { findBinary } = require('./find-binary');
88
const { install, update, readManifest, getPluginVersion, checkScopeConflict } = require('./lifecycle');
9-
const { checkForUpdate } = require('./auto-update');
9+
const { checkForUpdate, readState: readUpdateState } = require('./auto-update');
1010

1111
let BIN = findBinary();
1212

13+
// --- 0b. Retry pending binary update from previous failed auto-update ---
14+
{
15+
const updateState = readUpdateState();
16+
if (updateState.pendingBinaryUpdate) {
17+
const pendingVer = updateState.pendingBinaryUpdate;
18+
try {
19+
execFileSync('npm', ['install', '-g', `@sdsrs/code-graph@${pendingVer}`], {
20+
timeout: 30000, stdio: 'pipe'
21+
});
22+
try { fs.unlinkSync(path.join(os.homedir(), '.cache', 'code-graph', 'binary-path')); } catch {}
23+
// Clear pending flag
24+
const { writeJsonAtomic, CACHE_DIR } = require('./lifecycle');
25+
const s = readUpdateState();
26+
delete s.pendingBinaryUpdate;
27+
writeJsonAtomic(path.join(CACHE_DIR, 'update-state.json'), s);
28+
process.stderr.write(`[code-graph] Binary retry succeeded: v${pendingVer}\n`);
29+
BIN = findBinary(); // refresh
30+
} catch { /* npm still not available — will retry next session */ }
31+
}
32+
}
33+
1334
// --- 0. Auto-install binary if missing ---
1435
if (!BIN) {
1536
const version = getPluginVersion();
@@ -66,18 +87,27 @@ if (BIN) {
6687
(pv[0] === bv[0] && pv[1] === bv[1] && pv[2] > bv[2]);
6788
if (pluginNewer) {
6889
process.stderr.write(`[code-graph] Binary v${binVersion} < plugin v${pluginVersion}, updating...\n`);
90+
let binarySynced = false;
6991
try {
7092
execFileSync('npm', ['install', '-g', `@sdsrs/code-graph@${pluginVersion}`], {
7193
timeout: 30000, stdio: 'pipe'
7294
});
7395
// Clear cached binary path so next lookup finds the new binary
7496
try { fs.unlinkSync(path.join(os.homedir(), '.cache', 'code-graph', 'binary-path')); } catch {}
7597
process.stderr.write(`[code-graph] Binary updated to v${pluginVersion}\n`);
98+
binarySynced = true;
7699
} catch {
77100
process.stderr.write(
78101
`[code-graph] Auto-update failed. Run: npm install -g @sdsrs/code-graph@${pluginVersion}\n`
79102
);
80103
}
104+
if (binarySynced) {
105+
// MCP server is still running old binary — prompt user to reconnect
106+
process.stdout.write(
107+
`\n\u26A0\uFE0F [code-graph] Binary updated v${binVersion} \u2192 v${pluginVersion}. ` +
108+
`Run /mcp to reconnect MCP server with new version.\n`
109+
);
110+
}
81111
}
82112
}
83113
} catch { /* version check failed — not critical */ }
@@ -107,6 +137,10 @@ if (!manifest.version) {
107137
const result = await checkForUpdate();
108138
if (result && result.updated) {
109139
process.stderr.write(`[code-graph] Updated: v${result.from} \u2192 v${result.to}\n`);
140+
process.stdout.write(
141+
`\n\uD83D\uDD04 [code-graph] Auto-updated v${result.from} \u2192 v${result.to}. ` +
142+
`Run /mcp to use the new version.\n`
143+
);
110144
} else if (result && result.updateAvailable) {
111145
process.stderr.write(
112146
`[code-graph] Update available: v${result.from} \u2192 v${result.to}. ` +

scripts/sync-versions.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#!/usr/bin/env node
2+
'use strict';
3+
/**
4+
* Sync version across all project files.
5+
* Usage: node scripts/sync-versions.js <version>
6+
* Example: node scripts/sync-versions.js 0.5.27
7+
*/
8+
const fs = require('fs');
9+
const path = require('path');
10+
11+
const version = process.argv[2];
12+
if (!version || !/^\d+\.\d+\.\d+$/.test(version)) {
13+
console.error('Usage: node scripts/sync-versions.js <semver>');
14+
console.error('Example: node scripts/sync-versions.js 0.5.27');
15+
process.exit(1);
16+
}
17+
18+
const root = path.resolve(__dirname, '..');
19+
20+
const updates = [
21+
{
22+
file: 'Cargo.toml',
23+
transform: (content) => content.replace(/^version = ".*"/m, `version = "${version}"`),
24+
},
25+
{
26+
file: 'claude-plugin/.claude-plugin/plugin.json',
27+
json: true,
28+
transform: (obj) => { obj.version = version; return obj; },
29+
},
30+
{
31+
file: '.claude-plugin/marketplace.json',
32+
json: true,
33+
transform: (obj) => {
34+
if (obj.metadata) obj.metadata.version = version;
35+
if (obj.plugins && obj.plugins[0]) obj.plugins[0].version = version;
36+
return obj;
37+
},
38+
},
39+
];
40+
41+
let changed = 0;
42+
for (const { file, json, transform } of updates) {
43+
const filePath = path.join(root, file);
44+
if (!fs.existsSync(filePath)) {
45+
console.warn(` skip: ${file} (not found)`);
46+
continue;
47+
}
48+
const original = fs.readFileSync(filePath, 'utf8');
49+
let result;
50+
if (json) {
51+
const obj = JSON.parse(original);
52+
result = JSON.stringify(transform(obj), null, 2) + '\n';
53+
} else {
54+
result = transform(original);
55+
}
56+
if (result !== original) {
57+
fs.writeFileSync(filePath, result);
58+
console.log(` updated: ${file}`);
59+
changed++;
60+
} else {
61+
console.log(` unchanged: ${file}`);
62+
}
63+
}
64+
65+
console.log(`\nVersion synced to ${version} (${changed} file${changed !== 1 ? 's' : ''} updated)`);

0 commit comments

Comments
 (0)