careervivid 1.12.25 → 1.12.27

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.
@@ -1 +1 @@
1
- {"version":3,"file":"configurator.d.ts","sourceRoot":"","sources":["../../../src/commands/agent/configurator.ts"],"names":[],"mappings":"AAEA,OAAO,EAA0B,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAI3E,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAKpD,CAAC;AAEF,eAAO,MAAM,SAAS;;;;;GAyBrB,CAAC;AAEF,eAAO,MAAM,UAAU;;;;;GA+BtB,CAAC;AAEF,wBAAsB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAsEpD;AAED,wBAAsB,mBAAmB,CAAC,OAAO,GAAE,GAAQ,GAAG,OAAO,CAAC;IACpE,gBAAgB,EAAE,WAAW,CAAC;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC,CA6HD"}
1
+ {"version":3,"file":"configurator.d.ts","sourceRoot":"","sources":["../../../src/commands/agent/configurator.ts"],"names":[],"mappings":"AAEA,OAAO,EAA0D,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAI3G,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAKpD,CAAC;AAEF,eAAO,MAAM,SAAS;;;;;GAyBrB,CAAC;AAEF,eAAO,MAAM,UAAU;;;;;GA+BtB,CAAC;AAEF,wBAAsB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAsEpD;AAED,wBAAsB,mBAAmB,CAAC,OAAO,GAAE,GAAQ,GAAG,OAAO,CAAC;IACpE,gBAAgB,EAAE,WAAW,CAAC;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC,CAiKD"}
@@ -1,6 +1,6 @@
1
1
  import chalk from "chalk";
2
2
  import pkg from "enquirer";
3
- import { loadConfig, saveConfig } from "../../config.js";
3
+ import { loadConfig, saveConfig, setProviderKey, getProviderKey } from "../../config.js";
4
4
  const { prompt } = pkg;
5
5
  export const MODEL_CREDIT_COST = {
6
6
  "gemini-3.1-flash-lite-preview": 0.5,
@@ -165,20 +165,52 @@ export async function promptForAgentModel(options = {}) {
165
165
  __custom__: "custom",
166
166
  };
167
167
  selectedProvider = providerMap[picked] || "openai";
168
- const savedCfg = loadConfig();
169
- const savedKey = savedCfg.llmApiKey;
168
+ const providerLabels = {
169
+ openai: "OpenAI",
170
+ anthropic: "Anthropic",
171
+ gemini: "Gemini (Google AI Studio)",
172
+ openrouter: "OpenRouter",
173
+ custom: "Custom endpoint",
174
+ };
175
+ const providerKeyUrls = {
176
+ openai: "https://platform.openai.com/api-keys",
177
+ anthropic: "https://console.anthropic.com/settings/keys",
178
+ gemini: "https://aistudio.google.com/app/apikey",
179
+ openrouter: "https://openrouter.ai/settings/keys",
180
+ custom: "your custom endpoint's documentation",
181
+ };
182
+ // Check for this specific provider's saved key — NOT a shared key
183
+ const savedKey = getProviderKey(selectedProvider);
170
184
  if (!savedKey) {
171
- console.log(chalk.yellow(`\nšŸ”‘ BYO API key needed for ${selectedProvider}.`));
172
- console.log(chalk.dim(" Run: cv agent config to save your key permanently.\n"));
173
- const keyAnswer = await prompt({
174
- type: "password",
175
- name: "key",
176
- message: `Enter your ${selectedProvider} API key:`,
177
- });
178
- apiKey = keyAnswer.key.trim();
185
+ console.log();
186
+ console.log(chalk.bold.yellow(`šŸ”‘ API key required for ${providerLabels[selectedProvider] ?? selectedProvider}`) +
187
+ chalk.dim("\n Your key will be saved locally — you only need to enter it once."));
188
+ console.log(chalk.dim(`\n Get your key at: `) + chalk.cyan(providerKeyUrls[selectedProvider] ?? "the provider's website"));
189
+ console.log();
190
+ try {
191
+ const keyAnswer = await prompt({
192
+ type: "password",
193
+ name: "key",
194
+ message: `Enter your ${providerLabels[selectedProvider] ?? selectedProvider} API key:`,
195
+ });
196
+ apiKey = (keyAnswer?.key ?? "").trim();
197
+ }
198
+ catch {
199
+ // User pressed Escape or Ctrl+C — exit cleanly
200
+ console.log(chalk.dim("\n Cancelled. Run `cv agent` again when you have your API key.\n"));
201
+ process.exit(0);
202
+ }
203
+ if (apiKey) {
204
+ setProviderKey(selectedProvider, apiKey);
205
+ console.log(chalk.green(`\nāœ” Key saved for ${providerLabels[selectedProvider] ?? selectedProvider}\n`));
206
+ }
207
+ else {
208
+ console.log(chalk.yellow("\nāš ļø No key entered. Continuing without API key — the agent may fail.\n"));
209
+ }
179
210
  }
180
- if (!apiKey) {
181
- apiKey = savedKey; // Keep existing api key fallback
211
+ else {
212
+ apiKey = savedKey;
213
+ console.log(chalk.dim(`\n Using saved ${providerLabels[selectedProvider] ?? selectedProvider} key (••••${savedKey.slice(-4)})\n`));
182
214
  }
183
215
  // Attempt to automatically fetch and display Free OpenRouter models that support tools
184
216
  if (selectedProvider === "openrouter") {
@@ -217,6 +249,7 @@ export async function promptForAgentModel(options = {}) {
217
249
  }
218
250
  }
219
251
  // Pre-fill with the saved model when provider matches, otherwise use sensible defaults
252
+ const savedCfg = loadConfig();
220
253
  const defaultModel = savedCfg.llmProvider === selectedProvider && savedCfg.llmModel
221
254
  ? savedCfg.llmModel
222
255
  : selectedProvider === "openai"
@@ -1 +1 @@
1
- {"version":3,"file":"repl.d.ts","sourceRoot":"","sources":["../../../src/commands/agent/repl.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,sBAAsB,EAAE,MAAM,uCAAuC,CAAC;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAEzD,OAAO,EAA4B,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAI7E,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,GAAE,MAAM,GAAG,IAAW,QAwBtF;AAED,wBAAsB,OAAO,CAC3B,MAAM,EAAE,WAAW,GAAG,sBAAsB,GAAG,IAAI,EACnD,OAAO,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,EAC9K,gBAAgB,EAAE,WAAW,EAC7B,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,iBAAiB,EAAE,MAAM,EACzB,KAAK,EAAE,GAAG,EAAE,GACX,OAAO,CAAC,IAAI,CAAC,CA+bf"}
1
+ {"version":3,"file":"repl.d.ts","sourceRoot":"","sources":["../../../src/commands/agent/repl.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,sBAAsB,EAAE,MAAM,uCAAuC,CAAC;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAEzD,OAAO,EAA4D,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAI7G,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,GAAE,MAAM,GAAG,IAAW,QAwBtF;AAED,wBAAsB,OAAO,CAC3B,MAAM,EAAE,WAAW,GAAG,sBAAsB,GAAG,IAAI,EACnD,OAAO,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,EAC9K,gBAAgB,EAAE,WAAW,EAC7B,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,iBAAiB,EAAE,MAAM,EACzB,KAAK,EAAE,GAAG,EAAE,GACX,OAAO,CAAC,IAAI,CAAC,CA4ff"}
@@ -4,7 +4,7 @@ import ora from "ora";
4
4
  import { isSafeCommand } from "../../agent/tools/coding.js";
5
5
  import { CareerVividProxyEngine } from "../../agent/CareerVividProxyEngine.js";
6
6
  import { CV_MODELS } from "./configurator.js";
7
- import { loadConfig, getGeminiKey } from "../../config.js";
7
+ import { loadConfig, getProviderKey, setProviderKey } from "../../config.js";
8
8
  const { prompt } = pkg;
9
9
  export function printCreditStatus(remaining, limit = null) {
10
10
  if (remaining === null)
@@ -366,8 +366,8 @@ ${label}
366
366
  sessionTurns++;
367
367
  const { createOpenAICompatibleProvider } = await import("../../agent/providers/OpenAIProvider.js");
368
368
  const { AnthropicProvider } = await import("../../agent/providers/AnthropicProvider.js");
369
- const byoApiKey = options["api-key"] || loadConfig().llmApiKey;
370
- const key = byoApiKey || getGeminiKey() || "";
369
+ const byoApiKey = options["api-key"] || getProviderKey(selectedProvider) || loadConfig().llmApiKey;
370
+ const key = byoApiKey || "";
371
371
  const baseUrl = options["base-url"] || loadConfig().llmBaseUrl;
372
372
  let provider;
373
373
  if (selectedProvider === "anthropic") {
@@ -425,11 +425,71 @@ ${label}
425
425
  return ask();
426
426
  }
427
427
  catch (err) {
428
- if (err.message && err.message.includes("cancelled")) {
428
+ const msg = err?.message ?? "";
429
+ // ── Clean exit on Ctrl+C / enquirer cancel ────────────────────────
430
+ if (!msg || msg.includes("cancelled") || msg.includes("User force closed")) {
429
431
  console.log(chalk.gray("\nCancelled. Exiting.\n"));
430
432
  process.exit(0);
431
433
  }
432
- console.error(chalk.red(`\nAgent encountered an error: ${err.message}`));
434
+ // ── 401 unauthorized — offer key reset ───────────────────────────
435
+ const is401 = msg.includes("401") || msg.toLowerCase().includes("user not found") ||
436
+ msg.toLowerCase().includes("invalid api key") || msg.toLowerCase().includes("unauthorized");
437
+ if (is401 && selectedProvider && selectedProvider !== "careervivid") {
438
+ const providerLabels = {
439
+ openai: "OpenAI", anthropic: "Anthropic",
440
+ gemini: "Gemini", openrouter: "OpenRouter", custom: "Custom",
441
+ };
442
+ const providerKeyUrls = {
443
+ openai: "https://platform.openai.com/api-keys",
444
+ anthropic: "https://console.anthropic.com/settings/keys",
445
+ gemini: "https://aistudio.google.com/app/apikey",
446
+ openrouter: "https://openrouter.ai/settings/keys",
447
+ };
448
+ const label = providerLabels[selectedProvider] ?? selectedProvider;
449
+ console.log();
450
+ console.log(chalk.red(`āŒ API key rejected by ${label} (401 Unauthorized).`));
451
+ console.log(chalk.dim(` The saved key may be expired or invalid.`));
452
+ if (providerKeyUrls[selectedProvider]) {
453
+ console.log(chalk.dim(` Get a new key at: `) + chalk.cyan(providerKeyUrls[selectedProvider]));
454
+ }
455
+ console.log();
456
+ try {
457
+ const resetAnswer = await prompt({
458
+ type: "select",
459
+ name: "action",
460
+ message: "What would you like to do?",
461
+ choices: [
462
+ { name: "reset", message: `šŸ”‘ Enter a new ${label} API key` },
463
+ { name: "continue", message: "ā­ļø Continue anyway (will keep failing)" },
464
+ { name: "exit", message: "🚪 Exit the agent" },
465
+ ],
466
+ });
467
+ if (resetAnswer.action === "reset") {
468
+ const keyAnswer = await prompt({
469
+ type: "password",
470
+ name: "key",
471
+ message: `Enter your new ${label} API key:`,
472
+ });
473
+ const newKey = (keyAnswer?.key ?? "").trim();
474
+ if (newKey) {
475
+ setProviderKey(selectedProvider, newKey);
476
+ // Update the key used for subsequent turns this session
477
+ options["api-key"] = newKey;
478
+ console.log(chalk.green(`\nāœ” New ${label} key saved. Resuming session...\n`));
479
+ }
480
+ }
481
+ else if (resetAnswer.action === "exit") {
482
+ console.log(chalk.gray("\nGoodbye! šŸ‘‹\n"));
483
+ process.exit(0);
484
+ }
485
+ }
486
+ catch {
487
+ // User cancelled the reset prompt — just continue
488
+ }
489
+ return ask();
490
+ }
491
+ // ── Generic error ────────────────────────────────────────────────
492
+ console.error(chalk.red(`\nAgent encountered an error: ${msg}`));
433
493
  return ask();
434
494
  }
435
495
  };
package/dist/config.d.ts CHANGED
@@ -20,8 +20,10 @@ export interface CareerVividConfig {
20
20
  llmProvider?: LLMProvider;
21
21
  /** BYO model identifier (e.g. "gpt-4o", "claude-opus-4-5", "gemini-2.5-pro") */
22
22
  llmModel?: string;
23
- /** BYO API key for the chosen provider */
23
+ /** @deprecated use llmKeys[provider] instead — kept for migration */
24
24
  llmApiKey?: string;
25
+ /** Per-provider API keys — keyed by provider name */
26
+ llmKeys?: Partial<Record<LLMProvider, string>>;
25
27
  /** Custom base URL for OpenAI-compatible endpoints (OpenRouter, Kimi, GLM, Qwen, etc.) */
26
28
  llmBaseUrl?: string;
27
29
  }
@@ -36,6 +38,10 @@ export declare function saveConfig(config: CareerVividConfig): void;
36
38
  export declare function getApiKey(): string | undefined;
37
39
  /** Gemini API key used by `cv agent`. Priority: GEMINI_API_KEY env var > geminiKey in config. */
38
40
  export declare function getGeminiKey(): string | undefined;
41
+ /** Get the saved API key for a specific BYO provider */
42
+ export declare function getProviderKey(provider: LLMProvider): string | undefined;
43
+ /** Save the API key for a specific BYO provider */
44
+ export declare function setProviderKey(provider: LLMProvider, key: string): void;
39
45
  export declare function getApiUrl(): string;
40
46
  export declare function setConfigValue(key: keyof CareerVividConfig, value: string): void;
41
47
  /** How long a CLI session stays valid (90 days in ms) */
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,eAAO,MAAM,WAAW,QAAyC,CAAC;AAElE,eAAO,MAAM,eAAe,gCAAgC,CAAC;AAE7D,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG,QAAQ,GAAG,QAAQ,GAAG,WAAW,GAAG,YAAY,GAAG,QAAQ,CAAC;AAEtG,MAAM,WAAW,iBAAiB;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mEAAmE;IACnE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,wDAAwD;IACxD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kCAAkC;IAClC,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0CAA0C;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0FAA0F;IAC1F,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,SAAS;IACtB,QAAQ,EAAE,WAAW,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,UAAU,IAAI,iBAAiB,CAQ9C;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAE1D;AAED,wBAAgB,SAAS,IAAI,MAAM,GAAG,SAAS,CAG9C;AAED,iGAAiG;AACjG,wBAAgB,YAAY,IAAI,MAAM,GAAG,SAAS,CAEjD;AAED,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,iBAAiB,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAIhF;AAID,yDAAyD;AACzD,eAAO,MAAM,cAAc,QAA2B,CAAC;AAEvD,wDAAwD;AACxD,wBAAgB,cAAc,IAAI,OAAO,CAMxC;AAED,mFAAmF;AACnF,wBAAgB,YAAY,IAAI,IAAI,CAInC;AAED,0CAA0C;AAC1C,wBAAgB,YAAY,IAAI,IAAI,CAMnC;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,SAAS,CAAC,EAAE;IACrC,QAAQ,CAAC,EAAE,WAAW,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB,GAAG,SAAS,CA6BZ"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,eAAO,MAAM,WAAW,QAAyC,CAAC;AAElE,eAAO,MAAM,eAAe,gCAAgC,CAAC;AAE7D,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG,QAAQ,GAAG,QAAQ,GAAG,WAAW,GAAG,YAAY,GAAG,QAAQ,CAAC;AAEtG,MAAM,WAAW,iBAAiB;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mEAAmE;IACnE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,wDAAwD;IACxD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kCAAkC;IAClC,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qDAAqD;IACrD,OAAO,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC;IAC/C,0FAA0F;IAC1F,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,SAAS;IACtB,QAAQ,EAAE,WAAW,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,UAAU,IAAI,iBAAiB,CAQ9C;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAE1D;AAED,wBAAgB,SAAS,IAAI,MAAM,GAAG,SAAS,CAG9C;AAED,iGAAiG;AACjG,wBAAgB,YAAY,IAAI,MAAM,GAAG,SAAS,CAEjD;AAED,wDAAwD;AACxD,wBAAgB,cAAc,CAAC,QAAQ,EAAE,WAAW,GAAG,MAAM,GAAG,SAAS,CAIxE;AAED,mDAAmD;AACnD,wBAAgB,cAAc,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAQvE;AAED,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,iBAAiB,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAIhF;AAID,yDAAyD;AACzD,eAAO,MAAM,cAAc,QAA2B,CAAC;AAEvD,wDAAwD;AACxD,wBAAgB,cAAc,IAAI,OAAO,CAMxC;AAED,mFAAmF;AACnF,wBAAgB,YAAY,IAAI,IAAI,CAInC;AAED,0CAA0C;AAC1C,wBAAgB,YAAY,IAAI,IAAI,CAMnC;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,SAAS,CAAC,EAAE;IACrC,QAAQ,CAAC,EAAE,WAAW,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB,GAAG,SAAS,CA6BZ"}
package/dist/config.js CHANGED
@@ -32,6 +32,22 @@ export function getApiKey() {
32
32
  export function getGeminiKey() {
33
33
  return process.env.GEMINI_API_KEY || loadConfig().geminiKey;
34
34
  }
35
+ /** Get the saved API key for a specific BYO provider */
36
+ export function getProviderKey(provider) {
37
+ const cfg = loadConfig();
38
+ // Per-provider key takes priority, fallback to old llmApiKey (migration)
39
+ return cfg.llmKeys?.[provider] ?? (cfg.llmProvider === provider ? cfg.llmApiKey : undefined);
40
+ }
41
+ /** Save the API key for a specific BYO provider */
42
+ export function setProviderKey(provider, key) {
43
+ const cfg = loadConfig();
44
+ cfg.llmKeys = cfg.llmKeys ?? {};
45
+ cfg.llmKeys[provider] = key;
46
+ // Also write to llmApiKey for backwards compat when this is the active provider
47
+ cfg.llmApiKey = key;
48
+ cfg.llmProvider = provider;
49
+ saveConfig(cfg);
50
+ }
35
51
  export function getApiUrl() {
36
52
  return process.env.CV_API_URL || loadConfig().apiUrl || DEFAULT_API_URL;
37
53
  }
@@ -83,7 +99,7 @@ export function getLlmConfig(overrides) {
83
99
  'gemini-2.5-flash';
84
100
  const apiKey = overrides?.apiKey ??
85
101
  process.env.CV_LLM_API_KEY ??
86
- cfg.llmApiKey ??
102
+ getProviderKey(provider) ??
87
103
  cfg.geminiKey ??
88
104
  process.env.GEMINI_API_KEY;
89
105
  const baseUrl = overrides?.baseUrl ??
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "careervivid",
3
- "version": "1.12.25",
3
+ "version": "1.12.27",
4
4
  "description": "Official CLI for CareerVivid — publish articles, diagrams, and portfolio updates from your terminal or AI agent",
5
5
  "type": "module",
6
6
  "bin": {