@vellumai/cli 0.10.4 → 0.10.5-dev.202607022329.d89bf2f

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.
@@ -20,30 +20,74 @@ import { join } from "node:path";
20
20
 
21
21
  import type { AssistantEntry } from "../lib/assistant-config.js";
22
22
  import { loadAllAssistants } from "../lib/assistant-config.js";
23
+ import * as loopbackFetchModule from "../lib/loopback-fetch.js";
24
+ import * as platformClientModule from "../lib/platform-client.js";
23
25
  import * as retireLocalModule from "../lib/retire-local.js";
24
26
 
25
27
  const testDir = mkdtempSync(join(tmpdir(), "cli-retire-test-"));
26
28
  const originalArgv = [...process.argv];
27
29
  const originalExit = process.exit;
28
30
  const originalLockfileDir = process.env.VELLUM_LOCKFILE_DIR;
31
+ const originalPlatformToken = process.env.VELLUM_PLATFORM_TOKEN;
29
32
  const originalStdinIsTTY = process.stdin.isTTY;
30
33
  const originalStdoutIsTTY = process.stdout.isTTY;
31
34
  const originalStdinIsRaw = process.stdin.isRaw;
32
35
  const originalSetRawMode = process.stdin.setRawMode;
33
36
  const originalStdoutWrite = process.stdout.write;
34
37
  const realRetireLocalModule = { ...retireLocalModule };
38
+ const realPlatformClientModule = { ...platformClientModule };
39
+ const realLoopbackFetchModule = { ...loopbackFetchModule };
35
40
 
36
41
  const retireLocalMock = mock(async () => {});
42
+ const readPlatformTokenMock = mock((): string | null => null);
43
+ const authHeadersMock = mock(async () => ({
44
+ "Content-Type": "application/json",
45
+ "X-Session-Token": "session-token",
46
+ }));
47
+ const authHeadersForKnownOrganizationMock = mock(
48
+ (token: string, organizationId: string) => ({
49
+ "Content-Type": "application/json",
50
+ "X-Session-Token": token,
51
+ "Vellum-Organization-Id": organizationId,
52
+ }),
53
+ );
54
+ const getPlatformUrlMock = mock(() => "https://platform.example.com");
55
+ const readGatewayCredentialMock = mock(
56
+ async (
57
+ _gatewayUrl: string,
58
+ _name: string,
59
+ _bearerToken?: string,
60
+ ): Promise<{ value: string | null; unreachable: boolean }> => ({
61
+ value: null,
62
+ unreachable: false,
63
+ }),
64
+ );
65
+ const loopbackSafeFetchMock = mock(
66
+ async (): Promise<Response> => new Response(null, { status: 204 }),
67
+ );
37
68
 
38
69
  mock.module("../lib/retire-local.js", () => ({
39
70
  ...realRetireLocalModule,
40
71
  retireLocal: retireLocalMock,
41
72
  }));
42
73
 
74
+ mock.module("../lib/platform-client.js", () => ({
75
+ authHeaders: authHeadersMock,
76
+ authHeadersForKnownOrganization: authHeadersForKnownOrganizationMock,
77
+ getPlatformUrl: getPlatformUrlMock,
78
+ readGatewayCredential: readGatewayCredentialMock,
79
+ readPlatformToken: readPlatformTokenMock,
80
+ }));
81
+
82
+ mock.module("../lib/loopback-fetch.js", () => ({
83
+ loopbackSafeFetch: loopbackSafeFetchMock,
84
+ }));
85
+
43
86
  import { retire } from "../commands/retire.js";
44
87
 
45
88
  let consoleLogSpy: ReturnType<typeof spyOn>;
46
89
  let consoleErrorSpy: ReturnType<typeof spyOn>;
90
+ let consoleWarnSpy: ReturnType<typeof spyOn>;
47
91
 
48
92
  function makeEntry(
49
93
  assistantId: string,
@@ -123,6 +167,7 @@ function restoreTerminal(): void {
123
167
  describe("vellum retire", () => {
124
168
  beforeEach(() => {
125
169
  process.env.VELLUM_LOCKFILE_DIR = testDir;
170
+ delete process.env.VELLUM_PLATFORM_TOKEN;
126
171
  rmSync(join(testDir, ".vellum.lock.json"), { force: true });
127
172
  process.argv = ["bun", "vellum", "retire"];
128
173
  process.exit = ((code?: number) => {
@@ -130,9 +175,36 @@ describe("vellum retire", () => {
130
175
  }) as typeof process.exit;
131
176
  retireLocalMock.mockReset();
132
177
  retireLocalMock.mockResolvedValue(undefined);
178
+ readPlatformTokenMock.mockReset();
179
+ readPlatformTokenMock.mockReturnValue(null);
180
+ authHeadersMock.mockReset();
181
+ authHeadersMock.mockResolvedValue({
182
+ "Content-Type": "application/json",
183
+ "X-Session-Token": "session-token",
184
+ });
185
+ authHeadersForKnownOrganizationMock.mockReset();
186
+ authHeadersForKnownOrganizationMock.mockImplementation(
187
+ (token: string, organizationId: string) => ({
188
+ "Content-Type": "application/json",
189
+ "X-Session-Token": token,
190
+ "Vellum-Organization-Id": organizationId,
191
+ }),
192
+ );
193
+ getPlatformUrlMock.mockReset();
194
+ getPlatformUrlMock.mockReturnValue("https://platform.example.com");
195
+ readGatewayCredentialMock.mockReset();
196
+ readGatewayCredentialMock.mockResolvedValue({
197
+ value: null,
198
+ unreachable: false,
199
+ });
200
+ loopbackSafeFetchMock.mockReset();
201
+ loopbackSafeFetchMock.mockResolvedValue(
202
+ new Response(null, { status: 204 }),
203
+ );
133
204
  setTerminalMode(false);
134
205
  consoleLogSpy = spyOn(console, "log").mockImplementation(() => {});
135
206
  consoleErrorSpy = spyOn(console, "error").mockImplementation(() => {});
207
+ consoleWarnSpy = spyOn(console, "warn").mockImplementation(() => {});
136
208
  });
137
209
 
138
210
  afterEach(() => {
@@ -141,15 +213,23 @@ describe("vellum retire", () => {
141
213
  restoreTerminal();
142
214
  consoleLogSpy.mockRestore();
143
215
  consoleErrorSpy.mockRestore();
216
+ consoleWarnSpy.mockRestore();
144
217
  });
145
218
 
146
219
  afterAll(() => {
147
220
  mock.module("../lib/retire-local.js", () => realRetireLocalModule);
221
+ mock.module("../lib/platform-client.js", () => realPlatformClientModule);
222
+ mock.module("../lib/loopback-fetch.js", () => realLoopbackFetchModule);
148
223
  if (originalLockfileDir === undefined) {
149
224
  delete process.env.VELLUM_LOCKFILE_DIR;
150
225
  } else {
151
226
  process.env.VELLUM_LOCKFILE_DIR = originalLockfileDir;
152
227
  }
228
+ if (originalPlatformToken === undefined) {
229
+ delete process.env.VELLUM_PLATFORM_TOKEN;
230
+ } else {
231
+ process.env.VELLUM_PLATFORM_TOKEN = originalPlatformToken;
232
+ }
153
233
  rmSync(testDir, { recursive: true, force: true });
154
234
  });
155
235
 
@@ -170,6 +250,279 @@ describe("vellum retire", () => {
170
250
  );
171
251
  });
172
252
 
253
+ test("local retire unregisters the self-hosted platform record first", async () => {
254
+ const entry = makeEntry("assistant-1", { name: "Example Assistant" });
255
+ writeLockfile([entry]);
256
+ readPlatformTokenMock.mockReturnValue("session-token");
257
+ getPlatformUrlMock.mockReturnValue("https://platform.example.test/api");
258
+ readGatewayCredentialMock.mockImplementation(async (_gatewayUrl, name) => {
259
+ if (name === "vellum:platform_assistant_id") {
260
+ return { value: "platform-assistant-1", unreachable: false };
261
+ }
262
+ if (name === "vellum:platform_base_url") {
263
+ return { value: "https://platform.example.test", unreachable: false };
264
+ }
265
+ if (name === "vellum:platform_organization_id") {
266
+ return { value: "org-123", unreachable: false };
267
+ }
268
+ return { value: null, unreachable: false };
269
+ });
270
+ process.argv = ["bun", "vellum", "retire", "assistant-1", "--yes"];
271
+
272
+ await retire();
273
+
274
+ expect(readGatewayCredentialMock).toHaveBeenCalledWith(
275
+ entry.localUrl ?? entry.runtimeUrl,
276
+ "vellum:platform_assistant_id",
277
+ entry.bearerToken,
278
+ );
279
+ expect(authHeadersMock).not.toHaveBeenCalled();
280
+ expect(loopbackSafeFetchMock).toHaveBeenCalledWith(
281
+ "https://platform.example.test/api/v1/assistants/platform-assistant-1/retire/",
282
+ {
283
+ method: "DELETE",
284
+ headers: {
285
+ "Content-Type": "application/json",
286
+ "X-Session-Token": "session-token",
287
+ "Vellum-Organization-Id": "org-123",
288
+ },
289
+ },
290
+ );
291
+ expect(retireLocalMock).toHaveBeenCalledWith("assistant-1", entry);
292
+ expect(loadAllAssistants()).toEqual([]);
293
+ });
294
+
295
+ test("local retire reads platform registration through the loopback gateway URL", async () => {
296
+ const localGatewayUrl = "http://127.0.0.1:7831";
297
+ const entry = makeEntry("assistant-1", {
298
+ name: "Example Assistant",
299
+ runtimeUrl: "http://assistant-1.local:7831",
300
+ localUrl: localGatewayUrl,
301
+ });
302
+ writeLockfile([entry]);
303
+ readPlatformTokenMock.mockReturnValue("session-token");
304
+ getPlatformUrlMock.mockReturnValue("https://platform.example.test");
305
+ readGatewayCredentialMock.mockImplementation(async (gatewayUrl, name) => {
306
+ expect(gatewayUrl).toBe(localGatewayUrl);
307
+ if (name === "vellum:platform_assistant_id") {
308
+ return { value: "platform-assistant-1", unreachable: false };
309
+ }
310
+ if (name === "vellum:platform_base_url") {
311
+ return { value: "https://platform.example.test", unreachable: false };
312
+ }
313
+ if (name === "vellum:platform_organization_id") {
314
+ return { value: "org-123", unreachable: false };
315
+ }
316
+ return { value: null, unreachable: false };
317
+ });
318
+ process.argv = ["bun", "vellum", "retire", "assistant-1", "--yes"];
319
+
320
+ await retire();
321
+
322
+ expect(readGatewayCredentialMock).toHaveBeenCalledWith(
323
+ localGatewayUrl,
324
+ "vellum:platform_assistant_id",
325
+ entry.bearerToken,
326
+ );
327
+ expect(readGatewayCredentialMock).toHaveBeenCalledWith(
328
+ localGatewayUrl,
329
+ "vellum:platform_base_url",
330
+ entry.bearerToken,
331
+ );
332
+ expect(readGatewayCredentialMock).toHaveBeenCalledWith(
333
+ localGatewayUrl,
334
+ "vellum:platform_organization_id",
335
+ entry.bearerToken,
336
+ );
337
+ expect(loopbackSafeFetchMock).toHaveBeenCalledWith(
338
+ "https://platform.example.test/v1/assistants/platform-assistant-1/retire/",
339
+ expect.any(Object),
340
+ );
341
+ expect(retireLocalMock).toHaveBeenCalledWith("assistant-1", entry);
342
+ });
343
+
344
+ test("local retire uses saved platform registration when local credentials are unreachable", async () => {
345
+ const entry = makeEntry("assistant-1", {
346
+ name: "Example Assistant",
347
+ platformAssistantId: "platform-assistant-1",
348
+ platformBaseUrl: "https://platform.example.test",
349
+ platformOrganizationId: "org-123",
350
+ });
351
+ writeLockfile([entry]);
352
+ readPlatformTokenMock.mockReturnValue("session-token");
353
+ getPlatformUrlMock.mockReturnValue("https://platform.example.test");
354
+ readGatewayCredentialMock.mockResolvedValue({
355
+ value: null,
356
+ unreachable: true,
357
+ });
358
+ process.argv = ["bun", "vellum", "retire", "assistant-1", "--yes"];
359
+
360
+ await retire();
361
+
362
+ expect(readGatewayCredentialMock).toHaveBeenCalledWith(
363
+ entry.localUrl ?? entry.runtimeUrl,
364
+ "vellum:platform_assistant_id",
365
+ entry.bearerToken,
366
+ );
367
+ expect(loopbackSafeFetchMock).toHaveBeenCalledWith(
368
+ "https://platform.example.test/v1/assistants/platform-assistant-1/retire/",
369
+ {
370
+ method: "DELETE",
371
+ headers: {
372
+ "Content-Type": "application/json",
373
+ "X-Session-Token": "session-token",
374
+ "Vellum-Organization-Id": "org-123",
375
+ },
376
+ },
377
+ );
378
+ expect(consoleWarnSpy.mock.calls.flat().join("\n")).toContain(
379
+ "using saved platform registration",
380
+ );
381
+ expect(retireLocalMock).toHaveBeenCalledWith("assistant-1", entry);
382
+ expect(loadAllAssistants()).toEqual([]);
383
+ });
384
+
385
+ test("local retire continues when local credentials are unreachable and no saved platform registration exists", async () => {
386
+ const entry = makeEntry("assistant-1", { name: "Example Assistant" });
387
+ writeLockfile([entry]);
388
+ readPlatformTokenMock.mockReturnValue("session-token");
389
+ readGatewayCredentialMock.mockResolvedValue({
390
+ value: null,
391
+ unreachable: true,
392
+ });
393
+ process.argv = ["bun", "vellum", "retire", "assistant-1", "--yes"];
394
+
395
+ await retire();
396
+
397
+ expect(loopbackSafeFetchMock).not.toHaveBeenCalled();
398
+ expect(consoleWarnSpy.mock.calls.flat().join("\n")).toContain(
399
+ "no saved platform registration is available",
400
+ );
401
+ expect(retireLocalMock).toHaveBeenCalledWith("assistant-1", entry);
402
+ expect(loadAllAssistants()).toEqual([]);
403
+ });
404
+
405
+ test("local retire skips platform unregister when stored platform URL origin mismatches", async () => {
406
+ const entry = makeEntry("assistant-1", { name: "Example Assistant" });
407
+ writeLockfile([entry]);
408
+ readPlatformTokenMock.mockReturnValue("session-token");
409
+ getPlatformUrlMock.mockReturnValue("https://platform.example.com");
410
+ readGatewayCredentialMock.mockImplementation(async (_gatewayUrl, name) => {
411
+ if (name === "vellum:platform_assistant_id") {
412
+ return { value: "platform-assistant-1", unreachable: false };
413
+ }
414
+ if (name === "vellum:platform_base_url") {
415
+ return { value: "https://malicious.example.test", unreachable: false };
416
+ }
417
+ if (name === "vellum:platform_organization_id") {
418
+ return { value: "org-123", unreachable: false };
419
+ }
420
+ return { value: null, unreachable: false };
421
+ });
422
+ process.argv = ["bun", "vellum", "retire", "assistant-1", "--yes"];
423
+
424
+ await retire();
425
+
426
+ expect(loopbackSafeFetchMock).not.toHaveBeenCalled();
427
+ expect(consoleWarnSpy.mock.calls.flat().join("\n")).toContain(
428
+ "Stored platform URL does not match the configured platform URL",
429
+ );
430
+ expect(retireLocalMock).toHaveBeenCalledWith("assistant-1", entry);
431
+ });
432
+
433
+ test("local retire skips platform unregister when stored organization ID is missing", async () => {
434
+ const entry = makeEntry("assistant-1", { name: "Example Assistant" });
435
+ writeLockfile([entry]);
436
+ readPlatformTokenMock.mockReturnValue("session-token");
437
+ getPlatformUrlMock.mockReturnValue("https://platform.example.test");
438
+ readGatewayCredentialMock.mockImplementation(async (_gatewayUrl, name) => {
439
+ if (name === "vellum:platform_assistant_id") {
440
+ return { value: "platform-assistant-1", unreachable: false };
441
+ }
442
+ if (name === "vellum:platform_base_url") {
443
+ return { value: "https://platform.example.test", unreachable: false };
444
+ }
445
+ return { value: null, unreachable: false };
446
+ });
447
+ process.argv = ["bun", "vellum", "retire", "assistant-1", "--yes"];
448
+
449
+ await retire();
450
+
451
+ expect(loopbackSafeFetchMock).not.toHaveBeenCalled();
452
+ expect(consoleWarnSpy.mock.calls.flat().join("\n")).toContain(
453
+ "Could not read platform organization ID",
454
+ );
455
+ expect(retireLocalMock).toHaveBeenCalledWith("assistant-1", entry);
456
+ expect(loadAllAssistants()).toEqual([]);
457
+ });
458
+
459
+ test("local retire uses injected platform token from the app host", async () => {
460
+ const entry = makeEntry("assistant-1", { name: "Example Assistant" });
461
+ writeLockfile([entry]);
462
+ process.env.VELLUM_PLATFORM_TOKEN = "host-session-token";
463
+ readPlatformTokenMock.mockReturnValue(null);
464
+ getPlatformUrlMock.mockReturnValue("https://platform.example.test");
465
+ readGatewayCredentialMock.mockImplementation(async (_gatewayUrl, name) => {
466
+ if (name === "vellum:platform_assistant_id") {
467
+ return { value: "platform-assistant-1", unreachable: false };
468
+ }
469
+ if (name === "vellum:platform_base_url") {
470
+ return { value: "https://platform.example.test", unreachable: false };
471
+ }
472
+ if (name === "vellum:platform_organization_id") {
473
+ return { value: "org-123", unreachable: false };
474
+ }
475
+ return { value: null, unreachable: false };
476
+ });
477
+ process.argv = ["bun", "vellum", "retire", "assistant-1", "--yes"];
478
+
479
+ await retire();
480
+
481
+ expect(loopbackSafeFetchMock).toHaveBeenCalledWith(
482
+ "https://platform.example.test/v1/assistants/platform-assistant-1/retire/",
483
+ {
484
+ method: "DELETE",
485
+ headers: {
486
+ "Content-Type": "application/json",
487
+ "X-Session-Token": "host-session-token",
488
+ "Vellum-Organization-Id": "org-123",
489
+ },
490
+ },
491
+ );
492
+ expect(retireLocalMock).toHaveBeenCalledWith("assistant-1", entry);
493
+ });
494
+
495
+ test("local retire continues when self-hosted platform unregister fails", async () => {
496
+ const entry = makeEntry("assistant-1", { name: "Example Assistant" });
497
+ writeLockfile([entry]);
498
+ readPlatformTokenMock.mockReturnValue("session-token");
499
+ getPlatformUrlMock.mockReturnValue("https://platform.example.test");
500
+ readGatewayCredentialMock.mockImplementation(async (_gatewayUrl, name) => {
501
+ if (name === "vellum:platform_assistant_id") {
502
+ return { value: "platform-assistant-1", unreachable: false };
503
+ }
504
+ if (name === "vellum:platform_base_url") {
505
+ return { value: "https://platform.example.test", unreachable: false };
506
+ }
507
+ if (name === "vellum:platform_organization_id") {
508
+ return { value: "org-123", unreachable: false };
509
+ }
510
+ return { value: null, unreachable: false };
511
+ });
512
+ loopbackSafeFetchMock.mockResolvedValue(
513
+ new Response("platform unavailable", { status: 503 }),
514
+ );
515
+ process.argv = ["bun", "vellum", "retire", "assistant-1", "--yes"];
516
+
517
+ await retire();
518
+
519
+ expect(retireLocalMock).toHaveBeenCalledWith("assistant-1", entry);
520
+ expect(loadAllAssistants()).toEqual([]);
521
+ expect(consoleWarnSpy.mock.calls.flat().join("\n")).toContain(
522
+ "Platform self-hosted unregister failed (503): platform unavailable",
523
+ );
524
+ });
525
+
173
526
  test("non-interactive retire without --yes fails before deleting", async () => {
174
527
  const entry = makeEntry("assistant-1", { name: "Example Assistant" });
175
528
  writeLockfile([entry]);
@@ -2266,6 +2266,14 @@ describe("platform credential injection", () => {
2266
2266
  userId: "user-1",
2267
2267
  webhookSecret: "webhook-secret-123",
2268
2268
  });
2269
+ expect(saveAssistantEntryMock).toHaveBeenCalledWith(
2270
+ expect.objectContaining({
2271
+ assistantId: "my-local",
2272
+ platformAssistantId: "platform-assistant-1",
2273
+ platformBaseUrl: "https://platform.vellum.ai",
2274
+ platformOrganizationId: "org-1",
2275
+ }),
2276
+ );
2269
2277
  } finally {
2270
2278
  restoreFetch();
2271
2279
  }
@@ -76,13 +76,6 @@ describe("buildAuthorizeUrl", () => {
76
76
  expect(url.searchParams.has("prompt")).toBe(false);
77
77
  });
78
78
 
79
- test("provider hint replaces authkit", () => {
80
- const url = new URL(
81
- buildAuthorizeUrl({ ...base, providerHint: "GoogleOAuth" }),
82
- );
83
- expect(url.searchParams.get("provider")).toBe("GoogleOAuth");
84
- });
85
-
86
79
  test("login hint is forwarded", () => {
87
80
  const url = new URL(
88
81
  buildAuthorizeUrl({ ...base, loginHint: "user@example.com" }),
@@ -178,6 +178,7 @@ interface HatchArgs {
178
178
  remote: RemoteHost;
179
179
  watch: boolean;
180
180
  sourcePath: string | null;
181
+ preview: boolean;
181
182
  configValues: Record<string, string>;
182
183
  flagEnvVars: Record<string, string>;
183
184
  analyze: boolean;
@@ -203,6 +204,7 @@ function parseArgs(): HatchArgs {
203
204
  let remote: RemoteHost = DEFAULT_REMOTE;
204
205
  let watch = false;
205
206
  let sourcePath: string | null = null;
207
+ let preview = false;
206
208
  const configValues: Record<string, string> = {};
207
209
  let analyze = false;
208
210
  let netnsContainer: string | null = null;
@@ -231,6 +233,7 @@ function parseArgs(): HatchArgs {
231
233
  );
232
234
  console.log(
233
235
  " --source <path> Build images from a local source tree at <path> (no watcher). Useful for callers (e.g. evals) that want each run to pick up local CLI changes.",
236
+ " --preview When pulling published images (no local source), resolve from the preview channel (latest preview release) instead of latest-stable. Also settable via VELLUM_HATCH_CHANNEL=preview.",
234
237
  );
235
238
  console.log(
236
239
  " --keep-alive Stay alive after hatch, exit when gateway stops",
@@ -263,6 +266,8 @@ function parseArgs(): HatchArgs {
263
266
  watch = true;
264
267
  } else if (arg === "--analyze") {
265
268
  analyze = true;
269
+ } else if (arg === "--preview") {
270
+ preview = true;
266
271
  } else if (arg === "--source") {
267
272
  const next = args[i + 1];
268
273
  if (!next || next.startsWith("-")) {
@@ -349,7 +354,7 @@ function parseArgs(): HatchArgs {
349
354
  species = arg as Species;
350
355
  } else {
351
356
  console.error(
352
- `Error: Unknown argument '${arg}'. Valid options: ${VALID_SPECIES.join(", ")}, -d, --watch, --source <path>, --keep-alive, --name <name>, --remote <${VALID_REMOTE_HOSTS.join("|")}>, --config <key=value>, --flag <key=value>, --analyze, --disable-platform, --netns-container <name>, --gateway-port <port>, --assistant-ca-cert <path>`,
357
+ `Error: Unknown argument '${arg}'. Valid options: ${VALID_SPECIES.join(", ")}, -d, --watch, --source <path>, --preview, --keep-alive, --name <name>, --remote <${VALID_REMOTE_HOSTS.join("|")}>, --config <key=value>, --flag <key=value>, --analyze, --disable-platform, --netns-container <name>, --gateway-port <port>, --assistant-ca-cert <path>`,
353
358
  );
354
359
  process.exit(1);
355
360
  }
@@ -363,6 +368,7 @@ function parseArgs(): HatchArgs {
363
368
  remote,
364
369
  watch,
365
370
  sourcePath,
371
+ preview,
366
372
  configValues,
367
373
  flagEnvVars,
368
374
  analyze,
@@ -602,6 +608,7 @@ export async function hatch(): Promise<void> {
602
608
  remote,
603
609
  watch,
604
610
  sourcePath,
611
+ preview,
605
612
  configValues,
606
613
  flagEnvVars,
607
614
  analyze,
@@ -681,6 +688,7 @@ export async function hatch(): Promise<void> {
681
688
  flagEnvVars,
682
689
  sourcePath,
683
690
  analyze,
691
+ channel: preview ? "preview" : undefined,
684
692
  netnsContainer: netnsContainer ?? undefined,
685
693
  gatewayPort: gatewayPort ?? undefined,
686
694
  assistantCaCertPath: assistantCaCert ?? undefined,
@@ -7,6 +7,7 @@ import {
7
7
  loadAllAssistants,
8
8
  removeAssistantEntry,
9
9
  resolveAssistant,
10
+ saveAssistantEntry,
10
11
  setActiveAssistant,
11
12
  } from "../lib/assistant-config";
12
13
  import { computeDeviceId } from "../lib/guardian-token";
@@ -383,6 +384,7 @@ export async function login(): Promise<void> {
383
384
  fetchCurrentVersion(entry.runtimeUrl),
384
385
  fetchAssistantIngressUrl(entry.runtimeUrl, entry.bearerToken),
385
386
  ]);
387
+ const platformUrl = getPlatformUrl();
386
388
  const registration = await ensureSelfHostedLocalRegistration(
387
389
  token,
388
390
  orgId,
@@ -390,9 +392,15 @@ export async function login(): Promise<void> {
390
392
  entry.assistantId,
391
393
  "cli",
392
394
  assistantVersion,
393
- getPlatformUrl(),
395
+ platformUrl,
394
396
  ingressUrl,
395
397
  );
398
+ saveAssistantEntry({
399
+ ...entry,
400
+ platformAssistantId: registration.assistant.id,
401
+ platformBaseUrl: platformUrl,
402
+ platformOrganizationId: orgId,
403
+ });
396
404
  console.log(
397
405
  `Registered assistant: ${registration.assistant.name} (${registration.assistant.id})`,
398
406
  );
@@ -433,7 +441,7 @@ export async function login(): Promise<void> {
433
441
  bearerToken: entry.bearerToken,
434
442
  assistantApiKey,
435
443
  platformAssistantId: registration.assistant.id,
436
- platformBaseUrl: getPlatformUrl(),
444
+ platformBaseUrl: platformUrl,
437
445
  organizationId: orgId,
438
446
  userId: user.id,
439
447
  webhookSecret: registration.webhook_secret,