@vellumai/cli 0.1.1 → 0.1.3
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/README.md +97 -0
- package/package.json +1 -1
- package/src/adapters/openclaw.ts +7 -0
- package/src/commands/hatch.ts +159 -133
- package/src/commands/retire.ts +150 -0
- package/src/index.ts +4 -1
- package/src/lib/assistant-config.ts +95 -0
- package/src/lib/aws.ts +608 -0
- package/src/lib/gcp.ts +88 -15
package/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# @vellumai/cli
|
|
2
|
+
|
|
3
|
+
CLI tools for provisioning and managing Vellum assistant instances.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
This package is used internally by the [`vel`](https://github.com/vellum-ai/vellum-assistant-platform/tree/main/vel) CLI. You typically don't need to install it directly.
|
|
8
|
+
|
|
9
|
+
To run it standalone with [Bun](https://bun.sh):
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun run ./src/index.ts <command> [options]
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Commands
|
|
16
|
+
|
|
17
|
+
### `hatch`
|
|
18
|
+
|
|
19
|
+
Provision a new assistant instance and bootstrap the Vellum runtime on it.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
vellum-cli hatch [species] [options]
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
#### Species
|
|
26
|
+
|
|
27
|
+
| Species | Description |
|
|
28
|
+
| ---------- | ------------------------------------------- |
|
|
29
|
+
| `vellum` | Default. Provisions the Vellum assistant runtime. |
|
|
30
|
+
| `openclaw` | Provisions the OpenClaw runtime with gateway. |
|
|
31
|
+
|
|
32
|
+
#### Options
|
|
33
|
+
|
|
34
|
+
| Option | Description |
|
|
35
|
+
| ------------------- | ----------- |
|
|
36
|
+
| `-d` | Detached mode. Start the instance in the background without watching startup progress. |
|
|
37
|
+
| `--name <name>` | Use a specific instance name instead of an auto-generated one. |
|
|
38
|
+
| `--remote <target>` | Where to provision the instance. One of: `local`, `gcp`, `aws`, `custom`. Defaults to `local`. |
|
|
39
|
+
|
|
40
|
+
#### Remote Targets
|
|
41
|
+
|
|
42
|
+
- **`local`** -- Starts a local daemon on your machine via `bunx vellum daemon start`.
|
|
43
|
+
- **`gcp`** -- Creates a GCP Compute Engine VM (`e2-standard-4`: 4 vCPUs, 16 GB) with a startup script that bootstraps the assistant. Requires `gcloud` authentication and `GCP_PROJECT` / `GCP_DEFAULT_ZONE` environment variables.
|
|
44
|
+
- **`aws`** -- Provisions an AWS instance.
|
|
45
|
+
- **`custom`** -- Provisions on an arbitrary SSH host. Set `VELLUM_CUSTOM_HOST` (e.g. `user@hostname`) to specify the target.
|
|
46
|
+
|
|
47
|
+
#### Environment Variables
|
|
48
|
+
|
|
49
|
+
| Variable | Required For | Description |
|
|
50
|
+
| --------------------- | ------------ | ----------- |
|
|
51
|
+
| `ANTHROPIC_API_KEY` | All | Anthropic API key passed to the assistant runtime. |
|
|
52
|
+
| `GCP_PROJECT` | `gcp` | GCP project ID. Falls back to the active `gcloud` project. |
|
|
53
|
+
| `GCP_DEFAULT_ZONE` | `gcp` | GCP zone for the compute instance. |
|
|
54
|
+
| `VELLUM_CUSTOM_HOST` | `custom` | SSH host in `user@hostname` format. |
|
|
55
|
+
|
|
56
|
+
#### Examples
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# Hatch a local assistant (default)
|
|
60
|
+
vellum-cli hatch
|
|
61
|
+
|
|
62
|
+
# Hatch a vellum assistant on GCP
|
|
63
|
+
vellum-cli hatch vellum --remote gcp
|
|
64
|
+
|
|
65
|
+
# Hatch an openclaw assistant on GCP in detached mode
|
|
66
|
+
vellum-cli hatch openclaw --remote gcp -d
|
|
67
|
+
|
|
68
|
+
# Hatch with a specific instance name
|
|
69
|
+
vellum-cli hatch --name my-assistant --remote gcp
|
|
70
|
+
|
|
71
|
+
# Hatch on a custom SSH host
|
|
72
|
+
VELLUM_CUSTOM_HOST=user@10.0.0.1 vellum-cli hatch --remote custom
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
When hatching on GCP in interactive mode (without `-d`), the CLI displays an animated progress TUI that polls the instance's startup script output in real time. Press `Ctrl+C` to detach -- the instance will continue running in the background.
|
|
76
|
+
|
|
77
|
+
### `retire`
|
|
78
|
+
|
|
79
|
+
Delete a provisioned assistant instance. The cloud provider and connection details are automatically resolved from the saved assistant config (written during `hatch`).
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
vellum-cli retire <name>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The CLI looks up the instance by name in `~/.vellum.lock.json` and determines how to retire it based on the saved `cloud` field:
|
|
86
|
+
|
|
87
|
+
- **`gcp`** -- Deletes the GCP Compute Engine instance via `gcloud compute instances delete`.
|
|
88
|
+
- **`aws`** -- Terminates the AWS EC2 instance by looking up the instance ID from its Name tag.
|
|
89
|
+
- **`local`** -- Stops the local daemon (`bunx vellum daemon stop`) and removes the `~/.vellum` directory.
|
|
90
|
+
- **`custom`** -- SSHs to the remote host to stop the daemon/gateway and remove the `~/.vellum` directory.
|
|
91
|
+
|
|
92
|
+
#### Examples
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# Retire an instance (cloud type resolved from config)
|
|
96
|
+
vellum-cli retire my-assistant
|
|
97
|
+
```
|
package/package.json
CHANGED
package/src/adapters/openclaw.ts
CHANGED
|
@@ -97,7 +97,14 @@ else
|
|
|
97
97
|
echo "bun already installed: $(bun --version)"
|
|
98
98
|
fi
|
|
99
99
|
|
|
100
|
+
set +e
|
|
100
101
|
openclaw gateway install --token ${bearerToken}
|
|
102
|
+
GATEWAY_INSTALL_EXIT=\$?
|
|
103
|
+
set -e
|
|
104
|
+
|
|
105
|
+
if [ \$GATEWAY_INSTALL_EXIT -ne 0 ]; then
|
|
106
|
+
echo "WARN: openclaw gateway install exited with \$GATEWAY_INSTALL_EXIT (expected systemd mismatch), continuing with user-level systemd setup"
|
|
107
|
+
fi
|
|
101
108
|
|
|
102
109
|
mkdir -p /root/.openclaw
|
|
103
110
|
openclaw config set env.ANTHROPIC_API_KEY "${anthropicApiKey}"
|
package/src/commands/hatch.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
2
|
import { randomBytes } from "crypto";
|
|
3
|
-
import { existsSync,
|
|
4
|
-
import {
|
|
5
|
-
import { join } from "path";
|
|
3
|
+
import { existsSync, unlinkSync, writeFileSync } from "fs";
|
|
4
|
+
import { tmpdir, userInfo } from "os";
|
|
5
|
+
import { dirname, join } from "path";
|
|
6
6
|
|
|
7
7
|
import { buildOpenclawStartupScript } from "../adapters/openclaw";
|
|
8
|
+
import { saveAssistantEntry } from "../lib/assistant-config";
|
|
9
|
+
import type { AssistantEntry } from "../lib/assistant-config";
|
|
10
|
+
import { hatchAws } from "../lib/aws";
|
|
8
11
|
import {
|
|
9
12
|
FIREWALL_TAG,
|
|
10
13
|
GATEWAY_PORT,
|
|
@@ -19,11 +22,13 @@ import { buildInterfacesSeed } from "../lib/interfaces-seed";
|
|
|
19
22
|
import { generateRandomSuffix } from "../lib/random-name";
|
|
20
23
|
import { exec, execOutput } from "../lib/step-runner";
|
|
21
24
|
|
|
22
|
-
const DEFAULT_ZONE = "us-central1-a";
|
|
23
25
|
const INSTALL_SCRIPT_REMOTE_PATH = "/tmp/vellum-install.sh";
|
|
24
26
|
const INSTALL_SCRIPT_PATH = join(import.meta.dir, "..", "adapters", "install.sh");
|
|
25
27
|
const MACHINE_TYPE = "e2-standard-4"; // 4 vCPUs, 16 GB memory
|
|
26
|
-
const HATCH_TIMEOUT_MS
|
|
28
|
+
const HATCH_TIMEOUT_MS: Record<Species, number> = {
|
|
29
|
+
vellum: 2 * 60 * 1000,
|
|
30
|
+
openclaw: 10 * 60 * 1000,
|
|
31
|
+
};
|
|
27
32
|
const DEFAULT_SPECIES: Species = "vellum";
|
|
28
33
|
|
|
29
34
|
const DESIRED_FIREWALL_RULES: FirewallRuleSpec[] = [
|
|
@@ -71,12 +76,13 @@ chown -R "$SSH_USER:$SSH_USER" "$SSH_USER_HOME" 2>/dev/null || true
|
|
|
71
76
|
`;
|
|
72
77
|
}
|
|
73
78
|
|
|
74
|
-
function buildStartupScript(
|
|
79
|
+
export function buildStartupScript(
|
|
75
80
|
species: Species,
|
|
76
81
|
bearerToken: string,
|
|
77
82
|
sshUser: string,
|
|
78
83
|
anthropicApiKey: string,
|
|
79
84
|
): string {
|
|
85
|
+
const platformUrl = process.env.VELLUM_ASSISTANT_PLATFORM_URL ?? "https://assistant.vellum.ai";
|
|
80
86
|
const timestampRedirect = buildTimestampRedirect();
|
|
81
87
|
const userSetup = buildUserSetup(sshUser);
|
|
82
88
|
const ownershipFixup = buildOwnershipFixup();
|
|
@@ -125,7 +131,7 @@ CONFIG_EOF
|
|
|
125
131
|
${ownershipFixup}
|
|
126
132
|
|
|
127
133
|
export VELLUM_SSH_USER="\$SSH_USER"
|
|
128
|
-
curl -fsSL
|
|
134
|
+
curl -fsSL ${platformUrl}/install.sh -o ${INSTALL_SCRIPT_REMOTE_PATH}
|
|
129
135
|
chmod +x ${INSTALL_SCRIPT_REMOTE_PATH}
|
|
130
136
|
source ${INSTALL_SCRIPT_REMOTE_PATH}
|
|
131
137
|
`;
|
|
@@ -182,7 +188,7 @@ function parseArgs(): HatchArgs {
|
|
|
182
188
|
return { species, detached, name, remote };
|
|
183
189
|
}
|
|
184
190
|
|
|
185
|
-
interface PollResult {
|
|
191
|
+
export interface PollResult {
|
|
186
192
|
lastLine: string | null;
|
|
187
193
|
done: boolean;
|
|
188
194
|
failed: boolean;
|
|
@@ -192,6 +198,7 @@ async function pollInstance(
|
|
|
192
198
|
instanceName: string,
|
|
193
199
|
project: string,
|
|
194
200
|
zone: string,
|
|
201
|
+
account?: string,
|
|
195
202
|
): Promise<PollResult> {
|
|
196
203
|
try {
|
|
197
204
|
const remoteCmd =
|
|
@@ -199,7 +206,7 @@ async function pollInstance(
|
|
|
199
206
|
"S=$(systemctl is-active google-startup-scripts.service 2>/dev/null || true); " +
|
|
200
207
|
"E=$(cat /var/log/startup-error 2>/dev/null || true); " +
|
|
201
208
|
'printf "%s\\n===HATCH_SEP===\\n%s\\n===HATCH_ERR===\\n%s" "$L" "$S" "$E"';
|
|
202
|
-
const
|
|
209
|
+
const args = [
|
|
203
210
|
"compute",
|
|
204
211
|
"ssh",
|
|
205
212
|
instanceName,
|
|
@@ -211,7 +218,9 @@ async function pollInstance(
|
|
|
211
218
|
"--ssh-flag=-o ConnectTimeout=10",
|
|
212
219
|
"--ssh-flag=-o LogLevel=ERROR",
|
|
213
220
|
`--command=${remoteCmd}`,
|
|
214
|
-
]
|
|
221
|
+
];
|
|
222
|
+
if (account) args.push(`--account=${account}`);
|
|
223
|
+
const output = await execOutput("gcloud", args);
|
|
215
224
|
const sepIdx = output.indexOf("===HATCH_SEP===");
|
|
216
225
|
if (sepIdx === -1) {
|
|
217
226
|
return { lastLine: output.trim() || null, done: false, failed: false };
|
|
@@ -253,9 +262,10 @@ async function checkCurlFailure(
|
|
|
253
262
|
instanceName: string,
|
|
254
263
|
project: string,
|
|
255
264
|
zone: string,
|
|
265
|
+
account?: string,
|
|
256
266
|
): Promise<boolean> {
|
|
257
267
|
try {
|
|
258
|
-
const
|
|
268
|
+
const args = [
|
|
259
269
|
"compute",
|
|
260
270
|
"ssh",
|
|
261
271
|
instanceName,
|
|
@@ -267,7 +277,9 @@ async function checkCurlFailure(
|
|
|
267
277
|
"--ssh-flag=-o ConnectTimeout=10",
|
|
268
278
|
"--ssh-flag=-o LogLevel=ERROR",
|
|
269
279
|
`--command=test -s ${INSTALL_SCRIPT_REMOTE_PATH} && echo EXISTS || echo MISSING`,
|
|
270
|
-
]
|
|
280
|
+
];
|
|
281
|
+
if (account) args.push(`--account=${account}`);
|
|
282
|
+
const output = await execOutput("gcloud", args);
|
|
271
283
|
return output.trim() === "MISSING";
|
|
272
284
|
} catch {
|
|
273
285
|
return false;
|
|
@@ -279,36 +291,40 @@ async function recoverFromCurlFailure(
|
|
|
279
291
|
project: string,
|
|
280
292
|
zone: string,
|
|
281
293
|
sshUser: string,
|
|
294
|
+
account?: string,
|
|
282
295
|
): Promise<void> {
|
|
283
296
|
if (!existsSync(INSTALL_SCRIPT_PATH)) {
|
|
284
297
|
throw new Error(`Install script not found at ${INSTALL_SCRIPT_PATH}`);
|
|
285
298
|
}
|
|
286
299
|
|
|
287
|
-
|
|
288
|
-
await exec("gcloud", [
|
|
300
|
+
const scpArgs = [
|
|
289
301
|
"compute",
|
|
290
302
|
"scp",
|
|
291
303
|
INSTALL_SCRIPT_PATH,
|
|
292
304
|
`${instanceName}:${INSTALL_SCRIPT_REMOTE_PATH}`,
|
|
293
305
|
`--zone=${zone}`,
|
|
294
306
|
`--project=${project}`,
|
|
295
|
-
]
|
|
307
|
+
];
|
|
308
|
+
if (account) scpArgs.push(`--account=${account}`);
|
|
309
|
+
console.log("📋 Uploading install script to instance...");
|
|
310
|
+
await exec("gcloud", scpArgs);
|
|
296
311
|
|
|
297
|
-
|
|
298
|
-
await exec("gcloud", [
|
|
312
|
+
const sshArgs = [
|
|
299
313
|
"compute",
|
|
300
314
|
"ssh",
|
|
301
315
|
`${sshUser}@${instanceName}`,
|
|
302
316
|
`--zone=${zone}`,
|
|
303
317
|
`--project=${project}`,
|
|
304
318
|
`--command=source ${INSTALL_SCRIPT_REMOTE_PATH}`,
|
|
305
|
-
]
|
|
319
|
+
];
|
|
320
|
+
if (account) sshArgs.push(`--account=${account}`);
|
|
321
|
+
console.log("🔧 Running install script on instance...");
|
|
322
|
+
await exec("gcloud", sshArgs);
|
|
306
323
|
}
|
|
307
324
|
|
|
308
|
-
async function watchHatching(
|
|
325
|
+
export async function watchHatching(
|
|
326
|
+
pollFn: () => Promise<PollResult>,
|
|
309
327
|
instanceName: string,
|
|
310
|
-
project: string,
|
|
311
|
-
zone: string,
|
|
312
328
|
startTime: number,
|
|
313
329
|
species: Species,
|
|
314
330
|
): Promise<boolean> {
|
|
@@ -362,7 +378,7 @@ async function watchHatching(
|
|
|
362
378
|
if (pollInFlight || finished) return;
|
|
363
379
|
pollInFlight = true;
|
|
364
380
|
try {
|
|
365
|
-
const result = await
|
|
381
|
+
const result = await pollFn();
|
|
366
382
|
if (result.lastLine) {
|
|
367
383
|
lastLogLine = result.lastLine;
|
|
368
384
|
}
|
|
@@ -386,7 +402,7 @@ async function watchHatching(
|
|
|
386
402
|
}
|
|
387
403
|
|
|
388
404
|
const elapsed = Date.now() - startTime;
|
|
389
|
-
if (elapsed >= HATCH_TIMEOUT_MS) {
|
|
405
|
+
if (elapsed >= HATCH_TIMEOUT_MS[species]) {
|
|
390
406
|
clearInterval(interval);
|
|
391
407
|
console.log("");
|
|
392
408
|
console.log(` ⏰ Timed out after ${formatElapsed(elapsed)}. Instance is still running.`);
|
|
@@ -414,45 +430,6 @@ async function watchHatching(
|
|
|
414
430
|
});
|
|
415
431
|
}
|
|
416
432
|
|
|
417
|
-
interface CloudCredentials {
|
|
418
|
-
provider: string;
|
|
419
|
-
projectId?: string;
|
|
420
|
-
serviceAccountKey?: string;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
interface WorkspaceConfig {
|
|
424
|
-
cloudCredentials?: CloudCredentials;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
async function activateGcpCredentialsFromConfig(): Promise<void> {
|
|
428
|
-
const configPath = join(homedir(), ".vellum", "workspace", "config.json");
|
|
429
|
-
let config: WorkspaceConfig;
|
|
430
|
-
try {
|
|
431
|
-
config = JSON.parse(readFileSync(configPath, "utf8")) as WorkspaceConfig;
|
|
432
|
-
} catch {
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const creds = config.cloudCredentials;
|
|
437
|
-
if (!creds || creds.provider !== "gcp" || !creds.serviceAccountKey || !creds.projectId) {
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
const keyPath = join(tmpdir(), `vellum-sa-key-${Date.now()}.json`);
|
|
442
|
-
writeFileSync(keyPath, creds.serviceAccountKey);
|
|
443
|
-
try {
|
|
444
|
-
await exec("gcloud", [
|
|
445
|
-
"auth",
|
|
446
|
-
"activate-service-account",
|
|
447
|
-
`--key-file=${keyPath}`,
|
|
448
|
-
]);
|
|
449
|
-
await exec("gcloud", ["config", "set", "project", creds.projectId]);
|
|
450
|
-
} finally {
|
|
451
|
-
try {
|
|
452
|
-
unlinkSync(keyPath);
|
|
453
|
-
} catch {}
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
433
|
|
|
457
434
|
async function hatchGcp(
|
|
458
435
|
species: Species,
|
|
@@ -460,8 +437,8 @@ async function hatchGcp(
|
|
|
460
437
|
name: string | null,
|
|
461
438
|
): Promise<void> {
|
|
462
439
|
const startTime = Date.now();
|
|
440
|
+
const account = process.env.GCP_ACCOUNT_EMAIL;
|
|
463
441
|
try {
|
|
464
|
-
await activateGcpCredentialsFromConfig();
|
|
465
442
|
const project = process.env.GCP_PROJECT ?? (await getActiveProject());
|
|
466
443
|
let instanceName: string;
|
|
467
444
|
|
|
@@ -476,19 +453,25 @@ async function hatchGcp(
|
|
|
476
453
|
console.log(` Species: ${species}`);
|
|
477
454
|
console.log(` Cloud: GCP`);
|
|
478
455
|
console.log(` Project: ${project}`);
|
|
479
|
-
|
|
456
|
+
const zone = process.env.GCP_DEFAULT_ZONE;
|
|
457
|
+
if (!zone) {
|
|
458
|
+
console.error("Error: GCP_DEFAULT_ZONE environment variable is not set.");
|
|
459
|
+
process.exit(1);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
console.log(` Zone: ${zone}`);
|
|
480
463
|
console.log(` Machine type: ${MACHINE_TYPE}`);
|
|
481
464
|
console.log("");
|
|
482
465
|
|
|
483
466
|
if (name) {
|
|
484
|
-
if (await instanceExists(name, project,
|
|
467
|
+
if (await instanceExists(name, project, zone, account)) {
|
|
485
468
|
console.error(
|
|
486
469
|
`Error: Instance name '${name}' is already taken. Please choose a different name.`,
|
|
487
470
|
);
|
|
488
471
|
process.exit(1);
|
|
489
472
|
}
|
|
490
473
|
} else {
|
|
491
|
-
while (await instanceExists(instanceName, project,
|
|
474
|
+
while (await instanceExists(instanceName, project, zone, account)) {
|
|
492
475
|
console.log(`⚠️ Instance name ${instanceName} already exists, generating a new name...`);
|
|
493
476
|
const suffix = generateRandomSuffix();
|
|
494
477
|
instanceName = `${species}-${suffix}`;
|
|
@@ -508,13 +491,13 @@ async function hatchGcp(
|
|
|
508
491
|
|
|
509
492
|
console.log("🔨 Creating instance with startup script...");
|
|
510
493
|
try {
|
|
511
|
-
|
|
494
|
+
const createArgs = [
|
|
512
495
|
"compute",
|
|
513
496
|
"instances",
|
|
514
497
|
"create",
|
|
515
498
|
instanceName,
|
|
516
499
|
`--project=${project}`,
|
|
517
|
-
`--zone=${
|
|
500
|
+
`--zone=${zone}`,
|
|
518
501
|
`--machine-type=${MACHINE_TYPE}`,
|
|
519
502
|
"--image-family=debian-11",
|
|
520
503
|
"--image-project=debian-cloud",
|
|
@@ -523,7 +506,9 @@ async function hatchGcp(
|
|
|
523
506
|
`--metadata-from-file=startup-script=${startupScriptPath}`,
|
|
524
507
|
`--labels=species=${species},vellum-assistant=true`,
|
|
525
508
|
"--tags=vellum-assistant",
|
|
526
|
-
]
|
|
509
|
+
];
|
|
510
|
+
if (account) createArgs.push(`--account=${account}`);
|
|
511
|
+
await exec("gcloud", createArgs);
|
|
527
512
|
} finally {
|
|
528
513
|
try {
|
|
529
514
|
unlinkSync(startupScriptPath);
|
|
@@ -531,21 +516,23 @@ async function hatchGcp(
|
|
|
531
516
|
}
|
|
532
517
|
|
|
533
518
|
console.log("🔒 Syncing firewall rules...");
|
|
534
|
-
await syncFirewallRules(DESIRED_FIREWALL_RULES, project, FIREWALL_TAG);
|
|
519
|
+
await syncFirewallRules(DESIRED_FIREWALL_RULES, project, FIREWALL_TAG, account);
|
|
535
520
|
|
|
536
521
|
console.log(`✅ Instance ${instanceName} created successfully\n`);
|
|
537
522
|
|
|
538
523
|
let externalIp: string | null = null;
|
|
539
524
|
try {
|
|
540
|
-
const
|
|
525
|
+
const describeArgs = [
|
|
541
526
|
"compute",
|
|
542
527
|
"instances",
|
|
543
528
|
"describe",
|
|
544
529
|
instanceName,
|
|
545
530
|
`--project=${project}`,
|
|
546
|
-
`--zone=${
|
|
531
|
+
`--zone=${zone}`,
|
|
547
532
|
"--format=get(networkInterfaces[0].accessConfigs[0].natIP)",
|
|
548
|
-
]
|
|
533
|
+
];
|
|
534
|
+
if (account) describeArgs.push(`--account=${account}`);
|
|
535
|
+
const ipOutput = await execOutput("gcloud", describeArgs);
|
|
549
536
|
externalIp = ipOutput.trim() || null;
|
|
550
537
|
} catch {
|
|
551
538
|
console.log("⚠️ Could not retrieve external IP yet (instance may still be starting)");
|
|
@@ -554,22 +541,18 @@ async function hatchGcp(
|
|
|
554
541
|
const runtimeUrl = externalIp
|
|
555
542
|
? `http://${externalIp}:${GATEWAY_PORT}`
|
|
556
543
|
: `http://${instanceName}:${GATEWAY_PORT}`;
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
hatchedAt: new Date().toISOString(),
|
|
570
|
-
}),
|
|
571
|
-
);
|
|
572
|
-
}
|
|
544
|
+
const gcpEntry: AssistantEntry = {
|
|
545
|
+
assistantId: instanceName,
|
|
546
|
+
runtimeUrl,
|
|
547
|
+
bearerToken,
|
|
548
|
+
cloud: "gcp",
|
|
549
|
+
project,
|
|
550
|
+
zone,
|
|
551
|
+
species,
|
|
552
|
+
sshUser,
|
|
553
|
+
hatchedAt: new Date().toISOString(),
|
|
554
|
+
};
|
|
555
|
+
saveAssistantEntry(gcpEntry);
|
|
573
556
|
|
|
574
557
|
if (detached) {
|
|
575
558
|
console.log("🚀 Startup script is running on the instance...");
|
|
@@ -578,7 +561,7 @@ async function hatchGcp(
|
|
|
578
561
|
console.log("Instance details:");
|
|
579
562
|
console.log(` Name: ${instanceName}`);
|
|
580
563
|
console.log(` Project: ${project}`);
|
|
581
|
-
console.log(` Zone: ${
|
|
564
|
+
console.log(` Zone: ${zone}`);
|
|
582
565
|
if (externalIp) {
|
|
583
566
|
console.log(` External IP: ${externalIp}`);
|
|
584
567
|
}
|
|
@@ -588,9 +571,8 @@ async function hatchGcp(
|
|
|
588
571
|
console.log("");
|
|
589
572
|
|
|
590
573
|
const success = await watchHatching(
|
|
574
|
+
() => pollInstance(instanceName, project, zone, account),
|
|
591
575
|
instanceName,
|
|
592
|
-
project,
|
|
593
|
-
DEFAULT_ZONE,
|
|
594
576
|
startTime,
|
|
595
577
|
species,
|
|
596
578
|
);
|
|
@@ -598,11 +580,11 @@ async function hatchGcp(
|
|
|
598
580
|
if (!success) {
|
|
599
581
|
if (
|
|
600
582
|
species === "vellum" &&
|
|
601
|
-
(await checkCurlFailure(instanceName, project,
|
|
583
|
+
(await checkCurlFailure(instanceName, project, zone, account))
|
|
602
584
|
) {
|
|
603
585
|
console.log("");
|
|
604
586
|
console.log("🔄 Detected install script curl failure, attempting recovery...");
|
|
605
|
-
await recoverFromCurlFailure(instanceName, project,
|
|
587
|
+
await recoverFromCurlFailure(instanceName, project, zone, sshUser, account);
|
|
606
588
|
console.log("✅ Recovery successful!");
|
|
607
589
|
} else {
|
|
608
590
|
console.log("");
|
|
@@ -613,7 +595,7 @@ async function hatchGcp(
|
|
|
613
595
|
console.log("Instance details:");
|
|
614
596
|
console.log(` Name: ${instanceName}`);
|
|
615
597
|
console.log(` Project: ${project}`);
|
|
616
|
-
console.log(` Zone: ${
|
|
598
|
+
console.log(` Zone: ${zone}`);
|
|
617
599
|
if (externalIp) {
|
|
618
600
|
console.log(` External IP: ${externalIp}`);
|
|
619
601
|
}
|
|
@@ -703,20 +685,16 @@ async function hatchCustom(
|
|
|
703
685
|
}
|
|
704
686
|
|
|
705
687
|
const runtimeUrl = `http://${hostname}:${GATEWAY_PORT}`;
|
|
706
|
-
const
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
hatchedAt: new Date().toISOString(),
|
|
717
|
-
}),
|
|
718
|
-
);
|
|
719
|
-
}
|
|
688
|
+
const customEntry: AssistantEntry = {
|
|
689
|
+
assistantId: instanceName,
|
|
690
|
+
runtimeUrl,
|
|
691
|
+
bearerToken,
|
|
692
|
+
cloud: "custom",
|
|
693
|
+
species,
|
|
694
|
+
sshUser,
|
|
695
|
+
hatchedAt: new Date().toISOString(),
|
|
696
|
+
};
|
|
697
|
+
saveAssistantEntry(customEntry);
|
|
720
698
|
|
|
721
699
|
if (detached) {
|
|
722
700
|
console.log("");
|
|
@@ -744,36 +722,79 @@ async function hatchLocal(species: Species, name: string | null): Promise<void>
|
|
|
744
722
|
console.log("");
|
|
745
723
|
|
|
746
724
|
console.log("🔨 Starting local daemon...");
|
|
747
|
-
const
|
|
748
|
-
stdio: "inherit",
|
|
749
|
-
env: { ...process.env },
|
|
750
|
-
});
|
|
725
|
+
const daemonBinary = join(dirname(process.execPath), "vellum-daemon");
|
|
751
726
|
|
|
752
|
-
|
|
753
|
-
child
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
727
|
+
if (existsSync(daemonBinary)) {
|
|
728
|
+
const child = spawn(daemonBinary, [], {
|
|
729
|
+
detached: true,
|
|
730
|
+
stdio: "ignore",
|
|
731
|
+
env: { ...process.env },
|
|
732
|
+
});
|
|
733
|
+
child.unref();
|
|
734
|
+
|
|
735
|
+
const homeDir = process.env.HOME ?? userInfo().homedir;
|
|
736
|
+
const socketPath = join(homeDir, ".vellum", "vellum.sock");
|
|
737
|
+
const maxWait = 10000;
|
|
738
|
+
const pollInterval = 100;
|
|
739
|
+
let waited = 0;
|
|
740
|
+
while (waited < maxWait) {
|
|
741
|
+
if (existsSync(socketPath)) {
|
|
742
|
+
break;
|
|
758
743
|
}
|
|
744
|
+
await new Promise((r) => setTimeout(r, pollInterval));
|
|
745
|
+
waited += pollInterval;
|
|
746
|
+
}
|
|
747
|
+
if (!existsSync(socketPath)) {
|
|
748
|
+
console.warn("⚠️ Daemon socket did not appear within 10s — continuing anyway");
|
|
749
|
+
}
|
|
750
|
+
} else {
|
|
751
|
+
const child = spawn("bunx", ["vellum", "daemon", "start"], {
|
|
752
|
+
stdio: "inherit",
|
|
753
|
+
env: { ...process.env },
|
|
759
754
|
});
|
|
760
|
-
child.on("error", reject);
|
|
761
|
-
});
|
|
762
755
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
756
|
+
await new Promise<void>((resolve, reject) => {
|
|
757
|
+
child.on("close", (code) => {
|
|
758
|
+
if (code === 0) {
|
|
759
|
+
resolve();
|
|
760
|
+
} else {
|
|
761
|
+
reject(new Error(`Daemon start exited with code ${code}`));
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
child.on("error", reject);
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
console.log("🌐 Starting gateway...");
|
|
769
|
+
const gatewayDir = join(import.meta.dir, "..", "..", "..", "gateway");
|
|
770
|
+
if (!existsSync(gatewayDir)) {
|
|
771
|
+
console.warn("⚠️ Gateway directory not found at", gatewayDir);
|
|
772
|
+
console.warn(' Gateway will not be started\n');
|
|
773
|
+
} else {
|
|
774
|
+
const gateway = spawn("bun", ["run", "src/index.ts"], {
|
|
775
|
+
cwd: gatewayDir,
|
|
776
|
+
detached: true,
|
|
777
|
+
stdio: "ignore",
|
|
778
|
+
env: {
|
|
779
|
+
...process.env,
|
|
780
|
+
GATEWAY_RUNTIME_PROXY_ENABLED: "true",
|
|
781
|
+
GATEWAY_RUNTIME_PROXY_REQUIRE_AUTH: "false",
|
|
782
|
+
},
|
|
783
|
+
});
|
|
784
|
+
gateway.unref();
|
|
785
|
+
console.log("✅ Gateway started\n");
|
|
775
786
|
}
|
|
776
787
|
|
|
788
|
+
const runtimeUrl = `http://localhost:${GATEWAY_PORT}`;
|
|
789
|
+
const localEntry: AssistantEntry = {
|
|
790
|
+
assistantId: instanceName,
|
|
791
|
+
runtimeUrl,
|
|
792
|
+
cloud: "local",
|
|
793
|
+
species,
|
|
794
|
+
hatchedAt: new Date().toISOString(),
|
|
795
|
+
};
|
|
796
|
+
saveAssistantEntry(localEntry);
|
|
797
|
+
|
|
777
798
|
console.log("");
|
|
778
799
|
console.log(`✅ Local assistant hatched!`);
|
|
779
800
|
console.log("");
|
|
@@ -801,6 +822,11 @@ export async function hatch(): Promise<void> {
|
|
|
801
822
|
return;
|
|
802
823
|
}
|
|
803
824
|
|
|
825
|
+
if (remote === "aws") {
|
|
826
|
+
await hatchAws(species, detached, name);
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
|
|
804
830
|
console.error(`Error: Remote host '${remote}' is not yet supported.`);
|
|
805
831
|
process.exit(1);
|
|
806
832
|
}
|