@vellumai/cli 0.4.30 → 0.4.32
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/commands/contacts.ts +2 -8
- package/src/commands/hatch.ts +0 -3
- package/src/commands/ps.ts +0 -8
- package/src/commands/retire.ts +3 -6
- package/src/commands/sleep.ts +9 -14
- package/src/commands/wake.ts +1 -17
- package/src/lib/gcp.ts +0 -73
- package/src/lib/local.ts +96 -163
- package/src/lib/process.ts +5 -3
- package/src/lib/step-runner.ts +0 -41
package/package.json
CHANGED
package/src/commands/contacts.ts
CHANGED
|
@@ -82,10 +82,7 @@ interface ContactChannel {
|
|
|
82
82
|
interface Contact {
|
|
83
83
|
id: string;
|
|
84
84
|
displayName: string;
|
|
85
|
-
|
|
86
|
-
importance: number;
|
|
87
|
-
responseExpectation: string | null;
|
|
88
|
-
preferredTone: string | null;
|
|
85
|
+
notes: string | null;
|
|
89
86
|
lastInteraction: number | null;
|
|
90
87
|
interactionCount: number;
|
|
91
88
|
channels: ContactChannel[];
|
|
@@ -109,10 +106,7 @@ function formatContact(c: Contact): string {
|
|
|
109
106
|
const lines = [
|
|
110
107
|
` ID: ${c.id}`,
|
|
111
108
|
` Name: ${c.displayName}`,
|
|
112
|
-
`
|
|
113
|
-
` Importance: ${c.importance.toFixed(2)}`,
|
|
114
|
-
` Response: ${c.responseExpectation ?? "(none)"}`,
|
|
115
|
-
` Tone: ${c.preferredTone ?? "(none)"}`,
|
|
109
|
+
` Notes: ${c.notes ?? "(none)"}`,
|
|
116
110
|
` Interactions: ${c.interactionCount}`,
|
|
117
111
|
];
|
|
118
112
|
if (c.lastInteraction) {
|
package/src/commands/hatch.ts
CHANGED
|
@@ -39,7 +39,6 @@ import type { PollResult, WatchHatchingResult } from "../lib/gcp";
|
|
|
39
39
|
import {
|
|
40
40
|
startLocalDaemon,
|
|
41
41
|
startGateway,
|
|
42
|
-
startOutboundProxy,
|
|
43
42
|
stopLocalProcesses,
|
|
44
43
|
} from "../lib/local";
|
|
45
44
|
import { probePort } from "../lib/port-probe";
|
|
@@ -757,8 +756,6 @@ async function hatchLocal(
|
|
|
757
756
|
throw error;
|
|
758
757
|
}
|
|
759
758
|
|
|
760
|
-
await startOutboundProxy(watch);
|
|
761
|
-
|
|
762
759
|
// Read the bearer token written by the daemon so the client can authenticate
|
|
763
760
|
// with the gateway (which requires auth by default).
|
|
764
761
|
let bearerToken: string | undefined;
|
package/src/commands/ps.ts
CHANGED
|
@@ -220,8 +220,6 @@ function formatDetectionInfo(proc: DetectedProcess): string {
|
|
|
220
220
|
async function getLocalProcesses(entry: AssistantEntry): Promise<TableRow[]> {
|
|
221
221
|
const vellumDir = entry.baseDataDir ?? join(homedir(), ".vellum");
|
|
222
222
|
|
|
223
|
-
const PROXY_PORT = Number(process.env.PROXY_PORT) || 7829;
|
|
224
|
-
|
|
225
223
|
const specs: ProcessSpec[] = [
|
|
226
224
|
{
|
|
227
225
|
name: "assistant",
|
|
@@ -241,12 +239,6 @@ async function getLocalProcesses(entry: AssistantEntry): Promise<TableRow[]> {
|
|
|
241
239
|
port: GATEWAY_PORT,
|
|
242
240
|
pidFile: join(vellumDir, "gateway.pid"),
|
|
243
241
|
},
|
|
244
|
-
{
|
|
245
|
-
name: "outbound-proxy",
|
|
246
|
-
pgrepName: "outbound-proxy",
|
|
247
|
-
port: PROXY_PORT,
|
|
248
|
-
pidFile: join(vellumDir, "outbound-proxy.pid"),
|
|
249
|
-
},
|
|
250
242
|
{
|
|
251
243
|
name: "embed-worker",
|
|
252
244
|
pgrepName: "embed-worker",
|
package/src/commands/retire.ts
CHANGED
|
@@ -56,13 +56,10 @@ async function retireLocal(name: string, entry: AssistantEntry): Promise<void> {
|
|
|
56
56
|
socketFile,
|
|
57
57
|
]);
|
|
58
58
|
|
|
59
|
-
// Stop gateway via PID file
|
|
59
|
+
// Stop gateway via PID file — use a longer timeout because the gateway has a
|
|
60
|
+
// configurable drain window (GATEWAY_SHUTDOWN_DRAIN_MS, default 5s) before it exits.
|
|
60
61
|
const gatewayPidFile = join(vellumDir, "gateway.pid");
|
|
61
|
-
await stopProcessByPidFile(gatewayPidFile, "gateway");
|
|
62
|
-
|
|
63
|
-
// Stop outbound proxy via PID file
|
|
64
|
-
const outboundProxyPidFile = join(vellumDir, "outbound-proxy.pid");
|
|
65
|
-
await stopProcessByPidFile(outboundProxyPidFile, "outbound-proxy");
|
|
62
|
+
await stopProcessByPidFile(gatewayPidFile, "gateway", undefined, 7000);
|
|
66
63
|
|
|
67
64
|
// If the PID file didn't track a running daemon, scan for orphaned
|
|
68
65
|
// daemon processes that may have been started without writing a PID.
|
package/src/commands/sleep.ts
CHANGED
|
@@ -8,7 +8,7 @@ export async function sleep(): Promise<void> {
|
|
|
8
8
|
if (args.includes("--help") || args.includes("-h")) {
|
|
9
9
|
console.log("Usage: vellum sleep");
|
|
10
10
|
console.log("");
|
|
11
|
-
console.log("Stop the assistant
|
|
11
|
+
console.log("Stop the assistant and gateway processes.");
|
|
12
12
|
process.exit(0);
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -16,7 +16,6 @@ export async function sleep(): Promise<void> {
|
|
|
16
16
|
const daemonPidFile = join(vellumDir, "vellum.pid");
|
|
17
17
|
const socketFile = join(vellumDir, "vellum.sock");
|
|
18
18
|
const gatewayPidFile = join(vellumDir, "gateway.pid");
|
|
19
|
-
const outboundProxyPidFile = join(vellumDir, "outbound-proxy.pid");
|
|
20
19
|
|
|
21
20
|
// Stop daemon
|
|
22
21
|
const daemonStopped = await stopProcessByPidFile(daemonPidFile, "daemon", [
|
|
@@ -28,22 +27,18 @@ export async function sleep(): Promise<void> {
|
|
|
28
27
|
console.log("Assistant stopped.");
|
|
29
28
|
}
|
|
30
29
|
|
|
31
|
-
// Stop gateway
|
|
32
|
-
|
|
30
|
+
// Stop gateway — use a longer timeout because the gateway has a configurable
|
|
31
|
+
// drain window (GATEWAY_SHUTDOWN_DRAIN_MS, default 5s) before it exits.
|
|
32
|
+
const gatewayStopped = await stopProcessByPidFile(
|
|
33
|
+
gatewayPidFile,
|
|
34
|
+
"gateway",
|
|
35
|
+
undefined,
|
|
36
|
+
7000,
|
|
37
|
+
);
|
|
33
38
|
if (!gatewayStopped) {
|
|
34
39
|
console.log("Gateway is not running.");
|
|
35
40
|
} else {
|
|
36
41
|
console.log("Gateway stopped.");
|
|
37
42
|
}
|
|
38
43
|
|
|
39
|
-
// Stop outbound proxy
|
|
40
|
-
const outboundProxyStopped = await stopProcessByPidFile(
|
|
41
|
-
outboundProxyPidFile,
|
|
42
|
-
"outbound-proxy",
|
|
43
|
-
);
|
|
44
|
-
if (!outboundProxyStopped) {
|
|
45
|
-
console.log("Outbound proxy is not running.");
|
|
46
|
-
} else {
|
|
47
|
-
console.log("Outbound proxy stopped.");
|
|
48
|
-
}
|
|
49
44
|
}
|
package/src/commands/wake.ts
CHANGED
|
@@ -4,11 +4,7 @@ import { join } from "path";
|
|
|
4
4
|
|
|
5
5
|
import { loadAllAssistants } from "../lib/assistant-config";
|
|
6
6
|
import { isProcessAlive, stopProcessByPidFile } from "../lib/process";
|
|
7
|
-
import {
|
|
8
|
-
startLocalDaemon,
|
|
9
|
-
startGateway,
|
|
10
|
-
startOutboundProxy,
|
|
11
|
-
} from "../lib/local";
|
|
7
|
+
import { startLocalDaemon, startGateway } from "../lib/local";
|
|
12
8
|
|
|
13
9
|
export async function wake(): Promise<void> {
|
|
14
10
|
const args = process.argv.slice(3);
|
|
@@ -88,17 +84,5 @@ export async function wake(): Promise<void> {
|
|
|
88
84
|
}
|
|
89
85
|
}
|
|
90
86
|
|
|
91
|
-
// Start outbound proxy
|
|
92
|
-
const outboundProxyPidFile = join(vellumDir, "outbound-proxy.pid");
|
|
93
|
-
const outboundProxyStatus = isProcessAlive(outboundProxyPidFile);
|
|
94
|
-
if (outboundProxyStatus.alive && watch) {
|
|
95
|
-
// Restart in watch mode
|
|
96
|
-
console.log(
|
|
97
|
-
`Outbound proxy running (pid ${outboundProxyStatus.pid}) — restarting in watch mode...`,
|
|
98
|
-
);
|
|
99
|
-
await stopProcessByPidFile(outboundProxyPidFile, "outbound-proxy");
|
|
100
|
-
}
|
|
101
|
-
await startOutboundProxy(watch);
|
|
102
|
-
|
|
103
87
|
console.log("✅ Wake complete.");
|
|
104
88
|
}
|
package/src/lib/gcp.ts
CHANGED
|
@@ -198,58 +198,6 @@ export async function syncFirewallRules(
|
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
export async function fetchFirewallRules(
|
|
202
|
-
project: string,
|
|
203
|
-
tag: string,
|
|
204
|
-
): Promise<string> {
|
|
205
|
-
const output = await execOutput("gcloud", [
|
|
206
|
-
"compute",
|
|
207
|
-
"firewall-rules",
|
|
208
|
-
"list",
|
|
209
|
-
`--project=${project}`,
|
|
210
|
-
"--format=json",
|
|
211
|
-
]);
|
|
212
|
-
const rules = JSON.parse(output) as Array<{ targetTags?: string[] }>;
|
|
213
|
-
const filtered = rules.filter((r) => r.targetTags?.includes(tag));
|
|
214
|
-
return JSON.stringify(filtered, null, 2);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
export interface GcpInstance {
|
|
218
|
-
name: string;
|
|
219
|
-
zone: string;
|
|
220
|
-
externalIp: string | null;
|
|
221
|
-
species: string | null;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
export async function listAssistantInstances(
|
|
225
|
-
project: string,
|
|
226
|
-
): Promise<GcpInstance[]> {
|
|
227
|
-
const output = await execOutput("gcloud", [
|
|
228
|
-
"compute",
|
|
229
|
-
"instances",
|
|
230
|
-
"list",
|
|
231
|
-
`--project=${project}`,
|
|
232
|
-
"--filter=labels.vellum-assistant=true",
|
|
233
|
-
"--format=json(name,zone,networkInterfaces[0].accessConfigs[0].natIP,labels)",
|
|
234
|
-
]);
|
|
235
|
-
const parsed = JSON.parse(output) as Array<{
|
|
236
|
-
name: string;
|
|
237
|
-
zone: string;
|
|
238
|
-
networkInterfaces?: Array<{ accessConfigs?: Array<{ natIP?: string }> }>;
|
|
239
|
-
labels?: Record<string, string>;
|
|
240
|
-
}>;
|
|
241
|
-
return parsed.map((inst) => {
|
|
242
|
-
const zoneParts = (inst.zone ?? "").split("/");
|
|
243
|
-
return {
|
|
244
|
-
name: inst.name,
|
|
245
|
-
zone: zoneParts[zoneParts.length - 1] || "",
|
|
246
|
-
externalIp:
|
|
247
|
-
inst.networkInterfaces?.[0]?.accessConfigs?.[0]?.natIP ?? null,
|
|
248
|
-
species: inst.labels?.species ?? null,
|
|
249
|
-
};
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
|
|
253
201
|
export async function instanceExists(
|
|
254
202
|
instanceName: string,
|
|
255
203
|
project: string,
|
|
@@ -281,27 +229,6 @@ export async function instanceExists(
|
|
|
281
229
|
}
|
|
282
230
|
}
|
|
283
231
|
|
|
284
|
-
export async function sshCommand(
|
|
285
|
-
instanceName: string,
|
|
286
|
-
project: string,
|
|
287
|
-
zone: string,
|
|
288
|
-
command: string,
|
|
289
|
-
): Promise<string> {
|
|
290
|
-
return execOutput("gcloud", [
|
|
291
|
-
"compute",
|
|
292
|
-
"ssh",
|
|
293
|
-
instanceName,
|
|
294
|
-
`--project=${project}`,
|
|
295
|
-
`--zone=${zone}`,
|
|
296
|
-
"--quiet",
|
|
297
|
-
"--ssh-flag=-o StrictHostKeyChecking=no",
|
|
298
|
-
"--ssh-flag=-o UserKnownHostsFile=/dev/null",
|
|
299
|
-
"--ssh-flag=-o ConnectTimeout=5",
|
|
300
|
-
"--ssh-flag=-o LogLevel=ERROR",
|
|
301
|
-
`--command=${command}`,
|
|
302
|
-
]);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
232
|
export async function fetchAndDisplayStartupLogs(
|
|
306
233
|
instanceName: string,
|
|
307
234
|
project: string,
|
package/src/lib/local.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execFileSync, spawn } from "child_process";
|
|
1
|
+
import { execFileSync, execSync, spawn } from "child_process";
|
|
2
2
|
import {
|
|
3
3
|
closeSync,
|
|
4
4
|
existsSync,
|
|
@@ -79,18 +79,6 @@ function findGatewaySourceFromCwd(): string | undefined {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
function isOutboundProxySourceDir(dir: string): boolean {
|
|
83
|
-
const pkgPath = join(dir, "package.json");
|
|
84
|
-
if (!existsSync(pkgPath) || !existsSync(join(dir, "src", "main.ts")))
|
|
85
|
-
return false;
|
|
86
|
-
try {
|
|
87
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
88
|
-
return pkg.name === "@vellumai/outbound-proxy";
|
|
89
|
-
} catch {
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
82
|
function resolveAssistantIndexPath(): string | undefined {
|
|
95
83
|
// Source tree layout: cli/src/lib/ -> ../../.. -> repo root -> assistant/src/index.ts
|
|
96
84
|
const sourceTreeIndex = join(
|
|
@@ -161,6 +149,63 @@ async function waitForSocketFile(
|
|
|
161
149
|
return existsSync(socketPath);
|
|
162
150
|
}
|
|
163
151
|
|
|
152
|
+
function ensureBunInstalled(): void {
|
|
153
|
+
const bunBinDir = join(homedir(), ".bun", "bin");
|
|
154
|
+
const pathWithBun = [
|
|
155
|
+
bunBinDir,
|
|
156
|
+
process.env.PATH || "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",
|
|
157
|
+
].join(":");
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
execFileSync("bun", ["--version"], {
|
|
161
|
+
stdio: "pipe",
|
|
162
|
+
env: { ...process.env, PATH: pathWithBun },
|
|
163
|
+
});
|
|
164
|
+
return;
|
|
165
|
+
} catch {
|
|
166
|
+
// bun not found, try to install
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
console.log(" Installing bun...");
|
|
170
|
+
try {
|
|
171
|
+
const installEnv: Record<string, string> = {
|
|
172
|
+
HOME: process.env.HOME || homedir(),
|
|
173
|
+
PATH: process.env.PATH || "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",
|
|
174
|
+
TMPDIR: process.env.TMPDIR || "/tmp",
|
|
175
|
+
USER: process.env.USER || "",
|
|
176
|
+
LANG: process.env.LANG || "",
|
|
177
|
+
};
|
|
178
|
+
// Preserve proxy/TLS env vars so curl works in proxied/corporate environments
|
|
179
|
+
for (const key of [
|
|
180
|
+
"HTTP_PROXY",
|
|
181
|
+
"http_proxy",
|
|
182
|
+
"HTTPS_PROXY",
|
|
183
|
+
"https_proxy",
|
|
184
|
+
"ALL_PROXY",
|
|
185
|
+
"all_proxy",
|
|
186
|
+
"NO_PROXY",
|
|
187
|
+
"no_proxy",
|
|
188
|
+
"SSL_CERT_FILE",
|
|
189
|
+
"SSL_CERT_DIR",
|
|
190
|
+
"CURL_CA_BUNDLE",
|
|
191
|
+
]) {
|
|
192
|
+
if (process.env[key]) {
|
|
193
|
+
installEnv[key] = process.env[key]!;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
execSync("curl -fsSL https://bun.sh/install | bash", {
|
|
197
|
+
stdio: "pipe",
|
|
198
|
+
timeout: 60_000,
|
|
199
|
+
env: installEnv,
|
|
200
|
+
});
|
|
201
|
+
console.log(" Bun installed successfully");
|
|
202
|
+
} catch {
|
|
203
|
+
console.log(
|
|
204
|
+
" ⚠️ Failed to install bun — some features may be unavailable",
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
164
209
|
function resolveDaemonMainPath(assistantIndex: string): string {
|
|
165
210
|
return join(dirname(assistantIndex), "daemon", "main.ts");
|
|
166
211
|
}
|
|
@@ -354,21 +399,6 @@ function resolveGatewayDir(): string {
|
|
|
354
399
|
}
|
|
355
400
|
}
|
|
356
401
|
|
|
357
|
-
function resolveOutboundProxyDir(): string | undefined {
|
|
358
|
-
// Compiled binary: outbound-proxy/ bundled adjacent to the CLI executable.
|
|
359
|
-
const binProxy = join(dirname(process.execPath), "outbound-proxy");
|
|
360
|
-
if (isOutboundProxySourceDir(binProxy)) {
|
|
361
|
-
return binProxy;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
try {
|
|
365
|
-
const pkgPath = _require.resolve("@vellumai/outbound-proxy/package.json");
|
|
366
|
-
return dirname(pkgPath);
|
|
367
|
-
} catch {
|
|
368
|
-
return undefined;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
402
|
function normalizeIngressUrl(value: unknown): string | undefined {
|
|
373
403
|
if (typeof value !== "string") return undefined;
|
|
374
404
|
const normalized = value.trim().replace(/\/+$/, "");
|
|
@@ -632,6 +662,9 @@ export async function startLocalDaemon(watch: boolean = false): Promise<void> {
|
|
|
632
662
|
} else {
|
|
633
663
|
console.log(" Assistant socket is responsive — skipping restart\n");
|
|
634
664
|
}
|
|
665
|
+
// Ensure bun is available for runtime features (browser, skills install)
|
|
666
|
+
// even when reusing an existing daemon.
|
|
667
|
+
ensureBunInstalled();
|
|
635
668
|
return;
|
|
636
669
|
}
|
|
637
670
|
|
|
@@ -642,6 +675,9 @@ export async function startLocalDaemon(watch: boolean = false): Promise<void> {
|
|
|
642
675
|
|
|
643
676
|
console.log("🔨 Starting assistant...");
|
|
644
677
|
|
|
678
|
+
// Ensure bun is available for runtime features (browser, skills install)
|
|
679
|
+
ensureBunInstalled();
|
|
680
|
+
|
|
645
681
|
// Ensure ~/.vellum/ exists for PID/socket files
|
|
646
682
|
mkdirSync(vellumDir, { recursive: true });
|
|
647
683
|
|
|
@@ -649,10 +685,12 @@ export async function startLocalDaemon(watch: boolean = false): Promise<void> {
|
|
|
649
685
|
// macOS app the CLI inherits a huge environment (XPC_SERVICE_NAME,
|
|
650
686
|
// __CFBundleIdentifier, CLAUDE_CODE_ENTRYPOINT, etc.) that can cause
|
|
651
687
|
// the daemon to take 50+ seconds to start instead of ~1s.
|
|
688
|
+
const bunBinDir = join(homedir(), ".bun", "bin");
|
|
689
|
+
const basePath =
|
|
690
|
+
process.env.PATH || "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin";
|
|
652
691
|
const daemonEnv: Record<string, string> = {
|
|
653
692
|
HOME: process.env.HOME || homedir(),
|
|
654
|
-
PATH:
|
|
655
|
-
process.env.PATH || "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",
|
|
693
|
+
PATH: `${bunBinDir}:${basePath}`,
|
|
656
694
|
VELLUM_DAEMON_TCP_ENABLED: "1",
|
|
657
695
|
};
|
|
658
696
|
// Forward optional config env vars the daemon may need
|
|
@@ -674,14 +712,18 @@ export async function startLocalDaemon(watch: boolean = false): Promise<void> {
|
|
|
674
712
|
}
|
|
675
713
|
}
|
|
676
714
|
|
|
715
|
+
// Use fd inheritance instead of pipes so the daemon's stdout/stderr
|
|
716
|
+
// survive after the parent (hatch) exits. Bun does not ignore SIGPIPE,
|
|
717
|
+
// so piped stdio would kill the daemon on its first write after the
|
|
718
|
+
// parent closes.
|
|
677
719
|
const daemonLogFd = openLogFile("hatch.log");
|
|
678
720
|
const child = spawn(daemonBinary, [], {
|
|
679
721
|
cwd: dirname(daemonBinary),
|
|
680
722
|
detached: true,
|
|
681
|
-
stdio: ["ignore",
|
|
723
|
+
stdio: ["ignore", daemonLogFd, daemonLogFd],
|
|
682
724
|
env: daemonEnv,
|
|
683
725
|
});
|
|
684
|
-
|
|
726
|
+
if (typeof daemonLogFd === "number") closeSync(daemonLogFd);
|
|
685
727
|
child.unref();
|
|
686
728
|
const daemonPid = child.pid;
|
|
687
729
|
|
|
@@ -692,6 +734,13 @@ export async function startLocalDaemon(watch: boolean = false): Promise<void> {
|
|
|
692
734
|
}
|
|
693
735
|
}
|
|
694
736
|
|
|
737
|
+
// Ensure bun is available for runtime features (browser, skills install)
|
|
738
|
+
// Runs after daemon-reuse checks so the fast attach path is not blocked
|
|
739
|
+
// by a potentially slow bun install when the daemon is already alive.
|
|
740
|
+
if (daemonAlive) {
|
|
741
|
+
ensureBunInstalled();
|
|
742
|
+
}
|
|
743
|
+
|
|
695
744
|
// Wait for socket at ~/.vellum/vellum.sock (up to 60s — fresh installs
|
|
696
745
|
// may need 30-60s for Qdrant download, migrations, and first-time init)
|
|
697
746
|
let socketReady = await waitForSocketFile(socketFile, 60000);
|
|
@@ -848,6 +897,11 @@ export async function startGateway(
|
|
|
848
897
|
GATEWAY_RUNTIME_PROXY_REQUIRE_AUTH: "true",
|
|
849
898
|
RUNTIME_PROXY_BEARER_TOKEN: runtimeProxyBearerToken,
|
|
850
899
|
RUNTIME_HTTP_PORT: process.env.RUNTIME_HTTP_PORT || "7821",
|
|
900
|
+
// Skip the drain window for locally-launched gateways — there is no load
|
|
901
|
+
// balancer draining connections, so waiting serves no purpose and causes
|
|
902
|
+
// `vellum sleep` to SIGKILL the gateway when the CLI timeout is shorter
|
|
903
|
+
// than the drain window. Respect an explicit env override.
|
|
904
|
+
GATEWAY_SHUTDOWN_DRAIN_MS: process.env.GATEWAY_SHUTDOWN_DRAIN_MS || "0",
|
|
851
905
|
};
|
|
852
906
|
|
|
853
907
|
if (process.env.GATEWAY_UNMAPPED_POLICY) {
|
|
@@ -883,13 +937,15 @@ export async function startGateway(
|
|
|
883
937
|
);
|
|
884
938
|
}
|
|
885
939
|
|
|
940
|
+
// Use fd inheritance (not pipes) so the gateway survives after the
|
|
941
|
+
// hatch CLI exits — Bun does not ignore SIGPIPE.
|
|
886
942
|
const gatewayLogFd = openLogFile("hatch.log");
|
|
887
943
|
gateway = spawn(gatewayBinary, [], {
|
|
888
944
|
detached: true,
|
|
889
|
-
stdio: ["ignore",
|
|
945
|
+
stdio: ["ignore", gatewayLogFd, gatewayLogFd],
|
|
890
946
|
env: gatewayEnv,
|
|
891
947
|
});
|
|
892
|
-
|
|
948
|
+
if (typeof gatewayLogFd === "number") closeSync(gatewayLogFd);
|
|
893
949
|
} else {
|
|
894
950
|
// Source tree / bunx: resolve the gateway source directory and run via bun.
|
|
895
951
|
const gatewayDir = resolveGatewayDir();
|
|
@@ -900,10 +956,10 @@ export async function startGateway(
|
|
|
900
956
|
gateway = spawn("bun", bunArgs, {
|
|
901
957
|
cwd: gatewayDir,
|
|
902
958
|
detached: true,
|
|
903
|
-
stdio: ["ignore",
|
|
959
|
+
stdio: ["ignore", gwLogFd, gwLogFd],
|
|
904
960
|
env: gatewayEnv,
|
|
905
961
|
});
|
|
906
|
-
|
|
962
|
+
if (typeof gwLogFd === "number") closeSync(gwLogFd);
|
|
907
963
|
if (watch) {
|
|
908
964
|
console.log(" Gateway started in watch mode (bun --watch)");
|
|
909
965
|
}
|
|
@@ -949,130 +1005,10 @@ export async function startGateway(
|
|
|
949
1005
|
return gatewayUrl;
|
|
950
1006
|
}
|
|
951
1007
|
|
|
952
|
-
export async function startOutboundProxy(
|
|
953
|
-
watch: boolean = false,
|
|
954
|
-
): Promise<void> {
|
|
955
|
-
const proxyDir = resolveOutboundProxyDir();
|
|
956
|
-
if (!proxyDir) {
|
|
957
|
-
console.log(" ⚠️ Outbound proxy not found — skipping");
|
|
958
|
-
return;
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
console.log("🔒 Starting outbound proxy...");
|
|
962
|
-
|
|
963
|
-
const vellumDir = join(homedir(), ".vellum");
|
|
964
|
-
mkdirSync(vellumDir, { recursive: true });
|
|
965
|
-
|
|
966
|
-
const pidFile = join(vellumDir, "outbound-proxy.pid");
|
|
967
|
-
|
|
968
|
-
// Check if already running
|
|
969
|
-
if (existsSync(pidFile)) {
|
|
970
|
-
try {
|
|
971
|
-
const pid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
|
|
972
|
-
if (!isNaN(pid)) {
|
|
973
|
-
try {
|
|
974
|
-
process.kill(pid, 0);
|
|
975
|
-
console.log(` Outbound proxy already running (pid ${pid})\n`);
|
|
976
|
-
return;
|
|
977
|
-
} catch {
|
|
978
|
-
try {
|
|
979
|
-
unlinkSync(pidFile);
|
|
980
|
-
} catch {}
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
} catch {}
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
const proxyEnv: Record<string, string> = {
|
|
987
|
-
...(process.env as Record<string, string>),
|
|
988
|
-
PROXY_PORT: process.env.PROXY_PORT || "7829",
|
|
989
|
-
PROXY_HEALTH_PORT: process.env.PROXY_HEALTH_PORT || "7828",
|
|
990
|
-
};
|
|
991
|
-
|
|
992
|
-
const proxyLogFd = openLogFile("hatch.log");
|
|
993
|
-
|
|
994
|
-
let proxy;
|
|
995
|
-
if (process.env.VELLUM_DESKTOP_APP && !watch) {
|
|
996
|
-
const proxyBinary = join(
|
|
997
|
-
dirname(process.execPath),
|
|
998
|
-
"vellum-outbound-proxy",
|
|
999
|
-
);
|
|
1000
|
-
if (!existsSync(proxyBinary)) {
|
|
1001
|
-
console.log(
|
|
1002
|
-
" ⚠️ Outbound proxy binary not found — falling back to source",
|
|
1003
|
-
);
|
|
1004
|
-
const bunArgs = watch
|
|
1005
|
-
? ["--watch", "run", "src/main.ts"]
|
|
1006
|
-
: ["run", "src/main.ts"];
|
|
1007
|
-
proxy = spawn("bun", bunArgs, {
|
|
1008
|
-
cwd: proxyDir,
|
|
1009
|
-
detached: true,
|
|
1010
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1011
|
-
env: proxyEnv,
|
|
1012
|
-
});
|
|
1013
|
-
} else {
|
|
1014
|
-
proxy = spawn(proxyBinary, [], {
|
|
1015
|
-
detached: true,
|
|
1016
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1017
|
-
env: proxyEnv,
|
|
1018
|
-
});
|
|
1019
|
-
}
|
|
1020
|
-
} else {
|
|
1021
|
-
const bunArgs = watch
|
|
1022
|
-
? ["--watch", "run", "src/main.ts"]
|
|
1023
|
-
: ["run", "src/main.ts"];
|
|
1024
|
-
proxy = spawn("bun", bunArgs, {
|
|
1025
|
-
cwd: proxyDir,
|
|
1026
|
-
detached: true,
|
|
1027
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1028
|
-
env: proxyEnv,
|
|
1029
|
-
});
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
pipeToLogFile(proxy, proxyLogFd, "outbound-proxy");
|
|
1033
|
-
proxy.unref();
|
|
1034
|
-
|
|
1035
|
-
if (proxy.pid) {
|
|
1036
|
-
writeFileSync(pidFile, String(proxy.pid), "utf-8");
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
if (watch) {
|
|
1040
|
-
console.log(" Outbound proxy started in watch mode (bun --watch)");
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
// Wait for the health endpoint to respond
|
|
1044
|
-
const healthPort = Number(process.env.PROXY_HEALTH_PORT) || 7828;
|
|
1045
|
-
const start = Date.now();
|
|
1046
|
-
const timeoutMs = 15000;
|
|
1047
|
-
let ready = false;
|
|
1048
|
-
while (Date.now() - start < timeoutMs) {
|
|
1049
|
-
try {
|
|
1050
|
-
const res = await fetch(`http://localhost:${healthPort}/healthz`, {
|
|
1051
|
-
signal: AbortSignal.timeout(2000),
|
|
1052
|
-
});
|
|
1053
|
-
if (res.ok) {
|
|
1054
|
-
ready = true;
|
|
1055
|
-
break;
|
|
1056
|
-
}
|
|
1057
|
-
} catch {
|
|
1058
|
-
// Not ready yet
|
|
1059
|
-
}
|
|
1060
|
-
await new Promise((r) => setTimeout(r, 250));
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
if (!ready) {
|
|
1064
|
-
console.warn(
|
|
1065
|
-
" ⚠️ Outbound proxy started but health check did not respond within 15s",
|
|
1066
|
-
);
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
console.log("✅ Outbound proxy started\n");
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
1008
|
/**
|
|
1073
|
-
* Stop any locally-running daemon
|
|
1074
|
-
*
|
|
1075
|
-
*
|
|
1009
|
+
* Stop any locally-running daemon and gateway processes and clean up
|
|
1010
|
+
* PID/socket files. Called when hatch fails partway through so we don't
|
|
1011
|
+
* leave orphaned processes with no lock file entry.
|
|
1076
1012
|
*/
|
|
1077
1013
|
export async function stopLocalProcesses(): Promise<void> {
|
|
1078
1014
|
const vellumDir = join(homedir(), ".vellum");
|
|
@@ -1081,8 +1017,5 @@ export async function stopLocalProcesses(): Promise<void> {
|
|
|
1081
1017
|
await stopProcessByPidFile(daemonPidFile, "daemon", [socketFile]);
|
|
1082
1018
|
|
|
1083
1019
|
const gatewayPidFile = join(vellumDir, "gateway.pid");
|
|
1084
|
-
await stopProcessByPidFile(gatewayPidFile, "gateway");
|
|
1085
|
-
|
|
1086
|
-
const outboundProxyPidFile = join(vellumDir, "outbound-proxy.pid");
|
|
1087
|
-
await stopProcessByPidFile(outboundProxyPidFile, "outbound-proxy");
|
|
1020
|
+
await stopProcessByPidFile(gatewayPidFile, "gateway", undefined, 7000);
|
|
1088
1021
|
}
|
package/src/lib/process.ts
CHANGED
|
@@ -45,12 +45,13 @@ export function isProcessAlive(pidFile: string): {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
|
-
* Stop a process by PID: SIGTERM, wait up to
|
|
48
|
+
* Stop a process by PID: SIGTERM, wait up to `timeoutMs`, then SIGKILL if still alive.
|
|
49
49
|
* Returns true if the process was stopped, false if it wasn't alive.
|
|
50
50
|
*/
|
|
51
51
|
export async function stopProcess(
|
|
52
52
|
pid: number,
|
|
53
53
|
label: string,
|
|
54
|
+
timeoutMs: number = 2000,
|
|
54
55
|
): Promise<boolean> {
|
|
55
56
|
try {
|
|
56
57
|
process.kill(pid, 0);
|
|
@@ -61,7 +62,7 @@ export async function stopProcess(
|
|
|
61
62
|
console.log(`Stopping ${label} (pid ${pid})...`);
|
|
62
63
|
process.kill(pid, "SIGTERM");
|
|
63
64
|
|
|
64
|
-
const deadline = Date.now() +
|
|
65
|
+
const deadline = Date.now() + timeoutMs;
|
|
65
66
|
while (Date.now() < deadline) {
|
|
66
67
|
try {
|
|
67
68
|
process.kill(pid, 0);
|
|
@@ -90,6 +91,7 @@ export async function stopProcessByPidFile(
|
|
|
90
91
|
pidFile: string,
|
|
91
92
|
label: string,
|
|
92
93
|
cleanupFiles?: string[],
|
|
94
|
+
timeoutMs?: number,
|
|
93
95
|
): Promise<boolean> {
|
|
94
96
|
const { alive, pid } = isProcessAlive(pidFile);
|
|
95
97
|
|
|
@@ -129,7 +131,7 @@ export async function stopProcessByPidFile(
|
|
|
129
131
|
return false;
|
|
130
132
|
}
|
|
131
133
|
|
|
132
|
-
const stopped = await stopProcess(pid, label);
|
|
134
|
+
const stopped = await stopProcess(pid, label, timeoutMs);
|
|
133
135
|
|
|
134
136
|
try {
|
|
135
137
|
unlinkSync(pidFile);
|
package/src/lib/step-runner.ts
CHANGED
|
@@ -1,46 +1,5 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
2
|
|
|
3
|
-
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
4
|
-
|
|
5
|
-
interface Step {
|
|
6
|
-
name: string;
|
|
7
|
-
run: () => Promise<void>;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function clearLine(): void {
|
|
11
|
-
process.stdout.write("\r\x1b[K");
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function showSpinner(name: string): NodeJS.Timeout {
|
|
15
|
-
let frameIndex = 0;
|
|
16
|
-
return setInterval(() => {
|
|
17
|
-
clearLine();
|
|
18
|
-
process.stdout.write(` ${SPINNER_FRAMES[frameIndex]} ${name}...`);
|
|
19
|
-
frameIndex = (frameIndex + 1) % SPINNER_FRAMES.length;
|
|
20
|
-
}, 80);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export async function runSteps(steps: Step[]): Promise<void> {
|
|
24
|
-
for (let i = 0; i < steps.length; i++) {
|
|
25
|
-
const step = steps[i];
|
|
26
|
-
const label = `[${i + 1}/${steps.length}] ${step.name}`;
|
|
27
|
-
|
|
28
|
-
const spinner = showSpinner(label);
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
await step.run();
|
|
32
|
-
clearInterval(spinner);
|
|
33
|
-
clearLine();
|
|
34
|
-
console.log(` ✔ ${label}`);
|
|
35
|
-
} catch (error) {
|
|
36
|
-
clearInterval(spinner);
|
|
37
|
-
clearLine();
|
|
38
|
-
console.log(` ✖ ${label}`);
|
|
39
|
-
throw error;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
3
|
export function exec(
|
|
45
4
|
command: string,
|
|
46
5
|
args: string[],
|