@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.
- package/README.md +29 -3
- package/bin/vo-mcp +6 -3
- package/dist/agent-auth-probe-cli.mjs +1731 -0
- package/dist/autostart-cli.js +118 -60
- package/dist/autostart-cli.js.map +1 -2
- package/dist/ci/check-local-pr-overlap.js +107644 -0
- package/dist/cli.js +2905 -515
- package/dist/cli.js.map +3 -4
- package/dist/index.js +2495 -370
- package/dist/index.js.map +3 -4
- package/dist/install-cli.js +660 -115
- package/dist/install-cli.js.map +3 -4
- package/dist/login-cli.js +4 -4
- package/dist/login-cli.js.map +1 -2
- package/dist/pair-cli.js +1 -1
- package/dist/pair-cli.js.map +1 -2
- package/dist/runner-cli.js +14222 -3264
- package/dist/runner-cli.js.map +3 -4
- package/dist/runner-supervisor.js +3748 -0
- package/dist/runner-supervisor.js.map +6 -0
- package/dist/set-key-cli.js +82 -8
- package/dist/set-key-cli.js.map +1 -2
- package/dist/supervisor-credential-helper.js +233 -0
- package/dist/supervisor-credential-helper.js.map +6 -0
- package/dist/thresholds.json +64 -0
- package/dist/update-cli.js +75 -12
- package/dist/update-cli.js.map +3 -4
- package/package.json +5 -3
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
|
|
@@ -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
|
|
202
|
-
import { join as
|
|
203
|
-
import { existsSync as
|
|
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"] ??
|
|
209
|
-
const startupDir =
|
|
210
|
-
|
|
211
|
-
const launcherPath =
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
log(`
|
|
217
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
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
|
|
232
|
-
}
|
|
233
|
-
async function installMacAutostart(runnerCommand, log) {
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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>${
|
|
702
|
+
<string>${join5(home, ".claude", "vo-runner.log")}</string>
|
|
268
703
|
<key>StandardErrorPath</key>
|
|
269
|
-
<string>${
|
|
704
|
+
<string>${join5(home, ".claude", "vo-runner-error.log")}</string>
|
|
270
705
|
</dict>
|
|
271
706
|
</plist>
|
|
272
707
|
`;
|
|
273
|
-
|
|
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: ${
|
|
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() ||
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
797
|
+
return join6(home, ".claude.json");
|
|
360
798
|
}
|
|
361
799
|
function resolveDesktopConfigPath(home, plat, appData) {
|
|
362
800
|
if (plat === "win32") {
|
|
363
|
-
return
|
|
801
|
+
return join6(appData ?? join6(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
364
802
|
}
|
|
365
803
|
if (plat === "darwin") {
|
|
366
|
-
return
|
|
804
|
+
return join6(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
367
805
|
}
|
|
368
|
-
return
|
|
806
|
+
return join6(home, ".config", "Claude", "claude_desktop_config.json");
|
|
369
807
|
}
|
|
370
808
|
function readClaudeConfig(path) {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
|
846
|
+
return join6(dirname4(fileURLToPath(import.meta.url)), "cli.js");
|
|
387
847
|
}
|
|
388
|
-
|
|
389
|
-
|
|
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
|
|
392
|
-
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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() ||
|
|
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
|
-
|
|
424
|
-
|
|
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
|
|
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
|
|
444
|
-
|
|
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 (
|
|
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
|
|
460
|
-
log(" https://algosuite.ai/
|
|
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
|
|
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
|
});
|