@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 +3 -1
- package/package.json +1 -2
- package/src/adapters/install.sh +20 -2
- package/src/commands/hatch.ts +309 -63
- package/src/commands/ps.ts +92 -0
- package/src/commands/retire.ts +61 -23
- package/src/commands/sleep.ts +63 -0
- package/src/commands/wake.ts +37 -0
- package/src/index.ts +9 -0
- package/src/lib/assistant-config.ts +5 -0
- package/src/lib/aws.ts +19 -0
- package/src/lib/gcp.ts +19 -0
- package/src/lib/interfaces-seed.ts +15 -8
- package/src/lib/openclaw-runtime-server.ts +11 -3
- package/scripts/bump.ts +0 -36
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ vellum-cli hatch [species] [options]
|
|
|
39
39
|
|
|
40
40
|
#### Remote Targets
|
|
41
41
|
|
|
42
|
-
- **`local`** -- Starts
|
|
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.
|
|
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
|
},
|
package/src/adapters/install.sh
CHANGED
|
@@ -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\";
|
|
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
|
-
|
|
113
|
+
vellum hatch
|
|
96
114
|
fi
|
|
97
115
|
}
|
|
98
116
|
|
package/src/commands/hatch.ts
CHANGED
|
@@ -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
|
-
|
|
308
|
-
|
|
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
|
-
|
|
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
|
-
|
|
698
|
-
|
|
699
|
-
"
|
|
700
|
-
"
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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
|
-
|
|
778
|
-
|
|
779
|
-
|
|
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
|
-
|
|
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
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
const
|
|
793
|
-
const
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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
|
-
|
|
805
|
-
|
|
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
|
|
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
|
-
|
|
1117
|
+
if (!daemonOnly) {
|
|
1118
|
+
saveAssistantEntry(localEntry);
|
|
875
1119
|
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
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
|
-
|
|
888
|
-
const
|
|
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
|
+
}
|
package/src/commands/retire.ts
CHANGED
|
@@ -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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7
|
+
try {
|
|
8
|
+
const serverSource = await Bun.file(import.meta.dir + "/../adapters/openclaw-http-server.ts").text();
|
|
5
9
|
|
|
6
|
-
|
|
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);
|