@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.
@@ -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) {
@@ -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
- // Refresh cloud assistants from the platform before listing.
490
- const syncResult = await syncCloudAssistants({ log });
491
-
492
- // Show platform login status
493
- if (syncResult) {
494
- const parts = [`Platform: logged in`];
495
- if (syncResult.email) parts[0] += ` as ${syncResult.email}`;
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
 
@@ -1,80 +1,81 @@
1
- import { createInterface } from "readline";
2
-
3
1
  import { resolveAssistant } from "../lib/assistant-config.js";
4
-
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
- });
11
-
12
- process.stdout.write(prompt);
13
-
14
- const stdin = process.stdin;
15
- const wasRaw = stdin.isRaw;
16
- if (stdin.isTTY) {
17
- stdin.setRawMode(true);
18
- }
19
-
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("*");
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
- stdin.on("data", onData);
47
- });
31
+ return { provider };
48
32
  }
49
33
 
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 {
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("Interactive wizard to configure API keys.");
52
+ console.log("Options:");
72
53
  console.log(
73
- "Injects secrets into your running assistant via the gateway API.",
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
- 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
- }
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
- if (!valid) {
104
- console.error(
105
- "Error: Invalid API key. Could not authenticate with the Anthropic API.",
106
- );
107
- process.exit(1);
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
- 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
- }
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
- 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
- });
127
+ if (result.status === "skipped") {
128
+ console.log(result.message);
129
+ return;
130
+ }
128
131
 
129
- if (!response.ok) {
132
+ console.error(`Error: ${result.message}`);
133
+ process.exit(1);
134
+ } catch (error) {
130
135
  console.error(
131
- `Error: Failed to store API key in assistant (${response.status}).`,
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
  }