@vellumai/cli 0.6.6 → 0.7.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.
Files changed (61) hide show
  1. package/AGENTS.md +8 -2
  2. package/README.md +49 -0
  3. package/package.json +1 -1
  4. package/src/__tests__/assistant-config.test.ts +1 -7
  5. package/src/__tests__/backup.test.ts +475 -0
  6. package/src/__tests__/config-utils.test.ts +146 -0
  7. package/src/__tests__/env-drift.test.ts +10 -32
  8. package/src/__tests__/llm-provider-env-var-parity.test.ts +1 -21
  9. package/src/__tests__/multi-local.test.ts +0 -5
  10. package/src/__tests__/sleep.test.ts +1 -2
  11. package/src/__tests__/teleport.test.ts +988 -1266
  12. package/src/commands/backup.ts +117 -71
  13. package/src/commands/client.ts +10 -9
  14. package/src/commands/env.ts +93 -0
  15. package/src/commands/events.ts +2 -0
  16. package/src/commands/exec.ts +58 -13
  17. package/src/commands/login.ts +77 -12
  18. package/src/commands/logs.ts +2 -7
  19. package/src/commands/ps.ts +144 -25
  20. package/src/commands/restore.ts +26 -47
  21. package/src/commands/sleep.ts +5 -2
  22. package/src/commands/ssh.ts +17 -7
  23. package/src/commands/teleport.ts +462 -584
  24. package/src/commands/terminal.ts +9 -221
  25. package/src/commands/tunnel.ts +2 -7
  26. package/src/commands/upgrade.ts +108 -7
  27. package/src/commands/wake.ts +2 -1
  28. package/src/components/DefaultMainScreen.tsx +328 -154
  29. package/src/index.ts +5 -7
  30. package/src/lib/__tests__/docker.test.ts +50 -74
  31. package/src/lib/__tests__/job-polling.test.ts +278 -0
  32. package/src/lib/__tests__/local-runtime-client.test.ts +480 -0
  33. package/src/lib/__tests__/platform-client-signed-url.test.ts +405 -0
  34. package/src/lib/__tests__/runtime-url.test.ts +87 -0
  35. package/src/lib/__tests__/terminal-session.test.ts +202 -0
  36. package/src/lib/assistant-client.ts +5 -21
  37. package/src/lib/assistant-config.ts +46 -24
  38. package/src/lib/cli-error.ts +1 -0
  39. package/src/lib/client-identity.ts +67 -0
  40. package/src/lib/docker.ts +75 -77
  41. package/src/lib/environments/__tests__/paths.test.ts +2 -0
  42. package/src/lib/environments/resolve.ts +89 -7
  43. package/src/lib/environments/seeds.ts +8 -5
  44. package/src/lib/environments/types.ts +10 -0
  45. package/src/lib/hatch-local.ts +15 -120
  46. package/src/lib/health-check.ts +98 -0
  47. package/src/lib/job-polling.ts +195 -0
  48. package/src/lib/local-runtime-client.ts +231 -0
  49. package/src/lib/local.ts +165 -72
  50. package/src/lib/orphan-detection.ts +2 -35
  51. package/src/lib/platform-client.ts +190 -194
  52. package/src/lib/platform-releases.ts +23 -0
  53. package/src/lib/retire-local.ts +6 -2
  54. package/src/lib/runtime-url.ts +30 -0
  55. package/src/lib/sync-cloud-assistants.ts +126 -0
  56. package/src/lib/terminal-client.ts +6 -1
  57. package/src/lib/terminal-session.ts +536 -0
  58. package/src/lib/tui-log.ts +60 -0
  59. package/src/lib/xdg-log.ts +10 -4
  60. package/src/shared/provider-env-vars.ts +2 -3
  61. package/src/__tests__/orphan-detection.test.ts +0 -214
@@ -3,9 +3,11 @@ import { spawn } from "child_process";
3
3
  import { randomBytes } from "crypto";
4
4
 
5
5
  import {
6
- findAssistantByName,
7
6
  getActiveAssistant,
8
- loadLatestAssistant,
7
+ resolveAssistant,
8
+ loadAllAssistants,
9
+ removeAssistantEntry,
10
+ setActiveAssistant,
9
11
  } from "../lib/assistant-config";
10
12
  import { computeDeviceId } from "../lib/guardian-token";
11
13
  import {
@@ -13,13 +15,16 @@ import {
13
15
  ensureSelfHostedLocalRegistration,
14
16
  fetchCurrentUser,
15
17
  fetchOrganizationId,
18
+ fetchPlatformAssistants,
16
19
  getPlatformUrl,
20
+ getWebUrl,
17
21
  injectCredentialsIntoAssistant,
18
22
  readGatewayCredential,
19
23
  readPlatformToken,
20
24
  reprovisionAssistantApiKey,
21
25
  savePlatformToken,
22
26
  } from "../lib/platform-client";
27
+ import { syncCloudAssistants } from "../lib/sync-cloud-assistants";
23
28
 
24
29
  const LOGIN_TIMEOUT_MS = 120_000; // 2 minutes
25
30
 
@@ -45,7 +50,7 @@ function openBrowser(url: string): void {
45
50
  * Start a local HTTP server, open the browser to the platform login page,
46
51
  * and wait for the platform to redirect back with the session token.
47
52
  */
48
- function browserLogin(platformUrl: string): Promise<string> {
53
+ function browserLogin(webUrl: string): Promise<string> {
49
54
  return new Promise((resolve, reject) => {
50
55
  const state = randomBytes(32).toString("hex");
51
56
 
@@ -112,7 +117,7 @@ function browserLogin(platformUrl: string): Promise<string> {
112
117
 
113
118
  const port = addr.port;
114
119
  const returnTo = `/accounts/cli/callback?port=${port}&state=${state}`;
115
- const loginUrl = `${platformUrl}/account/login?returnTo=${encodeURIComponent(returnTo)}`;
120
+ const loginUrl = `${webUrl}/account/login?returnTo=${encodeURIComponent(returnTo)}`;
116
121
 
117
122
  console.log("Opening browser for login...");
118
123
  console.log(`If the browser doesn't open, visit: ${loginUrl}`);
@@ -125,22 +130,30 @@ export async function login(): Promise<void> {
125
130
  const args = process.argv.slice(3);
126
131
 
127
132
  if (args.includes("--help") || args.includes("-h")) {
128
- console.log("Usage: vellum login [--token <session-token>]");
133
+ console.log("Usage: vellum login [--token <session-token>] [--force]");
129
134
  console.log("");
130
135
  console.log("Log in to the Vellum platform.");
131
136
  console.log("");
132
137
  console.log("By default, opens a browser window for authentication.");
133
138
  console.log("Alternatively, pass a session token directly with --token.");
134
139
  console.log("");
140
+ console.log("On success, syncs cloud-managed assistants to the local");
141
+ console.log("lockfile so they appear in `vellum ps`.");
142
+ console.log("");
135
143
  console.log("Options:");
136
144
  console.log(" --token <token> Session token from the Vellum platform");
145
+ console.log(
146
+ " --force, -f Re-authenticate even if already logged in",
147
+ );
137
148
  console.log("");
138
149
  console.log("Examples:");
139
150
  console.log(" vellum login");
140
151
  console.log(" vellum login --token <session-token>");
152
+ console.log(" vellum login --force");
141
153
  process.exit(0);
142
154
  }
143
155
 
156
+ const forceFlag = args.includes("--force") || args.includes("-f");
144
157
  let token: string | null = null;
145
158
 
146
159
  for (let i = 0; i < args.length; i++) {
@@ -154,11 +167,27 @@ export async function login(): Promise<void> {
154
167
  }
155
168
  }
156
169
 
170
+ // Block if already authenticated (unless --force)
171
+ if (!forceFlag && !token) {
172
+ const existingToken = readPlatformToken();
173
+ if (existingToken) {
174
+ try {
175
+ const existingUser = await fetchCurrentUser(existingToken);
176
+ console.error(
177
+ `Already logged in as ${existingUser.email}. Run \`vellum logout\` first, or use \`vellum login --force\` to re-authenticate.`,
178
+ );
179
+ process.exit(1);
180
+ } catch {
181
+ // Token is stale/invalid — proceed with login
182
+ }
183
+ }
184
+ }
185
+
157
186
  // If no --token flag, use browser-based login
158
187
  if (!token) {
159
- const platformUrl = getPlatformUrl();
188
+ const webUrl = getWebUrl();
160
189
  try {
161
- token = await browserLogin(platformUrl);
190
+ token = await browserLogin(webUrl);
162
191
  } catch (error) {
163
192
  console.error(`❌ ${error instanceof Error ? error.message : error}`);
164
193
  process.exit(1);
@@ -175,10 +204,7 @@ export async function login(): Promise<void> {
175
204
  // Register the local assistant with the platform (non-fatal).
176
205
  // Mirrors the desktop app's LocalAssistantBootstrapService flow.
177
206
  try {
178
- const activeName = getActiveAssistant();
179
- const entry = activeName
180
- ? findAssistantByName(activeName)
181
- : loadLatestAssistant();
207
+ const entry = resolveAssistant();
182
208
 
183
209
  // Skip managed ("vellum") assistants — they are handled by the platform.
184
210
  if (entry && entry.cloud !== "vellum") {
@@ -247,6 +273,31 @@ export async function login(): Promise<void> {
247
273
  } catch {
248
274
  // Non-fatal — login succeeded even if registration fails
249
275
  }
276
+
277
+ // Sync cloud assistants from the platform into the local lockfile.
278
+ // This ensures `vellum ps` shows managed assistants immediately
279
+ // after login (e.g. after a retire-and-rehatch cycle).
280
+ try {
281
+ const result = await syncCloudAssistants();
282
+ if (result) {
283
+ const total = result.added + result.removed;
284
+ if (total > 0) {
285
+ console.log(
286
+ `Synced cloud assistants (${result.added} added, ${result.removed} removed).`,
287
+ );
288
+ }
289
+ }
290
+
291
+ // If no active assistant is set, activate the first cloud one.
292
+ if (!getActiveAssistant()) {
293
+ const platformAssistants = await fetchPlatformAssistants(token);
294
+ if (platformAssistants.length > 0) {
295
+ setActiveAssistant(platformAssistants[0].id);
296
+ }
297
+ }
298
+ } catch {
299
+ // Non-fatal — login succeeded even if sync fails
300
+ }
250
301
  } catch (error) {
251
302
  console.error(
252
303
  `❌ Login failed: ${error instanceof Error ? error.message : error}`,
@@ -261,11 +312,25 @@ export async function logout(): Promise<void> {
261
312
  console.log("Usage: vellum logout");
262
313
  console.log("");
263
314
  console.log(
264
- "Log out of the Vellum platform and remove the stored session token.",
315
+ "Log out of the Vellum platform, remove the stored session token,",
265
316
  );
317
+ console.log("and remove cloud-managed assistants from the local lockfile.");
266
318
  process.exit(0);
267
319
  }
268
320
 
321
+ // Remove cloud-managed assistants from the lockfile.
322
+ const cloudAssistants = loadAllAssistants().filter(
323
+ (a) => a.cloud === "vellum",
324
+ );
325
+ for (const a of cloudAssistants) {
326
+ removeAssistantEntry(a.assistantId);
327
+ }
328
+ if (cloudAssistants.length > 0) {
329
+ console.log(
330
+ `Removed ${cloudAssistants.length} cloud assistant${cloudAssistants.length > 1 ? "s" : ""} from local lockfile.`,
331
+ );
332
+ }
333
+
269
334
  clearPlatformToken();
270
335
  console.log("Logged out. Platform token removed.");
271
336
  }
@@ -4,10 +4,7 @@ import { createInterface } from "readline";
4
4
  import { watch } from "fs";
5
5
  import { join } from "path";
6
6
 
7
- import {
8
- findAssistantByName,
9
- loadLatestAssistant,
10
- } from "../lib/assistant-config";
7
+ import { resolveAssistant } from "../lib/assistant-config";
11
8
  import type { AssistantEntry } from "../lib/assistant-config";
12
9
  import { dockerResourceNames } from "../lib/docker";
13
10
  import { getLogDir } from "../lib/xdg-log";
@@ -593,9 +590,7 @@ async function showAwsLogs(
593
590
  export async function logs(): Promise<void> {
594
591
  const opts = parseArgs();
595
592
 
596
- const entry = opts.name
597
- ? findAssistantByName(opts.name)
598
- : loadLatestAssistant();
593
+ const entry = resolveAssistant(opts.name);
599
594
 
600
595
  if (!entry) {
601
596
  if (opts.name) {
@@ -3,11 +3,18 @@ import { join } from "path";
3
3
  import {
4
4
  findAssistantByName,
5
5
  getActiveAssistant,
6
+ getDaemonPidPath,
6
7
  loadAllAssistants,
7
8
  type AssistantEntry,
8
9
  } from "../lib/assistant-config";
10
+ import { resolveEnvironmentSource } from "../lib/environments/resolve";
9
11
  import { loadGuardianToken } from "../lib/guardian-token";
10
- import { checkHealth, checkManagedHealth } from "../lib/health-check";
12
+ import {
13
+ checkHealth,
14
+ checkManagedHealth,
15
+ fetchManagedPs,
16
+ type ManagedProcessEntry,
17
+ } from "../lib/health-check";
11
18
  import { dockerResourceNames } from "../lib/docker";
12
19
  import { existsSync } from "fs";
13
20
  import {
@@ -21,6 +28,10 @@ import { pgrepExact } from "../lib/pgrep";
21
28
  import { probePort } from "../lib/port-probe";
22
29
  import { withStatusEmoji } from "../lib/status-emoji";
23
30
  import { execOutput } from "../lib/step-runner";
31
+ import {
32
+ syncCloudAssistants,
33
+ type SyncLogger,
34
+ } from "../lib/sync-cloud-assistants";
24
35
 
25
36
  // ── Table formatting helpers ────────────────────────────────────
26
37
 
@@ -65,6 +76,49 @@ function printTable(rows: TableRow[]): void {
65
76
  }
66
77
  }
67
78
 
79
+ // ── Managed process tree rendering ──────────────────────────────
80
+
81
+ const STATUS_LABELS: Record<ManagedProcessEntry["status"], string> = {
82
+ running: "running",
83
+ not_running: "not running",
84
+ unreachable: "unreachable",
85
+ };
86
+
87
+ function flattenProcessTree(
88
+ entries: ManagedProcessEntry[],
89
+ depth = 0,
90
+ ): TableRow[] {
91
+ const rows: TableRow[] = [];
92
+ for (const entry of entries) {
93
+ const children = entry.children ?? [];
94
+
95
+ rows.push({
96
+ name:
97
+ depth === 0 ? entry.name : `${" ".repeat(depth - 1)}├─ ${entry.name}`,
98
+ status: withStatusEmoji(STATUS_LABELS[entry.status]),
99
+ info: entry.info ?? "",
100
+ });
101
+
102
+ for (let j = 0; j < children.length; j++) {
103
+ const child = children[j];
104
+ const isLast = j === children.length - 1;
105
+ const prefix = `${" ".repeat(depth)}${isLast ? "└─" : "├─"} ${child.name}`;
106
+ rows.push({
107
+ name: prefix,
108
+ status: withStatusEmoji(STATUS_LABELS[child.status]),
109
+ info: child.info ?? "",
110
+ });
111
+
112
+ // Recurse into grandchildren
113
+ const grandchildren = child.children ?? [];
114
+ if (grandchildren.length > 0) {
115
+ rows.push(...flattenProcessTree(grandchildren, depth + 2));
116
+ }
117
+ }
118
+ }
119
+ return rows;
120
+ }
121
+
68
122
  // ── Remote process listing via SSH ──────────────────────────────
69
123
 
70
124
  const SSH_OPTS = [
@@ -215,37 +269,38 @@ async function getLocalProcesses(entry: AssistantEntry): Promise<TableRow[]> {
215
269
  const resources = entry.resources;
216
270
  const vellumDir = join(resources.instanceDir, ".vellum");
217
271
 
218
- const specs: ProcessSpec[] = [
219
- {
220
- name: "assistant",
221
- pgrepName: "vellum-daemon",
222
- port: resources.daemonPort,
223
- pidFile: resources.pidFile,
224
- },
272
+ const assistantSpec: ProcessSpec = {
273
+ name: "assistant",
274
+ pgrepName: "vellum-daemon",
275
+ port: resources.daemonPort,
276
+ pidFile: getDaemonPidPath(resources),
277
+ };
278
+ const subSpecs: ProcessSpec[] = [
225
279
  {
226
- name: "qdrant",
280
+ name: "├─ qdrant",
227
281
  pgrepName: "qdrant",
228
282
  port: resources.qdrantPort,
229
283
  pidFile: join(vellumDir, "workspace", "data", "qdrant", "qdrant.pid"),
230
284
  },
231
285
  {
232
- name: "gateway",
233
- pgrepName: "vellum-gateway",
234
- port: resources.gatewayPort,
235
- pidFile: join(vellumDir, "gateway.pid"),
236
- },
237
- {
238
- name: "embed-worker",
286
+ name: "└─ embed-worker",
239
287
  pgrepName: "embed-worker",
240
288
  port: 0,
241
289
  pidFile: join(vellumDir, "workspace", "embed-worker.pid"),
242
290
  },
243
291
  ];
292
+ const gatewaySpec: ProcessSpec = {
293
+ name: "gateway",
294
+ pgrepName: "vellum-gateway",
295
+ port: resources.gatewayPort,
296
+ pidFile: join(vellumDir, "gateway.pid"),
297
+ };
244
298
 
245
- const results = await Promise.all(specs.map(detectProcess));
299
+ const allSpecs = [assistantSpec, ...subSpecs, gatewaySpec];
300
+ const results = await Promise.all(allSpecs.map(detectProcess));
246
301
 
247
- return results.map((proc) => ({
248
- name: proc.name,
302
+ return results.map((proc, i) => ({
303
+ name: allSpecs[i].name,
249
304
  status: withStatusEmoji(proc.running ? "running" : "not running"),
250
305
  info: proc.running ? formatDetectionInfo(proc) : "not detected",
251
306
  }));
@@ -335,6 +390,28 @@ async function showAssistantProcesses(entry: AssistantEntry): Promise<void> {
335
390
  return;
336
391
  }
337
392
 
393
+ if (cloud === "vellum") {
394
+ console.log(` Platform ID: ${entry.assistantId}\n`);
395
+
396
+ const psData = await fetchManagedPs(entry.runtimeUrl, entry.assistantId);
397
+
398
+ if (!psData) {
399
+ const rows: TableRow[] = [
400
+ {
401
+ name: "assistant",
402
+ status: withStatusEmoji("unreachable"),
403
+ info: "could not reach platform API — run `vellum login`",
404
+ },
405
+ ];
406
+ printTable(rows);
407
+ return;
408
+ }
409
+
410
+ const rows = flattenProcessTree(psData.processes);
411
+ printTable(rows);
412
+ return;
413
+ }
414
+
338
415
  if (cloud === "apple-container") {
339
416
  const mgmtSocket = entry.mgmtSocket as string | undefined;
340
417
  const socketAlive = mgmtSocket ? existsSync(mgmtSocket) : false;
@@ -395,7 +472,40 @@ async function showAssistantProcesses(entry: AssistantEntry): Promise<void> {
395
472
 
396
473
  // ── List all assistants (no arg) ────────────────────────────────
397
474
 
398
- async function listAllAssistants(): Promise<void> {
475
+ async function listAllAssistants(verbose: boolean): Promise<void> {
476
+ const { name: envName, source: envSource } = resolveEnvironmentSource();
477
+ const sourceLabels: Record<typeof envSource, string> = {
478
+ flag: "--environment flag",
479
+ env: "VELLUM_ENVIRONMENT",
480
+ config: "~/.config/vellum/environment",
481
+ default: "default",
482
+ };
483
+ console.log(`Environment: ${envName} (${sourceLabels[envSource]})`);
484
+
485
+ const log: SyncLogger | undefined = verbose
486
+ ? (msg) => console.log(` [verbose] ${msg}`)
487
+ : undefined;
488
+
489
+ // Refresh cloud assistants from the platform before listing.
490
+ const syncResult = await syncCloudAssistants({ log });
491
+
492
+ // Show platform login status
493
+ if (syncResult) {
494
+ const parts = [`Platform: logged in`];
495
+ if (syncResult.email) parts[0] += ` as ${syncResult.email}`;
496
+ if (syncResult.added > 0 || syncResult.removed > 0) {
497
+ const changes: string[] = [];
498
+ if (syncResult.added > 0) changes.push(`${syncResult.added} added`);
499
+ if (syncResult.removed > 0)
500
+ changes.push(`${syncResult.removed} removed`);
501
+ parts.push(`(${changes.join(", ")})`);
502
+ }
503
+ console.log(parts.join(" "));
504
+ } else {
505
+ console.log("Platform: not logged in");
506
+ }
507
+ console.log("");
508
+
399
509
  const assistants = loadAllAssistants();
400
510
  const activeId = getActiveAssistant();
401
511
 
@@ -454,7 +564,9 @@ async function listAllAssistants(): Promise<void> {
454
564
  let health: { status: string; detail: string | null; version?: string };
455
565
  const resources = a.resources;
456
566
  if (a.cloud === "local" && resources) {
457
- const pid = readPidFile(resources.pidFile);
567
+ // TODO(ATL-306): Remove readPidFile/getDaemonPidPath in favor of
568
+ // fetching daemon PIDs via the health API (Gateway Security Migration).
569
+ const pid = readPidFile(getDaemonPidPath(resources));
458
570
  const alive = pid !== null && isProcessAlive(pid);
459
571
  if (!alive) {
460
572
  health = { status: "sleeping", detail: null };
@@ -515,21 +627,28 @@ async function listAllAssistants(): Promise<void> {
515
627
  export async function ps(): Promise<void> {
516
628
  const args = process.argv.slice(3);
517
629
  if (args.includes("--help") || args.includes("-h")) {
518
- console.log("Usage: vellum ps [<name>]");
630
+ console.log("Usage: vellum ps [<name>] [--verbose]");
519
631
  console.log("");
520
632
  console.log(
521
633
  "List all assistants, or show processes for a specific assistant.",
522
634
  );
523
635
  console.log("");
524
636
  console.log("Arguments:");
525
- console.log(" <name> Show processes for the named assistant");
637
+ console.log(" <name> Show processes for the named assistant");
638
+ console.log("");
639
+ console.log("Options:");
640
+ console.log(
641
+ " --verbose Show diagnostic logs (platform sync, auth issues)",
642
+ );
526
643
  process.exit(0);
527
644
  }
528
645
 
529
- const assistantId = process.argv[3];
646
+ const verbose = args.includes("--verbose");
647
+ const positional = args.filter((a) => !a.startsWith("--"));
648
+ const assistantId = positional[0];
530
649
 
531
650
  if (!assistantId) {
532
- await listAllAssistants();
651
+ await listAllAssistants(verbose);
533
652
  return;
534
653
  }
535
654
 
@@ -9,13 +9,11 @@ import {
9
9
  import {
10
10
  readPlatformToken,
11
11
  rollbackPlatformAssistant,
12
- platformImportPreflight,
13
- platformImportBundle,
14
- platformRequestUploadUrl,
12
+ platformRequestSignedUrl,
15
13
  platformUploadToSignedUrl,
16
14
  platformImportPreflightFromGcs,
17
15
  platformImportBundleFromGcs,
18
- platformPollImportStatus,
16
+ platformPollJobStatus,
19
17
  } from "../lib/platform-client.js";
20
18
  import { performDockerRollback } from "../lib/upgrade-lifecycle.js";
21
19
 
@@ -181,24 +179,14 @@ async function restorePlatform(
181
179
  process.exit(1);
182
180
  }
183
181
 
184
- // Step 1.5 — Upload to GCS via signed URL (with fallback to inline)
185
- let bundleKey: string | null = null;
186
- try {
187
- const { uploadUrl, bundleKey: key } = await platformRequestUploadUrl(
188
- token,
189
- entry.runtimeUrl,
190
- );
191
- bundleKey = key;
192
- console.log("Uploading bundle...");
193
- await platformUploadToSignedUrl(uploadUrl, new Uint8Array(bundleData));
194
- } catch (err) {
195
- const msg = err instanceof Error ? err.message : String(err);
196
- if (msg.includes("not available")) {
197
- bundleKey = null;
198
- } else {
199
- throw err;
200
- }
201
- }
182
+ // Step 1.5 — Upload to GCS via signed URL
183
+ const { url: uploadUrl, bundleKey } = await platformRequestSignedUrl(
184
+ { operation: "upload" },
185
+ token,
186
+ entry.runtimeUrl,
187
+ );
188
+ console.log("Uploading bundle...");
189
+ await platformUploadToSignedUrl(uploadUrl, new Uint8Array(bundleData));
202
190
 
203
191
  // Step 2 — Dry-run path
204
192
  if (opts.dryRun) {
@@ -213,17 +201,11 @@ async function restorePlatform(
213
201
 
214
202
  let preflightResult: { statusCode: number; body: Record<string, unknown> };
215
203
  try {
216
- preflightResult = bundleKey
217
- ? await platformImportPreflightFromGcs(
218
- bundleKey,
219
- token,
220
- entry.runtimeUrl,
221
- )
222
- : await platformImportPreflight(
223
- new Uint8Array(bundleData),
224
- token,
225
- entry.runtimeUrl,
226
- );
204
+ preflightResult = await platformImportPreflightFromGcs(
205
+ bundleKey,
206
+ token,
207
+ entry.runtimeUrl,
208
+ );
227
209
  } catch (err) {
228
210
  if (err instanceof Error && err.name === "TimeoutError") {
229
211
  console.error("Error: Preflight request timed out after 2 minutes.");
@@ -353,13 +335,11 @@ async function restorePlatform(
353
335
 
354
336
  let importResult: { statusCode: number; body: Record<string, unknown> };
355
337
  try {
356
- importResult = bundleKey
357
- ? await platformImportBundleFromGcs(bundleKey, token, entry.runtimeUrl)
358
- : await platformImportBundle(
359
- new Uint8Array(bundleData),
360
- token,
361
- entry.runtimeUrl,
362
- );
338
+ importResult = await platformImportBundleFromGcs(
339
+ bundleKey,
340
+ token,
341
+ entry.runtimeUrl,
342
+ );
363
343
  } catch (err) {
364
344
  if (err instanceof Error && err.name === "TimeoutError") {
365
345
  console.error("Error: Import request timed out after 5 minutes.");
@@ -420,13 +400,9 @@ async function restorePlatform(
420
400
  while (Date.now() < deadline) {
421
401
  await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
422
402
 
423
- let status: {
424
- status: string;
425
- result?: Record<string, unknown>;
426
- error?: string;
427
- };
403
+ let status: Awaited<ReturnType<typeof platformPollJobStatus>>;
428
404
  try {
429
- status = await platformPollImportStatus(jobId, token, entry.runtimeUrl);
405
+ status = await platformPollJobStatus(jobId, token, entry.runtimeUrl);
430
406
  } catch (err) {
431
407
  const msg = err instanceof Error ? err.message : String(err);
432
408
  if (msg.includes("not found")) {
@@ -451,7 +427,10 @@ async function restorePlatform(
451
427
  }
452
428
 
453
429
  if (status.status === "complete") {
454
- importResult = { statusCode: 200, body: status.result ?? {} };
430
+ importResult = {
431
+ statusCode: 200,
432
+ body: (status.result as Record<string, unknown>) ?? {},
433
+ };
455
434
  break;
456
435
  }
457
436
 
@@ -1,7 +1,10 @@
1
1
  import { existsSync, readFileSync } from "fs";
2
2
  import { join } from "path";
3
3
 
4
- import { resolveTargetAssistant } from "../lib/assistant-config.js";
4
+ import {
5
+ getDaemonPidPath,
6
+ resolveTargetAssistant,
7
+ } from "../lib/assistant-config.js";
5
8
  import type { AssistantEntry } from "../lib/assistant-config.js";
6
9
  import { dockerResourceNames, sleepContainers } from "../lib/docker.js";
7
10
  import { isProcessAlive, stopProcessByPidFile } from "../lib/process";
@@ -93,7 +96,7 @@ export async function sleep(): Promise<void> {
93
96
  process.exit(1);
94
97
  }
95
98
  const resources = entry.resources;
96
- const assistantPidFile = resources.pidFile;
99
+ const assistantPidFile = getDaemonPidPath(resources);
97
100
  const vellumDir = getAssistantRootDir(entry);
98
101
  const gatewayPidFile = join(vellumDir, "gateway.pid");
99
102
 
@@ -1,12 +1,11 @@
1
1
  import { spawn } from "child_process";
2
2
 
3
- import {
4
- findAssistantByName,
5
- loadLatestAssistant,
6
- } from "../lib/assistant-config";
3
+ import { resolveAssistant } from "../lib/assistant-config";
7
4
  import type { AssistantEntry } from "../lib/assistant-config";
8
5
  import { dockerResourceNames } from "../lib/docker";
6
+ import { getPlatformUrl, readPlatformToken } from "../lib/platform-client";
9
7
  import { sshAppleContainer } from "../lib/ssh-apple-container";
8
+ import { interactiveSession } from "../lib/terminal-session";
10
9
 
11
10
  const SSH_OPTS = [
12
11
  "-o",
@@ -56,7 +55,7 @@ export async function ssh(): Promise<void> {
56
55
  }
57
56
 
58
57
  const name = process.argv[3];
59
- const entry = name ? findAssistantByName(name) : loadLatestAssistant();
58
+ const entry = resolveAssistant(name);
60
59
 
61
60
  if (!entry) {
62
61
  if (name) {
@@ -121,8 +120,19 @@ export async function ssh(): Promise<void> {
121
120
  { stdio: "inherit" },
122
121
  );
123
122
  } else if (cloud === "vellum") {
124
- console.error("SSH to Vellum-managed instances is not yet supported.");
125
- process.exit(1);
123
+ const token = readPlatformToken();
124
+ if (!token) {
125
+ console.error(
126
+ "Not logged in. Run `vellum login` first to authenticate with the platform.",
127
+ );
128
+ process.exit(1);
129
+ }
130
+ await interactiveSession({
131
+ assistantId: entry.assistantId,
132
+ token,
133
+ platformUrl: getPlatformUrl(),
134
+ });
135
+ return;
126
136
  } else if (cloud === "custom") {
127
137
  const host = extractHostFromUrl(entry.runtimeUrl);
128
138
  const sshUser = entry.sshUser ?? "root";