@vellumai/cli 0.9.0-dev.202606171623.2899a34 → 0.9.0-dev.202606171903.06eea08
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 +1 -1
- package/src/__tests__/pair.test.ts +199 -0
- package/src/commands/pair.ts +214 -3
- package/src/shared/provider-env-vars.ts +1 -0
package/package.json
CHANGED
|
@@ -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
|
});
|
package/src/commands/pair.ts
CHANGED
|
@@ -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
|
-
|
|
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 [
|
|
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. */
|