@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.
@@ -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 homedir3, platform as platform3 } from "node:os";
6
- import { join as join4, dirname as dirname3 } from "node:path";
7
- import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, copyFileSync as copyFileSync3 } from "node:fs";
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 { copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
202
- import { dirname as dirname2, join as join2 } from "node:path";
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 join2(home, ".codex", "config.toml");
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 renderCodexMcpConfig(raw, cliPath, controlPlaneUrl) {
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 managedBegin = lines.findIndex((line) => line.trim() === MANAGED_BEGIN);
278
- const managedEnd = lines.findIndex((line) => line.trim() === MANAGED_END);
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 = existsSync2(configPath);
303
- const raw = exists ? readFileSync2(configPath, "utf8") : "";
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
- if (exists) {
309
- const backupPath = `${configPath}.backup-${Date.now()}`;
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 (exists) {
320
- const backupPath = `${configPath}.backup-${Date.now()}`;
321
- copyFileSync(configPath, backupPath);
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
- mkdirSync2(dirname2(configPath), { recursive: true });
325
- writeFileSync2(configPath, rendered, "utf8");
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 homedir2, platform as platform2 } from "node:os";
331
- import { isAbsolute, join as join3 } from "node:path";
332
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, readFileSync as readFileSync3, unlinkSync, copyFileSync as copyFileSync2 } from "node:fs";
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 && isAbsolute(configured) ? configured : join3(home, ".config");
567
+ return configured && isAbsolute2(configured) ? configured : join5(home, ".config");
348
568
  }
349
569
  function launcherIsCurrent(path, desiredContent, label, log) {
350
- if (!existsSync3(path)) return false;
351
- if (readFileSync3(path, "utf8") === desiredContent) return true;
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"] ?? join3(homedir2(), "AppData", "Roaming");
359
- const startupDir = join3(appData, "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
360
- mkdirSync3(startupDir, { recursive: true });
361
- const launcherPath = join3(startupDir, "vo-runner.vbs");
362
- const legacyCmdPath = join3(startupDir, "vo-runner.cmd");
363
- if (existsSync3(legacyCmdPath)) {
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
- unlinkSync(legacyCmdPath);
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
- writeFileSync3(launcherPath, launcherContent, "utf8");
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() || homedir2();
413
- const launchAgentsDir = join3(home, "Library", "LaunchAgents");
414
- mkdirSync3(launchAgentsDir, { recursive: true });
415
- const plistPath = join3(launchAgentsDir, "ai.algosuite.vo-runner.plist");
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>${join3(home, "Library", "Application Support", "ai.algosuite.vo-runner", "clones")}</string>
659
+ <string>${join5(home, "Library", "Application Support", "ai.algosuite.vo-runner", "clones")}</string>
440
660
  </dict>
441
661
  <key>StandardOutPath</key>
442
- <string>${join3(home, ".claude", "vo-runner.log")}</string>
662
+ <string>${join5(home, ".claude", "vo-runner.log")}</string>
443
663
  <key>StandardErrorPath</key>
444
- <string>${join3(home, ".claude", "vo-runner-error.log")}</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
- writeFileSync3(plistPath, plistContent, "utf8");
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: ${join3(home, ".claude", "vo-runner.log")}`);
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() || homedir2();
687
+ const home = env["HOME"]?.trim() || homedir3();
468
688
  const configHome = resolveLinuxConfigHome(home, env);
469
- const unitDir = join3(configHome, "systemd", "user");
470
- mkdirSync3(unitDir, { recursive: true });
471
- const unitPath = join3(unitDir, "vo-runner.service");
472
- const logFile = join3(home, ".claude", "vo-runner.log");
473
- const errFile = join3(home, ".claude", "vo-runner-error.log");
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
- mkdirSync3(join3(home, ".claude"), { recursive: true });
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=${join3(configHome, "ai.algosuite.vo-runner", "clones")}"
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
- writeFileSync3(unitPath, unit, "utf8");
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 join4(home, ".claude.json");
757
+ return join6(home, ".claude.json");
538
758
  }
539
759
  function resolveDesktopConfigPath(home, plat, appData) {
540
760
  if (plat === "win32") {
541
- return join4(appData ?? join4(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
761
+ return join6(appData ?? join6(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
542
762
  }
543
763
  if (plat === "darwin") {
544
- return join4(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
764
+ return join6(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
545
765
  }
546
- return join4(home, ".config", "Claude", "claude_desktop_config.json");
766
+ return join6(home, ".config", "Claude", "claude_desktop_config.json");
547
767
  }
548
768
  function readClaudeConfig(path) {
549
- try {
550
- if (!existsSync4(path)) return {};
551
- const raw = readFileSync4(path, "utf8");
552
- const parsed = JSON.parse(raw);
553
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
554
- } catch {
555
- return {};
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
- mkdirSync4(dirname3(path), { recursive: true });
560
- writeFileSync4(path, `${JSON.stringify(config, null, 2)}
561
- `, "utf8");
791
+ mkdirSync5(dirname4(path), { recursive: true });
792
+ writeFileAtomic(path, `${JSON.stringify(config, null, 2)}
793
+ `);
562
794
  }
563
- function resolveVoMcpCliPath() {
564
- return join4(dirname3(fileURLToPath(import.meta.url)), "cli.js");
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 installMcpConfigAt(configPath, cliPath, controlPlaneUrl, log, label) {
567
- const existing = readClaudeConfig(configPath);
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 voEntry = mcpServers["vo"] ?? mcpServers["vo-mcp"];
570
- if (voEntry?.args?.some((a) => a.includes(cliPath))) {
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
- if (existsSync4(configPath)) {
575
- const backupPath = `${configPath}.backup-${Date.now()}`;
576
- copyFileSync3(configPath, backupPath);
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
- command: "node",
585
- args: [cliPath],
586
- // Preserve any existing env the user added (e.g. model API keys) — only
587
- // ensure VO_CONTROL_PLANE_URL is present. NEVER drop the user's env keys.
588
- env: { VO_CONTROL_PLANE_URL: controlPlaneUrl, ...voEntry?.env ?? {} }
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() || homedir3();
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
- installMcpConfigAt(resolveCodeConfigPath(home), cliPath, controlPlaneUrl, log, "Claude Code CLI");
602
- installMcpConfigAt(resolveDesktopConfigPath(home, plat, appData), cliPath, controlPlaneUrl, log, "Claude Desktop");
603
- installCodexMcpConfigAt(resolveCodexConfigPath(home), cliPath, controlPlaneUrl, log);
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");