@vellumai/cli 0.10.3 → 0.10.4-dev.202607010119.ad15dbb

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]);
@@ -259,12 +259,27 @@ const localRuntimePollJobStatusMock = mock<
259
259
  },
260
260
  }));
261
261
 
262
+ const localRuntimePreflightFromGcsMock = mock<
263
+ typeof localRuntimeClient.localRuntimePreflightFromGcs
264
+ >(async () => ({
265
+ can_import: true,
266
+ summary: {
267
+ files_to_create: 2,
268
+ files_to_overwrite: 1,
269
+ files_unchanged: 0,
270
+ total_files: 3,
271
+ },
272
+ files: [],
273
+ conflicts: [],
274
+ }));
275
+
262
276
  mock.module("../lib/local-runtime-client.js", () => ({
263
277
  ...realLocalRuntimeClient,
264
278
  localRuntimeExportToGcs: localRuntimeExportToGcsMock,
265
279
  localRuntimeImportFromGcs: localRuntimeImportFromGcsMock,
266
280
  localRuntimeIdentity: localRuntimeIdentityMock,
267
281
  localRuntimePollJobStatus: localRuntimePollJobStatusMock,
282
+ localRuntimePreflightFromGcs: localRuntimePreflightFromGcsMock,
268
283
  }));
269
284
 
270
285
  const hatchLocalMock = mock(async () => {});
@@ -483,6 +498,18 @@ beforeEach(() => {
483
498
  localRuntimeImportFromGcsMock.mockResolvedValue({
484
499
  jobId: "local-import-job-1",
485
500
  });
501
+ localRuntimePreflightFromGcsMock.mockReset();
502
+ localRuntimePreflightFromGcsMock.mockResolvedValue({
503
+ can_import: true,
504
+ summary: {
505
+ files_to_create: 2,
506
+ files_to_overwrite: 1,
507
+ files_unchanged: 0,
508
+ total_files: 3,
509
+ },
510
+ files: [],
511
+ conflicts: [],
512
+ });
486
513
  localRuntimeIdentityMock.mockReset();
487
514
  localRuntimeIdentityMock.mockResolvedValue({ version: "0.6.5" });
488
515
  localRuntimePollJobStatusMock.mockReset();
@@ -1932,7 +1959,7 @@ describe("dry-run", () => {
1932
1959
  }
1933
1960
  });
1934
1961
 
1935
- test("dry-run against local target fails fast (no preflight-from-gcs runtime endpoint yet)", async () => {
1962
+ test("dry-run with existing local target calls preflight-from-gcs on runtime", async () => {
1936
1963
  setArgv("--from", "my-platform", "--local", "my-local", "--dry-run");
1937
1964
 
1938
1965
  const platformEntry = makeEntry("my-platform", {
@@ -1947,26 +1974,24 @@ describe("dry-run", () => {
1947
1974
  return null;
1948
1975
  });
1949
1976
 
1950
- platformPollJobStatusMock.mockResolvedValue({
1951
- jobId: "platform-export-job-1",
1952
- type: "export",
1953
- status: "complete",
1954
- bundleKey: "bundle-key-from-platform",
1955
- });
1956
-
1957
1977
  const restoreFetch = installTrackingFetch();
1958
1978
  try {
1959
- await expect(teleport()).rejects.toThrow("process.exit:1");
1960
- expect(consoleErrorSpy).toHaveBeenCalledWith(
1961
- expect.stringContaining(
1962
- "--dry-run is not yet supported for local or docker targets",
1963
- ),
1979
+ await teleport();
1980
+
1981
+ // Preflight was run against the local runtime
1982
+ expect(localRuntimePreflightFromGcsMock).toHaveBeenCalledTimes(1);
1983
+ expect(localRuntimePreflightFromGcsMock).toHaveBeenCalledWith(
1984
+ expect.objectContaining({ assistantId: "my-local" }),
1985
+ expect.any(String),
1986
+ { bundleUrl: "https://storage.googleapis.com/bucket/signed-download" },
1964
1987
  );
1965
1988
 
1966
- // Must fail BEFORE any export work no signed URL request, no runtime
1967
- // export kickoff, nothing that costs time or bandwidth.
1968
- expect(platformRequestSignedUrlMock).not.toHaveBeenCalled();
1969
- expect(localRuntimeExportToGcsMock).not.toHaveBeenCalled();
1989
+ // No actual import happeneddry-run only
1990
+ expect(localRuntimeImportFromGcsMock).not.toHaveBeenCalled();
1991
+
1992
+ // Source was not retired
1993
+ expect(retireLocalMock).not.toHaveBeenCalled();
1994
+ expect(retireDockerMock).not.toHaveBeenCalled();
1970
1995
  } finally {
1971
1996
  restoreFetch();
1972
1997
  }
@@ -2241,6 +2266,14 @@ describe("platform credential injection", () => {
2241
2266
  userId: "user-1",
2242
2267
  webhookSecret: "webhook-secret-123",
2243
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
+ );
2244
2277
  } finally {
2245
2278
  restoreFetch();
2246
2279
  }
@@ -98,7 +98,7 @@ function printHelp(): void {
98
98
  " $ vellum flags set voice-mode true # enable a flag",
99
99
  );
100
100
  console.log(
101
- " $ vellum flags set external-plugins true --assistant eval-1 # target by name/id",
101
+ " $ vellum flags set voice-mode true --assistant eval-1 # target by name/id",
102
102
  );
103
103
  }
104
104
 
@@ -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,