@vellumai/cli 0.5.12 → 0.5.13

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.5.12",
3
+ "version": "0.5.13",
4
4
  "description": "CLI tools for vellum-assistant",
5
5
  "type": "module",
6
6
  "exports": {
@@ -66,7 +66,7 @@ export async function backup(): Promise<void> {
66
66
  const cloud =
67
67
  entry.cloud || (entry.project ? "gcp" : entry.sshUser ? "custom" : "local");
68
68
  if (cloud === "vellum") {
69
- await backupPlatform(name, outputArg);
69
+ await backupPlatform(name, outputArg, entry.runtimeUrl);
70
70
  return;
71
71
  }
72
72
 
@@ -184,7 +184,11 @@ export async function backup(): Promise<void> {
184
184
  // Platform (Vellum-hosted) backup via Django async migration export
185
185
  // ---------------------------------------------------------------------------
186
186
 
187
- async function backupPlatform(name: string, outputArg?: string): Promise<void> {
187
+ async function backupPlatform(
188
+ name: string,
189
+ outputArg?: string,
190
+ runtimeUrl?: string,
191
+ ): Promise<void> {
188
192
  // Step 1 — Authenticate
189
193
  const token = readPlatformToken();
190
194
  if (!token) {
@@ -194,7 +198,7 @@ async function backupPlatform(name: string, outputArg?: string): Promise<void> {
194
198
 
195
199
  let orgId: string;
196
200
  try {
197
- orgId = await fetchOrganizationId(token);
201
+ orgId = await fetchOrganizationId(token, runtimeUrl);
198
202
  } catch (err) {
199
203
  const msg = err instanceof Error ? err.message : String(err);
200
204
  if (msg.includes("401") || msg.includes("403")) {
@@ -207,7 +211,12 @@ async function backupPlatform(name: string, outputArg?: string): Promise<void> {
207
211
  // Step 2 — Initiate export job
208
212
  let jobId: string;
209
213
  try {
210
- const result = await platformInitiateExport(token, orgId, "CLI backup");
214
+ const result = await platformInitiateExport(
215
+ token,
216
+ orgId,
217
+ "CLI backup",
218
+ runtimeUrl,
219
+ );
211
220
  jobId = result.jobId;
212
221
  } catch (err) {
213
222
  const msg = err instanceof Error ? err.message : String(err);
@@ -235,7 +244,7 @@ async function backupPlatform(name: string, outputArg?: string): Promise<void> {
235
244
  while (Date.now() < deadline) {
236
245
  let status: { status: string; downloadUrl?: string; error?: string };
237
246
  try {
238
- status = await platformPollExportStatus(jobId, token, orgId);
247
+ status = await platformPollExportStatus(jobId, token, orgId, runtimeUrl);
239
248
  } catch (err) {
240
249
  const msg = err instanceof Error ? err.message : String(err);
241
250
  // Let non-transient errors (e.g. 404 "job not found") propagate immediately
@@ -47,7 +47,11 @@ import {
47
47
  stopLocalProcesses,
48
48
  } from "../lib/local";
49
49
  import { maybeStartNgrokTunnel } from "../lib/ngrok";
50
- import { getPlatformUrl } from "../lib/platform-client";
50
+ import {
51
+ getPlatformUrl,
52
+ hatchAssistant,
53
+ readPlatformToken,
54
+ } from "../lib/platform-client";
51
55
  import { httpHealthCheck } from "../lib/http-client";
52
56
  import { detectOrphanedProcesses } from "../lib/orphan-detection";
53
57
  import { isProcessAlive, stopProcess } from "../lib/process";
@@ -237,7 +241,7 @@ function parseArgs(): HatchArgs {
237
241
  console.log(" -d Run in detached mode");
238
242
  console.log(" --name <name> Custom instance name");
239
243
  console.log(
240
- " --remote <host> Remote host (local, gcp, aws, docker, custom)",
244
+ " --remote <host> Remote host (local, gcp, aws, docker, custom, vellum)",
241
245
  );
242
246
  console.log(
243
247
  " --restart Restart processes without onboarding side effects",
@@ -943,6 +947,48 @@ export async function hatch(): Promise<void> {
943
947
  return;
944
948
  }
945
949
 
950
+ if (remote === "vellum") {
951
+ await hatchVellumPlatform();
952
+ return;
953
+ }
954
+
946
955
  console.error(`Error: Remote host '${remote}' is not yet supported.`);
947
956
  process.exit(1);
948
957
  }
958
+
959
+ async function hatchVellumPlatform(): Promise<void> {
960
+ const token = readPlatformToken();
961
+ if (!token) {
962
+ console.error("Not logged in. Run `vellum login --token <token>` first.");
963
+ process.exit(1);
964
+ }
965
+
966
+ const config = SPECIES_CONFIG.vellum;
967
+ console.log("");
968
+ for (const line of config.art) {
969
+ console.log(` ${line}`);
970
+ }
971
+ console.log("");
972
+ console.log(" Hatching assistant on Vellum platform...");
973
+ console.log("");
974
+
975
+ const result = await hatchAssistant(token);
976
+
977
+ const platformUrl = getPlatformUrl();
978
+
979
+ saveAssistantEntry({
980
+ assistantId: result.id,
981
+ runtimeUrl: platformUrl,
982
+ cloud: "vellum",
983
+ species: "vellum",
984
+ hatchedAt: new Date().toISOString(),
985
+ });
986
+ setActiveAssistant(result.id);
987
+
988
+ console.log(` ${config.hatchedEmoji} Your assistant has hatched!`);
989
+ console.log("");
990
+ console.log(` ID: ${result.id}`);
991
+ console.log(` Name: ${result.name}`);
992
+ console.log(` Status: ${result.status}`);
993
+ console.log("");
994
+ }
@@ -179,7 +179,7 @@ async function restorePlatform(
179
179
 
180
180
  let orgId: string;
181
181
  try {
182
- orgId = await fetchOrganizationId(token);
182
+ orgId = await fetchOrganizationId(token, entry.runtimeUrl);
183
183
  } catch (err) {
184
184
  const msg = err instanceof Error ? err.message : String(err);
185
185
  if (msg.includes("401") || msg.includes("403")) {
@@ -206,6 +206,7 @@ async function restorePlatform(
206
206
  new Uint8Array(bundleData),
207
207
  token,
208
208
  orgId,
209
+ entry.runtimeUrl,
209
210
  );
210
211
  } catch (err) {
211
212
  if (err instanceof Error && err.name === "TimeoutError") {
@@ -315,7 +316,12 @@ async function restorePlatform(
315
316
  );
316
317
 
317
318
  try {
318
- await rollbackPlatformAssistant(token, orgId, opts.version);
319
+ await rollbackPlatformAssistant(
320
+ token,
321
+ orgId,
322
+ opts.version,
323
+ entry.runtimeUrl,
324
+ );
319
325
  } catch (err) {
320
326
  const msg = err instanceof Error ? err.message : String(err);
321
327
  if (msg.includes("401") || msg.includes("403")) {
@@ -340,6 +346,7 @@ async function restorePlatform(
340
346
  new Uint8Array(bundleData),
341
347
  token,
342
348
  orgId,
349
+ entry.runtimeUrl,
343
350
  );
344
351
  } catch (err) {
345
352
  if (err instanceof Error && err.name === "TimeoutError") {
@@ -197,7 +197,10 @@ async function retireCustom(entry: AssistantEntry): Promise<void> {
197
197
  console.log(`\u2705 Custom instance retired.`);
198
198
  }
199
199
 
200
- async function retireVellum(assistantId: string): Promise<void> {
200
+ async function retireVellum(
201
+ assistantId: string,
202
+ runtimeUrl?: string,
203
+ ): Promise<void> {
201
204
  console.log("\u{1F5D1}\ufe0f Retiring platform-hosted instance...\n");
202
205
 
203
206
  const token = readPlatformToken();
@@ -208,9 +211,10 @@ async function retireVellum(assistantId: string): Promise<void> {
208
211
  process.exit(1);
209
212
  }
210
213
 
211
- const orgId = await fetchOrganizationId(token);
214
+ const orgId = await fetchOrganizationId(token, runtimeUrl);
212
215
 
213
- const url = `${getPlatformUrl()}/v1/assistants/${encodeURIComponent(assistantId)}/retire/`;
216
+ const platformUrl = runtimeUrl || getPlatformUrl();
217
+ const url = `${platformUrl}/v1/assistants/${encodeURIComponent(assistantId)}/retire/`;
214
218
  const response = await fetch(url, {
215
219
  method: "DELETE",
216
220
  headers: {
@@ -343,7 +347,7 @@ async function retireInner(): Promise<void> {
343
347
  } else if (cloud === "custom") {
344
348
  await retireCustom(entry);
345
349
  } else if (cloud === "vellum") {
346
- await retireVellum(entry.assistantId);
350
+ await retireVellum(entry.assistantId, entry.runtimeUrl);
347
351
  } else {
348
352
  console.error(`Error: Unknown cloud type '${cloud}'.`);
349
353
  process.exit(1);
@@ -185,7 +185,7 @@ async function rollbackPlatformViaEndpoint(
185
185
 
186
186
  let orgId: string;
187
187
  try {
188
- orgId = await fetchOrganizationId(token);
188
+ orgId = await fetchOrganizationId(token, entry.runtimeUrl);
189
189
  } catch (err) {
190
190
  const msg = err instanceof Error ? err.message : String(err);
191
191
  if (msg.includes("401") || msg.includes("403")) {
@@ -206,7 +206,12 @@ async function rollbackPlatformViaEndpoint(
206
206
 
207
207
  let result: { detail: string; version: string | null };
208
208
  try {
209
- result = await rollbackPlatformAssistant(token, orgId, version);
209
+ result = await rollbackPlatformAssistant(
210
+ token,
211
+ orgId,
212
+ version,
213
+ entry.runtimeUrl,
214
+ );
210
215
  } catch (err) {
211
216
  const detail = err instanceof Error ? err.message : String(err);
212
217
 
@@ -729,9 +729,9 @@ async function upgradePlatform(
729
729
  process.exit(1);
730
730
  }
731
731
 
732
- const orgId = await fetchOrganizationId(token);
732
+ const orgId = await fetchOrganizationId(token, entry.runtimeUrl);
733
733
 
734
- const url = `${getPlatformUrl()}/v1/assistants/upgrade/`;
734
+ const url = `${entry.runtimeUrl || getPlatformUrl()}/v1/assistants/upgrade/`;
735
735
  const body: { assistant_id?: string; version?: string } = {
736
736
  assistant_id: entry.assistantId,
737
737
  };
@@ -16,6 +16,7 @@ import {
16
16
  DEFAULT_DAEMON_PORT,
17
17
  DEFAULT_GATEWAY_PORT,
18
18
  DEFAULT_QDRANT_PORT,
19
+ LOCKFILE_NAMES,
19
20
  } from "./constants.js";
20
21
  import { probePort } from "./port-probe.js";
21
22
 
@@ -119,10 +120,7 @@ function getLockfileDir(): string {
119
120
 
120
121
  function readLockfile(): LockfileData {
121
122
  const base = getLockfileDir();
122
- const candidates = [
123
- join(base, ".vellum.lock.json"),
124
- join(base, ".vellum.lockfile.json"),
125
- ];
123
+ const candidates = LOCKFILE_NAMES.map((name) => join(base, name));
126
124
  for (const lockfilePath of candidates) {
127
125
  if (!existsSync(lockfilePath)) continue;
128
126
  try {
@@ -139,7 +137,7 @@ function readLockfile(): LockfileData {
139
137
  }
140
138
 
141
139
  function writeLockfile(data: LockfileData): void {
142
- const lockfilePath = join(getLockfileDir(), ".vellum.lock.json");
140
+ const lockfilePath = join(getLockfileDir(), LOCKFILE_NAMES[0]);
143
141
  const tmpPath = `${lockfilePath}.${randomBytes(4).toString("hex")}.tmp`;
144
142
  try {
145
143
  writeFileSync(tmpPath, JSON.stringify(data, null, 2) + "\n");
@@ -18,6 +18,16 @@ export const DEFAULT_GATEWAY_PORT = 7830;
18
18
  export const DEFAULT_QDRANT_PORT = 6333;
19
19
  export const DEFAULT_CES_PORT = 8090;
20
20
 
21
+ /**
22
+ * Lockfile candidate filenames, checked in priority order.
23
+ * `.vellum.lock.json` is the current name; `.vellum.lockfile.json` is the
24
+ * legacy name kept for backwards compatibility with older installs.
25
+ */
26
+ export const LOCKFILE_NAMES = [
27
+ ".vellum.lock.json",
28
+ ".vellum.lockfile.json",
29
+ ] as const;
30
+
21
31
  /**
22
32
  * Environment variable names for provider API keys, keyed by provider ID.
23
33
  * Loaded from the shared registry at `meta/provider-env-vars.json` — the
@@ -33,6 +43,7 @@ export const VALID_REMOTE_HOSTS = [
33
43
  "aws",
34
44
  "docker",
35
45
  "custom",
46
+ "vellum",
36
47
  ] as const;
37
48
  export type RemoteHost = (typeof VALID_REMOTE_HOSTS)[number];
38
49
  export const VALID_SPECIES = ["openclaw", "vellum"] as const;
@@ -28,7 +28,7 @@ export async function checkManagedHealth(
28
28
  let orgId: string;
29
29
  try {
30
30
  const { fetchOrganizationId } = await import("./platform-client.js");
31
- orgId = await fetchOrganizationId(token);
31
+ orgId = await fetchOrganizationId(token, runtimeUrl);
32
32
  } catch (err) {
33
33
  return {
34
34
  status: "error (auth)",
@@ -65,6 +65,67 @@ export function clearPlatformToken(): void {
65
65
  }
66
66
  }
67
67
 
68
+ const VAK_PREFIX = "vak_";
69
+
70
+ /**
71
+ * Returns the appropriate auth header for the given platform token.
72
+ *
73
+ * - `vak_`-prefixed tokens are long-lived platform API keys and use
74
+ * `Authorization: Bearer`.
75
+ * - All other tokens are allauth session tokens and use `X-Session-Token`.
76
+ */
77
+ export function authHeaders(token: string): Record<string, string> {
78
+ if (token.startsWith(VAK_PREFIX)) {
79
+ return { Authorization: `Bearer ${token}` };
80
+ }
81
+ return { "X-Session-Token": token };
82
+ }
83
+
84
+ export interface HatchedAssistant {
85
+ id: string;
86
+ name: string;
87
+ status: string;
88
+ }
89
+
90
+ export async function hatchAssistant(token: string): Promise<HatchedAssistant> {
91
+ const url = `${getPlatformUrl()}/v1/assistants/hatch/`;
92
+
93
+ const response = await fetch(url, {
94
+ method: "POST",
95
+ headers: {
96
+ "Content-Type": "application/json",
97
+ ...authHeaders(token),
98
+ },
99
+ body: JSON.stringify({}),
100
+ });
101
+
102
+ if (response.ok) {
103
+ return (await response.json()) as HatchedAssistant;
104
+ }
105
+
106
+ if (response.status === 401 || response.status === 403) {
107
+ const detail = (await response.json().catch(() => ({}))) as {
108
+ detail?: string;
109
+ };
110
+ throw new Error(
111
+ detail.detail ??
112
+ "Invalid or expired token. Run `vellum login` to re-authenticate.",
113
+ );
114
+ }
115
+
116
+ if (response.status === 402) {
117
+ throw new Error("Insufficient balance to hatch a new assistant.");
118
+ }
119
+
120
+ const errorBody = (await response.json().catch(() => ({}))) as {
121
+ detail?: string;
122
+ };
123
+ throw new Error(
124
+ errorBody.detail ??
125
+ `Platform API error: ${response.status} ${response.statusText}`,
126
+ );
127
+ }
128
+
68
129
  export interface PlatformUser {
69
130
  id: string;
70
131
  email: string;
@@ -75,16 +136,19 @@ interface OrganizationListResponse {
75
136
  results: { id: string; name: string }[];
76
137
  }
77
138
 
78
- export async function fetchOrganizationId(token: string): Promise<string> {
79
- const platformUrl = getPlatformUrl();
80
- const url = `${platformUrl}/v1/organizations/`;
139
+ export async function fetchOrganizationId(
140
+ token: string,
141
+ platformUrl?: string,
142
+ ): Promise<string> {
143
+ const resolvedUrl = platformUrl || getPlatformUrl();
144
+ const url = `${resolvedUrl}/v1/organizations/`;
81
145
  const response = await fetch(url, {
82
146
  headers: { "X-Session-Token": token },
83
147
  });
84
148
 
85
149
  if (!response.ok) {
86
150
  throw new Error(
87
- `Failed to fetch organizations from ${platformUrl} (${response.status}). Try logging in again.`,
151
+ `Failed to fetch organizations from ${resolvedUrl} (${response.status}). Try logging in again.`,
88
152
  );
89
153
  }
90
154
 
@@ -107,8 +171,12 @@ interface AllauthSessionResponse {
107
171
  };
108
172
  }
109
173
 
110
- export async function fetchCurrentUser(token: string): Promise<PlatformUser> {
111
- const url = `${getPlatformUrl()}/_allauth/app/v1/auth/session`;
174
+ export async function fetchCurrentUser(
175
+ token: string,
176
+ platformUrl?: string,
177
+ ): Promise<PlatformUser> {
178
+ const resolvedUrl = platformUrl || getPlatformUrl();
179
+ const url = `${resolvedUrl}/_allauth/app/v1/auth/session`;
112
180
  const response = await fetch(url, {
113
181
  headers: { "X-Session-Token": token },
114
182
  });
@@ -138,9 +206,10 @@ export async function rollbackPlatformAssistant(
138
206
  token: string,
139
207
  orgId: string,
140
208
  version?: string,
209
+ platformUrl?: string,
141
210
  ): Promise<{ detail: string; version: string | null }> {
142
- const platformUrl = getPlatformUrl();
143
- const response = await fetch(`${platformUrl}/v1/assistants/rollback/`, {
211
+ const resolvedUrl = platformUrl || getPlatformUrl();
212
+ const response = await fetch(`${resolvedUrl}/v1/assistants/rollback/`, {
144
213
  method: "POST",
145
214
  headers: {
146
215
  "Content-Type": "application/json",
@@ -182,9 +251,10 @@ export async function platformInitiateExport(
182
251
  token: string,
183
252
  orgId: string,
184
253
  description?: string,
254
+ platformUrl?: string,
185
255
  ): Promise<{ jobId: string; status: string }> {
186
- const platformUrl = getPlatformUrl();
187
- const response = await fetch(`${platformUrl}/v1/migrations/export/`, {
256
+ const resolvedUrl = platformUrl || getPlatformUrl();
257
+ const response = await fetch(`${resolvedUrl}/v1/migrations/export/`, {
188
258
  method: "POST",
189
259
  headers: {
190
260
  "Content-Type": "application/json",
@@ -215,10 +285,11 @@ export async function platformPollExportStatus(
215
285
  jobId: string,
216
286
  token: string,
217
287
  orgId: string,
288
+ platformUrl?: string,
218
289
  ): Promise<{ status: string; downloadUrl?: string; error?: string }> {
219
- const platformUrl = getPlatformUrl();
290
+ const resolvedUrl = platformUrl || getPlatformUrl();
220
291
  const response = await fetch(
221
- `${platformUrl}/v1/migrations/export/${jobId}/status/`,
292
+ `${resolvedUrl}/v1/migrations/export/${jobId}/status/`,
222
293
  {
223
294
  headers: {
224
295
  "X-Session-Token": token,
@@ -269,10 +340,11 @@ export async function platformImportPreflight(
269
340
  bundleData: Uint8Array<ArrayBuffer>,
270
341
  token: string,
271
342
  orgId: string,
343
+ platformUrl?: string,
272
344
  ): Promise<{ statusCode: number; body: Record<string, unknown> }> {
273
- const platformUrl = getPlatformUrl();
345
+ const resolvedUrl = platformUrl || getPlatformUrl();
274
346
  const response = await fetch(
275
- `${platformUrl}/v1/migrations/import-preflight/`,
347
+ `${resolvedUrl}/v1/migrations/import-preflight/`,
276
348
  {
277
349
  method: "POST",
278
350
  headers: {
@@ -296,9 +368,10 @@ export async function platformImportBundle(
296
368
  bundleData: Uint8Array<ArrayBuffer>,
297
369
  token: string,
298
370
  orgId: string,
371
+ platformUrl?: string,
299
372
  ): Promise<{ statusCode: number; body: Record<string, unknown> }> {
300
- const platformUrl = getPlatformUrl();
301
- const response = await fetch(`${platformUrl}/v1/migrations/import/`, {
373
+ const resolvedUrl = platformUrl || getPlatformUrl();
374
+ const response = await fetch(`${resolvedUrl}/v1/migrations/import/`, {
302
375
  method: "POST",
303
376
  headers: {
304
377
  "Content-Type": "application/octet-stream",