@vellumai/cli 0.1.8 → 0.1.10

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 CHANGED
@@ -39,7 +39,7 @@ vellum-cli hatch [species] [options]
39
39
 
40
40
  #### Remote Targets
41
41
 
42
- - **`local`** -- Starts a local daemon on your machine via `bunx vellum daemon start`.
42
+ - **`local`** -- Starts the local daemon and local gateway. Gateway source resolution order is: `VELLUM_GATEWAY_DIR` override, repo source tree, then installed `@vellumai/vellum-gateway` package.
43
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
44
  - **`aws`** -- Provisions an AWS instance.
45
45
  - **`custom`** -- Provisions on an arbitrary SSH host. Set `VELLUM_CUSTOM_HOST` (e.g. `user@hostname`) to specify the target.
@@ -52,6 +52,8 @@ vellum-cli hatch [species] [options]
52
52
  | `GCP_PROJECT` | `gcp` | GCP project ID. Falls back to the active `gcloud` project. |
53
53
  | `GCP_DEFAULT_ZONE` | `gcp` | GCP zone for the compute instance. |
54
54
  | `VELLUM_CUSTOM_HOST` | `custom` | SSH host in `user@hostname` format. |
55
+ | `VELLUM_GATEWAY_DIR` | `local` | Optional absolute path to a local gateway source directory to run instead of the packaged gateway. |
56
+ | `INGRESS_PUBLIC_BASE_URL` | `local` | Optional fallback public ingress URL when `ingress.publicBaseUrl` is not set in workspace config. |
55
57
 
56
58
  #### Examples
57
59
 
package/package.json CHANGED
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "@vellumai/cli",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "CLI tools for vellum-assistant",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "vellum-cli": "./src/index.ts"
8
8
  },
9
9
  "scripts": {
10
- "bump": "bun run scripts/bump.ts",
11
10
  "lint": "eslint",
12
11
  "typecheck": "bunx tsc --noEmit"
13
12
  },
@@ -79,6 +79,23 @@ ensure_bun() {
79
79
  success "bun installed ($(bun --version))"
80
80
  }
81
81
 
82
+ install_vellum() {
83
+ if command -v vellum >/dev/null 2>&1; then
84
+ info "Updating vellum to latest..."
85
+ bun install -g vellum@latest
86
+ else
87
+ info "Installing vellum globally..."
88
+ bun install -g vellum@latest
89
+ fi
90
+
91
+ if ! command -v vellum >/dev/null 2>&1; then
92
+ error "vellum installation failed. Please install manually: bun install -g vellum"
93
+ exit 1
94
+ fi
95
+
96
+ success "vellum installed ($(vellum --version 2>/dev/null || echo 'unknown'))"
97
+ }
98
+
82
99
  main() {
83
100
  printf "\n"
84
101
  printf ' %bVellum Installer%b\n' "$BOLD" "$RESET"
@@ -86,13 +103,14 @@ main() {
86
103
 
87
104
  ensure_git
88
105
  ensure_bun
106
+ install_vellum
89
107
 
90
108
  info "Running vellum hatch..."
91
109
  printf "\n"
92
110
  if [ -n "${VELLUM_SSH_USER:-}" ] && [ "$(id -u)" = "0" ]; then
93
- su - "$VELLUM_SSH_USER" -c "set -a; [ -f \"\$HOME/.vellum/.env\" ] && . \"\$HOME/.vellum/.env\"; set +a; export PATH=\"$HOME/.bun/bin:\$PATH\"; bunx vellum hatch"
111
+ su - "$VELLUM_SSH_USER" -c "set -a; [ -f \"\$HOME/.vellum/.env\" ] && . \"\$HOME/.vellum/.env\"; set +a; export PATH=\"$HOME/.bun/bin:\$PATH\"; vellum hatch"
94
112
  else
95
- bunx vellum hatch
113
+ vellum hatch
96
114
  fi
97
115
  }
98
116
 
@@ -1,8 +1,8 @@
1
1
  import { spawn } from "child_process";
2
2
  import { randomBytes } from "crypto";
3
- import { existsSync, readFileSync, unlinkSync, writeFileSync } from "fs";
3
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
4
4
  import { createRequire } from "module";
5
- import { tmpdir, userInfo } from "os";
5
+ import { tmpdir, userInfo, homedir } from "os";
6
6
  import { dirname, join } from "path";
7
7
 
8
8
  import { buildOpenclawStartupScript } from "../adapters/openclaw";
@@ -26,8 +26,18 @@ import { exec, execOutput } from "../lib/step-runner";
26
26
  const _require = createRequire(import.meta.url);
27
27
 
28
28
  const INSTALL_SCRIPT_REMOTE_PATH = "/tmp/vellum-install.sh";
29
- const INSTALL_SCRIPT_PATH = join(import.meta.dir, "..", "adapters", "install.sh");
30
29
  const MACHINE_TYPE = "e2-standard-4"; // 4 vCPUs, 16 GB memory
30
+
31
+ // Resolve the install script path. In source tree, use the file directly.
32
+ // In compiled binary ($bunfs), the file may not be available.
33
+ async function resolveInstallScriptPath(): Promise<string | null> {
34
+ const sourcePath = join(import.meta.dir, "..", "adapters", "install.sh");
35
+ if (existsSync(sourcePath)) {
36
+ return sourcePath;
37
+ }
38
+ console.warn("⚠️ Install script not found at", sourcePath, "(expected in compiled binary)");
39
+ return null;
40
+ }
31
41
  const HATCH_TIMEOUT_MS: Record<Species, number> = {
32
42
  vellum: 2 * 60 * 1000,
33
43
  openclaw: 10 * 60 * 1000,
@@ -85,6 +95,7 @@ export async function buildStartupScript(
85
95
  sshUser: string,
86
96
  anthropicApiKey: string,
87
97
  instanceName: string,
98
+ cloud: RemoteHost,
88
99
  ): Promise<string> {
89
100
  const platformUrl = process.env.VELLUM_ASSISTANT_PLATFORM_URL ?? "https://assistant.vellum.ai";
90
101
  const timestampRedirect = buildTimestampRedirect();
@@ -102,7 +113,7 @@ export async function buildStartupScript(
102
113
  );
103
114
  }
104
115
 
105
- const interfacesSeed = buildInterfacesSeed();
116
+ const interfacesSeed = await buildInterfacesSeed();
106
117
 
107
118
  return `#!/bin/bash
108
119
  set -e
@@ -115,6 +126,7 @@ ANTHROPIC_API_KEY=${anthropicApiKey}
115
126
  GATEWAY_RUNTIME_PROXY_ENABLED=true
116
127
  RUNTIME_PROXY_BEARER_TOKEN=${bearerToken}
117
128
  VELLUM_ASSISTANT_NAME=${instanceName}
129
+ VELLUM_CLOUD=${cloud}
118
130
  ${interfacesSeed}
119
131
  mkdir -p "\$HOME/.vellum"
120
132
  cat > "\$HOME/.vellum/.env" << DOTENV_EOF
@@ -123,6 +135,7 @@ GATEWAY_RUNTIME_PROXY_ENABLED=\$GATEWAY_RUNTIME_PROXY_ENABLED
123
135
  RUNTIME_PROXY_BEARER_TOKEN=\$RUNTIME_PROXY_BEARER_TOKEN
124
136
  INTERFACES_SEED_DIR=\$INTERFACES_SEED_DIR
125
137
  RUNTIME_HTTP_PORT=7821
138
+ VELLUM_CLOUD=\$VELLUM_CLOUD
126
139
  DOTENV_EOF
127
140
 
128
141
  mkdir -p "\$HOME/.vellum/workspace"
@@ -154,6 +167,7 @@ interface HatchArgs {
154
167
  detached: boolean;
155
168
  name: string | null;
156
169
  remote: RemoteHost;
170
+ daemonOnly: boolean;
157
171
  }
158
172
 
159
173
  function parseArgs(): HatchArgs {
@@ -162,11 +176,14 @@ function parseArgs(): HatchArgs {
162
176
  let detached = false;
163
177
  let name: string | null = null;
164
178
  let remote: RemoteHost = DEFAULT_REMOTE;
179
+ let daemonOnly = false;
165
180
 
166
181
  for (let i = 0; i < args.length; i++) {
167
182
  const arg = args[i];
168
183
  if (arg === "-d") {
169
184
  detached = true;
185
+ } else if (arg === "--daemon-only") {
186
+ daemonOnly = true;
170
187
  } else if (arg === "--name") {
171
188
  const next = args[i + 1];
172
189
  if (!next || next.startsWith("-")) {
@@ -189,13 +206,13 @@ function parseArgs(): HatchArgs {
189
206
  species = arg as Species;
190
207
  } else {
191
208
  console.error(
192
- `Error: Unknown argument '${arg}'. Valid options: ${VALID_SPECIES.join(", ")}, -d, --name <name>, --remote <${VALID_REMOTE_HOSTS.join("|")}>`,
209
+ `Error: Unknown argument '${arg}'. Valid options: ${VALID_SPECIES.join(", ")}, -d, --daemon-only, --name <name>, --remote <${VALID_REMOTE_HOSTS.join("|")}>`,
193
210
  );
194
211
  process.exit(1);
195
212
  }
196
213
  }
197
214
 
198
- return { species, detached, name, remote };
215
+ return { species, detached, name, remote, daemonOnly };
199
216
  }
200
217
 
201
218
  export interface PollResult {
@@ -304,14 +321,16 @@ async function recoverFromCurlFailure(
304
321
  sshUser: string,
305
322
  account?: string,
306
323
  ): Promise<void> {
307
- if (!existsSync(INSTALL_SCRIPT_PATH)) {
308
- throw new Error(`Install script not found at ${INSTALL_SCRIPT_PATH}`);
324
+ const installScriptPath = await resolveInstallScriptPath();
325
+ if (!installScriptPath) {
326
+ console.warn("⚠️ Skipping install script upload (not available in compiled binary)");
327
+ return;
309
328
  }
310
329
 
311
330
  const scpArgs = [
312
331
  "compute",
313
332
  "scp",
314
- INSTALL_SCRIPT_PATH,
333
+ installScriptPath,
315
334
  `${instanceName}:${INSTALL_SCRIPT_REMOTE_PATH}`,
316
335
  `--zone=${zone}`,
317
336
  `--project=${project}`,
@@ -511,6 +530,7 @@ async function hatchGcp(
511
530
  sshUser,
512
531
  anthropicApiKey,
513
532
  instanceName,
533
+ "gcp",
514
534
  );
515
535
  const startupScriptPath = join(tmpdir(), `${instanceName}-startup.sh`);
516
536
  writeFileSync(startupScriptPath, startupScript);
@@ -689,19 +709,25 @@ async function hatchCustom(
689
709
  sshUser,
690
710
  anthropicApiKey,
691
711
  instanceName,
712
+ "custom",
692
713
  );
693
714
  const startupScriptPath = join(tmpdir(), `${instanceName}-startup.sh`);
694
715
  writeFileSync(startupScriptPath, startupScript);
695
716
 
696
717
  try {
697
- console.log("📋 Uploading install script to instance...");
698
- await exec("scp", [
699
- "-o", "StrictHostKeyChecking=no",
700
- "-o", "UserKnownHostsFile=/dev/null",
701
- "-o", "LogLevel=ERROR",
702
- INSTALL_SCRIPT_PATH,
703
- `${host}:${INSTALL_SCRIPT_REMOTE_PATH}`,
704
- ]);
718
+ const installScriptPath = await resolveInstallScriptPath();
719
+ if (installScriptPath) {
720
+ console.log("📋 Uploading install script to instance...");
721
+ await exec("scp", [
722
+ "-o", "StrictHostKeyChecking=no",
723
+ "-o", "UserKnownHostsFile=/dev/null",
724
+ "-o", "LogLevel=ERROR",
725
+ installScriptPath,
726
+ `${host}:${INSTALL_SCRIPT_REMOTE_PATH}`,
727
+ ]);
728
+ } else {
729
+ console.warn("⚠️ Skipping install script upload (not available in compiled binary)");
730
+ }
705
731
 
706
732
  console.log("📋 Uploading startup script to instance...");
707
733
  const remoteStartupPath = `/tmp/${instanceName}-startup.sh`;
@@ -754,57 +780,229 @@ async function hatchCustom(
754
780
  }
755
781
  }
756
782
 
783
+ function isGatewaySourceDir(dir: string): boolean {
784
+ const pkgPath = join(dir, "package.json");
785
+ if (!existsSync(pkgPath) || !existsSync(join(dir, "src", "index.ts"))) return false;
786
+ try {
787
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
788
+ return pkg.name === "@vellumai/vellum-gateway";
789
+ } catch {
790
+ return false;
791
+ }
792
+ }
793
+
794
+ function findGatewaySourceFromCwd(): string | undefined {
795
+ let current = process.cwd();
796
+ while (true) {
797
+ if (isGatewaySourceDir(current)) {
798
+ return current;
799
+ }
800
+ const nestedCandidate = join(current, "gateway");
801
+ if (isGatewaySourceDir(nestedCandidate)) {
802
+ return nestedCandidate;
803
+ }
804
+ const parent = dirname(current);
805
+ if (parent === current) {
806
+ return undefined;
807
+ }
808
+ current = parent;
809
+ }
810
+ }
811
+
757
812
  function resolveGatewayDir(): string {
813
+ const override = process.env.VELLUM_GATEWAY_DIR?.trim();
814
+ if (override) {
815
+ if (!isGatewaySourceDir(override)) {
816
+ throw new Error(
817
+ `VELLUM_GATEWAY_DIR is set to "${override}", but it is not a valid gateway source directory.`,
818
+ );
819
+ }
820
+ return override;
821
+ }
822
+
758
823
  const sourceDir = join(import.meta.dir, "..", "..", "..", "gateway");
759
- if (existsSync(sourceDir)) {
824
+ if (isGatewaySourceDir(sourceDir)) {
760
825
  return sourceDir;
761
826
  }
762
827
 
828
+ const cwdSourceDir = findGatewaySourceFromCwd();
829
+ if (cwdSourceDir) {
830
+ return cwdSourceDir;
831
+ }
832
+
763
833
  try {
764
834
  const pkgPath = _require.resolve("@vellumai/vellum-gateway/package.json");
765
835
  return dirname(pkgPath);
766
836
  } catch {
767
837
  throw new Error(
768
- "Gateway not found. Ensure @vellumai/vellum-gateway is installed or run from the source tree.",
838
+ "Gateway not found. Ensure @vellumai/vellum-gateway is installed, run from the source tree, or set VELLUM_GATEWAY_DIR.",
769
839
  );
770
840
  }
771
841
  }
772
842
 
773
- async function hatchLocal(species: Species, name: string | null): Promise<void> {
774
- const instanceName =
775
- name ?? process.env.VELLUM_ASSISTANT_NAME ?? `${species}-${generateRandomSuffix()}`;
843
+ function normalizeIngressUrl(value: unknown): string | undefined {
844
+ if (typeof value !== "string") return undefined;
845
+ const normalized = value.trim().replace(/\/+$/, "");
846
+ return normalized || undefined;
847
+ }
776
848
 
777
- console.log(`🥚 Hatching local assistant: ${instanceName}`);
778
- console.log(` Species: ${species}`);
779
- console.log("");
849
+ function readWorkspaceIngressPublicBaseUrl(): string | undefined {
850
+ const baseDataDir = process.env.BASE_DATA_DIR?.trim() || (process.env.HOME ?? homedir());
851
+ const workspaceConfigPath = join(baseDataDir, ".vellum", "workspace", "config.json");
852
+ try {
853
+ const raw = JSON.parse(readFileSync(workspaceConfigPath, "utf-8")) as Record<string, unknown>;
854
+ const ingress = raw.ingress as Record<string, unknown> | undefined;
855
+ return normalizeIngressUrl(ingress?.publicBaseUrl);
856
+ } catch {
857
+ return undefined;
858
+ }
859
+ }
780
860
 
781
- console.log("🔨 Starting local daemon...");
861
+ async function discoverPublicUrl(): Promise<string | undefined> {
862
+ const cloud = process.env.VELLUM_CLOUD;
863
+ if (!cloud || cloud === "local") {
864
+ return `http://localhost:${GATEWAY_PORT}`;
865
+ }
782
866
 
867
+ let externalIp: string | undefined;
868
+ try {
869
+ if (cloud === "gcp") {
870
+ const resp = await fetch(
871
+ "http://169.254.169.254/computeMetadata/v1/instance/network-interfaces/0/access-configs/0/external-ip",
872
+ { headers: { "Metadata-Flavor": "Google" } },
873
+ );
874
+ if (resp.ok) externalIp = (await resp.text()).trim();
875
+ } else if (cloud === "aws") {
876
+ // Use IMDSv2 (token-based) for compatibility with HttpTokens=required
877
+ const tokenResp = await fetch(
878
+ "http://169.254.169.254/latest/api/token",
879
+ { method: "PUT", headers: { "X-aws-ec2-metadata-token-ttl-seconds": "30" } },
880
+ );
881
+ if (tokenResp.ok) {
882
+ const token = await tokenResp.text();
883
+ const ipResp = await fetch(
884
+ "http://169.254.169.254/latest/meta-data/public-ipv4",
885
+ { headers: { "X-aws-ec2-metadata-token": token } },
886
+ );
887
+ if (ipResp.ok) externalIp = (await ipResp.text()).trim();
888
+ }
889
+ }
890
+ } catch {
891
+ // metadata service not reachable
892
+ }
893
+
894
+ if (externalIp) {
895
+ console.log(` Discovered external IP: ${externalIp}`);
896
+ return `http://${externalIp}:${GATEWAY_PORT}`;
897
+ }
898
+ return undefined;
899
+ }
900
+
901
+ export async function startLocalDaemon(): Promise<void> {
783
902
  if (process.env.VELLUM_DESKTOP_APP) {
903
+ // When running inside the desktop app, the CLI owns the daemon lifecycle.
904
+ // Find the vellum-daemon binary adjacent to the CLI binary.
784
905
  const daemonBinary = join(dirname(process.execPath), "vellum-daemon");
785
- const child = spawn(daemonBinary, [], {
786
- detached: true,
787
- stdio: "ignore",
788
- env: { ...process.env },
789
- });
790
- child.unref();
791
-
792
- const homeDir = process.env.HOME ?? userInfo().homedir;
793
- const socketPath = join(homeDir, ".vellum", "vellum.sock");
794
- const maxWait = 10000;
795
- const pollInterval = 100;
796
- let waited = 0;
797
- while (waited < maxWait) {
798
- if (existsSync(socketPath)) {
799
- break;
906
+ if (!existsSync(daemonBinary)) {
907
+ throw new Error(
908
+ `vellum-daemon binary not found at ${daemonBinary}.\n` +
909
+ " Ensure the daemon binary is bundled alongside the CLI in the app bundle.",
910
+ );
911
+ }
912
+
913
+ const vellumDir = join(homedir(), ".vellum");
914
+ const pidFile = join(vellumDir, "vellum.pid");
915
+ const socketFile = join(vellumDir, "vellum.sock");
916
+
917
+ // If a daemon is already running, skip spawning a new one.
918
+ // This prevents cascading kill→restart cycles when multiple callers
919
+ // invoke hatch() concurrently (setupDaemonClient + ensureDaemonConnected).
920
+ let daemonAlive = false;
921
+ if (existsSync(pidFile)) {
922
+ try {
923
+ const pid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
924
+ if (!isNaN(pid)) {
925
+ try {
926
+ process.kill(pid, 0); // Check if alive
927
+ daemonAlive = true;
928
+ console.log(` Daemon already running (pid ${pid})\n`);
929
+ } catch {
930
+ // Process doesn't exist, clean up stale PID file
931
+ try { unlinkSync(pidFile); } catch {}
932
+ }
933
+ }
934
+ } catch {}
935
+ }
936
+
937
+ if (!daemonAlive) {
938
+ // Remove stale socket so we can detect the fresh one
939
+ try { unlinkSync(socketFile); } catch {}
940
+
941
+ console.log("🔨 Starting daemon...");
942
+
943
+ // Ensure ~/.vellum/ exists for PID/socket files
944
+ mkdirSync(vellumDir, { recursive: true });
945
+
946
+ // Build a minimal environment for the daemon. When launched from the
947
+ // macOS app the CLI inherits a huge environment (XPC_SERVICE_NAME,
948
+ // __CFBundleIdentifier, CLAUDE_CODE_ENTRYPOINT, etc.) that can cause
949
+ // the daemon to take 50+ seconds to start instead of ~1s.
950
+ const daemonEnv: Record<string, string> = {
951
+ HOME: process.env.HOME || homedir(),
952
+ PATH: process.env.PATH || "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",
953
+ VELLUM_DAEMON_TCP_ENABLED: "1",
954
+ };
955
+ // Forward optional config env vars the daemon may need
956
+ for (const key of [
957
+ "ANTHROPIC_API_KEY",
958
+ "BASE_DATA_DIR",
959
+ "VELLUM_DAEMON_TCP_PORT",
960
+ "VELLUM_DAEMON_TCP_HOST",
961
+ "VELLUM_DAEMON_SOCKET",
962
+ "VELLUM_DEBUG",
963
+ "SENTRY_DSN",
964
+ "TMPDIR",
965
+ "USER",
966
+ "LANG",
967
+ ]) {
968
+ if (process.env[key]) {
969
+ daemonEnv[key] = process.env[key]!;
970
+ }
971
+ }
972
+
973
+ const child = spawn(daemonBinary, [], {
974
+ detached: true,
975
+ stdio: "ignore",
976
+ env: daemonEnv,
977
+ });
978
+ child.unref();
979
+
980
+ // Write PID file immediately so the health monitor can find the process
981
+ // and concurrent hatch() calls see it as alive.
982
+ if (child.pid) {
983
+ writeFileSync(pidFile, String(child.pid), "utf-8");
800
984
  }
801
- await new Promise((r) => setTimeout(r, pollInterval));
802
- waited += pollInterval;
803
985
  }
804
- if (!existsSync(socketPath)) {
805
- console.warn("⚠️ Daemon socket did not appear within 10s continuing anyway");
986
+
987
+ // Wait for socket at ~/.vellum/vellum.sock (up to 15s)
988
+ if (!existsSync(socketFile)) {
989
+ const maxWait = 15000;
990
+ const start = Date.now();
991
+ while (Date.now() - start < maxWait) {
992
+ if (existsSync(socketFile)) {
993
+ break;
994
+ }
995
+ await new Promise((r) => setTimeout(r, 100));
996
+ }
997
+ }
998
+ if (existsSync(socketFile)) {
999
+ console.log(" Daemon socket ready\n");
1000
+ } else {
1001
+ console.log(" ⚠️ Daemon socket did not appear within 15s — continuing anyway\n");
806
1002
  }
807
1003
  } else {
1004
+ console.log("🔨 Starting local daemon...");
1005
+
808
1006
  // Source tree layout: cli/src/commands/ -> ../../.. -> repo root -> assistant/src/index.ts
809
1007
  const sourceTreeIndex = join(import.meta.dir, "..", "..", "..", "assistant", "src", "index.ts");
810
1008
  // bunx layout: @vellumai/cli/src/commands/ -> ../../../.. -> node_modules/ -> vellum/src/index.ts
@@ -847,45 +1045,93 @@ async function hatchLocal(species: Species, name: string | null): Promise<void>
847
1045
  child.on("error", reject);
848
1046
  });
849
1047
  }
1048
+ }
1049
+
1050
+ export async function startGateway(): Promise<string> {
1051
+ const publicUrl = await discoverPublicUrl();
1052
+ if (publicUrl) {
1053
+ console.log(` Public URL: ${publicUrl}`);
1054
+ }
850
1055
 
851
1056
  console.log("🌐 Starting gateway...");
852
1057
  const gatewayDir = resolveGatewayDir();
1058
+ const gatewayEnv: Record<string, string> = {
1059
+ ...process.env as Record<string, string>,
1060
+ GATEWAY_RUNTIME_PROXY_ENABLED: "true",
1061
+ GATEWAY_RUNTIME_PROXY_REQUIRE_AUTH: "false",
1062
+ };
1063
+ const workspaceIngressPublicBaseUrl = readWorkspaceIngressPublicBaseUrl();
1064
+ const ingressPublicBaseUrl =
1065
+ workspaceIngressPublicBaseUrl
1066
+ ?? normalizeIngressUrl(process.env.INGRESS_PUBLIC_BASE_URL);
1067
+ if (ingressPublicBaseUrl) {
1068
+ gatewayEnv.INGRESS_PUBLIC_BASE_URL = ingressPublicBaseUrl;
1069
+ console.log(` Ingress URL: ${ingressPublicBaseUrl}`);
1070
+ if (!workspaceIngressPublicBaseUrl) {
1071
+ console.log(" (using INGRESS_PUBLIC_BASE_URL env fallback)");
1072
+ }
1073
+ }
1074
+ if (publicUrl) gatewayEnv.GATEWAY_PUBLIC_URL = publicUrl;
1075
+
853
1076
  const gateway = spawn("bun", ["run", "src/index.ts"], {
854
1077
  cwd: gatewayDir,
855
1078
  detached: true,
856
1079
  stdio: "ignore",
857
- env: {
858
- ...process.env,
859
- GATEWAY_RUNTIME_PROXY_ENABLED: "true",
860
- GATEWAY_RUNTIME_PROXY_REQUIRE_AUTH: "false",
861
- },
1080
+ env: gatewayEnv,
862
1081
  });
863
1082
  gateway.unref();
864
1083
  console.log("✅ Gateway started\n");
1084
+ return publicUrl || `http://localhost:${GATEWAY_PORT}`;
1085
+ }
1086
+
1087
+ async function hatchLocal(species: Species, name: string | null, daemonOnly: boolean = false): Promise<void> {
1088
+ const instanceName =
1089
+ name ?? process.env.VELLUM_ASSISTANT_NAME ?? `${species}-${generateRandomSuffix()}`;
1090
+
1091
+ console.log(`🥚 Hatching local assistant: ${instanceName}`);
1092
+ console.log(` Species: ${species}`);
1093
+ console.log("");
1094
+
1095
+ await startLocalDaemon();
1096
+
1097
+ // The desktop app communicates with the daemon directly via Unix socket,
1098
+ // so the HTTP gateway is only needed for non-desktop (CLI) usage.
1099
+ let runtimeUrl: string;
1100
+
1101
+ if (process.env.VELLUM_DESKTOP_APP) {
1102
+ // No gateway needed — the macOS app uses DaemonClient over the Unix socket.
1103
+ runtimeUrl = "local";
1104
+ } else {
1105
+ runtimeUrl = await startGateway();
1106
+ }
865
1107
 
866
- const runtimeUrl = `http://localhost:${GATEWAY_PORT}`;
1108
+ const baseDataDir = join(process.env.BASE_DATA_DIR?.trim() || (process.env.HOME ?? userInfo().homedir), ".vellum");
867
1109
  const localEntry: AssistantEntry = {
868
1110
  assistantId: instanceName,
869
1111
  runtimeUrl,
1112
+ baseDataDir,
870
1113
  cloud: "local",
871
1114
  species,
872
1115
  hatchedAt: new Date().toISOString(),
873
1116
  };
874
- saveAssistantEntry(localEntry);
1117
+ if (!daemonOnly) {
1118
+ saveAssistantEntry(localEntry);
875
1119
 
876
- console.log("");
877
- console.log(`✅ Local assistant hatched!`);
878
- console.log("");
879
- console.log("Instance details:");
880
- console.log(` Name: ${instanceName}`);
881
- console.log(` Runtime: ${runtimeUrl}`);
882
- console.log("");
1120
+ console.log("");
1121
+ console.log(`✅ Local assistant hatched!`);
1122
+ console.log("");
1123
+ console.log("Instance details:");
1124
+ console.log(` Name: ${instanceName}`);
1125
+ console.log(` Runtime: ${runtimeUrl}`);
1126
+ console.log("");
1127
+ }
883
1128
  }
884
1129
 
885
1130
  function getCliVersion(): string {
886
1131
  try {
887
- const pkgPath = join(import.meta.dir, "..", "..", "package.json");
888
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
1132
+ // Use createRequire for JSON import works in both Bun dev and compiled binary.
1133
+ const require = createRequire(import.meta.url);
1134
+ const pkg = require("../../package.json") as { version?: string };
889
1135
  return pkg.version ?? "unknown";
890
1136
  } catch {
891
1137
  return "unknown";
@@ -896,10 +1142,10 @@ export async function hatch(): Promise<void> {
896
1142
  const cliVersion = getCliVersion();
897
1143
  console.log(`@vellumai/cli v${cliVersion}`);
898
1144
 
899
- const { species, detached, name, remote } = parseArgs();
1145
+ const { species, detached, name, remote, daemonOnly } = parseArgs();
900
1146
 
901
1147
  if (remote === "local") {
902
- await hatchLocal(species, name);
1148
+ await hatchLocal(species, name, daemonOnly);
903
1149
  return;
904
1150
  }
905
1151
 
@@ -0,0 +1,92 @@
1
+ import { loadAllAssistants } from "../lib/assistant-config";
2
+ import { checkHealth } from "../lib/health-check";
3
+ import { withStatusEmoji } from "../lib/status-emoji";
4
+
5
+ interface TableRow {
6
+ name: string;
7
+ status: string;
8
+ info: string;
9
+ }
10
+
11
+ interface ColWidths {
12
+ name: number;
13
+ status: number;
14
+ info: number;
15
+ }
16
+
17
+ function pad(s: string, w: number): string {
18
+ return s + " ".repeat(Math.max(0, w - s.length));
19
+ }
20
+
21
+ function computeColWidths(rows: TableRow[]): ColWidths {
22
+ const headers: TableRow = { name: "NAME", status: "STATUS", info: "INFO" };
23
+ const all = [headers, ...rows];
24
+ return {
25
+ name: Math.max(...all.map((r) => r.name.length)),
26
+ status: Math.max(...all.map((r) => r.status.length), "checking...".length),
27
+ info: Math.max(...all.map((r) => r.info.length)),
28
+ };
29
+ }
30
+
31
+ function formatRow(r: TableRow, colWidths: ColWidths): string {
32
+ return ` ${pad(r.name, colWidths.name)} ${pad(r.status, colWidths.status)} ${r.info}`;
33
+ }
34
+
35
+ export async function ps(): Promise<void> {
36
+ const assistants = loadAllAssistants();
37
+
38
+ if (assistants.length === 0) {
39
+ console.log("No assistants found.");
40
+ return;
41
+ }
42
+
43
+ const rows: TableRow[] = assistants.map((a) => {
44
+ const infoParts = [a.runtimeUrl];
45
+ if (a.cloud) infoParts.push(`cloud: ${a.cloud}`);
46
+ if (a.species) infoParts.push(`species: ${a.species}`);
47
+
48
+ return {
49
+ name: a.assistantId,
50
+ status: withStatusEmoji("checking..."),
51
+ info: infoParts.join(" | "),
52
+ };
53
+ });
54
+
55
+ const colWidths = computeColWidths(rows);
56
+
57
+ const headers: TableRow = { name: "NAME", status: "STATUS", info: "INFO" };
58
+ console.log(formatRow(headers, colWidths));
59
+ const sep = ` ${"-".repeat(colWidths.name)} ${"-".repeat(colWidths.status)} ${"-".repeat(colWidths.info)}`;
60
+ console.log(sep);
61
+ for (const row of rows) {
62
+ console.log(formatRow(row, colWidths));
63
+ }
64
+
65
+ const totalDataRows = rows.length;
66
+
67
+ await Promise.all(
68
+ assistants.map(async (a, rowIndex) => {
69
+ const health = await checkHealth(a.runtimeUrl);
70
+
71
+ const infoParts = [a.runtimeUrl];
72
+ if (a.cloud) infoParts.push(`cloud: ${a.cloud}`);
73
+ if (a.species) infoParts.push(`species: ${a.species}`);
74
+ if (health.detail) infoParts.push(health.detail);
75
+
76
+ const updatedRow: TableRow = {
77
+ name: a.assistantId,
78
+ status: withStatusEmoji(health.status),
79
+ info: infoParts.join(" | "),
80
+ };
81
+
82
+ const linesUp = totalDataRows - rowIndex;
83
+ process.stdout.write(
84
+ `\x1b[${linesUp}A` +
85
+ `\r\x1b[K` +
86
+ formatRow(updatedRow, colWidths) +
87
+ `\n` +
88
+ (linesUp > 1 ? `\x1b[${linesUp - 1}B` : ""),
89
+ );
90
+ }),
91
+ );
92
+ }
@@ -1,5 +1,5 @@
1
1
  import { spawn } from "child_process";
2
- import { rmSync } from "fs";
2
+ import { existsSync, readFileSync, rmSync, unlinkSync } from "fs";
3
3
  import { homedir } from "os";
4
4
  import { join } from "path";
5
5
 
@@ -34,30 +34,68 @@ function extractHostFromUrl(url: string): string {
34
34
  async function retireLocal(): Promise<void> {
35
35
  console.log("\u{1F5D1}\ufe0f Stopping local daemon...\n");
36
36
 
37
- try {
38
- const child = spawn("bunx", ["vellum", "daemon", "stop"], {
39
- stdio: "inherit",
40
- });
41
-
42
- await new Promise<void>((resolve) => {
43
- child.on("close", () => resolve());
44
- child.on("error", () => resolve());
45
- });
46
- } catch {}
47
-
48
- try {
49
- const killGateway = spawn("pkill", ["-f", "gateway/src/index.ts"], {
50
- stdio: "ignore",
51
- });
37
+ const vellumDir = join(homedir(), ".vellum");
38
+ const isDesktopApp = !!process.env.VELLUM_DESKTOP_APP;
39
+
40
+ // Stop daemon via PID file (works for both desktop app and standalone)
41
+ const pidFile = join(vellumDir, "vellum.pid");
42
+ const socketFile = join(vellumDir, "vellum.sock");
43
+
44
+ if (existsSync(pidFile)) {
45
+ try {
46
+ const pid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
47
+ if (!isNaN(pid)) {
48
+ try {
49
+ process.kill(pid, 0); // Check if alive
50
+ process.kill(pid, "SIGTERM");
51
+ const deadline = Date.now() + 2000;
52
+ while (Date.now() < deadline) {
53
+ try {
54
+ process.kill(pid, 0);
55
+ await new Promise((r) => setTimeout(r, 100));
56
+ } catch {
57
+ break;
58
+ }
59
+ }
60
+ try {
61
+ process.kill(pid, 0);
62
+ process.kill(pid, "SIGKILL");
63
+ } catch {}
64
+ } catch {}
65
+ }
66
+ } catch {}
67
+ try { unlinkSync(pidFile); } catch {}
68
+ try { unlinkSync(socketFile); } catch {}
69
+ }
52
70
 
53
- await new Promise<void>((resolve) => {
54
- killGateway.on("close", () => resolve());
55
- killGateway.on("error", () => resolve());
56
- });
57
- } catch {}
71
+ if (!isDesktopApp) {
72
+ // Non-desktop: also stop daemon via bunx (fallback) and kill gateway
73
+ try {
74
+ const child = spawn("bunx", ["vellum", "daemon", "stop"], {
75
+ stdio: "inherit",
76
+ });
77
+
78
+ await new Promise<void>((resolve) => {
79
+ child.on("close", () => resolve());
80
+ child.on("error", () => resolve());
81
+ });
82
+ } catch {}
83
+
84
+ try {
85
+ const killGateway = spawn("pkill", ["-f", "gateway/src/index.ts"], {
86
+ stdio: "ignore",
87
+ });
88
+
89
+ await new Promise<void>((resolve) => {
90
+ killGateway.on("close", () => resolve());
91
+ killGateway.on("error", () => resolve());
92
+ });
93
+ } catch {}
94
+
95
+ // Only delete ~/.vellum in non-desktop mode
96
+ rmSync(vellumDir, { recursive: true, force: true });
97
+ }
58
98
 
59
- const vellumDir = join(homedir(), ".vellum");
60
- rmSync(vellumDir, { recursive: true, force: true });
61
99
  console.log("\u2705 Local instance retired.");
62
100
  }
63
101
 
@@ -0,0 +1,63 @@
1
+ import { existsSync, readFileSync, unlinkSync } from "fs";
2
+ import { homedir } from "os";
3
+ import { join } from "path";
4
+
5
+ export async function sleep(): Promise<void> {
6
+ const vellumDir = join(homedir(), ".vellum");
7
+ const pidFile = join(vellumDir, "vellum.pid");
8
+ const socketFile = join(vellumDir, "vellum.sock");
9
+
10
+ if (!existsSync(pidFile)) {
11
+ console.log("No daemon PID file found — nothing to stop.");
12
+ process.exit(0);
13
+ }
14
+
15
+ const pidStr = readFileSync(pidFile, "utf-8").trim();
16
+ const pid = parseInt(pidStr, 10);
17
+
18
+ if (isNaN(pid)) {
19
+ console.log("Invalid PID file contents — cleaning up.");
20
+ try { unlinkSync(pidFile); } catch {}
21
+ try { unlinkSync(socketFile); } catch {}
22
+ process.exit(0);
23
+ }
24
+
25
+ // Check if process is alive
26
+ try {
27
+ process.kill(pid, 0);
28
+ } catch {
29
+ console.log(`Daemon process ${pid} is not running — cleaning up stale files.`);
30
+ try { unlinkSync(pidFile); } catch {}
31
+ try { unlinkSync(socketFile); } catch {}
32
+ process.exit(0);
33
+ }
34
+
35
+ console.log(`Stopping daemon (pid ${pid})...`);
36
+ process.kill(pid, "SIGTERM");
37
+
38
+ // Wait up to 2s for graceful exit
39
+ const deadline = Date.now() + 2000;
40
+ while (Date.now() < deadline) {
41
+ try {
42
+ process.kill(pid, 0);
43
+ await new Promise((r) => setTimeout(r, 100));
44
+ } catch {
45
+ break; // Process exited
46
+ }
47
+ }
48
+
49
+ // Force kill if still alive
50
+ try {
51
+ process.kill(pid, 0);
52
+ console.log("Daemon did not exit after SIGTERM, sending SIGKILL...");
53
+ process.kill(pid, "SIGKILL");
54
+ } catch {
55
+ // Already dead
56
+ }
57
+
58
+ // Clean up PID and socket files
59
+ try { unlinkSync(pidFile); } catch {}
60
+ try { unlinkSync(socketFile); } catch {}
61
+
62
+ console.log("Daemon stopped.");
63
+ }
@@ -0,0 +1,37 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+ import { homedir } from "os";
3
+ import { join } from "path";
4
+
5
+ import { startLocalDaemon, startGateway } from "./hatch";
6
+
7
+ export async function wake(): Promise<void> {
8
+ const vellumDir = join(homedir(), ".vellum");
9
+ const pidFile = join(vellumDir, "vellum.pid");
10
+
11
+ // Check if daemon is already running
12
+ let daemonRunning = false;
13
+ if (existsSync(pidFile)) {
14
+ const pidStr = readFileSync(pidFile, "utf-8").trim();
15
+ const pid = parseInt(pidStr, 10);
16
+ if (!isNaN(pid)) {
17
+ try {
18
+ process.kill(pid, 0);
19
+ daemonRunning = true;
20
+ console.log(`Daemon already running (pid ${pid}).`);
21
+ } catch {
22
+ // Process not alive, will start below
23
+ }
24
+ }
25
+ }
26
+
27
+ if (!daemonRunning) {
28
+ await startLocalDaemon();
29
+ }
30
+
31
+ // Start gateway (non-desktop only)
32
+ if (!process.env.VELLUM_DESKTOP_APP) {
33
+ await startGateway();
34
+ }
35
+
36
+ console.log("✅ Wake complete.");
37
+ }
package/src/index.ts CHANGED
@@ -1,11 +1,17 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  import { hatch } from "./commands/hatch";
4
+ import { ps } from "./commands/ps";
4
5
  import { retire } from "./commands/retire";
6
+ import { sleep } from "./commands/sleep";
7
+ import { wake } from "./commands/wake";
5
8
 
6
9
  const commands = {
7
10
  hatch,
11
+ ps,
8
12
  retire,
13
+ sleep,
14
+ wake,
9
15
  } as const;
10
16
 
11
17
  type CommandName = keyof typeof commands;
@@ -19,7 +25,10 @@ async function main() {
19
25
  console.log("");
20
26
  console.log("Commands:");
21
27
  console.log(" hatch Create a new assistant instance");
28
+ console.log(" ps List assistants and their health status");
22
29
  console.log(" retire Delete an assistant instance");
30
+ console.log(" sleep Stop the daemon process");
31
+ console.log(" wake Start the daemon and gateway");
23
32
  process.exit(0);
24
33
  }
25
34
 
@@ -5,6 +5,7 @@ import { join } from "path";
5
5
  export interface AssistantEntry {
6
6
  assistantId: string;
7
7
  runtimeUrl: string;
8
+ baseDataDir?: string;
8
9
  bearerToken?: string;
9
10
  cloud: string;
10
11
  instanceId?: string;
@@ -88,6 +89,10 @@ export function removeAssistantEntry(assistantId: string): void {
88
89
  writeAssistants(entries.filter((e) => e.assistantId !== assistantId));
89
90
  }
90
91
 
92
+ export function loadAllAssistants(): AssistantEntry[] {
93
+ return readAssistants();
94
+ }
95
+
91
96
  export function saveAssistantEntry(entry: AssistantEntry): void {
92
97
  const entries = readAssistants();
93
98
  entries.unshift(entry);
package/src/lib/aws.ts CHANGED
@@ -426,6 +426,7 @@ export async function hatchAws(
426
426
  sshUser,
427
427
  anthropicApiKey,
428
428
  instanceName,
429
+ "aws",
429
430
  );
430
431
  const startupScriptPath = join(tmpdir(), `${instanceName}-startup.sh`);
431
432
  writeFileSync(startupScriptPath, startupScript);
@@ -555,11 +556,29 @@ async function getInstanceIdByName(
555
556
  }
556
557
  }
557
558
 
559
+ async function checkAwsCliAvailable(): Promise<boolean> {
560
+ try {
561
+ await execOutput("aws", ["--version"]);
562
+ return true;
563
+ } catch {
564
+ return false;
565
+ }
566
+ }
567
+
558
568
  export async function retireInstance(
559
569
  name: string,
560
570
  region: string,
561
571
  source?: string,
562
572
  ): Promise<void> {
573
+ const awsOk = await checkAwsCliAvailable();
574
+ if (!awsOk) {
575
+ throw new Error(
576
+ `Cannot retire AWS instance '${name}': AWS CLI is not installed or not in PATH. ` +
577
+ `Please install the AWS CLI and try again, or terminate the instance manually ` +
578
+ `via the AWS Console (region=${region}).`,
579
+ );
580
+ }
581
+
563
582
  const instanceId = await getInstanceIdByName(name, region);
564
583
  if (!instanceId) {
565
584
  console.warn(
package/src/lib/gcp.ts CHANGED
@@ -313,12 +313,30 @@ export async function fetchAndDisplayStartupLogs(
313
313
  }
314
314
  }
315
315
 
316
+ async function checkGcloudAvailable(): Promise<boolean> {
317
+ try {
318
+ await execOutput("gcloud", ["--version"]);
319
+ return true;
320
+ } catch {
321
+ return false;
322
+ }
323
+ }
324
+
316
325
  export async function retireInstance(
317
326
  name: string,
318
327
  project: string,
319
328
  zone: string,
320
329
  source?: string,
321
330
  ): Promise<void> {
331
+ const gcloudOk = await checkGcloudAvailable();
332
+ if (!gcloudOk) {
333
+ throw new Error(
334
+ `Cannot retire GCP instance '${name}': gcloud CLI is not installed or not in PATH. ` +
335
+ `Please install the Google Cloud SDK and try again, or delete the instance manually ` +
336
+ `via the GCP Console (project=${project}, zone=${zone}).`,
337
+ );
338
+ }
339
+
322
340
  const exists = await instanceExists(name, project, zone);
323
341
  if (!exists) {
324
342
  console.warn(
@@ -354,6 +372,7 @@ export async function retireInstance(
354
372
  name,
355
373
  `--project=${project}`,
356
374
  `--zone=${zone}`,
375
+ "--quiet",
357
376
  ],
358
377
  { stdio: "inherit" },
359
378
  );
@@ -1,21 +1,28 @@
1
- import { join } from "path";
1
+ // Read source files using Bun.file() with string concatenation (not join())
2
+ // so Bun's bundler can statically analyze the paths and embed the files
3
+ // in the compiled binary ($bunfs). Files must also be passed via --embed
4
+ // in the bun build --compile invocation.
2
5
 
3
- const constantsSource = await Bun.file(join(import.meta.dir, "constants.ts")).text();
4
- const defaultMainScreenSource = await Bun.file(join(import.meta.dir, "..", "components", "DefaultMainScreen.tsx")).text();
5
-
6
- function inlineLocalImports(source: string): string {
6
+ function inlineLocalImports(source: string, constantsSource: string): string {
7
7
  return source
8
8
  .replace(/import\s*\{[^}]*\}\s*from\s*["'][^"']*\/constants["'];?\s*\n/, constantsSource + "\n")
9
9
  .replace(/import\s*\{[^}]*\}\s*from\s*["']path["'];?\s*\n/, "");
10
10
  }
11
11
 
12
- export function buildInterfacesSeed(): string {
13
- const mainWindowSource = inlineLocalImports(defaultMainScreenSource);
12
+ export async function buildInterfacesSeed(): Promise<string> {
13
+ try {
14
+ const constantsSource = await Bun.file(import.meta.dir + "/constants.ts").text();
15
+ const defaultMainScreenSource = await Bun.file(import.meta.dir + "/../components/DefaultMainScreen.tsx").text();
16
+ const mainWindowSource = inlineLocalImports(defaultMainScreenSource, constantsSource);
14
17
 
15
- return `
18
+ return `
16
19
  INTERFACES_SEED_DIR="/tmp/interfaces-seed"
17
20
  mkdir -p "\$INTERFACES_SEED_DIR/tui"
18
21
  cat > "\$INTERFACES_SEED_DIR/tui/main-window.tsx" << 'INTERFACES_SEED_EOF'
19
22
  ${mainWindowSource}INTERFACES_SEED_EOF
20
23
  `;
24
+ } catch (err) {
25
+ console.warn("⚠️ Could not embed interfaces seed files (expected in compiled binary without --embed):", (err as Error).message);
26
+ return "# interfaces-seed: skipped (source files not available in compiled binary)";
27
+ }
21
28
  }
@@ -1,9 +1,13 @@
1
- import { join } from "path";
1
+ // Read source file using Bun.file() with string concatenation (not join())
2
+ // so Bun's bundler can statically analyze the path and embed the file
3
+ // in the compiled binary ($bunfs). The file must also be passed via --embed
4
+ // in the bun build --compile invocation.
2
5
 
3
6
  export async function buildOpenclawRuntimeServer(): Promise<string> {
4
- const serverSource = await Bun.file(join(import.meta.dir, "..", "adapters", "openclaw-http-server.ts")).text();
7
+ try {
8
+ const serverSource = await Bun.file(import.meta.dir + "/../adapters/openclaw-http-server.ts").text();
5
9
 
6
- return `
10
+ return `
7
11
  cat > /opt/openclaw-runtime-server.ts << 'RUNTIME_SERVER_EOF'
8
12
  ${serverSource}
9
13
  RUNTIME_SERVER_EOF
@@ -12,4 +16,8 @@ mkdir -p "\$HOME/.vellum"
12
16
  nohup bun run /opt/openclaw-runtime-server.ts >> "\$HOME/.vellum/http-gateway.log" 2>&1 &
13
17
  echo "OpenClaw runtime server started (PID: \$!)"
14
18
  `;
19
+ } catch (err) {
20
+ console.warn("⚠️ Could not embed openclaw runtime server (expected in compiled binary without --embed):", (err as Error).message);
21
+ return "# openclaw-runtime-server: skipped (source files not available in compiled binary)";
22
+ }
15
23
  }
package/scripts/bump.ts DELETED
@@ -1,36 +0,0 @@
1
- import { resolve } from "path";
2
-
3
- const CLI_DIR = resolve(import.meta.dirname, "..");
4
- const ASSISTANT_DIR = resolve(CLI_DIR, "../assistant");
5
-
6
- const cliPkgPath = resolve(CLI_DIR, "package.json");
7
- const assistantPkgPath = resolve(ASSISTANT_DIR, "package.json");
8
- const assistantLockPath = resolve(ASSISTANT_DIR, "bun.lock");
9
-
10
- function bumpPatch(version: string): string {
11
- const parts = version.split(".");
12
- parts[2] = String(Number(parts[2]) + 1);
13
- return parts.join(".");
14
- }
15
-
16
- const cliPkg = await Bun.file(cliPkgPath).json();
17
- const oldVersion: string = cliPkg.version;
18
- const newVersion = bumpPatch(oldVersion);
19
- cliPkg.version = newVersion;
20
- await Bun.write(cliPkgPath, JSON.stringify(cliPkg, null, 2) + "\n");
21
-
22
- const assistantPkg = await Bun.file(assistantPkgPath).json();
23
- assistantPkg.dependencies["@vellumai/cli"] = newVersion;
24
- assistantPkg.version = bumpPatch(assistantPkg.version);
25
- await Bun.write(assistantPkgPath, JSON.stringify(assistantPkg, null, 2) + "\n");
26
-
27
- let lockContent = await Bun.file(assistantLockPath).text();
28
- lockContent = lockContent.replace(
29
- /"@vellumai\/cli": "[^"]*"/g,
30
- `"@vellumai/cli": "${newVersion}"`
31
- );
32
- lockContent = lockContent.replace(
33
- /@vellumai\/cli@\d+\.\d+\.\d+/g,
34
- `@vellumai/cli@${newVersion}`
35
- );
36
- await Bun.write(assistantLockPath, lockContent);