@vellumai/cli 0.6.6 → 0.7.0

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 (45) hide show
  1. package/AGENTS.md +8 -2
  2. package/package.json +1 -1
  3. package/src/__tests__/assistant-config.test.ts +1 -7
  4. package/src/__tests__/config-utils.test.ts +159 -0
  5. package/src/__tests__/env-drift.test.ts +10 -32
  6. package/src/__tests__/llm-provider-env-var-parity.test.ts +1 -21
  7. package/src/__tests__/multi-local.test.ts +0 -5
  8. package/src/__tests__/sleep.test.ts +1 -2
  9. package/src/__tests__/teleport.test.ts +919 -1255
  10. package/src/commands/env.ts +93 -0
  11. package/src/commands/events.ts +2 -0
  12. package/src/commands/exec.ts +40 -8
  13. package/src/commands/hatch.ts +6 -2
  14. package/src/commands/login.ts +89 -6
  15. package/src/commands/ps.ts +104 -20
  16. package/src/commands/sleep.ts +5 -2
  17. package/src/commands/ssh.ts +15 -2
  18. package/src/commands/teleport.ts +447 -583
  19. package/src/commands/terminal.ts +9 -221
  20. package/src/commands/wake.ts +2 -1
  21. package/src/components/DefaultMainScreen.tsx +304 -152
  22. package/src/index.ts +3 -0
  23. package/src/lib/__tests__/docker.test.ts +50 -74
  24. package/src/lib/__tests__/job-polling.test.ts +278 -0
  25. package/src/lib/__tests__/local-runtime-client.test.ts +383 -0
  26. package/src/lib/__tests__/platform-client-signed-url.test.ts +405 -0
  27. package/src/lib/assistant-config.ts +12 -8
  28. package/src/lib/client-identity.ts +67 -0
  29. package/src/lib/config-utils.ts +97 -1
  30. package/src/lib/docker.ts +73 -75
  31. package/src/lib/environments/__tests__/paths.test.ts +2 -0
  32. package/src/lib/environments/resolve.ts +89 -7
  33. package/src/lib/environments/seeds.ts +8 -5
  34. package/src/lib/environments/types.ts +10 -0
  35. package/src/lib/hatch-local.ts +15 -120
  36. package/src/lib/health-check.ts +98 -0
  37. package/src/lib/job-polling.ts +195 -0
  38. package/src/lib/local-runtime-client.ts +178 -0
  39. package/src/lib/local.ts +139 -15
  40. package/src/lib/orphan-detection.ts +2 -35
  41. package/src/lib/platform-client.ts +215 -0
  42. package/src/lib/retire-local.ts +6 -2
  43. package/src/lib/terminal-session.ts +457 -0
  44. package/src/shared/provider-env-vars.ts +2 -3
  45. package/src/__tests__/orphan-detection.test.ts +0 -214
@@ -0,0 +1,93 @@
1
+ import { SEEDS } from "../lib/environments/seeds.js";
2
+ import {
3
+ clearDefaultEnvironment,
4
+ readDefaultEnvironment,
5
+ resolveEnvironmentSource,
6
+ writeDefaultEnvironment,
7
+ } from "../lib/environments/resolve.js";
8
+
9
+ function printUsage(): void {
10
+ console.log("Usage: vellum env <subcommand>");
11
+ console.log("");
12
+ console.log("Manage the default CLI environment.");
13
+ console.log("");
14
+ console.log("Subcommands:");
15
+ console.log(" set <name> Set the default environment");
16
+ console.log(" get Show the current environment and its source");
17
+ console.log(" clear Remove the default, falling back to production");
18
+ console.log("");
19
+ console.log(`Known environments: ${Object.keys(SEEDS).join(", ")}`);
20
+ console.log("");
21
+ console.log("Examples:");
22
+ console.log(" $ vellum env set local # all commands default to local");
23
+ console.log(" $ vellum env get # show resolved environment");
24
+ console.log(" $ vellum env clear # revert to production default");
25
+ }
26
+
27
+ function envSet(name: string | undefined): void {
28
+ if (!name) {
29
+ console.error(
30
+ `Usage: vellum env set <name>\nKnown environments: ${Object.keys(SEEDS).join(", ")}`,
31
+ );
32
+ process.exitCode = 1;
33
+ return;
34
+ }
35
+ if (!SEEDS[name]) {
36
+ console.error(
37
+ `Unknown environment "${name}". Known environments: ${Object.keys(SEEDS).join(", ")}`,
38
+ );
39
+ process.exitCode = 1;
40
+ return;
41
+ }
42
+ writeDefaultEnvironment(name);
43
+ console.log(`Default environment set to "${name}".`);
44
+ }
45
+
46
+ function envGet(): void {
47
+ const { name, source } = resolveEnvironmentSource();
48
+ const sourceLabels: Record<typeof source, string> = {
49
+ flag: "--environment flag",
50
+ env: "VELLUM_ENVIRONMENT env var",
51
+ config: "~/.config/vellum/environment",
52
+ default: "default",
53
+ };
54
+ console.log(`${name} (from ${sourceLabels[source]})`);
55
+ }
56
+
57
+ function envClear(): void {
58
+ const current = readDefaultEnvironment();
59
+ if (!current) {
60
+ console.log("No default environment is set (already using production).");
61
+ return;
62
+ }
63
+ clearDefaultEnvironment();
64
+ console.log(
65
+ `Cleared default environment "${current}". Falling back to production.`,
66
+ );
67
+ }
68
+
69
+ export async function env(): Promise<void> {
70
+ const args = process.argv.slice(3);
71
+ const sub = args[0];
72
+
73
+ if (!sub || sub === "--help" || sub === "-h") {
74
+ printUsage();
75
+ return;
76
+ }
77
+
78
+ switch (sub) {
79
+ case "set":
80
+ envSet(args[1]);
81
+ break;
82
+ case "get":
83
+ envGet();
84
+ break;
85
+ case "clear":
86
+ envClear();
87
+ break;
88
+ default:
89
+ console.error(`Unknown subcommand: ${sub}`);
90
+ printUsage();
91
+ process.exitCode = 1;
92
+ }
93
+ }
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { extractFlag } from "../lib/arg-utils.js";
11
11
  import { AssistantClient } from "../lib/assistant-client.js";
12
+ import { getClientRegistrationHeaders } from "../lib/client-identity.js";
12
13
 
13
14
  function printUsage(): void {
14
15
  console.log(`vellum events - Stream events from a running assistant
@@ -136,6 +137,7 @@ export async function events(): Promise<void> {
136
137
  for await (const event of client.stream<AssistantEvent>("/events", {
137
138
  signal: controller.signal,
138
139
  query,
140
+ headers: getClientRegistrationHeaders(),
139
141
  })) {
140
142
  if (jsonOutput) {
141
143
  console.log(JSON.stringify(event));
@@ -8,7 +8,13 @@ import {
8
8
  import { dockerResourceNames } from "../lib/docker";
9
9
  import type { ServiceName } from "../lib/docker";
10
10
  import { execAppleContainer } from "../lib/exec-apple-container";
11
+ import { getPlatformUrl, readPlatformToken } from "../lib/platform-client";
11
12
  import { sshAppleContainer } from "../lib/ssh-apple-container";
13
+ import {
14
+ interactiveSession,
15
+ nonInteractiveExec,
16
+ shellEscapeArgs,
17
+ } from "../lib/terminal-session";
12
18
 
13
19
  const SERVICE_ALIASES: Record<string, ServiceName> = {
14
20
  assistant: "assistant",
@@ -74,11 +80,12 @@ export async function exec(): Promise<void> {
74
80
  );
75
81
  console.log("");
76
82
  console.log("Options:");
83
+ console.log(" --service <svc> Target service (default: assistant)");
77
84
  console.log(
78
- " --service <svc> Target service (default: assistant)",
85
+ " -it Interactive mode with TTY (like docker exec -it)",
79
86
  );
80
87
  console.log(
81
- " -it Interactive mode with TTY (like docker exec -it)",
88
+ " --verbose Show debug output (SSE events, sentinel parsing)",
82
89
  );
83
90
  console.log("");
84
91
  console.log("Services:");
@@ -90,9 +97,7 @@ export async function exec(): Promise<void> {
90
97
  console.log(" vellum exec -- ls -la /workspace");
91
98
  console.log(" vellum exec -- cat /workspace/NOW.md");
92
99
  console.log(" vellum exec -it -- /bin/bash");
93
- console.log(
94
- " vellum exec --service gateway -- cat /tmp/gateway.log",
95
- );
100
+ console.log(" vellum exec --service gateway -- cat /tmp/gateway.log");
96
101
  process.exit(0);
97
102
  }
98
103
 
@@ -114,12 +119,15 @@ export async function exec(): Promise<void> {
114
119
  let nameArg: string | undefined;
115
120
  let serviceRaw = "assistant";
116
121
  let interactive = false;
122
+ let verbose = false;
117
123
 
118
124
  for (let i = 0; i < preArgs.length; i++) {
119
125
  if (preArgs[i] === "--service" && preArgs[i + 1]) {
120
126
  serviceRaw = preArgs[++i];
121
127
  } else if (preArgs[i] === "-it" || preArgs[i] === "-ti") {
122
128
  interactive = true;
129
+ } else if (preArgs[i] === "--verbose") {
130
+ verbose = true;
123
131
  } else if (!preArgs[i].startsWith("-")) {
124
132
  nameArg = preArgs[i];
125
133
  }
@@ -127,9 +135,7 @@ export async function exec(): Promise<void> {
127
135
 
128
136
  const service = normalizeService(serviceRaw);
129
137
 
130
- const entry = nameArg
131
- ? findAssistantByName(nameArg)
132
- : loadLatestAssistant();
138
+ const entry = nameArg ? findAssistantByName(nameArg) : loadLatestAssistant();
133
139
 
134
140
  if (!entry) {
135
141
  if (nameArg) {
@@ -179,6 +185,32 @@ export async function exec(): Promise<void> {
179
185
  return;
180
186
  }
181
187
 
188
+ if (cloud === "vellum") {
189
+ const token = readPlatformToken();
190
+ if (!token) {
191
+ console.error(
192
+ "Not logged in. Run `vellum login` first to authenticate with the platform.",
193
+ );
194
+ process.exit(1);
195
+ }
196
+
197
+ const assistant = {
198
+ assistantId: entry.assistantId,
199
+ token,
200
+ platformUrl: getPlatformUrl(),
201
+ };
202
+
203
+ if (interactive) {
204
+ // Interactive mode: shell-escape argv and delegate to full terminal
205
+ await interactiveSession(assistant, shellEscapeArgs(command));
206
+ return;
207
+ }
208
+
209
+ // Non-interactive: sentinel-based output capture with exit code
210
+ await nonInteractiveExec(assistant, command, { verbose });
211
+ return;
212
+ }
213
+
182
214
  console.error(
183
215
  `Error: 'vellum exec' is not supported for ${cloud} instances.`,
184
216
  );
@@ -15,7 +15,7 @@ import {
15
15
  VALID_SPECIES,
16
16
  } from "../lib/constants";
17
17
  import type { RemoteHost, Species } from "../lib/constants";
18
- import { buildNestedConfig } from "../lib/config-utils";
18
+ import { buildInitialConfig } from "../lib/config-utils";
19
19
  import { hatchDocker } from "../lib/docker";
20
20
  import { hatchGcp } from "../lib/gcp";
21
21
  import type { PollResult, WatchHatchingResult } from "../lib/gcp";
@@ -123,7 +123,11 @@ export async function buildStartupScript(
123
123
  // and export the env var so the daemon reads it on first boot.
124
124
  let configWriteBlock = "";
125
125
  if (Object.keys(configValues).length > 0) {
126
- const configJson = JSON.stringify(buildNestedConfig(configValues), null, 2);
126
+ const configJson = JSON.stringify(
127
+ buildInitialConfig(configValues),
128
+ null,
129
+ 2,
130
+ );
127
131
  configWriteBlock = `
128
132
  echo "Writing default workspace config..."
129
133
  VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH="/tmp/vellum-initial-config-$$.json"
@@ -6,6 +6,10 @@ import {
6
6
  findAssistantByName,
7
7
  getActiveAssistant,
8
8
  loadLatestAssistant,
9
+ loadAllAssistants,
10
+ removeAssistantEntry,
11
+ saveAssistantEntry,
12
+ setActiveAssistant,
9
13
  } from "../lib/assistant-config";
10
14
  import { computeDeviceId } from "../lib/guardian-token";
11
15
  import {
@@ -13,7 +17,9 @@ import {
13
17
  ensureSelfHostedLocalRegistration,
14
18
  fetchCurrentUser,
15
19
  fetchOrganizationId,
20
+ fetchPlatformAssistants,
16
21
  getPlatformUrl,
22
+ getWebUrl,
17
23
  injectCredentialsIntoAssistant,
18
24
  readGatewayCredential,
19
25
  readPlatformToken,
@@ -45,7 +51,7 @@ function openBrowser(url: string): void {
45
51
  * Start a local HTTP server, open the browser to the platform login page,
46
52
  * and wait for the platform to redirect back with the session token.
47
53
  */
48
- function browserLogin(platformUrl: string): Promise<string> {
54
+ function browserLogin(webUrl: string): Promise<string> {
49
55
  return new Promise((resolve, reject) => {
50
56
  const state = randomBytes(32).toString("hex");
51
57
 
@@ -112,7 +118,7 @@ function browserLogin(platformUrl: string): Promise<string> {
112
118
 
113
119
  const port = addr.port;
114
120
  const returnTo = `/accounts/cli/callback?port=${port}&state=${state}`;
115
- const loginUrl = `${platformUrl}/account/login?returnTo=${encodeURIComponent(returnTo)}`;
121
+ const loginUrl = `${webUrl}/account/login?returnTo=${encodeURIComponent(returnTo)}`;
116
122
 
117
123
  console.log("Opening browser for login...");
118
124
  console.log(`If the browser doesn't open, visit: ${loginUrl}`);
@@ -125,22 +131,30 @@ export async function login(): Promise<void> {
125
131
  const args = process.argv.slice(3);
126
132
 
127
133
  if (args.includes("--help") || args.includes("-h")) {
128
- console.log("Usage: vellum login [--token <session-token>]");
134
+ console.log("Usage: vellum login [--token <session-token>] [--force]");
129
135
  console.log("");
130
136
  console.log("Log in to the Vellum platform.");
131
137
  console.log("");
132
138
  console.log("By default, opens a browser window for authentication.");
133
139
  console.log("Alternatively, pass a session token directly with --token.");
134
140
  console.log("");
141
+ console.log("On success, syncs cloud-managed assistants to the local");
142
+ console.log("lockfile so they appear in `vellum ps`.");
143
+ console.log("");
135
144
  console.log("Options:");
136
145
  console.log(" --token <token> Session token from the Vellum platform");
146
+ console.log(
147
+ " --force, -f Re-authenticate even if already logged in",
148
+ );
137
149
  console.log("");
138
150
  console.log("Examples:");
139
151
  console.log(" vellum login");
140
152
  console.log(" vellum login --token <session-token>");
153
+ console.log(" vellum login --force");
141
154
  process.exit(0);
142
155
  }
143
156
 
157
+ const forceFlag = args.includes("--force") || args.includes("-f");
144
158
  let token: string | null = null;
145
159
 
146
160
  for (let i = 0; i < args.length; i++) {
@@ -154,11 +168,27 @@ export async function login(): Promise<void> {
154
168
  }
155
169
  }
156
170
 
171
+ // Block if already authenticated (unless --force)
172
+ if (!forceFlag && !token) {
173
+ const existingToken = readPlatformToken();
174
+ if (existingToken) {
175
+ try {
176
+ const existingUser = await fetchCurrentUser(existingToken);
177
+ console.error(
178
+ `Already logged in as ${existingUser.email}. Run \`vellum logout\` first, or use \`vellum login --force\` to re-authenticate.`,
179
+ );
180
+ process.exit(1);
181
+ } catch {
182
+ // Token is stale/invalid — proceed with login
183
+ }
184
+ }
185
+ }
186
+
157
187
  // If no --token flag, use browser-based login
158
188
  if (!token) {
159
- const platformUrl = getPlatformUrl();
189
+ const webUrl = getWebUrl();
160
190
  try {
161
- token = await browserLogin(platformUrl);
191
+ token = await browserLogin(webUrl);
162
192
  } catch (error) {
163
193
  console.error(`❌ ${error instanceof Error ? error.message : error}`);
164
194
  process.exit(1);
@@ -247,6 +277,45 @@ export async function login(): Promise<void> {
247
277
  } catch {
248
278
  // Non-fatal — login succeeded even if registration fails
249
279
  }
280
+
281
+ // Sync cloud assistants from the platform into the local lockfile.
282
+ // This ensures `vellum ps` shows managed assistants immediately
283
+ // after login (e.g. after a retire-and-rehatch cycle).
284
+ try {
285
+ const platformAssistants = await fetchPlatformAssistants(token);
286
+ const existingIds = new Set(
287
+ loadAllAssistants()
288
+ .filter((a) => a.cloud === "vellum")
289
+ .map((a) => a.assistantId),
290
+ );
291
+
292
+ let synced = 0;
293
+ for (const pa of platformAssistants) {
294
+ if (!existingIds.has(pa.id)) {
295
+ saveAssistantEntry({
296
+ assistantId: pa.id,
297
+ runtimeUrl: getPlatformUrl(),
298
+ cloud: "vellum",
299
+ species: "vellum",
300
+ hatchedAt: new Date().toISOString(),
301
+ });
302
+ synced++;
303
+ }
304
+ }
305
+
306
+ if (synced > 0) {
307
+ console.log(
308
+ `Synced ${synced} cloud assistant${synced > 1 ? "s" : ""} to local lockfile.`,
309
+ );
310
+ }
311
+
312
+ // If no active assistant is set, activate the first cloud one.
313
+ if (!getActiveAssistant() && platformAssistants.length > 0) {
314
+ setActiveAssistant(platformAssistants[0].id);
315
+ }
316
+ } catch {
317
+ // Non-fatal — login succeeded even if sync fails
318
+ }
250
319
  } catch (error) {
251
320
  console.error(
252
321
  `❌ Login failed: ${error instanceof Error ? error.message : error}`,
@@ -261,11 +330,25 @@ export async function logout(): Promise<void> {
261
330
  console.log("Usage: vellum logout");
262
331
  console.log("");
263
332
  console.log(
264
- "Log out of the Vellum platform and remove the stored session token.",
333
+ "Log out of the Vellum platform, remove the stored session token,",
265
334
  );
335
+ console.log("and remove cloud-managed assistants from the local lockfile.");
266
336
  process.exit(0);
267
337
  }
268
338
 
339
+ // Remove cloud-managed assistants from the lockfile.
340
+ const cloudAssistants = loadAllAssistants().filter(
341
+ (a) => a.cloud === "vellum",
342
+ );
343
+ for (const a of cloudAssistants) {
344
+ removeAssistantEntry(a.assistantId);
345
+ }
346
+ if (cloudAssistants.length > 0) {
347
+ console.log(
348
+ `Removed ${cloudAssistants.length} cloud assistant${cloudAssistants.length > 1 ? "s" : ""} from local lockfile.`,
349
+ );
350
+ }
351
+
269
352
  clearPlatformToken();
270
353
  console.log("Logged out. Platform token removed.");
271
354
  }
@@ -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 {
@@ -65,6 +72,49 @@ function printTable(rows: TableRow[]): void {
65
72
  }
66
73
  }
67
74
 
75
+ // ── Managed process tree rendering ──────────────────────────────
76
+
77
+ const STATUS_LABELS: Record<ManagedProcessEntry["status"], string> = {
78
+ running: "running",
79
+ not_running: "not running",
80
+ unreachable: "unreachable",
81
+ };
82
+
83
+ function flattenProcessTree(
84
+ entries: ManagedProcessEntry[],
85
+ depth = 0,
86
+ ): TableRow[] {
87
+ const rows: TableRow[] = [];
88
+ for (const entry of entries) {
89
+ const children = entry.children ?? [];
90
+
91
+ rows.push({
92
+ name:
93
+ depth === 0 ? entry.name : `${" ".repeat(depth - 1)}├─ ${entry.name}`,
94
+ status: withStatusEmoji(STATUS_LABELS[entry.status]),
95
+ info: entry.info ?? "",
96
+ });
97
+
98
+ for (let j = 0; j < children.length; j++) {
99
+ const child = children[j];
100
+ const isLast = j === children.length - 1;
101
+ const prefix = `${" ".repeat(depth)}${isLast ? "└─" : "├─"} ${child.name}`;
102
+ rows.push({
103
+ name: prefix,
104
+ status: withStatusEmoji(STATUS_LABELS[child.status]),
105
+ info: child.info ?? "",
106
+ });
107
+
108
+ // Recurse into grandchildren
109
+ const grandchildren = child.children ?? [];
110
+ if (grandchildren.length > 0) {
111
+ rows.push(...flattenProcessTree(grandchildren, depth + 2));
112
+ }
113
+ }
114
+ }
115
+ return rows;
116
+ }
117
+
68
118
  // ── Remote process listing via SSH ──────────────────────────────
69
119
 
70
120
  const SSH_OPTS = [
@@ -215,37 +265,38 @@ async function getLocalProcesses(entry: AssistantEntry): Promise<TableRow[]> {
215
265
  const resources = entry.resources;
216
266
  const vellumDir = join(resources.instanceDir, ".vellum");
217
267
 
218
- const specs: ProcessSpec[] = [
219
- {
220
- name: "assistant",
221
- pgrepName: "vellum-daemon",
222
- port: resources.daemonPort,
223
- pidFile: resources.pidFile,
224
- },
268
+ const assistantSpec: ProcessSpec = {
269
+ name: "assistant",
270
+ pgrepName: "vellum-daemon",
271
+ port: resources.daemonPort,
272
+ pidFile: getDaemonPidPath(resources),
273
+ };
274
+ const subSpecs: ProcessSpec[] = [
225
275
  {
226
- name: "qdrant",
276
+ name: "├─ qdrant",
227
277
  pgrepName: "qdrant",
228
278
  port: resources.qdrantPort,
229
279
  pidFile: join(vellumDir, "workspace", "data", "qdrant", "qdrant.pid"),
230
280
  },
231
281
  {
232
- name: "gateway",
233
- pgrepName: "vellum-gateway",
234
- port: resources.gatewayPort,
235
- pidFile: join(vellumDir, "gateway.pid"),
236
- },
237
- {
238
- name: "embed-worker",
282
+ name: "└─ embed-worker",
239
283
  pgrepName: "embed-worker",
240
284
  port: 0,
241
285
  pidFile: join(vellumDir, "workspace", "embed-worker.pid"),
242
286
  },
243
287
  ];
288
+ const gatewaySpec: ProcessSpec = {
289
+ name: "gateway",
290
+ pgrepName: "vellum-gateway",
291
+ port: resources.gatewayPort,
292
+ pidFile: join(vellumDir, "gateway.pid"),
293
+ };
244
294
 
245
- const results = await Promise.all(specs.map(detectProcess));
295
+ const allSpecs = [assistantSpec, ...subSpecs, gatewaySpec];
296
+ const results = await Promise.all(allSpecs.map(detectProcess));
246
297
 
247
- return results.map((proc) => ({
248
- name: proc.name,
298
+ return results.map((proc, i) => ({
299
+ name: allSpecs[i].name,
249
300
  status: withStatusEmoji(proc.running ? "running" : "not running"),
250
301
  info: proc.running ? formatDetectionInfo(proc) : "not detected",
251
302
  }));
@@ -335,6 +386,28 @@ async function showAssistantProcesses(entry: AssistantEntry): Promise<void> {
335
386
  return;
336
387
  }
337
388
 
389
+ if (cloud === "vellum") {
390
+ console.log(` Platform ID: ${entry.assistantId}\n`);
391
+
392
+ const psData = await fetchManagedPs(entry.runtimeUrl, entry.assistantId);
393
+
394
+ if (!psData) {
395
+ const rows: TableRow[] = [
396
+ {
397
+ name: "assistant",
398
+ status: withStatusEmoji("unreachable"),
399
+ info: "could not reach platform API — run `vellum login`",
400
+ },
401
+ ];
402
+ printTable(rows);
403
+ return;
404
+ }
405
+
406
+ const rows = flattenProcessTree(psData.processes);
407
+ printTable(rows);
408
+ return;
409
+ }
410
+
338
411
  if (cloud === "apple-container") {
339
412
  const mgmtSocket = entry.mgmtSocket as string | undefined;
340
413
  const socketAlive = mgmtSocket ? existsSync(mgmtSocket) : false;
@@ -396,6 +469,15 @@ async function showAssistantProcesses(entry: AssistantEntry): Promise<void> {
396
469
  // ── List all assistants (no arg) ────────────────────────────────
397
470
 
398
471
  async function listAllAssistants(): Promise<void> {
472
+ const { name: envName, source: envSource } = resolveEnvironmentSource();
473
+ const sourceLabels: Record<typeof envSource, string> = {
474
+ flag: "--environment flag",
475
+ env: "VELLUM_ENVIRONMENT",
476
+ config: "~/.config/vellum/environment",
477
+ default: "default",
478
+ };
479
+ console.log(`Environment: ${envName} (${sourceLabels[envSource]})\n`);
480
+
399
481
  const assistants = loadAllAssistants();
400
482
  const activeId = getActiveAssistant();
401
483
 
@@ -454,7 +536,9 @@ async function listAllAssistants(): Promise<void> {
454
536
  let health: { status: string; detail: string | null; version?: string };
455
537
  const resources = a.resources;
456
538
  if (a.cloud === "local" && resources) {
457
- const pid = readPidFile(resources.pidFile);
539
+ // TODO(ATL-306): Remove readPidFile/getDaemonPidPath in favor of
540
+ // fetching daemon PIDs via the health API (Gateway Security Migration).
541
+ const pid = readPidFile(getDaemonPidPath(resources));
458
542
  const alive = pid !== null && isProcessAlive(pid);
459
543
  if (!alive) {
460
544
  health = { status: "sleeping", detail: null };
@@ -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
 
@@ -6,7 +6,9 @@ import {
6
6
  } from "../lib/assistant-config";
7
7
  import type { AssistantEntry } from "../lib/assistant-config";
8
8
  import { dockerResourceNames } from "../lib/docker";
9
+ import { getPlatformUrl, readPlatformToken } from "../lib/platform-client";
9
10
  import { sshAppleContainer } from "../lib/ssh-apple-container";
11
+ import { interactiveSession } from "../lib/terminal-session";
10
12
 
11
13
  const SSH_OPTS = [
12
14
  "-o",
@@ -121,8 +123,19 @@ export async function ssh(): Promise<void> {
121
123
  { stdio: "inherit" },
122
124
  );
123
125
  } else if (cloud === "vellum") {
124
- console.error("SSH to Vellum-managed instances is not yet supported.");
125
- process.exit(1);
126
+ const token = readPlatformToken();
127
+ if (!token) {
128
+ console.error(
129
+ "Not logged in. Run `vellum login` first to authenticate with the platform.",
130
+ );
131
+ process.exit(1);
132
+ }
133
+ await interactiveSession({
134
+ assistantId: entry.assistantId,
135
+ token,
136
+ platformUrl: getPlatformUrl(),
137
+ });
138
+ return;
126
139
  } else if (cloud === "custom") {
127
140
  const host = extractHostFromUrl(entry.runtimeUrl);
128
141
  const sshUser = entry.sshUser ?? "root";