@algosuite/vo-mcp 0.2.0-beta.7 → 0.2.0-beta.70

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 join3, dirname as dirname2 } from "node:path";
7
- import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, copyFileSync as copyFileSync2 } 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
@@ -197,54 +197,482 @@ async function runPairing(deps = {}) {
197
197
  }
198
198
  }
199
199
 
200
+ // src/codex-mcp-config.ts
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
405
+ var MANAGED_BEGIN = "# BEGIN AlgoHQ MCP (managed by vo-mcp install)";
406
+ var MANAGED_END = "# END AlgoHQ MCP (managed by vo-mcp install)";
407
+ var MANAGED_SERVER_NAMES = /* @__PURE__ */ new Set(["algohq", "vo", "vo-mcp", "vo_mcp"]);
408
+ function resolveCodexConfigPath(home) {
409
+ return join4(home, ".codex", "config.toml");
410
+ }
411
+ function normalizeKey(value) {
412
+ const trimmed = value.trim();
413
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
414
+ return trimmed.slice(1, -1);
415
+ }
416
+ return trimmed;
417
+ }
418
+ function tablePath(line) {
419
+ const match = /^\s*\[([^\r\n]+)\]\s*(?:#.*)?$/.exec(line);
420
+ const rawPath = match?.[1];
421
+ if (!rawPath || rawPath.includes("[") || rawPath.includes("]")) return null;
422
+ return rawPath.split(".").map(normalizeKey);
423
+ }
424
+ function tableSections(lines) {
425
+ const starts = [];
426
+ for (let index = 0; index < lines.length; index += 1) {
427
+ const path = tablePath(lines[index] ?? "");
428
+ if (path) starts.push({ path, start: index });
429
+ }
430
+ return starts.map((section, index) => ({
431
+ ...section,
432
+ end: starts[index + 1]?.start ?? lines.length
433
+ }));
434
+ }
435
+ function isManagedSection(section) {
436
+ return section.path[0] === "mcp_servers" && MANAGED_SERVER_NAMES.has(section.path[1] ?? "");
437
+ }
438
+ function assignmentKey(line) {
439
+ const match = /^\s*((?:[A-Za-z0-9_-]+)|(?:"[^"]+")|(?:'[^']+'))\s*=/.exec(line);
440
+ return match?.[1] ? normalizeKey(match[1]) : null;
441
+ }
442
+ function isStructurallySafeToml(raw) {
443
+ const lines = raw.split(/\r?\n/);
444
+ const beginIndexes = lines.flatMap((line, index) => line.trim() === MANAGED_BEGIN ? [index] : []);
445
+ const endIndexes = lines.flatMap((line, index) => line.trim() === MANAGED_END ? [index] : []);
446
+ if (beginIndexes.length !== endIndexes.length || beginIndexes.length > 1) return false;
447
+ if (beginIndexes[0] !== void 0 && (endIndexes[0] ?? -1) <= beginIndexes[0]) return false;
448
+ for (const line of lines) {
449
+ const trimmed = line.trim();
450
+ if (/^\[\[?mcp_servers(?:\.|\s|$)/.test(trimmed) && tablePath(line) === null) return false;
451
+ }
452
+ return true;
453
+ }
454
+ function tomlString(value) {
455
+ return JSON.stringify(value);
456
+ }
457
+ function preservedSectionLines(lines, section, managedKeys) {
458
+ if (!section) return [];
459
+ return lines.slice(section.start + 1, section.end).filter((line) => {
460
+ if (line.trim() === MANAGED_BEGIN || line.trim() === MANAGED_END) return false;
461
+ const key = assignmentKey(line);
462
+ return key === null || !managedKeys.has(key);
463
+ }).filter((line, index, all) => line.trim() !== "" || index > 0 && index < all.length - 1);
464
+ }
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 = {}) {
486
+ if (!isStructurallySafeToml(raw)) {
487
+ throw new Error("Codex config is malformed; refusing to overwrite it");
488
+ }
489
+ const eol = raw.includes("\r\n") ? "\r\n" : "\n";
490
+ const lines = raw.split(/\r?\n/);
491
+ const sections = tableSections(lines);
492
+ const rootSections = sections.filter((section) => isManagedSection(section) && section.path.length === 2);
493
+ const preferredRoot = rootSections.find((section) => section.path[1] === "algohq") ?? rootSections[0];
494
+ const preferredName = preferredRoot?.path[1];
495
+ const envSection = sections.find((section) => isManagedSection(section) && section.path[1] === preferredName && section.path[2] === "env");
496
+ const rootExtras = preservedSectionLines(lines, preferredRoot, /* @__PURE__ */ new Set(["command", "args", "required"]));
497
+ const envExtras = preservedSectionLines(lines, envSection, /* @__PURE__ */ new Set(["VO_CONTROL_PLANE_URL", MCP_FALLBACK_CLI_ENV, ...Object.keys(managedEnv)]));
498
+ const removed = /* @__PURE__ */ new Set();
499
+ for (const [index, line] of lines.entries()) {
500
+ if (line.trim() === MANAGED_BEGIN || line.trim() === MANAGED_END) removed.add(index);
501
+ }
502
+ for (const section of sections.filter(isManagedSection)) {
503
+ for (let index = section.start; index < section.end; index += 1) removed.add(index);
504
+ }
505
+ const base = lines.filter((_line, index) => !removed.has(index)).join(eol).trimEnd();
506
+ const block = [
507
+ MANAGED_BEGIN,
508
+ "[mcp_servers.algohq]",
509
+ 'command = "node"',
510
+ `args = [${tomlString(cliPath)}]`,
511
+ "required = true",
512
+ ...rootExtras,
513
+ "",
514
+ "[mcp_servers.algohq.env]",
515
+ `VO_CONTROL_PLANE_URL = ${tomlString(controlPlaneUrl)}`,
516
+ ...Object.entries(managedEnv).map(([key, value]) => `${key} = ${tomlString(value)}`),
517
+ ...envExtras,
518
+ MANAGED_END
519
+ ].join(eol);
520
+ return `${base}${base ? `${eol}${eol}` : ""}${block}${eol}`;
521
+ }
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") : "";
526
+ let rendered;
527
+ try {
528
+ rendered = renderCodexMcpConfig(raw, cliPath, controlPlaneUrl, managedEnv);
529
+ } catch (error) {
530
+ const backupPath2 = backupConfigOnce(configPath);
531
+ if (backupPath2) log(` Backed up malformed Codex config \u2192 ${backupPath2}`);
532
+ throw error;
533
+ }
534
+ if (rendered === raw) {
535
+ log(` Codex already current: ${configPath}`);
536
+ return;
537
+ }
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;
541
+ }
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);
546
+ log(`\u2713 Wrote AlgoHQ MCP to Codex: ${configPath}`);
547
+ }
548
+
549
+ // src/remote-mcp-entry.ts
550
+ var REMOTE_MCP_SERVER_KEY = "vo-mcp-remote";
551
+ var REMOTE_MCP_TOKEN_ENV = "VO_MCP_REMOTE_TOKEN";
552
+ var REMOTE_MCP_ENDPOINT_PATH = "/api/v1/mcp";
553
+ var REMOTE_MCP_AUTH_HEADER = `Bearer \${${REMOTE_MCP_TOKEN_ENV}}`;
554
+ function remoteMcpOptIn(env) {
555
+ return (env["VO_REMOTE_MCP"] ?? "").trim() === "1";
556
+ }
557
+ function remoteMcpUrl(controlPlaneUrl) {
558
+ return `${controlPlaneUrl.trim().replace(/\/+$/, "")}${REMOTE_MCP_ENDPOINT_PATH}`;
559
+ }
560
+ function buildRemoteMcpEntry(controlPlaneUrl) {
561
+ return {
562
+ type: "http",
563
+ url: remoteMcpUrl(controlPlaneUrl),
564
+ headers: { Authorization: REMOTE_MCP_AUTH_HEADER }
565
+ };
566
+ }
567
+ function asRecord(value) {
568
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
569
+ }
570
+ function isManagedRemoteMcpEntry(value) {
571
+ const entry = asRecord(value);
572
+ if (!entry) return false;
573
+ if (entry["type"] !== "http") return false;
574
+ const url = entry["url"];
575
+ if (typeof url !== "string" || !url.endsWith(REMOTE_MCP_ENDPOINT_PATH)) return false;
576
+ const headers = asRecord(entry["headers"]);
577
+ return headers?.["Authorization"] === REMOTE_MCP_AUTH_HEADER;
578
+ }
579
+ function remoteMcpEntryIsCurrent(value, desired) {
580
+ const entry = asRecord(value);
581
+ if (!entry) return false;
582
+ const headers = asRecord(entry["headers"]);
583
+ return entry["type"] === desired.type && entry["url"] === desired.url && headers?.["Authorization"] === desired.headers["Authorization"];
584
+ }
585
+
200
586
  // src/autostart.ts
201
- import { homedir as homedir2, platform as platform2 } from "node:os";
202
- import { join as join2 } from "node:path";
203
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync2, unlinkSync, copyFileSync } from "node:fs";
587
+ import { homedir as homedir3, platform as platform2 } from "node:os";
588
+ import { isAbsolute as isAbsolute2, join as join5 } from "node:path";
589
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, readFileSync as readFileSync5, unlinkSync as unlinkSync2, copyFileSync as copyFileSync2 } from "node:fs";
590
+ var WINDOWS_RESTART_BACKOFF_MS = 1e4;
591
+ var WINDOWS_HEALTHY_RUN_MS = 6e4;
592
+ var WINDOWS_MAX_BACKOFF_MS = 3e5;
204
593
  function resolveRunnerCommand(override) {
205
594
  return override ?? "vo-mcp runner";
206
595
  }
596
+ function quotePosixShellArgument(value) {
597
+ if (value.includes("\0") || value.includes("\r") || value.includes("\n")) {
598
+ throw new Error("Runner command must not contain NUL, carriage return, or newline characters.");
599
+ }
600
+ return `'${value.replace(/'/gu, `'"'"'`)}'`;
601
+ }
602
+ function resolveLinuxConfigHome(home, env) {
603
+ const configured = env["XDG_CONFIG_HOME"]?.trim();
604
+ return configured && isAbsolute2(configured) ? configured : join5(home, ".config");
605
+ }
606
+ function launcherIsCurrent(path, desiredContent, label, log) {
607
+ if (!existsSync5(path)) return false;
608
+ if (readFileSync5(path, "utf8") === desiredContent) return true;
609
+ const backupPath = `${path}.backup-${Date.now()}`;
610
+ copyFileSync2(path, backupPath);
611
+ log(` Backed up existing ${label} to: ${backupPath}`);
612
+ return false;
613
+ }
207
614
  function installWindowsAutostart(runnerCommand, log, env) {
208
- const appData = env["APPDATA"] ?? join2(homedir2(), "AppData", "Roaming");
209
- const startupDir = join2(appData, "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
210
- mkdirSync2(startupDir, { recursive: true });
211
- const launcherPath = join2(startupDir, "vo-runner.cmd");
212
- if (existsSync2(launcherPath)) {
213
- const existing = readFileSync2(launcherPath, "utf8");
214
- if (existing.includes("vo-mcp runner")) {
215
- log(`\u2713 Auto-start is already configured (Windows Startup folder)`);
216
- log(` Path: ${launcherPath}`);
217
- return;
615
+ const appData = env["APPDATA"] ?? join5(homedir3(), "AppData", "Roaming");
616
+ const startupDir = join5(appData, "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
617
+ mkdirSync4(startupDir, { recursive: true });
618
+ const launcherPath = join5(startupDir, "vo-runner.vbs");
619
+ const legacyCmdPath = join5(startupDir, "vo-runner.cmd");
620
+ if (existsSync5(legacyCmdPath)) {
621
+ try {
622
+ unlinkSync2(legacyCmdPath);
623
+ log(` Removed legacy minimized .cmd launcher: ${legacyCmdPath}`);
624
+ } catch (error) {
625
+ log(`\u26A0 Could not remove legacy launcher ${legacyCmdPath} (${error instanceof Error ? error.message : String(error)}); remove it manually to avoid a double start`);
218
626
  }
219
- const backupPath = `${launcherPath}.backup-${Date.now()}`;
220
- copyFileSync(launcherPath, backupPath);
221
- log(` Backed up existing launcher to: ${backupPath}`);
222
- }
223
- const launcherContent = `@echo off
224
- REM Auto-start launcher for vo-mcp runner
225
- REM Created by vo-mcp autostart installer
226
- start /min cmd /c "${runnerCommand}"
627
+ }
628
+ const runnerConsoleCommand = `cmd /c ${runnerCommand} >> "%USERPROFILE%\\.claude\\vo-runner.log" 2>&1`.replace(/"/g, '""');
629
+ const launcherContent = `' Auto-start launcher for vo-mcp runner
630
+ ' Created by vo-mcp autostart installer
631
+ ' Keepalive supervisor: restarts the runner if it exits (parity with launchd
632
+ ' KeepAlive on macOS and systemd Restart=on-failure on Linux).
633
+ ' Runs the runner in a MINIMIZED (style 7), never a hidden (style 0) window:
634
+ ' hidden script-host exec trips Defender's PowhidSubExec.B heuristic and gets
635
+ ' blocked at logon. To stop: create %USERPROFILE%\\.claude\\vo-runner.stop, or
636
+ ' end wscript.exe.
637
+ Dim sh, fso, stopFile, backoff, startedAt, ranMs
638
+ Set sh = CreateObject("WScript.Shell")
639
+ Set fso = CreateObject("Scripting.FileSystemObject")
640
+ sh.CurrentDirectory = sh.ExpandEnvironmentStrings("%USERPROFILE%")
641
+ sh.Environment("Process")("VO_CODE_RUNNER_CLONES_ROOT") = sh.ExpandEnvironmentStrings("%APPDATA%\\ai.algosuite.vo-runner\\clones")
642
+ stopFile = sh.ExpandEnvironmentStrings("%USERPROFILE%\\.claude\\vo-runner.stop")
643
+ backoff = ${WINDOWS_RESTART_BACKOFF_MS}
644
+ Do
645
+ If fso.FileExists(stopFile) Then
646
+ fso.DeleteFile stopFile
647
+ WScript.Quit 0
648
+ End If
649
+ startedAt = Timer
650
+ sh.Run "${runnerConsoleCommand}", 7, True
651
+ ranMs = (Timer - startedAt) * 1000
652
+ If ranMs < 0 Then ranMs = ${WINDOWS_HEALTHY_RUN_MS}
653
+ If ranMs >= ${WINDOWS_HEALTHY_RUN_MS} Then
654
+ backoff = ${WINDOWS_RESTART_BACKOFF_MS}
655
+ ElseIf backoff < ${WINDOWS_MAX_BACKOFF_MS} Then
656
+ backoff = backoff * 2
657
+ End If
658
+ WScript.Sleep backoff
659
+ Loop
227
660
  `;
228
- writeFileSync2(launcherPath, launcherContent, "utf8");
661
+ if (launcherIsCurrent(launcherPath, launcherContent, "launcher", log)) {
662
+ log(`\u2713 Auto-start is already configured (Windows Startup folder)`);
663
+ log(` Path: ${launcherPath}`);
664
+ return;
665
+ }
666
+ writeFileSync4(launcherPath, launcherContent, "utf8");
229
667
  log(`\u2713 Installed Windows auto-start launcher`);
230
668
  log(` Path: ${launcherPath}`);
231
- log(` The runner will start minimized at next login.`);
232
- }
233
- async function installMacAutostart(runnerCommand, log) {
234
- const launchAgentsDir = join2(homedir2(), "Library", "LaunchAgents");
235
- mkdirSync2(launchAgentsDir, { recursive: true });
236
- const plistPath = join2(launchAgentsDir, "ai.algosuite.vo-runner.plist");
237
- if (existsSync2(plistPath)) {
238
- const existing = readFileSync2(plistPath, "utf8");
239
- if (existing.includes("vo-mcp runner")) {
240
- log(`\u2713 Auto-start is already configured (launchd)`);
241
- log(` Path: ${plistPath}`);
242
- return;
243
- }
244
- const backupPath = `${plistPath}.backup-${Date.now()}`;
245
- copyFileSync(plistPath, backupPath);
246
- log(` Backed up existing plist to: ${backupPath}`);
247
- }
669
+ log(` The runner will start hidden at next login.`);
670
+ }
671
+ async function installMacAutostart(runnerCommand, log, env) {
672
+ const home = env["HOME"]?.trim() || homedir3();
673
+ const launchAgentsDir = join5(home, "Library", "LaunchAgents");
674
+ mkdirSync4(launchAgentsDir, { recursive: true });
675
+ const plistPath = join5(launchAgentsDir, "ai.algosuite.vo-runner.plist");
248
676
  const parts = runnerCommand.split(/\s+/);
249
677
  const program = parts[0] ?? "vo-mcp";
250
678
  const args = parts.length > 1 ? parts.slice(1) : ["runner"];
@@ -263,53 +691,58 @@ ${args.map((a) => ` <string>${a}</string>`).join("\n")}
263
691
  <true/>
264
692
  <key>KeepAlive</key>
265
693
  <true/>
694
+ <key>WorkingDirectory</key>
695
+ <string>${home}</string>
696
+ <key>EnvironmentVariables</key>
697
+ <dict>
698
+ <key>VO_CODE_RUNNER_CLONES_ROOT</key>
699
+ <string>${join5(home, "Library", "Application Support", "ai.algosuite.vo-runner", "clones")}</string>
700
+ </dict>
266
701
  <key>StandardOutPath</key>
267
- <string>${join2(homedir2(), ".claude", "vo-runner.log")}</string>
702
+ <string>${join5(home, ".claude", "vo-runner.log")}</string>
268
703
  <key>StandardErrorPath</key>
269
- <string>${join2(homedir2(), ".claude", "vo-runner-error.log")}</string>
704
+ <string>${join5(home, ".claude", "vo-runner-error.log")}</string>
270
705
  </dict>
271
706
  </plist>
272
707
  `;
273
- writeFileSync2(plistPath, plistContent, "utf8");
708
+ if (launcherIsCurrent(plistPath, plistContent, "plist", log)) {
709
+ log(`\u2713 Auto-start is already configured (launchd)`);
710
+ log(` Path: ${plistPath}`);
711
+ return;
712
+ }
713
+ writeFileSync4(plistPath, plistContent, "utf8");
274
714
  log(`\u2713 Installed launchd plist`);
275
715
  log(` Path: ${plistPath}`);
276
716
  try {
277
717
  const { execSync } = await import("node:child_process");
278
718
  execSync(`launchctl load "${plistPath}"`, { stdio: "ignore" });
279
719
  log(`\u2713 Loaded plist with launchctl (runner will start at next login)`);
280
- log(` Logs: ${join2(homedir2(), ".claude", "vo-runner.log")}`);
720
+ log(` Logs: ${join5(home, ".claude", "vo-runner.log")}`);
281
721
  } catch {
282
722
  log(`\u26A0 Failed to load plist with launchctl (you may need to load it manually)`);
283
723
  log(` Run: launchctl load "${plistPath}"`);
284
724
  }
285
725
  }
286
726
  async function installLinuxAutostart(runnerCommand, log, env) {
287
- const home = env["HOME"]?.trim() || homedir2();
288
- const unitDir = join2(home, ".config", "systemd", "user");
289
- mkdirSync2(unitDir, { recursive: true });
290
- const unitPath = join2(unitDir, "vo-runner.service");
291
- if (existsSync2(unitPath)) {
292
- const existing = readFileSync2(unitPath, "utf8");
293
- if (existing.includes(runnerCommand) || existing.includes("vo-mcp runner")) {
294
- log(`\u2713 Auto-start is already configured (systemd user unit)`);
295
- log(` Path: ${unitPath}`);
296
- return;
297
- }
298
- const backupPath = `${unitPath}.backup-${Date.now()}`;
299
- copyFileSync(unitPath, backupPath);
300
- log(` Backed up existing unit to: ${backupPath}`);
301
- }
302
- const logFile = join2(home, ".claude", "vo-runner.log");
303
- const errFile = join2(home, ".claude", "vo-runner-error.log");
304
- mkdirSync2(join2(home, ".claude"), { recursive: true });
727
+ const home = env["HOME"]?.trim() || homedir3();
728
+ const configHome = resolveLinuxConfigHome(home, env);
729
+ const unitDir = join5(configHome, "systemd", "user");
730
+ mkdirSync4(unitDir, { recursive: true });
731
+ const unitPath = join5(unitDir, "vo-runner.service");
732
+ const logFile = join5(home, ".claude", "vo-runner.log");
733
+ const errFile = join5(home, ".claude", "vo-runner-error.log");
734
+ const quotedRunnerCommand = quotePosixShellArgument(runnerCommand);
735
+ mkdirSync4(join5(home, ".claude"), { recursive: true });
305
736
  const unit = `[Unit]
306
- Description=VO Code Runner (vo-mcp)
737
+ Description=AlgoHQ Code Runner (vo-mcp)
307
738
  After=network-online.target
308
739
  Wants=network-online.target
309
740
 
310
741
  [Service]
311
742
  Type=simple
312
- ExecStart=/bin/sh -lc '${runnerCommand}'
743
+ WorkingDirectory=${home}
744
+ Environment="VO_CODE_RUNNER_CLONES_ROOT=${join5(configHome, "ai.algosuite.vo-runner", "clones")}"
745
+ ExecStart=/bin/sh -lc ${quotedRunnerCommand}
313
746
  Restart=on-failure
314
747
  RestartSec=10
315
748
  StandardOutput=append:${logFile}
@@ -318,7 +751,12 @@ StandardError=append:${errFile}
318
751
  [Install]
319
752
  WantedBy=default.target
320
753
  `;
321
- writeFileSync2(unitPath, unit, "utf8");
754
+ if (launcherIsCurrent(unitPath, unit, "unit", log)) {
755
+ log(`\u2713 Auto-start is already configured (systemd user unit)`);
756
+ log(` Path: ${unitPath}`);
757
+ return;
758
+ }
759
+ writeFileSync4(unitPath, unit, "utf8");
322
760
  log(`\u2713 Installed systemd user unit`);
323
761
  log(` Path: ${unitPath}`);
324
762
  if (process.env["VITEST"]) {
@@ -340,11 +778,11 @@ async function installAutostart(opts = {}) {
340
778
  const log = opts.log ?? ((m) => console.error(m));
341
779
  const env = opts.env ?? process.env;
342
780
  const runnerCommand = resolveRunnerCommand(opts.runnerCommand);
343
- const plat = platform2();
781
+ const plat = opts.platform ?? platform2();
344
782
  if (plat === "win32") {
345
783
  installWindowsAutostart(runnerCommand, log, env);
346
784
  } else if (plat === "darwin") {
347
- await installMacAutostart(runnerCommand, log);
785
+ await installMacAutostart(runnerCommand, log, env);
348
786
  } else if (plat === "linux") {
349
787
  await installLinuxAutostart(runnerCommand, log, env);
350
788
  } else {
@@ -356,75 +794,170 @@ async function installAutostart(opts = {}) {
356
794
  // src/install.ts
357
795
  var DEFAULT_CONTROL_PLANE_URL2 = "https://vo-control-plane-bzjphrajaq-uc.a.run.app";
358
796
  function resolveCodeConfigPath(home) {
359
- return join3(home, ".claude.json");
797
+ return join6(home, ".claude.json");
360
798
  }
361
799
  function resolveDesktopConfigPath(home, plat, appData) {
362
800
  if (plat === "win32") {
363
- return join3(appData ?? join3(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
801
+ return join6(appData ?? join6(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
364
802
  }
365
803
  if (plat === "darwin") {
366
- return join3(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
804
+ return join6(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
367
805
  }
368
- return join3(home, ".config", "Claude", "claude_desktop_config.json");
806
+ return join6(home, ".config", "Claude", "claude_desktop_config.json");
369
807
  }
370
808
  function readClaudeConfig(path) {
371
- try {
372
- if (!existsSync3(path)) return {};
373
- const raw = readFileSync3(path, "utf8");
374
- const parsed = JSON.parse(raw);
375
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
376
- } catch {
377
- return {};
809
+ if (!existsSync6(path)) return { kind: "absent", config: {}, mtimeMs: null };
810
+ for (let attempt = 0; attempt < 3; attempt += 1) {
811
+ const before = statSync3(path).mtimeMs;
812
+ let raw;
813
+ try {
814
+ raw = readFileSync6(path, "utf8");
815
+ } catch {
816
+ return { kind: "invalid", config: {}, mtimeMs: before };
817
+ }
818
+ if (!existsSync6(path) || statSync3(path).mtimeMs !== before) continue;
819
+ const text = raw.replace(/^\uFEFF/u, "");
820
+ if (!text.trim()) return { kind: "empty", config: {}, mtimeMs: before };
821
+ try {
822
+ const parsed = JSON.parse(text);
823
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? { kind: "ok", config: parsed, mtimeMs: before } : { kind: "invalid", config: {}, mtimeMs: before };
824
+ } catch {
825
+ return { kind: "invalid", config: {}, mtimeMs: before };
826
+ }
378
827
  }
828
+ return { kind: "invalid", config: {}, mtimeMs: null };
379
829
  }
380
830
  function writeClaudeConfig(path, config) {
381
- mkdirSync3(dirname2(path), { recursive: true });
382
- writeFileSync3(path, `${JSON.stringify(config, null, 2)}
383
- `, "utf8");
831
+ mkdirSync5(dirname4(path), { recursive: true });
832
+ writeFileAtomic(path, `${JSON.stringify(config, null, 2)}
833
+ `);
834
+ }
835
+ function carriedEntryKeys(entry) {
836
+ if (!entry) return {};
837
+ const { command: _c, args: _a, env: _e, type, url: _u, headers: _h, ...rest } = entry;
838
+ const stdio = type === void 0 || type === "stdio";
839
+ return { ...stdio ? rest : {}, ...type === "stdio" ? { type } : {} };
840
+ }
841
+ function preferredNodeCommand(existing) {
842
+ const current = String(existing ?? "").trim();
843
+ return current && /(^|[\\/])node(\.exe)?$/iu.test(current) ? current : "node";
384
844
  }
385
845
  function resolveVoMcpCliPath() {
386
- return join3(dirname2(fileURLToPath(import.meta.url)), "cli.js");
846
+ return join6(dirname4(fileURLToPath(import.meta.url)), "cli.js");
387
847
  }
388
- function installMcpConfigAt(configPath, cliPath, controlPlaneUrl, log, label) {
389
- const existing = readClaudeConfig(configPath);
848
+ var INSTALL_LAUNCHER = { sticky: false, onlyExisting: false };
849
+ var HEAL_LAUNCHER = { sticky: true, onlyExisting: true };
850
+ function reconcileRemoteEntry(mcpServers, remote) {
851
+ const existing = mcpServers[REMOTE_MCP_SERVER_KEY];
852
+ if (remote) {
853
+ return {
854
+ fragment: { [REMOTE_MCP_SERVER_KEY]: remote },
855
+ current: remoteMcpEntryIsCurrent(existing, remote)
856
+ };
857
+ }
858
+ if (existing !== void 0 && isManagedRemoteMcpEntry(existing)) {
859
+ return { fragment: { [REMOTE_MCP_SERVER_KEY]: void 0 }, current: false };
860
+ }
861
+ return { fragment: {}, current: true };
862
+ }
863
+ function installMcpConfigAt(configPath, cliPath, controlPlaneUrl, log, label, launcher = { launcherPath: null, ...INSTALL_LAUNCHER }, remote = null) {
864
+ if (launcher.onlyExisting && !existsSync6(configPath)) return;
865
+ const read = readClaudeConfig(configPath);
866
+ if (read.kind === "invalid" || read.kind === "empty" && launcher.onlyExisting) {
867
+ 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)"}`);
868
+ return;
869
+ }
870
+ const existing = read.config;
871
+ const readMtime = read.mtimeMs;
390
872
  const mcpServers = existing["mcpServers"] && typeof existing["mcpServers"] === "object" ? existing["mcpServers"] : {};
391
- const voEntry = mcpServers["vo"] ?? mcpServers["vo-mcp"];
392
- if (voEntry?.args?.some((a) => a.includes(cliPath))) {
873
+ const managedEntry = mcpServers["vo-mcp"];
874
+ const voEntry = managedEntry ?? mcpServers["vo"];
875
+ const { launcherPath } = launcher;
876
+ const fallbackCli = chooseFallbackCli(voEntry?.env?.[MCP_FALLBACK_CLI_ENV], cliPath, launcher.sticky, launcherPath ? dirname4(launcherPath) : null);
877
+ const localCurrent = launcherPath ? !isStaleVoMcpEntry(voEntry, launcherPath, fallbackCli) : Boolean(voEntry?.args?.some((a) => a.includes(cliPath)));
878
+ const remoteState = reconcileRemoteEntry(mcpServers, remote);
879
+ if (localCurrent && remoteState.current) {
393
880
  log(` ${label} already current: ${configPath}`);
394
881
  return;
395
882
  }
396
- if (existsSync3(configPath)) {
397
- const backupPath = `${configPath}.backup-${Date.now()}`;
398
- copyFileSync2(configPath, backupPath);
399
- log(` Backed up ${label} config \u2192 ${backupPath}`);
400
- }
883
+ const backupPath = backupConfigOnce(configPath);
884
+ if (backupPath) log(` Backed up ${label} config \u2192 ${backupPath}`);
885
+ const { [MCP_FALLBACK_CLI_ENV]: _previousFallback, ...preservedEnv } = voEntry?.env ?? {};
401
886
  const merged = {
402
887
  ...existing,
403
888
  mcpServers: {
404
889
  ...mcpServers,
405
890
  "vo-mcp": {
406
- command: "node",
407
- args: [cliPath],
408
- // Preserve any existing env the user added (e.g. model API keys) — only
409
- // ensure VO_CONTROL_PLANE_URL is present. NEVER drop the user's env keys.
410
- env: { VO_CONTROL_PLANE_URL: controlPlaneUrl, ...voEntry?.env ?? {} }
411
- }
891
+ ...carriedEntryKeys(managedEntry),
892
+ command: preferredNodeCommand(managedEntry?.command),
893
+ args: [launcherPath ?? cliPath],
894
+ env: {
895
+ VO_CONTROL_PLANE_URL: controlPlaneUrl,
896
+ ...preservedEnv,
897
+ ...launcherPath && fallbackCli ? { [MCP_FALLBACK_CLI_ENV]: fallbackCli } : {}
898
+ }
899
+ },
900
+ // Additive remote entry (11.3 slice A) — or the removal of one we wrote,
901
+ // when the operator has opted back out. Spread LAST so it can delete its
902
+ // own key; it never names 'vo-mcp', so the stdio entry above is safe.
903
+ ...remoteState.fragment
412
904
  }
413
905
  };
906
+ if (readMtime !== null && (!existsSync6(configPath) || statSync3(configPath).mtimeMs !== readMtime)) {
907
+ log(` \u26A0 ${label} config changed while updating \u2014 left untouched this time: ${configPath}`);
908
+ return;
909
+ }
414
910
  writeClaudeConfig(configPath, merged);
415
911
  log(`\u2713 Wrote vo-mcp to ${label}: ${configPath}`);
912
+ if (remote) {
913
+ log(` + remote MCP entry '${REMOTE_MCP_SERVER_KEY}' \u2192 ${remote.url} (serves vo_skill_list; every other tool stays on the local vo-mcp entry)`);
914
+ log(` Set ${REMOTE_MCP_TOKEN_ENV} in your environment to authenticate it \u2014 no token is written to this file.`);
915
+ } else if (!remoteState.current) {
916
+ log(` - removed the remote MCP entry '${REMOTE_MCP_SERVER_KEY}' (VO_REMOTE_MCP is not set to 1)`);
917
+ }
416
918
  }
417
- function installMcpConfig(log, env) {
418
- const home = env["HOME"]?.trim() || env["USERPROFILE"]?.trim() || homedir3();
919
+ function installMcpConfig(log, env, mode = "install") {
920
+ const home = env["HOME"]?.trim() || env["USERPROFILE"]?.trim() || homedir4();
419
921
  const appData = env["APPDATA"]?.trim();
420
922
  const plat = platform3();
421
923
  const cliPath = resolveVoMcpCliPath();
422
924
  const controlPlaneUrl = env["VO_CONTROL_PLANE_URL"]?.trim() || DEFAULT_CONTROL_PLANE_URL2;
423
- installMcpConfigAt(resolveCodeConfigPath(home), cliPath, controlPlaneUrl, log, "Claude Code CLI");
424
- installMcpConfigAt(resolveDesktopConfigPath(home, plat, appData), cliPath, controlPlaneUrl, log, "Claude Desktop");
925
+ const remote = remoteMcpOptIn(env) ? buildRemoteMcpEntry(controlPlaneUrl) : null;
926
+ const launcherPath = writeMcpLauncherForEnv(env, log, { platform: plat, home });
927
+ const launcher = { launcherPath, ...mode === "heal" ? HEAL_LAUNCHER : INSTALL_LAUNCHER };
928
+ const leg = (label, run) => {
929
+ try {
930
+ run();
931
+ } catch (err) {
932
+ if (mode !== "heal") throw err;
933
+ log(` \u26A0 ${label}: could not update the MCP registration \u2014 ${err instanceof Error ? err.message : String(err)}`);
934
+ }
935
+ };
936
+ leg("Claude Code CLI", () => installMcpConfigAt(resolveCodeConfigPath(home), cliPath, controlPlaneUrl, log, "Claude Code CLI", launcher, remote));
937
+ leg("Claude Desktop", () => installMcpConfigAt(resolveDesktopConfigPath(home, plat, appData), cliPath, controlPlaneUrl, log, "Claude Desktop", launcher, remote));
938
+ const codexPath = resolveCodexConfigPath(home);
939
+ if (launcher.onlyExisting && !existsSync6(codexPath)) return;
940
+ leg("Codex", () => {
941
+ if (launcherPath) {
942
+ const fallbackCli = chooseFallbackCli(readCodexManagedEnv(codexPath, MCP_FALLBACK_CLI_ENV), cliPath, launcher.sticky, dirname4(launcherPath));
943
+ installCodexMcpConfigAt(codexPath, launcherPath, controlPlaneUrl, log, fallbackCli ? { [MCP_FALLBACK_CLI_ENV]: fallbackCli } : {});
944
+ } else {
945
+ installCodexMcpConfigAt(codexPath, cliPath, controlPlaneUrl, log);
946
+ }
947
+ });
948
+ }
949
+ function writeMcpLauncherForEnv(env, log, hint) {
950
+ try {
951
+ const root = defaultRunnerRuntimeRoot(env, hint);
952
+ if (!root) return null;
953
+ return writeMcpLauncher(root);
954
+ } catch (err) {
955
+ log(` \u26A0 could not write the vo-mcp launcher (registering cli.js directly): ${err instanceof Error ? err.message : String(err)}`);
956
+ return null;
957
+ }
425
958
  }
426
959
  async function runPairFlow(log, env) {
427
- log("\n\u2501\u2501\u2501 Step 2: Link your VO account (device code) \u2501\u2501\u2501");
960
+ log("\n\u2501\u2501\u2501 Step 2: Link your AlgoHQ account (device code) \u2501\u2501\u2501");
428
961
  log("A short code appears below \u2014 open the URL it prints and enter the code.");
429
962
  log("Your raw token never persists; a scoped credential is stored in your OS keychain.\n");
430
963
  try {
@@ -437,27 +970,33 @@ async function runPairFlow(log, env) {
437
970
  log(" No problem \u2014 the rest will finish; pair anytime with: vo-mcp pair");
438
971
  }
439
972
  }
440
- function printNextSteps(log, autostartInstalled) {
973
+ function printNextSteps(log, autostartInstalled, configOnly) {
441
974
  log("\n\u2501\u2501\u2501 Installation complete! \u2501\u2501\u2501\n");
442
975
  log("What's configured:");
443
- log(" \u2713 Claude Desktop / Claude Code will load vo-mcp on next restart");
444
- log(" \u2713 Your scoped credential is stored (revocable via the dashboard)");
976
+ log(" \u2713 Claude Desktop, Claude Code, and Codex will load AlgoHQ MCP on next restart");
977
+ if (configOnly) {
978
+ log(" \u2713 Existing pairing and runner auto-start settings were left unchanged");
979
+ } else {
980
+ log(" \u2713 Your scoped credential is stored (revocable via the dashboard)");
981
+ }
445
982
  if (autostartInstalled) {
446
983
  log(" \u2713 Runner daemon will start automatically at login\n");
447
984
  } else {
448
985
  log("\n");
449
986
  }
450
987
  log("Next steps:");
451
- log(" 1. Restart Claude Desktop / Claude Code (if running).");
452
- if (autostartInstalled) {
988
+ log(" 1. Restart Claude Desktop / Claude Code / Codex (if running).");
989
+ if (configOnly) {
990
+ log(" 2. Restart the existing AlgoHQ runner service or runner terminal.");
991
+ } else if (autostartInstalled) {
453
992
  log(" 2. Log out and back in (or start the runner manually now: vo-mcp runner)");
454
993
  } else {
455
994
  log(" 2. Start the agent runner in a terminal (keep it running):");
456
995
  log(" vo-mcp runner");
457
996
  log(" (To set up auto-start at login: vo-mcp runner --install-autostart)");
458
997
  }
459
- log(" 3. Visit the VO Command Center to dispatch your first agent:");
460
- log(" https://algosuite.ai/virtualoffice\n");
998
+ log(" 3. Visit AlgoHQ to dispatch your first agent:");
999
+ log(" https://algosuite.ai/algohq\n");
461
1000
  log("The runner watches for tasks you dispatch and spins up agents in fresh worktrees.");
462
1001
  log("Agents only run while the runner is connected. Ctrl+C to stop it anytime.\n");
463
1002
  }
@@ -465,11 +1004,13 @@ async function install(opts = {}) {
465
1004
  const log = opts.log ?? ((m) => console.error(m));
466
1005
  const env = opts.env ?? process.env;
467
1006
  log("\u2501\u2501\u2501 vo-mcp installer \u2501\u2501\u2501");
468
- log("This will set up your machine to dispatch VO agents from anywhere.\n");
469
- log("\u2501\u2501\u2501 Step 1: Configure Claude Desktop / Claude Code \u2501\u2501\u2501");
1007
+ log("This will set up your machine to dispatch AlgoHQ agents from anywhere.\n");
1008
+ log("\u2501\u2501\u2501 Step 1: Configure Claude Desktop / Claude Code / Codex \u2501\u2501\u2501");
470
1009
  installMcpConfig(log, env);
471
1010
  if (!opts.skipLogin) {
472
1011
  await runPairFlow(log, env);
1012
+ } else if (opts.configOnly) {
1013
+ log("\n(Config-only refresh \u2014 existing pairing left unchanged.)");
473
1014
  } else {
474
1015
  log("\n(Pairing skipped \u2014 run `vo-mcp pair` when ready.)");
475
1016
  }
@@ -484,11 +1025,15 @@ async function install(opts = {}) {
484
1025
  autostartInstalled = true;
485
1026
  }
486
1027
  }
487
- printNextSteps(log, autostartInstalled);
1028
+ printNextSteps(log, autostartInstalled, opts.configOnly === true);
1029
+ }
1030
+ function installOptionsFromArgs(args) {
1031
+ const configOnly = args.includes("--config-only");
1032
+ return configOnly ? { configOnly: true, skipLogin: true, skipAutostart: true } : {};
488
1033
  }
489
1034
 
490
1035
  // src/install-cli.ts
491
- install().catch((err) => {
1036
+ install(installOptionsFromArgs(process.argv.slice(2))).catch((err) => {
492
1037
  console.error("[vo-mcp install] fatal:", err);
493
1038
  process.exit(1);
494
1039
  });