careervivid 2.1.13 โ†’ 2.1.18

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":"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;AAO7G,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,CAosBf"}
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;AAS7G,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,CA6uBf"}
@@ -4,10 +4,10 @@ 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, getProviderKey, setProviderKey } from "../../config.js";
7
+ import { loadConfig, getProviderKey, setProviderKey } from "../../config.js";
8
8
  import { auditLog, writeSessionSummary, SESSION_ID } from "../../agent/agentAuditLog.js";
9
9
  import { runShellEscape } from "../../lib/shell.js";
10
- import { isVoiceEnabled, setVoiceEnabled, setLastResponse, getLastResponse, speakText, stopPlayback } from "../../lib/tts.js";
10
+ import { isVoiceEnabled, setVoiceEnabled, setLastResponse, getLastResponse, speakText, stopPlayback, getCurrentVoice, setCurrentVoice, getCurrentTtsModel, setCurrentTtsModel, AVAILABLE_VOICES, AVAILABLE_TTS_MODELS } from "../../lib/tts.js";
11
11
  const { prompt } = pkg;
12
12
  export function printCreditStatus(remaining, limit = null) {
13
13
  if (remaining === null)
@@ -198,16 +198,60 @@ export async function askLoop(engine, options, selectedProvider, selectedModel,
198
198
  if (cmd === "voice") {
199
199
  if (arg === "on") {
200
200
  setVoiceEnabled(true);
201
- console.log(chalk.green("\n ๐Ÿ”Š Voice enabled. Agent responses will be spoken aloud.\n"));
201
+ console.log(chalk.green(`\n ๐Ÿ”Š Voice enabled (${getCurrentVoice()} ยท ${getCurrentTtsModel()}).\n`));
202
202
  }
203
203
  else if (arg === "off") {
204
204
  setVoiceEnabled(false);
205
205
  stopPlayback();
206
206
  console.log(chalk.yellow("\n ๐Ÿ”‡ Voice disabled.\n"));
207
207
  }
208
+ else if (arg === "list-voices" || arg === "voices") {
209
+ console.log(chalk.cyan("\n Available voices:"));
210
+ for (const v of AVAILABLE_VOICES) {
211
+ const active = v === getCurrentVoice() ? chalk.green(" โ† active") : "";
212
+ console.log(chalk.dim(` ${v}${active}`));
213
+ }
214
+ console.log(chalk.dim("\n Usage: /voice set-voice Puck\n"));
215
+ }
216
+ else if (arg.startsWith("set-voice ")) {
217
+ const name = arg.slice("set-voice ".length).trim();
218
+ const match = AVAILABLE_VOICES.find(v => v.toLowerCase() === name.toLowerCase());
219
+ if (!match) {
220
+ console.log(chalk.red(`\n Unknown voice: "${name}". Run /voice list-voices to see options.\n`));
221
+ }
222
+ else {
223
+ setCurrentVoice(match);
224
+ console.log(chalk.green(`\n ๐ŸŽต Voice set to ${chalk.bold(match)}.\n`));
225
+ }
226
+ }
227
+ else if (arg === "list-models" || arg === "models") {
228
+ console.log(chalk.cyan("\n Available TTS models:"));
229
+ for (const m of AVAILABLE_TTS_MODELS) {
230
+ const active = m === getCurrentTtsModel() ? chalk.green(" โ† active") : "";
231
+ console.log(chalk.dim(` ${m}${active}`));
232
+ }
233
+ console.log(chalk.dim("\n Usage: /voice set-model gemini-2.5-pro-preview-tts\n"));
234
+ }
235
+ else if (arg.startsWith("set-model ")) {
236
+ const name = arg.slice("set-model ".length).trim();
237
+ const match = AVAILABLE_TTS_MODELS.find(m => m === name);
238
+ if (!match) {
239
+ console.log(chalk.red(`\n Unknown model: "${name}". Run /voice list-models to see options.\n`));
240
+ }
241
+ else {
242
+ setCurrentTtsModel(match);
243
+ console.log(chalk.green(`\n โš™๏ธ TTS model set to ${chalk.bold(match)}.\n`));
244
+ }
245
+ }
208
246
  else {
209
247
  const status = isVoiceEnabled() ? chalk.green("on") : chalk.yellow("off");
210
- console.log(chalk.dim(`\n Voice is currently ${status}. Usage: /voice on or /voice off\n`));
248
+ console.log(chalk.dim(`\n Voice is ${status} ยท Voice: ${chalk.white(getCurrentVoice())} ยท Model: ${chalk.white(getCurrentTtsModel())}`));
249
+ console.log(chalk.dim("\n Commands:"));
250
+ console.log(chalk.dim(" /voice on | off"));
251
+ console.log(chalk.dim(" /voice set-voice <name> e.g. /voice set-voice Puck"));
252
+ console.log(chalk.dim(" /voice list-voices"));
253
+ console.log(chalk.dim(" /voice set-model <name> e.g. /voice set-model gemini-2.5-pro-preview-tts"));
254
+ console.log(chalk.dim(" /voice list-models\n"));
211
255
  }
212
256
  return ask();
213
257
  }
@@ -217,8 +261,7 @@ export async function askLoop(engine, options, selectedProvider, selectedModel,
217
261
  console.log(chalk.dim("\n Nothing to speak yet. Ask the agent something first.\n"));
218
262
  }
219
263
  else {
220
- const geminiKey = getGeminiKey() || process.env.GEMINI_API_KEY;
221
- speakText(last, geminiKey ?? undefined).catch(() => { });
264
+ speakText(last).catch(() => { });
222
265
  console.log(chalk.dim("\n ๐Ÿ”Š Speaking last response...\n"));
223
266
  }
224
267
  return ask();
@@ -525,8 +568,7 @@ export async function askLoop(engine, options, selectedProvider, selectedModel,
525
568
  if (responseAccumulator) {
526
569
  setLastResponse(responseAccumulator);
527
570
  if (isVoiceEnabled()) {
528
- const geminiKey = getGeminiKey() || process.env.GEMINI_API_KEY;
529
- speakText(responseAccumulator, geminiKey ?? undefined).catch(() => { });
571
+ speakText(responseAccumulator).catch(() => { });
530
572
  }
531
573
  }
532
574
  // โ”€โ”€ Clean turn separator after every AI reply โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
package/dist/lib/tts.d.ts CHANGED
@@ -1,26 +1,27 @@
1
1
  /**
2
2
  * tts.ts โ€” Text-to-Speech engine for the CareerVivid REPL
3
3
  *
4
- * Uses the Gemini API (TTS model) to synthesize speech from text,
5
- * writes the result to a temp WAV file, and plays it via the OS
6
- * audio player (afplay on macOS, aplay on Linux) without blocking
7
- * the main thread.
4
+ * Authenticates using the user's CareerVivid API key (cv_live_...) to fetch
5
+ * a short-lived Gemini key from the backend โ€” exactly like `cv interview`.
6
+ * No separate GEMINI_API_KEY required.
8
7
  *
9
8
  * Toggle: /voice on | /voice off
10
- * Speak: /speak (reads the last agent response)
9
+ * Replay: /speak
11
10
  */
11
+ export declare const AVAILABLE_VOICES: readonly ["Zephyr", "Puck", "Charon", "Kore", "Fenrir", "Aoede", "Orbit", "Stellar", "Leda", "Orus"];
12
+ export declare const AVAILABLE_TTS_MODELS: readonly ["gemini-3.1-flash-preview-tts", "gemini-3.1-pro-preview-tts", "gemini-2.5-flash-preview-tts", "gemini-2.5-pro-preview-tts"];
12
13
  export declare function isVoiceEnabled(): boolean;
13
14
  export declare function setVoiceEnabled(on: boolean): void;
14
15
  export declare function setLastResponse(text: string): void;
15
16
  export declare function getLastResponse(): string;
16
- /**
17
- * Stops any currently playing audio immediately.
18
- */
17
+ export declare function getCurrentVoice(): string;
18
+ export declare function setCurrentVoice(v: string): void;
19
+ export declare function getCurrentTtsModel(): string;
20
+ export declare function setCurrentTtsModel(m: string): void;
19
21
  export declare function stopPlayback(): void;
20
22
  /**
21
- * Synthesizes `text` via Gemini TTS and plays it asynchronously.
22
- * Does nothing if voice is disabled. Silently ignores errors so the
23
- * REPL is never disrupted by TTS failures.
23
+ * Synthesizes `text` via Gemini TTS using the CareerVivid API key for auth.
24
+ * Non-blocking โ€” errors are silently swallowed so the REPL is never disrupted.
24
25
  */
25
- export declare function speakText(text: string, apiKey?: string): Promise<void>;
26
+ export declare function speakText(text: string, _unusedKey?: string): Promise<void>;
26
27
  //# sourceMappingURL=tts.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tts.d.ts","sourceRoot":"","sources":["../../src/lib/tts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAaH,wBAAgB,cAAc,YAA2B;AACzD,wBAAgB,eAAe,CAAC,EAAE,EAAE,OAAO,QAAwB;AACnE,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,QAA0B;AACtE,wBAAgB,eAAe,WAA2B;AAI1D;;GAEG;AACH,wBAAgB,YAAY,SAK3B;AAoED;;;;GAIG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAuD5E"}
1
+ {"version":3,"file":"tts.d.ts","sourceRoot":"","sources":["../../src/lib/tts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAgBH,eAAO,MAAM,gBAAgB,sGAWnB,CAAC;AAEX,eAAO,MAAM,oBAAoB,uIAKvB,CAAC;AAYX,wBAAgB,cAAc,YAA2B;AACzD,wBAAgB,eAAe,CAAC,EAAE,EAAE,OAAO,QAAwB;AACnE,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,QAA0B;AACtE,wBAAgB,eAAe,WAA2B;AAC1D,wBAAgB,eAAe,WAA2B;AAC1D,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,QAAuB;AAChE,wBAAgB,kBAAkB,WAA8B;AAChE,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,QAA0B;AA8BtE,wBAAgB,YAAY,SAK3B;AA2DD;;;GAGG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAuDhF"}
package/dist/lib/tts.js CHANGED
@@ -1,41 +1,91 @@
1
1
  /**
2
2
  * tts.ts โ€” Text-to-Speech engine for the CareerVivid REPL
3
3
  *
4
- * Uses the Gemini API (TTS model) to synthesize speech from text,
5
- * writes the result to a temp WAV file, and plays it via the OS
6
- * audio player (afplay on macOS, aplay on Linux) without blocking
7
- * the main thread.
4
+ * Authenticates using the user's CareerVivid API key (cv_live_...) to fetch
5
+ * a short-lived Gemini key from the backend โ€” exactly like `cv interview`.
6
+ * No separate GEMINI_API_KEY required.
8
7
  *
9
8
  * Toggle: /voice on | /voice off
10
- * Speak: /speak (reads the last agent response)
9
+ * Replay: /speak
11
10
  */
12
11
  import { writeFileSync, unlinkSync } from "fs";
13
12
  import { spawn } from "child_process";
14
13
  import { tmpdir } from "os";
15
14
  import { join } from "path";
16
15
  import { GoogleGenAI, Modality } from "@google/genai";
16
+ import { getApiKey } from "../config.js";
17
+ // โ”€โ”€ Backend endpoint (same as interview token vend) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
18
+ const TTS_TOKEN_URL = process.env.CV_FUNCTIONS_URL
19
+ ? `${process.env.CV_FUNCTIONS_URL}/cliGetInterviewToken`
20
+ : "https://us-west1-jastalk-firebase.cloudfunctions.net/cliGetInterviewToken";
21
+ // โ”€โ”€ Available options โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
22
+ export const AVAILABLE_VOICES = [
23
+ "Zephyr", // Bright, energetic
24
+ "Puck", // Upbeat, playful
25
+ "Charon", // Informative, measured
26
+ "Kore", // Firm, confident
27
+ "Fenrir", // Excitable, dynamic
28
+ "Aoede", // Breezy, easy-going
29
+ "Orbit", // Friendly, relaxed
30
+ "Stellar", // Smooth, polished
31
+ "Leda", // Warm, natural
32
+ "Orus", // Confident, authoritative
33
+ ];
34
+ export const AVAILABLE_TTS_MODELS = [
35
+ "gemini-3.1-flash-preview-tts", // Latest, fast (default)
36
+ "gemini-3.1-pro-preview-tts", // Latest, highest quality
37
+ "gemini-2.5-flash-preview-tts", // Previous gen, fast
38
+ "gemini-2.5-pro-preview-tts", // Previous gen, high quality
39
+ ];
17
40
  // โ”€โ”€ State โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
18
41
  let voiceEnabled = false;
19
42
  let lastResponse = "";
20
43
  let playbackProcess = null;
44
+ let currentVoice = "Zephyr";
45
+ let currentTtsModel = "gemini-3.1-flash-preview-tts";
46
+ // Cache the Gemini key for the session so we don't hit the endpoint every turn
47
+ let cachedGeminiKey = null;
21
48
  export function isVoiceEnabled() { return voiceEnabled; }
22
49
  export function setVoiceEnabled(on) { voiceEnabled = on; }
23
50
  export function setLastResponse(text) { lastResponse = text; }
24
51
  export function getLastResponse() { return lastResponse; }
52
+ export function getCurrentVoice() { return currentVoice; }
53
+ export function setCurrentVoice(v) { currentVoice = v; }
54
+ export function getCurrentTtsModel() { return currentTtsModel; }
55
+ export function setCurrentTtsModel(m) { currentTtsModel = m; }
56
+ // โ”€โ”€ Gemini key via CV API key โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
57
+ async function fetchGeminiKey() {
58
+ if (cachedGeminiKey)
59
+ return cachedGeminiKey;
60
+ const apiKey = getApiKey();
61
+ if (!apiKey)
62
+ return null;
63
+ try {
64
+ const res = await fetch(TTS_TOKEN_URL, {
65
+ method: "POST",
66
+ headers: { "Content-Type": "application/json" },
67
+ body: JSON.stringify({ apiKey, role: "tts" }),
68
+ });
69
+ if (!res.ok)
70
+ return null;
71
+ const data = await res.json();
72
+ if (data?.geminiKey) {
73
+ cachedGeminiKey = data.geminiKey;
74
+ return cachedGeminiKey;
75
+ }
76
+ }
77
+ catch {
78
+ // Network error โ€” fall through
79
+ }
80
+ return null;
81
+ }
25
82
  // โ”€โ”€ Audio Playback โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
26
- /**
27
- * Stops any currently playing audio immediately.
28
- */
29
83
  export function stopPlayback() {
30
84
  if (playbackProcess && !playbackProcess.killed) {
31
85
  playbackProcess.kill("SIGKILL");
32
86
  playbackProcess = null;
33
87
  }
34
88
  }
35
- /**
36
- * Plays a WAV buffer using the OS audio player (non-blocking).
37
- * afplay on macOS, aplay on Linux, powershell on Windows.
38
- */
39
89
  function playWav(wavBuffer) {
40
90
  const tmpFile = join(tmpdir(), `cv-tts-${Date.now()}.wav`);
41
91
  writeFileSync(tmpFile, wavBuffer);
@@ -51,30 +101,28 @@ function playWav(wavBuffer) {
51
101
  playerArgs = ["-q", tmpFile];
52
102
  }
53
103
  else {
54
- // Windows fallback via PowerShell
55
104
  playerCmd = "powershell";
56
105
  playerArgs = ["-c", `(New-Object System.Media.SoundPlayer '${tmpFile}').PlaySync()`];
57
106
  }
58
- stopPlayback(); // Stop any previous playback
107
+ stopPlayback();
59
108
  const child = spawn(playerCmd, playerArgs, { stdio: "ignore", detached: false });
60
109
  playbackProcess = child;
61
110
  child.on("close", () => {
62
111
  try {
63
112
  unlinkSync(tmpFile);
64
113
  }
65
- catch { /* ignore cleanup errors */ }
114
+ catch { /* ignore */ }
66
115
  if (playbackProcess === child)
67
116
  playbackProcess = null;
68
117
  });
69
118
  child.on("error", () => {
70
- // Silently ignore โ€” player may not be installed
71
119
  try {
72
120
  unlinkSync(tmpFile);
73
121
  }
74
122
  catch { /* ignore */ }
75
123
  });
76
124
  }
77
- // โ”€โ”€ WAV Header Builder โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
125
+ // โ”€โ”€ WAV Builder โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
78
126
  function buildWavHeader(dataLength, sampleRate = 24000, channels = 1, bitsPerSample = 16) {
79
127
  const byteRate = sampleRate * channels * bitsPerSample / 8;
80
128
  const blockAlign = channels * bitsPerSample / 8;
@@ -94,40 +142,39 @@ function buildWavHeader(dataLength, sampleRate = 24000, channels = 1, bitsPerSam
94
142
  header.writeUInt32LE(dataLength, 40);
95
143
  return header;
96
144
  }
97
- // โ”€โ”€ TTS Synthesis โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
145
+ // โ”€โ”€ TTS Synthesis โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
98
146
  /**
99
- * Synthesizes `text` via Gemini TTS and plays it asynchronously.
100
- * Does nothing if voice is disabled. Silently ignores errors so the
101
- * REPL is never disrupted by TTS failures.
147
+ * Synthesizes `text` via Gemini TTS using the CareerVivid API key for auth.
148
+ * Non-blocking โ€” errors are silently swallowed so the REPL is never disrupted.
102
149
  */
103
- export async function speakText(text, apiKey) {
150
+ export async function speakText(text, _unusedKey) {
104
151
  if (!text.trim())
105
152
  return;
106
- const key = apiKey || process.env.GEMINI_API_KEY;
107
- if (!key)
108
- return; // No key โ€” silently skip
109
- // Strip markdown so the audio sounds natural
153
+ const geminiKey = await fetchGeminiKey();
154
+ if (!geminiKey)
155
+ return; // No key available โ€” silently skip
156
+ // Strip markdown for natural-sounding speech
110
157
  const cleaned = text
111
- .replace(/```[\s\S]*?```/g, "") // code blocks
112
- .replace(/`[^`]+`/g, "") // inline code
113
- .replace(/\*\*(.*?)\*\*/g, "$1") // bold
114
- .replace(/\*(.*?)\*/g, "$1") // italic
115
- .replace(/^[#>โ€ข\-*]\s*/gm, "") // headings, quotes, bullets
158
+ .replace(/```[\s\S]*?```/g, "")
159
+ .replace(/`[^`]+`/g, "")
160
+ .replace(/\*\*(.*?)\*\*/g, "$1")
161
+ .replace(/\*(.*?)\*/g, "$1")
162
+ .replace(/^[#>โ€ข\-*]\s*/gm, "")
116
163
  .replace(/\s+/g, " ")
117
164
  .trim()
118
- .slice(0, 1000); // Cap to ~1000 chars to keep TTS fast
165
+ .slice(0, 1000);
119
166
  if (!cleaned)
120
167
  return;
121
168
  try {
122
- const ai = new GoogleGenAI({ apiKey: key });
169
+ const ai = new GoogleGenAI({ apiKey: geminiKey });
123
170
  const response = await ai.models.generateContent({
124
- model: "gemini-2.5-flash-preview-tts",
171
+ model: currentTtsModel,
125
172
  contents: [{ parts: [{ text: cleaned }] }],
126
173
  config: {
127
174
  responseModalities: [Modality.AUDIO],
128
175
  speechConfig: {
129
176
  voiceConfig: {
130
- prebuiltVoiceConfig: { voiceName: "Zephyr" },
177
+ prebuiltVoiceConfig: { voiceName: currentVoice },
131
178
  },
132
179
  },
133
180
  },
@@ -142,12 +189,12 @@ export async function speakText(text, apiKey) {
142
189
  if (audioParts.length === 0)
143
190
  return;
144
191
  const pcmData = Buffer.concat(audioParts);
145
- const wavHeader = buildWavHeader(pcmData.length);
146
- const wavBuffer = Buffer.concat([wavHeader, pcmData]);
147
- // Fire and forget โ€” does not block the REPL
192
+ const wavBuffer = Buffer.concat([buildWavHeader(pcmData.length), pcmData]);
148
193
  playWav(wavBuffer);
149
194
  }
150
195
  catch {
151
- // Silently swallow TTS errors โ€” never disrupt the agent session
196
+ // Silently ignore โ€” TTS errors must never crash the agent REPL
197
+ // Invalidate cached key so we retry fetching on the next call
198
+ cachedGeminiKey = null;
152
199
  }
153
200
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "careervivid",
3
- "version": "2.1.13",
3
+ "version": "2.1.18",
4
4
  "description": "Official CLI for CareerVivid โ€” AI voice interviews, autonomous job applications, resume editing, and portfolio publishing from your terminal",
5
5
  "type": "module",
6
6
  "bin": {