@algosuite/vo-mcp 0.2.0-beta.0 → 0.2.0-beta.10
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 +178 -153
- package/bin/vo-mcp +44 -33
- package/dist/autostart-cli.js +79 -2
- package/dist/autostart-cli.js.map +2 -2
- package/dist/cli.js +1595 -848
- package/dist/cli.js.map +4 -4
- package/dist/index.js +519 -115
- package/dist/index.js.map +4 -4
- package/dist/install-cli.js +351 -322
- package/dist/install-cli.js.map +4 -4
- package/dist/login-cli.js +3 -3
- package/dist/login-cli.js.map +2 -2
- package/dist/pair-cli.js +214 -0
- package/dist/pair-cli.js.map +7 -0
- package/dist/runner-cli.js +7145 -837
- package/dist/runner-cli.js.map +4 -4
- package/dist/runner-supervisor.js +727 -0
- package/dist/runner-supervisor.js.map +7 -0
- package/dist/set-key-cli.js +204 -0
- package/dist/set-key-cli.js.map +7 -0
- package/dist/supervisor-credential-helper.js +227 -0
- package/dist/supervisor-credential-helper.js.map +7 -0
- package/dist/update-cli.js +62 -0
- package/dist/update-cli.js.map +7 -0
- package/package.json +4 -2
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,262 +123,230 @@ 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
|
-
headers: { "content-type": "application/json", authorization: `Bearer ${idToken}` },
|
|
350
|
-
body: JSON.stringify(opts.label ? { label: opts.label } : {})
|
|
351
|
-
});
|
|
352
|
-
if (res.status !== 200) return null;
|
|
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;
|
|
360
314
|
}
|
|
315
|
+
if (rendered === raw) {
|
|
316
|
+
log(` Codex already current: ${configPath}`);
|
|
317
|
+
return;
|
|
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 { 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";
|
|
367
333
|
function resolveRunnerCommand(override) {
|
|
368
334
|
return override ?? "vo-mcp runner";
|
|
369
335
|
}
|
|
370
336
|
function installWindowsAutostart(runnerCommand, log, env) {
|
|
371
|
-
const appData = env["APPDATA"] ??
|
|
372
|
-
const startupDir =
|
|
373
|
-
|
|
374
|
-
const launcherPath =
|
|
375
|
-
if (
|
|
376
|
-
const existing =
|
|
337
|
+
const appData = env["APPDATA"] ?? join3(homedir2(), "AppData", "Roaming");
|
|
338
|
+
const startupDir = join3(appData, "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
|
|
339
|
+
mkdirSync3(startupDir, { recursive: true });
|
|
340
|
+
const launcherPath = join3(startupDir, "vo-runner.cmd");
|
|
341
|
+
if (existsSync3(launcherPath)) {
|
|
342
|
+
const existing = readFileSync3(launcherPath, "utf8");
|
|
377
343
|
if (existing.includes("vo-mcp runner")) {
|
|
378
344
|
log(`\u2713 Auto-start is already configured (Windows Startup folder)`);
|
|
379
345
|
log(` Path: ${launcherPath}`);
|
|
380
346
|
return;
|
|
381
347
|
}
|
|
382
348
|
const backupPath = `${launcherPath}.backup-${Date.now()}`;
|
|
383
|
-
|
|
349
|
+
copyFileSync2(launcherPath, backupPath);
|
|
384
350
|
log(` Backed up existing launcher to: ${backupPath}`);
|
|
385
351
|
}
|
|
386
352
|
const launcherContent = `@echo off
|
|
@@ -388,24 +354,24 @@ REM Auto-start launcher for vo-mcp runner
|
|
|
388
354
|
REM Created by vo-mcp autostart installer
|
|
389
355
|
start /min cmd /c "${runnerCommand}"
|
|
390
356
|
`;
|
|
391
|
-
|
|
357
|
+
writeFileSync3(launcherPath, launcherContent, "utf8");
|
|
392
358
|
log(`\u2713 Installed Windows auto-start launcher`);
|
|
393
359
|
log(` Path: ${launcherPath}`);
|
|
394
360
|
log(` The runner will start minimized at next login.`);
|
|
395
361
|
}
|
|
396
362
|
async function installMacAutostart(runnerCommand, log) {
|
|
397
|
-
const launchAgentsDir =
|
|
398
|
-
|
|
399
|
-
const plistPath =
|
|
400
|
-
if (
|
|
401
|
-
const existing =
|
|
363
|
+
const launchAgentsDir = join3(homedir2(), "Library", "LaunchAgents");
|
|
364
|
+
mkdirSync3(launchAgentsDir, { recursive: true });
|
|
365
|
+
const plistPath = join3(launchAgentsDir, "ai.algosuite.vo-runner.plist");
|
|
366
|
+
if (existsSync3(plistPath)) {
|
|
367
|
+
const existing = readFileSync3(plistPath, "utf8");
|
|
402
368
|
if (existing.includes("vo-mcp runner")) {
|
|
403
369
|
log(`\u2713 Auto-start is already configured (launchd)`);
|
|
404
370
|
log(` Path: ${plistPath}`);
|
|
405
371
|
return;
|
|
406
372
|
}
|
|
407
373
|
const backupPath = `${plistPath}.backup-${Date.now()}`;
|
|
408
|
-
|
|
374
|
+
copyFileSync2(plistPath, backupPath);
|
|
409
375
|
log(` Backed up existing plist to: ${backupPath}`);
|
|
410
376
|
}
|
|
411
377
|
const parts = runnerCommand.split(/\s+/);
|
|
@@ -427,150 +393,207 @@ ${args.map((a) => ` <string>${a}</string>`).join("\n")}
|
|
|
427
393
|
<key>KeepAlive</key>
|
|
428
394
|
<true/>
|
|
429
395
|
<key>StandardOutPath</key>
|
|
430
|
-
<string>${
|
|
396
|
+
<string>${join3(homedir2(), ".claude", "vo-runner.log")}</string>
|
|
431
397
|
<key>StandardErrorPath</key>
|
|
432
|
-
<string>${
|
|
398
|
+
<string>${join3(homedir2(), ".claude", "vo-runner-error.log")}</string>
|
|
433
399
|
</dict>
|
|
434
400
|
</plist>
|
|
435
401
|
`;
|
|
436
|
-
|
|
402
|
+
writeFileSync3(plistPath, plistContent, "utf8");
|
|
437
403
|
log(`\u2713 Installed launchd plist`);
|
|
438
404
|
log(` Path: ${plistPath}`);
|
|
439
405
|
try {
|
|
440
406
|
const { execSync } = await import("node:child_process");
|
|
441
407
|
execSync(`launchctl load "${plistPath}"`, { stdio: "ignore" });
|
|
442
408
|
log(`\u2713 Loaded plist with launchctl (runner will start at next login)`);
|
|
443
|
-
log(` Logs: ${
|
|
409
|
+
log(` Logs: ${join3(homedir2(), ".claude", "vo-runner.log")}`);
|
|
444
410
|
} catch {
|
|
445
411
|
log(`\u26A0 Failed to load plist with launchctl (you may need to load it manually)`);
|
|
446
412
|
log(` Run: launchctl load "${plistPath}"`);
|
|
447
413
|
}
|
|
448
414
|
}
|
|
415
|
+
async function installLinuxAutostart(runnerCommand, log, env) {
|
|
416
|
+
const home = env["HOME"]?.trim() || homedir2();
|
|
417
|
+
const unitDir = join3(home, ".config", "systemd", "user");
|
|
418
|
+
mkdirSync3(unitDir, { recursive: true });
|
|
419
|
+
const unitPath = join3(unitDir, "vo-runner.service");
|
|
420
|
+
if (existsSync3(unitPath)) {
|
|
421
|
+
const existing = readFileSync3(unitPath, "utf8");
|
|
422
|
+
if (existing.includes(runnerCommand) || existing.includes("vo-mcp runner")) {
|
|
423
|
+
log(`\u2713 Auto-start is already configured (systemd user unit)`);
|
|
424
|
+
log(` Path: ${unitPath}`);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
const backupPath = `${unitPath}.backup-${Date.now()}`;
|
|
428
|
+
copyFileSync2(unitPath, backupPath);
|
|
429
|
+
log(` Backed up existing unit to: ${backupPath}`);
|
|
430
|
+
}
|
|
431
|
+
const logFile = join3(home, ".claude", "vo-runner.log");
|
|
432
|
+
const errFile = join3(home, ".claude", "vo-runner-error.log");
|
|
433
|
+
mkdirSync3(join3(home, ".claude"), { recursive: true });
|
|
434
|
+
const unit = `[Unit]
|
|
435
|
+
Description=AlgoHQ Code Runner (vo-mcp)
|
|
436
|
+
After=network-online.target
|
|
437
|
+
Wants=network-online.target
|
|
438
|
+
|
|
439
|
+
[Service]
|
|
440
|
+
Type=simple
|
|
441
|
+
ExecStart=/bin/sh -lc '${runnerCommand}'
|
|
442
|
+
Restart=on-failure
|
|
443
|
+
RestartSec=10
|
|
444
|
+
StandardOutput=append:${logFile}
|
|
445
|
+
StandardError=append:${errFile}
|
|
446
|
+
|
|
447
|
+
[Install]
|
|
448
|
+
WantedBy=default.target
|
|
449
|
+
`;
|
|
450
|
+
writeFileSync3(unitPath, unit, "utf8");
|
|
451
|
+
log(`\u2713 Installed systemd user unit`);
|
|
452
|
+
log(` Path: ${unitPath}`);
|
|
453
|
+
if (process.env["VITEST"]) {
|
|
454
|
+
log(` (test mode: skipping systemctl enable)`);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
try {
|
|
458
|
+
const { execSync } = await import("node:child_process");
|
|
459
|
+
execSync("systemctl --user daemon-reload", { stdio: "ignore" });
|
|
460
|
+
execSync("systemctl --user enable --now vo-runner.service", { stdio: "ignore" });
|
|
461
|
+
log(`\u2713 Enabled + started vo-runner.service (starts at login)`);
|
|
462
|
+
log(` Logs: ${logFile}`);
|
|
463
|
+
} catch {
|
|
464
|
+
log(`\u26A0 Could not enable via systemctl (enable it manually):`);
|
|
465
|
+
log(` systemctl --user daemon-reload && systemctl --user enable --now vo-runner.service`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
449
468
|
async function installAutostart(opts = {}) {
|
|
450
469
|
const log = opts.log ?? ((m) => console.error(m));
|
|
451
470
|
const env = opts.env ?? process.env;
|
|
452
471
|
const runnerCommand = resolveRunnerCommand(opts.runnerCommand);
|
|
453
|
-
const plat =
|
|
472
|
+
const plat = platform2();
|
|
454
473
|
if (plat === "win32") {
|
|
455
474
|
installWindowsAutostart(runnerCommand, log, env);
|
|
456
475
|
} else if (plat === "darwin") {
|
|
457
476
|
await installMacAutostart(runnerCommand, log);
|
|
477
|
+
} else if (plat === "linux") {
|
|
478
|
+
await installLinuxAutostart(runnerCommand, log, env);
|
|
458
479
|
} else {
|
|
459
480
|
log(`\u2717 Auto-start is not supported on platform: ${plat}`);
|
|
460
|
-
log(` Supported platforms: win32 (Windows), darwin (macOS)`);
|
|
481
|
+
log(` Supported platforms: win32 (Windows), darwin (macOS), linux (Linux)`);
|
|
461
482
|
}
|
|
462
483
|
}
|
|
463
484
|
|
|
464
485
|
// src/install.ts
|
|
465
|
-
var
|
|
466
|
-
function
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
486
|
+
var DEFAULT_CONTROL_PLANE_URL2 = "https://vo-control-plane-bzjphrajaq-uc.a.run.app";
|
|
487
|
+
function resolveCodeConfigPath(home) {
|
|
488
|
+
return join4(home, ".claude.json");
|
|
489
|
+
}
|
|
490
|
+
function resolveDesktopConfigPath(home, plat, appData) {
|
|
470
491
|
if (plat === "win32") {
|
|
471
|
-
|
|
472
|
-
return join3(appData, "Claude", "claude_desktop_config.json");
|
|
492
|
+
return join4(appData ?? join4(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
473
493
|
}
|
|
474
494
|
if (plat === "darwin") {
|
|
475
|
-
return
|
|
495
|
+
return join4(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
476
496
|
}
|
|
477
|
-
return
|
|
497
|
+
return join4(home, ".config", "Claude", "claude_desktop_config.json");
|
|
478
498
|
}
|
|
479
499
|
function readClaudeConfig(path) {
|
|
480
500
|
try {
|
|
481
|
-
if (!
|
|
482
|
-
const raw =
|
|
501
|
+
if (!existsSync4(path)) return {};
|
|
502
|
+
const raw = readFileSync4(path, "utf8");
|
|
483
503
|
const parsed = JSON.parse(raw);
|
|
484
|
-
return
|
|
504
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
485
505
|
} catch {
|
|
486
|
-
return {
|
|
506
|
+
return {};
|
|
487
507
|
}
|
|
488
508
|
}
|
|
489
509
|
function writeClaudeConfig(path, config) {
|
|
490
|
-
|
|
491
|
-
|
|
510
|
+
mkdirSync4(dirname3(path), { recursive: true });
|
|
511
|
+
writeFileSync4(path, `${JSON.stringify(config, null, 2)}
|
|
492
512
|
`, "utf8");
|
|
493
513
|
}
|
|
494
514
|
function resolveVoMcpCliPath() {
|
|
495
|
-
|
|
496
|
-
if (scriptPath.includes("install-cli.js") || scriptPath.includes("install-cli")) {
|
|
497
|
-
return scriptPath.replace(/install-cli\.js$/, "cli.js").replace(/install-cli$/, "cli.js");
|
|
498
|
-
}
|
|
499
|
-
return resolve(dirname2(scriptPath), "cli.js");
|
|
515
|
+
return join4(dirname3(fileURLToPath(import.meta.url)), "cli.js");
|
|
500
516
|
}
|
|
501
|
-
function
|
|
502
|
-
const configPath = resolveClaudeConfigPath();
|
|
517
|
+
function installMcpConfigAt(configPath, cliPath, controlPlaneUrl, log, label) {
|
|
503
518
|
const existing = readClaudeConfig(configPath);
|
|
504
|
-
const
|
|
505
|
-
const voEntry =
|
|
506
|
-
if (voEntry
|
|
507
|
-
log(
|
|
519
|
+
const mcpServers = existing["mcpServers"] && typeof existing["mcpServers"] === "object" ? existing["mcpServers"] : {};
|
|
520
|
+
const voEntry = mcpServers["vo"] ?? mcpServers["vo-mcp"];
|
|
521
|
+
if (voEntry?.args?.some((a) => a.includes(cliPath))) {
|
|
522
|
+
log(` ${label} already current: ${configPath}`);
|
|
508
523
|
return;
|
|
509
524
|
}
|
|
510
|
-
if (
|
|
525
|
+
if (existsSync4(configPath)) {
|
|
511
526
|
const backupPath = `${configPath}.backup-${Date.now()}`;
|
|
512
|
-
|
|
513
|
-
log(` Backed up
|
|
527
|
+
copyFileSync3(configPath, backupPath);
|
|
528
|
+
log(` Backed up ${label} config \u2192 ${backupPath}`);
|
|
514
529
|
}
|
|
515
|
-
const controlPlaneUrl = env["VO_CONTROL_PLANE_URL"]?.trim() || DEFAULT_CONTROL_PLANE_URL;
|
|
516
530
|
const merged = {
|
|
531
|
+
...existing,
|
|
517
532
|
mcpServers: {
|
|
518
|
-
...
|
|
533
|
+
...mcpServers,
|
|
519
534
|
"vo-mcp": {
|
|
520
535
|
command: "node",
|
|
521
536
|
args: [cliPath],
|
|
522
|
-
env
|
|
523
|
-
|
|
524
|
-
}
|
|
537
|
+
// Preserve any existing env the user added (e.g. model API keys) — only
|
|
538
|
+
// ensure VO_CONTROL_PLANE_URL is present. NEVER drop the user's env keys.
|
|
539
|
+
env: { VO_CONTROL_PLANE_URL: controlPlaneUrl, ...voEntry?.env ?? {} }
|
|
525
540
|
}
|
|
526
541
|
}
|
|
527
542
|
};
|
|
528
543
|
writeClaudeConfig(configPath, merged);
|
|
529
|
-
log(`\u2713 Wrote vo-mcp
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
const
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
+
log(`\u2713 Wrote vo-mcp to ${label}: ${configPath}`);
|
|
545
|
+
}
|
|
546
|
+
function installMcpConfig(log, env) {
|
|
547
|
+
const home = env["HOME"]?.trim() || env["USERPROFILE"]?.trim() || homedir3();
|
|
548
|
+
const appData = env["APPDATA"]?.trim();
|
|
549
|
+
const plat = platform3();
|
|
550
|
+
const cliPath = resolveVoMcpCliPath();
|
|
551
|
+
const controlPlaneUrl = env["VO_CONTROL_PLANE_URL"]?.trim() || DEFAULT_CONTROL_PLANE_URL2;
|
|
552
|
+
installMcpConfigAt(resolveCodeConfigPath(home), cliPath, controlPlaneUrl, log, "Claude Code CLI");
|
|
553
|
+
installMcpConfigAt(resolveDesktopConfigPath(home, plat, appData), cliPath, controlPlaneUrl, log, "Claude Desktop");
|
|
554
|
+
installCodexMcpConfigAt(resolveCodexConfigPath(home), cliPath, controlPlaneUrl, log);
|
|
555
|
+
}
|
|
556
|
+
async function runPairFlow(log, env) {
|
|
557
|
+
log("\n\u2501\u2501\u2501 Step 2: Link your AlgoHQ account (device code) \u2501\u2501\u2501");
|
|
558
|
+
log("A short code appears below \u2014 open the URL it prints and enter the code.");
|
|
559
|
+
log("Your raw token never persists; a scoped credential is stored in your OS keychain.\n");
|
|
544
560
|
try {
|
|
545
|
-
const result = await
|
|
546
|
-
log(
|
|
561
|
+
const result = await runPairing({ env, log });
|
|
562
|
+
log(`
|
|
563
|
+
\u2713 Paired \u2014 scoped credential stored at: ${result.credentialPath}`);
|
|
547
564
|
} catch (err) {
|
|
548
|
-
log(
|
|
549
|
-
|
|
550
|
-
|
|
565
|
+
log(`
|
|
566
|
+
\u26A0 Pairing didn't complete: ${err instanceof Error ? err.message : String(err)}`);
|
|
567
|
+
log(" No problem \u2014 the rest will finish; pair anytime with: vo-mcp pair");
|
|
551
568
|
}
|
|
552
569
|
}
|
|
553
|
-
function printNextSteps(log, autostartInstalled) {
|
|
570
|
+
function printNextSteps(log, autostartInstalled, configOnly) {
|
|
554
571
|
log("\n\u2501\u2501\u2501 Installation complete! \u2501\u2501\u2501\n");
|
|
555
572
|
log("What's configured:");
|
|
556
|
-
log(" \u2713 Claude Desktop
|
|
557
|
-
|
|
573
|
+
log(" \u2713 Claude Desktop, Claude Code, and Codex will load AlgoHQ MCP on next restart");
|
|
574
|
+
if (configOnly) {
|
|
575
|
+
log(" \u2713 Existing pairing and runner auto-start settings were left unchanged");
|
|
576
|
+
} else {
|
|
577
|
+
log(" \u2713 Your scoped credential is stored (revocable via the dashboard)");
|
|
578
|
+
}
|
|
558
579
|
if (autostartInstalled) {
|
|
559
580
|
log(" \u2713 Runner daemon will start automatically at login\n");
|
|
560
581
|
} else {
|
|
561
582
|
log("\n");
|
|
562
583
|
}
|
|
563
584
|
log("Next steps:");
|
|
564
|
-
log(" 1. Restart Claude Desktop / Claude Code (if running).");
|
|
565
|
-
if (
|
|
585
|
+
log(" 1. Restart Claude Desktop / Claude Code / Codex (if running).");
|
|
586
|
+
if (configOnly) {
|
|
587
|
+
log(" 2. Restart the existing AlgoHQ runner service or runner terminal.");
|
|
588
|
+
} else if (autostartInstalled) {
|
|
566
589
|
log(" 2. Log out and back in (or start the runner manually now: vo-mcp runner)");
|
|
567
590
|
} else {
|
|
568
591
|
log(" 2. Start the agent runner in a terminal (keep it running):");
|
|
569
592
|
log(" vo-mcp runner");
|
|
570
593
|
log(" (To set up auto-start at login: vo-mcp runner --install-autostart)");
|
|
571
594
|
}
|
|
572
|
-
log(" 3. Visit
|
|
573
|
-
log(" https://algosuite.ai/
|
|
595
|
+
log(" 3. Visit AlgoHQ to dispatch your first agent:");
|
|
596
|
+
log(" https://algosuite.ai/algohq\n");
|
|
574
597
|
log("The runner watches for tasks you dispatch and spins up agents in fresh worktrees.");
|
|
575
598
|
log("Agents only run while the runner is connected. Ctrl+C to stop it anytime.\n");
|
|
576
599
|
}
|
|
@@ -578,18 +601,20 @@ async function install(opts = {}) {
|
|
|
578
601
|
const log = opts.log ?? ((m) => console.error(m));
|
|
579
602
|
const env = opts.env ?? process.env;
|
|
580
603
|
log("\u2501\u2501\u2501 vo-mcp installer \u2501\u2501\u2501");
|
|
581
|
-
log("This will set up your machine to dispatch
|
|
582
|
-
log("\u2501\u2501\u2501 Step 1: Configure Claude Desktop / Claude Code \u2501\u2501\u2501");
|
|
604
|
+
log("This will set up your machine to dispatch AlgoHQ agents from anywhere.\n");
|
|
605
|
+
log("\u2501\u2501\u2501 Step 1: Configure Claude Desktop / Claude Code / Codex \u2501\u2501\u2501");
|
|
583
606
|
installMcpConfig(log, env);
|
|
584
607
|
if (!opts.skipLogin) {
|
|
585
|
-
await
|
|
608
|
+
await runPairFlow(log, env);
|
|
609
|
+
} else if (opts.configOnly) {
|
|
610
|
+
log("\n(Config-only refresh \u2014 existing pairing left unchanged.)");
|
|
586
611
|
} else {
|
|
587
|
-
log("\n(
|
|
612
|
+
log("\n(Pairing skipped \u2014 run `vo-mcp pair` when ready.)");
|
|
588
613
|
}
|
|
589
614
|
let autostartInstalled = false;
|
|
590
615
|
if (!opts.skipAutostart) {
|
|
591
|
-
const plat =
|
|
592
|
-
if (plat === "win32" || plat === "darwin") {
|
|
616
|
+
const plat = platform3();
|
|
617
|
+
if (plat === "win32" || plat === "darwin" || plat === "linux") {
|
|
593
618
|
log("\n\u2501\u2501\u2501 Step 3: Set up auto-start \u2501\u2501\u2501");
|
|
594
619
|
log("Would you like the runner daemon to start automatically at login?");
|
|
595
620
|
log("(You can skip this and set it up later with: vo-mcp runner --install-autostart)\n");
|
|
@@ -597,11 +622,15 @@ async function install(opts = {}) {
|
|
|
597
622
|
autostartInstalled = true;
|
|
598
623
|
}
|
|
599
624
|
}
|
|
600
|
-
printNextSteps(log, autostartInstalled);
|
|
625
|
+
printNextSteps(log, autostartInstalled, opts.configOnly === true);
|
|
626
|
+
}
|
|
627
|
+
function installOptionsFromArgs(args) {
|
|
628
|
+
const configOnly = args.includes("--config-only");
|
|
629
|
+
return configOnly ? { configOnly: true, skipLogin: true, skipAutostart: true } : {};
|
|
601
630
|
}
|
|
602
631
|
|
|
603
632
|
// src/install-cli.ts
|
|
604
|
-
install().catch((err) => {
|
|
633
|
+
install(installOptionsFromArgs(process.argv.slice(2))).catch((err) => {
|
|
605
634
|
console.error("[vo-mcp install] fatal:", err);
|
|
606
635
|
process.exit(1);
|
|
607
636
|
});
|