@vellumai/cli 0.9.0-dev.202606171623.2899a34 → 0.9.0-dev.202606172051.0afee75

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/cli",
3
- "version": "0.9.0-dev.202606171623.2899a34",
3
+ "version": "0.9.0-dev.202606172051.0afee75",
4
4
  "description": "CLI tools for vellum-assistant",
5
5
  "type": "module",
6
6
  "exports": {
@@ -268,4 +268,203 @@ describe("pair command", () => {
268
268
  const out = JSON.parse(logs.join("\n"));
269
269
  expect(out.gatewayUrl).toBe(OVERRIDE);
270
270
  });
271
+
272
+ test("--web creates a browser pairing URL without printing tokens", async () => {
273
+ const calls: Array<[string, RequestInit | undefined]> = [];
274
+ const origFetch = globalThis.fetch;
275
+ globalThis.fetch = (async (url: string, init?: RequestInit) => {
276
+ calls.push([url, init]);
277
+ if (url === `${LOCAL_URL}/v1/assistants/pair-test/feature-flags`) {
278
+ return new Response(
279
+ JSON.stringify({
280
+ flags: [{ key: "web-remote-ingress", enabled: true }],
281
+ }),
282
+ { status: 200, headers: { "content-type": "application/json" } },
283
+ );
284
+ }
285
+ if (url === `${LOCAL_URL}/v1/remote-web/pairing-challenge`) {
286
+ return new Response(
287
+ JSON.stringify({
288
+ deviceCode: "device-code",
289
+ userCode: "ABCD-EFGH",
290
+ verificationUri:
291
+ "https://abc123.ngrok.app/assistant-123/assistant/pair",
292
+ expiresAt: "2026-06-04T00:10:00.000Z",
293
+ expiresInSeconds: 600,
294
+ intervalSeconds: 5,
295
+ }),
296
+ { status: 200, headers: { "content-type": "application/json" } },
297
+ );
298
+ }
299
+ return new Response("not found", { status: 404 });
300
+ }) as unknown as typeof fetch;
301
+
302
+ const logs: string[] = [];
303
+ const logSpy = spyOn(console, "log").mockImplementation(
304
+ (...a: unknown[]) => {
305
+ logs.push(a.join(" "));
306
+ },
307
+ );
308
+
309
+ process.argv = [
310
+ "bun",
311
+ "vellum",
312
+ "pair",
313
+ "--web",
314
+ "--url",
315
+ "https://abc123.ngrok.app/assistant-123/assistant/",
316
+ "--json",
317
+ ];
318
+ try {
319
+ await pair();
320
+ } finally {
321
+ logSpy.mockRestore();
322
+ globalThis.fetch = origFetch;
323
+ }
324
+
325
+ expect(calls).toHaveLength(2);
326
+ expect(calls[1][0]).toBe(`${LOCAL_URL}/v1/remote-web/pairing-challenge`);
327
+ expect(JSON.parse(calls[1][1]?.body as string)).toEqual({
328
+ publicBaseUrl: "https://abc123.ngrok.app/assistant-123",
329
+ });
330
+
331
+ const out = JSON.parse(logs.join("\n"));
332
+ expect(out).toEqual({
333
+ pairUrl:
334
+ "https://abc123.ngrok.app/assistant-123/assistant/pair#device_code=device-code&user_code=ABCD-EFGH",
335
+ userCode: "ABCD-EFGH",
336
+ verificationUri: "https://abc123.ngrok.app/assistant-123/assistant/pair",
337
+ expiresAt: "2026-06-04T00:10:00.000Z",
338
+ expiresInSeconds: 600,
339
+ });
340
+ expect(logs.join("\n")).not.toContain("access");
341
+ expect(logs.join("\n")).not.toContain("refresh");
342
+ });
343
+
344
+ test("--web refuses when the web remote ingress feature flag is off", async () => {
345
+ const calls: Array<[string, RequestInit | undefined]> = [];
346
+ const origFetch = globalThis.fetch;
347
+ globalThis.fetch = (async (url: string, init?: RequestInit) => {
348
+ calls.push([url, init]);
349
+ return new Response(
350
+ JSON.stringify({
351
+ flags: [{ key: "web-remote-ingress", enabled: false }],
352
+ }),
353
+ { status: 200, headers: { "content-type": "application/json" } },
354
+ );
355
+ }) as unknown as typeof fetch;
356
+
357
+ const errors: string[] = [];
358
+ const errSpy = spyOn(console, "error").mockImplementation(
359
+ (...a: unknown[]) => {
360
+ errors.push(a.join(" "));
361
+ },
362
+ );
363
+ const exitSpy = spyOn(process, "exit").mockImplementation(((
364
+ code?: number,
365
+ ) => {
366
+ throw new Error(`exit:${code}`);
367
+ }) as never);
368
+
369
+ process.argv = [
370
+ "bun",
371
+ "vellum",
372
+ "pair",
373
+ "--web",
374
+ "--url",
375
+ "https://abc123.ngrok.app",
376
+ ];
377
+ let exited = false;
378
+ try {
379
+ await pair();
380
+ } catch (e) {
381
+ exited = (e as Error).message === "exit:1";
382
+ } finally {
383
+ errSpy.mockRestore();
384
+ exitSpy.mockRestore();
385
+ globalThis.fetch = origFetch;
386
+ }
387
+
388
+ expect(exited).toBe(true);
389
+ expect(errors.join("\n")).toContain("web-remote-ingress");
390
+ expect(calls).toHaveLength(1);
391
+ expect(calls[0][0]).toBe(
392
+ `${LOCAL_URL}/v1/assistants/pair-test/feature-flags`,
393
+ );
394
+ });
395
+
396
+ test("--web-approve approves a browser pairing code over loopback", async () => {
397
+ writeFileSync(
398
+ join(testDir, ".vellum.lock.json"),
399
+ JSON.stringify({
400
+ assistants: [
401
+ {
402
+ assistantId: "pair-test",
403
+ runtimeUrl: LOCAL_URL,
404
+ localUrl: LOCAL_URL,
405
+ cloud: "local",
406
+ },
407
+ ],
408
+ activeAssistant: "pair-test",
409
+ }),
410
+ );
411
+
412
+ const calls: Array<[string, RequestInit | undefined]> = [];
413
+ const origFetch = globalThis.fetch;
414
+ globalThis.fetch = (async (url: string, init?: RequestInit) => {
415
+ calls.push([url, init]);
416
+ if (url === `${LOCAL_URL}/v1/assistants/pair-test/feature-flags`) {
417
+ return new Response(
418
+ JSON.stringify({
419
+ flags: [{ key: "web-remote-ingress", enabled: true }],
420
+ }),
421
+ { status: 200, headers: { "content-type": "application/json" } },
422
+ );
423
+ }
424
+ if (url === `${LOCAL_URL}/v1/remote-web/pairing-verification`) {
425
+ return new Response(
426
+ JSON.stringify({
427
+ status: "approved",
428
+ verificationUri: "https://abc123.ngrok.app/assistant/pair",
429
+ expiresAt: "2026-06-04T00:10:00.000Z",
430
+ }),
431
+ { status: 200, headers: { "content-type": "application/json" } },
432
+ );
433
+ }
434
+ return new Response("not found", { status: 404 });
435
+ }) as unknown as typeof fetch;
436
+
437
+ const logs: string[] = [];
438
+ const logSpy = spyOn(console, "log").mockImplementation(
439
+ (...a: unknown[]) => {
440
+ logs.push(a.join(" "));
441
+ },
442
+ );
443
+
444
+ process.argv = [
445
+ "bun",
446
+ "vellum",
447
+ "pair",
448
+ "--web-approve",
449
+ "ABCD-EFGH",
450
+ "--json",
451
+ ];
452
+ try {
453
+ await pair();
454
+ } finally {
455
+ logSpy.mockRestore();
456
+ globalThis.fetch = origFetch;
457
+ }
458
+
459
+ expect(calls).toHaveLength(2);
460
+ expect(calls[1][0]).toBe(`${LOCAL_URL}/v1/remote-web/pairing-verification`);
461
+ expect(JSON.parse(calls[1][1]?.body as string)).toEqual({
462
+ userCode: "ABCD-EFGH",
463
+ });
464
+ expect(JSON.parse(logs.join("\n"))).toEqual({
465
+ status: "approved",
466
+ verificationUri: "https://abc123.ngrok.app/assistant/pair",
467
+ expiresAt: "2026-06-04T00:10:00.000Z",
468
+ });
469
+ });
271
470
  });
@@ -25,6 +25,11 @@ import {
25
25
  getClientRegistrationHeaders,
26
26
  } from "../lib/client-identity.js";
27
27
  import { GATEWAY_PORT } from "../lib/constants.js";
28
+ import {
29
+ formatFeatureFlagGateMessage,
30
+ isAssistantFeatureFlagEnabled,
31
+ WEB_REMOTE_INGRESS_FLAG,
32
+ } from "../lib/feature-flags.js";
28
33
  import { getLocalLanIPv4 } from "../lib/local.js";
29
34
  import { loopbackSafeFetch } from "../lib/loopback-fetch.js";
30
35
 
@@ -50,12 +55,17 @@ OPTIONS:
50
55
  --url <url> Reachable gateway URL to advertise in the bundle
51
56
  (default: the assistant's runtime URL, not loopback)
52
57
  --label <name> Human label for this pairing (echoed in the output)
58
+ --web Create a browser pairing URL for remote web access
59
+ --web-approve <code>
60
+ Approve a browser pairing code shown by /assistant/pair
53
61
  --json Output the raw bundle as JSON
54
62
 
55
63
  EXAMPLES:
56
64
  vellum pair
57
65
  vellum pair "My Assistant" --label "phone"
58
66
  vellum pair --url https://abc123.ngrok.app
67
+ vellum pair --web --url https://abc123.ngrok.app
68
+ vellum pair --web-approve ABCD-EFGH
59
69
  vellum pair --json
60
70
  `);
61
71
  }
@@ -72,6 +82,72 @@ interface PairResponse {
72
82
  refreshAfter?: string;
73
83
  }
74
84
 
85
+ interface RemoteWebPairingChallengeResponse {
86
+ deviceCode: string;
87
+ userCode: string;
88
+ verificationUri: string;
89
+ expiresAt: string;
90
+ expiresInSeconds: number;
91
+ }
92
+
93
+ interface RemoteWebPairingApprovalResponse {
94
+ status: "approved";
95
+ verificationUri: string;
96
+ expiresAt: string;
97
+ }
98
+
99
+ function normalizePublicBaseUrl(value: string): string {
100
+ const url = new URL(value);
101
+ url.search = "";
102
+ url.hash = "";
103
+ const parts = url.pathname.split("/").filter(Boolean);
104
+ const assistantIndex = parts.indexOf("assistant");
105
+ if (assistantIndex >= 0) {
106
+ parts.splice(assistantIndex);
107
+ }
108
+ url.pathname = parts.length ? `/${parts.join("/")}` : "/";
109
+ return url.toString().replace(/\/+$/, "");
110
+ }
111
+
112
+ function buildRemoteWebPairingUrl(
113
+ challenge: RemoteWebPairingChallengeResponse,
114
+ ): string {
115
+ const url = new URL(challenge.verificationUri);
116
+ url.hash = new URLSearchParams({
117
+ device_code: challenge.deviceCode,
118
+ user_code: challenge.userCode,
119
+ }).toString();
120
+ return url.toString();
121
+ }
122
+
123
+ async function assertWebRemoteIngressEnabled(
124
+ assistantId: string,
125
+ runtimeUrl: string,
126
+ ): Promise<void> {
127
+ let enabled: boolean;
128
+ try {
129
+ enabled = await isAssistantFeatureFlagEnabled(
130
+ assistantId,
131
+ WEB_REMOTE_INGRESS_FLAG,
132
+ { runtimeUrl },
133
+ );
134
+ } catch (err) {
135
+ console.error(
136
+ `Error: could not verify the \`${WEB_REMOTE_INGRESS_FLAG}\` feature flag. Is the assistant running? Try \`vellum wake\` and retry. ${
137
+ err instanceof Error ? err.message : String(err)
138
+ }`,
139
+ );
140
+ process.exit(1);
141
+ }
142
+
143
+ if (!enabled) {
144
+ console.error(
145
+ `Error: ${formatFeatureFlagGateMessage(WEB_REMOTE_INGRESS_FLAG)}`,
146
+ );
147
+ process.exit(1);
148
+ }
149
+ }
150
+
75
151
  export async function pair(): Promise<void> {
76
152
  const rawArgs = process.argv.slice(3);
77
153
 
@@ -81,12 +157,27 @@ export async function pair(): Promise<void> {
81
157
  }
82
158
 
83
159
  const jsonOutput = rawArgs.includes("--json");
84
- let args = rawArgs.filter((a) => a !== "--json");
160
+ const webPairing = rawArgs.includes("--web");
161
+ const webApproval = rawArgs.includes("--web-approve");
162
+ let args = rawArgs.filter((a) => a !== "--json" && a !== "--web");
85
163
 
86
164
  const [label, afterLabel] = extractFlag(args, "--label");
87
- const [urlOverride, afterUrl] = extractFlag(afterLabel, "--url");
165
+ const [webApproveCode, afterWebApprove] = extractFlag(
166
+ afterLabel,
167
+ "--web-approve",
168
+ );
169
+ const [urlOverride, afterUrl] = extractFlag(afterWebApprove, "--url");
88
170
  args = afterUrl;
89
171
 
172
+ if (webPairing && webApproveCode) {
173
+ console.error("Error: use either --web or --web-approve, not both.");
174
+ process.exit(1);
175
+ }
176
+ if (webApproval && !webApproveCode) {
177
+ console.error("Error: --web-approve requires a pairing code.");
178
+ process.exit(1);
179
+ }
180
+
90
181
  // Resolve the target. An explicit argument is matched by display name OR id
91
182
  // (with the standard ambiguity error); no argument falls back to the active
92
183
  // assistant. Join positional tokens so multi-word display names work even
@@ -126,7 +217,7 @@ export async function pair(): Promise<void> {
126
217
  // so without an explicit --url the bundle would point the other machine at
127
218
  // its own localhost. Refuse to advertise a loopback URL unless the user
128
219
  // explicitly passed one. (An explicit --url is trusted as-is.)
129
- if (!urlOverride && isLoopbackHost(advertisedUrl)) {
220
+ if (!urlOverride && !webApproveCode && isLoopbackHost(advertisedUrl)) {
130
221
  const lan = getLocalLanIPv4();
131
222
  // Use THIS assistant's gateway port (not the global default) — second
132
223
  // local instances listen on a different port.
@@ -149,6 +240,126 @@ export async function pair(): Promise<void> {
149
240
  process.exit(1);
150
241
  }
151
242
 
243
+ if (webApproveCode) {
244
+ await assertWebRemoteIngressEnabled(entry.assistantId, mintUrl);
245
+
246
+ let response: Response;
247
+ try {
248
+ response = await loopbackSafeFetch(
249
+ `${mintUrl}/v1/remote-web/pairing-verification`,
250
+ {
251
+ method: "POST",
252
+ headers: { "Content-Type": "application/json" },
253
+ body: JSON.stringify({ userCode: webApproveCode }),
254
+ },
255
+ );
256
+ } catch (err) {
257
+ console.error(
258
+ `Error: could not reach the gateway at ${mintUrl} ` +
259
+ `(${err instanceof Error ? err.message : String(err)}).`,
260
+ );
261
+ console.error("Is the assistant running? Try `vellum wake`.");
262
+ process.exit(1);
263
+ }
264
+
265
+ if (!response.ok) {
266
+ const body = await response.text().catch(() => "");
267
+ console.error(
268
+ `Error: HTTP ${response.status}: ${body || response.statusText}`,
269
+ );
270
+ process.exit(1);
271
+ }
272
+
273
+ const result = (await response.json()) as RemoteWebPairingApprovalResponse;
274
+ if (jsonOutput) {
275
+ console.log(JSON.stringify(result, null, 2));
276
+ return;
277
+ }
278
+ console.log("Remote web pairing approved.");
279
+ console.log(`Expires: ${result.expiresAt}`);
280
+ return;
281
+ }
282
+
283
+ if (webPairing) {
284
+ await assertWebRemoteIngressEnabled(entry.assistantId, mintUrl);
285
+
286
+ let publicBaseUrl: string;
287
+ try {
288
+ publicBaseUrl = normalizePublicBaseUrl(advertisedUrl);
289
+ } catch {
290
+ console.error(`Error: invalid --url value '${advertisedUrl}'.`);
291
+ process.exit(1);
292
+ }
293
+
294
+ let response: Response;
295
+ try {
296
+ response = await loopbackSafeFetch(
297
+ `${mintUrl}/v1/remote-web/pairing-challenge`,
298
+ {
299
+ method: "POST",
300
+ headers: { "Content-Type": "application/json" },
301
+ body: JSON.stringify({ publicBaseUrl }),
302
+ },
303
+ );
304
+ } catch (err) {
305
+ console.error(
306
+ `Error: could not reach the gateway at ${mintUrl} ` +
307
+ `(${err instanceof Error ? err.message : String(err)}).`,
308
+ );
309
+ console.error("Is the assistant running? Try `vellum wake`.");
310
+ process.exit(1);
311
+ }
312
+
313
+ if (!response.ok) {
314
+ const body = await response.text().catch(() => "");
315
+ console.error(
316
+ `Error: HTTP ${response.status}: ${body || response.statusText}`,
317
+ );
318
+ process.exit(1);
319
+ }
320
+
321
+ const challenge =
322
+ (await response.json()) as RemoteWebPairingChallengeResponse;
323
+ const pairUrl = buildRemoteWebPairingUrl(challenge);
324
+
325
+ if (jsonOutput) {
326
+ console.log(
327
+ JSON.stringify(
328
+ {
329
+ pairUrl,
330
+ userCode: challenge.userCode,
331
+ verificationUri: challenge.verificationUri,
332
+ expiresAt: challenge.expiresAt,
333
+ expiresInSeconds: challenge.expiresInSeconds,
334
+ },
335
+ null,
336
+ 2,
337
+ ),
338
+ );
339
+ return;
340
+ }
341
+
342
+ const displayName = entry.name || entry.assistantName || entry.assistantId;
343
+ console.log(`Created remote web pairing for ${displayName}.`);
344
+ console.log("");
345
+ console.log("Open this URL in the browser:");
346
+ console.log("");
347
+ console.log(` ${pairUrl}`);
348
+ console.log("");
349
+ console.log("When the browser shows this code, approve it locally:");
350
+ console.log("");
351
+ const approveTarget = assistantName
352
+ ? `${JSON.stringify(assistantName)} `
353
+ : "";
354
+ console.log(` Code: ${challenge.userCode}`);
355
+ console.log(
356
+ ` Run: vellum pair ${approveTarget}--web-approve ${challenge.userCode}`,
357
+ );
358
+ console.log("");
359
+ console.log(`Expires: ${challenge.expiresAt}`);
360
+ return;
361
+ }
362
+
152
363
  // Fresh per-pairing device identity — each `vellum pair` is independently
153
364
  // revocable.
154
365
  const deviceId = nanoid();
@@ -27,6 +27,7 @@ export const LLM_PROVIDER_ENV_VAR_NAMES: Record<string, string> = {
27
27
  fireworks: "FIREWORKS_API_KEY",
28
28
  openrouter: "OPENROUTER_API_KEY",
29
29
  minimax: "MINIMAX_API_KEY",
30
+ atlascloud: "ATLASCLOUD_API_KEY",
30
31
  };
31
32
 
32
33
  /** Search-provider env var names. Mirrors `SEARCH_PROVIDER_CATALOG` BYOK entries. */