@vellumai/cli 0.8.1 → 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.
@@ -8,7 +8,6 @@ import {
8
8
  saveAssistantEntry,
9
9
  setActiveAssistant,
10
10
  } from "../lib/assistant-config";
11
- import { hatchAws } from "../lib/aws";
12
11
  import {
13
12
  SPECIES_CONFIG,
14
13
  VALID_REMOTE_HOSTS,
@@ -17,7 +16,6 @@ import {
17
16
  import type { RemoteHost, Species } from "../lib/constants";
18
17
  import { buildNestedConfig } from "../lib/config-utils";
19
18
  import { hatchDocker } from "../lib/docker";
20
- import { hatchGcp } from "../lib/gcp";
21
19
  import type { PollResult, WatchHatchingResult } from "../lib/gcp";
22
20
  import { hatchLocal } from "../lib/hatch-local";
23
21
  import {
@@ -169,6 +167,7 @@ source ${INSTALL_SCRIPT_REMOTE_PATH}
169
167
  }
170
168
 
171
169
  const DEFAULT_REMOTE: RemoteHost = "local";
170
+ const UNSUPPORTED_REMOTE_HATCH_TARGETS = new Set<RemoteHost>(["aws", "gcp"]);
172
171
 
173
172
  interface HatchArgs {
174
173
  species: Species;
@@ -177,7 +176,9 @@ interface HatchArgs {
177
176
  name: string | null;
178
177
  remote: RemoteHost;
179
178
  watch: boolean;
179
+ sourcePath: string | null;
180
180
  configValues: Record<string, string>;
181
+ analyze: boolean;
181
182
  }
182
183
 
183
184
  function parseArgs(): HatchArgs {
@@ -188,7 +189,9 @@ function parseArgs(): HatchArgs {
188
189
  let name: string | null = null;
189
190
  let remote: RemoteHost = DEFAULT_REMOTE;
190
191
  let watch = false;
192
+ let sourcePath: string | null = null;
191
193
  const configValues: Record<string, string> = {};
194
+ let analyze = false;
192
195
 
193
196
  for (let i = 0; i < args.length; i++) {
194
197
  const arg = args[i];
@@ -210,17 +213,33 @@ function parseArgs(): HatchArgs {
210
213
  console.log(
211
214
  " --watch Run assistant and gateway in watch mode (hot reload on source changes)",
212
215
  );
216
+ console.log(
217
+ " --source <path> Build images from a local source tree at <path> (no watcher). Useful for callers (e.g. evals) that want each run to pick up local CLI changes.",
218
+ );
213
219
  console.log(
214
220
  " --keep-alive Stay alive after hatch, exit when gateway stops",
215
221
  );
216
222
  console.log(
217
223
  " --config <key=value> Set a workspace config value (repeatable)",
218
224
  );
225
+ console.log(
226
+ " --analyze Emit a structured hatch-timing log line on stdout",
227
+ );
219
228
  process.exit(0);
220
229
  } else if (arg === "-d") {
221
230
  detached = true;
222
231
  } else if (arg === "--watch") {
223
232
  watch = true;
233
+ } else if (arg === "--analyze") {
234
+ analyze = true;
235
+ } else if (arg === "--source") {
236
+ const next = args[i + 1];
237
+ if (!next || next.startsWith("-")) {
238
+ console.error("Error: --source requires a path argument");
239
+ process.exit(1);
240
+ }
241
+ sourcePath = next;
242
+ i++;
224
243
  } else if (arg === "--keep-alive") {
225
244
  keepAlive = true;
226
245
  } else if (arg === "--name") {
@@ -270,7 +289,7 @@ function parseArgs(): HatchArgs {
270
289
  species = arg as Species;
271
290
  } else {
272
291
  console.error(
273
- `Error: Unknown argument '${arg}'. Valid options: ${VALID_SPECIES.join(", ")}, -d, --watch, --keep-alive, --name <name>, --remote <${VALID_REMOTE_HOSTS.join("|")}>, --config <key=value>`,
292
+ `Error: Unknown argument '${arg}'. Valid options: ${VALID_SPECIES.join(", ")}, -d, --watch, --source <path>, --keep-alive, --name <name>, --remote <${VALID_REMOTE_HOSTS.join("|")}>, --config <key=value>, --analyze`,
274
293
  );
275
294
  process.exit(1);
276
295
  }
@@ -283,7 +302,9 @@ function parseArgs(): HatchArgs {
283
302
  name,
284
303
  remote,
285
304
  watch,
305
+ sourcePath,
286
306
  configValues,
307
+ analyze,
287
308
  };
288
309
  }
289
310
 
@@ -508,8 +529,17 @@ export async function hatch(): Promise<void> {
508
529
  const cliVersion = getCliVersion();
509
530
  console.log(`@vellumai/cli v${cliVersion}`);
510
531
 
511
- const { species, detached, keepAlive, name, remote, watch, configValues } =
512
- parseArgs();
532
+ const {
533
+ species,
534
+ detached,
535
+ keepAlive,
536
+ name,
537
+ remote,
538
+ watch,
539
+ sourcePath,
540
+ configValues,
541
+ analyze,
542
+ } = parseArgs();
513
543
 
514
544
  if (watch && remote !== "local" && remote !== "docker") {
515
545
  console.error(
@@ -518,30 +548,33 @@ export async function hatch(): Promise<void> {
518
548
  process.exit(1);
519
549
  }
520
550
 
521
- if (remote === "local") {
522
- await hatchLocal(species, name, watch, keepAlive, configValues);
523
- return;
551
+ if (sourcePath !== null && remote !== "docker") {
552
+ console.error(
553
+ "Error: --source is only supported for docker hatch targets.",
554
+ );
555
+ process.exit(1);
524
556
  }
525
557
 
526
- if (remote === "gcp") {
527
- await hatchGcp(
528
- species,
529
- detached,
530
- name,
531
- buildStartupScript,
532
- watchHatching,
533
- configValues,
558
+ if (UNSUPPORTED_REMOTE_HATCH_TARGETS.has(remote)) {
559
+ console.error(
560
+ `Error: \`vellum hatch --remote ${remote}\` is not a supported provisioning target yet.`,
534
561
  );
535
- return;
562
+ console.error(
563
+ "No cloud resources were created. To self-host on AWS/GCP, SSH into the VM and run `vellum hatch` or `vellum hatch --remote docker` there.",
564
+ );
565
+ process.exit(1);
536
566
  }
537
567
 
538
- if (remote === "aws") {
539
- await hatchAws(species, detached, name, configValues);
568
+ if (remote === "local") {
569
+ await hatchLocal(species, name, watch, keepAlive, configValues);
540
570
  return;
541
571
  }
542
572
 
543
573
  if (remote === "docker") {
544
- await hatchDocker(species, detached, name, watch, configValues);
574
+ await hatchDocker(species, detached, name, watch, configValues, {
575
+ sourcePath,
576
+ analyze,
577
+ });
545
578
  return;
546
579
  }
547
580
 
@@ -1,80 +1,126 @@
1
- import { createInterface } from "readline";
2
-
3
1
  import { resolveAssistant } from "../lib/assistant-config.js";
2
+ import {
3
+ leaseGuardianToken,
4
+ loadGuardianToken,
5
+ refreshGuardianToken,
6
+ type GuardianTokenData,
7
+ } from "../lib/guardian-token.js";
8
+ import {
9
+ ensureProviderApiKey,
10
+ formatProviderName,
11
+ } from "../lib/provider-secrets.js";
12
+
13
+ function parseSetupArgs(args: string[]): { provider: string } {
14
+ let provider = "anthropic";
15
+
16
+ for (let i = 0; i < args.length; i++) {
17
+ const arg = args[i];
18
+ if (arg === "--provider") {
19
+ const value = args[i + 1];
20
+ if (!value || value.startsWith("-")) {
21
+ throw new Error("--provider requires a provider name.");
22
+ }
23
+ provider = value;
24
+ i++;
25
+ } else if (arg.startsWith("--provider=")) {
26
+ provider = arg.slice("--provider=".length);
27
+ } else {
28
+ throw new Error(`Unknown option: ${arg}`);
29
+ }
30
+ }
4
31
 
5
- async function promptMasked(prompt: string): Promise<string> {
6
- return new Promise((resolve) => {
7
- const rl = createInterface({
8
- input: process.stdin,
9
- output: process.stdout,
10
- });
32
+ return { provider };
33
+ }
11
34
 
12
- process.stdout.write(prompt);
35
+ function isGuardianAccessTokenUsable(
36
+ tokenData: GuardianTokenData | null,
37
+ ): tokenData is GuardianTokenData {
38
+ if (!tokenData?.accessToken) {
39
+ return false;
40
+ }
41
+ const expiresAt = new Date(tokenData.accessTokenExpiresAt).getTime();
42
+ return Number.isFinite(expiresAt) && expiresAt > Date.now();
43
+ }
13
44
 
14
- const stdin = process.stdin;
15
- const wasRaw = stdin.isRaw;
16
- if (stdin.isTTY) {
17
- stdin.setRawMode(true);
45
+ async function resolveSetupBearerToken(
46
+ entry: NonNullable<ReturnType<typeof resolveAssistant>>,
47
+ gatewayUrl: string,
48
+ ): Promise<string | undefined> {
49
+ const guardianToken = loadGuardianToken(entry.assistantId);
50
+ if (isGuardianAccessTokenUsable(guardianToken)) {
51
+ return guardianToken.accessToken;
52
+ }
53
+
54
+ if (guardianToken) {
55
+ const refreshedToken = await refreshGuardianToken(
56
+ gatewayUrl,
57
+ entry.assistantId,
58
+ );
59
+ if (isGuardianAccessTokenUsable(refreshedToken)) {
60
+ return refreshedToken.accessToken;
18
61
  }
62
+ }
19
63
 
20
- let input = "";
21
- const onData = (key: Buffer): void => {
22
- const char = key.toString("utf-8");
23
-
24
- if (char === "\r" || char === "\n") {
25
- stdin.removeListener("data", onData);
26
- if (stdin.isTTY) {
27
- stdin.setRawMode(wasRaw ?? false);
28
- }
29
- process.stdout.write("\n");
30
- rl.close();
31
- resolve(input);
32
- } else if (char === "\u0003") {
33
- process.stdout.write("\n");
34
- process.exit(1);
35
- } else if (char === "\u007F" || char === "\b") {
36
- if (input.length > 0) {
37
- input = input.slice(0, -1);
38
- process.stdout.write("\b \b");
39
- }
40
- } else if (char.length === 1 && char >= " ") {
41
- input += char;
42
- process.stdout.write("*");
64
+ const canLeaseGuardianToken =
65
+ entry.cloud === "local" || entry.cloud === "docker" || entry.localUrl;
66
+ if (canLeaseGuardianToken) {
67
+ try {
68
+ const bootstrapSecret =
69
+ typeof entry.guardianBootstrapSecret === "string"
70
+ ? entry.guardianBootstrapSecret
71
+ : undefined;
72
+ const leasedToken = await leaseGuardianToken(
73
+ gatewayUrl,
74
+ entry.assistantId,
75
+ bootstrapSecret,
76
+ );
77
+ if (isGuardianAccessTokenUsable(leasedToken)) {
78
+ return leasedToken.accessToken;
43
79
  }
44
- };
45
-
46
- stdin.on("data", onData);
47
- });
48
- }
49
-
50
- async function validateAnthropicKey(apiKey: string): Promise<boolean> {
51
- try {
52
- const resp = await fetch("https://api.anthropic.com/v1/models", {
53
- headers: {
54
- "x-api-key": apiKey,
55
- "anthropic-version": "2023-06-01",
56
- },
57
- signal: AbortSignal.timeout(10_000),
58
- });
59
- return resp.ok;
60
- } catch {
61
- return false;
80
+ } catch {
81
+ // Fall through to any lockfile bearer token, or let the setup request
82
+ // surface the gateway's auth error below.
83
+ }
62
84
  }
85
+
86
+ return entry.bearerToken;
63
87
  }
64
88
 
65
89
  export async function setup(): Promise<void> {
66
90
  const args = process.argv.slice(3);
67
91
 
68
92
  if (args.includes("--help") || args.includes("-h")) {
69
- console.log("Usage: vellum setup");
93
+ console.log("Usage: vellum setup [--provider <provider>]");
70
94
  console.log("");
71
- console.log("Interactive wizard to configure API keys.");
95
+ console.log("Configure a provider API key on the active assistant.");
96
+ console.log("");
97
+ console.log("Options:");
72
98
  console.log(
73
- "Injects secrets into your running assistant via the gateway API.",
99
+ " --provider <provider> Provider to configure. Defaults to anthropic.",
74
100
  );
101
+ console.log("");
102
+ console.log("Behavior:");
103
+ console.log(
104
+ " - Checks the active assistant for an existing provider key.",
105
+ );
106
+ console.log(" - Uses the matching environment variable when it is set.");
107
+ console.log(" - Otherwise prompts securely without echoing the key.");
108
+ console.log("");
109
+ console.log("Examples:");
110
+ console.log(" vellum setup");
111
+ console.log(" ANTHROPIC_API_KEY=... vellum setup");
112
+ console.log(" vellum setup --provider openai");
75
113
  process.exit(0);
76
114
  }
77
115
 
116
+ let parsed: { provider: string };
117
+ try {
118
+ parsed = parseSetupArgs(args);
119
+ } catch (error) {
120
+ console.error(error instanceof Error ? `Error: ${error.message}` : error);
121
+ process.exit(1);
122
+ }
123
+
78
124
  const entry = resolveAssistant();
79
125
  if (!entry) {
80
126
  console.error(
@@ -84,54 +130,47 @@ export async function setup(): Promise<void> {
84
130
  }
85
131
 
86
132
  const gatewayUrl = entry.localUrl ?? entry.runtimeUrl;
133
+ const bearerToken = await resolveSetupBearerToken(entry, gatewayUrl);
87
134
 
88
135
  console.log("Vellum Setup");
89
136
  console.log("============\n");
90
137
 
91
- const apiKey = await promptMasked(
92
- "Enter your Anthropic API key (sk-ant-...): ",
93
- );
94
-
95
- if (!apiKey.trim()) {
96
- console.error("Error: API key cannot be empty.");
97
- process.exit(1);
98
- }
138
+ try {
139
+ const result = await ensureProviderApiKey({
140
+ gatewayUrl,
141
+ provider: parsed.provider,
142
+ bearerToken,
143
+ env: process.env,
144
+ });
99
145
 
100
- console.log("Validating key...");
101
- const valid = await validateAnthropicKey(apiKey.trim());
146
+ if (result.status === "already_configured") {
147
+ console.log(
148
+ `${formatProviderName(result.provider)} API key is already configured.`,
149
+ );
150
+ return;
151
+ }
102
152
 
103
- if (!valid) {
104
- console.error(
105
- "Error: Invalid API key. Could not authenticate with the Anthropic API.",
106
- );
107
- process.exit(1);
108
- }
153
+ if (result.status === "configured") {
154
+ const providerName = formatProviderName(result.provider);
155
+ const source = result.source === "env" ? " from the environment" : "";
156
+ console.log(`\n${providerName} API key saved to assistant${source}.`);
157
+ console.log("Setup complete.");
158
+ return;
159
+ }
109
160
 
110
- const headers: Record<string, string> = {
111
- "Content-Type": "application/json",
112
- Accept: "application/json",
113
- };
114
- if (entry.bearerToken) {
115
- headers["Authorization"] = `Bearer ${entry.bearerToken}`;
116
- }
161
+ if (result.status === "skipped") {
162
+ console.log(result.message);
163
+ return;
164
+ }
117
165
 
118
- const response = await fetch(`${gatewayUrl}/v1/secrets`, {
119
- method: "POST",
120
- headers,
121
- body: JSON.stringify({
122
- type: "credential",
123
- name: "ANTHROPIC_API_KEY",
124
- value: apiKey.trim(),
125
- }),
126
- signal: AbortSignal.timeout(10_000),
127
- });
128
-
129
- if (!response.ok) {
166
+ console.error(`Error: ${result.message}`);
167
+ process.exit(1);
168
+ } catch (error) {
130
169
  console.error(
131
- `Error: Failed to store API key in assistant (${response.status}).`,
170
+ error instanceof Error
171
+ ? `Error: ${error.message}`
172
+ : "Error: Setup failed.",
132
173
  );
133
174
  process.exit(1);
134
175
  }
135
-
136
- console.log("\nAPI key saved to assistant. Setup complete.");
137
176
  }
@@ -877,7 +877,16 @@ export async function resolveOrHatchTarget(
877
877
  // Hatch a new assistant in the target environment
878
878
  if (targetEnv === "local") {
879
879
  const beforeIds = new Set(loadAllAssistants().map((e) => e.assistantId));
880
- await hatchLocal("vellum", targetName ?? null, false, false, {});
880
+ await hatchLocal(
881
+ "vellum",
882
+ targetName ?? null,
883
+ false,
884
+ false,
885
+ {},
886
+ {
887
+ setupProviderCredentials: false,
888
+ },
889
+ );
881
890
  const entry = targetName
882
891
  ? findAssistantByName(targetName)
883
892
  : (loadAllAssistants().find((e) => !beforeIds.has(e.assistantId)) ??
@@ -892,7 +901,16 @@ export async function resolveOrHatchTarget(
892
901
 
893
902
  if (targetEnv === "docker") {
894
903
  const beforeIds = new Set(loadAllAssistants().map((e) => e.assistantId));
895
- await hatchDocker("vellum", false, targetName ?? null, false, {});
904
+ await hatchDocker(
905
+ "vellum",
906
+ false,
907
+ targetName ?? null,
908
+ false,
909
+ {},
910
+ {
911
+ setupProviderCredentials: false,
912
+ },
913
+ );
896
914
  const entry = targetName
897
915
  ? findAssistantByName(targetName)
898
916
  : (loadAllAssistants().find((e) => !beforeIds.has(e.assistantId)) ??