@vellumai/cli 0.8.0 → 0.8.2
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 +1 -1
- package/src/__tests__/backup.test.ts +13 -3
- package/src/__tests__/input-history.test.ts +102 -0
- package/src/__tests__/orphan-detection.test.ts +287 -0
- package/src/__tests__/preload.ts +5 -1
- package/src/__tests__/provider-secrets.test.ts +290 -0
- package/src/__tests__/ps-platform-status.test.ts +182 -0
- package/src/__tests__/search-provider-env-var-parity.test.ts +48 -0
- package/src/__tests__/setup.test.ts +296 -0
- package/src/__tests__/sync-events.test.ts +54 -0
- package/src/__tests__/teleport.test.ts +190 -163
- package/src/commands/client.ts +128 -10
- package/src/commands/events.ts +13 -1
- package/src/commands/login.ts +3 -2
- package/src/commands/ps.ts +28 -17
- package/src/commands/setup.ts +101 -96
- package/src/components/DefaultMainScreen.tsx +80 -128
- package/src/lib/__tests__/docker.test.ts +11 -0
- package/src/lib/assistant-config.ts +69 -2
- package/src/lib/client-identity.ts +1 -0
- package/src/lib/environments/paths.ts +21 -0
- package/src/lib/input-history.ts +5 -8
- package/src/lib/orphan-detection.ts +66 -1
- package/src/lib/platform-client.ts +8 -7
- package/src/lib/provider-secrets.ts +413 -0
- package/src/lib/statefulset.ts +12 -0
- package/src/lib/sync-cloud-assistants.ts +39 -18
- package/src/lib/upgrade-lifecycle.ts +9 -73
- package/src/shared/provider-env-vars.ts +15 -8
- package/src/lib/doctor-client.ts +0 -153
package/src/commands/login.ts
CHANGED
|
@@ -287,9 +287,10 @@ export async function login(): Promise<void> {
|
|
|
287
287
|
|
|
288
288
|
// Sync cloud assistants from the platform into the local lockfile.
|
|
289
289
|
// This ensures `vellum ps` shows managed assistants immediately
|
|
290
|
-
// after login (e.g. after a retire-and-rehatch cycle).
|
|
290
|
+
// after login (e.g. after a retire-and-rehatch cycle). We've just
|
|
291
|
+
// saved this token, so it's guaranteed non-empty here.
|
|
291
292
|
try {
|
|
292
|
-
const result = await syncCloudAssistants();
|
|
293
|
+
const result = await syncCloudAssistants(token);
|
|
293
294
|
if (result) {
|
|
294
295
|
const total = result.added + result.removed;
|
|
295
296
|
if (total > 0) {
|
package/src/commands/ps.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
fetchManagedPs,
|
|
16
16
|
type ManagedProcessEntry,
|
|
17
17
|
} from "../lib/health-check";
|
|
18
|
+
import { readPlatformToken } from "../lib/platform-client";
|
|
18
19
|
import { dockerResourceNames } from "../lib/docker";
|
|
19
20
|
import { existsSync } from "fs";
|
|
20
21
|
import {
|
|
@@ -472,7 +473,7 @@ async function showAssistantProcesses(entry: AssistantEntry): Promise<void> {
|
|
|
472
473
|
|
|
473
474
|
// ── List all assistants (no arg) ────────────────────────────────
|
|
474
475
|
|
|
475
|
-
async function listAllAssistants(verbose: boolean): Promise<void> {
|
|
476
|
+
export async function listAllAssistants(verbose: boolean): Promise<void> {
|
|
476
477
|
const { name: envName, source: envSource } = resolveEnvironmentSource();
|
|
477
478
|
const sourceLabels: Record<typeof envSource, string> = {
|
|
478
479
|
flag: "--environment flag",
|
|
@@ -486,23 +487,33 @@ async function listAllAssistants(verbose: boolean): Promise<void> {
|
|
|
486
487
|
? (msg) => console.log(` [verbose] ${msg}`)
|
|
487
488
|
: undefined;
|
|
488
489
|
|
|
489
|
-
//
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
//
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
if (syncResult.added > 0 || syncResult.removed > 0) {
|
|
497
|
-
const changes: string[] = [];
|
|
498
|
-
if (syncResult.added > 0) changes.push(`${syncResult.added} added`);
|
|
499
|
-
if (syncResult.removed > 0)
|
|
500
|
-
changes.push(`${syncResult.removed} removed`);
|
|
501
|
-
parts.push(`(${changes.join(", ")})`);
|
|
502
|
-
}
|
|
503
|
-
console.log(parts.join(" "));
|
|
504
|
-
} else {
|
|
490
|
+
// Decide platform login status FIRST, before touching the network. With no
|
|
491
|
+
// local token we never enter the platform fetch path — so unreachable-host
|
|
492
|
+
// errors from the org-ID/user lookups can't leak onto stderr ahead of the
|
|
493
|
+
// "Platform: not logged in" line.
|
|
494
|
+
const platformToken = readPlatformToken();
|
|
495
|
+
if (!platformToken) {
|
|
496
|
+
log?.("No platform token found — skipping cloud sync");
|
|
505
497
|
console.log("Platform: not logged in");
|
|
498
|
+
} else {
|
|
499
|
+
const syncResult = await syncCloudAssistants(platformToken, { log });
|
|
500
|
+
if (syncResult) {
|
|
501
|
+
const parts = [`Platform: logged in`];
|
|
502
|
+
if (syncResult.email) parts[0] += ` as ${syncResult.email}`;
|
|
503
|
+
if (syncResult.added > 0 || syncResult.removed > 0) {
|
|
504
|
+
const changes: string[] = [];
|
|
505
|
+
if (syncResult.added > 0) changes.push(`${syncResult.added} added`);
|
|
506
|
+
if (syncResult.removed > 0)
|
|
507
|
+
changes.push(`${syncResult.removed} removed`);
|
|
508
|
+
parts.push(`(${changes.join(", ")})`);
|
|
509
|
+
}
|
|
510
|
+
console.log(parts.join(" "));
|
|
511
|
+
} else {
|
|
512
|
+
// We had a token but the platform fetch failed (offline, expired, etc.).
|
|
513
|
+
// Treat it the same as "not logged in" from a UX perspective — the user
|
|
514
|
+
// can't reach cloud-managed assistants right now either way.
|
|
515
|
+
console.log("Platform: not logged in");
|
|
516
|
+
}
|
|
506
517
|
}
|
|
507
518
|
console.log("");
|
|
508
519
|
|
package/src/commands/setup.ts
CHANGED
|
@@ -1,80 +1,81 @@
|
|
|
1
|
-
import { createInterface } from "readline";
|
|
2
|
-
|
|
3
1
|
import { resolveAssistant } from "../lib/assistant-config.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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("*");
|
|
2
|
+
import {
|
|
3
|
+
loadGuardianToken,
|
|
4
|
+
refreshGuardianToken,
|
|
5
|
+
type GuardianTokenData,
|
|
6
|
+
} from "../lib/guardian-token.js";
|
|
7
|
+
import {
|
|
8
|
+
ensureProviderApiKey,
|
|
9
|
+
formatProviderName,
|
|
10
|
+
} from "../lib/provider-secrets.js";
|
|
11
|
+
|
|
12
|
+
function parseSetupArgs(args: string[]): { provider: string } {
|
|
13
|
+
let provider = "anthropic";
|
|
14
|
+
|
|
15
|
+
for (let i = 0; i < args.length; i++) {
|
|
16
|
+
const arg = args[i];
|
|
17
|
+
if (arg === "--provider") {
|
|
18
|
+
const value = args[i + 1];
|
|
19
|
+
if (!value || value.startsWith("-")) {
|
|
20
|
+
throw new Error("--provider requires a provider name.");
|
|
43
21
|
}
|
|
44
|
-
|
|
22
|
+
provider = value;
|
|
23
|
+
i++;
|
|
24
|
+
} else if (arg.startsWith("--provider=")) {
|
|
25
|
+
provider = arg.slice("--provider=".length);
|
|
26
|
+
} else {
|
|
27
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
45
30
|
|
|
46
|
-
|
|
47
|
-
});
|
|
31
|
+
return { provider };
|
|
48
32
|
}
|
|
49
33
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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 {
|
|
34
|
+
function isGuardianAccessTokenUsable(
|
|
35
|
+
tokenData: GuardianTokenData | null,
|
|
36
|
+
): tokenData is GuardianTokenData {
|
|
37
|
+
if (!tokenData?.accessToken) {
|
|
61
38
|
return false;
|
|
62
39
|
}
|
|
40
|
+
const expiresAt = new Date(tokenData.accessTokenExpiresAt).getTime();
|
|
41
|
+
return Number.isFinite(expiresAt) && expiresAt > Date.now();
|
|
63
42
|
}
|
|
64
43
|
|
|
65
44
|
export async function setup(): Promise<void> {
|
|
66
45
|
const args = process.argv.slice(3);
|
|
67
46
|
|
|
68
47
|
if (args.includes("--help") || args.includes("-h")) {
|
|
69
|
-
console.log("Usage: vellum setup");
|
|
48
|
+
console.log("Usage: vellum setup [--provider <provider>]");
|
|
49
|
+
console.log("");
|
|
50
|
+
console.log("Configure a provider API key on the active assistant.");
|
|
70
51
|
console.log("");
|
|
71
|
-
console.log("
|
|
52
|
+
console.log("Options:");
|
|
72
53
|
console.log(
|
|
73
|
-
"
|
|
54
|
+
" --provider <provider> Provider to configure. Defaults to anthropic.",
|
|
74
55
|
);
|
|
56
|
+
console.log("");
|
|
57
|
+
console.log("Behavior:");
|
|
58
|
+
console.log(
|
|
59
|
+
" - Checks the active assistant for an existing provider key.",
|
|
60
|
+
);
|
|
61
|
+
console.log(" - Uses the matching environment variable when it is set.");
|
|
62
|
+
console.log(" - Otherwise prompts securely without echoing the key.");
|
|
63
|
+
console.log("");
|
|
64
|
+
console.log("Examples:");
|
|
65
|
+
console.log(" vellum setup");
|
|
66
|
+
console.log(" ANTHROPIC_API_KEY=... vellum setup");
|
|
67
|
+
console.log(" vellum setup --provider openai");
|
|
75
68
|
process.exit(0);
|
|
76
69
|
}
|
|
77
70
|
|
|
71
|
+
let parsed: { provider: string };
|
|
72
|
+
try {
|
|
73
|
+
parsed = parseSetupArgs(args);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error(error instanceof Error ? `Error: ${error.message}` : error);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
78
79
|
const entry = resolveAssistant();
|
|
79
80
|
if (!entry) {
|
|
80
81
|
console.error(
|
|
@@ -84,54 +85,58 @@ export async function setup(): Promise<void> {
|
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
const gatewayUrl = entry.localUrl ?? entry.runtimeUrl;
|
|
88
|
+
let bearerToken: string | undefined;
|
|
89
|
+
const guardianToken = loadGuardianToken(entry.assistantId);
|
|
90
|
+
if (isGuardianAccessTokenUsable(guardianToken)) {
|
|
91
|
+
bearerToken = guardianToken.accessToken;
|
|
92
|
+
} else {
|
|
93
|
+
const refreshedToken = guardianToken
|
|
94
|
+
? await refreshGuardianToken(gatewayUrl, entry.assistantId)
|
|
95
|
+
: null;
|
|
96
|
+
bearerToken = isGuardianAccessTokenUsable(refreshedToken)
|
|
97
|
+
? refreshedToken.accessToken
|
|
98
|
+
: entry.bearerToken;
|
|
99
|
+
}
|
|
87
100
|
|
|
88
101
|
console.log("Vellum Setup");
|
|
89
102
|
console.log("============\n");
|
|
90
103
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
console.log("Validating key...");
|
|
101
|
-
const valid = await validateAnthropicKey(apiKey.trim());
|
|
104
|
+
try {
|
|
105
|
+
const result = await ensureProviderApiKey({
|
|
106
|
+
gatewayUrl,
|
|
107
|
+
provider: parsed.provider,
|
|
108
|
+
bearerToken,
|
|
109
|
+
env: process.env,
|
|
110
|
+
});
|
|
102
111
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
112
|
+
if (result.status === "already_configured") {
|
|
113
|
+
console.log(
|
|
114
|
+
`${formatProviderName(result.provider)} API key is already configured.`,
|
|
115
|
+
);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
109
118
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
119
|
+
if (result.status === "configured") {
|
|
120
|
+
const providerName = formatProviderName(result.provider);
|
|
121
|
+
const source = result.source === "env" ? " from the environment" : "";
|
|
122
|
+
console.log(`\n${providerName} API key saved to assistant${source}.`);
|
|
123
|
+
console.log("Setup complete.");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
117
126
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
type: "credential",
|
|
123
|
-
name: "ANTHROPIC_API_KEY",
|
|
124
|
-
value: apiKey.trim(),
|
|
125
|
-
}),
|
|
126
|
-
signal: AbortSignal.timeout(10_000),
|
|
127
|
-
});
|
|
127
|
+
if (result.status === "skipped") {
|
|
128
|
+
console.log(result.message);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
128
131
|
|
|
129
|
-
|
|
132
|
+
console.error(`Error: ${result.message}`);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
} catch (error) {
|
|
130
135
|
console.error(
|
|
131
|
-
|
|
136
|
+
error instanceof Error
|
|
137
|
+
? `Error: ${error.message}`
|
|
138
|
+
: "Error: Setup failed.",
|
|
132
139
|
);
|
|
133
140
|
process.exit(1);
|
|
134
141
|
}
|
|
135
|
-
|
|
136
|
-
console.log("\nAPI key saved to assistant. Setup complete.");
|
|
137
142
|
}
|