@vellumai/cli 0.5.1 → 0.5.2

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/knip.json CHANGED
@@ -1,4 +1,8 @@
1
1
  {
2
- "entry": ["src/**/*.test.ts", "src/**/__tests__/**/*.ts", "src/adapters/openclaw-http-server.ts"],
2
+ "entry": [
3
+ "src/**/*.test.ts",
4
+ "src/**/__tests__/**/*.ts",
5
+ "src/adapters/openclaw-http-server.ts"
6
+ ],
3
7
  "project": ["src/**/*.ts", "src/**/*.tsx"]
4
8
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/cli",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "CLI tools for vellum-assistant",
5
5
  "type": "module",
6
6
  "exports": {
@@ -5,7 +5,11 @@ import {
5
5
  getActiveAssistant,
6
6
  loadLatestAssistant,
7
7
  } from "../lib/assistant-config";
8
- import { DAEMON_INTERNAL_ASSISTANT_ID, GATEWAY_PORT, type Species } from "../lib/constants";
8
+ import {
9
+ DAEMON_INTERNAL_ASSISTANT_ID,
10
+ GATEWAY_PORT,
11
+ type Species,
12
+ } from "../lib/constants";
9
13
  import { loadGuardianToken } from "../lib/guardian-token";
10
14
  import { getLocalLanIPv4, getMacLocalHostname } from "../lib/local";
11
15
 
@@ -84,7 +88,8 @@ function parseArgs(): ParsedArgs {
84
88
 
85
89
  let runtimeUrl = entry?.localUrl || entry?.runtimeUrl || FALLBACK_RUNTIME_URL;
86
90
  let assistantId = entry?.assistantId || DAEMON_INTERNAL_ASSISTANT_ID;
87
- const bearerToken = loadGuardianToken(entry?.assistantId ?? "")?.accessToken ?? undefined;
91
+ const bearerToken =
92
+ loadGuardianToken(entry?.assistantId ?? "")?.accessToken ?? undefined;
88
93
  const species: Species = (entry?.species as Species) ?? "vellum";
89
94
 
90
95
  for (let i = 0; i < flagArgs.length; i++) {
@@ -50,6 +50,7 @@ import { detectOrphanedProcesses } from "../lib/orphan-detection";
50
50
  import { isProcessAlive, stopProcess } from "../lib/process";
51
51
  import { generateInstanceName } from "../lib/random-name";
52
52
  import { validateAssistantName } from "../lib/retire-archive";
53
+ import { leaseGuardianToken } from "../lib/guardian-token";
53
54
  import { archiveLogFile, resetLogFile } from "../lib/xdg-log";
54
55
 
55
56
  export type { PollResult, WatchHatchingResult } from "../lib/gcp";
@@ -717,6 +718,14 @@ async function hatchLocal(
717
718
  throw error;
718
719
  }
719
720
 
721
+ // Lease a guardian token so the desktop app can import it on first launch
722
+ // instead of hitting /v1/guardian/init itself.
723
+ try {
724
+ await leaseGuardianToken(runtimeUrl, instanceName);
725
+ } catch (err) {
726
+ console.error(`⚠️ Guardian token lease failed: ${err}`);
727
+ }
728
+
720
729
  // Auto-start ngrok if webhook integrations (e.g. Telegram, Twilio) are configured.
721
730
  // Set BASE_DATA_DIR so ngrok reads the correct instance config.
722
731
  const prevBaseDataDir = process.env.BASE_DATA_DIR;
@@ -7,6 +7,7 @@ import {
7
7
  } from "../lib/assistant-config";
8
8
  import type { AssistantEntry } from "../lib/assistant-config";
9
9
  import {
10
+ captureImageRefs,
10
11
  DOCKERHUB_IMAGES,
11
12
  DOCKER_READY_TIMEOUT_MS,
12
13
  GATEWAY_INTERNAL_PORT,
@@ -212,11 +213,21 @@ async function upgradeDocker(
212
213
  `🔄 Upgrading Docker assistant '${instanceName}' to ${versionTag}...\n`,
213
214
  );
214
215
 
215
- console.log("📦 Pulling new Docker images...");
216
- await exec("docker", ["pull", imageTags.assistant]);
217
- await exec("docker", ["pull", imageTags.gateway]);
218
- await exec("docker", ["pull", imageTags["credential-executor"]]);
219
- console.log(" Docker images pulled\n");
216
+ // Capture rollback state from existing containers BEFORE pulling new
217
+ // images or stopping anything. captureImageRefs uses the immutable
218
+ // image digest ({{.Image}}), but capturing first keeps the intent
219
+ // explicit and avoids relying on container-inspect ordering subtleties.
220
+ console.log("📸 Capturing current image references for rollback...");
221
+ const previousImageRefs = await captureImageRefs(res);
222
+ if (previousImageRefs) {
223
+ console.log(
224
+ ` Captured refs for ${Object.keys(previousImageRefs).length} service(s)\n`,
225
+ );
226
+ } else {
227
+ console.log(
228
+ " Could not capture all container refs (fresh install or partial deployment)\n",
229
+ );
230
+ }
220
231
 
221
232
  console.log("💾 Capturing existing container environment...");
222
233
  const capturedEnv = await captureContainerEnv(res.assistantContainer);
@@ -224,6 +235,12 @@ async function upgradeDocker(
224
235
  ` Captured ${Object.keys(capturedEnv).length} env var(s) from ${res.assistantContainer}\n`,
225
236
  );
226
237
 
238
+ console.log("📦 Pulling new Docker images...");
239
+ await exec("docker", ["pull", imageTags.assistant]);
240
+ await exec("docker", ["pull", imageTags.gateway]);
241
+ await exec("docker", ["pull", imageTags["credential-executor"]]);
242
+ console.log("✅ Docker images pulled\n");
243
+
227
244
  console.log("🛑 Stopping existing containers...");
228
245
  await stopContainers(res);
229
246
  console.log("✅ Containers stopped\n");
@@ -281,10 +298,54 @@ async function upgradeDocker(
281
298
  `\n✅ Docker assistant '${instanceName}' upgraded to ${versionTag}.`,
282
299
  );
283
300
  } else {
284
- console.log(
285
- `\n⚠️ Containers are running but the assistant did not become ready within the timeout.`,
286
- );
287
- console.log(` Check logs with: docker logs -f ${res.assistantContainer}`);
301
+ console.error(`\n❌ Containers failed to become ready within the timeout.`);
302
+
303
+ if (previousImageRefs) {
304
+ console.log(`\n🔄 Rolling back to previous images...`);
305
+ try {
306
+ await stopContainers(res);
307
+
308
+ await startContainers(
309
+ {
310
+ extraAssistantEnv,
311
+ gatewayPort,
312
+ imageTags: previousImageRefs,
313
+ instanceName,
314
+ res,
315
+ },
316
+ (msg) => console.log(msg),
317
+ );
318
+
319
+ const rollbackReady = await waitForReady(entry.runtimeUrl);
320
+ if (rollbackReady) {
321
+ console.log(
322
+ `\n⚠️ Rolled back to previous version. Upgrade to ${versionTag} failed.`,
323
+ );
324
+ } else {
325
+ console.error(
326
+ `\n❌ Rollback also failed. Manual intervention required.`,
327
+ );
328
+ console.log(
329
+ ` Check logs with: docker logs -f ${res.assistantContainer}`,
330
+ );
331
+ }
332
+ } catch (rollbackErr) {
333
+ console.error(
334
+ `\n❌ Rollback failed: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`,
335
+ );
336
+ console.error(` Manual intervention required.`);
337
+ console.log(
338
+ ` Check logs with: docker logs -f ${res.assistantContainer}`,
339
+ );
340
+ }
341
+ } else {
342
+ console.log(` No previous images available for rollback.`);
343
+ console.log(
344
+ ` Check logs with: docker logs -f ${res.assistantContainer}`,
345
+ );
346
+ }
347
+
348
+ process.exit(1);
288
349
  }
289
350
  }
290
351
 
package/src/lib/docker.ts CHANGED
@@ -569,6 +569,54 @@ export async function stopContainers(
569
569
  await removeContainer(res.assistantContainer);
570
570
  }
571
571
 
572
+ /**
573
+ * Capture the current image references for running service containers.
574
+ * Returns a complete record of service → immutable image ID (sha256 digest)
575
+ * for all three services. Uses `{{.Image}}` rather than `{{.Config.Image}}`
576
+ * so rollback targets the exact image that was running, even if the tag has
577
+ * since been retagged to a different image.
578
+ *
579
+ * Returns null if any container could not be inspected (e.g. fresh install
580
+ * or partial deployment).
581
+ */
582
+ export async function captureImageRefs(
583
+ res: ReturnType<typeof dockerResourceNames>,
584
+ ): Promise<Record<ServiceName, string> | null> {
585
+ const containerForService: Record<ServiceName, string> = {
586
+ assistant: res.assistantContainer,
587
+ "credential-executor": res.cesContainer,
588
+ gateway: res.gatewayContainer,
589
+ };
590
+
591
+ const refs: Partial<Record<ServiceName, string>> = {};
592
+
593
+ for (const [service, container] of Object.entries(containerForService)) {
594
+ try {
595
+ const imageRef = (
596
+ await execOutput("docker", [
597
+ "inspect",
598
+ "--format",
599
+ "{{.Image}}",
600
+ container,
601
+ ])
602
+ ).trim();
603
+ if (imageRef) {
604
+ refs[service as ServiceName] = imageRef;
605
+ }
606
+ } catch {
607
+ // Container doesn't exist or can't be inspected — skip
608
+ }
609
+ }
610
+
611
+ const allServices: ServiceName[] = [
612
+ "assistant",
613
+ "credential-executor",
614
+ "gateway",
615
+ ];
616
+ const hasAll = allServices.every((s) => s in refs);
617
+ return hasAll ? (refs as Record<ServiceName, string>) : null;
618
+ }
619
+
572
620
  /**
573
621
  * Determine which services are affected by a changed file path relative
574
622
  * to the repository root.
@@ -1,6 +1,12 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { execSync } from "node:child_process";
3
- import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
3
+ import {
4
+ chmodSync,
5
+ existsSync,
6
+ mkdirSync,
7
+ readFileSync,
8
+ writeFileSync,
9
+ } from "fs";
4
10
  import { homedir, platform } from "os";
5
11
  import { dirname, join } from "path";
6
12
 
package/src/lib/local.ts CHANGED
@@ -805,8 +805,8 @@ export function isAssistantWatchModeAvailable(): boolean {
805
805
  */
806
806
  export function isGatewayWatchModeAvailable(): boolean {
807
807
  try {
808
- resolveGatewayDir();
809
- return true;
808
+ const dir = resolveGatewayDir();
809
+ return existsSync(join(dir, "src", "index.ts"));
810
810
  } catch {
811
811
  return false;
812
812
  }