@vellumai/cli 0.4.48 → 0.4.50

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
@@ -62,21 +62,19 @@ vellum hatch [species] [options]
62
62
 
63
63
  #### Remote Targets
64
64
 
65
- - **`local`** -- Starts the local assistant and local gateway. Gateway source resolution order is: `VELLUM_GATEWAY_DIR` override, repo source tree, then installed `@vellumai/vellum-gateway` package.
65
+ - **`local`** -- Starts the local assistant and local gateway. Gateway source resolution order is: repo source tree, then installed `@vellumai/vellum-gateway` package.
66
66
  - **`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.
67
67
  - **`aws`** -- Provisions an AWS instance.
68
68
  - **`custom`** -- Provisions on an arbitrary SSH host. Set `VELLUM_CUSTOM_HOST` (e.g. `user@hostname`) to specify the target.
69
69
 
70
70
  #### Environment Variables
71
71
 
72
- | Variable | Required For | Description |
73
- | ------------------------- | ------------ | -------------------------------------------------------------------------------------------------- |
74
- | `ANTHROPIC_API_KEY` | All | Anthropic API key passed to the assistant runtime. |
75
- | `GCP_PROJECT` | `gcp` | GCP project ID. Falls back to the active `gcloud` project. |
76
- | `GCP_DEFAULT_ZONE` | `gcp` | GCP zone for the compute instance. |
77
- | `VELLUM_CUSTOM_HOST` | `custom` | SSH host in `user@hostname` format. |
78
- | `VELLUM_GATEWAY_DIR` | `local` | Optional absolute path to a local gateway source directory to run instead of the packaged gateway. |
79
- | `INGRESS_PUBLIC_BASE_URL` | `local` | Optional fallback public ingress URL when `ingress.publicBaseUrl` is not set in workspace config. |
72
+ | Variable | Required For | Description |
73
+ | -------------------- | ------------ | ---------------------------------------------------------- |
74
+ | `ANTHROPIC_API_KEY` | All | Anthropic API key passed to the assistant runtime. |
75
+ | `GCP_PROJECT` | `gcp` | GCP project ID. Falls back to the active `gcloud` project. |
76
+ | `GCP_DEFAULT_ZONE` | `gcp` | GCP zone for the compute instance. |
77
+ | `VELLUM_CUSTOM_HOST` | `custom` | SSH host in `user@hostname` format. |
80
78
 
81
79
  #### Examples
82
80
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/cli",
3
- "version": "0.4.48",
3
+ "version": "0.4.50",
4
4
  "description": "CLI tools for vellum-assistant",
5
5
  "type": "module",
6
6
  "exports": {
@@ -229,14 +229,14 @@ symlink_vellum() {
229
229
  symlink_cli "assistant"
230
230
  }
231
231
 
232
- # Write a small sourceable env file to ~/.config/vellum/env so callers can
233
- # pick up PATH changes without restarting their shell:
232
+ # Append PATH setup to ~/.config/vellum/env so callers can pick up PATH
233
+ # changes without restarting their shell:
234
234
  # curl -fsSL https://assistant.vellum.ai/install.sh | bash && . ~/.config/vellum/env
235
235
  write_env_file() {
236
236
  local env_dir="${XDG_CONFIG_HOME:-$HOME/.config}/vellum"
237
237
  local env_file="$env_dir/env"
238
238
  mkdir -p "$env_dir"
239
- cat > "$env_file" <<'ENVEOF'
239
+ cat >> "$env_file" <<'ENVEOF'
240
240
  export BUN_INSTALL="$HOME/.bun"
241
241
  case ":$PATH:" in
242
242
  *":$BUN_INSTALL/bin:"*) ;;
@@ -278,8 +278,8 @@ main() {
278
278
  info "Note: 'assistant' command may require opening a new terminal session"
279
279
  fi
280
280
 
281
- # Write a sourceable env file so the quickstart one-liner can pick up
282
- # PATH changes in the caller's shell:
281
+ # Append PATH config to the env file so the quickstart one-liner can
282
+ # pick up PATH changes in the caller's shell:
283
283
  # curl ... | bash && . ~/.config/vellum/env
284
284
  write_env_file
285
285
 
@@ -292,7 +292,7 @@ main() {
292
292
  info "Running vellum hatch..."
293
293
  printf "\n"
294
294
  if [ -n "${VELLUM_SSH_USER:-}" ] && [ "$(id -u)" = "0" ]; then
295
- su - "$VELLUM_SSH_USER" -c "set -a; [ -f \"\$HOME/.vellum/.env\" ] && . \"\$HOME/.vellum/.env\"; set +a; export PATH=\"$HOME/.bun/bin:\$PATH\"; vellum hatch"
295
+ su - "$VELLUM_SSH_USER" -c "set -a; [ -f \"\$HOME/.config/vellum/env\" ] && . \"\$HOME/.config/vellum/env\"; set +a; export PATH=\"$HOME/.bun/bin:\$PATH\"; vellum hatch"
296
296
  else
297
297
  vellum hatch
298
298
  fi
@@ -6,7 +6,9 @@ export async function clean(): Promise<void> {
6
6
  if (args.includes("--help") || args.includes("-h")) {
7
7
  console.log("Usage: vellum clean");
8
8
  console.log("");
9
- console.log("Kill all orphaned vellum processes that are not tracked by any assistant.");
9
+ console.log(
10
+ "Kill all orphaned vellum processes that are not tracked by any assistant.",
11
+ );
10
12
  process.exit(0);
11
13
  }
12
14
 
@@ -17,18 +19,21 @@ export async function clean(): Promise<void> {
17
19
  return;
18
20
  }
19
21
 
20
- console.log(`Found ${orphans.length} orphaned process${orphans.length === 1 ? "" : "es"}.\n`);
22
+ console.log(
23
+ `Found ${orphans.length} orphaned process${orphans.length === 1 ? "" : "es"}.\n`,
24
+ );
21
25
 
22
26
  let killed = 0;
23
27
  for (const orphan of orphans) {
24
28
  const pid = parseInt(orphan.pid, 10);
25
- const stopped = await stopProcess(pid, `${orphan.name} (PID ${orphan.pid})`);
29
+ const stopped = await stopProcess(
30
+ pid,
31
+ `${orphan.name} (PID ${orphan.pid})`,
32
+ );
26
33
  if (stopped) {
27
34
  killed++;
28
35
  }
29
36
  }
30
37
 
31
- console.log(
32
- `\nCleaned up ${killed} process${killed === 1 ? "" : "es"}.`,
33
- );
38
+ console.log(`\nCleaned up ${killed} process${killed === 1 ? "" : "es"}.`);
34
39
  }
@@ -60,9 +60,7 @@ function parseArgs(): ParsedArgs {
60
60
  }
61
61
  } else {
62
62
  const hasExplicitUrl =
63
- process.env.RUNTIME_URL ||
64
- flagArgs.includes("--url") ||
65
- flagArgs.includes("-u");
63
+ flagArgs.includes("--url") || flagArgs.includes("-u");
66
64
  const active = getActiveAssistant();
67
65
  if (active) {
68
66
  entry = findAssistantByName(active);
@@ -84,15 +82,9 @@ function parseArgs(): ParsedArgs {
84
82
  }
85
83
  }
86
84
 
87
- let runtimeUrl =
88
- process.env.RUNTIME_URL ||
89
- entry?.localUrl ||
90
- entry?.runtimeUrl ||
91
- FALLBACK_RUNTIME_URL;
92
- let assistantId =
93
- process.env.ASSISTANT_ID || entry?.assistantId || FALLBACK_ASSISTANT_ID;
94
- const bearerToken =
95
- process.env.RUNTIME_PROXY_BEARER_TOKEN || entry?.bearerToken || undefined;
85
+ let runtimeUrl = entry?.localUrl || entry?.runtimeUrl || FALLBACK_RUNTIME_URL;
86
+ let assistantId = entry?.assistantId || FALLBACK_ASSISTANT_ID;
87
+ const bearerToken = entry?.bearerToken || undefined;
96
88
  const species: Species = (entry?.species as Species) ?? "vellum";
97
89
 
98
90
  for (let i = 0; i < flagArgs.length; i++) {
@@ -177,7 +169,7 @@ ${ANSI.bold}OPTIONS:${ANSI.reset}
177
169
 
178
170
  ${ANSI.bold}DEFAULTS:${ANSI.reset}
179
171
  Reads from ~/.vellum.lock.json (created by vellum hatch).
180
- Override with flags above or env vars RUNTIME_URL / ASSISTANT_ID.
172
+ Override with flags above.
181
173
 
182
174
  ${ANSI.bold}EXAMPLES:${ANSI.reset}
183
175
  vellum client
@@ -103,7 +103,7 @@ export async function buildStartupScript(
103
103
  cloud: RemoteHost,
104
104
  ): Promise<string> {
105
105
  const platformUrl =
106
- process.env.VELLUM_ASSISTANT_PLATFORM_URL ?? "https://assistant.vellum.ai";
106
+ process.env.VELLUM_PLATFORM_URL ?? "https://assistant.vellum.ai";
107
107
  const logPath =
108
108
  cloud === "custom"
109
109
  ? "/tmp/vellum-startup.log"
@@ -133,28 +133,13 @@ ${timestampRedirect}
133
133
  trap 'EXIT_CODE=\$?; if [ \$EXIT_CODE -ne 0 ]; then echo "Startup script failed with exit code \$EXIT_CODE at line \$LINENO" > ${errorPath}; echo "Last 20 log lines:" >> ${errorPath}; tail -20 ${logPath} >> ${errorPath} 2>/dev/null || true; fi' EXIT
134
134
  ${userSetup}
135
135
  ANTHROPIC_API_KEY=${anthropicApiKey}
136
- GATEWAY_RUNTIME_PROXY_ENABLED=true
137
- RUNTIME_PROXY_BEARER_TOKEN=${bearerToken}
138
136
  VELLUM_ASSISTANT_NAME=${instanceName}
139
- VELLUM_CLOUD=${cloud}
140
- mkdir -p "\$HOME/.vellum"
141
- cat > "\$HOME/.vellum/.env" << DOTENV_EOF
137
+ mkdir -p "\$HOME/.config/vellum"
138
+ cat > "\$HOME/.config/vellum/env" << DOTENV_EOF
142
139
  ANTHROPIC_API_KEY=\$ANTHROPIC_API_KEY
143
- GATEWAY_RUNTIME_PROXY_ENABLED=\$GATEWAY_RUNTIME_PROXY_ENABLED
144
- RUNTIME_PROXY_BEARER_TOKEN=\$RUNTIME_PROXY_BEARER_TOKEN
145
140
  RUNTIME_HTTP_PORT=7821
146
- VELLUM_CLOUD=\$VELLUM_CLOUD
147
141
  DOTENV_EOF
148
142
 
149
- mkdir -p "\$HOME/.vellum/workspace"
150
- cat > "\$HOME/.vellum/workspace/config.json" << CONFIG_EOF
151
- {
152
- "logFile": {
153
- "dir": "\$HOME/.vellum/workspace/data/logs"
154
- }
155
- }
156
- CONFIG_EOF
157
-
158
143
  ${ownershipFixup}
159
144
 
160
145
  export VELLUM_SSH_USER="\$SSH_USER"
@@ -743,7 +728,10 @@ async function hatchLocal(
743
728
  `🧹 Found ${orphans.length} orphaned process${orphans.length === 1 ? "" : "es"} — cleaning up...`,
744
729
  );
745
730
  for (const orphan of orphans) {
746
- await stopProcess(parseInt(orphan.pid, 10), `${orphan.name} (PID ${orphan.pid})`);
731
+ await stopProcess(
732
+ parseInt(orphan.pid, 10),
733
+ `${orphan.name} (PID ${orphan.pid})`,
734
+ );
747
735
  }
748
736
  }
749
737
  }
@@ -776,7 +764,7 @@ async function hatchLocal(
776
764
 
777
765
  let runtimeUrl: string;
778
766
  try {
779
- runtimeUrl = await startGateway(instanceName, watch, resources);
767
+ runtimeUrl = await startGateway(watch, resources);
780
768
  } catch (error) {
781
769
  // Gateway failed — stop the daemon we just started so we don't leave
782
770
  // orphaned processes with no lock file entry.
@@ -6,7 +6,7 @@ import {
6
6
  loadAllAssistants,
7
7
  type AssistantEntry,
8
8
  } from "../lib/assistant-config";
9
- import { checkHealth } from "../lib/health-check";
9
+ import { checkHealth, checkManagedHealth } from "../lib/health-check";
10
10
  import {
11
11
  classifyProcess,
12
12
  detectOrphanedProcesses,
@@ -150,7 +150,13 @@ async function detectProcess(spec: ProcessSpec): Promise<DetectedProcess> {
150
150
  const pids = await pgrepExact(spec.pgrepName);
151
151
  if (pids.length > 0) {
152
152
  const watch = await isWatchMode(pids[0]);
153
- return { name: spec.name, pid: pids[0], port: spec.port, running: true, watch };
153
+ return {
154
+ name: spec.name,
155
+ pid: pids[0],
156
+ port: spec.port,
157
+ running: true,
158
+ watch,
159
+ };
154
160
  }
155
161
 
156
162
  // Tier 2: TCP port probe (skip for processes without a port)
@@ -171,10 +177,22 @@ async function detectProcess(spec: ProcessSpec): Promise<DetectedProcess> {
171
177
  const filePid = readPidFile(spec.pidFile);
172
178
  if (filePid && isProcessAlive(filePid)) {
173
179
  const watch = await isWatchMode(filePid);
174
- return { name: spec.name, pid: filePid, port: spec.port, running: true, watch };
180
+ return {
181
+ name: spec.name,
182
+ pid: filePid,
183
+ port: spec.port,
184
+ running: true,
185
+ watch,
186
+ };
175
187
  }
176
188
 
177
- return { name: spec.name, pid: null, port: spec.port, running: false, watch: false };
189
+ return {
190
+ name: spec.name,
191
+ pid: null,
192
+ port: spec.port,
193
+ running: false,
194
+ watch: false,
195
+ };
178
196
  }
179
197
 
180
198
  function formatDetectionInfo(proc: DetectedProcess): string {
@@ -341,6 +359,8 @@ async function listAllAssistants(): Promise<void> {
341
359
  } else {
342
360
  health = await checkHealth(a.localUrl ?? a.runtimeUrl, a.bearerToken);
343
361
  }
362
+ } else if (a.cloud === "vellum") {
363
+ health = await checkManagedHealth(a.runtimeUrl, a.assistantId);
344
364
  } else {
345
365
  health = await checkHealth(a.localUrl ?? a.runtimeUrl, a.bearerToken);
346
366
  }
@@ -69,7 +69,7 @@ export async function recover(): Promise<void> {
69
69
  // 7. Start daemon + gateway (same as wake)
70
70
  await startLocalDaemon(false, entry.resources);
71
71
  if (!process.env.VELLUM_DESKTOP_APP) {
72
- await startGateway(undefined, false, entry.resources);
72
+ await startGateway(false, entry.resources);
73
73
  }
74
74
 
75
75
  console.log(`✅ Recovered assistant '${name}'.`);
@@ -9,10 +9,7 @@ import {
9
9
  removeAssistantEntry,
10
10
  } from "../lib/assistant-config";
11
11
  import type { AssistantEntry } from "../lib/assistant-config";
12
- import {
13
- getPlatformUrl,
14
- readPlatformToken,
15
- } from "../lib/platform-client";
12
+ import { getPlatformUrl, readPlatformToken } from "../lib/platform-client";
16
13
  import { retireInstance as retireAwsInstance } from "../lib/aws";
17
14
  import { retireDocker } from "../lib/docker";
18
15
  import { retireInstance as retireGcpInstance } from "../lib/gcp";
@@ -83,7 +80,7 @@ async function retireLocal(name: string, entry: AssistantEntry): Promise<void> {
83
80
  const daemonStopped = await stopProcessByPidFile(daemonPidFile, "daemon");
84
81
 
85
82
  // Stop gateway via PID file — use a longer timeout because the gateway has a
86
- // configurable drain window (GATEWAY_SHUTDOWN_DRAIN_MS, default 5s) before it exits.
83
+ // drain window (5s) before it exits.
87
84
  const gatewayPidFile = join(vellumDir, "gateway.pid");
88
85
  await stopProcessByPidFile(gatewayPidFile, "gateway", undefined, 7000);
89
86
 
@@ -0,0 +1,172 @@
1
+ import { createInterface } from "readline";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
3
+ import { homedir } from "os";
4
+ import { dirname, join } from "path";
5
+
6
+ function getVellumDir(): string {
7
+ const base = process.env.BASE_DATA_DIR?.trim() || homedir();
8
+ return join(base, ".vellum");
9
+ }
10
+
11
+ function getEnvFilePath(): string {
12
+ return join(getVellumDir(), ".env");
13
+ }
14
+
15
+ function readEnvFile(): Record<string, string> {
16
+ const envPath = getEnvFilePath();
17
+ const vars: Record<string, string> = {};
18
+ if (!existsSync(envPath)) return vars;
19
+
20
+ const content = readFileSync(envPath, "utf-8");
21
+ for (const line of content.split("\n")) {
22
+ const trimmed = line.trim();
23
+ if (!trimmed || trimmed.startsWith("#")) continue;
24
+ const eqIdx = trimmed.indexOf("=");
25
+ if (eqIdx === -1) continue;
26
+ const key = trimmed.slice(0, eqIdx).trim();
27
+ const value = trimmed.slice(eqIdx + 1).trim();
28
+ vars[key] = value;
29
+ }
30
+ return vars;
31
+ }
32
+
33
+ function writeEnvFile(vars: Record<string, string>): void {
34
+ const envPath = getEnvFilePath();
35
+ const dir = dirname(envPath);
36
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
37
+
38
+ const lines = Object.entries(vars).map(([k, v]) => `${k}=${v}`);
39
+ writeFileSync(envPath, lines.join("\n") + "\n", { mode: 0o600 });
40
+ }
41
+
42
+ async function promptMasked(prompt: string): Promise<string> {
43
+ return new Promise((resolve) => {
44
+ const rl = createInterface({
45
+ input: process.stdin,
46
+ output: process.stdout,
47
+ });
48
+
49
+ // Disable echoing by writing the prompt manually and intercepting keystrokes
50
+ process.stdout.write(prompt);
51
+
52
+ const stdin = process.stdin;
53
+ const wasRaw = stdin.isRaw;
54
+ if (stdin.isTTY) {
55
+ stdin.setRawMode(true);
56
+ }
57
+
58
+ let input = "";
59
+ const onData = (key: Buffer): void => {
60
+ const char = key.toString("utf-8");
61
+
62
+ if (char === "\r" || char === "\n") {
63
+ // Enter pressed
64
+ stdin.removeListener("data", onData);
65
+ if (stdin.isTTY) {
66
+ stdin.setRawMode(wasRaw ?? false);
67
+ }
68
+ process.stdout.write("\n");
69
+ rl.close();
70
+ resolve(input);
71
+ } else if (char === "\u0003") {
72
+ // Ctrl+C
73
+ process.stdout.write("\n");
74
+ process.exit(1);
75
+ } else if (char === "\u007F" || char === "\b") {
76
+ // Backspace
77
+ if (input.length > 0) {
78
+ input = input.slice(0, -1);
79
+ process.stdout.write("\b \b");
80
+ }
81
+ } else if (char.length === 1 && char >= " ") {
82
+ input += char;
83
+ process.stdout.write("*");
84
+ }
85
+ };
86
+
87
+ stdin.on("data", onData);
88
+ });
89
+ }
90
+
91
+ async function validateAnthropicKey(apiKey: string): Promise<boolean> {
92
+ try {
93
+ const resp = await fetch("https://api.anthropic.com/v1/models", {
94
+ headers: {
95
+ "x-api-key": apiKey,
96
+ "anthropic-version": "2023-06-01",
97
+ },
98
+ signal: AbortSignal.timeout(10_000),
99
+ });
100
+ return resp.ok;
101
+ } catch {
102
+ return false;
103
+ }
104
+ }
105
+
106
+ export async function setup(): Promise<void> {
107
+ const args = process.argv.slice(3);
108
+
109
+ if (args.includes("--help") || args.includes("-h")) {
110
+ console.log("Usage: vellum setup");
111
+ console.log("");
112
+ console.log("Interactive wizard to configure API keys.");
113
+ console.log(
114
+ "Keys are validated against their APIs and saved to <BASE_DATA_DIR>/.vellum/.env.",
115
+ );
116
+ process.exit(0);
117
+ }
118
+
119
+ console.log("Vellum Setup");
120
+ console.log("============\n");
121
+
122
+ const existingVars = readEnvFile();
123
+ const hasExistingKey = !!existingVars.ANTHROPIC_API_KEY;
124
+
125
+ if (hasExistingKey) {
126
+ const masked =
127
+ existingVars.ANTHROPIC_API_KEY.slice(0, 7) +
128
+ "..." +
129
+ existingVars.ANTHROPIC_API_KEY.slice(-4);
130
+ console.log(`Anthropic API key is already configured (${masked}).`);
131
+
132
+ const rl = createInterface({
133
+ input: process.stdin,
134
+ output: process.stdout,
135
+ });
136
+ const answer = await new Promise<string>((resolve) => {
137
+ rl.question("Overwrite? [y/N] ", resolve);
138
+ });
139
+ rl.close();
140
+
141
+ if (answer.trim().toLowerCase() !== "y") {
142
+ console.log("\nSetup complete. No changes made.");
143
+ return;
144
+ }
145
+ console.log("");
146
+ }
147
+
148
+ const apiKey = await promptMasked(
149
+ "Enter your Anthropic API key (sk-ant-...): ",
150
+ );
151
+
152
+ if (!apiKey.trim()) {
153
+ console.error("Error: API key cannot be empty.");
154
+ process.exit(1);
155
+ }
156
+
157
+ console.log("Validating key...");
158
+ const valid = await validateAnthropicKey(apiKey.trim());
159
+
160
+ if (!valid) {
161
+ console.error(
162
+ "Error: Invalid API key. Could not authenticate with the Anthropic API.",
163
+ );
164
+ process.exit(1);
165
+ }
166
+
167
+ existingVars.ANTHROPIC_API_KEY = apiKey.trim();
168
+ writeEnvFile(existingVars);
169
+
170
+ console.log(`\nAPI key saved to ${getEnvFilePath()}`);
171
+ console.log("Setup complete.");
172
+ }
@@ -120,7 +120,7 @@ export async function sleep(): Promise<void> {
120
120
  }
121
121
 
122
122
  // Stop gateway — use a longer timeout because the gateway has a configurable
123
- // drain window (GATEWAY_SHUTDOWN_DRAIN_MS, default 5s) before it exits.
123
+ // drain window (5s) before it exits.
124
124
  const gatewayStopped = await stopProcessByPidFile(
125
125
  gatewayPidFile,
126
126
  "gateway",
@@ -87,12 +87,12 @@ export async function wake(): Promise<void> {
87
87
  `Gateway running (pid ${pid}) — restarting in watch mode...`,
88
88
  );
89
89
  await stopProcessByPidFile(gatewayPidFile, "gateway");
90
- await startGateway(undefined, watch, resources);
90
+ await startGateway(watch, resources);
91
91
  } else {
92
92
  console.log(`Gateway already running (pid ${pid}).`);
93
93
  }
94
94
  } else {
95
- await startGateway(undefined, watch, resources);
95
+ await startGateway(watch, resources);
96
96
  }
97
97
  }
98
98