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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,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 platform2 } from "node:os";
5
+ import { homedir as homedir3, platform as platform3 } from "node:os";
6
6
  import { join as join3, dirname as dirname2 } from "node:path";
7
7
  import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, copyFileSync as copyFileSync2 } from "node:fs";
8
- import { resolve } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
9
 
10
- // src/cloud/login.ts
11
- import { createServer } from "node:http";
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,243 +123,82 @@ function writeStoredCredential(cred, storedAt, env = process.env, keychain = rea
125
123
  return p;
126
124
  }
127
125
 
128
- // src/cloud/login.ts
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
- var DEFAULT_TIMEOUT_MS = 12e4;
131
- var MAX_BODY_BYTES = 16384;
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
- };
156
- }
157
- function captureHtml() {
158
- return `<!doctype html><html><head><meta charset="utf-8"><title>VO login</title></head>
159
- <body style="font-family:system-ui;max-width:32rem;margin:4rem auto;text-align:center">
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>`;
129
+ function formatPairingCode(code) {
130
+ return code.length === 8 ? `${code.slice(0, 4)}-${code.slice(4)}` : code;
170
131
  }
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 runLogin(opts = {}) {
182
- const env = opts.env ?? process.env;
183
- const dashboardUrl = (opts.dashboardUrl ?? env["VO_DASHBOARD_URL"]?.trim() ?? DEFAULT_DASHBOARD_URL).replace(/\/+$/, "") || DEFAULT_DASHBOARD_URL;
184
- const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
185
- const log = opts.log ?? ((m) => console.error(m));
186
- const nowIso = opts.nowIso ?? (() => (/* @__PURE__ */ new Date()).toISOString());
187
- const openBrowser = opts.openBrowser ?? defaultOpenBrowser;
188
- const state = randomBytes(32).toString("base64url");
189
- return new Promise((resolve2, reject) => {
190
- let settled = false;
191
- const finish = (err, result) => {
192
- if (settled) return;
193
- settled = true;
194
- clearTimeout(timer);
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
- // src/cloud/auth-token-source.ts
279
- var FIREBASE_SECURETOKEN_URL = "https://securetoken.googleapis.com/v1/token";
280
- var FIREBASE_TOKEN_REFERER = "https://algosuite.ai/";
281
- var REFRESH_SKEW_MS = 6e4;
282
- function createFirebaseRefreshTokenSource(opts) {
283
- const refreshToken = opts.refreshToken.trim();
284
- const apiKey = opts.apiKey.trim();
285
- const now = opts.now ?? (() => Date.now());
286
- const fetchFn = opts.fetchFn ?? globalThis.fetch;
287
- let cachedToken = null;
288
- let expiresAtMs = 0;
289
- let inFlight = null;
290
- async function refresh() {
291
- try {
292
- const res = await fetchFn(`${FIREBASE_SECURETOKEN_URL}?key=${encodeURIComponent(apiKey)}`, {
293
- method: "POST",
294
- headers: {
295
- "content-type": "application/x-www-form-urlencoded",
296
- referer: FIREBASE_TOKEN_REFERER
297
- },
298
- body: `grant_type=refresh_token&refresh_token=${encodeURIComponent(refreshToken)}`
299
- });
300
- const text = await res.text();
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;
319
- }
320
- }
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;
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.");
331
169
  }
332
- };
333
- }
334
-
335
- // src/cloud/vo-credential-exchange.ts
336
- async function exchangeForVoCredential(opts) {
337
- const base = opts.controlPlaneUrl.replace(/\/+$/, "");
338
- if (!base) return null;
339
- const fetchFn = opts.fetchFn ?? globalThis.fetch;
340
- try {
341
- const idToken = await createFirebaseRefreshTokenSource({
342
- refreshToken: opts.refreshToken,
343
- apiKey: opts.apiKey,
344
- ...opts.fetchFn ? { fetchFn: opts.fetchFn } : {}
345
- }).getToken();
346
- if (!idToken) return null;
347
- const res = await fetchFn(`${base}/api/v1/auth/vo-credential`, {
348
- method: "POST",
349
- headers: { "content-type": "application/json", authorization: `Bearer ${idToken}` },
350
- body: JSON.stringify(opts.label ? { label: opts.label } : {})
170
+ await sleep(intervalMs);
171
+ const pollRes = await fetchImpl(`${controlPlaneUrl}/api/v1/pair/poll`, {
172
+ method: "GET",
173
+ headers: { "x-vo-poll-token": pollToken }
351
174
  });
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;
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"] } : {}
191
+ },
192
+ now().toISOString()
193
+ );
194
+ return { credentialPath: credentialPath2, expires_at: String(body["expires_at"] ?? "") };
195
+ }
196
+ throw new Error("Unexpected response from the pairing service.");
360
197
  }
361
198
  }
362
199
 
363
200
  // src/autostart.ts
364
- import { homedir as homedir2, platform } from "node:os";
201
+ import { homedir as homedir2, platform as platform2 } from "node:os";
365
202
  import { join as join2 } from "node:path";
366
203
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync2, unlinkSync, copyFileSync } from "node:fs";
367
204
  function resolveRunnerCommand(override) {
@@ -503,7 +340,7 @@ async function installAutostart(opts = {}) {
503
340
  const log = opts.log ?? ((m) => console.error(m));
504
341
  const env = opts.env ?? process.env;
505
342
  const runnerCommand = resolveRunnerCommand(opts.runnerCommand);
506
- const plat = platform();
343
+ const plat = platform2();
507
344
  if (plat === "win32") {
508
345
  installWindowsAutostart(runnerCommand, log, env);
509
346
  } else if (plat === "darwin") {
@@ -517,7 +354,7 @@ async function installAutostart(opts = {}) {
517
354
  }
518
355
 
519
356
  // src/install.ts
520
- var DEFAULT_CONTROL_PLANE_URL = "https://vo-control-plane-bzjphrajaq-uc.a.run.app";
357
+ var DEFAULT_CONTROL_PLANE_URL2 = "https://vo-control-plane-bzjphrajaq-uc.a.run.app";
521
358
  function resolveCodeConfigPath(home) {
522
359
  return join3(home, ".claude.json");
523
360
  }
@@ -546,11 +383,7 @@ function writeClaudeConfig(path, config) {
546
383
  `, "utf8");
547
384
  }
548
385
  function resolveVoMcpCliPath() {
549
- const scriptPath = process.argv[1] ?? "";
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");
386
+ return join3(dirname2(fileURLToPath(import.meta.url)), "cli.js");
554
387
  }
555
388
  function installMcpConfigAt(configPath, cliPath, controlPlaneUrl, log, label) {
556
389
  const existing = readClaudeConfig(configPath);
@@ -584,32 +417,24 @@ function installMcpConfigAt(configPath, cliPath, controlPlaneUrl, log, label) {
584
417
  function installMcpConfig(log, env) {
585
418
  const home = env["HOME"]?.trim() || env["USERPROFILE"]?.trim() || homedir3();
586
419
  const appData = env["APPDATA"]?.trim();
587
- const plat = platform2();
420
+ const plat = platform3();
588
421
  const cliPath = resolveVoMcpCliPath();
589
- const controlPlaneUrl = env["VO_CONTROL_PLANE_URL"]?.trim() || DEFAULT_CONTROL_PLANE_URL;
422
+ const controlPlaneUrl = env["VO_CONTROL_PLANE_URL"]?.trim() || DEFAULT_CONTROL_PLANE_URL2;
590
423
  installMcpConfigAt(resolveCodeConfigPath(home), cliPath, controlPlaneUrl, log, "Claude Code CLI");
591
424
  installMcpConfigAt(resolveDesktopConfigPath(home, plat, appData), cliPath, controlPlaneUrl, log, "Claude Desktop");
592
425
  }
593
- async function runLoginFlow(log, env) {
594
- log("\n\u2501\u2501\u2501 Step 2: Link your Claude account \u2501\u2501\u2501");
595
- log("We'll open your browser to sign in, then mint a scoped credential.");
596
- log("Your raw token never persists locally (it's exchanged for a vocred_).\n");
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
- };
426
+ async function runPairFlow(log, env) {
427
+ log("\n\u2501\u2501\u2501 Step 2: Link your VO account (device code) \u2501\u2501\u2501");
428
+ log("A short code appears below \u2014 open the URL it prints and enter the code.");
429
+ log("Your raw token never persists; a scoped credential is stored in your OS keychain.\n");
606
430
  try {
607
- const result = await runLogin({ env, log, exchange });
608
- log(`\u2713 Signed in${result.email ? ` as ${result.email}` : ""}. Credential stored at: ${result.credentialPath}`);
431
+ const result = await runPairing({ env, log });
432
+ log(`
433
+ \u2713 Paired \u2014 scoped credential stored at: ${result.credentialPath}`);
609
434
  } catch (err) {
610
- log(`\u2717 Login failed: ${err instanceof Error ? err.message : String(err)}`);
611
- log(" You can retry later by running: vo-mcp login");
612
- throw err;
435
+ log(`
436
+ \u26A0 Pairing didn't complete: ${err instanceof Error ? err.message : String(err)}`);
437
+ log(" No problem \u2014 the rest will finish; pair anytime with: vo-mcp pair");
613
438
  }
614
439
  }
615
440
  function printNextSteps(log, autostartInstalled) {
@@ -644,13 +469,13 @@ async function install(opts = {}) {
644
469
  log("\u2501\u2501\u2501 Step 1: Configure Claude Desktop / Claude Code \u2501\u2501\u2501");
645
470
  installMcpConfig(log, env);
646
471
  if (!opts.skipLogin) {
647
- await runLoginFlow(log, env);
472
+ await runPairFlow(log, env);
648
473
  } else {
649
- log("\n(Login skipped \u2014 run `vo-mcp login` manually when ready.)");
474
+ log("\n(Pairing skipped \u2014 run `vo-mcp pair` when ready.)");
650
475
  }
651
476
  let autostartInstalled = false;
652
477
  if (!opts.skipAutostart) {
653
- const plat = platform2();
478
+ const plat = platform3();
654
479
  if (plat === "win32" || plat === "darwin" || plat === "linux") {
655
480
  log("\n\u2501\u2501\u2501 Step 3: Set up auto-start \u2501\u2501\u2501");
656
481
  log("Would you like the runner daemon to start automatically at login?");