@vellumai/cli 0.1.9 → 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/package.json +1 -1
- package/src/adapters/install.sh +20 -2
- package/src/commands/hatch.ts +58 -43
- package/src/commands/ps.ts +92 -0
- package/src/commands/wake.ts +37 -0
- package/src/index.ts +6 -0
- package/src/lib/assistant-config.ts +4 -0
- package/src/lib/aws.ts +18 -0
- package/src/lib/gcp.ts +18 -0
package/package.json
CHANGED
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
|
@@ -781,7 +781,14 @@ async function hatchCustom(
|
|
|
781
781
|
}
|
|
782
782
|
|
|
783
783
|
function isGatewaySourceDir(dir: string): boolean {
|
|
784
|
-
|
|
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
|
+
}
|
|
785
792
|
}
|
|
786
793
|
|
|
787
794
|
function findGatewaySourceFromCwd(): string | undefined {
|
|
@@ -891,14 +898,7 @@ async function discoverPublicUrl(): Promise<string | undefined> {
|
|
|
891
898
|
return undefined;
|
|
892
899
|
}
|
|
893
900
|
|
|
894
|
-
async function
|
|
895
|
-
const instanceName =
|
|
896
|
-
name ?? process.env.VELLUM_ASSISTANT_NAME ?? `${species}-${generateRandomSuffix()}`;
|
|
897
|
-
|
|
898
|
-
console.log(`🥚 Hatching local assistant: ${instanceName}`);
|
|
899
|
-
console.log(` Species: ${species}`);
|
|
900
|
-
console.log("");
|
|
901
|
-
|
|
901
|
+
export async function startLocalDaemon(): Promise<void> {
|
|
902
902
|
if (process.env.VELLUM_DESKTOP_APP) {
|
|
903
903
|
// When running inside the desktop app, the CLI owns the daemon lifecycle.
|
|
904
904
|
// Find the vellum-daemon binary adjacent to the CLI binary.
|
|
@@ -1045,6 +1045,54 @@ async function hatchLocal(species: Species, name: string | null, daemonOnly: boo
|
|
|
1045
1045
|
child.on("error", reject);
|
|
1046
1046
|
});
|
|
1047
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
|
+
}
|
|
1055
|
+
|
|
1056
|
+
console.log("🌐 Starting gateway...");
|
|
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
|
+
|
|
1076
|
+
const gateway = spawn("bun", ["run", "src/index.ts"], {
|
|
1077
|
+
cwd: gatewayDir,
|
|
1078
|
+
detached: true,
|
|
1079
|
+
stdio: "ignore",
|
|
1080
|
+
env: gatewayEnv,
|
|
1081
|
+
});
|
|
1082
|
+
gateway.unref();
|
|
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();
|
|
1048
1096
|
|
|
1049
1097
|
// The desktop app communicates with the daemon directly via Unix socket,
|
|
1050
1098
|
// so the HTTP gateway is only needed for non-desktop (CLI) usage.
|
|
@@ -1054,40 +1102,7 @@ async function hatchLocal(species: Species, name: string | null, daemonOnly: boo
|
|
|
1054
1102
|
// No gateway needed — the macOS app uses DaemonClient over the Unix socket.
|
|
1055
1103
|
runtimeUrl = "local";
|
|
1056
1104
|
} else {
|
|
1057
|
-
|
|
1058
|
-
if (publicUrl) {
|
|
1059
|
-
console.log(` Public URL: ${publicUrl}`);
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
console.log("🌐 Starting gateway...");
|
|
1063
|
-
const gatewayDir = resolveGatewayDir();
|
|
1064
|
-
const gatewayEnv: Record<string, string> = {
|
|
1065
|
-
...process.env as Record<string, string>,
|
|
1066
|
-
GATEWAY_RUNTIME_PROXY_ENABLED: "true",
|
|
1067
|
-
GATEWAY_RUNTIME_PROXY_REQUIRE_AUTH: "false",
|
|
1068
|
-
};
|
|
1069
|
-
const workspaceIngressPublicBaseUrl = readWorkspaceIngressPublicBaseUrl();
|
|
1070
|
-
const ingressPublicBaseUrl =
|
|
1071
|
-
workspaceIngressPublicBaseUrl
|
|
1072
|
-
?? normalizeIngressUrl(process.env.INGRESS_PUBLIC_BASE_URL);
|
|
1073
|
-
if (ingressPublicBaseUrl) {
|
|
1074
|
-
gatewayEnv.INGRESS_PUBLIC_BASE_URL = ingressPublicBaseUrl;
|
|
1075
|
-
console.log(` Ingress URL: ${ingressPublicBaseUrl}`);
|
|
1076
|
-
if (!workspaceIngressPublicBaseUrl) {
|
|
1077
|
-
console.log(" (using INGRESS_PUBLIC_BASE_URL env fallback)");
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
if (publicUrl) gatewayEnv.GATEWAY_PUBLIC_URL = publicUrl;
|
|
1081
|
-
|
|
1082
|
-
const gateway = spawn("bun", ["run", "src/index.ts"], {
|
|
1083
|
-
cwd: gatewayDir,
|
|
1084
|
-
detached: true,
|
|
1085
|
-
stdio: "ignore",
|
|
1086
|
-
env: gatewayEnv,
|
|
1087
|
-
});
|
|
1088
|
-
gateway.unref();
|
|
1089
|
-
console.log("✅ Gateway started\n");
|
|
1090
|
-
runtimeUrl = publicUrl || `http://localhost:${GATEWAY_PORT}`;
|
|
1105
|
+
runtimeUrl = await startGateway();
|
|
1091
1106
|
}
|
|
1092
1107
|
|
|
1093
1108
|
const baseDataDir = join(process.env.BASE_DATA_DIR?.trim() || (process.env.HOME ?? userInfo().homedir), ".vellum");
|
|
@@ -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
|
+
}
|
|
@@ -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,13 +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";
|
|
5
6
|
import { sleep } from "./commands/sleep";
|
|
7
|
+
import { wake } from "./commands/wake";
|
|
6
8
|
|
|
7
9
|
const commands = {
|
|
8
10
|
hatch,
|
|
11
|
+
ps,
|
|
9
12
|
retire,
|
|
10
13
|
sleep,
|
|
14
|
+
wake,
|
|
11
15
|
} as const;
|
|
12
16
|
|
|
13
17
|
type CommandName = keyof typeof commands;
|
|
@@ -21,8 +25,10 @@ async function main() {
|
|
|
21
25
|
console.log("");
|
|
22
26
|
console.log("Commands:");
|
|
23
27
|
console.log(" hatch Create a new assistant instance");
|
|
28
|
+
console.log(" ps List assistants and their health status");
|
|
24
29
|
console.log(" retire Delete an assistant instance");
|
|
25
30
|
console.log(" sleep Stop the daemon process");
|
|
31
|
+
console.log(" wake Start the daemon and gateway");
|
|
26
32
|
process.exit(0);
|
|
27
33
|
}
|
|
28
34
|
|
|
@@ -89,6 +89,10 @@ export function removeAssistantEntry(assistantId: string): void {
|
|
|
89
89
|
writeAssistants(entries.filter((e) => e.assistantId !== assistantId));
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
export function loadAllAssistants(): AssistantEntry[] {
|
|
93
|
+
return readAssistants();
|
|
94
|
+
}
|
|
95
|
+
|
|
92
96
|
export function saveAssistantEntry(entry: AssistantEntry): void {
|
|
93
97
|
const entries = readAssistants();
|
|
94
98
|
entries.unshift(entry);
|
package/src/lib/aws.ts
CHANGED
|
@@ -556,11 +556,29 @@ async function getInstanceIdByName(
|
|
|
556
556
|
}
|
|
557
557
|
}
|
|
558
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
|
+
|
|
559
568
|
export async function retireInstance(
|
|
560
569
|
name: string,
|
|
561
570
|
region: string,
|
|
562
571
|
source?: string,
|
|
563
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
|
+
|
|
564
582
|
const instanceId = await getInstanceIdByName(name, region);
|
|
565
583
|
if (!instanceId) {
|
|
566
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(
|