@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.
- package/README.md +24 -32
- package/package.json +1 -1
- package/src/__tests__/backup.test.ts +13 -3
- package/src/__tests__/config-utils.test.ts +31 -1
- package/src/__tests__/hatch-provider-secrets.test.ts +284 -0
- package/src/__tests__/input-history.test.ts +102 -0
- package/src/__tests__/preload.ts +5 -1
- package/src/__tests__/provider-secrets.test.ts +290 -0
- package/src/__tests__/setup.test.ts +360 -0
- package/src/__tests__/teleport.test.ts +191 -163
- package/src/commands/client.ts +57 -1
- package/src/commands/hatch.ts +53 -20
- package/src/commands/setup.ts +134 -95
- package/src/commands/teleport.ts +20 -2
- package/src/components/DefaultMainScreen.tsx +72 -119
- package/src/lib/__tests__/docker.test.ts +106 -0
- package/src/lib/assistant-config.ts +6 -2
- package/src/lib/config-utils.ts +18 -0
- package/src/lib/docker.ts +180 -19
- package/src/lib/environments/paths.ts +21 -0
- package/src/lib/hatch-local.ts +42 -3
- package/src/lib/hatch-next-steps.ts +12 -0
- package/src/lib/input-history.ts +5 -8
- package/src/lib/provider-secrets.ts +564 -0
- package/src/lib/sync-cloud-assistants.ts +23 -9
- package/src/lib/doctor-client.ts +0 -153
package/src/commands/hatch.ts
CHANGED
|
@@ -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 {
|
|
512
|
-
|
|
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
|
|
522
|
-
|
|
523
|
-
|
|
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
|
|
527
|
-
|
|
528
|
-
|
|
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
|
-
|
|
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 === "
|
|
539
|
-
await
|
|
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
|
|
package/src/commands/setup.ts
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
7
|
-
const rl = createInterface({
|
|
8
|
-
input: process.stdin,
|
|
9
|
-
output: process.stdout,
|
|
10
|
-
});
|
|
32
|
+
return { provider };
|
|
33
|
+
}
|
|
11
34
|
|
|
12
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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("
|
|
95
|
+
console.log("Configure a provider API key on the active assistant.");
|
|
96
|
+
console.log("");
|
|
97
|
+
console.log("Options:");
|
|
72
98
|
console.log(
|
|
73
|
-
"
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
138
|
+
try {
|
|
139
|
+
const result = await ensureProviderApiKey({
|
|
140
|
+
gatewayUrl,
|
|
141
|
+
provider: parsed.provider,
|
|
142
|
+
bearerToken,
|
|
143
|
+
env: process.env,
|
|
144
|
+
});
|
|
99
145
|
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/commands/teleport.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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)) ??
|