@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/cli",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "CLI tools for vellum-assistant",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
 
@@ -781,7 +781,14 @@ async function hatchCustom(
781
781
  }
782
782
 
783
783
  function isGatewaySourceDir(dir: string): boolean {
784
- return existsSync(join(dir, "package.json")) && existsSync(join(dir, "src", "index.ts"));
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 hatchLocal(species: Species, name: string | null, daemonOnly: boolean = false): Promise<void> {
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
- const publicUrl = await discoverPublicUrl();
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(