careervivid 2.1.12 → 2.1.13

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;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"}
@@ -4,8 +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, getProviderKey, setProviderKey } from "../../config.js";
7
+ import { loadConfig, getGeminiKey, 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 } 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,45 @@ 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. Agent responses will be spoken aloud.\n"));
202
+ }
203
+ else if (arg === "off") {
204
+ setVoiceEnabled(false);
205
+ stopPlayback();
206
+ console.log(chalk.yellow("\n 🔇 Voice disabled.\n"));
207
+ }
208
+ else {
209
+ 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`));
211
+ }
212
+ return ask();
213
+ }
214
+ if (cmd === "speak") {
215
+ const last = getLastResponse();
216
+ if (!last) {
217
+ console.log(chalk.dim("\n Nothing to speak yet. Ask the agent something first.\n"));
218
+ }
219
+ else {
220
+ const geminiKey = getGeminiKey() || process.env.GEMINI_API_KEY;
221
+ speakText(last, geminiKey ?? undefined).catch(() => { });
222
+ console.log(chalk.dim("\n 🔊 Speaking last response...\n"));
223
+ }
224
+ return ask();
225
+ }
183
226
  if (cmd === "models") {
184
227
  console.log(chalk.cyan("\n Available CareerVivid models:"));
185
228
  for (const m of CV_MODELS) {
@@ -418,6 +461,7 @@ export async function askLoop(engine, options, selectedProvider, selectedModel,
418
461
  };
419
462
  if (engine) {
420
463
  sessionTurns++;
464
+ let responseAccumulator = "";
421
465
  const sharedOnChunk = (text) => {
422
466
  if (firstChunk) {
423
467
  thinkingSpinner.stop();
@@ -426,6 +470,7 @@ export async function askLoop(engine, options, selectedProvider, selectedModel,
426
470
  firstChunk = false;
427
471
  }
428
472
  process.stdout.write(text);
473
+ responseAccumulator += text; // Accumulate for TTS
429
474
  };
430
475
  const sharedOnError = (error) => {
431
476
  thinkingSpinner.stop();
@@ -476,6 +521,14 @@ export async function askLoop(engine, options, selectedProvider, selectedModel,
476
521
  onError: sharedOnError,
477
522
  });
478
523
  }
524
+ // ── TTS: store last response + auto-speak if voice enabled ──────
525
+ if (responseAccumulator) {
526
+ setLastResponse(responseAccumulator);
527
+ if (isVoiceEnabled()) {
528
+ const geminiKey = getGeminiKey() || process.env.GEMINI_API_KEY;
529
+ speakText(responseAccumulator, geminiKey ?? undefined).catch(() => { });
530
+ }
531
+ }
479
532
  // ── Clean turn separator after every AI reply ─────────────────────────────
480
533
  process.stdout.write("\n" + chalk.dim("─".repeat(48)) + "\n");
481
534
  }
@@ -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,26 @@
1
+ /**
2
+ * tts.ts — Text-to-Speech engine for the CareerVivid REPL
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.
8
+ *
9
+ * Toggle: /voice on | /voice off
10
+ * Speak: /speak (reads the last agent response)
11
+ */
12
+ export declare function isVoiceEnabled(): boolean;
13
+ export declare function setVoiceEnabled(on: boolean): void;
14
+ export declare function setLastResponse(text: string): void;
15
+ export declare function getLastResponse(): string;
16
+ /**
17
+ * Stops any currently playing audio immediately.
18
+ */
19
+ export declare function stopPlayback(): void;
20
+ /**
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.
24
+ */
25
+ export declare function speakText(text: string, apiKey?: string): Promise<void>;
26
+ //# sourceMappingURL=tts.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,153 @@
1
+ /**
2
+ * tts.ts — Text-to-Speech engine for the CareerVivid REPL
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.
8
+ *
9
+ * Toggle: /voice on | /voice off
10
+ * Speak: /speak (reads the last agent response)
11
+ */
12
+ import { writeFileSync, unlinkSync } from "fs";
13
+ import { spawn } from "child_process";
14
+ import { tmpdir } from "os";
15
+ import { join } from "path";
16
+ import { GoogleGenAI, Modality } from "@google/genai";
17
+ // ── State ────────────────────────────────────────────────────────────────────
18
+ let voiceEnabled = false;
19
+ let lastResponse = "";
20
+ let playbackProcess = null;
21
+ export function isVoiceEnabled() { return voiceEnabled; }
22
+ export function setVoiceEnabled(on) { voiceEnabled = on; }
23
+ export function setLastResponse(text) { lastResponse = text; }
24
+ export function getLastResponse() { return lastResponse; }
25
+ // ── Audio Playback ────────────────────────────────────────────────────────────
26
+ /**
27
+ * Stops any currently playing audio immediately.
28
+ */
29
+ export function stopPlayback() {
30
+ if (playbackProcess && !playbackProcess.killed) {
31
+ playbackProcess.kill("SIGKILL");
32
+ playbackProcess = null;
33
+ }
34
+ }
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
+ function playWav(wavBuffer) {
40
+ const tmpFile = join(tmpdir(), `cv-tts-${Date.now()}.wav`);
41
+ writeFileSync(tmpFile, wavBuffer);
42
+ const platform = process.platform;
43
+ let playerCmd;
44
+ let playerArgs;
45
+ if (platform === "darwin") {
46
+ playerCmd = "afplay";
47
+ playerArgs = [tmpFile];
48
+ }
49
+ else if (platform === "linux") {
50
+ playerCmd = "aplay";
51
+ playerArgs = ["-q", tmpFile];
52
+ }
53
+ else {
54
+ // Windows fallback via PowerShell
55
+ playerCmd = "powershell";
56
+ playerArgs = ["-c", `(New-Object System.Media.SoundPlayer '${tmpFile}').PlaySync()`];
57
+ }
58
+ stopPlayback(); // Stop any previous playback
59
+ const child = spawn(playerCmd, playerArgs, { stdio: "ignore", detached: false });
60
+ playbackProcess = child;
61
+ child.on("close", () => {
62
+ try {
63
+ unlinkSync(tmpFile);
64
+ }
65
+ catch { /* ignore cleanup errors */ }
66
+ if (playbackProcess === child)
67
+ playbackProcess = null;
68
+ });
69
+ child.on("error", () => {
70
+ // Silently ignore — player may not be installed
71
+ try {
72
+ unlinkSync(tmpFile);
73
+ }
74
+ catch { /* ignore */ }
75
+ });
76
+ }
77
+ // ── WAV Header Builder ────────────────────────────────────────────────────────
78
+ function buildWavHeader(dataLength, sampleRate = 24000, channels = 1, bitsPerSample = 16) {
79
+ const byteRate = sampleRate * channels * bitsPerSample / 8;
80
+ const blockAlign = channels * bitsPerSample / 8;
81
+ const header = Buffer.alloc(44);
82
+ header.write("RIFF", 0);
83
+ header.writeUInt32LE(36 + dataLength, 4);
84
+ header.write("WAVE", 8);
85
+ header.write("fmt ", 12);
86
+ header.writeUInt32LE(16, 16);
87
+ header.writeUInt16LE(1, 20); // PCM
88
+ header.writeUInt16LE(channels, 22);
89
+ header.writeUInt32LE(sampleRate, 24);
90
+ header.writeUInt32LE(byteRate, 28);
91
+ header.writeUInt16LE(blockAlign, 32);
92
+ header.writeUInt16LE(bitsPerSample, 34);
93
+ header.write("data", 36);
94
+ header.writeUInt32LE(dataLength, 40);
95
+ return header;
96
+ }
97
+ // ── TTS Synthesis ────────────────────────────────────────────────────────────
98
+ /**
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.
102
+ */
103
+ export async function speakText(text, apiKey) {
104
+ if (!text.trim())
105
+ 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
110
+ 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
116
+ .replace(/\s+/g, " ")
117
+ .trim()
118
+ .slice(0, 1000); // Cap to ~1000 chars to keep TTS fast
119
+ if (!cleaned)
120
+ return;
121
+ try {
122
+ const ai = new GoogleGenAI({ apiKey: key });
123
+ const response = await ai.models.generateContent({
124
+ model: "gemini-2.5-flash-preview-tts",
125
+ contents: [{ parts: [{ text: cleaned }] }],
126
+ config: {
127
+ responseModalities: [Modality.AUDIO],
128
+ speechConfig: {
129
+ voiceConfig: {
130
+ prebuiltVoiceConfig: { voiceName: "Zephyr" },
131
+ },
132
+ },
133
+ },
134
+ });
135
+ const parts = response?.candidates?.[0]?.content?.parts ?? [];
136
+ const audioParts = [];
137
+ for (const part of parts) {
138
+ if (part.inlineData?.data) {
139
+ audioParts.push(Buffer.from(part.inlineData.data, "base64"));
140
+ }
141
+ }
142
+ if (audioParts.length === 0)
143
+ return;
144
+ 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
148
+ playWav(wavBuffer);
149
+ }
150
+ catch {
151
+ // Silently swallow TTS errors — never disrupt the agent session
152
+ }
153
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "careervivid",
3
- "version": "2.1.12",
3
+ "version": "2.1.13",
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
  },