@vellumai/cli 0.4.17 → 0.4.18
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 +1 -1
- package/src/lib/local.ts +114 -38
- package/src/lib/process.ts +1 -5
package/package.json
CHANGED
package/src/lib/local.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { execFileSync, spawn } from "child_process";
|
|
2
|
-
import {
|
|
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")))
|
|
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")))
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
197
|
-
|
|
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(
|
|
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(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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(
|
|
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
|
-
{
|
|
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 (
|
|
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 (
|
|
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 {
|
|
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(
|
|
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 {
|
|
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:
|
|
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(
|
|
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(
|
|
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
|
-
|
|
526
|
-
|
|
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 =
|
|
532
|
-
|
|
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(
|
|
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
|
-
|
|
602
|
-
|
|
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(
|
|
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");
|
package/src/lib/process.ts
CHANGED
|
@@ -13,11 +13,7 @@ function isVellumProcess(pid: number): boolean {
|
|
|
13
13
|
timeout: 3000,
|
|
14
14
|
stdio: ["ignore", "pipe", "ignore"],
|
|
15
15
|
}).trim();
|
|
16
|
-
|
|
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
|
}
|