@vellumai/cli 0.8.2 → 0.8.3

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
@@ -54,27 +54,25 @@ vellum hatch [species] [options]
54
54
 
55
55
  #### Options
56
56
 
57
- | Option | Description |
58
- | ------------------- | ---------------------------------------------------------------------------------------------- |
59
- | `-d` | Detached mode. Start the instance in the background without watching startup progress. |
60
- | `--name <name>` | Use a specific instance name instead of an auto-generated one. |
61
- | `--remote <target>` | Where to provision the instance. One of: `local`, `gcp`, `aws`, `custom`. Defaults to `local`. |
57
+ | Option | Description |
58
+ | ------------------- | ------------------------------------------------------------------------------------------------------------------ |
59
+ | `-d` | Detached mode. Start the instance in the background without watching startup progress. |
60
+ | `--name <name>` | Use a specific instance name instead of an auto-generated one. |
61
+ | `--remote <target>` | Where to provision the instance. One of: `local`, `docker`, `vellum`, `gcp`, `aws`, `custom`. Defaults to `local`. |
62
62
 
63
63
  #### Remote Targets
64
64
 
65
65
  - **`local`** -- Starts the local assistant and local gateway. Gateway source resolution order is: repo source tree, then installed `@vellumai/vellum-gateway` package.
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
- - **`aws`** -- Provisions an AWS instance.
68
- - **`custom`** -- Provisions on an arbitrary SSH host. Set `VELLUM_CUSTOM_HOST` (e.g. `user@hostname`) to specify the target.
66
+ - **`docker`** -- Starts the assistant, gateway, and credential service in Docker containers.
67
+ - **`vellum`** -- Hatches an assistant on the Vellum platform.
68
+ - **`gcp`** and **`aws`** -- Recognized but not supported as provisioning targets yet. The CLI exits before creating cloud resources. To self-host on AWS/GCP, SSH into the VM and run `vellum hatch` or `vellum hatch --remote docker` there.
69
+ - **`custom`** -- Recognized but not yet implemented.
69
70
 
70
71
  #### Environment Variables
71
72
 
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. |
73
+ | Variable | Required For | Description |
74
+ | ------------------- | ------------ | -------------------------------------------------------------------------- |
75
+ | `ANTHROPIC_API_KEY` | Optional | Used during setup when no Anthropic API key is already stored or prompted. |
78
76
 
79
77
  #### Examples
80
78
 
@@ -82,20 +80,14 @@ vellum hatch [species] [options]
82
80
  # Hatch a local assistant (default)
83
81
  vellum hatch
84
82
 
85
- # Hatch a vellum assistant on GCP
86
- vellum hatch vellum --remote gcp
87
-
88
- # Hatch an openclaw assistant on GCP in detached mode
89
- vellum hatch openclaw --remote gcp -d
83
+ # Hatch a Docker assistant
84
+ vellum hatch --remote docker
90
85
 
91
86
  # Hatch with a specific instance name
92
- vellum hatch --name my-assistant --remote gcp
93
-
94
- # Hatch on a custom SSH host
95
- VELLUM_CUSTOM_HOST=user@10.0.0.1 vellum hatch --remote custom
87
+ vellum hatch --name my-assistant --remote docker
96
88
  ```
97
89
 
98
- When hatching on GCP in interactive mode (without `-d`), the CLI displays an animated progress TUI that polls the instance's startup script output in real time. Press `Ctrl+C` to detach -- the instance will continue running in the background.
90
+ AWS and GCP hatch targets are recognized so users receive an explicit unsupported-target error instead of an unknown-option error. They currently exit without creating cloud resources; self-hosting on an AWS/GCP VM still works by running `vellum hatch` from inside that machine.
99
91
 
100
92
  ### `terminal`
101
93
 
@@ -111,18 +103,18 @@ Only available for managed assistants (those running in a Vellum Cloud container
111
103
 
112
104
  #### Subcommands
113
105
 
114
- | Subcommand | Description |
115
- | ------------------ | ------------------------------------------------------------------------ |
116
- | _(none)_ | Open an interactive shell session inside the container. |
117
- | `attach <session>` | Attach to an existing `tmux` session by name inside the container. |
118
- | `list` | List the `tmux` sessions currently running inside the container. |
106
+ | Subcommand | Description |
107
+ | ------------------ | ------------------------------------------------------------------ |
108
+ | _(none)_ | Open an interactive shell session inside the container. |
109
+ | `attach <session>` | Attach to an existing `tmux` session by name inside the container. |
110
+ | `list` | List the `tmux` sessions currently running inside the container. |
119
111
 
120
112
  #### Options
121
113
 
122
- | Option | Description |
123
- | -------------------- | -------------------------------------------------------------------------------------------- |
114
+ | Option | Description |
115
+ | -------------------- | --------------------------------------------------------------------------------------------------- |
124
116
  | `[name]` | Positional. Name of the assistant to target. Defaults to the active assistant set via `vellum use`. |
125
- | `--assistant <name>` | Explicit form of the assistant name. Equivalent to the positional argument. |
117
+ | `--assistant <name>` | Explicit form of the assistant name. Equivalent to the positional argument. |
126
118
 
127
119
  If no assistant is named and no active assistant is set, the CLI uses the only managed assistant in the lockfile -- or errors out if there's more than one. Use `vellum ps` to see your assistants and `vellum use <name>` to set the active one.
128
120
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/cli",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
4
4
  "description": "CLI tools for vellum-assistant",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,7 +1,11 @@
1
1
  import { readFileSync, rmSync } from "fs";
2
2
  import { describe, expect, test } from "bun:test";
3
3
 
4
- import { buildNestedConfig, writeInitialConfig } from "../lib/config-utils.js";
4
+ import {
5
+ buildHatchConfigValues,
6
+ buildNestedConfig,
7
+ writeInitialConfig,
8
+ } from "../lib/config-utils.js";
5
9
 
6
10
  function readInitialConfig(
7
11
  configValues: Record<string, string>,
@@ -32,6 +36,32 @@ describe("config-utils", () => {
32
36
  });
33
37
  });
34
38
 
39
+ test("buildHatchConfigValues adds the default hatch provider when no config exists", () => {
40
+ expect(buildHatchConfigValues({}, "anthropic")).toEqual({
41
+ "llm.default.provider": "anthropic",
42
+ });
43
+ });
44
+
45
+ test("buildHatchConfigValues preserves explicit provider config", () => {
46
+ expect(
47
+ buildHatchConfigValues(
48
+ {
49
+ "llm.default.provider": "openai",
50
+ "llm.default.model": "gpt-5.4",
51
+ },
52
+ "anthropic",
53
+ ),
54
+ ).toEqual({
55
+ "llm.default.provider": "openai",
56
+ "llm.default.model": "gpt-5.4",
57
+ });
58
+ });
59
+
60
+ test("buildHatchConfigValues skips internal hatches without provider setup", () => {
61
+ expect(buildHatchConfigValues({}, undefined)).toEqual({});
62
+ expect(buildHatchConfigValues({}, null)).toEqual({});
63
+ });
64
+
35
65
  test("writeInitialConfig does not add a mainAgent callSite for Anthropic defaults", () => {
36
66
  expect(
37
67
  readInitialConfig({
@@ -0,0 +1,284 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ configureHatchProviderApiKey,
5
+ resolveHatchProvider,
6
+ type ProviderSecretFetch,
7
+ } from "../lib/provider-secrets.js";
8
+
9
+ interface RecordedFetchCall {
10
+ url: string;
11
+ init?: RequestInit;
12
+ body: unknown;
13
+ }
14
+
15
+ function jsonResponse(body: unknown, status = 200): Response {
16
+ return new Response(JSON.stringify(body), {
17
+ status,
18
+ headers: { "Content-Type": "application/json" },
19
+ });
20
+ }
21
+
22
+ function makeFetch(responses: Response[]): {
23
+ calls: RecordedFetchCall[];
24
+ fetchImpl: ProviderSecretFetch;
25
+ } {
26
+ const calls: RecordedFetchCall[] = [];
27
+ const fetchImpl: ProviderSecretFetch = async (input, init) => {
28
+ calls.push({
29
+ url: String(input),
30
+ init,
31
+ body: typeof init?.body === "string" ? JSON.parse(init.body) : init?.body,
32
+ });
33
+ const response = responses.shift();
34
+ if (!response) {
35
+ throw new Error("Unexpected fetch call.");
36
+ }
37
+ return response;
38
+ };
39
+
40
+ return { calls, fetchImpl };
41
+ }
42
+
43
+ describe("hatch provider secrets", () => {
44
+ test("defaults hatch provider setup to Anthropic", () => {
45
+ expect(resolveHatchProvider({})).toBe("anthropic");
46
+ });
47
+
48
+ test("uses llm.default.provider override for hatch provider setup", () => {
49
+ expect(resolveHatchProvider({ "llm.default.provider": "openai" })).toBe(
50
+ "openai",
51
+ );
52
+ });
53
+
54
+ test("uses active profile provider for hatch provider setup", () => {
55
+ expect(
56
+ resolveHatchProvider({
57
+ "llm.activeProfile": "work",
58
+ "llm.profiles.work.provider": "openai",
59
+ }),
60
+ ).toBe("openai");
61
+ });
62
+
63
+ test("infers active profile provider from model for hatch provider setup", () => {
64
+ expect(
65
+ resolveHatchProvider({
66
+ "llm.activeProfile": "work",
67
+ "llm.profiles.work.model": "gpt-5.4",
68
+ }),
69
+ ).toBe("openai");
70
+ });
71
+
72
+ test("active profile provider wins over main agent call-site provider", () => {
73
+ expect(
74
+ resolveHatchProvider({
75
+ "llm.activeProfile": "work",
76
+ "llm.profiles.work.provider": "openai",
77
+ "llm.callSites.mainAgent.provider": "gemini",
78
+ }),
79
+ ).toBe("openai");
80
+ });
81
+
82
+ test("uses default provider before static main agent call-site provider", () => {
83
+ expect(
84
+ resolveHatchProvider({
85
+ "llm.default.provider": "anthropic",
86
+ "llm.callSites.mainAgent.provider": "gemini",
87
+ }),
88
+ ).toBe("anthropic");
89
+ });
90
+
91
+ test("uses default provider before static main agent call-site profile", () => {
92
+ expect(
93
+ resolveHatchProvider({
94
+ "llm.default.provider": "anthropic",
95
+ "llm.callSites.mainAgent.profile": "work",
96
+ "llm.profiles.work.provider": "openai",
97
+ }),
98
+ ).toBe("anthropic");
99
+ });
100
+
101
+ test("uses main agent call-site provider when no hatch default exists", () => {
102
+ expect(
103
+ resolveHatchProvider({
104
+ "llm.callSites.mainAgent.provider": "gemini",
105
+ }),
106
+ ).toBe("gemini");
107
+ });
108
+
109
+ test("uses main agent call-site profile when no hatch default exists", () => {
110
+ expect(
111
+ resolveHatchProvider({
112
+ "llm.callSites.mainAgent.profile": "work",
113
+ "llm.profiles.work.provider": "openai",
114
+ }),
115
+ ).toBe("openai");
116
+ });
117
+
118
+ test("infers default provider from model before falling back to Anthropic", () => {
119
+ expect(
120
+ resolveHatchProvider({ "llm.default.model": "gemini-2.5-flash" }),
121
+ ).toBe("gemini");
122
+ });
123
+
124
+ test("skips hatch provider setup for ollama", () => {
125
+ expect(
126
+ resolveHatchProvider({ "llm.default.provider": "ollama" }),
127
+ ).toBeNull();
128
+ });
129
+
130
+ test("skips hatch provider setup for active Ollama profile", () => {
131
+ expect(
132
+ resolveHatchProvider({
133
+ "llm.activeProfile": "local",
134
+ "llm.profiles.local.model": "llama3.2",
135
+ }),
136
+ ).toBeNull();
137
+ });
138
+
139
+ test("rejects unsupported hatch providers before hatch starts", () => {
140
+ expect(() =>
141
+ resolveHatchProvider({ "llm.default.provider": "custom" }),
142
+ ).toThrow("supported API-key setup flow");
143
+ });
144
+
145
+ test("configures default Anthropic credentials from the environment", async () => {
146
+ const { calls, fetchImpl } = makeFetch([
147
+ jsonResponse({ found: false }),
148
+ jsonResponse({ success: true }),
149
+ ]);
150
+ const logs: string[] = [];
151
+
152
+ await configureHatchProviderApiKey({
153
+ gatewayUrl: "http://127.0.0.1:7830",
154
+ provider: resolveHatchProvider({}),
155
+ bearerToken: "guardian-token",
156
+ env: { ANTHROPIC_API_KEY: "test-anthropic-key" },
157
+ fetchImpl,
158
+ log: (message) => logs.push(message),
159
+ });
160
+
161
+ expect(calls).toHaveLength(2);
162
+ expect(calls[0].body).toEqual({
163
+ type: "api_key",
164
+ name: "anthropic",
165
+ reveal: false,
166
+ });
167
+ expect(calls[0].init?.headers).toMatchObject({
168
+ Authorization: "Bearer guardian-token",
169
+ });
170
+ expect(calls[1].body).toEqual({
171
+ type: "api_key",
172
+ name: "anthropic",
173
+ value: "test-anthropic-key",
174
+ });
175
+ expect(logs.join("\n")).toContain(
176
+ "Configured Anthropic credentials from ANTHROPIC_API_KEY.",
177
+ );
178
+ expect(logs.join("\n")).not.toContain("test-anthropic-key");
179
+ });
180
+
181
+ test("uses OpenAI override and skips prompt when credentials already exist", async () => {
182
+ const { calls, fetchImpl } = makeFetch([jsonResponse({ found: true })]);
183
+ const logs: string[] = [];
184
+ let prompted = false;
185
+
186
+ await configureHatchProviderApiKey({
187
+ gatewayUrl: "http://127.0.0.1:7830",
188
+ provider: resolveHatchProvider({ "llm.default.provider": "openai" }),
189
+ bearerToken: "guardian-token",
190
+ env: {},
191
+ fetchImpl,
192
+ prompt: async () => {
193
+ prompted = true;
194
+ return "unused";
195
+ },
196
+ log: (message) => logs.push(message),
197
+ });
198
+
199
+ expect(prompted).toBe(false);
200
+ expect(calls).toHaveLength(1);
201
+ expect(calls[0].body).toEqual({
202
+ type: "api_key",
203
+ name: "openai",
204
+ reveal: false,
205
+ });
206
+ expect(logs.join("\n")).toContain(
207
+ "Provider credentials already configured for OpenAI.",
208
+ );
209
+ });
210
+
211
+ test("uses active profile provider when selecting environment credential", async () => {
212
+ const { calls, fetchImpl } = makeFetch([
213
+ jsonResponse({ found: false }),
214
+ jsonResponse({ success: true }),
215
+ ]);
216
+
217
+ await configureHatchProviderApiKey({
218
+ gatewayUrl: "http://127.0.0.1:7830",
219
+ provider: resolveHatchProvider({
220
+ "llm.activeProfile": "work",
221
+ "llm.profiles.work.provider": "openai",
222
+ }),
223
+ bearerToken: "guardian-token",
224
+ env: { OPENAI_API_KEY: "test-openai-key" },
225
+ fetchImpl,
226
+ log: () => {},
227
+ });
228
+
229
+ expect(calls[0].body).toEqual({
230
+ type: "api_key",
231
+ name: "openai",
232
+ reveal: false,
233
+ });
234
+ expect(calls[1].body).toEqual({
235
+ type: "api_key",
236
+ name: "openai",
237
+ value: "test-openai-key",
238
+ });
239
+ });
240
+
241
+ test("keeps hatch recoverable when provider credentials are missing in a non-interactive shell", async () => {
242
+ const { fetchImpl } = makeFetch([jsonResponse({ found: false })]);
243
+ const logs: string[] = [];
244
+
245
+ await configureHatchProviderApiKey({
246
+ gatewayUrl: "http://127.0.0.1:7830",
247
+ provider: "anthropic",
248
+ env: {},
249
+ fetchImpl,
250
+ stdinIsTTY: false,
251
+ log: (message) => logs.push(message),
252
+ });
253
+
254
+ const output = logs.join("\n");
255
+ expect(output).toContain("Provider credential setup skipped");
256
+ expect(output).toContain("Missing ANTHROPIC_API_KEY");
257
+ expect(output).toContain("vellum setup --provider anthropic");
258
+ });
259
+
260
+ test("surfaces gateway validation failures without throwing or logging the key", async () => {
261
+ const { fetchImpl } = makeFetch([
262
+ jsonResponse({ found: false }),
263
+ jsonResponse(
264
+ { error: { message: "API key is invalid or expired." } },
265
+ 400,
266
+ ),
267
+ ]);
268
+ const logs: string[] = [];
269
+
270
+ await configureHatchProviderApiKey({
271
+ gatewayUrl: "http://127.0.0.1:7830",
272
+ provider: "anthropic",
273
+ env: { ANTHROPIC_API_KEY: "test-anthropic-key" },
274
+ fetchImpl,
275
+ log: (message) => logs.push(message),
276
+ });
277
+
278
+ const output = logs.join("\n");
279
+ expect(output).toContain("Provider credential setup failed");
280
+ expect(output).toContain("API key is invalid or expired.");
281
+ expect(output).toContain("vellum setup --provider anthropic");
282
+ expect(output).not.toContain("test-anthropic-key");
283
+ });
284
+ });
@@ -75,7 +75,10 @@ function writeLockfile(entry: AssistantEntry): void {
75
75
  }
76
76
 
77
77
  function installFetchStub(
78
- options: { refreshedToken?: GuardianTokenData } = {},
78
+ options: {
79
+ leasedToken?: GuardianTokenData;
80
+ refreshedToken?: GuardianTokenData;
81
+ } = {},
79
82
  ) {
80
83
  fetchCalls = [];
81
84
  globalThis.fetch = (async (input, init) => {
@@ -103,6 +106,19 @@ function installFetchStub(
103
106
  });
104
107
  }
105
108
 
109
+ if (url.endsWith("/v1/guardian/init")) {
110
+ if (!options.leasedToken) {
111
+ return new Response(JSON.stringify({ error: "not allowed" }), {
112
+ status: 401,
113
+ headers: { "Content-Type": "application/json" },
114
+ });
115
+ }
116
+ return new Response(JSON.stringify(options.leasedToken), {
117
+ status: 200,
118
+ headers: { "Content-Type": "application/json" },
119
+ });
120
+ }
121
+
106
122
  if (url.endsWith("/v1/secrets/read")) {
107
123
  return new Response(JSON.stringify({ found: false }), {
108
124
  status: 200,
@@ -209,6 +225,54 @@ describe("setup command", () => {
209
225
  );
210
226
  });
211
227
 
228
+ test("leases a guardian token for local assistants when no token exists", async () => {
229
+ process.env.ANTHROPIC_API_KEY = "test-anthropic-key";
230
+ writeLockfile({
231
+ assistantId: "assistant-123",
232
+ runtimeUrl: "http://runtime.example",
233
+ localUrl: "http://127.0.0.1:3000",
234
+ cloud: "local",
235
+ });
236
+ installFetchStub({
237
+ leasedToken: guardianTokenFixture({
238
+ accessToken: "leased-guardian-token",
239
+ }),
240
+ });
241
+
242
+ await setup();
243
+
244
+ expect(fetchCalls[0].url).toBe("http://127.0.0.1:3000/v1/guardian/init");
245
+ expect(fetchCalls[1].url).toBe("http://127.0.0.1:3000/v1/secrets/read");
246
+ expect(fetchCalls[1].headers.get("Authorization")).toBe(
247
+ "Bearer leased-guardian-token",
248
+ );
249
+ });
250
+
251
+ test("uses saved bootstrap secret when leasing a Docker guardian token", async () => {
252
+ process.env.ANTHROPIC_API_KEY = "test-anthropic-key";
253
+ writeLockfile({
254
+ assistantId: "assistant-123",
255
+ runtimeUrl: "http://localhost:7831",
256
+ cloud: "docker",
257
+ guardianBootstrapSecret: "test-bootstrap-secret",
258
+ });
259
+ installFetchStub({
260
+ leasedToken: guardianTokenFixture({
261
+ accessToken: "leased-docker-token",
262
+ }),
263
+ });
264
+
265
+ await setup();
266
+
267
+ expect(fetchCalls[0].url).toBe("http://localhost:7831/v1/guardian/init");
268
+ expect(fetchCalls[0].headers.get("x-bootstrap-secret")).toBe(
269
+ "test-bootstrap-secret",
270
+ );
271
+ expect(fetchCalls[1].headers.get("Authorization")).toBe(
272
+ "Bearer leased-docker-token",
273
+ );
274
+ });
275
+
212
276
  test("falls back to runtime URL and lockfile bearer token", async () => {
213
277
  process.env.ANTHROPIC_API_KEY = "test-anthropic-key";
214
278
  writeLockfile({
@@ -779,6 +779,7 @@ describe("resolveOrHatchTarget", () => {
779
779
  "new-one",
780
780
  false,
781
781
  {},
782
+ { setupProviderCredentials: false },
782
783
  );
783
784
  expect(result).toBe(newEntry);
784
785
  });