@vellumai/cli 0.4.26 → 0.4.29

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,5 +1,15 @@
1
1
  import { createHash, randomBytes, randomUUID } from "crypto";
2
- import { appendFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, symlinkSync, unlinkSync, writeFileSync } from "fs";
2
+ import {
3
+ appendFileSync,
4
+ existsSync,
5
+ lstatSync,
6
+ mkdirSync,
7
+ readFileSync,
8
+ readlinkSync,
9
+ symlinkSync,
10
+ unlinkSync,
11
+ writeFileSync,
12
+ } from "fs";
3
13
  import { homedir, hostname, userInfo } from "os";
4
14
  import { join } from "path";
5
15
 
@@ -10,7 +20,11 @@ import qrcode from "qrcode-terminal";
10
20
  import cliPkg from "../../package.json";
11
21
 
12
22
  import { buildOpenclawStartupScript } from "../adapters/openclaw";
13
- import { loadAllAssistants, saveAssistantEntry, syncConfigToLockfile } from "../lib/assistant-config";
23
+ import {
24
+ loadAllAssistants,
25
+ saveAssistantEntry,
26
+ syncConfigToLockfile,
27
+ } from "../lib/assistant-config";
14
28
  import type { AssistantEntry } from "../lib/assistant-config";
15
29
  import { hatchAws } from "../lib/aws";
16
30
  import {
@@ -22,7 +36,12 @@ import {
22
36
  import type { RemoteHost, Species } from "../lib/constants";
23
37
  import { hatchGcp } from "../lib/gcp";
24
38
  import type { PollResult, WatchHatchingResult } from "../lib/gcp";
25
- import { startLocalDaemon, startGateway, stopLocalProcesses } from "../lib/local";
39
+ import {
40
+ startLocalDaemon,
41
+ startGateway,
42
+ startOutboundProxy,
43
+ stopLocalProcesses,
44
+ } from "../lib/local";
26
45
  import { probePort } from "../lib/port-probe";
27
46
  import { isProcessAlive } from "../lib/process";
28
47
  import { generateRandomSuffix } from "../lib/random-name";
@@ -32,14 +51,13 @@ export type { PollResult, WatchHatchingResult } from "../lib/gcp";
32
51
 
33
52
  const INSTALL_SCRIPT_REMOTE_PATH = "/tmp/vellum-install.sh";
34
53
 
35
-
36
54
  const HATCH_TIMEOUT_MS: Record<Species, number> = {
37
55
  vellum: 2 * 60 * 1000,
38
56
  openclaw: 10 * 60 * 1000,
39
57
  };
40
58
  const DEFAULT_SPECIES: Species = "vellum";
41
59
 
42
- const SPINNER_FRAMES= ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
60
+ const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
43
61
 
44
62
  const IS_DESKTOP = !!process.env.VELLUM_DESKTOP_APP;
45
63
 
@@ -77,9 +95,14 @@ export async function buildStartupScript(
77
95
  instanceName: string,
78
96
  cloud: RemoteHost,
79
97
  ): Promise<string> {
80
- const platformUrl = process.env.VELLUM_ASSISTANT_PLATFORM_URL ?? "https://assistant.vellum.ai";
81
- const logPath = cloud === "custom" ? "/tmp/vellum-startup.log" : "/var/log/startup-script.log";
82
- const errorPath = cloud === "custom" ? "/tmp/vellum-startup-error" : "/var/log/startup-error";
98
+ const platformUrl =
99
+ process.env.VELLUM_ASSISTANT_PLATFORM_URL ?? "https://assistant.vellum.ai";
100
+ const logPath =
101
+ cloud === "custom"
102
+ ? "/tmp/vellum-startup.log"
103
+ : "/var/log/startup-script.log";
104
+ const errorPath =
105
+ cloud === "custom" ? "/tmp/vellum-startup-error" : "/var/log/startup-error";
83
106
  const timestampRedirect = buildTimestampRedirect(logPath);
84
107
  const userSetup = buildUserSetup(sshUser);
85
108
  const ownershipFixup = buildOwnershipFixup();
@@ -174,10 +197,18 @@ function parseArgs(): HatchArgs {
174
197
  console.log("Options:");
175
198
  console.log(" -d Run in detached mode");
176
199
  console.log(" --name <name> Custom instance name");
177
- console.log(" --remote <host> Remote host (local, gcp, aws, custom)");
178
- console.log(" --daemon-only Start daemon only, skip gateway");
179
- console.log(" --restart Restart processes without onboarding side effects");
180
- console.log(" --watch Run daemon and gateway in watch mode (hot reload on source changes)");
200
+ console.log(
201
+ " --remote <host> Remote host (local, gcp, aws, custom)",
202
+ );
203
+ console.log(
204
+ " --daemon-only Start assistant only, skip gateway",
205
+ );
206
+ console.log(
207
+ " --restart Restart processes without onboarding side effects",
208
+ );
209
+ console.log(
210
+ " --watch Run assistant and gateway in watch mode (hot reload on source changes)",
211
+ );
181
212
  process.exit(0);
182
213
  } else if (arg === "-d") {
183
214
  detached = true;
@@ -196,7 +227,9 @@ function parseArgs(): HatchArgs {
196
227
  try {
197
228
  validateAssistantName(next);
198
229
  } catch {
199
- console.error(`Error: --name contains invalid characters (path separators or traversal segments are not allowed)`);
230
+ console.error(
231
+ `Error: --name contains invalid characters (path separators or traversal segments are not allowed)`,
232
+ );
200
233
  process.exit(1);
201
234
  }
202
235
  name = next;
@@ -236,7 +269,11 @@ function pickMessage(messages: string[], elapsedMs: number): string {
236
269
  return messages[idx];
237
270
  }
238
271
 
239
- function getPhaseIcon(hasLogs: boolean, elapsedMs: number, species: Species): string {
272
+ function getPhaseIcon(
273
+ hasLogs: boolean,
274
+ elapsedMs: number,
275
+ species: Species,
276
+ ): string {
240
277
  if (!hasLogs) {
241
278
  return elapsedMs < 30000 ? "🥚" : "🪺";
242
279
  }
@@ -292,7 +329,11 @@ export async function watchHatching(
292
329
  : pickMessage(config.waitingMessages, elapsed);
293
330
  spinnerIdx++;
294
331
 
295
- const lines = ["", ` ${icon} ${spinner} ${message} ⏱ ${formatElapsed(elapsed)}`, ""];
332
+ const lines = [
333
+ "",
334
+ ` ${icon} ${spinner} ${message} ⏱ ${formatElapsed(elapsed)}`,
335
+ "",
336
+ ];
296
337
 
297
338
  for (const line of lines) {
298
339
  process.stdout.write(`\x1b[K${line}\n`);
@@ -334,7 +375,9 @@ export async function watchHatching(
334
375
  if (elapsed >= HATCH_TIMEOUT_MS[species]) {
335
376
  clearInterval(interval);
336
377
  console.log("");
337
- console.log(` ⏰ Timed out after ${formatElapsed(elapsed)}. Instance is still running.`);
378
+ console.log(
379
+ ` ⏰ Timed out after ${formatElapsed(elapsed)}. Instance is still running.`,
380
+ );
338
381
  console.log(` Monitor with: vel logs ${instanceName}`);
339
382
  console.log("");
340
383
  resolve({ success: true, errorContent: lastErrorContent });
@@ -378,7 +421,9 @@ function watchHatchingDesktop(
378
421
 
379
422
  if (elapsed >= HATCH_TIMEOUT_MS[species]) {
380
423
  clearInterval(interval);
381
- desktopLog(`Timed out after ${formatElapsed(elapsed)}. Instance is still running.`);
424
+ desktopLog(
425
+ `Timed out after ${formatElapsed(elapsed)}. Instance is still running.`,
426
+ );
382
427
  desktopLog(`Monitor with: vel logs ${instanceName}`);
383
428
  resolve({ success: true, errorContent: lastErrorContent });
384
429
  return;
@@ -474,7 +519,9 @@ function ensureLocalBinInShellProfile(localBinDir: string): void {
474
519
  if (!profilePath) return;
475
520
 
476
521
  try {
477
- const contents = existsSync(profilePath) ? readFileSync(profilePath, "utf-8") : "";
522
+ const contents = existsSync(profilePath)
523
+ ? readFileSync(profilePath, "utf-8")
524
+ : "";
478
525
  // Check if ~/.local/bin is already referenced in PATH exports
479
526
  if (contents.includes(localBinDir)) return;
480
527
  const line = `\nexport PATH="${localBinDir}:\$PATH"\n`;
@@ -506,10 +553,16 @@ function installCLISymlink(): void {
506
553
  return;
507
554
  }
508
555
 
509
- console.log(` ⚠ Could not create symlink for vellum CLI (tried ${preferredPath} and ${fallbackPath})`);
556
+ console.log(
557
+ ` ⚠ Could not create symlink for vellum CLI (tried ${preferredPath} and ${fallbackPath})`,
558
+ );
510
559
  }
511
560
 
512
- async function waitForDaemonReady(runtimeUrl: string, bearerToken: string | undefined, timeoutMs = 15000): Promise<boolean> {
561
+ async function waitForDaemonReady(
562
+ runtimeUrl: string,
563
+ bearerToken: string | undefined,
564
+ timeoutMs = 15000,
565
+ ): Promise<boolean> {
513
566
  const start = Date.now();
514
567
  const pollInterval = 1000;
515
568
  while (Date.now() - start < timeoutMs) {
@@ -528,7 +581,10 @@ async function waitForDaemonReady(runtimeUrl: string, bearerToken: string | unde
528
581
  return false;
529
582
  }
530
583
 
531
- async function displayPairingQRCode(runtimeUrl: string, bearerToken: string | undefined): Promise<void> {
584
+ async function displayPairingQRCode(
585
+ runtimeUrl: string,
586
+ bearerToken: string | undefined,
587
+ ): Promise<void> {
532
588
  try {
533
589
  const pairingRequestId = randomUUID();
534
590
  const pairingSecret = randomBytes(32).toString("hex");
@@ -539,7 +595,9 @@ async function displayPairingQRCode(runtimeUrl: string, bearerToken: string | un
539
595
  // health endpoint through the gateway to ensure it's reachable.
540
596
  const daemonReady = await waitForDaemonReady(runtimeUrl, bearerToken);
541
597
  if (!daemonReady) {
542
- console.warn("⚠ Daemon health check did not pass within 15s. Run `vellum pair` to try again.\n");
598
+ console.warn(
599
+ "⚠ Assistant health check did not pass within 15s. Run `vellum pair` to try again.\n",
600
+ );
543
601
  return;
544
602
  }
545
603
 
@@ -549,16 +607,24 @@ async function displayPairingQRCode(runtimeUrl: string, bearerToken: string | un
549
607
  "Content-Type": "application/json",
550
608
  ...(bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {}),
551
609
  },
552
- body: JSON.stringify({ pairingRequestId, pairingSecret, gatewayUrl: runtimeUrl }),
610
+ body: JSON.stringify({
611
+ pairingRequestId,
612
+ pairingSecret,
613
+ gatewayUrl: runtimeUrl,
614
+ }),
553
615
  });
554
616
 
555
617
  if (!registerRes.ok) {
556
618
  const body = await registerRes.text().catch(() => "");
557
- console.warn(`⚠ Could not register pairing request: ${registerRes.status} ${registerRes.statusText}${body ? ` — ${body}` : ""}. Run \`vellum pair\` to try again.\n`);
619
+ console.warn(
620
+ `⚠ Could not register pairing request: ${registerRes.status} ${registerRes.statusText}${body ? ` — ${body}` : ""}. Run \`vellum pair\` to try again.\n`,
621
+ );
558
622
  return;
559
623
  }
560
624
 
561
- const hostId = createHash("sha256").update(hostname() + userInfo().username).digest("hex");
625
+ const hostId = createHash("sha256")
626
+ .update(hostname() + userInfo().username)
627
+ .digest("hex");
562
628
  const payload = JSON.stringify({
563
629
  type: "vellum-daemon",
564
630
  v: 4,
@@ -580,11 +646,15 @@ async function displayPairingQRCode(runtimeUrl: string, bearerToken: string | un
580
646
  mkdirSync(qrDir, { recursive: true });
581
647
  const qrPngPath = join(qrDir, "initial.png");
582
648
  try {
583
- const pngBuffer = await QRCode.toBuffer(payload, { type: "png", width: 512 });
649
+ const pngBuffer = await QRCode.toBuffer(payload, {
650
+ type: "png",
651
+ width: 512,
652
+ });
584
653
  writeFileSync(qrPngPath, pngBuffer);
585
654
  console.log(`QR code PNG saved to ${qrPngPath}\n`);
586
655
  } catch (pngErr) {
587
- const pngReason = pngErr instanceof Error ? pngErr.message : String(pngErr);
656
+ const pngReason =
657
+ pngErr instanceof Error ? pngErr.message : String(pngErr);
588
658
  console.warn(`\u26A0 Could not save QR code PNG: ${pngReason}\n`);
589
659
  }
590
660
 
@@ -595,18 +665,30 @@ async function displayPairingQRCode(runtimeUrl: string, bearerToken: string | un
595
665
  } catch (err) {
596
666
  // Non-fatal — pairing is optional
597
667
  const reason = err instanceof Error ? err.message : String(err);
598
- console.warn(`⚠ Could not generate pairing QR code: ${reason}. Run \`vellum pair\` to try again.\n`);
668
+ console.warn(
669
+ `⚠ Could not generate pairing QR code: ${reason}. Run \`vellum pair\` to try again.\n`,
670
+ );
599
671
  }
600
672
  }
601
673
 
602
- async function hatchLocal(species: Species, name: string | null, daemonOnly: boolean = false, restart: boolean = false, watch: boolean = false): Promise<void> {
674
+ async function hatchLocal(
675
+ species: Species,
676
+ name: string | null,
677
+ daemonOnly: boolean = false,
678
+ restart: boolean = false,
679
+ watch: boolean = false,
680
+ ): Promise<void> {
603
681
  if (restart && !name && !process.env.VELLUM_ASSISTANT_NAME) {
604
- console.error("Error: Cannot restart without a known assistant ID. Provide --name or ensure VELLUM_ASSISTANT_NAME is set.");
682
+ console.error(
683
+ "Error: Cannot restart without a known assistant ID. Provide --name or ensure VELLUM_ASSISTANT_NAME is set.",
684
+ );
605
685
  process.exit(1);
606
686
  }
607
687
 
608
688
  const instanceName =
609
- name ?? process.env.VELLUM_ASSISTANT_NAME ?? `${species}-${generateRandomSuffix()}`;
689
+ name ??
690
+ process.env.VELLUM_ASSISTANT_NAME ??
691
+ `${species}-${generateRandomSuffix()}`;
610
692
 
611
693
  // Clean up stale local state: if daemon/gateway processes are running but
612
694
  // the lock file has no entries, stop them before starting fresh.
@@ -617,7 +699,9 @@ async function hatchLocal(species: Species, name: string | null, daemonOnly: boo
617
699
  const daemonPid = isProcessAlive(join(vellumDir, "vellum.pid"));
618
700
  const gatewayPid = isProcessAlive(join(vellumDir, "gateway.pid"));
619
701
  if (daemonPid.alive || gatewayPid.alive) {
620
- console.log("🧹 Cleaning up stale local processes (no lock file entry)...\n");
702
+ console.log(
703
+ "🧹 Cleaning up stale local processes (no lock file entry)...\n",
704
+ );
621
705
  await stopLocalProcesses();
622
706
  }
623
707
 
@@ -648,7 +732,11 @@ async function hatchLocal(species: Species, name: string | null, daemonOnly: boo
648
732
  }
649
733
  }
650
734
 
651
- const baseDataDir = join(process.env.BASE_DATA_DIR?.trim() || (process.env.HOME ?? userInfo().homedir), ".vellum");
735
+ const baseDataDir = join(
736
+ process.env.BASE_DATA_DIR?.trim() ||
737
+ (process.env.HOME ?? userInfo().homedir),
738
+ ".vellum",
739
+ );
652
740
 
653
741
  console.log(`🥚 Hatching local assistant: ${instanceName}`);
654
742
  console.log(` Species: ${species}`);
@@ -662,11 +750,15 @@ async function hatchLocal(species: Species, name: string | null, daemonOnly: boo
662
750
  } catch (error) {
663
751
  // Gateway failed — stop the daemon we just started so we don't leave
664
752
  // orphaned processes with no lock file entry.
665
- console.error(`\n❌ Gateway startup failed — stopping daemon to avoid orphaned processes.`);
753
+ console.error(
754
+ `\n❌ Gateway startup failed — stopping assistant to avoid orphaned processes.`,
755
+ );
666
756
  await stopLocalProcesses();
667
757
  throw error;
668
758
  }
669
759
 
760
+ await startOutboundProxy(watch);
761
+
670
762
  // Read the bearer token written by the daemon so the client can authenticate
671
763
  // with the gateway (which requires auth by default).
672
764
  let bearerToken: string | undefined;
@@ -715,10 +807,13 @@ export async function hatch(): Promise<void> {
715
807
  const cliVersion = getCliVersion();
716
808
  console.log(`@vellumai/cli v${cliVersion}`);
717
809
 
718
- const { species, detached, name, remote, daemonOnly, restart, watch } = parseArgs();
810
+ const { species, detached, name, remote, daemonOnly, restart, watch } =
811
+ parseArgs();
719
812
 
720
813
  if (restart && remote !== "local") {
721
- console.error("Error: --restart is only supported for local hatch targets.");
814
+ console.error(
815
+ "Error: --restart is only supported for local hatch targets.",
816
+ );
722
817
  process.exit(1);
723
818
  }
724
819
 
@@ -48,7 +48,9 @@ export async function login(): Promise<void> {
48
48
  savePlatformToken(token);
49
49
  console.log(`✅ Logged in as ${user.email}`);
50
50
  } catch (error) {
51
- console.error(`❌ Login failed: ${error instanceof Error ? error.message : error}`);
51
+ console.error(
52
+ `❌ Login failed: ${error instanceof Error ? error.message : error}`,
53
+ );
52
54
  process.exit(1);
53
55
  }
54
56
  }
@@ -58,7 +60,9 @@ export async function logout(): Promise<void> {
58
60
  if (args.includes("--help") || args.includes("-h")) {
59
61
  console.log("Usage: vellum logout");
60
62
  console.log("");
61
- console.log("Log out of the Vellum platform and remove the stored session token.");
63
+ console.log(
64
+ "Log out of the Vellum platform and remove the stored session token.",
65
+ );
62
66
  process.exit(0);
63
67
  }
64
68
 
@@ -55,16 +55,20 @@ async function pollForApproval(
55
55
 
56
56
  if (!statusRes.ok) {
57
57
  const body = await statusRes.text().catch(() => "");
58
- throw new Error(`Failed to check pairing status: HTTP ${statusRes.status}: ${body || statusRes.statusText}`);
58
+ throw new Error(
59
+ `Failed to check pairing status: HTTP ${statusRes.status}: ${body || statusRes.statusText}`,
60
+ );
59
61
  }
60
62
 
61
- const statusBody = await statusRes.json() as PairingResponse;
63
+ const statusBody = (await statusRes.json()) as PairingResponse;
62
64
 
63
65
  if (statusBody.status === "approved") {
64
66
  return statusBody;
65
67
  }
66
68
 
67
- await new Promise((resolve) => setTimeout(resolve, PAIRING_POLL_INTERVAL_MS));
69
+ await new Promise((resolve) =>
70
+ setTimeout(resolve, PAIRING_POLL_INTERVAL_MS),
71
+ );
68
72
  }
69
73
 
70
74
  throw new Error("Pairing timed out waiting for approval.");
@@ -76,7 +80,9 @@ export async function pair(): Promise<void> {
76
80
  if (args.includes("--help") || args.includes("-h")) {
77
81
  console.log("Usage: vellum pair <path-to-qrcode.png>");
78
82
  console.log("");
79
- console.log("Pair with a remote assistant by scanning the QR code PNG generated during setup.");
83
+ console.log(
84
+ "Pair with a remote assistant by scanning the QR code PNG generated during setup.",
85
+ );
80
86
  process.exit(0);
81
87
  }
82
88
 
@@ -85,7 +91,9 @@ export async function pair(): Promise<void> {
85
91
  if (!qrCodePath) {
86
92
  console.error("Usage: vellum pair <path-to-qrcode.png>");
87
93
  console.error("");
88
- console.error("Pair with a remote assistant by scanning the QR code PNG generated during setup.");
94
+ console.error(
95
+ "Pair with a remote assistant by scanning the QR code PNG generated during setup.",
96
+ );
89
97
  process.exit(1);
90
98
  }
91
99
 
@@ -102,7 +110,12 @@ export async function pair(): Promise<void> {
102
110
  throw new Error("QR code does not contain valid pairing data.");
103
111
  }
104
112
 
105
- if (payload.type !== "vellum-daemon" || !payload.g || !payload.pairingRequestId || !payload.pairingSecret) {
113
+ if (
114
+ payload.type !== "vellum-daemon" ||
115
+ !payload.g ||
116
+ !payload.pairingRequestId ||
117
+ !payload.pairingSecret
118
+ ) {
106
119
  throw new Error("QR code does not contain valid Vellum pairing data.");
107
120
  }
108
121
 
@@ -127,10 +140,12 @@ export async function pair(): Promise<void> {
127
140
 
128
141
  if (!requestRes.ok) {
129
142
  const body = await requestRes.text().catch(() => "");
130
- throw new Error(`Failed to initiate pairing: HTTP ${requestRes.status}: ${body || requestRes.statusText}`);
143
+ throw new Error(
144
+ `Failed to initiate pairing: HTTP ${requestRes.status}: ${body || requestRes.statusText}`,
145
+ );
131
146
  }
132
147
 
133
- const requestBody = await requestRes.json() as PairingResponse;
148
+ const requestBody = (await requestRes.json()) as PairingResponse;
134
149
 
135
150
  let bearerToken: string | undefined;
136
151
 
@@ -145,7 +160,9 @@ export async function pair(): Promise<void> {
145
160
  );
146
161
  bearerToken = approvedResponse.bearerToken;
147
162
  } else {
148
- throw new Error(`Unexpected pairing response status: ${requestBody.status}`);
163
+ throw new Error(
164
+ `Unexpected pairing response status: ${requestBody.status}`,
165
+ );
149
166
  }
150
167
 
151
168
  const customEntry: AssistantEntry = {
@@ -63,10 +63,14 @@ function printTable(rows: TableRow[]): void {
63
63
  // ── Remote process listing via SSH ──────────────────────────────
64
64
 
65
65
  const SSH_OPTS = [
66
- "-o", "StrictHostKeyChecking=no",
67
- "-o", "UserKnownHostsFile=/dev/null",
68
- "-o", "ConnectTimeout=10",
69
- "-o", "LogLevel=ERROR",
66
+ "-o",
67
+ "StrictHostKeyChecking=no",
68
+ "-o",
69
+ "UserKnownHostsFile=/dev/null",
70
+ "-o",
71
+ "ConnectTimeout=10",
72
+ "-o",
73
+ "LogLevel=ERROR",
70
74
  ];
71
75
 
72
76
  const REMOTE_PS_CMD = [
@@ -86,8 +90,8 @@ function classifyProcess(command: string): string {
86
90
  if (/qdrant/.test(command)) return "qdrant";
87
91
  if (/vellum-gateway/.test(command)) return "gateway";
88
92
  if (/openclaw/.test(command)) return "openclaw-adapter";
89
- if (/vellum-daemon/.test(command)) return "daemon";
90
- if (/daemon\s+(start|restart)/.test(command)) return "daemon";
93
+ if (/vellum-daemon/.test(command)) return "assistant";
94
+ if (/daemon\s+(start|restart)/.test(command)) return "assistant";
91
95
  // Exclude macOS desktop app processes — their path contains .app/Contents/MacOS/
92
96
  // but they are not background service processes.
93
97
  if (/\.app\/Contents\/MacOS\//.test(command)) return "unknown";
@@ -126,9 +130,7 @@ function resolveCloud(entry: AssistantEntry): string {
126
130
  return "local";
127
131
  }
128
132
 
129
- async function getRemoteProcessesGcp(
130
- entry: AssistantEntry,
131
- ): Promise<string> {
133
+ async function getRemoteProcessesGcp(entry: AssistantEntry): Promise<string> {
132
134
  return execOutput("gcloud", [
133
135
  "compute",
134
136
  "ssh",
@@ -148,11 +150,7 @@ async function getRemoteProcessesCustom(
148
150
  ): Promise<string> {
149
151
  const host = extractHostFromUrl(entry.runtimeUrl);
150
152
  const sshUser = entry.sshUser ?? "root";
151
- return execOutput("ssh", [
152
- ...SSH_OPTS,
153
- `${sshUser}@${host}`,
154
- REMOTE_PS_CMD,
155
- ]);
153
+ return execOutput("ssh", [...SSH_OPTS, `${sshUser}@${host}`, REMOTE_PS_CMD]);
156
154
  }
157
155
 
158
156
  interface ProcessSpec {
@@ -192,7 +190,7 @@ async function detectProcess(spec: ProcessSpec): Promise<DetectedProcess> {
192
190
  }
193
191
 
194
192
  // Tier 2: TCP port probe (skip for processes without a port)
195
- const listening = spec.port > 0 && await probePort(spec.port);
193
+ const listening = spec.port > 0 && (await probePort(spec.port));
196
194
  if (listening) {
197
195
  const filePid = readPidFile(spec.pidFile);
198
196
  return {
@@ -222,11 +220,39 @@ function formatDetectionInfo(proc: DetectedProcess): string {
222
220
  async function getLocalProcesses(entry: AssistantEntry): Promise<TableRow[]> {
223
221
  const vellumDir = entry.baseDataDir ?? join(homedir(), ".vellum");
224
222
 
223
+ const PROXY_PORT = Number(process.env.PROXY_PORT) || 7829;
224
+
225
225
  const specs: ProcessSpec[] = [
226
- { name: "daemon", pgrepName: "vellum-daemon", port: RUNTIME_HTTP_PORT, pidFile: join(vellumDir, "vellum.pid") },
227
- { name: "qdrant", pgrepName: "qdrant", port: QDRANT_PORT, pidFile: join(vellumDir, "workspace", "data", "qdrant", "qdrant.pid") },
228
- { name: "gateway", pgrepName: "vellum-gateway", port: GATEWAY_PORT, pidFile: join(vellumDir, "gateway.pid") },
229
- { name: "embed-worker", pgrepName: "embed-worker", port: 0, pidFile: join(vellumDir, "embed-worker.pid") },
226
+ {
227
+ name: "assistant",
228
+ pgrepName: "vellum-daemon",
229
+ port: RUNTIME_HTTP_PORT,
230
+ pidFile: join(vellumDir, "vellum.pid"),
231
+ },
232
+ {
233
+ name: "qdrant",
234
+ pgrepName: "qdrant",
235
+ port: QDRANT_PORT,
236
+ pidFile: join(vellumDir, "workspace", "data", "qdrant", "qdrant.pid"),
237
+ },
238
+ {
239
+ name: "gateway",
240
+ pgrepName: "vellum-gateway",
241
+ port: GATEWAY_PORT,
242
+ pidFile: join(vellumDir, "gateway.pid"),
243
+ },
244
+ {
245
+ name: "outbound-proxy",
246
+ pgrepName: "outbound-proxy",
247
+ port: PROXY_PORT,
248
+ pidFile: join(vellumDir, "outbound-proxy.pid"),
249
+ },
250
+ {
251
+ name: "embed-worker",
252
+ pgrepName: "embed-worker",
253
+ port: 0,
254
+ pidFile: join(vellumDir, "embed-worker.pid"),
255
+ },
230
256
  ];
231
257
 
232
258
  const results = await Promise.all(specs.map(detectProcess));
@@ -260,7 +286,9 @@ async function showAssistantProcesses(entry: AssistantEntry): Promise<void> {
260
286
  process.exit(1);
261
287
  }
262
288
  } catch (error) {
263
- console.error(`Failed to list processes: ${error instanceof Error ? error.message : error}`);
289
+ console.error(
290
+ `Failed to list processes: ${error instanceof Error ? error.message : error}`,
291
+ );
264
292
  process.exit(1);
265
293
  }
266
294
 
@@ -295,7 +323,7 @@ async function detectOrphanedProcesses(): Promise<OrphanedProcess[]> {
295
323
 
296
324
  // Strategy 1: PID file scan
297
325
  const pidFiles: Array<{ file: string; name: string }> = [
298
- { file: join(vellumDir, "vellum.pid"), name: "daemon" },
326
+ { file: join(vellumDir, "vellum.pid"), name: "assistant" },
299
327
  { file: join(vellumDir, "gateway.pid"), name: "gateway" },
300
328
  { file: join(vellumDir, "qdrant.pid"), name: "qdrant" },
301
329
  ];
@@ -349,7 +377,9 @@ async function listAllAssistants(): Promise<void> {
349
377
  }));
350
378
  printTable(rows);
351
379
  const pids = orphans.map((o) => o.pid).join(" ");
352
- console.log(`\nHint: Run \`kill ${pids}\` to clean up orphaned processes.`);
380
+ console.log(
381
+ `\nHint: Run \`kill ${pids}\` to clean up orphaned processes.`,
382
+ );
353
383
  }
354
384
 
355
385
  return;
@@ -413,7 +443,9 @@ export async function ps(): Promise<void> {
413
443
  if (args.includes("--help") || args.includes("-h")) {
414
444
  console.log("Usage: vellum ps [<name>]");
415
445
  console.log("");
416
- console.log("List all assistants, or show processes for a specific assistant.");
446
+ console.log(
447
+ "List all assistants, or show processes for a specific assistant.",
448
+ );
417
449
  console.log("");
418
450
  console.log("Arguments:");
419
451
  console.log(" <name> Show processes for the named assistant");
@@ -13,7 +13,9 @@ export async function recover(): Promise<void> {
13
13
  if (args.includes("--help") || args.includes("-h")) {
14
14
  console.log("Usage: vellum recover <name>");
15
15
  console.log("");
16
- console.log("Restore a previously retired local assistant from its archive.");
16
+ console.log(
17
+ "Restore a previously retired local assistant from its archive.",
18
+ );
17
19
  console.log("");
18
20
  console.log("Arguments:");
19
21
  console.log(" <name> Name of the retired assistant to recover");
@@ -39,7 +41,7 @@ export async function recover(): Promise<void> {
39
41
  const vellumDir = join(homedir(), ".vellum");
40
42
  if (existsSync(vellumDir)) {
41
43
  console.error(
42
- "Error: ~/.vellum already exists. Retire the current assistant first."
44
+ "Error: ~/.vellum already exists. Retire the current assistant first.",
43
45
  );
44
46
  process.exit(1);
45
47
  }