careervivid 2.1.12 → 2.1.16

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;AAK7G,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+oBf"}
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"}
@@ -6,6 +6,8 @@ import { CareerVividProxyEngine } from "../../agent/CareerVividProxyEngine.js";
6
6
  import { CV_MODELS } from "./configurator.js";
7
7
  import { loadConfig, getProviderKey, setProviderKey } from "../../config.js";
8
8
  import { auditLog, writeSessionSummary, SESSION_ID } from "../../agent/agentAuditLog.js";
9
+ import { runShellEscape } from "../../lib/shell.js";
10
+ import { isVoiceEnabled, setVoiceEnabled, setLastResponse, getLastResponse, speakText, stopPlayback, getCurrentVoice, setCurrentVoice, getCurrentTtsModel, setCurrentTtsModel, AVAILABLE_VOICES, AVAILABLE_TTS_MODELS } from "../../lib/tts.js";
9
11
  const { prompt } = pkg;
10
12
  export function printCreditStatus(remaining, limit = null) {
11
13
  if (remaining === null)
@@ -165,6 +167,15 @@ export async function askLoop(engine, options, selectedProvider, selectedModel,
165
167
  chalk.dim("\n Then paste the job description, and press Enter twice to submit.\n"));
166
168
  return ask();
167
169
  }
170
+ // ── Subshell escape: input starting with ! runs as a raw shell command ──
171
+ if (userInput.startsWith("!")) {
172
+ const shellCmd = userInput.slice(1).trim();
173
+ if (shellCmd) {
174
+ process.stdout.write(chalk.dim(`\n $ ${shellCmd}\n`));
175
+ await runShellEscape(shellCmd);
176
+ }
177
+ return ask();
178
+ }
168
179
  // ── Slash commands ──────────────────────────────────────────────
169
180
  if (userInput.startsWith("/")) {
170
181
  const [cmd, ...rest] = userInput.slice(1).split(" ");
@@ -173,13 +184,88 @@ export async function askLoop(engine, options, selectedProvider, selectedModel,
173
184
  console.log(chalk.cyan("\n Slash commands:"));
174
185
  console.log(chalk.dim(" /model <name> — Switch to a different model mid-session"));
175
186
  console.log(chalk.dim(" /models — List all available CareerVivid models"));
187
+ console.log(chalk.dim(" /voice on|off — Toggle automatic TTS for agent responses"));
188
+ console.log(chalk.dim(" /speak — Read the last agent response aloud"));
176
189
  console.log(chalk.dim(" /help — Show this help message"));
177
190
  console.log(chalk.dim(" exit — End the session"));
178
- console.log(chalk.cyan("\n Paste long content (job descriptions, cover letters):"));
191
+ console.log(chalk.cyan("\n Shell escape (run terminal commands without leaving the agent):"));
192
+ console.log(chalk.dim(" !<command> — e.g. !ls -la or !git status\n"));
193
+ console.log(chalk.cyan(" Paste long content (job descriptions, cover letters):"));
179
194
  console.log(chalk.dim(" <<< — Open multi-line paste mode; press Enter twice when done"));
180
195
  console.log(chalk.dim(" <<<your text — Start with text directly after <<<\n"));
181
196
  return ask();
182
197
  }
198
+ if (cmd === "voice") {
199
+ if (arg === "on") {
200
+ setVoiceEnabled(true);
201
+ console.log(chalk.green(`\n 🔊 Voice enabled (${getCurrentVoice()} · ${getCurrentTtsModel()}).\n`));
202
+ }
203
+ else if (arg === "off") {
204
+ setVoiceEnabled(false);
205
+ stopPlayback();
206
+ console.log(chalk.yellow("\n 🔇 Voice disabled.\n"));
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
+ }
246
+ else {
247
+ const status = isVoiceEnabled() ? chalk.green("on") : chalk.yellow("off");
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"));
255
+ }
256
+ return ask();
257
+ }
258
+ if (cmd === "speak") {
259
+ const last = getLastResponse();
260
+ if (!last) {
261
+ console.log(chalk.dim("\n Nothing to speak yet. Ask the agent something first.\n"));
262
+ }
263
+ else {
264
+ speakText(last).catch(() => { });
265
+ console.log(chalk.dim("\n 🔊 Speaking last response...\n"));
266
+ }
267
+ return ask();
268
+ }
183
269
  if (cmd === "models") {
184
270
  console.log(chalk.cyan("\n Available CareerVivid models:"));
185
271
  for (const m of CV_MODELS) {
@@ -418,6 +504,7 @@ export async function askLoop(engine, options, selectedProvider, selectedModel,
418
504
  };
419
505
  if (engine) {
420
506
  sessionTurns++;
507
+ let responseAccumulator = "";
421
508
  const sharedOnChunk = (text) => {
422
509
  if (firstChunk) {
423
510
  thinkingSpinner.stop();
@@ -426,6 +513,7 @@ export async function askLoop(engine, options, selectedProvider, selectedModel,
426
513
  firstChunk = false;
427
514
  }
428
515
  process.stdout.write(text);
516
+ responseAccumulator += text; // Accumulate for TTS
429
517
  };
430
518
  const sharedOnError = (error) => {
431
519
  thinkingSpinner.stop();
@@ -476,6 +564,13 @@ export async function askLoop(engine, options, selectedProvider, selectedModel,
476
564
  onError: sharedOnError,
477
565
  });
478
566
  }
567
+ // ── TTS: store last response + auto-speak if voice enabled ──────
568
+ if (responseAccumulator) {
569
+ setLastResponse(responseAccumulator);
570
+ if (isVoiceEnabled()) {
571
+ speakText(responseAccumulator).catch(() => { });
572
+ }
573
+ }
479
574
  // ── Clean turn separator after every AI reply ─────────────────────────────
480
575
  process.stdout.write("\n" + chalk.dim("─".repeat(48)) + "\n");
481
576
  }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * shell.ts — Subshell escape for the CareerVivid REPL
3
+ *
4
+ * Intercepts user input starting with `!` and spawns it as a raw
5
+ * shell command. stdout/stderr are piped directly to the terminal.
6
+ * The agent session and conversation history are never affected.
7
+ */
8
+ /**
9
+ * Runs a shell command and streams its output to the terminal.
10
+ * Returns a promise that resolves when the command exits.
11
+ */
12
+ export declare function runShellEscape(command: string): Promise<void>;
13
+ //# sourceMappingURL=shell.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shell.d.ts","sourceRoot":"","sources":["../../src/lib/shell.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAmB7D"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * shell.ts — Subshell escape for the CareerVivid REPL
3
+ *
4
+ * Intercepts user input starting with `!` and spawns it as a raw
5
+ * shell command. stdout/stderr are piped directly to the terminal.
6
+ * The agent session and conversation history are never affected.
7
+ */
8
+ import { spawn } from "child_process";
9
+ /**
10
+ * Runs a shell command and streams its output to the terminal.
11
+ * Returns a promise that resolves when the command exits.
12
+ */
13
+ export function runShellEscape(command) {
14
+ return new Promise((resolve) => {
15
+ const child = spawn(command, {
16
+ shell: true,
17
+ stdio: "inherit", // pipe stdin/stdout/stderr directly to TTY
18
+ });
19
+ child.on("error", (err) => {
20
+ process.stderr.write(`\n⚠️ Shell error: ${err.message}\n`);
21
+ resolve();
22
+ });
23
+ child.on("close", (code) => {
24
+ if (code !== 0 && code !== null) {
25
+ process.stdout.write(`\n [exit ${code}]\n`);
26
+ }
27
+ resolve();
28
+ });
29
+ });
30
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * tts.ts — Text-to-Speech engine for the CareerVivid REPL
3
+ *
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.
7
+ *
8
+ * Toggle: /voice on | /voice off
9
+ * Replay: /speak
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-2.5-flash-preview-tts", "gemini-2.5-pro-preview-tts"];
13
+ export declare function isVoiceEnabled(): boolean;
14
+ export declare function setVoiceEnabled(on: boolean): void;
15
+ export declare function setLastResponse(text: string): void;
16
+ export declare function getLastResponse(): string;
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;
21
+ export declare function stopPlayback(): void;
22
+ /**
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.
25
+ */
26
+ export declare function speakText(text: string, _unusedKey?: string): Promise<void>;
27
+ //# sourceMappingURL=tts.d.ts.map
@@ -0,0 +1 @@
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,yEAGvB,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"}
@@ -0,0 +1,198 @@
1
+ /**
2
+ * tts.ts — Text-to-Speech engine for the CareerVivid REPL
3
+ *
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.
7
+ *
8
+ * Toggle: /voice on | /voice off
9
+ * Replay: /speak
10
+ */
11
+ import { writeFileSync, unlinkSync } from "fs";
12
+ import { spawn } from "child_process";
13
+ import { tmpdir } from "os";
14
+ import { join } from "path";
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-2.5-flash-preview-tts", // Fast, efficient (default)
36
+ "gemini-2.5-pro-preview-tts", // Higher quality, slower
37
+ ];
38
+ // ── State ────────────────────────────────────────────────────────────────────
39
+ let voiceEnabled = false;
40
+ let lastResponse = "";
41
+ let playbackProcess = null;
42
+ let currentVoice = "Zephyr";
43
+ let currentTtsModel = "gemini-2.5-flash-preview-tts";
44
+ // Cache the Gemini key for the session so we don't hit the endpoint every turn
45
+ let cachedGeminiKey = null;
46
+ export function isVoiceEnabled() { return voiceEnabled; }
47
+ export function setVoiceEnabled(on) { voiceEnabled = on; }
48
+ export function setLastResponse(text) { lastResponse = text; }
49
+ export function getLastResponse() { return lastResponse; }
50
+ export function getCurrentVoice() { return currentVoice; }
51
+ export function setCurrentVoice(v) { currentVoice = v; }
52
+ export function getCurrentTtsModel() { return currentTtsModel; }
53
+ export function setCurrentTtsModel(m) { currentTtsModel = m; }
54
+ // ── Gemini key via CV API key ─────────────────────────────────────────────────
55
+ async function fetchGeminiKey() {
56
+ if (cachedGeminiKey)
57
+ return cachedGeminiKey;
58
+ const apiKey = getApiKey();
59
+ if (!apiKey)
60
+ return null;
61
+ try {
62
+ const res = await fetch(TTS_TOKEN_URL, {
63
+ method: "POST",
64
+ headers: { "Content-Type": "application/json" },
65
+ body: JSON.stringify({ apiKey, role: "tts" }),
66
+ });
67
+ if (!res.ok)
68
+ return null;
69
+ const data = await res.json();
70
+ if (data?.geminiKey) {
71
+ cachedGeminiKey = data.geminiKey;
72
+ return cachedGeminiKey;
73
+ }
74
+ }
75
+ catch {
76
+ // Network error — fall through
77
+ }
78
+ return null;
79
+ }
80
+ // ── Audio Playback ────────────────────────────────────────────────────────────
81
+ export function stopPlayback() {
82
+ if (playbackProcess && !playbackProcess.killed) {
83
+ playbackProcess.kill("SIGKILL");
84
+ playbackProcess = null;
85
+ }
86
+ }
87
+ function playWav(wavBuffer) {
88
+ const tmpFile = join(tmpdir(), `cv-tts-${Date.now()}.wav`);
89
+ writeFileSync(tmpFile, wavBuffer);
90
+ const platform = process.platform;
91
+ let playerCmd;
92
+ let playerArgs;
93
+ if (platform === "darwin") {
94
+ playerCmd = "afplay";
95
+ playerArgs = [tmpFile];
96
+ }
97
+ else if (platform === "linux") {
98
+ playerCmd = "aplay";
99
+ playerArgs = ["-q", tmpFile];
100
+ }
101
+ else {
102
+ playerCmd = "powershell";
103
+ playerArgs = ["-c", `(New-Object System.Media.SoundPlayer '${tmpFile}').PlaySync()`];
104
+ }
105
+ stopPlayback();
106
+ const child = spawn(playerCmd, playerArgs, { stdio: "ignore", detached: false });
107
+ playbackProcess = child;
108
+ child.on("close", () => {
109
+ try {
110
+ unlinkSync(tmpFile);
111
+ }
112
+ catch { /* ignore */ }
113
+ if (playbackProcess === child)
114
+ playbackProcess = null;
115
+ });
116
+ child.on("error", () => {
117
+ try {
118
+ unlinkSync(tmpFile);
119
+ }
120
+ catch { /* ignore */ }
121
+ });
122
+ }
123
+ // ── WAV Builder ───────────────────────────────────────────────────────────────
124
+ function buildWavHeader(dataLength, sampleRate = 24000, channels = 1, bitsPerSample = 16) {
125
+ const byteRate = sampleRate * channels * bitsPerSample / 8;
126
+ const blockAlign = channels * bitsPerSample / 8;
127
+ const header = Buffer.alloc(44);
128
+ header.write("RIFF", 0);
129
+ header.writeUInt32LE(36 + dataLength, 4);
130
+ header.write("WAVE", 8);
131
+ header.write("fmt ", 12);
132
+ header.writeUInt32LE(16, 16);
133
+ header.writeUInt16LE(1, 20); // PCM
134
+ header.writeUInt16LE(channels, 22);
135
+ header.writeUInt32LE(sampleRate, 24);
136
+ header.writeUInt32LE(byteRate, 28);
137
+ header.writeUInt16LE(blockAlign, 32);
138
+ header.writeUInt16LE(bitsPerSample, 34);
139
+ header.write("data", 36);
140
+ header.writeUInt32LE(dataLength, 40);
141
+ return header;
142
+ }
143
+ // ── TTS Synthesis ─────────────────────────────────────────────────────────────
144
+ /**
145
+ * Synthesizes `text` via Gemini TTS using the CareerVivid API key for auth.
146
+ * Non-blocking — errors are silently swallowed so the REPL is never disrupted.
147
+ */
148
+ export async function speakText(text, _unusedKey) {
149
+ if (!text.trim())
150
+ return;
151
+ const geminiKey = await fetchGeminiKey();
152
+ if (!geminiKey)
153
+ return; // No key available — silently skip
154
+ // Strip markdown for natural-sounding speech
155
+ const cleaned = text
156
+ .replace(/```[\s\S]*?```/g, "")
157
+ .replace(/`[^`]+`/g, "")
158
+ .replace(/\*\*(.*?)\*\*/g, "$1")
159
+ .replace(/\*(.*?)\*/g, "$1")
160
+ .replace(/^[#>•\-*]\s*/gm, "")
161
+ .replace(/\s+/g, " ")
162
+ .trim()
163
+ .slice(0, 1000);
164
+ if (!cleaned)
165
+ return;
166
+ try {
167
+ const ai = new GoogleGenAI({ apiKey: geminiKey });
168
+ const response = await ai.models.generateContent({
169
+ model: currentTtsModel,
170
+ contents: [{ parts: [{ text: cleaned }] }],
171
+ config: {
172
+ responseModalities: [Modality.AUDIO],
173
+ speechConfig: {
174
+ voiceConfig: {
175
+ prebuiltVoiceConfig: { voiceName: currentVoice },
176
+ },
177
+ },
178
+ },
179
+ });
180
+ const parts = response?.candidates?.[0]?.content?.parts ?? [];
181
+ const audioParts = [];
182
+ for (const part of parts) {
183
+ if (part.inlineData?.data) {
184
+ audioParts.push(Buffer.from(part.inlineData.data, "base64"));
185
+ }
186
+ }
187
+ if (audioParts.length === 0)
188
+ return;
189
+ const pcmData = Buffer.concat(audioParts);
190
+ const wavBuffer = Buffer.concat([buildWavHeader(pcmData.length), pcmData]);
191
+ playWav(wavBuffer);
192
+ }
193
+ catch {
194
+ // Silently ignore — TTS errors must never crash the agent REPL
195
+ // Invalidate cached key so we retry fetching on the next call
196
+ cachedGeminiKey = null;
197
+ }
198
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "careervivid",
3
- "version": "2.1.12",
3
+ "version": "2.1.16",
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": {
@@ -31,7 +31,7 @@
31
31
  "mermaid": "11.14.0",
32
32
  "open": "^10.1.0",
33
33
  "ora": "^8.1.0",
34
- "playwright-core": "^1.51.0",
34
+ "playwright-core": "1.59.1",
35
35
  "semver": "7.7.4",
36
36
  "update-notifier": "^7.3.1"
37
37
  },