@vellumai/cli 0.5.5 → 0.5.7

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
@@ -2,7 +2,10 @@
2
2
  "entry": [
3
3
  "src/**/*.test.ts",
4
4
  "src/**/__tests__/**/*.ts",
5
- "src/adapters/openclaw-http-server.ts"
5
+ "src/adapters/openclaw-http-server.ts",
6
+ "src/lib/version-compat.ts",
7
+ "src/lib/platform-releases.ts",
8
+ "src/lib/cli-error.ts"
6
9
  ],
7
10
  "project": ["src/**/*.ts", "src/**/*.tsx"]
8
11
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/cli",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "description": "CLI tools for vellum-assistant",
5
5
  "type": "module",
6
6
  "exports": {
@@ -10,6 +10,7 @@ describe("checkHealth", () => {
10
10
  test("returns unreachable for non-existent host", async () => {
11
11
  const result = await checkHealth("http://127.0.0.1:1");
12
12
  expect(["unreachable", "timeout"]).toContain(result.status);
13
+ expect(result.version).toBeUndefined();
13
14
  });
14
15
 
15
16
  test("returns healthy for a mock healthy endpoint", async () => {
@@ -24,6 +25,24 @@ describe("checkHealth", () => {
24
25
  const result = await checkHealth(`http://localhost:${server.port}`);
25
26
  expect(result.status).toBe("healthy");
26
27
  expect(result.detail).toBeNull();
28
+ expect(result.version).toBeUndefined();
29
+ } finally {
30
+ server.stop(true);
31
+ }
32
+ });
33
+
34
+ test("returns version when present in response", async () => {
35
+ const server = Bun.serve({
36
+ port: 0,
37
+ fetch() {
38
+ return Response.json({ status: "healthy", version: "1.2.3" });
39
+ },
40
+ });
41
+
42
+ try {
43
+ const result = await checkHealth(`http://localhost:${server.port}`);
44
+ expect(result.status).toBe("healthy");
45
+ expect(result.version).toBe("1.2.3");
27
46
  } finally {
28
47
  server.stop(true);
29
48
  }
@@ -33,7 +52,11 @@ describe("checkHealth", () => {
33
52
  const server = Bun.serve({
34
53
  port: 0,
35
54
  fetch() {
36
- return Response.json({ status: "degraded", message: "high latency" });
55
+ return Response.json({
56
+ status: "degraded",
57
+ message: "high latency",
58
+ version: "0.9.0",
59
+ });
37
60
  },
38
61
  });
39
62
 
@@ -41,6 +64,7 @@ describe("checkHealth", () => {
41
64
  const result = await checkHealth(`http://localhost:${server.port}`);
42
65
  expect(result.status).toBe("degraded");
43
66
  expect(result.detail).toBe("high latency");
67
+ expect(result.version).toBe("0.9.0");
44
68
  } finally {
45
69
  server.stop(true);
46
70
  }
@@ -57,6 +81,7 @@ describe("checkHealth", () => {
57
81
  try {
58
82
  const result = await checkHealth(`http://localhost:${server.port}`);
59
83
  expect(result.status).toBe("error (500)");
84
+ expect(result.version).toBeUndefined();
60
85
  } finally {
61
86
  server.stop(true);
62
87
  }
@@ -1,22 +1,10 @@
1
1
  import { mkdirSync, writeFileSync } from "fs";
2
- import { homedir } from "os";
3
2
  import { dirname, join } from "path";
4
3
 
5
4
  import { findAssistantByName } from "../lib/assistant-config";
5
+ import { getBackupsDir, formatSize } from "../lib/backup-ops.js";
6
6
  import { loadGuardianToken, leaseGuardianToken } from "../lib/guardian-token";
7
7
 
8
- function getBackupsDir(): string {
9
- const dataHome =
10
- process.env.XDG_DATA_HOME?.trim() || join(homedir(), ".local", "share");
11
- return join(dataHome, "vellum", "backups");
12
- }
13
-
14
- function formatSize(bytes: number): string {
15
- if (bytes < 1024) return `${bytes} B`;
16
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
17
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
18
- }
19
-
20
8
  export async function backup(): Promise<void> {
21
9
  const args = process.argv.slice(3);
22
10
 
@@ -104,6 +92,33 @@ export async function backup(): Promise<void> {
104
92
  body: JSON.stringify({ description: "CLI backup" }),
105
93
  signal: AbortSignal.timeout(120_000),
106
94
  });
95
+
96
+ // Retry once with a fresh token on 401 — the cached token may be stale
97
+ // after a container restart that generated a new gateway signing key.
98
+ if (response.status === 401) {
99
+ let refreshedToken: string | null = null;
100
+ try {
101
+ const freshToken = await leaseGuardianToken(
102
+ entry.runtimeUrl,
103
+ entry.assistantId,
104
+ );
105
+ refreshedToken = freshToken.accessToken;
106
+ } catch {
107
+ // If token refresh fails, fall through to the !response.ok handler below
108
+ }
109
+ if (refreshedToken) {
110
+ accessToken = refreshedToken;
111
+ response = await fetch(`${entry.runtimeUrl}/v1/migrations/export`, {
112
+ method: "POST",
113
+ headers: {
114
+ Authorization: `Bearer ${accessToken}`,
115
+ "Content-Type": "application/json",
116
+ },
117
+ body: JSON.stringify({ description: "CLI backup" }),
118
+ signal: AbortSignal.timeout(120_000),
119
+ });
120
+ }
121
+ }
107
122
  } catch (err) {
108
123
  if (err instanceof Error && err.name === "TimeoutError") {
109
124
  console.error("Error: Export request timed out after 2 minutes.");
@@ -39,12 +39,14 @@ import type { RemoteHost, Species } from "../lib/constants";
39
39
  import { hatchDocker } from "../lib/docker";
40
40
  import { hatchGcp } from "../lib/gcp";
41
41
  import type { PollResult, WatchHatchingResult } from "../lib/gcp";
42
+ import { buildNestedConfig, writeInitialConfig } from "../lib/config-utils";
42
43
  import {
43
44
  startLocalDaemon,
44
45
  startGateway,
45
46
  stopLocalProcesses,
46
47
  } from "../lib/local";
47
48
  import { maybeStartNgrokTunnel } from "../lib/ngrok";
49
+ import { getPlatformUrl } from "../lib/platform-client";
48
50
  import { httpHealthCheck } from "../lib/http-client";
49
51
  import { detectOrphanedProcesses } from "../lib/orphan-detection";
50
52
  import { isProcessAlive, stopProcess } from "../lib/process";
@@ -99,8 +101,9 @@ export async function buildStartupScript(
99
101
  providerApiKeys: Record<string, string>,
100
102
  instanceName: string,
101
103
  cloud: RemoteHost,
104
+ configValues: Record<string, string> = {},
102
105
  ): Promise<string> {
103
- const platformUrl = process.env.VELLUM_PLATFORM_URL ?? "https://vellum.ai";
106
+ const platformUrl = getPlatformUrl();
104
107
  const logPath =
105
108
  cloud === "custom"
106
109
  ? "/tmp/vellum-startup.log"
@@ -130,6 +133,22 @@ export async function buildStartupScript(
130
133
  .map((envVar) => `${envVar}=\$${envVar}`)
131
134
  .join("\n");
132
135
 
136
+ // Write --config key=value pairs to a temp JSON file on the remote host
137
+ // and export the env var so the daemon reads it on first boot.
138
+ let configWriteBlock = "";
139
+ if (Object.keys(configValues).length > 0) {
140
+ const configJson = JSON.stringify(buildNestedConfig(configValues), null, 2);
141
+ configWriteBlock = `
142
+ echo "Writing default workspace config..."
143
+ VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH="/tmp/vellum-initial-config-$$.json"
144
+ cat > "\$VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH" << 'VELLUM_CONFIG_EOF'
145
+ ${configJson}
146
+ VELLUM_CONFIG_EOF
147
+ export VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH
148
+ echo "Default workspace config written to \$VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH"
149
+ `;
150
+ }
151
+
133
152
  return `#!/bin/bash
134
153
  set -e
135
154
 
@@ -146,9 +165,10 @@ RUNTIME_HTTP_PORT=7821
146
165
  DOTENV_EOF
147
166
 
148
167
  ${ownershipFixup}
149
-
168
+ ${configWriteBlock}
150
169
  export VELLUM_SSH_USER="\$SSH_USER"
151
170
  export VELLUM_ASSISTANT_NAME="\$VELLUM_ASSISTANT_NAME"
171
+ export VELLUM_CLOUD="${cloud}"
152
172
  echo "Downloading install script from ${platformUrl}/install.sh..."
153
173
  curl -fsSL ${platformUrl}/install.sh -o ${INSTALL_SCRIPT_REMOTE_PATH}
154
174
  echo "Install script downloaded (\$(wc -c < ${INSTALL_SCRIPT_REMOTE_PATH}) bytes)"
@@ -166,9 +186,9 @@ interface HatchArgs {
166
186
  keepAlive: boolean;
167
187
  name: string | null;
168
188
  remote: RemoteHost;
169
- daemonOnly: boolean;
170
189
  restart: boolean;
171
190
  watch: boolean;
191
+ configValues: Record<string, string>;
172
192
  }
173
193
 
174
194
  function parseArgs(): HatchArgs {
@@ -178,9 +198,9 @@ function parseArgs(): HatchArgs {
178
198
  let keepAlive = false;
179
199
  let name: string | null = null;
180
200
  let remote: RemoteHost = DEFAULT_REMOTE;
181
- let daemonOnly = false;
182
201
  let restart = false;
183
202
  let watch = false;
203
+ const configValues: Record<string, string> = {};
184
204
 
185
205
  for (let i = 0; i < args.length; i++) {
186
206
  const arg = args[i];
@@ -199,9 +219,6 @@ function parseArgs(): HatchArgs {
199
219
  console.log(
200
220
  " --remote <host> Remote host (local, gcp, aws, docker, custom)",
201
221
  );
202
- console.log(
203
- " --daemon-only Start assistant only, skip gateway",
204
- );
205
222
  console.log(
206
223
  " --restart Restart processes without onboarding side effects",
207
224
  );
@@ -211,11 +228,12 @@ function parseArgs(): HatchArgs {
211
228
  console.log(
212
229
  " --keep-alive Stay alive after hatch, exit when gateway stops",
213
230
  );
231
+ console.log(
232
+ " --config <key=value> Set a workspace config value (repeatable)",
233
+ );
214
234
  process.exit(0);
215
235
  } else if (arg === "-d") {
216
236
  detached = true;
217
- } else if (arg === "--daemon-only") {
218
- daemonOnly = true;
219
237
  } else if (arg === "--restart") {
220
238
  restart = true;
221
239
  } else if (arg === "--watch") {
@@ -248,11 +266,28 @@ function parseArgs(): HatchArgs {
248
266
  }
249
267
  remote = next as RemoteHost;
250
268
  i++;
269
+ } else if (arg === "--config") {
270
+ const next = args[i + 1];
271
+ if (!next || next.startsWith("-")) {
272
+ console.error("Error: --config requires a key=value argument");
273
+ process.exit(1);
274
+ }
275
+ const eqIndex = next.indexOf("=");
276
+ if (eqIndex <= 0) {
277
+ console.error(
278
+ `Error: --config value must be in key=value format, got '${next}'`,
279
+ );
280
+ process.exit(1);
281
+ }
282
+ const key = next.slice(0, eqIndex);
283
+ const value = next.slice(eqIndex + 1);
284
+ configValues[key] = value;
285
+ i++;
251
286
  } else if (VALID_SPECIES.includes(arg as Species)) {
252
287
  species = arg as Species;
253
288
  } else {
254
289
  console.error(
255
- `Error: Unknown argument '${arg}'. Valid options: ${VALID_SPECIES.join(", ")}, -d, --daemon-only, --restart, --watch, --keep-alive, --name <name>, --remote <${VALID_REMOTE_HOSTS.join("|")}>`,
290
+ `Error: Unknown argument '${arg}'. Valid options: ${VALID_SPECIES.join(", ")}, -d, --restart, --watch, --keep-alive, --name <name>, --remote <${VALID_REMOTE_HOSTS.join("|")}>, --config <key=value>`,
256
291
  );
257
292
  process.exit(1);
258
293
  }
@@ -264,9 +299,9 @@ function parseArgs(): HatchArgs {
264
299
  keepAlive,
265
300
  name,
266
301
  remote,
267
- daemonOnly,
268
302
  restart,
269
303
  watch,
304
+ configValues,
270
305
  };
271
306
  }
272
307
 
@@ -574,10 +609,10 @@ function installCLISymlink(): void {
574
609
  async function hatchLocal(
575
610
  species: Species,
576
611
  name: string | null,
577
- daemonOnly: boolean = false,
578
612
  restart: boolean = false,
579
613
  watch: boolean = false,
580
614
  keepAlive: boolean = false,
615
+ configValues: Record<string, string> = {},
581
616
  ): Promise<void> {
582
617
  if (restart && !name && !process.env.VELLUM_ASSISTANT_NAME) {
583
618
  console.error(
@@ -709,46 +744,44 @@ async function hatchLocal(
709
744
  process.env.APP_VERSION = cliPkg.version;
710
745
  }
711
746
 
712
- await startLocalDaemon(watch, resources);
747
+ const defaultWorkspaceConfigPath = writeInitialConfig(configValues);
748
+
749
+ await startLocalDaemon(watch, resources, { defaultWorkspaceConfigPath });
713
750
 
714
- // When daemonOnly is set, skip gateway and ngrok — the caller only wants
715
- // the daemon restarted (e.g. macOS app bootstrap retry).
716
751
  let runtimeUrl = `http://127.0.0.1:${resources.gatewayPort}`;
717
- if (!daemonOnly) {
718
- try {
719
- runtimeUrl = await startGateway(watch, resources);
720
- } catch (error) {
721
- // Gateway failed stop the daemon we just started so we don't leave
722
- // orphaned processes with no lock file entry.
723
- console.error(
724
- `\n❌ Gateway startup failed — stopping assistant to avoid orphaned processes.`,
725
- );
726
- await stopLocalProcesses(resources);
727
- throw error;
728
- }
752
+ try {
753
+ runtimeUrl = await startGateway(watch, resources);
754
+ } catch (error) {
755
+ // Gateway failed — stop the daemon we just started so we don't leave
756
+ // orphaned processes with no lock file entry.
757
+ console.error(
758
+ `\n❌ Gateway startup failed — stopping assistant to avoid orphaned processes.`,
759
+ );
760
+ await stopLocalProcesses(resources);
761
+ throw error;
762
+ }
729
763
 
730
- // Lease a guardian token so the desktop app can import it on first launch
731
- // instead of hitting /v1/guardian/init itself.
732
- try {
733
- await leaseGuardianToken(runtimeUrl, instanceName);
734
- } catch (err) {
735
- console.error(`⚠️ Guardian token lease failed: ${err}`);
736
- }
764
+ // Lease a guardian token so the desktop app can import it on first launch
765
+ // instead of hitting /v1/guardian/init itself.
766
+ try {
767
+ await leaseGuardianToken(runtimeUrl, instanceName);
768
+ } catch (err) {
769
+ console.error(`⚠️ Guardian token lease failed: ${err}`);
770
+ }
737
771
 
738
- // Auto-start ngrok if webhook integrations (e.g. Telegram, Twilio) are configured.
739
- // Set BASE_DATA_DIR so ngrok reads the correct instance config.
740
- const prevBaseDataDir = process.env.BASE_DATA_DIR;
741
- process.env.BASE_DATA_DIR = resources.instanceDir;
742
- const ngrokChild = await maybeStartNgrokTunnel(resources.gatewayPort);
743
- if (ngrokChild?.pid) {
744
- const ngrokPidFile = join(resources.instanceDir, ".vellum", "ngrok.pid");
745
- writeFileSync(ngrokPidFile, String(ngrokChild.pid));
746
- }
747
- if (prevBaseDataDir !== undefined) {
748
- process.env.BASE_DATA_DIR = prevBaseDataDir;
749
- } else {
750
- delete process.env.BASE_DATA_DIR;
751
- }
772
+ // Auto-start ngrok if webhook integrations (e.g. Telegram, Twilio) are configured.
773
+ // Set BASE_DATA_DIR so ngrok reads the correct instance config.
774
+ const prevBaseDataDir = process.env.BASE_DATA_DIR;
775
+ process.env.BASE_DATA_DIR = resources.instanceDir;
776
+ const ngrokChild = await maybeStartNgrokTunnel(resources.gatewayPort);
777
+ if (ngrokChild?.pid) {
778
+ const ngrokPidFile = join(resources.instanceDir, ".vellum", "ngrok.pid");
779
+ writeFileSync(ngrokPidFile, String(ngrokChild.pid));
780
+ }
781
+ if (prevBaseDataDir !== undefined) {
782
+ process.env.BASE_DATA_DIR = prevBaseDataDir;
783
+ } else {
784
+ delete process.env.BASE_DATA_DIR;
752
785
  }
753
786
 
754
787
  const localEntry: AssistantEntry = {
@@ -761,7 +794,7 @@ async function hatchLocal(
761
794
  serviceGroupVersion: cliPkg.version ? `v${cliPkg.version}` : undefined,
762
795
  resources,
763
796
  };
764
- if (!daemonOnly && !restart) {
797
+ if (!restart) {
765
798
  saveAssistantEntry(localEntry);
766
799
  setActiveAssistant(instanceName);
767
800
  syncConfigToLockfile();
@@ -780,12 +813,8 @@ async function hatchLocal(
780
813
  }
781
814
 
782
815
  if (keepAlive) {
783
- // When --daemon-only is set, no gateway is running — poll the daemon
784
- // health endpoint instead of the gateway to avoid self-termination.
785
- const healthUrl = daemonOnly
786
- ? `http://127.0.0.1:${resources.daemonPort}/healthz`
787
- : `http://127.0.0.1:${resources.gatewayPort}/healthz`;
788
- const healthTarget = daemonOnly ? "Assistant" : "Gateway";
816
+ const healthUrl = `http://127.0.0.1:${resources.gatewayPort}/healthz`;
817
+ const healthTarget = "Gateway";
789
818
  const POLL_INTERVAL_MS = 5000;
790
819
  const MAX_FAILURES = 3;
791
820
  let consecutiveFailures = 0;
@@ -839,9 +868,9 @@ export async function hatch(): Promise<void> {
839
868
  keepAlive,
840
869
  name,
841
870
  remote,
842
- daemonOnly,
843
871
  restart,
844
872
  watch,
873
+ configValues,
845
874
  } = parseArgs();
846
875
 
847
876
  if (restart && remote !== "local") {
@@ -859,22 +888,29 @@ export async function hatch(): Promise<void> {
859
888
  }
860
889
 
861
890
  if (remote === "local") {
862
- await hatchLocal(species, name, daemonOnly, restart, watch, keepAlive);
891
+ await hatchLocal(species, name, restart, watch, keepAlive, configValues);
863
892
  return;
864
893
  }
865
894
 
866
895
  if (remote === "gcp") {
867
- await hatchGcp(species, detached, name, buildStartupScript, watchHatching);
896
+ await hatchGcp(
897
+ species,
898
+ detached,
899
+ name,
900
+ buildStartupScript,
901
+ watchHatching,
902
+ configValues,
903
+ );
868
904
  return;
869
905
  }
870
906
 
871
907
  if (remote === "aws") {
872
- await hatchAws(species, detached, name);
908
+ await hatchAws(species, detached, name, configValues);
873
909
  return;
874
910
  }
875
911
 
876
912
  if (remote === "docker") {
877
- await hatchDocker(species, detached, name, watch);
913
+ await hatchDocker(species, detached, name, watch, configValues);
878
914
  return;
879
915
  }
880
916
 
@@ -4,6 +4,7 @@ import {
4
4
  findAssistantByName,
5
5
  getActiveAssistant,
6
6
  loadAllAssistants,
7
+ updateServiceGroupVersion,
7
8
  type AssistantEntry,
8
9
  } from "../lib/assistant-config";
9
10
  import { loadGuardianToken } from "../lib/guardian-token";
@@ -424,7 +425,7 @@ async function listAllAssistants(): Promise<void> {
424
425
  // hitting the health endpoint. If the PID file is missing or the
425
426
  // process isn't running, the assistant is sleeping — skip the
426
427
  // network health check to avoid a misleading "unreachable" status.
427
- let health: { status: string; detail: string | null };
428
+ let health: { status: string; detail: string | null; version?: string };
428
429
  const resources = a.resources;
429
430
  if (a.cloud === "local" && resources) {
430
431
  const pid = readPidFile(resources.pidFile);
@@ -451,6 +452,10 @@ async function listAllAssistants(): Promise<void> {
451
452
  health = await checkHealth(a.localUrl ?? a.runtimeUrl, token);
452
453
  }
453
454
 
455
+ if (health.status === "healthy" && health.version) {
456
+ updateServiceGroupVersion(a.assistantId, health.version);
457
+ }
458
+
454
459
  const infoParts = [a.runtimeUrl];
455
460
  if (a.cloud) infoParts.push(`cloud: ${a.cloud}`);
456
461
  if (a.species) infoParts.push(`species: ${a.species}`);
@@ -89,28 +89,39 @@ interface PreflightFileEntry {
89
89
  action: string;
90
90
  }
91
91
 
92
+ interface StructuredError {
93
+ code: string;
94
+ message: string;
95
+ path?: string;
96
+ }
97
+
92
98
  interface PreflightResponse {
93
99
  can_import: boolean;
94
- errors?: string[];
100
+ validation?: {
101
+ is_valid: false;
102
+ errors: StructuredError[];
103
+ };
95
104
  files?: PreflightFileEntry[];
96
105
  summary?: {
97
- create: number;
98
- overwrite: number;
99
- unchanged: number;
100
- total: number;
106
+ files_to_create: number;
107
+ files_to_overwrite: number;
108
+ files_unchanged: number;
109
+ total_files: number;
101
110
  };
102
- conflicts?: string[];
111
+ conflicts?: StructuredError[];
103
112
  }
104
113
 
105
114
  interface ImportResponse {
106
115
  success: boolean;
107
116
  reason?: string;
108
- errors?: string[];
117
+ errors?: StructuredError[];
118
+ message?: string;
109
119
  warnings?: string[];
110
120
  summary?: {
111
- created: number;
112
- overwritten: number;
113
- skipped: number;
121
+ total_files: number;
122
+ files_created: number;
123
+ files_overwritten: number;
124
+ files_skipped: number;
114
125
  backups_created: number;
115
126
  };
116
127
  }
@@ -201,30 +212,38 @@ export async function restore(): Promise<void> {
201
212
  const result = (await response.json()) as PreflightResponse;
202
213
 
203
214
  if (!result.can_import) {
204
- console.error("Import blocked by validation errors:");
205
- for (const err of result.errors ?? []) {
206
- console.error(` - ${err}`);
215
+ if (result.validation?.errors?.length) {
216
+ console.error("Import blocked by validation errors:");
217
+ for (const err of result.validation.errors) {
218
+ console.error(` - ${err.message}${err.path ? ` (${err.path})` : ""}`);
219
+ }
220
+ }
221
+ if (result.conflicts?.length) {
222
+ console.error("Import blocked by conflicts:");
223
+ for (const conflict of result.conflicts) {
224
+ console.error(` - ${conflict.message}${conflict.path ? ` (${conflict.path})` : ""}`);
225
+ }
207
226
  }
208
227
  process.exit(1);
209
228
  }
210
229
 
211
230
  // Print summary table
212
231
  const summary = result.summary ?? {
213
- create: 0,
214
- overwrite: 0,
215
- unchanged: 0,
216
- total: 0,
232
+ files_to_create: 0,
233
+ files_to_overwrite: 0,
234
+ files_unchanged: 0,
235
+ total_files: 0,
217
236
  };
218
237
  console.log("Preflight analysis:");
219
- console.log(` Files to create: ${summary.create}`);
220
- console.log(` Files to overwrite: ${summary.overwrite}`);
221
- console.log(` Files unchanged: ${summary.unchanged}`);
222
- console.log(` Total: ${summary.total}`);
238
+ console.log(` Files to create: ${summary.files_to_create}`);
239
+ console.log(` Files to overwrite: ${summary.files_to_overwrite}`);
240
+ console.log(` Files unchanged: ${summary.files_unchanged}`);
241
+ console.log(` Total: ${summary.total_files}`);
223
242
  console.log("");
224
243
 
225
244
  const conflicts = result.conflicts ?? [];
226
245
  console.log(
227
- `Conflicts: ${conflicts.length > 0 ? conflicts.join(", ") : "none"}`,
246
+ `Conflicts: ${conflicts.length > 0 ? conflicts.map((c) => c.message).join(", ") : "none"}`,
228
247
  );
229
248
 
230
249
  // List individual files with their action
@@ -276,25 +295,26 @@ export async function restore(): Promise<void> {
276
295
 
277
296
  if (!result.success) {
278
297
  console.error(
279
- `Error: Import failed — ${result.reason ?? "unknown reason"}`,
298
+ `Error: Import failed — ${result.message ?? result.reason ?? "unknown reason"}`,
280
299
  );
281
300
  for (const err of result.errors ?? []) {
282
- console.error(` - ${err}`);
301
+ console.error(` - ${err.message}${err.path ? ` (${err.path})` : ""}`);
283
302
  }
284
303
  process.exit(1);
285
304
  }
286
305
 
287
306
  // Print import report
288
307
  const summary = result.summary ?? {
289
- created: 0,
290
- overwritten: 0,
291
- skipped: 0,
308
+ total_files: 0,
309
+ files_created: 0,
310
+ files_overwritten: 0,
311
+ files_skipped: 0,
292
312
  backups_created: 0,
293
313
  };
294
314
  console.log("✅ Restore complete.");
295
- console.log(` Files created: ${summary.created}`);
296
- console.log(` Files overwritten: ${summary.overwritten}`);
297
- console.log(` Files skipped: ${summary.skipped}`);
315
+ console.log(` Files created: ${summary.files_created}`);
316
+ console.log(` Files overwritten: ${summary.files_overwritten}`);
317
+ console.log(` Files skipped: ${summary.files_skipped}`);
298
318
  console.log(` Backups created: ${summary.backups_created}`);
299
319
 
300
320
  // Print warnings if any
@@ -1,10 +1,10 @@
1
1
  import { spawn } from "child_process";
2
2
  import { existsSync, mkdirSync, renameSync, writeFileSync } from "fs";
3
- import { homedir } from "os";
4
3
  import { basename, dirname, join } from "path";
5
4
 
6
5
  import {
7
6
  findAssistantByName,
7
+ getBaseDir,
8
8
  loadAllAssistants,
9
9
  removeAssistantEntry,
10
10
  } from "../lib/assistant-config";
@@ -109,10 +109,10 @@ async function retireLocal(name: string, entry: AssistantEntry): Promise<void> {
109
109
  await stopOrphanedDaemonProcesses();
110
110
  }
111
111
 
112
- // For named instances (instanceDir differs from homedir), archive and
113
- // remove the entire instance directory. For the default instance
114
- // (instanceDir is homedir), archive only the .vellum subdirectory.
115
- const isNamedInstance = resources.instanceDir !== homedir();
112
+ // For named instances (instanceDir differs from the base directory),
113
+ // archive and remove the entire instance directory. For the default
114
+ // instance, archive only the .vellum subdirectory.
115
+ const isNamedInstance = resources.instanceDir !== getBaseDir();
116
116
  const dirToArchive = isNamedInstance ? resources.instanceDir : vellumDir;
117
117
 
118
118
  // Move the data directory out of the way so the path is immediately available