@algosuite/vo-mcp 0.2.0-beta.4 → 0.2.0-beta.42
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 +27 -3
- package/bin/vo-mcp +9 -3
- package/dist/agent-auth-probe-cli.mjs +1718 -0
- package/dist/autostart-cli.js +115 -60
- package/dist/autostart-cli.js.map +2 -2
- package/dist/ci/check-local-pr-overlap.js +107511 -0
- package/dist/cli.js +2392 -340
- package/dist/cli.js.map +4 -4
- package/dist/index.js +2118 -199
- package/dist/index.js.map +4 -4
- package/dist/install-cli.js +361 -345
- package/dist/install-cli.js.map +4 -4
- package/dist/login-cli.js +4 -4
- package/dist/login-cli.js.map +2 -2
- package/dist/pair-cli.js +1 -1
- package/dist/pair-cli.js.map +2 -2
- package/dist/runner-cli.js +14072 -2594
- package/dist/runner-cli.js.map +4 -4
- package/dist/runner-supervisor.js +2628 -0
- package/dist/runner-supervisor.js.map +7 -0
- package/dist/set-key-cli.js +89 -5
- package/dist/set-key-cli.js.map +2 -2
- package/dist/supervisor-credential-helper.js +233 -0
- package/dist/supervisor-credential-helper.js.map +7 -0
- package/dist/thresholds.json +64 -0
- package/dist/update-cli.js +125 -0
- package/dist/update-cli.js.map +7 -0
- package/package.json +5 -3
package/dist/install-cli.js
CHANGED
|
@@ -2,15 +2,13 @@
|
|
|
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
|
|
6
|
-
import { join as
|
|
7
|
-
import { existsSync as
|
|
8
|
-
import {
|
|
5
|
+
import { homedir as homedir3, platform as platform3 } from "node:os";
|
|
6
|
+
import { join as join4, dirname as dirname3 } from "node:path";
|
|
7
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, copyFileSync as copyFileSync3 } from "node:fs";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
9
|
|
|
10
|
-
// src/cloud/
|
|
11
|
-
import {
|
|
12
|
-
import { randomBytes } from "node:crypto";
|
|
13
|
-
import { spawn } from "node:child_process";
|
|
10
|
+
// src/cloud/pairing.ts
|
|
11
|
+
import { hostname, platform } from "node:os";
|
|
14
12
|
|
|
15
13
|
// src/cloud/credential-store.ts
|
|
16
14
|
import { homedir } from "node:os";
|
|
@@ -125,289 +123,296 @@ function writeStoredCredential(cred, storedAt, env = process.env, keychain = rea
|
|
|
125
123
|
return p;
|
|
126
124
|
}
|
|
127
125
|
|
|
128
|
-
// src/cloud/
|
|
126
|
+
// src/cloud/pairing.ts
|
|
127
|
+
var DEFAULT_CONTROL_PLANE_URL = "https://vo-control-plane-bzjphrajaq-uc.a.run.app";
|
|
129
128
|
var DEFAULT_DASHBOARD_URL = "https://algosuite.ai";
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
function processCapture(rawBody, expectedState, store) {
|
|
133
|
-
let data;
|
|
134
|
-
try {
|
|
135
|
-
data = JSON.parse(rawBody);
|
|
136
|
-
} catch {
|
|
137
|
-
return { ok: false, httpStatus: 400, error: "invalid JSON body" };
|
|
138
|
-
}
|
|
139
|
-
if (!data || typeof data !== "object") return { ok: false, httpStatus: 400, error: "invalid body" };
|
|
140
|
-
if (typeof data.state !== "string" || data.state !== expectedState) {
|
|
141
|
-
return { ok: false, httpStatus: 403, error: "state mismatch (possible CSRF) \u2014 login aborted" };
|
|
142
|
-
}
|
|
143
|
-
const refresh = typeof data.refresh_token === "string" ? data.refresh_token.trim() : "";
|
|
144
|
-
const apiKey = typeof data.api_key === "string" ? data.api_key.trim() : "";
|
|
145
|
-
if (!refresh || !apiKey) {
|
|
146
|
-
return { ok: false, httpStatus: 400, error: "login response missing refresh_token / api_key" };
|
|
147
|
-
}
|
|
148
|
-
const email = typeof data.email === "string" && data.email.trim() ? data.email.trim() : void 0;
|
|
149
|
-
const path = store({ refresh_token: refresh, api_key: apiKey, ...email ? { email } : {} });
|
|
150
|
-
return {
|
|
151
|
-
ok: true,
|
|
152
|
-
httpStatus: 200,
|
|
153
|
-
result: { ...email ? { email } : {}, credentialPath: path },
|
|
154
|
-
captured: { refresh_token: refresh, api_key: apiKey, ...email ? { email } : {} }
|
|
155
|
-
};
|
|
129
|
+
function formatPairingCode(code) {
|
|
130
|
+
return code.length === 8 ? `${code.slice(0, 4)}-${code.slice(4)}` : code;
|
|
156
131
|
}
|
|
157
|
-
function
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
<h2 id="m">Completing sign-in\u2026</h2>
|
|
161
|
-
<script>
|
|
162
|
-
(function(){
|
|
163
|
-
var h=location.hash.replace(/^#/,''), p=new URLSearchParams(h), b={};
|
|
164
|
-
['state','refresh_token','api_key','email'].forEach(function(k){ if(p.get(k)) b[k]=p.get(k); });
|
|
165
|
-
fetch('/capture',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(b)})
|
|
166
|
-
.then(function(r){ document.getElementById('m').textContent = r.ok ? 'Sign-in complete \u2014 you can close this tab.' : 'Sign-in failed \u2014 check the terminal.'; })
|
|
167
|
-
.catch(function(){ document.getElementById('m').textContent = 'Sign-in failed \u2014 check the terminal.'; });
|
|
168
|
-
})();
|
|
169
|
-
</script></body></html>`;
|
|
170
|
-
}
|
|
171
|
-
function defaultOpenBrowser(url) {
|
|
172
|
-
const platform3 = process.platform;
|
|
173
|
-
if (platform3 === "win32") {
|
|
174
|
-
spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" }).unref();
|
|
175
|
-
} else if (platform3 === "darwin") {
|
|
176
|
-
spawn("open", [url], { detached: true, stdio: "ignore" }).unref();
|
|
177
|
-
} else {
|
|
178
|
-
spawn("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
|
|
179
|
-
}
|
|
132
|
+
async function readJson(res) {
|
|
133
|
+
const body = await res.json().catch(() => ({}));
|
|
134
|
+
return body && typeof body === "object" ? body : {};
|
|
180
135
|
}
|
|
181
|
-
async function
|
|
182
|
-
const env =
|
|
183
|
-
const
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
server.close();
|
|
196
|
-
if (err) reject(err);
|
|
197
|
-
else resolve2(result);
|
|
198
|
-
};
|
|
199
|
-
const server = createServer((req, res) => {
|
|
200
|
-
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
201
|
-
if (req.method === "GET" && url.pathname === "/callback") {
|
|
202
|
-
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
203
|
-
res.end(captureHtml());
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
if (req.method === "POST" && url.pathname === "/capture") {
|
|
207
|
-
let body = "";
|
|
208
|
-
req.on("data", (chunk) => {
|
|
209
|
-
body += chunk.toString("utf8");
|
|
210
|
-
if (body.length > MAX_BODY_BYTES) req.destroy();
|
|
211
|
-
});
|
|
212
|
-
req.on("end", () => {
|
|
213
|
-
void (async () => {
|
|
214
|
-
if (body.length > MAX_BODY_BYTES) {
|
|
215
|
-
res.writeHead(413, { "content-type": "text/plain" });
|
|
216
|
-
res.end("payload too large");
|
|
217
|
-
finish(new Error("login request body exceeded the size cap"));
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
const writeNow = (cred) => writeStoredCredential(cred, nowIso(), env);
|
|
221
|
-
const outcome = processCapture(body, state, opts.exchange ? () => "pending" : writeNow);
|
|
222
|
-
let result = outcome.result;
|
|
223
|
-
if (outcome.ok && outcome.captured && opts.exchange) {
|
|
224
|
-
const capt = outcome.captured;
|
|
225
|
-
let cred = {
|
|
226
|
-
refresh_token: capt.refresh_token,
|
|
227
|
-
api_key: capt.api_key,
|
|
228
|
-
...capt.email ? { email: capt.email } : {}
|
|
229
|
-
};
|
|
230
|
-
try {
|
|
231
|
-
const voc = await opts.exchange(capt.refresh_token, capt.api_key);
|
|
232
|
-
if (voc && voc.vo_credential) {
|
|
233
|
-
cred = {
|
|
234
|
-
vo_credential: voc.vo_credential,
|
|
235
|
-
vo_credential_expires_at: voc.expires_at,
|
|
236
|
-
...capt.email ? { email: capt.email } : {}
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
} catch {
|
|
240
|
-
}
|
|
241
|
-
const path = writeNow(cred);
|
|
242
|
-
result = { ...capt.email ? { email: capt.email } : {}, credentialPath: path };
|
|
243
|
-
}
|
|
244
|
-
res.writeHead(outcome.httpStatus, { "content-type": "text/html; charset=utf-8" });
|
|
245
|
-
res.end(outcome.ok ? "<h2>VO login complete \u2014 you can close this tab.</h2>" : `<h2>Login failed: ${outcome.error}</h2>`);
|
|
246
|
-
finish(outcome.ok ? null : new Error(outcome.error ?? "login failed"), result);
|
|
247
|
-
})();
|
|
248
|
-
});
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
res.writeHead(404);
|
|
252
|
-
res.end("not found");
|
|
253
|
-
});
|
|
254
|
-
const timer = setTimeout(
|
|
255
|
-
() => finish(new Error(`login timed out after ${timeoutMs}ms \u2014 no sign-in captured`)),
|
|
256
|
-
timeoutMs
|
|
257
|
-
);
|
|
258
|
-
server.on("error", (e) => finish(e));
|
|
259
|
-
server.listen(0, "127.0.0.1", () => {
|
|
260
|
-
const addr = server.address();
|
|
261
|
-
const port = addr && typeof addr === "object" ? addr.port : 0;
|
|
262
|
-
if (!port) {
|
|
263
|
-
finish(new Error("failed to bind a loopback port"));
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
const loginUrl = `${dashboardUrl}/cli-login?port=${port}&state=${encodeURIComponent(state)}`;
|
|
267
|
-
log(`[vo-mcp] Opening your browser to sign in:
|
|
268
|
-
${loginUrl}`);
|
|
269
|
-
log("[vo-mcp] If it did not open, paste that URL into your browser. Waiting for sign-in\u2026");
|
|
270
|
-
try {
|
|
271
|
-
openBrowser(loginUrl);
|
|
272
|
-
} catch {
|
|
273
|
-
}
|
|
274
|
-
});
|
|
136
|
+
async function runPairing(deps = {}) {
|
|
137
|
+
const env = deps.env ?? process.env;
|
|
138
|
+
const log = deps.log ?? ((m) => console.error(m));
|
|
139
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
140
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
141
|
+
const now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
142
|
+
const store = deps.store ?? ((cred, iso) => writeStoredCredential(cred, iso, env));
|
|
143
|
+
const controlPlaneUrl = env["VO_CONTROL_PLANE_URL"]?.trim() || DEFAULT_CONTROL_PLANE_URL;
|
|
144
|
+
const dashboardUrl = env["VO_DASHBOARD_URL"]?.trim() || DEFAULT_DASHBOARD_URL;
|
|
145
|
+
const deviceLabel = `${platform()} on ${hostname()}`.slice(0, 120);
|
|
146
|
+
const initRes = await fetchImpl(`${controlPlaneUrl}/api/v1/pair/initiate`, {
|
|
147
|
+
method: "POST",
|
|
148
|
+
headers: { "content-type": "application/json" },
|
|
149
|
+
body: JSON.stringify({ device_label: deviceLabel })
|
|
275
150
|
});
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
151
|
+
if (!initRes.ok) throw new Error(`Could not start pairing (server ${initRes.status}).`);
|
|
152
|
+
const init = await readJson(initRes);
|
|
153
|
+
const code = String(init["code"] ?? "");
|
|
154
|
+
const pollToken = String(init["poll_token"] ?? "");
|
|
155
|
+
if (!code || !pollToken) throw new Error("Pairing service returned an incomplete response.");
|
|
156
|
+
const intervalMs = (Number(init["poll_interval_seconds"]) || 5) * 1e3;
|
|
157
|
+
const expiresAtMs = new Date(String(init["expires_at"] ?? "")).getTime();
|
|
158
|
+
log("");
|
|
159
|
+
log(" To connect this runner, open this page in your browser:");
|
|
160
|
+
log(` ${dashboardUrl}/pair`);
|
|
161
|
+
log(" and enter this code:");
|
|
162
|
+
log("");
|
|
163
|
+
log(` ${formatPairingCode(code)}`);
|
|
164
|
+
log("");
|
|
165
|
+
log(" Your Anthropic key never leaves this computer. Waiting for you to authorize\u2026");
|
|
166
|
+
for (; ; ) {
|
|
167
|
+
if (Number.isFinite(expiresAtMs) && now().getTime() >= expiresAtMs) {
|
|
168
|
+
throw new Error("The pairing code expired before it was authorized. Run `vo-mcp pair` again.");
|
|
169
|
+
}
|
|
170
|
+
await sleep(intervalMs);
|
|
171
|
+
const pollRes = await fetchImpl(`${controlPlaneUrl}/api/v1/pair/poll`, {
|
|
172
|
+
method: "GET",
|
|
173
|
+
headers: { "x-vo-poll-token": pollToken }
|
|
174
|
+
});
|
|
175
|
+
if (pollRes.status === 404) {
|
|
176
|
+
throw new Error("The pairing expired. Run `vo-mcp pair` again.");
|
|
177
|
+
}
|
|
178
|
+
if (pollRes.status === 410) {
|
|
179
|
+
throw new Error("This code was already used. Run `vo-mcp pair` again.");
|
|
180
|
+
}
|
|
181
|
+
if (!pollRes.ok) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const body = await readJson(pollRes);
|
|
185
|
+
if (body["status"] === "pending") continue;
|
|
186
|
+
if (body["status"] === "authorized" && typeof body["vo_credential"] === "string") {
|
|
187
|
+
const credentialPath2 = store(
|
|
188
|
+
{
|
|
189
|
+
vo_credential: body["vo_credential"],
|
|
190
|
+
...typeof body["expires_at"] === "string" ? { vo_credential_expires_at: body["expires_at"] } : {}
|
|
297
191
|
},
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
if (res.status < 200 || res.status >= 300) {
|
|
302
|
-
cachedToken = null;
|
|
303
|
-
return null;
|
|
304
|
-
}
|
|
305
|
-
const parsed = JSON.parse(text);
|
|
306
|
-
const idToken = typeof parsed.id_token === "string" ? parsed.id_token : "";
|
|
307
|
-
if (!idToken) {
|
|
308
|
-
cachedToken = null;
|
|
309
|
-
return null;
|
|
310
|
-
}
|
|
311
|
-
const expiresInSec = Number(parsed.expires_in);
|
|
312
|
-
const ttlMs = Number.isFinite(expiresInSec) && expiresInSec > 0 ? expiresInSec * 1e3 : 36e5;
|
|
313
|
-
cachedToken = idToken;
|
|
314
|
-
expiresAtMs = now() + ttlMs;
|
|
315
|
-
return idToken;
|
|
316
|
-
} catch {
|
|
317
|
-
cachedToken = null;
|
|
318
|
-
return null;
|
|
192
|
+
now().toISOString()
|
|
193
|
+
);
|
|
194
|
+
return { credentialPath: credentialPath2, expires_at: String(body["expires_at"] ?? "") };
|
|
319
195
|
}
|
|
196
|
+
throw new Error("Unexpected response from the pairing service.");
|
|
320
197
|
}
|
|
321
|
-
return {
|
|
322
|
-
kind: "firebase-refresh",
|
|
323
|
-
async getToken() {
|
|
324
|
-
if (cachedToken && now() < expiresAtMs - REFRESH_SKEW_MS) return cachedToken;
|
|
325
|
-
if (!inFlight) {
|
|
326
|
-
inFlight = refresh().finally(() => {
|
|
327
|
-
inFlight = null;
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
return inFlight;
|
|
331
|
-
}
|
|
332
|
-
};
|
|
333
198
|
}
|
|
334
199
|
|
|
335
|
-
// src/
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
200
|
+
// src/codex-mcp-config.ts
|
|
201
|
+
import { copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
202
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
203
|
+
var MANAGED_BEGIN = "# BEGIN AlgoHQ MCP (managed by vo-mcp install)";
|
|
204
|
+
var MANAGED_END = "# END AlgoHQ MCP (managed by vo-mcp install)";
|
|
205
|
+
var MANAGED_SERVER_NAMES = /* @__PURE__ */ new Set(["algohq", "vo", "vo-mcp", "vo_mcp"]);
|
|
206
|
+
function resolveCodexConfigPath(home) {
|
|
207
|
+
return join2(home, ".codex", "config.toml");
|
|
208
|
+
}
|
|
209
|
+
function normalizeKey(value) {
|
|
210
|
+
const trimmed = value.trim();
|
|
211
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
212
|
+
return trimmed.slice(1, -1);
|
|
213
|
+
}
|
|
214
|
+
return trimmed;
|
|
215
|
+
}
|
|
216
|
+
function tablePath(line) {
|
|
217
|
+
const match = /^\s*\[([^\r\n]+)\]\s*(?:#.*)?$/.exec(line);
|
|
218
|
+
const rawPath = match?.[1];
|
|
219
|
+
if (!rawPath || rawPath.includes("[") || rawPath.includes("]")) return null;
|
|
220
|
+
return rawPath.split(".").map(normalizeKey);
|
|
221
|
+
}
|
|
222
|
+
function tableSections(lines) {
|
|
223
|
+
const starts = [];
|
|
224
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
225
|
+
const path = tablePath(lines[index] ?? "");
|
|
226
|
+
if (path) starts.push({ path, start: index });
|
|
227
|
+
}
|
|
228
|
+
return starts.map((section, index) => ({
|
|
229
|
+
...section,
|
|
230
|
+
end: starts[index + 1]?.start ?? lines.length
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
function isManagedSection(section) {
|
|
234
|
+
return section.path[0] === "mcp_servers" && MANAGED_SERVER_NAMES.has(section.path[1] ?? "");
|
|
235
|
+
}
|
|
236
|
+
function assignmentKey(line) {
|
|
237
|
+
const match = /^\s*((?:[A-Za-z0-9_-]+)|(?:"[^"]+")|(?:'[^']+'))\s*=/.exec(line);
|
|
238
|
+
return match?.[1] ? normalizeKey(match[1]) : null;
|
|
239
|
+
}
|
|
240
|
+
function isStructurallySafeToml(raw) {
|
|
241
|
+
const lines = raw.split(/\r?\n/);
|
|
242
|
+
const beginIndexes = lines.flatMap((line, index) => line.trim() === MANAGED_BEGIN ? [index] : []);
|
|
243
|
+
const endIndexes = lines.flatMap((line, index) => line.trim() === MANAGED_END ? [index] : []);
|
|
244
|
+
if (beginIndexes.length !== endIndexes.length || beginIndexes.length > 1) return false;
|
|
245
|
+
if (beginIndexes[0] !== void 0 && (endIndexes[0] ?? -1) <= beginIndexes[0]) return false;
|
|
246
|
+
for (const line of lines) {
|
|
247
|
+
const trimmed = line.trim();
|
|
248
|
+
if (/^\[\[?mcp_servers(?:\.|\s|$)/.test(trimmed) && tablePath(line) === null) return false;
|
|
249
|
+
}
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
function tomlString(value) {
|
|
253
|
+
return JSON.stringify(value);
|
|
254
|
+
}
|
|
255
|
+
function preservedSectionLines(lines, section, managedKeys) {
|
|
256
|
+
if (!section) return [];
|
|
257
|
+
return lines.slice(section.start + 1, section.end).filter((line) => {
|
|
258
|
+
if (line.trim() === MANAGED_BEGIN || line.trim() === MANAGED_END) return false;
|
|
259
|
+
const key = assignmentKey(line);
|
|
260
|
+
return key === null || !managedKeys.has(key);
|
|
261
|
+
}).filter((line, index, all) => line.trim() !== "" || index > 0 && index < all.length - 1);
|
|
262
|
+
}
|
|
263
|
+
function renderCodexMcpConfig(raw, cliPath, controlPlaneUrl) {
|
|
264
|
+
if (!isStructurallySafeToml(raw)) {
|
|
265
|
+
throw new Error("Codex config is malformed; refusing to overwrite it");
|
|
266
|
+
}
|
|
267
|
+
const eol = raw.includes("\r\n") ? "\r\n" : "\n";
|
|
268
|
+
const lines = raw.split(/\r?\n/);
|
|
269
|
+
const sections = tableSections(lines);
|
|
270
|
+
const rootSections = sections.filter((section) => isManagedSection(section) && section.path.length === 2);
|
|
271
|
+
const preferredRoot = rootSections.find((section) => section.path[1] === "algohq") ?? rootSections[0];
|
|
272
|
+
const preferredName = preferredRoot?.path[1];
|
|
273
|
+
const envSection = sections.find((section) => isManagedSection(section) && section.path[1] === preferredName && section.path[2] === "env");
|
|
274
|
+
const rootExtras = preservedSectionLines(lines, preferredRoot, /* @__PURE__ */ new Set(["command", "args", "required"]));
|
|
275
|
+
const envExtras = preservedSectionLines(lines, envSection, /* @__PURE__ */ new Set(["VO_CONTROL_PLANE_URL"]));
|
|
276
|
+
const removed = /* @__PURE__ */ new Set();
|
|
277
|
+
const managedBegin = lines.findIndex((line) => line.trim() === MANAGED_BEGIN);
|
|
278
|
+
const managedEnd = lines.findIndex((line) => line.trim() === MANAGED_END);
|
|
279
|
+
if (managedBegin >= 0 && managedEnd >= managedBegin) {
|
|
280
|
+
for (let index = managedBegin; index <= managedEnd; index += 1) removed.add(index);
|
|
281
|
+
}
|
|
282
|
+
for (const section of sections.filter(isManagedSection)) {
|
|
283
|
+
for (let index = section.start; index < section.end; index += 1) removed.add(index);
|
|
284
|
+
}
|
|
285
|
+
const base = lines.filter((_line, index) => !removed.has(index)).join(eol).trimEnd();
|
|
286
|
+
const block = [
|
|
287
|
+
MANAGED_BEGIN,
|
|
288
|
+
"[mcp_servers.algohq]",
|
|
289
|
+
'command = "node"',
|
|
290
|
+
`args = [${tomlString(cliPath)}]`,
|
|
291
|
+
"required = true",
|
|
292
|
+
...rootExtras,
|
|
293
|
+
"",
|
|
294
|
+
"[mcp_servers.algohq.env]",
|
|
295
|
+
`VO_CONTROL_PLANE_URL = ${tomlString(controlPlaneUrl)}`,
|
|
296
|
+
...envExtras,
|
|
297
|
+
MANAGED_END
|
|
298
|
+
].join(eol);
|
|
299
|
+
return `${base}${base ? `${eol}${eol}` : ""}${block}${eol}`;
|
|
300
|
+
}
|
|
301
|
+
function installCodexMcpConfigAt(configPath, cliPath, controlPlaneUrl, log) {
|
|
302
|
+
const exists = existsSync2(configPath);
|
|
303
|
+
const raw = exists ? readFileSync2(configPath, "utf8") : "";
|
|
304
|
+
let rendered;
|
|
340
305
|
try {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
const parsed = JSON.parse(await res.text());
|
|
354
|
-
const token = typeof parsed.token === "string" ? parsed.token : "";
|
|
355
|
-
const expiresAt = typeof parsed.expires_at === "string" ? parsed.expires_at : "";
|
|
356
|
-
if (!token.startsWith("vocred_") || !expiresAt) return null;
|
|
357
|
-
return { vo_credential: token, expires_at: expiresAt };
|
|
358
|
-
} catch {
|
|
359
|
-
return null;
|
|
306
|
+
rendered = renderCodexMcpConfig(raw, cliPath, controlPlaneUrl);
|
|
307
|
+
} catch (error) {
|
|
308
|
+
if (exists) {
|
|
309
|
+
const backupPath = `${configPath}.backup-${Date.now()}`;
|
|
310
|
+
copyFileSync(configPath, backupPath);
|
|
311
|
+
log(` Backed up malformed Codex config \u2192 ${backupPath}`);
|
|
312
|
+
}
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
315
|
+
if (rendered === raw) {
|
|
316
|
+
log(` Codex already current: ${configPath}`);
|
|
317
|
+
return;
|
|
360
318
|
}
|
|
319
|
+
if (exists) {
|
|
320
|
+
const backupPath = `${configPath}.backup-${Date.now()}`;
|
|
321
|
+
copyFileSync(configPath, backupPath);
|
|
322
|
+
log(` Backed up Codex config \u2192 ${backupPath}`);
|
|
323
|
+
}
|
|
324
|
+
mkdirSync2(dirname2(configPath), { recursive: true });
|
|
325
|
+
writeFileSync2(configPath, rendered, "utf8");
|
|
326
|
+
log(`\u2713 Wrote AlgoHQ MCP to Codex: ${configPath}`);
|
|
361
327
|
}
|
|
362
328
|
|
|
363
329
|
// src/autostart.ts
|
|
364
|
-
import { homedir as homedir2, platform } from "node:os";
|
|
365
|
-
import { join as
|
|
366
|
-
import { existsSync as
|
|
330
|
+
import { homedir as homedir2, platform as platform2 } from "node:os";
|
|
331
|
+
import { isAbsolute, join as join3 } from "node:path";
|
|
332
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, readFileSync as readFileSync3, unlinkSync, copyFileSync as copyFileSync2 } from "node:fs";
|
|
333
|
+
var WINDOWS_RESTART_BACKOFF_MS = 1e4;
|
|
334
|
+
var WINDOWS_HEALTHY_RUN_MS = 6e4;
|
|
335
|
+
var WINDOWS_MAX_BACKOFF_MS = 3e5;
|
|
367
336
|
function resolveRunnerCommand(override) {
|
|
368
337
|
return override ?? "vo-mcp runner";
|
|
369
338
|
}
|
|
339
|
+
function quotePosixShellArgument(value) {
|
|
340
|
+
if (value.includes("\0") || value.includes("\r") || value.includes("\n")) {
|
|
341
|
+
throw new Error("Runner command must not contain NUL, carriage return, or newline characters.");
|
|
342
|
+
}
|
|
343
|
+
return `'${value.replace(/'/gu, `'"'"'`)}'`;
|
|
344
|
+
}
|
|
345
|
+
function resolveLinuxConfigHome(home, env) {
|
|
346
|
+
const configured = env["XDG_CONFIG_HOME"]?.trim();
|
|
347
|
+
return configured && isAbsolute(configured) ? configured : join3(home, ".config");
|
|
348
|
+
}
|
|
349
|
+
function launcherIsCurrent(path, desiredContent, label, log) {
|
|
350
|
+
if (!existsSync3(path)) return false;
|
|
351
|
+
if (readFileSync3(path, "utf8") === desiredContent) return true;
|
|
352
|
+
const backupPath = `${path}.backup-${Date.now()}`;
|
|
353
|
+
copyFileSync2(path, backupPath);
|
|
354
|
+
log(` Backed up existing ${label} to: ${backupPath}`);
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
370
357
|
function installWindowsAutostart(runnerCommand, log, env) {
|
|
371
|
-
const appData = env["APPDATA"] ??
|
|
372
|
-
const startupDir =
|
|
373
|
-
|
|
374
|
-
const launcherPath =
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
log(`
|
|
380
|
-
|
|
358
|
+
const appData = env["APPDATA"] ?? join3(homedir2(), "AppData", "Roaming");
|
|
359
|
+
const startupDir = join3(appData, "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
|
|
360
|
+
mkdirSync3(startupDir, { recursive: true });
|
|
361
|
+
const launcherPath = join3(startupDir, "vo-runner.vbs");
|
|
362
|
+
const legacyCmdPath = join3(startupDir, "vo-runner.cmd");
|
|
363
|
+
if (existsSync3(legacyCmdPath)) {
|
|
364
|
+
try {
|
|
365
|
+
unlinkSync(legacyCmdPath);
|
|
366
|
+
log(` Removed legacy minimized .cmd launcher: ${legacyCmdPath}`);
|
|
367
|
+
} catch (error) {
|
|
368
|
+
log(`\u26A0 Could not remove legacy launcher ${legacyCmdPath} (${error instanceof Error ? error.message : String(error)}); remove it manually to avoid a double start`);
|
|
381
369
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
370
|
+
}
|
|
371
|
+
const hiddenCommand = `cmd /c ${runnerCommand} >> "%USERPROFILE%\\.claude\\vo-runner.log" 2>&1`.replace(/"/g, '""');
|
|
372
|
+
const launcherContent = `' Auto-start launcher for vo-mcp runner
|
|
373
|
+
' Created by vo-mcp autostart installer
|
|
374
|
+
' Keepalive supervisor: restarts the runner if it exits (parity with launchd
|
|
375
|
+
' KeepAlive on macOS and systemd Restart=on-failure on Linux).
|
|
376
|
+
' To stop: create %USERPROFILE%\\.claude\\vo-runner.stop, or end wscript.exe.
|
|
377
|
+
Dim sh, fso, stopFile, backoff, startedAt, ranMs
|
|
378
|
+
Set sh = CreateObject("WScript.Shell")
|
|
379
|
+
Set fso = CreateObject("Scripting.FileSystemObject")
|
|
380
|
+
sh.CurrentDirectory = sh.ExpandEnvironmentStrings("%USERPROFILE%")
|
|
381
|
+
sh.Environment("Process")("VO_CODE_RUNNER_CLONES_ROOT") = sh.ExpandEnvironmentStrings("%APPDATA%\\ai.algosuite.vo-runner\\clones")
|
|
382
|
+
stopFile = sh.ExpandEnvironmentStrings("%USERPROFILE%\\.claude\\vo-runner.stop")
|
|
383
|
+
backoff = ${WINDOWS_RESTART_BACKOFF_MS}
|
|
384
|
+
Do
|
|
385
|
+
If fso.FileExists(stopFile) Then
|
|
386
|
+
fso.DeleteFile stopFile
|
|
387
|
+
WScript.Quit 0
|
|
388
|
+
End If
|
|
389
|
+
startedAt = Timer
|
|
390
|
+
sh.Run "${hiddenCommand}", 0, True
|
|
391
|
+
ranMs = (Timer - startedAt) * 1000
|
|
392
|
+
If ranMs < 0 Then ranMs = ${WINDOWS_HEALTHY_RUN_MS}
|
|
393
|
+
If ranMs >= ${WINDOWS_HEALTHY_RUN_MS} Then
|
|
394
|
+
backoff = ${WINDOWS_RESTART_BACKOFF_MS}
|
|
395
|
+
ElseIf backoff < ${WINDOWS_MAX_BACKOFF_MS} Then
|
|
396
|
+
backoff = backoff * 2
|
|
397
|
+
End If
|
|
398
|
+
WScript.Sleep backoff
|
|
399
|
+
Loop
|
|
390
400
|
`;
|
|
391
|
-
|
|
401
|
+
if (launcherIsCurrent(launcherPath, launcherContent, "launcher", log)) {
|
|
402
|
+
log(`\u2713 Auto-start is already configured (Windows Startup folder)`);
|
|
403
|
+
log(` Path: ${launcherPath}`);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
writeFileSync3(launcherPath, launcherContent, "utf8");
|
|
392
407
|
log(`\u2713 Installed Windows auto-start launcher`);
|
|
393
408
|
log(` Path: ${launcherPath}`);
|
|
394
|
-
log(` The runner will start
|
|
395
|
-
}
|
|
396
|
-
async function installMacAutostart(runnerCommand, log) {
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
const existing = readFileSync2(plistPath, "utf8");
|
|
402
|
-
if (existing.includes("vo-mcp runner")) {
|
|
403
|
-
log(`\u2713 Auto-start is already configured (launchd)`);
|
|
404
|
-
log(` Path: ${plistPath}`);
|
|
405
|
-
return;
|
|
406
|
-
}
|
|
407
|
-
const backupPath = `${plistPath}.backup-${Date.now()}`;
|
|
408
|
-
copyFileSync(plistPath, backupPath);
|
|
409
|
-
log(` Backed up existing plist to: ${backupPath}`);
|
|
410
|
-
}
|
|
409
|
+
log(` The runner will start hidden at next login.`);
|
|
410
|
+
}
|
|
411
|
+
async function installMacAutostart(runnerCommand, log, env) {
|
|
412
|
+
const home = env["HOME"]?.trim() || homedir2();
|
|
413
|
+
const launchAgentsDir = join3(home, "Library", "LaunchAgents");
|
|
414
|
+
mkdirSync3(launchAgentsDir, { recursive: true });
|
|
415
|
+
const plistPath = join3(launchAgentsDir, "ai.algosuite.vo-runner.plist");
|
|
411
416
|
const parts = runnerCommand.split(/\s+/);
|
|
412
417
|
const program = parts[0] ?? "vo-mcp";
|
|
413
418
|
const args = parts.length > 1 ? parts.slice(1) : ["runner"];
|
|
@@ -426,21 +431,33 @@ ${args.map((a) => ` <string>${a}</string>`).join("\n")}
|
|
|
426
431
|
<true/>
|
|
427
432
|
<key>KeepAlive</key>
|
|
428
433
|
<true/>
|
|
434
|
+
<key>WorkingDirectory</key>
|
|
435
|
+
<string>${home}</string>
|
|
436
|
+
<key>EnvironmentVariables</key>
|
|
437
|
+
<dict>
|
|
438
|
+
<key>VO_CODE_RUNNER_CLONES_ROOT</key>
|
|
439
|
+
<string>${join3(home, "Library", "Application Support", "ai.algosuite.vo-runner", "clones")}</string>
|
|
440
|
+
</dict>
|
|
429
441
|
<key>StandardOutPath</key>
|
|
430
|
-
<string>${
|
|
442
|
+
<string>${join3(home, ".claude", "vo-runner.log")}</string>
|
|
431
443
|
<key>StandardErrorPath</key>
|
|
432
|
-
<string>${
|
|
444
|
+
<string>${join3(home, ".claude", "vo-runner-error.log")}</string>
|
|
433
445
|
</dict>
|
|
434
446
|
</plist>
|
|
435
447
|
`;
|
|
436
|
-
|
|
448
|
+
if (launcherIsCurrent(plistPath, plistContent, "plist", log)) {
|
|
449
|
+
log(`\u2713 Auto-start is already configured (launchd)`);
|
|
450
|
+
log(` Path: ${plistPath}`);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
writeFileSync3(plistPath, plistContent, "utf8");
|
|
437
454
|
log(`\u2713 Installed launchd plist`);
|
|
438
455
|
log(` Path: ${plistPath}`);
|
|
439
456
|
try {
|
|
440
457
|
const { execSync } = await import("node:child_process");
|
|
441
458
|
execSync(`launchctl load "${plistPath}"`, { stdio: "ignore" });
|
|
442
459
|
log(`\u2713 Loaded plist with launchctl (runner will start at next login)`);
|
|
443
|
-
log(` Logs: ${
|
|
460
|
+
log(` Logs: ${join3(home, ".claude", "vo-runner.log")}`);
|
|
444
461
|
} catch {
|
|
445
462
|
log(`\u26A0 Failed to load plist with launchctl (you may need to load it manually)`);
|
|
446
463
|
log(` Run: launchctl load "${plistPath}"`);
|
|
@@ -448,31 +465,24 @@ ${args.map((a) => ` <string>${a}</string>`).join("\n")}
|
|
|
448
465
|
}
|
|
449
466
|
async function installLinuxAutostart(runnerCommand, log, env) {
|
|
450
467
|
const home = env["HOME"]?.trim() || homedir2();
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
461
|
-
const backupPath = `${unitPath}.backup-${Date.now()}`;
|
|
462
|
-
copyFileSync(unitPath, backupPath);
|
|
463
|
-
log(` Backed up existing unit to: ${backupPath}`);
|
|
464
|
-
}
|
|
465
|
-
const logFile = join2(home, ".claude", "vo-runner.log");
|
|
466
|
-
const errFile = join2(home, ".claude", "vo-runner-error.log");
|
|
467
|
-
mkdirSync2(join2(home, ".claude"), { recursive: true });
|
|
468
|
+
const configHome = resolveLinuxConfigHome(home, env);
|
|
469
|
+
const unitDir = join3(configHome, "systemd", "user");
|
|
470
|
+
mkdirSync3(unitDir, { recursive: true });
|
|
471
|
+
const unitPath = join3(unitDir, "vo-runner.service");
|
|
472
|
+
const logFile = join3(home, ".claude", "vo-runner.log");
|
|
473
|
+
const errFile = join3(home, ".claude", "vo-runner-error.log");
|
|
474
|
+
const quotedRunnerCommand = quotePosixShellArgument(runnerCommand);
|
|
475
|
+
mkdirSync3(join3(home, ".claude"), { recursive: true });
|
|
468
476
|
const unit = `[Unit]
|
|
469
|
-
Description=
|
|
477
|
+
Description=AlgoHQ Code Runner (vo-mcp)
|
|
470
478
|
After=network-online.target
|
|
471
479
|
Wants=network-online.target
|
|
472
480
|
|
|
473
481
|
[Service]
|
|
474
482
|
Type=simple
|
|
475
|
-
|
|
483
|
+
WorkingDirectory=${home}
|
|
484
|
+
Environment="VO_CODE_RUNNER_CLONES_ROOT=${join3(configHome, "ai.algosuite.vo-runner", "clones")}"
|
|
485
|
+
ExecStart=/bin/sh -lc ${quotedRunnerCommand}
|
|
476
486
|
Restart=on-failure
|
|
477
487
|
RestartSec=10
|
|
478
488
|
StandardOutput=append:${logFile}
|
|
@@ -481,7 +491,12 @@ StandardError=append:${errFile}
|
|
|
481
491
|
[Install]
|
|
482
492
|
WantedBy=default.target
|
|
483
493
|
`;
|
|
484
|
-
|
|
494
|
+
if (launcherIsCurrent(unitPath, unit, "unit", log)) {
|
|
495
|
+
log(`\u2713 Auto-start is already configured (systemd user unit)`);
|
|
496
|
+
log(` Path: ${unitPath}`);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
writeFileSync3(unitPath, unit, "utf8");
|
|
485
500
|
log(`\u2713 Installed systemd user unit`);
|
|
486
501
|
log(` Path: ${unitPath}`);
|
|
487
502
|
if (process.env["VITEST"]) {
|
|
@@ -503,11 +518,11 @@ async function installAutostart(opts = {}) {
|
|
|
503
518
|
const log = opts.log ?? ((m) => console.error(m));
|
|
504
519
|
const env = opts.env ?? process.env;
|
|
505
520
|
const runnerCommand = resolveRunnerCommand(opts.runnerCommand);
|
|
506
|
-
const plat = platform();
|
|
521
|
+
const plat = opts.platform ?? platform2();
|
|
507
522
|
if (plat === "win32") {
|
|
508
523
|
installWindowsAutostart(runnerCommand, log, env);
|
|
509
524
|
} else if (plat === "darwin") {
|
|
510
|
-
await installMacAutostart(runnerCommand, log);
|
|
525
|
+
await installMacAutostart(runnerCommand, log, env);
|
|
511
526
|
} else if (plat === "linux") {
|
|
512
527
|
await installLinuxAutostart(runnerCommand, log, env);
|
|
513
528
|
} else {
|
|
@@ -517,23 +532,23 @@ async function installAutostart(opts = {}) {
|
|
|
517
532
|
}
|
|
518
533
|
|
|
519
534
|
// src/install.ts
|
|
520
|
-
var
|
|
535
|
+
var DEFAULT_CONTROL_PLANE_URL2 = "https://vo-control-plane-bzjphrajaq-uc.a.run.app";
|
|
521
536
|
function resolveCodeConfigPath(home) {
|
|
522
|
-
return
|
|
537
|
+
return join4(home, ".claude.json");
|
|
523
538
|
}
|
|
524
539
|
function resolveDesktopConfigPath(home, plat, appData) {
|
|
525
540
|
if (plat === "win32") {
|
|
526
|
-
return
|
|
541
|
+
return join4(appData ?? join4(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
527
542
|
}
|
|
528
543
|
if (plat === "darwin") {
|
|
529
|
-
return
|
|
544
|
+
return join4(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
530
545
|
}
|
|
531
|
-
return
|
|
546
|
+
return join4(home, ".config", "Claude", "claude_desktop_config.json");
|
|
532
547
|
}
|
|
533
548
|
function readClaudeConfig(path) {
|
|
534
549
|
try {
|
|
535
|
-
if (!
|
|
536
|
-
const raw =
|
|
550
|
+
if (!existsSync4(path)) return {};
|
|
551
|
+
const raw = readFileSync4(path, "utf8");
|
|
537
552
|
const parsed = JSON.parse(raw);
|
|
538
553
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
539
554
|
} catch {
|
|
@@ -541,16 +556,12 @@ function readClaudeConfig(path) {
|
|
|
541
556
|
}
|
|
542
557
|
}
|
|
543
558
|
function writeClaudeConfig(path, config) {
|
|
544
|
-
|
|
545
|
-
|
|
559
|
+
mkdirSync4(dirname3(path), { recursive: true });
|
|
560
|
+
writeFileSync4(path, `${JSON.stringify(config, null, 2)}
|
|
546
561
|
`, "utf8");
|
|
547
562
|
}
|
|
548
563
|
function resolveVoMcpCliPath() {
|
|
549
|
-
|
|
550
|
-
if (scriptPath.includes("install-cli.js") || scriptPath.includes("install-cli")) {
|
|
551
|
-
return scriptPath.replace(/install-cli\.js$/, "cli.js").replace(/install-cli$/, "cli.js");
|
|
552
|
-
}
|
|
553
|
-
return resolve(dirname2(scriptPath), "cli.js");
|
|
564
|
+
return join4(dirname3(fileURLToPath(import.meta.url)), "cli.js");
|
|
554
565
|
}
|
|
555
566
|
function installMcpConfigAt(configPath, cliPath, controlPlaneUrl, log, label) {
|
|
556
567
|
const existing = readClaudeConfig(configPath);
|
|
@@ -560,9 +571,9 @@ function installMcpConfigAt(configPath, cliPath, controlPlaneUrl, log, label) {
|
|
|
560
571
|
log(` ${label} already current: ${configPath}`);
|
|
561
572
|
return;
|
|
562
573
|
}
|
|
563
|
-
if (
|
|
574
|
+
if (existsSync4(configPath)) {
|
|
564
575
|
const backupPath = `${configPath}.backup-${Date.now()}`;
|
|
565
|
-
|
|
576
|
+
copyFileSync3(configPath, backupPath);
|
|
566
577
|
log(` Backed up ${label} config \u2192 ${backupPath}`);
|
|
567
578
|
}
|
|
568
579
|
const merged = {
|
|
@@ -584,55 +595,54 @@ function installMcpConfigAt(configPath, cliPath, controlPlaneUrl, log, label) {
|
|
|
584
595
|
function installMcpConfig(log, env) {
|
|
585
596
|
const home = env["HOME"]?.trim() || env["USERPROFILE"]?.trim() || homedir3();
|
|
586
597
|
const appData = env["APPDATA"]?.trim();
|
|
587
|
-
const plat =
|
|
598
|
+
const plat = platform3();
|
|
588
599
|
const cliPath = resolveVoMcpCliPath();
|
|
589
|
-
const controlPlaneUrl = env["VO_CONTROL_PLANE_URL"]?.trim() ||
|
|
600
|
+
const controlPlaneUrl = env["VO_CONTROL_PLANE_URL"]?.trim() || DEFAULT_CONTROL_PLANE_URL2;
|
|
590
601
|
installMcpConfigAt(resolveCodeConfigPath(home), cliPath, controlPlaneUrl, log, "Claude Code CLI");
|
|
591
602
|
installMcpConfigAt(resolveDesktopConfigPath(home, plat, appData), cliPath, controlPlaneUrl, log, "Claude Desktop");
|
|
603
|
+
installCodexMcpConfigAt(resolveCodexConfigPath(home), cliPath, controlPlaneUrl, log);
|
|
592
604
|
}
|
|
593
|
-
async function
|
|
594
|
-
log("\n\u2501\u2501\u2501 Step 2: Link your
|
|
595
|
-
log("
|
|
596
|
-
log("Your raw token never persists
|
|
597
|
-
const controlPlaneUrl = env["VO_CONTROL_PLANE_URL"]?.trim() || "https://vo-control-plane-bzjphrajaq-uc.a.run.app";
|
|
598
|
-
const exchange = async (refreshToken, apiKey) => {
|
|
599
|
-
return exchangeForVoCredential({
|
|
600
|
-
refreshToken,
|
|
601
|
-
apiKey,
|
|
602
|
-
controlPlaneUrl,
|
|
603
|
-
label: `vo-mcp install (${platform2()})`
|
|
604
|
-
});
|
|
605
|
-
};
|
|
605
|
+
async function runPairFlow(log, env) {
|
|
606
|
+
log("\n\u2501\u2501\u2501 Step 2: Link your AlgoHQ account (device code) \u2501\u2501\u2501");
|
|
607
|
+
log("A short code appears below \u2014 open the URL it prints and enter the code.");
|
|
608
|
+
log("Your raw token never persists; a scoped credential is stored in your OS keychain.\n");
|
|
606
609
|
try {
|
|
607
|
-
const result = await
|
|
608
|
-
log(
|
|
610
|
+
const result = await runPairing({ env, log });
|
|
611
|
+
log(`
|
|
612
|
+
\u2713 Paired \u2014 scoped credential stored at: ${result.credentialPath}`);
|
|
609
613
|
} catch (err) {
|
|
610
|
-
log(
|
|
611
|
-
|
|
612
|
-
|
|
614
|
+
log(`
|
|
615
|
+
\u26A0 Pairing didn't complete: ${err instanceof Error ? err.message : String(err)}`);
|
|
616
|
+
log(" No problem \u2014 the rest will finish; pair anytime with: vo-mcp pair");
|
|
613
617
|
}
|
|
614
618
|
}
|
|
615
|
-
function printNextSteps(log, autostartInstalled) {
|
|
619
|
+
function printNextSteps(log, autostartInstalled, configOnly) {
|
|
616
620
|
log("\n\u2501\u2501\u2501 Installation complete! \u2501\u2501\u2501\n");
|
|
617
621
|
log("What's configured:");
|
|
618
|
-
log(" \u2713 Claude Desktop
|
|
619
|
-
|
|
622
|
+
log(" \u2713 Claude Desktop, Claude Code, and Codex will load AlgoHQ MCP on next restart");
|
|
623
|
+
if (configOnly) {
|
|
624
|
+
log(" \u2713 Existing pairing and runner auto-start settings were left unchanged");
|
|
625
|
+
} else {
|
|
626
|
+
log(" \u2713 Your scoped credential is stored (revocable via the dashboard)");
|
|
627
|
+
}
|
|
620
628
|
if (autostartInstalled) {
|
|
621
629
|
log(" \u2713 Runner daemon will start automatically at login\n");
|
|
622
630
|
} else {
|
|
623
631
|
log("\n");
|
|
624
632
|
}
|
|
625
633
|
log("Next steps:");
|
|
626
|
-
log(" 1. Restart Claude Desktop / Claude Code (if running).");
|
|
627
|
-
if (
|
|
634
|
+
log(" 1. Restart Claude Desktop / Claude Code / Codex (if running).");
|
|
635
|
+
if (configOnly) {
|
|
636
|
+
log(" 2. Restart the existing AlgoHQ runner service or runner terminal.");
|
|
637
|
+
} else if (autostartInstalled) {
|
|
628
638
|
log(" 2. Log out and back in (or start the runner manually now: vo-mcp runner)");
|
|
629
639
|
} else {
|
|
630
640
|
log(" 2. Start the agent runner in a terminal (keep it running):");
|
|
631
641
|
log(" vo-mcp runner");
|
|
632
642
|
log(" (To set up auto-start at login: vo-mcp runner --install-autostart)");
|
|
633
643
|
}
|
|
634
|
-
log(" 3. Visit
|
|
635
|
-
log(" https://algosuite.ai/
|
|
644
|
+
log(" 3. Visit AlgoHQ to dispatch your first agent:");
|
|
645
|
+
log(" https://algosuite.ai/algohq\n");
|
|
636
646
|
log("The runner watches for tasks you dispatch and spins up agents in fresh worktrees.");
|
|
637
647
|
log("Agents only run while the runner is connected. Ctrl+C to stop it anytime.\n");
|
|
638
648
|
}
|
|
@@ -640,17 +650,19 @@ async function install(opts = {}) {
|
|
|
640
650
|
const log = opts.log ?? ((m) => console.error(m));
|
|
641
651
|
const env = opts.env ?? process.env;
|
|
642
652
|
log("\u2501\u2501\u2501 vo-mcp installer \u2501\u2501\u2501");
|
|
643
|
-
log("This will set up your machine to dispatch
|
|
644
|
-
log("\u2501\u2501\u2501 Step 1: Configure Claude Desktop / Claude Code \u2501\u2501\u2501");
|
|
653
|
+
log("This will set up your machine to dispatch AlgoHQ agents from anywhere.\n");
|
|
654
|
+
log("\u2501\u2501\u2501 Step 1: Configure Claude Desktop / Claude Code / Codex \u2501\u2501\u2501");
|
|
645
655
|
installMcpConfig(log, env);
|
|
646
656
|
if (!opts.skipLogin) {
|
|
647
|
-
await
|
|
657
|
+
await runPairFlow(log, env);
|
|
658
|
+
} else if (opts.configOnly) {
|
|
659
|
+
log("\n(Config-only refresh \u2014 existing pairing left unchanged.)");
|
|
648
660
|
} else {
|
|
649
|
-
log("\n(
|
|
661
|
+
log("\n(Pairing skipped \u2014 run `vo-mcp pair` when ready.)");
|
|
650
662
|
}
|
|
651
663
|
let autostartInstalled = false;
|
|
652
664
|
if (!opts.skipAutostart) {
|
|
653
|
-
const plat =
|
|
665
|
+
const plat = platform3();
|
|
654
666
|
if (plat === "win32" || plat === "darwin" || plat === "linux") {
|
|
655
667
|
log("\n\u2501\u2501\u2501 Step 3: Set up auto-start \u2501\u2501\u2501");
|
|
656
668
|
log("Would you like the runner daemon to start automatically at login?");
|
|
@@ -659,11 +671,15 @@ async function install(opts = {}) {
|
|
|
659
671
|
autostartInstalled = true;
|
|
660
672
|
}
|
|
661
673
|
}
|
|
662
|
-
printNextSteps(log, autostartInstalled);
|
|
674
|
+
printNextSteps(log, autostartInstalled, opts.configOnly === true);
|
|
675
|
+
}
|
|
676
|
+
function installOptionsFromArgs(args) {
|
|
677
|
+
const configOnly = args.includes("--config-only");
|
|
678
|
+
return configOnly ? { configOnly: true, skipLogin: true, skipAutostart: true } : {};
|
|
663
679
|
}
|
|
664
680
|
|
|
665
681
|
// src/install-cli.ts
|
|
666
|
-
install().catch((err) => {
|
|
682
|
+
install(installOptionsFromArgs(process.argv.slice(2))).catch((err) => {
|
|
667
683
|
console.error("[vo-mcp install] fatal:", err);
|
|
668
684
|
process.exit(1);
|
|
669
685
|
});
|