@vellumai/cli 0.4.17 → 0.4.19

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/cli",
3
- "version": "0.4.17",
3
+ "version": "0.4.19",
4
4
  "description": "CLI tools for vellum-assistant",
5
5
  "type": "module",
6
6
  "exports": {
package/src/lib/local.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  import { execFileSync, spawn } from "child_process";
2
- import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ unlinkSync,
7
+ writeFileSync,
8
+ } from "fs";
3
9
  import { createRequire } from "module";
4
10
  import { createConnection } from "net";
5
11
  import { homedir, hostname, networkInterfaces, platform } from "os";
@@ -12,10 +18,10 @@ import { openLogFile, closeLogFile } from "./xdg-log.js";
12
18
 
13
19
  const _require = createRequire(import.meta.url);
14
20
 
15
-
16
21
  function isAssistantSourceDir(dir: string): boolean {
17
22
  const pkgPath = join(dir, "package.json");
18
- if (!existsSync(pkgPath) || !existsSync(join(dir, "src", "index.ts"))) return false;
23
+ if (!existsSync(pkgPath) || !existsSync(join(dir, "src", "index.ts")))
24
+ return false;
19
25
  try {
20
26
  const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
21
27
  return pkg.name === "@vellumai/assistant";
@@ -44,7 +50,8 @@ function findAssistantSourceFrom(startDir: string): string | undefined {
44
50
 
45
51
  function isGatewaySourceDir(dir: string): boolean {
46
52
  const pkgPath = join(dir, "package.json");
47
- if (!existsSync(pkgPath) || !existsSync(join(dir, "src", "index.ts"))) return false;
53
+ if (!existsSync(pkgPath) || !existsSync(join(dir, "src", "index.ts")))
54
+ return false;
48
55
  try {
49
56
  const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
50
57
  return pkg.name === "@vellumai/vellum-gateway";
@@ -73,13 +80,30 @@ function findGatewaySourceFromCwd(): string | undefined {
73
80
 
74
81
  function resolveAssistantIndexPath(): string | undefined {
75
82
  // Source tree layout: cli/src/lib/ -> ../../.. -> repo root -> assistant/src/index.ts
76
- const sourceTreeIndex = join(import.meta.dir, "..", "..", "..", "assistant", "src", "index.ts");
83
+ const sourceTreeIndex = join(
84
+ import.meta.dir,
85
+ "..",
86
+ "..",
87
+ "..",
88
+ "assistant",
89
+ "src",
90
+ "index.ts",
91
+ );
77
92
  if (existsSync(sourceTreeIndex)) {
78
93
  return sourceTreeIndex;
79
94
  }
80
95
 
81
96
  // bunx layout: @vellumai/cli/src/lib/ -> ../../../.. -> node_modules/vellum/src/index.ts
82
- const bunxIndex = join(import.meta.dir, "..", "..", "..", "..", "vellum", "src", "index.ts");
97
+ const bunxIndex = join(
98
+ import.meta.dir,
99
+ "..",
100
+ "..",
101
+ "..",
102
+ "..",
103
+ "vellum",
104
+ "src",
105
+ "index.ts",
106
+ );
83
107
  if (existsSync(bunxIndex)) {
84
108
  return bunxIndex;
85
109
  }
@@ -107,7 +131,10 @@ function resolveAssistantIndexPath(): string | undefined {
107
131
  return undefined;
108
132
  }
109
133
 
110
- async function waitForSocketFile(socketPath: string, timeoutMs = 60000): Promise<boolean> {
134
+ async function waitForSocketFile(
135
+ socketPath: string,
136
+ timeoutMs = 60000,
137
+ ): Promise<boolean> {
111
138
  if (existsSync(socketPath)) return true;
112
139
 
113
140
  const start = Date.now();
@@ -128,7 +155,8 @@ async function startDaemonFromSource(assistantIndex: string): Promise<void> {
128
155
  };
129
156
  // Preserve TCP listener flag when falling back from bundled desktop daemon
130
157
  if (process.env.VELLUM_DESKTOP_APP) {
131
- env.VELLUM_DAEMON_TCP_ENABLED = process.env.VELLUM_DAEMON_TCP_ENABLED || "1";
158
+ env.VELLUM_DAEMON_TCP_ENABLED =
159
+ process.env.VELLUM_DAEMON_TCP_ENABLED || "1";
132
160
  }
133
161
 
134
162
  const child = spawn("bun", ["run", assistantIndex, "daemon", "start"], {
@@ -193,10 +221,18 @@ function normalizeIngressUrl(value: unknown): string | undefined {
193
221
  }
194
222
 
195
223
  function readWorkspaceIngressPublicBaseUrl(): string | undefined {
196
- const baseDataDir = process.env.BASE_DATA_DIR?.trim() || (process.env.HOME ?? homedir());
197
- const workspaceConfigPath = join(baseDataDir, ".vellum", "workspace", "config.json");
224
+ const baseDataDir =
225
+ process.env.BASE_DATA_DIR?.trim() || (process.env.HOME ?? homedir());
226
+ const workspaceConfigPath = join(
227
+ baseDataDir,
228
+ ".vellum",
229
+ "workspace",
230
+ "config.json",
231
+ );
198
232
  try {
199
- const raw = JSON.parse(readFileSync(workspaceConfigPath, "utf-8")) as Record<string, unknown>;
233
+ const raw = JSON.parse(
234
+ readFileSync(workspaceConfigPath, "utf-8"),
235
+ ) as Record<string, unknown>;
200
236
  const ingress = raw.ingress as Record<string, unknown> | undefined;
201
237
  return normalizeIngressUrl(ingress?.publicBaseUrl);
202
238
  } catch {
@@ -208,11 +244,15 @@ function readWorkspaceIngressPublicBaseUrl(): string | undefined {
208
244
  * Returns the PID if found, undefined otherwise. */
209
245
  function findSocketOwnerPid(socketPath: string): number | undefined {
210
246
  try {
211
- const output = execFileSync("lsof", ["-U", "-a", "-F", "p", "--", socketPath], {
212
- encoding: "utf-8",
213
- timeout: 3000,
214
- stdio: ["ignore", "pipe", "ignore"],
215
- });
247
+ const output = execFileSync(
248
+ "lsof",
249
+ ["-U", "-a", "-F", "p", "--", socketPath],
250
+ {
251
+ encoding: "utf-8",
252
+ timeout: 3000,
253
+ stdio: ["ignore", "pipe", "ignore"],
254
+ },
255
+ );
216
256
  // lsof -F p outputs lines like "p1234" — extract the first PID
217
257
  const match = output.match(/^p(\d+)/m);
218
258
  if (match) {
@@ -228,7 +268,10 @@ function findSocketOwnerPid(socketPath: string): number | undefined {
228
268
  /** Try a TCP connect to the Unix socket. Returns true if the handshake
229
269
  * completes within the timeout — false on connection refused, timeout,
230
270
  * or missing socket file. */
231
- function isSocketResponsive(socketPath: string, timeoutMs = 1500): Promise<boolean> {
271
+ function isSocketResponsive(
272
+ socketPath: string,
273
+ timeoutMs = 1500,
274
+ ): Promise<boolean> {
232
275
  if (!existsSync(socketPath)) return Promise.resolve(false);
233
276
  return new Promise((resolve) => {
234
277
  const socket = createConnection(socketPath);
@@ -267,7 +310,10 @@ async function discoverPublicUrl(): Promise<string | undefined> {
267
310
  // Use IMDSv2 (token-based) for compatibility with HttpTokens=required
268
311
  const tokenResp = await fetch(
269
312
  "http://169.254.169.254/latest/api/token",
270
- { method: "PUT", headers: { "X-aws-ec2-metadata-token-ttl-seconds": "30" } },
313
+ {
314
+ method: "PUT",
315
+ headers: { "X-aws-ec2-metadata-token-ttl-seconds": "30" },
316
+ },
271
317
  );
272
318
  if (tokenResp.ok) {
273
319
  const token = await tokenResp.text();
@@ -344,7 +390,11 @@ function getLocalLanIPv4(): string | undefined {
344
390
  const addrs = ifaces[ifName];
345
391
  if (!addrs) continue;
346
392
  for (const addr of addrs) {
347
- if (addr.family === "IPv4" && !addr.internal && !addr.address.startsWith("169.254.")) {
393
+ if (
394
+ addr.family === "IPv4" &&
395
+ !addr.internal &&
396
+ !addr.address.startsWith("169.254.")
397
+ ) {
348
398
  return addr.address;
349
399
  }
350
400
  }
@@ -354,7 +404,11 @@ function getLocalLanIPv4(): string | undefined {
354
404
  for (const [, addrs] of Object.entries(ifaces)) {
355
405
  if (!addrs) continue;
356
406
  for (const addr of addrs) {
357
- if (addr.family === "IPv4" && !addr.internal && !addr.address.startsWith("169.254.")) {
407
+ if (
408
+ addr.family === "IPv4" &&
409
+ !addr.internal &&
410
+ !addr.address.startsWith("169.254.")
411
+ ) {
358
412
  return addr.address;
359
413
  }
360
414
  }
@@ -393,7 +447,9 @@ export async function startLocalDaemon(): Promise<void> {
393
447
  console.log(` Daemon already running (pid ${pid})\n`);
394
448
  } catch {
395
449
  // Process doesn't exist, clean up stale PID file
396
- try { unlinkSync(pidFile); } catch {}
450
+ try {
451
+ unlinkSync(pidFile);
452
+ } catch {}
397
453
  }
398
454
  }
399
455
  } catch {}
@@ -409,7 +465,9 @@ export async function startLocalDaemon(): Promise<void> {
409
465
  const ownerPid = findSocketOwnerPid(socketFile);
410
466
  if (ownerPid) {
411
467
  writeFileSync(pidFile, String(ownerPid), "utf-8");
412
- console.log(` Daemon socket is responsive (pid ${ownerPid}) — skipping restart\n`);
468
+ console.log(
469
+ ` Daemon socket is responsive (pid ${ownerPid}) — skipping restart\n`,
470
+ );
413
471
  } else {
414
472
  console.log(" Daemon socket is responsive — skipping restart\n");
415
473
  }
@@ -417,7 +475,9 @@ export async function startLocalDaemon(): Promise<void> {
417
475
  }
418
476
 
419
477
  // Socket is unresponsive or missing — safe to clean up and start fresh.
420
- try { unlinkSync(socketFile); } catch {}
478
+ try {
479
+ unlinkSync(socketFile);
480
+ } catch {}
421
481
 
422
482
  console.log("🔨 Starting daemon...");
423
483
 
@@ -430,7 +490,8 @@ export async function startLocalDaemon(): Promise<void> {
430
490
  // the daemon to take 50+ seconds to start instead of ~1s.
431
491
  const daemonEnv: Record<string, string> = {
432
492
  HOME: process.env.HOME || homedir(),
433
- PATH: process.env.PATH || "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",
493
+ PATH:
494
+ process.env.PATH || "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",
434
495
  VELLUM_DAEMON_TCP_ENABLED: "1",
435
496
  };
436
497
  // Forward optional config env vars the daemon may need
@@ -483,7 +544,9 @@ export async function startLocalDaemon(): Promise<void> {
483
544
  if (!socketReady) {
484
545
  const assistantIndex = resolveAssistantIndexPath();
485
546
  if (assistantIndex) {
486
- console.log(" Bundled daemon socket not ready after 60s — falling back to source daemon...");
547
+ console.log(
548
+ " Bundled daemon socket not ready after 60s — falling back to source daemon...",
549
+ );
487
550
  // Kill the bundled daemon to avoid two processes competing for the same socket/port
488
551
  await stopProcessByPidFile(pidFile, "bundled daemon", [socketFile]);
489
552
  await startDaemonFromSource(assistantIndex);
@@ -494,7 +557,9 @@ export async function startLocalDaemon(): Promise<void> {
494
557
  if (socketReady) {
495
558
  console.log(" Daemon socket ready\n");
496
559
  } else {
497
- console.log(" ⚠️ Daemon socket did not appear within 60s — continuing anyway\n");
560
+ console.log(
561
+ " ⚠️ Daemon socket did not appear within 60s — continuing anyway\n",
562
+ );
498
563
  }
499
564
  } else {
500
565
  console.log("🔨 Starting local daemon...");
@@ -521,15 +586,20 @@ export async function startGateway(assistantId?: string): Promise<string> {
521
586
  // Resolve the default assistant ID for the gateway. Prefer the explicitly
522
587
  // provided assistantId (from hatch), then env override, then lockfile.
523
588
  const resolvedAssistantId =
524
- assistantId
525
- || process.env.GATEWAY_DEFAULT_ASSISTANT_ID
526
- || loadLatestAssistant()?.assistantId;
589
+ assistantId ||
590
+ process.env.GATEWAY_DEFAULT_ASSISTANT_ID ||
591
+ loadLatestAssistant()?.assistantId;
527
592
 
528
593
  // Read the bearer token so the gateway can authenticate proxied requests
529
594
  // (e.g. from paired iOS devices). Respect VELLUM_HTTP_TOKEN_PATH and
530
595
  // BASE_DATA_DIR for consistency with gateway/config.ts and the daemon.
531
- const httpTokenPath = process.env.VELLUM_HTTP_TOKEN_PATH
532
- ?? join(process.env.BASE_DATA_DIR?.trim() || homedir(), ".vellum", "http-token");
596
+ const httpTokenPath =
597
+ process.env.VELLUM_HTTP_TOKEN_PATH ??
598
+ join(
599
+ process.env.BASE_DATA_DIR?.trim() || homedir(),
600
+ ".vellum",
601
+ "http-token",
602
+ );
533
603
  let runtimeProxyBearerToken: string | undefined;
534
604
  try {
535
605
  const tok = readFileSync(httpTokenPath, "utf-8").trim();
@@ -548,7 +618,11 @@ export async function startGateway(assistantId?: string): Promise<string> {
548
618
  const maxWait = 60000;
549
619
  const pollInterval = 500;
550
620
  const start = Date.now();
551
- const pidFile = join(process.env.BASE_DATA_DIR?.trim() || homedir(), ".vellum", "vellum.pid");
621
+ const pidFile = join(
622
+ process.env.BASE_DATA_DIR?.trim() || homedir(),
623
+ ".vellum",
624
+ "vellum.pid",
625
+ );
552
626
  while (Date.now() - start < maxWait) {
553
627
  await new Promise((r) => setTimeout(r, pollInterval));
554
628
  try {
@@ -579,7 +653,7 @@ export async function startGateway(assistantId?: string): Promise<string> {
579
653
  }
580
654
 
581
655
  const gatewayEnv: Record<string, string> = {
582
- ...process.env as Record<string, string>,
656
+ ...(process.env as Record<string, string>),
583
657
  GATEWAY_RUNTIME_PROXY_ENABLED: "true",
584
658
  GATEWAY_RUNTIME_PROXY_REQUIRE_AUTH: "true",
585
659
  RUNTIME_PROXY_BEARER_TOKEN: runtimeProxyBearerToken,
@@ -597,9 +671,9 @@ export async function startGateway(assistantId?: string): Promise<string> {
597
671
  }
598
672
  const workspaceIngressPublicBaseUrl = readWorkspaceIngressPublicBaseUrl();
599
673
  const ingressPublicBaseUrl =
600
- workspaceIngressPublicBaseUrl
601
- ?? normalizeIngressUrl(process.env.INGRESS_PUBLIC_BASE_URL)
602
- ?? publicUrl;
674
+ workspaceIngressPublicBaseUrl ??
675
+ normalizeIngressUrl(process.env.INGRESS_PUBLIC_BASE_URL) ??
676
+ publicUrl;
603
677
  if (ingressPublicBaseUrl) {
604
678
  gatewayEnv.INGRESS_PUBLIC_BASE_URL = ingressPublicBaseUrl;
605
679
  console.log(` Ingress URL: ${ingressPublicBaseUrl}`);
@@ -632,7 +706,7 @@ export async function startGateway(assistantId?: string): Promise<string> {
632
706
  const gatewayDir = resolveGatewayDir();
633
707
  const gwLogFd = openLogFile("gateway.log");
634
708
  try {
635
- gateway = spawn("bun", ["run", "src/index.ts"], {
709
+ gateway = spawn("bun", ["run", "src/index.ts", "--vellum-gateway"], {
636
710
  cwd: gatewayDir,
637
711
  detached: true,
638
712
  stdio: ["ignore", gwLogFd, gwLogFd],
@@ -674,7 +748,9 @@ export async function startGateway(assistantId?: string): Promise<string> {
674
748
  }
675
749
 
676
750
  if (!ready) {
677
- console.warn("⚠ Gateway started but health check did not respond within 30s");
751
+ console.warn(
752
+ "⚠ Gateway started but health check did not respond within 30s",
753
+ );
678
754
  }
679
755
 
680
756
  console.log("✅ Gateway started\n");
@@ -13,11 +13,7 @@ function isVellumProcess(pid: number): boolean {
13
13
  timeout: 3000,
14
14
  stdio: ["ignore", "pipe", "ignore"],
15
15
  }).trim();
16
- // Match daemon/gateway binaries (vellum-daemon, vellum-gateway), npm
17
- // package names (@vellumai/*), or bun-run source invocations where the
18
- // command line may only show "bun run src/index.ts" without any
19
- // vellum-specific path component (e.g. gateway launched from its cwd).
20
- return /vellum|@vellumai|bun\s+(run\s+)?src\/index\.ts/.test(output);
16
+ return /vellum|@vellumai|--vellum-gateway/.test(output);
21
17
  } catch {
22
18
  return false;
23
19
  }