@vellumai/cli 0.5.16 → 0.6.1

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.
@@ -12,7 +12,6 @@ import {
12
12
  } from "../lib/guardian-token.js";
13
13
  import {
14
14
  readPlatformToken,
15
- fetchOrganizationId,
16
15
  getPlatformUrl,
17
16
  hatchAssistant,
18
17
  platformInitiateExport,
@@ -20,6 +19,10 @@ import {
20
19
  platformDownloadExport,
21
20
  platformImportPreflight,
22
21
  platformImportBundle,
22
+ platformRequestUploadUrl,
23
+ platformUploadToSignedUrl,
24
+ platformImportPreflightFromGcs,
25
+ platformImportBundleFromGcs,
23
26
  } from "../lib/platform-client.js";
24
27
  import {
25
28
  hatchDocker,
@@ -419,7 +422,7 @@ async function importViaHttp(
419
422
  "Content-Type": "application/octet-stream",
420
423
  },
421
424
  body: new Blob([bundleData]),
422
- signal: AbortSignal.timeout(120_000),
425
+ signal: AbortSignal.timeout(300_000),
423
426
  });
424
427
 
425
428
  // Retry once with a fresh token on 401
@@ -443,13 +446,13 @@ async function importViaHttp(
443
446
  "Content-Type": "application/octet-stream",
444
447
  },
445
448
  body: new Blob([bundleData]),
446
- signal: AbortSignal.timeout(120_000),
449
+ signal: AbortSignal.timeout(300_000),
447
450
  });
448
451
  }
449
452
  }
450
453
  } catch (err) {
451
454
  if (err instanceof Error && err.name === "TimeoutError") {
452
- console.error("Error: Import request timed out after 2 minutes.");
455
+ console.error("Error: Import request timed out after 5 minutes.");
453
456
  process.exit(1);
454
457
  }
455
458
  const msg = err instanceof Error ? err.message : String(err);
@@ -485,36 +488,12 @@ async function exportFromAssistant(
485
488
  process.exit(1);
486
489
  }
487
490
 
488
- let orgId: string;
489
- try {
490
- orgId = await fetchOrganizationId(token, entry.runtimeUrl);
491
- } catch (err) {
492
- const msg = err instanceof Error ? err.message : String(err);
493
- if (msg.includes("401") || msg.includes("403")) {
494
- console.error("Authentication failed. Run 'vellum login' to refresh.");
495
- process.exit(1);
496
- }
497
- throw err;
498
- }
499
-
500
491
  // Initiate export job
501
- let jobId: string;
502
- try {
503
- const result = await platformInitiateExport(
504
- token,
505
- orgId,
506
- "teleport export",
507
- entry.runtimeUrl,
508
- );
509
- jobId = result.jobId;
510
- } catch (err) {
511
- const msg = err instanceof Error ? err.message : String(err);
512
- if (msg.includes("401") || msg.includes("403")) {
513
- console.error("Authentication failed. Run 'vellum login' to refresh.");
514
- process.exit(1);
515
- }
516
- throw err;
517
- }
492
+ const { jobId } = await platformInitiateExport(
493
+ token,
494
+ "teleport export",
495
+ entry.runtimeUrl,
496
+ );
518
497
 
519
498
  console.log(`Export started (job ${jobId})...`);
520
499
 
@@ -527,12 +506,7 @@ async function exportFromAssistant(
527
506
  while (Date.now() < deadline) {
528
507
  let status: { status: string; downloadUrl?: string; error?: string };
529
508
  try {
530
- status = await platformPollExportStatus(
531
- jobId,
532
- token,
533
- orgId,
534
- entry.runtimeUrl,
535
- );
509
+ status = await platformPollExportStatus(jobId, token, entry.runtimeUrl);
536
510
  } catch (err) {
537
511
  const msg = err instanceof Error ? err.message : String(err);
538
512
  if (msg.includes("not found")) {
@@ -628,6 +602,7 @@ async function importToAssistant(
628
602
  cloud: string,
629
603
  bundleData: Uint8Array<ArrayBuffer>,
630
604
  dryRun: boolean,
605
+ preUploadedBundleKey?: string | null,
631
606
  ): Promise<void> {
632
607
  if (cloud === "vellum") {
633
608
  // Platform target
@@ -637,16 +612,29 @@ async function importToAssistant(
637
612
  process.exit(1);
638
613
  }
639
614
 
640
- let orgId: string;
641
- try {
642
- orgId = await fetchOrganizationId(token, entry.runtimeUrl);
643
- } catch (err) {
644
- const msg = err instanceof Error ? err.message : String(err);
645
- if (msg.includes("401") || msg.includes("403")) {
646
- console.error("Authentication failed. Run 'vellum login' to refresh.");
647
- process.exit(1);
615
+ // Use pre-uploaded bundle key if provided (string), skip upload if null
616
+ // (signals signed URLs were already tried and unavailable), or try
617
+ // signed-URL upload if undefined (never attempted).
618
+ let bundleKey: string | undefined =
619
+ preUploadedBundleKey === null ? undefined : preUploadedBundleKey;
620
+ if (preUploadedBundleKey === undefined) {
621
+ try {
622
+ const { uploadUrl, bundleKey: key } = await platformRequestUploadUrl(
623
+ token,
624
+ entry.runtimeUrl,
625
+ );
626
+ bundleKey = key;
627
+ console.log("Uploading bundle...");
628
+ await platformUploadToSignedUrl(uploadUrl, bundleData);
629
+ } catch (err) {
630
+ // If signed uploads unavailable (503), fall back to inline upload
631
+ const msg = err instanceof Error ? err.message : String(err);
632
+ if (msg.includes("not available")) {
633
+ bundleKey = undefined;
634
+ } else {
635
+ throw err;
636
+ }
648
637
  }
649
- throw err;
650
638
  }
651
639
 
652
640
  if (dryRun) {
@@ -657,12 +645,13 @@ async function importToAssistant(
657
645
  body: Record<string, unknown>;
658
646
  };
659
647
  try {
660
- preflightResult = await platformImportPreflight(
661
- bundleData,
662
- token,
663
- orgId,
664
- entry.runtimeUrl,
665
- );
648
+ preflightResult = bundleKey
649
+ ? await platformImportPreflightFromGcs(
650
+ bundleKey,
651
+ token,
652
+ entry.runtimeUrl,
653
+ )
654
+ : await platformImportPreflight(bundleData, token, entry.runtimeUrl);
666
655
  } catch (err) {
667
656
  if (err instanceof Error && err.name === "TimeoutError") {
668
657
  console.error("Error: Preflight request timed out after 2 minutes.");
@@ -712,15 +701,12 @@ async function importToAssistant(
712
701
 
713
702
  let importResult: { statusCode: number; body: Record<string, unknown> };
714
703
  try {
715
- importResult = await platformImportBundle(
716
- bundleData,
717
- token,
718
- orgId,
719
- entry.runtimeUrl,
720
- );
704
+ importResult = bundleKey
705
+ ? await platformImportBundleFromGcs(bundleKey, token, entry.runtimeUrl)
706
+ : await platformImportBundle(bundleData, token, entry.runtimeUrl);
721
707
  } catch (err) {
722
708
  if (err instanceof Error && err.name === "TimeoutError") {
723
- console.error("Error: Import request timed out after 2 minutes.");
709
+ console.error("Error: Import request timed out after 5 minutes.");
724
710
  process.exit(1);
725
711
  }
726
712
  throw err;
@@ -1080,10 +1066,19 @@ export async function teleport(): Promise<void> {
1080
1066
  // No existing target — just describe what would happen
1081
1067
  console.log("Dry run summary:");
1082
1068
  console.log(` Would export data from: ${from} (${fromCloud})`);
1083
- console.log(
1084
- ` Would hatch a new ${targetEnv} assistant${targetName ? ` named '${targetName}'` : ""}`,
1085
- );
1086
- console.log(` Would import data into the new assistant`);
1069
+ if (targetEnv === "platform") {
1070
+ // For platform targets, reflect the reordered flow
1071
+ console.log(` Would upload bundle via signed URL (if available)`);
1072
+ console.log(
1073
+ ` Would hatch a new ${targetEnv} assistant${targetName ? ` named '${targetName}'` : ""}`,
1074
+ );
1075
+ console.log(` Would import data into the new assistant`);
1076
+ } else {
1077
+ console.log(
1078
+ ` Would hatch a new ${targetEnv} assistant${targetName ? ` named '${targetName}'` : ""}`,
1079
+ );
1080
+ console.log(` Would import data into the new assistant`);
1081
+ }
1087
1082
  }
1088
1083
 
1089
1084
  console.log(`Dry run complete — no changes were made.`);
@@ -1094,6 +1089,71 @@ export async function teleport(): Promise<void> {
1094
1089
  console.log(`Exporting from ${from} (${fromCloud})...`);
1095
1090
  const bundleData = await exportFromAssistant(fromEntry, fromCloud);
1096
1091
 
1092
+ // Platform target: reordered flow — upload to GCS before hatching so that
1093
+ // if upload fails, no empty assistant is left dangling on the platform.
1094
+ if (targetEnv === "platform") {
1095
+ // Step B — Auth
1096
+ const token = readPlatformToken();
1097
+ if (!token) {
1098
+ console.error("Not logged in. Run 'vellum login' first.");
1099
+ process.exit(1);
1100
+ }
1101
+
1102
+ // If targeting an existing assistant, validate cloud match early — before
1103
+ // uploading — so we don't waste a GCS upload on an invalid command.
1104
+ const existingTarget = targetName ? findAssistantByName(targetName) : null;
1105
+ if (existingTarget) {
1106
+ const existingCloud = resolveCloud(existingTarget);
1107
+ if (existingCloud !== "vellum") {
1108
+ console.error(
1109
+ `Error: Assistant '${targetName}' is a ${existingCloud} assistant, not platform. ` +
1110
+ `Use --${existingCloud} to target it.`,
1111
+ );
1112
+ process.exit(1);
1113
+ }
1114
+ }
1115
+
1116
+ // Use the existing target's runtimeUrl for all platform calls so upload
1117
+ // and import hit the same instance.
1118
+ const targetPlatformUrl = existingTarget?.runtimeUrl;
1119
+
1120
+ // Step C — Upload to GCS
1121
+ // bundleKey: string = uploaded successfully, null = tried but unavailable,
1122
+ // undefined would mean "never tried" (not used here).
1123
+ let bundleKey: string | null = null;
1124
+ try {
1125
+ const { uploadUrl, bundleKey: key } = await platformRequestUploadUrl(
1126
+ token,
1127
+ targetPlatformUrl,
1128
+ );
1129
+ bundleKey = key;
1130
+ console.log("Uploading bundle to GCS...");
1131
+ await platformUploadToSignedUrl(uploadUrl, bundleData);
1132
+ } catch (err) {
1133
+ // If signed uploads unavailable (503), fall back to inline upload later
1134
+ const msg = err instanceof Error ? err.message : String(err);
1135
+ if (msg.includes("not available")) {
1136
+ bundleKey = null;
1137
+ } else {
1138
+ throw err;
1139
+ }
1140
+ }
1141
+
1142
+ // Step D — Hatch (upload succeeded or fallback to inline — safe to hatch)
1143
+ const toEntry = await resolveOrHatchTarget(targetEnv, targetName);
1144
+ const toCloud = resolveCloud(toEntry);
1145
+
1146
+ // Step E — Import from GCS (or inline fallback)
1147
+ // Pass bundleKey (string) or null to signal "already tried, use inline".
1148
+ console.log(`Importing to ${toEntry.assistantId} (${toCloud})...`);
1149
+ await importToAssistant(toEntry, toCloud, bundleData, false, bundleKey);
1150
+
1151
+ // Success summary
1152
+ console.log(`Teleport complete: ${from} → ${toEntry.assistantId}`);
1153
+ return;
1154
+ }
1155
+
1156
+ // Non-platform targets (local/docker): existing flow unchanged
1097
1157
  // For local<->docker transfers, stop (sleep) the source to free up ports
1098
1158
  // before hatching the target. We do NOT retire yet — if hatch or import
1099
1159
  // fails, the user can recover by running `vellum wake <source>`.
@@ -18,7 +18,7 @@ import {
18
18
  } from "../lib/docker";
19
19
  import { resolveImageRefs } from "../lib/platform-releases";
20
20
  import {
21
- fetchOrganizationId,
21
+ authHeaders,
22
22
  getPlatformUrl,
23
23
  readPlatformToken,
24
24
  } from "../lib/platform-client";
@@ -42,7 +42,7 @@ import {
42
42
  UPGRADE_PROGRESS,
43
43
  waitForReady,
44
44
  } from "../lib/upgrade-lifecycle.js";
45
- import { parseVersion } from "../lib/version-compat.js";
45
+ import { compareVersions } from "../lib/version-compat.js";
46
46
 
47
47
  interface UpgradeArgs {
48
48
  name: string | null;
@@ -193,21 +193,12 @@ async function upgradeDocker(
193
193
  // Users should use `vellum rollback --version <version>` for downgrades.
194
194
  const currentVersion = entry.serviceGroupVersion;
195
195
  if (currentVersion && versionTag) {
196
- const current = parseVersion(currentVersion);
197
- const target = parseVersion(versionTag);
198
- if (current && target) {
199
- const isOlder =
200
- target.major < current.major ||
201
- (target.major === current.major && target.minor < current.minor) ||
202
- (target.major === current.major &&
203
- target.minor === current.minor &&
204
- target.patch < current.patch);
205
- if (isOlder) {
206
- const msg = `Cannot upgrade to an older version (${versionTag} < ${currentVersion}). Use \`vellum rollback --version ${versionTag}\` instead.`;
207
- console.error(msg);
208
- emitCliError("VERSION_DIRECTION", msg);
209
- process.exit(1);
210
- }
196
+ const cmp = compareVersions(versionTag, currentVersion);
197
+ if (cmp !== null && cmp < 0) {
198
+ const msg = `Cannot upgrade to an older version (${versionTag} < ${currentVersion}). Use \`vellum rollback --version ${versionTag}\` instead.`;
199
+ console.error(msg);
200
+ emitCliError("VERSION_DIRECTION", msg);
201
+ process.exit(1);
211
202
  }
212
203
  }
213
204
 
@@ -291,6 +282,11 @@ async function upgradeDocker(
291
282
  ` Captured ${Object.keys(capturedEnv).length} env var(s) from ${res.assistantContainer}\n`,
292
283
  );
293
284
 
285
+ // Capture GUARDIAN_BOOTSTRAP_SECRET from the gateway container (it is only
286
+ // set on gateway, not assistant) so it persists across container restarts.
287
+ const gatewayEnv = await captureContainerEnv(res.gatewayContainer);
288
+ const bootstrapSecret = gatewayEnv["GUARDIAN_BOOTSTRAP_SECRET"];
289
+
294
290
  // Notify connected clients that an upgrade is about to begin.
295
291
  // This must fire BEFORE any progress broadcasts so the UI sets
296
292
  // isUpdateInProgress = true and starts displaying status messages.
@@ -419,6 +415,7 @@ async function upgradeDocker(
419
415
  await startContainers(
420
416
  {
421
417
  signingKey,
418
+ bootstrapSecret,
422
419
  cesServiceToken,
423
420
  extraAssistantEnv,
424
421
  gatewayPort,
@@ -517,6 +514,7 @@ async function upgradeDocker(
517
514
  await startContainers(
518
515
  {
519
516
  signingKey,
517
+ bootstrapSecret,
520
518
  cesServiceToken,
521
519
  extraAssistantEnv,
522
520
  gatewayPort,
@@ -687,21 +685,12 @@ async function upgradePlatform(
687
685
  // we must not block the request based on the local CLI version.
688
686
  const currentVersion = entry.serviceGroupVersion;
689
687
  if (version && currentVersion) {
690
- const current = parseVersion(currentVersion);
691
- const target = parseVersion(version);
692
- if (current && target) {
693
- const isOlder =
694
- target.major < current.major ||
695
- (target.major === current.major && target.minor < current.minor) ||
696
- (target.major === current.major &&
697
- target.minor === current.minor &&
698
- target.patch < current.patch);
699
- if (isOlder) {
700
- const msg = `Cannot upgrade to an older version (${version} < ${currentVersion}). Use \`vellum rollback --version ${version}\` instead.`;
701
- console.error(msg);
702
- emitCliError("VERSION_DIRECTION", msg);
703
- process.exit(1);
704
- }
688
+ const cmp = compareVersions(version, currentVersion);
689
+ if (cmp !== null && cmp < 0) {
690
+ const msg = `Cannot upgrade to an older version (${version} < ${currentVersion}). Use \`vellum rollback --version ${version}\` instead.`;
691
+ console.error(msg);
692
+ emitCliError("VERSION_DIRECTION", msg);
693
+ process.exit(1);
705
694
  }
706
695
  }
707
696
 
@@ -718,7 +707,7 @@ async function upgradePlatform(
718
707
  process.exit(1);
719
708
  }
720
709
 
721
- const orgId = await fetchOrganizationId(token, entry.runtimeUrl);
710
+ const headers = await authHeaders(token, entry.runtimeUrl);
722
711
 
723
712
  const url = `${entry.runtimeUrl || getPlatformUrl()}/v1/assistants/upgrade/`;
724
713
  const body: { assistant_id?: string; version?: string } = {
@@ -730,14 +719,28 @@ async function upgradePlatform(
730
719
 
731
720
  const response = await fetch(url, {
732
721
  method: "POST",
733
- headers: {
734
- "Content-Type": "application/json",
735
- "X-Session-Token": token,
736
- "Vellum-Organization-Id": orgId,
737
- },
722
+ headers,
738
723
  body: JSON.stringify(body),
739
724
  });
740
725
 
726
+ if (response.status === 401 || response.status === 403) {
727
+ const text = await response.text();
728
+ console.error(
729
+ `Authentication failed (${response.status}). Run 'vellum login' to refresh.`,
730
+ );
731
+ emitCliError("AUTH_FAILED", "Authentication failed", text);
732
+ try {
733
+ await broadcastUpgradeEvent(
734
+ entry.runtimeUrl,
735
+ entry.assistantId,
736
+ buildCompleteEvent(entry.serviceGroupVersion ?? "unknown", false),
737
+ );
738
+ } catch {
739
+ // Best-effort — broadcast may fail if the assistant is unreachable
740
+ }
741
+ process.exit(1);
742
+ }
743
+
741
744
  if (!response.ok) {
742
745
  const text = await response.text();
743
746
  console.error(
@@ -748,11 +751,15 @@ async function upgradePlatform(
748
751
  `Platform upgrade failed (${response.status})`,
749
752
  text,
750
753
  );
751
- await broadcastUpgradeEvent(
752
- entry.runtimeUrl,
753
- entry.assistantId,
754
- buildCompleteEvent(entry.serviceGroupVersion ?? "unknown", false),
755
- );
754
+ try {
755
+ await broadcastUpgradeEvent(
756
+ entry.runtimeUrl,
757
+ entry.assistantId,
758
+ buildCompleteEvent(entry.serviceGroupVersion ?? "unknown", false),
759
+ );
760
+ } catch {
761
+ // Best-effort — broadcast may fail if the assistant is unreachable
762
+ }
756
763
  process.exit(1);
757
764
  }
758
765
 
package/src/index.ts CHANGED
@@ -4,8 +4,10 @@ import cliPkg from "../package.json";
4
4
  import { backup } from "./commands/backup";
5
5
  import { clean } from "./commands/clean";
6
6
  import { client } from "./commands/client";
7
+ import { events } from "./commands/events";
7
8
  import { hatch } from "./commands/hatch";
8
9
  import { login, logout, whoami } from "./commands/login";
10
+ import { message } from "./commands/message";
9
11
  import { pair } from "./commands/pair";
10
12
  import { ps } from "./commands/ps";
11
13
  import { recover } from "./commands/recover";
@@ -33,9 +35,11 @@ const commands = {
33
35
  backup,
34
36
  clean,
35
37
  client,
38
+ events,
36
39
  hatch,
37
40
  login,
38
41
  logout,
42
+ message,
39
43
  pair,
40
44
  ps,
41
45
  recover,
@@ -62,9 +66,11 @@ function printHelp(): void {
62
66
  console.log(" backup Export a backup of a running assistant");
63
67
  console.log(" clean Kill orphaned vellum processes");
64
68
  console.log(" client Connect to a hatched assistant");
69
+ console.log(" events Stream events from a running assistant");
65
70
  console.log(" hatch Create a new assistant instance");
66
71
  console.log(" login Log in to the Vellum platform");
67
72
  console.log(" logout Log out of the Vellum platform");
73
+ console.log(" message Send a message to a running assistant");
68
74
  console.log(" pair Pair with a remote assistant via QR code");
69
75
  console.log(
70
76
  " ps List assistants (or processes for a specific assistant)",
@@ -0,0 +1,13 @@
1
+ /** Extract a named flag's value from an arg list, returning [value, remaining]. */
2
+ export function extractFlag(
3
+ args: string[],
4
+ flag: string,
5
+ ): [string | undefined, string[]] {
6
+ const idx = args.indexOf(flag);
7
+ if (idx === -1 || idx + 1 >= args.length) {
8
+ return [undefined, args.filter((a) => a !== flag)];
9
+ }
10
+ const value = args[idx + 1]!;
11
+ const remaining = [...args.slice(0, idx), ...args.slice(idx + 2)];
12
+ return [value, remaining];
13
+ }