@vellumai/cli 0.1.1 → 0.1.2

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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "CLI tools for vellum-assistant",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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}"
@@ -1,10 +1,13 @@
1
1
  import { spawn } from "child_process";
2
2
  import { randomBytes } from "crypto";
3
- import { existsSync, readFileSync, unlinkSync, writeFileSync } from "fs";
4
- import { homedir, tmpdir, userInfo } from "os";
3
+ import { existsSync, unlinkSync, writeFileSync } from "fs";
4
+ import { tmpdir, userInfo } from "os";
5
5
  import { 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 = 2 * 60 * 1000; // 2 minutes
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 https://assistant.vellum.ai/install.sh -o ${INSTALL_SCRIPT_REMOTE_PATH}
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 output = await execOutput("gcloud", [
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 output = await execOutput("gcloud", [
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
- console.log("📋 Uploading install script to instance...");
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
- console.log("🔧 Running install script on instance...");
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 pollInstance(instanceName, project, zone);
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
- console.log(` Zone: ${DEFAULT_ZONE}`);
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, DEFAULT_ZONE)) {
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, DEFAULT_ZONE)) {
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
- await exec("gcloud", [
494
+ const createArgs = [
512
495
  "compute",
513
496
  "instances",
514
497
  "create",
515
498
  instanceName,
516
499
  `--project=${project}`,
517
- `--zone=${DEFAULT_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 ipOutput = await execOutput("gcloud", [
525
+ const describeArgs = [
541
526
  "compute",
542
527
  "instances",
543
528
  "describe",
544
529
  instanceName,
545
530
  `--project=${project}`,
546
- `--zone=${DEFAULT_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 entryFilePath = process.env.VELLUM_HATCH_ENTRY_FILE;
558
- if (entryFilePath) {
559
- writeFileSync(
560
- entryFilePath,
561
- JSON.stringify({
562
- assistantId: instanceName,
563
- runtimeUrl,
564
- bearerToken,
565
- project,
566
- zone: DEFAULT_ZONE,
567
- species,
568
- sshUser,
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: ${DEFAULT_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, DEFAULT_ZONE))
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, DEFAULT_ZONE, sshUser);
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: ${DEFAULT_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 entryFilePath = process.env.VELLUM_HATCH_ENTRY_FILE;
707
- if (entryFilePath) {
708
- writeFileSync(
709
- entryFilePath,
710
- JSON.stringify({
711
- assistantId: instanceName,
712
- runtimeUrl,
713
- bearerToken,
714
- species,
715
- sshUser,
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("");
@@ -760,20 +738,36 @@ async function hatchLocal(species: Species, name: string | null): Promise<void>
760
738
  child.on("error", reject);
761
739
  });
762
740
 
763
- const runtimeUrl = `http://localhost:${GATEWAY_PORT}`;
764
- const entryFilePath = process.env.VELLUM_HATCH_ENTRY_FILE;
765
- if (entryFilePath) {
766
- writeFileSync(
767
- entryFilePath,
768
- JSON.stringify({
769
- assistantId: instanceName,
770
- runtimeUrl,
771
- species,
772
- hatchedAt: new Date().toISOString(),
773
- }),
774
- );
741
+ console.log("🌐 Starting gateway...");
742
+ const gatewayDir = join(import.meta.dir, "..", "..", "..", "gateway");
743
+ if (!existsSync(gatewayDir)) {
744
+ console.warn("⚠️ Gateway directory not found at", gatewayDir);
745
+ console.warn(' Gateway will not be started\n');
746
+ } else {
747
+ const gateway = spawn("bun", ["run", "src/index.ts"], {
748
+ cwd: gatewayDir,
749
+ detached: true,
750
+ stdio: "ignore",
751
+ env: {
752
+ ...process.env,
753
+ GATEWAY_RUNTIME_PROXY_ENABLED: "true",
754
+ GATEWAY_RUNTIME_PROXY_REQUIRE_AUTH: "false",
755
+ },
756
+ });
757
+ gateway.unref();
758
+ console.log("✅ Gateway started\n");
775
759
  }
776
760
 
761
+ const runtimeUrl = `http://localhost:${GATEWAY_PORT}`;
762
+ const localEntry: AssistantEntry = {
763
+ assistantId: instanceName,
764
+ runtimeUrl,
765
+ cloud: "local",
766
+ species,
767
+ hatchedAt: new Date().toISOString(),
768
+ };
769
+ saveAssistantEntry(localEntry);
770
+
777
771
  console.log("");
778
772
  console.log(`✅ Local assistant hatched!`);
779
773
  console.log("");
@@ -801,6 +795,11 @@ export async function hatch(): Promise<void> {
801
795
  return;
802
796
  }
803
797
 
798
+ if (remote === "aws") {
799
+ await hatchAws(species, detached, name);
800
+ return;
801
+ }
802
+
804
803
  console.error(`Error: Remote host '${remote}' is not yet supported.`);
805
804
  process.exit(1);
806
805
  }