@algosuite/vo-mcp 0.2.0-beta.46 → 0.2.0-beta.49
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/dist/ci/check-local-pr-overlap.js +31 -8
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +2 -2
- package/dist/install-cli.js +383 -94
- package/dist/install-cli.js.map +4 -4
- package/dist/runner-cli.js +1136 -157
- package/dist/runner-cli.js.map +4 -4
- package/package.json +1 -1
package/dist/install-cli.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import { createRequire as __cr } from 'module'; const require = __cr(import.meta.url);
|
|
3
3
|
|
|
4
4
|
// src/install.ts
|
|
5
|
-
import { homedir as
|
|
6
|
-
import { join as
|
|
7
|
-
import { existsSync as
|
|
5
|
+
import { homedir as homedir4, platform as platform3 } from "node:os";
|
|
6
|
+
import { join as join6, dirname as dirname4 } from "node:path";
|
|
7
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6, mkdirSync as mkdirSync5, statSync as statSync3 } from "node:fs";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
|
|
10
10
|
// src/cloud/pairing.ts
|
|
@@ -198,13 +198,215 @@ async function runPairing(deps = {}) {
|
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
// src/codex-mcp-config.ts
|
|
201
|
-
import {
|
|
202
|
-
import { dirname as
|
|
201
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, statSync as statSync2 } from "node:fs";
|
|
202
|
+
import { dirname as dirname3, join as join4 } from "node:path";
|
|
203
|
+
|
|
204
|
+
// src/config-backup.ts
|
|
205
|
+
import { chmodSync as chmodSync2, copyFileSync, existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
206
|
+
import { basename, dirname as dirname2, join as join2 } from "node:path";
|
|
207
|
+
var BACKUP_SUFFIX_RE = /^\.backup-\d+$/u;
|
|
208
|
+
var TEMP_SUFFIX_RE = /^\.vo-mcp-tmp-\d+-\d+$/u;
|
|
209
|
+
var RENAME_RETRIES = 5;
|
|
210
|
+
var RENAME_RETRY_MS = 50;
|
|
211
|
+
function sweepStaleTempFiles(configPath) {
|
|
212
|
+
const dir = dirname2(configPath);
|
|
213
|
+
const name = basename(configPath);
|
|
214
|
+
if (!existsSync2(dir)) return 0;
|
|
215
|
+
let removed = 0;
|
|
216
|
+
for (const entry of readdirSync(dir)) {
|
|
217
|
+
if (!entry.startsWith(name) || !TEMP_SUFFIX_RE.test(entry.slice(name.length))) continue;
|
|
218
|
+
try {
|
|
219
|
+
unlinkSync(join2(dir, entry));
|
|
220
|
+
removed += 1;
|
|
221
|
+
} catch {
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return removed;
|
|
225
|
+
}
|
|
226
|
+
function sleepSync(ms) {
|
|
227
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
228
|
+
}
|
|
229
|
+
function hasIdenticalBackup(configPath, content) {
|
|
230
|
+
const dir = dirname2(configPath);
|
|
231
|
+
const name = basename(configPath);
|
|
232
|
+
if (!existsSync2(dir)) return false;
|
|
233
|
+
const buffer = Buffer.isBuffer(content) ? content : Buffer.from(content, "utf8");
|
|
234
|
+
for (const entry of readdirSync(dir)) {
|
|
235
|
+
if (!entry.startsWith(name) || !BACKUP_SUFFIX_RE.test(entry.slice(name.length))) continue;
|
|
236
|
+
const candidate = join2(dir, entry);
|
|
237
|
+
try {
|
|
238
|
+
if (statSync(candidate).size !== buffer.length) continue;
|
|
239
|
+
if (readFileSync2(candidate).equals(buffer)) return true;
|
|
240
|
+
} catch {
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
function backupConfigOnce(configPath) {
|
|
246
|
+
if (!existsSync2(configPath)) return null;
|
|
247
|
+
const current = readFileSync2(configPath);
|
|
248
|
+
if (hasIdenticalBackup(configPath, current)) return null;
|
|
249
|
+
const backupPath = `${configPath}.backup-${Date.now()}`;
|
|
250
|
+
copyFileSync(configPath, backupPath);
|
|
251
|
+
return backupPath;
|
|
252
|
+
}
|
|
253
|
+
function writeFileAtomic(path, content) {
|
|
254
|
+
sweepStaleTempFiles(path);
|
|
255
|
+
const temp = `${path}.vo-mcp-tmp-${process.pid}-${Date.now()}`;
|
|
256
|
+
try {
|
|
257
|
+
writeFileSync2(temp, content, { encoding: "utf8", mode: 384 });
|
|
258
|
+
if (existsSync2(path)) {
|
|
259
|
+
try {
|
|
260
|
+
chmodSync2(temp, statSync(path).mode & 511);
|
|
261
|
+
} catch {
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
let lastErr = null;
|
|
265
|
+
for (let attempt = 0; attempt < RENAME_RETRIES; attempt += 1) {
|
|
266
|
+
try {
|
|
267
|
+
renameSync(temp, path);
|
|
268
|
+
return;
|
|
269
|
+
} catch (err) {
|
|
270
|
+
lastErr = err;
|
|
271
|
+
const code = err.code;
|
|
272
|
+
if (code !== "EPERM" && code !== "EBUSY" && code !== "EACCES") throw err;
|
|
273
|
+
sleepSync(RENAME_RETRY_MS);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
writeFileSync2(path, content, "utf8");
|
|
277
|
+
try {
|
|
278
|
+
unlinkSync(temp);
|
|
279
|
+
} catch {
|
|
280
|
+
}
|
|
281
|
+
void lastErr;
|
|
282
|
+
} catch (err) {
|
|
283
|
+
try {
|
|
284
|
+
unlinkSync(temp);
|
|
285
|
+
} catch {
|
|
286
|
+
}
|
|
287
|
+
throw err;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/mcp-launcher.ts
|
|
292
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
293
|
+
import { homedir as homedir2, platform as osPlatform } from "node:os";
|
|
294
|
+
import { isAbsolute, join as join3, normalize, resolve, sep, posix as pathPosix, win32 as pathWin32 } from "node:path";
|
|
295
|
+
var MCP_LAUNCHER_FILE = "vo-mcp-launcher.mjs";
|
|
296
|
+
var MCP_FALLBACK_CLI_ENV = "VO_MCP_FALLBACK_CLI";
|
|
297
|
+
var APP_IDENTIFIER = "ai.algosuite.vo-runner";
|
|
298
|
+
var RUNNER_RUNTIME_DIR = "runner-runtime";
|
|
299
|
+
function defaultRunnerRuntimeRoot(env = process.env, { platform: platform4 = osPlatform(), home = homedir2() } = {}) {
|
|
300
|
+
const explicit = String(env["VO_RUNNER_RUNTIME_ROOT"] || "").trim();
|
|
301
|
+
if (explicit) return isAbsolute(explicit) ? resolve(explicit) : null;
|
|
302
|
+
if (platform4 === "win32") {
|
|
303
|
+
const appData = String(env["APPDATA"] || "").trim();
|
|
304
|
+
return appData && isAbsolute(appData) ? resolve(join3(appData, APP_IDENTIFIER, RUNNER_RUNTIME_DIR)) : null;
|
|
305
|
+
}
|
|
306
|
+
if (!home) return null;
|
|
307
|
+
if (platform4 === "darwin") return resolve(join3(home, "Library", "Application Support", APP_IDENTIFIER, RUNNER_RUNTIME_DIR));
|
|
308
|
+
const xdg = String(env["XDG_CONFIG_HOME"] || "").trim();
|
|
309
|
+
return resolve(join3(xdg && isAbsolute(xdg) ? xdg : join3(home, ".config"), APP_IDENTIFIER, RUNNER_RUNTIME_DIR));
|
|
310
|
+
}
|
|
311
|
+
function mcpLauncherPath(runtimeRoot) {
|
|
312
|
+
return join3(runtimeRoot, MCP_LAUNCHER_FILE);
|
|
313
|
+
}
|
|
314
|
+
function renderMcpLauncher() {
|
|
315
|
+
return `#!/usr/bin/env node
|
|
316
|
+
// Written by \`vo-mcp install\` / the runner daemon (F24, 2026-08-16). Claude's and Codex's vo-mcp MCP
|
|
317
|
+
// server entries point here so every MCP spawn runs the ACTIVE runner runtime slot \u2014 the fleet-approved
|
|
318
|
+
// vo-mcp \u2014 instead of whatever copy \`install\` happened to run from. Resolution: this file's directory,
|
|
319
|
+
// then an ABSOLUTE VO_RUNNER_RUNTIME_ROOT -> current.json (schema 1) active slot -> previous slot ->
|
|
320
|
+
// ${MCP_FALLBACK_CLI_ENV} (a non-slot package cli.js) -> exit 2.
|
|
321
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
322
|
+
import { dirname, isAbsolute, join } from 'node:path';
|
|
323
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
324
|
+
const SLOT_ID_RE = /^vo-mcp-[0-9A-Za-z._-]{1,96}$/u;
|
|
325
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
326
|
+
const envRoot = String(process.env.VO_RUNNER_RUNTIME_ROOT || '').trim();
|
|
327
|
+
const ROOTS = [here, ...(envRoot && isAbsolute(envRoot) && envRoot !== here ? [envRoot] : [])];
|
|
328
|
+
const FALLBACK_CLI = process.env.${MCP_FALLBACK_CLI_ENV} || '';
|
|
329
|
+
function slotCli(root, slotId) {
|
|
330
|
+
if (typeof slotId !== 'string' || !SLOT_ID_RE.test(slotId)) return null;
|
|
331
|
+
const cli = join(root, 'slots', slotId, 'node_modules', '@algosuite', 'vo-mcp', 'dist', 'cli.js');
|
|
332
|
+
return existsSync(cli) ? cli : null;
|
|
333
|
+
}
|
|
334
|
+
function rootCli(root) {
|
|
335
|
+
try {
|
|
336
|
+
const cur = JSON.parse(readFileSync(join(root, 'current.json'), 'utf8'));
|
|
337
|
+
if (!cur || cur.schema_version !== 1) return null;
|
|
338
|
+
return slotCli(root, cur.active && cur.active.slot_id) || slotCli(root, cur.previous && cur.previous.slot_id);
|
|
339
|
+
} catch { return null; }
|
|
340
|
+
}
|
|
341
|
+
// Floor: no usable pointer (quarantined by a bootstrap rollback, corrupt, or a
|
|
342
|
+
// future schema) must not leave every agent without MCP \u2014 run the NEWEST slot
|
|
343
|
+
// on disk that carries a cli.js, and say so on stderr.
|
|
344
|
+
function newestSlotCli(root) {
|
|
345
|
+
try {
|
|
346
|
+
const dir = join(root, 'slots');
|
|
347
|
+
const found = readdirSync(dir).filter((id) => SLOT_ID_RE.test(id) && slotCli(root, id))
|
|
348
|
+
.map((id) => ({ id, mtime: statSync(join(dir, id)).mtimeMs }))
|
|
349
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
350
|
+
return found.length ? slotCli(root, found[0].id) : null;
|
|
351
|
+
} catch { return null; }
|
|
352
|
+
}
|
|
353
|
+
let target = null;
|
|
354
|
+
for (const root of ROOTS) { target = rootCli(root); if (target) break; }
|
|
355
|
+
if (!target) {
|
|
356
|
+
for (const root of ROOTS) { target = newestSlotCli(root); if (target) { process.stderr.write('[vo-mcp-launcher] no usable runtime pointer \u2014 using the newest slot on disk\\n'); break; } }
|
|
357
|
+
}
|
|
358
|
+
if (!target && FALLBACK_CLI && existsSync(FALLBACK_CLI)) target = FALLBACK_CLI;
|
|
359
|
+
if (!target) { process.stderr.write('[vo-mcp-launcher] no runner runtime slot and no fallback cli\\n'); process.exit(2); }
|
|
360
|
+
process.stderr.write(\`[vo-mcp-launcher] \${target}\\n\`);
|
|
361
|
+
await import(pathToFileURL(target).href);
|
|
362
|
+
`;
|
|
363
|
+
}
|
|
364
|
+
function writeMcpLauncher(runtimeRoot) {
|
|
365
|
+
mkdirSync2(runtimeRoot, { recursive: true });
|
|
366
|
+
const target = mcpLauncherPath(runtimeRoot);
|
|
367
|
+
const content = renderMcpLauncher();
|
|
368
|
+
if (!existsSync3(target) || readFileSync3(target, "utf8") !== content) writeFileSync3(target, content, "utf8");
|
|
369
|
+
return target;
|
|
370
|
+
}
|
|
371
|
+
function samePath(a, b, platform4 = osPlatform()) {
|
|
372
|
+
const pathModule = platform4 === "win32" ? pathWin32 : pathPosix;
|
|
373
|
+
const norm = (p) => {
|
|
374
|
+
const slashed = pathModule.normalize(p).replace(/\\/g, "/").replace(/\/+$/u, "");
|
|
375
|
+
return platform4 === "win32" || platform4 === "darwin" ? slashed.toLowerCase() : slashed;
|
|
376
|
+
};
|
|
377
|
+
return norm(a) === norm(b);
|
|
378
|
+
}
|
|
379
|
+
function isNotLauncherEntry(entry, launcherPath, platform4) {
|
|
380
|
+
const first = entry?.args && entry.args.length > 0 ? String(entry.args[0]) : "";
|
|
381
|
+
return !first || !samePath(first, launcherPath, platform4);
|
|
382
|
+
}
|
|
383
|
+
function isStaleVoMcpEntry(entry, launcherPath, fallbackCli, platform4) {
|
|
384
|
+
if (!entry || isNotLauncherEntry(entry, launcherPath, platform4)) return true;
|
|
385
|
+
const recorded = String(entry.env?.[MCP_FALLBACK_CLI_ENV] ?? "").trim();
|
|
386
|
+
if (!fallbackCli) return recorded !== "";
|
|
387
|
+
return !recorded || !samePath(recorded, fallbackCli, platform4);
|
|
388
|
+
}
|
|
389
|
+
function isSlotCli(cliPath, runtimeRoot = null) {
|
|
390
|
+
if (runtimeRoot) {
|
|
391
|
+
const prefix = normalize(join3(runtimeRoot, "slots")) + sep;
|
|
392
|
+
const candidate = normalize(cliPath);
|
|
393
|
+
const under = process.platform === "win32" ? candidate.toLowerCase().startsWith(prefix.toLowerCase()) : candidate.startsWith(prefix);
|
|
394
|
+
return under && /^vo-mcp-[0-9A-Za-z._-]{1,96}$/u.test(candidate.slice(prefix.length).split(/[\\/]/u)[0] ?? "");
|
|
395
|
+
}
|
|
396
|
+
return /[\\/]slots[\\/]vo-mcp-[0-9A-Za-z._-]{1,96}[\\/]/iu.test(cliPath);
|
|
397
|
+
}
|
|
398
|
+
function chooseFallbackCli(existing, cliPath, sticky, runtimeRoot = null) {
|
|
399
|
+
const current = String(existing ?? "").trim();
|
|
400
|
+
if (sticky && current && !isSlotCli(current, runtimeRoot) && existsSync3(current)) return current;
|
|
401
|
+
return isSlotCli(cliPath, runtimeRoot) ? null : cliPath;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// src/codex-mcp-config.ts
|
|
203
405
|
var MANAGED_BEGIN = "# BEGIN AlgoHQ MCP (managed by vo-mcp install)";
|
|
204
406
|
var MANAGED_END = "# END AlgoHQ MCP (managed by vo-mcp install)";
|
|
205
407
|
var MANAGED_SERVER_NAMES = /* @__PURE__ */ new Set(["algohq", "vo", "vo-mcp", "vo_mcp"]);
|
|
206
408
|
function resolveCodexConfigPath(home) {
|
|
207
|
-
return
|
|
409
|
+
return join4(home, ".codex", "config.toml");
|
|
208
410
|
}
|
|
209
411
|
function normalizeKey(value) {
|
|
210
412
|
const trimmed = value.trim();
|
|
@@ -260,7 +462,27 @@ function preservedSectionLines(lines, section, managedKeys) {
|
|
|
260
462
|
return key === null || !managedKeys.has(key);
|
|
261
463
|
}).filter((line, index, all) => line.trim() !== "" || index > 0 && index < all.length - 1);
|
|
262
464
|
}
|
|
263
|
-
function
|
|
465
|
+
function readCodexManagedEnv(configPath, key) {
|
|
466
|
+
if (!existsSync4(configPath)) return void 0;
|
|
467
|
+
const raw = readFileSync4(configPath, "utf8");
|
|
468
|
+
const lines = raw.split(/\r?\n/);
|
|
469
|
+
const sections = tableSections(lines);
|
|
470
|
+
const envSection = sections.find((section) => isManagedSection(section) && section.path[2] === "env");
|
|
471
|
+
if (!envSection) return void 0;
|
|
472
|
+
for (const line of lines.slice(envSection.start + 1, envSection.end)) {
|
|
473
|
+
if (assignmentKey(line) !== key) continue;
|
|
474
|
+
const match = /=\s*("(?:[^"\\]|\\.)*")\s*(?:#.*)?$/.exec(line);
|
|
475
|
+
if (!match?.[1]) return void 0;
|
|
476
|
+
try {
|
|
477
|
+
const value = JSON.parse(match[1]);
|
|
478
|
+
return typeof value === "string" ? value : void 0;
|
|
479
|
+
} catch {
|
|
480
|
+
return void 0;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return void 0;
|
|
484
|
+
}
|
|
485
|
+
function renderCodexMcpConfig(raw, cliPath, controlPlaneUrl, managedEnv = {}) {
|
|
264
486
|
if (!isStructurallySafeToml(raw)) {
|
|
265
487
|
throw new Error("Codex config is malformed; refusing to overwrite it");
|
|
266
488
|
}
|
|
@@ -272,12 +494,10 @@ function renderCodexMcpConfig(raw, cliPath, controlPlaneUrl) {
|
|
|
272
494
|
const preferredName = preferredRoot?.path[1];
|
|
273
495
|
const envSection = sections.find((section) => isManagedSection(section) && section.path[1] === preferredName && section.path[2] === "env");
|
|
274
496
|
const rootExtras = preservedSectionLines(lines, preferredRoot, /* @__PURE__ */ new Set(["command", "args", "required"]));
|
|
275
|
-
const envExtras = preservedSectionLines(lines, envSection, /* @__PURE__ */ new Set(["VO_CONTROL_PLANE_URL"]));
|
|
497
|
+
const envExtras = preservedSectionLines(lines, envSection, /* @__PURE__ */ new Set(["VO_CONTROL_PLANE_URL", MCP_FALLBACK_CLI_ENV, ...Object.keys(managedEnv)]));
|
|
276
498
|
const removed = /* @__PURE__ */ new Set();
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
if (managedBegin >= 0 && managedEnd >= managedBegin) {
|
|
280
|
-
for (let index = managedBegin; index <= managedEnd; index += 1) removed.add(index);
|
|
499
|
+
for (const [index, line] of lines.entries()) {
|
|
500
|
+
if (line.trim() === MANAGED_BEGIN || line.trim() === MANAGED_END) removed.add(index);
|
|
281
501
|
}
|
|
282
502
|
for (const section of sections.filter(isManagedSection)) {
|
|
283
503
|
for (let index = section.start; index < section.end; index += 1) removed.add(index);
|
|
@@ -293,43 +513,43 @@ function renderCodexMcpConfig(raw, cliPath, controlPlaneUrl) {
|
|
|
293
513
|
"",
|
|
294
514
|
"[mcp_servers.algohq.env]",
|
|
295
515
|
`VO_CONTROL_PLANE_URL = ${tomlString(controlPlaneUrl)}`,
|
|
516
|
+
...Object.entries(managedEnv).map(([key, value]) => `${key} = ${tomlString(value)}`),
|
|
296
517
|
...envExtras,
|
|
297
518
|
MANAGED_END
|
|
298
519
|
].join(eol);
|
|
299
520
|
return `${base}${base ? `${eol}${eol}` : ""}${block}${eol}`;
|
|
300
521
|
}
|
|
301
|
-
function installCodexMcpConfigAt(configPath, cliPath, controlPlaneUrl, log) {
|
|
302
|
-
const exists =
|
|
303
|
-
const
|
|
522
|
+
function installCodexMcpConfigAt(configPath, cliPath, controlPlaneUrl, log, managedEnv = {}) {
|
|
523
|
+
const exists = existsSync4(configPath);
|
|
524
|
+
const readMtime = exists ? statSync2(configPath).mtimeMs : null;
|
|
525
|
+
const raw = exists ? readFileSync4(configPath, "utf8") : "";
|
|
304
526
|
let rendered;
|
|
305
527
|
try {
|
|
306
|
-
rendered = renderCodexMcpConfig(raw, cliPath, controlPlaneUrl);
|
|
528
|
+
rendered = renderCodexMcpConfig(raw, cliPath, controlPlaneUrl, managedEnv);
|
|
307
529
|
} catch (error) {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
copyFileSync(configPath, backupPath);
|
|
311
|
-
log(` Backed up malformed Codex config \u2192 ${backupPath}`);
|
|
312
|
-
}
|
|
530
|
+
const backupPath2 = backupConfigOnce(configPath);
|
|
531
|
+
if (backupPath2) log(` Backed up malformed Codex config \u2192 ${backupPath2}`);
|
|
313
532
|
throw error;
|
|
314
533
|
}
|
|
315
534
|
if (rendered === raw) {
|
|
316
535
|
log(` Codex already current: ${configPath}`);
|
|
317
536
|
return;
|
|
318
537
|
}
|
|
319
|
-
if (
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
log(` Backed up Codex config \u2192 ${backupPath}`);
|
|
538
|
+
if (readMtime !== null && (!existsSync4(configPath) || statSync2(configPath).mtimeMs !== readMtime)) {
|
|
539
|
+
log(` \u26A0 Codex config changed while updating \u2014 left untouched this time: ${configPath}`);
|
|
540
|
+
return;
|
|
323
541
|
}
|
|
324
|
-
|
|
325
|
-
|
|
542
|
+
const backupPath = backupConfigOnce(configPath);
|
|
543
|
+
if (backupPath) log(` Backed up Codex config \u2192 ${backupPath}`);
|
|
544
|
+
mkdirSync3(dirname3(configPath), { recursive: true });
|
|
545
|
+
writeFileAtomic(configPath, rendered);
|
|
326
546
|
log(`\u2713 Wrote AlgoHQ MCP to Codex: ${configPath}`);
|
|
327
547
|
}
|
|
328
548
|
|
|
329
549
|
// src/autostart.ts
|
|
330
|
-
import { homedir as
|
|
331
|
-
import { isAbsolute, join as
|
|
332
|
-
import { existsSync as
|
|
550
|
+
import { homedir as homedir3, platform as platform2 } from "node:os";
|
|
551
|
+
import { isAbsolute as isAbsolute2, join as join5 } from "node:path";
|
|
552
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, readFileSync as readFileSync5, unlinkSync as unlinkSync2, copyFileSync as copyFileSync2 } from "node:fs";
|
|
333
553
|
var WINDOWS_RESTART_BACKOFF_MS = 1e4;
|
|
334
554
|
var WINDOWS_HEALTHY_RUN_MS = 6e4;
|
|
335
555
|
var WINDOWS_MAX_BACKOFF_MS = 3e5;
|
|
@@ -344,25 +564,25 @@ function quotePosixShellArgument(value) {
|
|
|
344
564
|
}
|
|
345
565
|
function resolveLinuxConfigHome(home, env) {
|
|
346
566
|
const configured = env["XDG_CONFIG_HOME"]?.trim();
|
|
347
|
-
return configured &&
|
|
567
|
+
return configured && isAbsolute2(configured) ? configured : join5(home, ".config");
|
|
348
568
|
}
|
|
349
569
|
function launcherIsCurrent(path, desiredContent, label, log) {
|
|
350
|
-
if (!
|
|
351
|
-
if (
|
|
570
|
+
if (!existsSync5(path)) return false;
|
|
571
|
+
if (readFileSync5(path, "utf8") === desiredContent) return true;
|
|
352
572
|
const backupPath = `${path}.backup-${Date.now()}`;
|
|
353
573
|
copyFileSync2(path, backupPath);
|
|
354
574
|
log(` Backed up existing ${label} to: ${backupPath}`);
|
|
355
575
|
return false;
|
|
356
576
|
}
|
|
357
577
|
function installWindowsAutostart(runnerCommand, log, env) {
|
|
358
|
-
const appData = env["APPDATA"] ??
|
|
359
|
-
const startupDir =
|
|
360
|
-
|
|
361
|
-
const launcherPath =
|
|
362
|
-
const legacyCmdPath =
|
|
363
|
-
if (
|
|
578
|
+
const appData = env["APPDATA"] ?? join5(homedir3(), "AppData", "Roaming");
|
|
579
|
+
const startupDir = join5(appData, "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
|
|
580
|
+
mkdirSync4(startupDir, { recursive: true });
|
|
581
|
+
const launcherPath = join5(startupDir, "vo-runner.vbs");
|
|
582
|
+
const legacyCmdPath = join5(startupDir, "vo-runner.cmd");
|
|
583
|
+
if (existsSync5(legacyCmdPath)) {
|
|
364
584
|
try {
|
|
365
|
-
|
|
585
|
+
unlinkSync2(legacyCmdPath);
|
|
366
586
|
log(` Removed legacy minimized .cmd launcher: ${legacyCmdPath}`);
|
|
367
587
|
} catch (error) {
|
|
368
588
|
log(`\u26A0 Could not remove legacy launcher ${legacyCmdPath} (${error instanceof Error ? error.message : String(error)}); remove it manually to avoid a double start`);
|
|
@@ -403,16 +623,16 @@ Loop
|
|
|
403
623
|
log(` Path: ${launcherPath}`);
|
|
404
624
|
return;
|
|
405
625
|
}
|
|
406
|
-
|
|
626
|
+
writeFileSync4(launcherPath, launcherContent, "utf8");
|
|
407
627
|
log(`\u2713 Installed Windows auto-start launcher`);
|
|
408
628
|
log(` Path: ${launcherPath}`);
|
|
409
629
|
log(` The runner will start hidden at next login.`);
|
|
410
630
|
}
|
|
411
631
|
async function installMacAutostart(runnerCommand, log, env) {
|
|
412
|
-
const home = env["HOME"]?.trim() ||
|
|
413
|
-
const launchAgentsDir =
|
|
414
|
-
|
|
415
|
-
const plistPath =
|
|
632
|
+
const home = env["HOME"]?.trim() || homedir3();
|
|
633
|
+
const launchAgentsDir = join5(home, "Library", "LaunchAgents");
|
|
634
|
+
mkdirSync4(launchAgentsDir, { recursive: true });
|
|
635
|
+
const plistPath = join5(launchAgentsDir, "ai.algosuite.vo-runner.plist");
|
|
416
636
|
const parts = runnerCommand.split(/\s+/);
|
|
417
637
|
const program = parts[0] ?? "vo-mcp";
|
|
418
638
|
const args = parts.length > 1 ? parts.slice(1) : ["runner"];
|
|
@@ -436,12 +656,12 @@ ${args.map((a) => ` <string>${a}</string>`).join("\n")}
|
|
|
436
656
|
<key>EnvironmentVariables</key>
|
|
437
657
|
<dict>
|
|
438
658
|
<key>VO_CODE_RUNNER_CLONES_ROOT</key>
|
|
439
|
-
<string>${
|
|
659
|
+
<string>${join5(home, "Library", "Application Support", "ai.algosuite.vo-runner", "clones")}</string>
|
|
440
660
|
</dict>
|
|
441
661
|
<key>StandardOutPath</key>
|
|
442
|
-
<string>${
|
|
662
|
+
<string>${join5(home, ".claude", "vo-runner.log")}</string>
|
|
443
663
|
<key>StandardErrorPath</key>
|
|
444
|
-
<string>${
|
|
664
|
+
<string>${join5(home, ".claude", "vo-runner-error.log")}</string>
|
|
445
665
|
</dict>
|
|
446
666
|
</plist>
|
|
447
667
|
`;
|
|
@@ -450,29 +670,29 @@ ${args.map((a) => ` <string>${a}</string>`).join("\n")}
|
|
|
450
670
|
log(` Path: ${plistPath}`);
|
|
451
671
|
return;
|
|
452
672
|
}
|
|
453
|
-
|
|
673
|
+
writeFileSync4(plistPath, plistContent, "utf8");
|
|
454
674
|
log(`\u2713 Installed launchd plist`);
|
|
455
675
|
log(` Path: ${plistPath}`);
|
|
456
676
|
try {
|
|
457
677
|
const { execSync } = await import("node:child_process");
|
|
458
678
|
execSync(`launchctl load "${plistPath}"`, { stdio: "ignore" });
|
|
459
679
|
log(`\u2713 Loaded plist with launchctl (runner will start at next login)`);
|
|
460
|
-
log(` Logs: ${
|
|
680
|
+
log(` Logs: ${join5(home, ".claude", "vo-runner.log")}`);
|
|
461
681
|
} catch {
|
|
462
682
|
log(`\u26A0 Failed to load plist with launchctl (you may need to load it manually)`);
|
|
463
683
|
log(` Run: launchctl load "${plistPath}"`);
|
|
464
684
|
}
|
|
465
685
|
}
|
|
466
686
|
async function installLinuxAutostart(runnerCommand, log, env) {
|
|
467
|
-
const home = env["HOME"]?.trim() ||
|
|
687
|
+
const home = env["HOME"]?.trim() || homedir3();
|
|
468
688
|
const configHome = resolveLinuxConfigHome(home, env);
|
|
469
|
-
const unitDir =
|
|
470
|
-
|
|
471
|
-
const unitPath =
|
|
472
|
-
const logFile =
|
|
473
|
-
const errFile =
|
|
689
|
+
const unitDir = join5(configHome, "systemd", "user");
|
|
690
|
+
mkdirSync4(unitDir, { recursive: true });
|
|
691
|
+
const unitPath = join5(unitDir, "vo-runner.service");
|
|
692
|
+
const logFile = join5(home, ".claude", "vo-runner.log");
|
|
693
|
+
const errFile = join5(home, ".claude", "vo-runner-error.log");
|
|
474
694
|
const quotedRunnerCommand = quotePosixShellArgument(runnerCommand);
|
|
475
|
-
|
|
695
|
+
mkdirSync4(join5(home, ".claude"), { recursive: true });
|
|
476
696
|
const unit = `[Unit]
|
|
477
697
|
Description=AlgoHQ Code Runner (vo-mcp)
|
|
478
698
|
After=network-online.target
|
|
@@ -481,7 +701,7 @@ Wants=network-online.target
|
|
|
481
701
|
[Service]
|
|
482
702
|
Type=simple
|
|
483
703
|
WorkingDirectory=${home}
|
|
484
|
-
Environment="VO_CODE_RUNNER_CLONES_ROOT=${
|
|
704
|
+
Environment="VO_CODE_RUNNER_CLONES_ROOT=${join5(configHome, "ai.algosuite.vo-runner", "clones")}"
|
|
485
705
|
ExecStart=/bin/sh -lc ${quotedRunnerCommand}
|
|
486
706
|
Restart=on-failure
|
|
487
707
|
RestartSec=10
|
|
@@ -496,7 +716,7 @@ WantedBy=default.target
|
|
|
496
716
|
log(` Path: ${unitPath}`);
|
|
497
717
|
return;
|
|
498
718
|
}
|
|
499
|
-
|
|
719
|
+
writeFileSync4(unitPath, unit, "utf8");
|
|
500
720
|
log(`\u2713 Installed systemd user unit`);
|
|
501
721
|
log(` Path: ${unitPath}`);
|
|
502
722
|
if (process.env["VITEST"]) {
|
|
@@ -534,73 +754,142 @@ async function installAutostart(opts = {}) {
|
|
|
534
754
|
// src/install.ts
|
|
535
755
|
var DEFAULT_CONTROL_PLANE_URL2 = "https://vo-control-plane-bzjphrajaq-uc.a.run.app";
|
|
536
756
|
function resolveCodeConfigPath(home) {
|
|
537
|
-
return
|
|
757
|
+
return join6(home, ".claude.json");
|
|
538
758
|
}
|
|
539
759
|
function resolveDesktopConfigPath(home, plat, appData) {
|
|
540
760
|
if (plat === "win32") {
|
|
541
|
-
return
|
|
761
|
+
return join6(appData ?? join6(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
542
762
|
}
|
|
543
763
|
if (plat === "darwin") {
|
|
544
|
-
return
|
|
764
|
+
return join6(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
545
765
|
}
|
|
546
|
-
return
|
|
766
|
+
return join6(home, ".config", "Claude", "claude_desktop_config.json");
|
|
547
767
|
}
|
|
548
768
|
function readClaudeConfig(path) {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
769
|
+
if (!existsSync6(path)) return { kind: "absent", config: {}, mtimeMs: null };
|
|
770
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
771
|
+
const before = statSync3(path).mtimeMs;
|
|
772
|
+
let raw;
|
|
773
|
+
try {
|
|
774
|
+
raw = readFileSync6(path, "utf8");
|
|
775
|
+
} catch {
|
|
776
|
+
return { kind: "invalid", config: {}, mtimeMs: before };
|
|
777
|
+
}
|
|
778
|
+
if (!existsSync6(path) || statSync3(path).mtimeMs !== before) continue;
|
|
779
|
+
const text = raw.replace(/^\uFEFF/u, "");
|
|
780
|
+
if (!text.trim()) return { kind: "empty", config: {}, mtimeMs: before };
|
|
781
|
+
try {
|
|
782
|
+
const parsed = JSON.parse(text);
|
|
783
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? { kind: "ok", config: parsed, mtimeMs: before } : { kind: "invalid", config: {}, mtimeMs: before };
|
|
784
|
+
} catch {
|
|
785
|
+
return { kind: "invalid", config: {}, mtimeMs: before };
|
|
786
|
+
}
|
|
556
787
|
}
|
|
788
|
+
return { kind: "invalid", config: {}, mtimeMs: null };
|
|
557
789
|
}
|
|
558
790
|
function writeClaudeConfig(path, config) {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
791
|
+
mkdirSync5(dirname4(path), { recursive: true });
|
|
792
|
+
writeFileAtomic(path, `${JSON.stringify(config, null, 2)}
|
|
793
|
+
`);
|
|
562
794
|
}
|
|
563
|
-
function
|
|
564
|
-
|
|
795
|
+
function carriedEntryKeys(entry) {
|
|
796
|
+
if (!entry) return {};
|
|
797
|
+
const { command: _c, args: _a, env: _e, type, url: _u, headers: _h, ...rest } = entry;
|
|
798
|
+
const stdio = type === void 0 || type === "stdio";
|
|
799
|
+
return { ...stdio ? rest : {}, ...type === "stdio" ? { type } : {} };
|
|
565
800
|
}
|
|
566
|
-
function
|
|
567
|
-
const
|
|
801
|
+
function preferredNodeCommand(existing) {
|
|
802
|
+
const current = String(existing ?? "").trim();
|
|
803
|
+
return current && /(^|[\\/])node(\.exe)?$/iu.test(current) ? current : "node";
|
|
804
|
+
}
|
|
805
|
+
function resolveVoMcpCliPath() {
|
|
806
|
+
return join6(dirname4(fileURLToPath(import.meta.url)), "cli.js");
|
|
807
|
+
}
|
|
808
|
+
var INSTALL_LAUNCHER = { sticky: false, onlyExisting: false };
|
|
809
|
+
var HEAL_LAUNCHER = { sticky: true, onlyExisting: true };
|
|
810
|
+
function installMcpConfigAt(configPath, cliPath, controlPlaneUrl, log, label, launcher = { launcherPath: null, ...INSTALL_LAUNCHER }) {
|
|
811
|
+
if (launcher.onlyExisting && !existsSync6(configPath)) return;
|
|
812
|
+
const read = readClaudeConfig(configPath);
|
|
813
|
+
if (read.kind === "invalid" || read.kind === "empty" && launcher.onlyExisting) {
|
|
814
|
+
log(` \u26A0 ${label} config is ${read.kind === "empty" ? "empty" : "not valid JSON"} \u2014 left untouched: ${configPath}${read.kind === "empty" ? "" : " (fix or remove it, then re-run vo-mcp install)"}`);
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
const existing = read.config;
|
|
818
|
+
const readMtime = read.mtimeMs;
|
|
568
819
|
const mcpServers = existing["mcpServers"] && typeof existing["mcpServers"] === "object" ? existing["mcpServers"] : {};
|
|
569
|
-
const
|
|
570
|
-
|
|
820
|
+
const managedEntry = mcpServers["vo-mcp"];
|
|
821
|
+
const voEntry = managedEntry ?? mcpServers["vo"];
|
|
822
|
+
const { launcherPath } = launcher;
|
|
823
|
+
const fallbackCli = chooseFallbackCli(voEntry?.env?.[MCP_FALLBACK_CLI_ENV], cliPath, launcher.sticky, launcherPath ? dirname4(launcherPath) : null);
|
|
824
|
+
const current = launcherPath ? !isStaleVoMcpEntry(voEntry, launcherPath, fallbackCli) : Boolean(voEntry?.args?.some((a) => a.includes(cliPath)));
|
|
825
|
+
if (current) {
|
|
571
826
|
log(` ${label} already current: ${configPath}`);
|
|
572
827
|
return;
|
|
573
828
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
log(` Backed up ${label} config \u2192 ${backupPath}`);
|
|
578
|
-
}
|
|
829
|
+
const backupPath = backupConfigOnce(configPath);
|
|
830
|
+
if (backupPath) log(` Backed up ${label} config \u2192 ${backupPath}`);
|
|
831
|
+
const { [MCP_FALLBACK_CLI_ENV]: _previousFallback, ...preservedEnv } = voEntry?.env ?? {};
|
|
579
832
|
const merged = {
|
|
580
833
|
...existing,
|
|
581
834
|
mcpServers: {
|
|
582
835
|
...mcpServers,
|
|
583
836
|
"vo-mcp": {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
837
|
+
...carriedEntryKeys(managedEntry),
|
|
838
|
+
command: preferredNodeCommand(managedEntry?.command),
|
|
839
|
+
args: [launcherPath ?? cliPath],
|
|
840
|
+
env: {
|
|
841
|
+
VO_CONTROL_PLANE_URL: controlPlaneUrl,
|
|
842
|
+
...preservedEnv,
|
|
843
|
+
...launcherPath && fallbackCli ? { [MCP_FALLBACK_CLI_ENV]: fallbackCli } : {}
|
|
844
|
+
}
|
|
589
845
|
}
|
|
590
846
|
}
|
|
591
847
|
};
|
|
848
|
+
if (readMtime !== null && (!existsSync6(configPath) || statSync3(configPath).mtimeMs !== readMtime)) {
|
|
849
|
+
log(` \u26A0 ${label} config changed while updating \u2014 left untouched this time: ${configPath}`);
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
592
852
|
writeClaudeConfig(configPath, merged);
|
|
593
853
|
log(`\u2713 Wrote vo-mcp to ${label}: ${configPath}`);
|
|
594
854
|
}
|
|
595
|
-
function installMcpConfig(log, env) {
|
|
596
|
-
const home = env["HOME"]?.trim() || env["USERPROFILE"]?.trim() ||
|
|
855
|
+
function installMcpConfig(log, env, mode = "install") {
|
|
856
|
+
const home = env["HOME"]?.trim() || env["USERPROFILE"]?.trim() || homedir4();
|
|
597
857
|
const appData = env["APPDATA"]?.trim();
|
|
598
858
|
const plat = platform3();
|
|
599
859
|
const cliPath = resolveVoMcpCliPath();
|
|
600
860
|
const controlPlaneUrl = env["VO_CONTROL_PLANE_URL"]?.trim() || DEFAULT_CONTROL_PLANE_URL2;
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
861
|
+
const launcherPath = writeMcpLauncherForEnv(env, log, { platform: plat, home });
|
|
862
|
+
const launcher = { launcherPath, ...mode === "heal" ? HEAL_LAUNCHER : INSTALL_LAUNCHER };
|
|
863
|
+
const leg = (label, run) => {
|
|
864
|
+
try {
|
|
865
|
+
run();
|
|
866
|
+
} catch (err) {
|
|
867
|
+
if (mode !== "heal") throw err;
|
|
868
|
+
log(` \u26A0 ${label}: could not update the MCP registration \u2014 ${err instanceof Error ? err.message : String(err)}`);
|
|
869
|
+
}
|
|
870
|
+
};
|
|
871
|
+
leg("Claude Code CLI", () => installMcpConfigAt(resolveCodeConfigPath(home), cliPath, controlPlaneUrl, log, "Claude Code CLI", launcher));
|
|
872
|
+
leg("Claude Desktop", () => installMcpConfigAt(resolveDesktopConfigPath(home, plat, appData), cliPath, controlPlaneUrl, log, "Claude Desktop", launcher));
|
|
873
|
+
const codexPath = resolveCodexConfigPath(home);
|
|
874
|
+
if (launcher.onlyExisting && !existsSync6(codexPath)) return;
|
|
875
|
+
leg("Codex", () => {
|
|
876
|
+
if (launcherPath) {
|
|
877
|
+
const fallbackCli = chooseFallbackCli(readCodexManagedEnv(codexPath, MCP_FALLBACK_CLI_ENV), cliPath, launcher.sticky, dirname4(launcherPath));
|
|
878
|
+
installCodexMcpConfigAt(codexPath, launcherPath, controlPlaneUrl, log, fallbackCli ? { [MCP_FALLBACK_CLI_ENV]: fallbackCli } : {});
|
|
879
|
+
} else {
|
|
880
|
+
installCodexMcpConfigAt(codexPath, cliPath, controlPlaneUrl, log);
|
|
881
|
+
}
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
function writeMcpLauncherForEnv(env, log, hint) {
|
|
885
|
+
try {
|
|
886
|
+
const root = defaultRunnerRuntimeRoot(env, hint);
|
|
887
|
+
if (!root) return null;
|
|
888
|
+
return writeMcpLauncher(root);
|
|
889
|
+
} catch (err) {
|
|
890
|
+
log(` \u26A0 could not write the vo-mcp launcher (registering cli.js directly): ${err instanceof Error ? err.message : String(err)}`);
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
604
893
|
}
|
|
605
894
|
async function runPairFlow(log, env) {
|
|
606
895
|
log("\n\u2501\u2501\u2501 Step 2: Link your AlgoHQ account (device code) \u2501\u2501\u2501");
|