@vellumai/cli 0.5.3 → 0.5.5

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.
@@ -1,9 +1,12 @@
1
+ import { randomBytes } from "crypto";
2
+
1
3
  import cliPkg from "../../package.json";
2
4
 
3
5
  import {
4
6
  findAssistantByName,
5
7
  getActiveAssistant,
6
8
  loadAllAssistants,
9
+ saveAssistantEntry,
7
10
  } from "../lib/assistant-config";
8
11
  import type { AssistantEntry } from "../lib/assistant-config";
9
12
  import {
@@ -12,6 +15,8 @@ import {
12
15
  DOCKER_READY_TIMEOUT_MS,
13
16
  GATEWAY_INTERNAL_PORT,
14
17
  dockerResourceNames,
18
+ migrateCesSecurityFiles,
19
+ migrateGatewaySecurityFiles,
15
20
  startContainers,
16
21
  stopContainers,
17
22
  } from "../lib/docker";
@@ -257,10 +262,18 @@ async function upgradeDocker(
257
262
  // use default
258
263
  }
259
264
 
265
+ // Extract CES_SERVICE_TOKEN from the captured env so it can be passed via
266
+ // the dedicated cesServiceToken parameter (which propagates it to all three
267
+ // containers). If the old instance predates CES_SERVICE_TOKEN, generate a
268
+ // fresh one so gateway and CES can authenticate.
269
+ const cesServiceToken =
270
+ capturedEnv["CES_SERVICE_TOKEN"] || randomBytes(32).toString("hex");
271
+
260
272
  // Build the set of extra env vars to replay on the new assistant container.
261
273
  // Captured env vars serve as the base; keys already managed by
262
274
  // serviceDockerRunArgs are excluded to avoid duplicates.
263
275
  const envKeysSetByRunArgs = new Set([
276
+ "CES_SERVICE_TOKEN",
264
277
  "VELLUM_ASSISTANT_NAME",
265
278
  "RUNTIME_HTTP_HOST",
266
279
  "PATH",
@@ -278,9 +291,16 @@ async function upgradeDocker(
278
291
  }
279
292
  }
280
293
 
294
+ console.log("šŸ”„ Migrating security files to gateway volume...");
295
+ await migrateGatewaySecurityFiles(res, (msg) => console.log(msg));
296
+
297
+ console.log("šŸ”„ Migrating credential files to CES security volume...");
298
+ await migrateCesSecurityFiles(res, (msg) => console.log(msg));
299
+
281
300
  console.log("šŸš€ Starting upgraded containers...");
282
301
  await startContainers(
283
302
  {
303
+ cesServiceToken,
284
304
  extraAssistantEnv,
285
305
  gatewayPort,
286
306
  imageTags,
@@ -294,6 +314,23 @@ async function upgradeDocker(
294
314
  console.log("Waiting for assistant to become ready...");
295
315
  const ready = await waitForReady(entry.runtimeUrl);
296
316
  if (ready) {
317
+ // Update lockfile with new service group topology
318
+ const newDigests = await captureImageRefs(res);
319
+ const updatedEntry: AssistantEntry = {
320
+ ...entry,
321
+ serviceGroupVersion: versionTag,
322
+ containerInfo: {
323
+ assistantImage: imageTags.assistant,
324
+ gatewayImage: imageTags.gateway,
325
+ cesImage: imageTags["credential-executor"],
326
+ assistantDigest: newDigests?.assistant,
327
+ gatewayDigest: newDigests?.gateway,
328
+ cesDigest: newDigests?.["credential-executor"],
329
+ networkName: res.network,
330
+ },
331
+ };
332
+ saveAssistantEntry(updatedEntry);
333
+
297
334
  console.log(
298
335
  `\nāœ… Docker assistant '${instanceName}' upgraded to ${versionTag}.`,
299
336
  );
@@ -307,6 +344,7 @@ async function upgradeDocker(
307
344
 
308
345
  await startContainers(
309
346
  {
347
+ cesServiceToken,
310
348
  extraAssistantEnv,
311
349
  gatewayPort,
312
350
  imageTags: previousImageRefs,
@@ -318,6 +356,19 @@ async function upgradeDocker(
318
356
 
319
357
  const rollbackReady = await waitForReady(entry.runtimeUrl);
320
358
  if (rollbackReady) {
359
+ // Restore previous container info in lockfile after rollback
360
+ if (previousImageRefs) {
361
+ const rolledBackEntry: AssistantEntry = {
362
+ ...entry,
363
+ containerInfo: {
364
+ assistantImage: previousImageRefs.assistant,
365
+ gatewayImage: previousImageRefs.gateway,
366
+ cesImage: previousImageRefs["credential-executor"],
367
+ networkName: res.network,
368
+ },
369
+ };
370
+ saveAssistantEntry(rolledBackEntry);
371
+ }
321
372
  console.log(
322
373
  `\nāš ļø Rolled back to previous version. Upgrade to ${versionTag} failed.`,
323
374
  );
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, writeFileSync } from "fs";
2
2
  import { join } from "path";
3
3
 
4
4
  import { resolveTargetAssistant } from "../lib/assistant-config.js";
5
+ import { dockerResourceNames, wakeContainers } from "../lib/docker.js";
5
6
  import { isProcessAlive, stopProcessByPidFile } from "../lib/process";
6
7
  import {
7
8
  isAssistantWatchModeAvailable,
@@ -38,9 +39,17 @@ export async function wake(): Promise<void> {
38
39
  const nameArg = args.find((a) => !a.startsWith("-"));
39
40
  const entry = resolveTargetAssistant(nameArg);
40
41
 
42
+ if (entry.cloud === "docker") {
43
+ const res = dockerResourceNames(entry.assistantId);
44
+ await wakeContainers(res);
45
+ console.log("Docker containers started.");
46
+ console.log("Wake complete.");
47
+ return;
48
+ }
49
+
41
50
  if (entry.cloud && entry.cloud !== "local") {
42
51
  console.error(
43
- `Error: 'vellum wake' only works with local assistants. '${entry.assistantId}' is a ${entry.cloud} instance.`,
52
+ `Error: 'vellum wake' only works with local and docker assistants. '${entry.assistantId}' is a ${entry.cloud} instance.`,
44
53
  );
45
54
  process.exit(1);
46
55
  }
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  import cliPkg from "../package.json";
4
+ import { backup } from "./commands/backup";
4
5
  import { clean } from "./commands/clean";
5
6
  import { client } from "./commands/client";
6
7
  import { hatch } from "./commands/hatch";
@@ -8,6 +9,7 @@ import { login, logout, whoami } from "./commands/login";
8
9
  import { pair } from "./commands/pair";
9
10
  import { ps } from "./commands/ps";
10
11
  import { recover } from "./commands/recover";
12
+ import { restore } from "./commands/restore";
11
13
  import { retire } from "./commands/retire";
12
14
  import { setup } from "./commands/setup";
13
15
  import { sleep } from "./commands/sleep";
@@ -26,6 +28,7 @@ import { loadGuardianToken } from "./lib/guardian-token";
26
28
  import { checkHealth } from "./lib/health-check";
27
29
 
28
30
  const commands = {
31
+ backup,
29
32
  clean,
30
33
  client,
31
34
  hatch,
@@ -34,6 +37,7 @@ const commands = {
34
37
  pair,
35
38
  ps,
36
39
  recover,
40
+ restore,
37
41
  retire,
38
42
  setup,
39
43
  sleep,
@@ -51,6 +55,7 @@ function printHelp(): void {
51
55
  console.log("Usage: vellum <command> [options]");
52
56
  console.log("");
53
57
  console.log("Commands:");
58
+ console.log(" backup Export a backup of a running assistant");
54
59
  console.log(" clean Kill orphaned vellum processes");
55
60
  console.log(" client Connect to a hatched assistant");
56
61
  console.log(" hatch Create a new assistant instance");
@@ -61,6 +66,7 @@ function printHelp(): void {
61
66
  " ps List assistants (or processes for a specific assistant)",
62
67
  );
63
68
  console.log(" recover Restore a previously retired local assistant");
69
+ console.log(" restore Restore a .vbundle backup into a running assistant");
64
70
  console.log(" retire Delete an assistant instance");
65
71
  console.log(" setup Configure API keys interactively");
66
72
  console.log(" sleep Stop the assistant process");
@@ -4,6 +4,7 @@ import { join } from "path";
4
4
 
5
5
  import {
6
6
  DAEMON_INTERNAL_ASSISTANT_ID,
7
+ DEFAULT_CES_PORT,
7
8
  DEFAULT_DAEMON_PORT,
8
9
  DEFAULT_GATEWAY_PORT,
9
10
  DEFAULT_QDRANT_PORT,
@@ -29,11 +30,28 @@ export interface LocalInstanceResources {
29
30
  gatewayPort: number;
30
31
  /** HTTP port for the Qdrant vector store */
31
32
  qdrantPort: number;
33
+ /** HTTP port for the CES (Claude Extension Server) */
34
+ cesPort: number;
32
35
  /** Absolute path to the daemon PID file */
33
36
  pidFile: string;
34
37
  [key: string]: unknown;
35
38
  }
36
39
 
40
+ /** Docker image metadata for the service group. Enables rollback to known-good digests. */
41
+ export interface ContainerInfo {
42
+ assistantImage: string;
43
+ gatewayImage: string;
44
+ cesImage: string;
45
+ /** sha256 digest of the assistant image at time of hatch/upgrade */
46
+ assistantDigest?: string;
47
+ /** sha256 digest of the gateway image at time of hatch/upgrade */
48
+ gatewayDigest?: string;
49
+ /** sha256 digest of the CES image at time of hatch/upgrade */
50
+ cesDigest?: string;
51
+ /** Docker network name for the service group */
52
+ networkName?: string;
53
+ }
54
+
37
55
  export interface AssistantEntry {
38
56
  assistantId: string;
39
57
  runtimeUrl: string;
@@ -56,6 +74,10 @@ export interface AssistantEntry {
56
74
  resources?: LocalInstanceResources;
57
75
  /** PID of the file watcher process for docker instances hatched with --watch. */
58
76
  watcherPid?: number;
77
+ /** Last-known version of the service group, populated at hatch and updated by health checks. */
78
+ serviceGroupVersion?: string;
79
+ /** Docker image metadata for rollback. Only present for docker topology entries. */
80
+ containerInfo?: ContainerInfo;
59
81
  [key: string]: unknown;
60
82
  }
61
83
 
@@ -166,6 +188,7 @@ export function migrateLegacyEntry(raw: Record<string, unknown>): boolean {
166
188
  daemonPort: DEFAULT_DAEMON_PORT,
167
189
  gatewayPort,
168
190
  qdrantPort: DEFAULT_QDRANT_PORT,
191
+ cesPort: DEFAULT_CES_PORT,
169
192
  pidFile: join(instanceDir, ".vellum", "vellum.pid"),
170
193
  };
171
194
  mutated = true;
@@ -198,6 +221,10 @@ export function migrateLegacyEntry(raw: Record<string, unknown>): boolean {
198
221
  res.qdrantPort = DEFAULT_QDRANT_PORT;
199
222
  mutated = true;
200
223
  }
224
+ if (typeof res.cesPort !== "number") {
225
+ res.cesPort = DEFAULT_CES_PORT;
226
+ mutated = true;
227
+ }
201
228
  if (typeof res.pidFile !== "string") {
202
229
  res.pidFile = join(res.instanceDir as string, ".vellum", "vellum.pid");
203
230
  mutated = true;
@@ -373,6 +400,7 @@ export async function allocateLocalResources(
373
400
  daemonPort: DEFAULT_DAEMON_PORT,
374
401
  gatewayPort: DEFAULT_GATEWAY_PORT,
375
402
  qdrantPort: DEFAULT_QDRANT_PORT,
403
+ cesPort: DEFAULT_CES_PORT,
376
404
  pidFile: join(vellumDir, "vellum.pid"),
377
405
  };
378
406
  }
@@ -423,6 +451,7 @@ export async function allocateLocalResources(
423
451
  daemonPort,
424
452
  gatewayPort,
425
453
  qdrantPort,
454
+ cesPort: DEFAULT_CES_PORT,
426
455
  pidFile: join(instanceDir, ".vellum", "vellum.pid"),
427
456
  };
428
457
  }
package/src/lib/aws.ts CHANGED
@@ -6,7 +6,7 @@ import { buildStartupScript, watchHatching } from "../commands/hatch";
6
6
  import type { PollResult } from "../commands/hatch";
7
7
  import { saveAssistantEntry, setActiveAssistant } from "./assistant-config";
8
8
  import type { AssistantEntry } from "./assistant-config";
9
- import { GATEWAY_PORT } from "./constants";
9
+ import { GATEWAY_PORT, PROVIDER_ENV_VAR_NAMES } from "./constants";
10
10
  import type { Species } from "./constants";
11
11
  import { leaseGuardianToken } from "./guardian-token";
12
12
  import { generateInstanceName } from "./random-name";
@@ -410,10 +410,18 @@ export async function hatchAws(
410
410
 
411
411
  const sshUser = userInfo().username;
412
412
  const hatchedBy = process.env.VELLUM_HATCHED_BY;
413
- const anthropicApiKey = process.env.ANTHROPIC_API_KEY;
414
- if (!anthropicApiKey) {
413
+ const providerApiKeys: Record<string, string> = {};
414
+ for (const [, envVar] of Object.entries(PROVIDER_ENV_VAR_NAMES)) {
415
+ const value = process.env[envVar];
416
+ if (value) {
417
+ providerApiKeys[envVar] = value;
418
+ }
419
+ }
420
+ if (Object.keys(providerApiKeys).length === 0) {
415
421
  console.error(
416
- "Error: ANTHROPIC_API_KEY environment variable is not set.",
422
+ "Error: No provider API key environment variable is set. " +
423
+ "Set at least one of: " +
424
+ Object.values(PROVIDER_ENV_VAR_NAMES).join(", "),
417
425
  );
418
426
  process.exit(1);
419
427
  }
@@ -437,7 +445,7 @@ export async function hatchAws(
437
445
  const startupScript = await buildStartupScript(
438
446
  species,
439
447
  sshUser,
440
- anthropicApiKey,
448
+ providerApiKeys,
441
449
  instanceName,
442
450
  "aws",
443
451
  );
@@ -1,3 +1,5 @@
1
+ import providerEnvVarsRegistry from "../../../meta/provider-env-vars.json";
2
+
1
3
  /**
2
4
  * Canonical internal assistant ID used as the default/fallback across the CLI
3
5
  * and daemon. Mirrors `DAEMON_INTERNAL_ASSISTANT_ID` from
@@ -14,6 +16,16 @@ export const GATEWAY_PORT = process.env.GATEWAY_PORT
14
16
  export const DEFAULT_DAEMON_PORT = 7821;
15
17
  export const DEFAULT_GATEWAY_PORT = 7830;
16
18
  export const DEFAULT_QDRANT_PORT = 6333;
19
+ export const DEFAULT_CES_PORT = 8090;
20
+
21
+ /**
22
+ * Environment variable names for provider API keys, keyed by provider ID.
23
+ * Loaded from the shared registry at `meta/provider-env-vars.json` — the
24
+ * single source of truth also consumed by the assistant runtime and the
25
+ * macOS client.
26
+ */
27
+ export const PROVIDER_ENV_VAR_NAMES: Record<string, string> =
28
+ providerEnvVarsRegistry.providers;
17
29
 
18
30
  export const VALID_REMOTE_HOSTS = [
19
31
  "local",