careervivid 2.1.21 → 2.1.22

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.
@@ -50,6 +50,6 @@ export function printBanner(options, selectedProvider, selectedModel, _thinkingB
50
50
  else {
51
51
  console.log(chalk.dim(` Provider: ${selectedProvider} · Model: ${selectedModel} [0 credits]`));
52
52
  }
53
- const modeLabel = options.jobs ? "Jobs & Applications" : options.resume ? "Resume" : "General";
53
+ const modeLabel = options.resume ? "Resume" : options.coding ? "Coding" : "Jobs & Applications";
54
54
  console.log(chalk.dim(` Mode: ${modeLabel} · Type 'exit' to quit · /help for commands.\n`));
55
55
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/agent/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAapC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,QA6IpD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/agent/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAapC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,QAmJpD"}
@@ -13,7 +13,7 @@ export function registerAgentCommand(program) {
13
13
  .description("Start an interactive autonomous AI agent in your terminal.")
14
14
  .option("--coding", "Enable full coding tool suite (file I/O, shell, search).")
15
15
  .option("--resume", "Add resume tools — load and discuss your CareerVivid resume.")
16
- .option("--jobs", "Add job-hunting tools — search jobs, save to tracker, update statuses.")
16
+ .option("--jobs", "Add job-hunting tools (default enabled automatically).")
17
17
  .option("--flash", "Use gemini-3-flash-preview — latest flash model (fast, 1 credit/turn).")
18
18
  .option("--pro", "Use gemini-3.1-pro-preview with thinking mode (recommended for complex tasks).")
19
19
  .option("--think <budget>", "Enable Gemini thinking mode with the given token budget (e.g. 8192).", parseInt)
@@ -24,6 +24,11 @@ export function registerAgentCommand(program) {
24
24
  .option("--api-key <key>", "BYO API key for this session (not saved).")
25
25
  .option("--base-url <url>", "Custom OpenAI-compatible base URL.")
26
26
  .action(async (options) => {
27
+ // Jobs mode is the primary entry point — always enabled unless an explicit
28
+ // alternate mode flag (--resume, --coding) was passed.
29
+ if (!options.resume && !options.coding) {
30
+ options.jobs = true;
31
+ }
27
32
  const cvApiKey = getApiKey();
28
33
  const project = options.project ?? process.env.GOOGLE_CLOUD_PROJECT;
29
34
  const wantsCareerVividCloud = !options.provider || options.provider === "careervivid";
@@ -114,8 +119,8 @@ export function registerAgentCommand(program) {
114
119
  }
115
120
  const engine = buildEngine(selectedProvider, selectedModel, systemInstruction, tools, thinkingBudget, includeThoughts, cvApiKey, geminiApiKey, project);
116
121
  printBanner(options, selectedProvider, selectedModel, thinkingBudget);
117
- // ── Record session context for memory ────────────────────────────────────────────────
118
- const agentMode = options.jobs ? "jobs" : options.resume ? "resume" : "coding";
122
+ // agentMode: jobs is now always the default
123
+ const agentMode = options.resume ? "resume" : options.coding ? "coding" : "jobs";
119
124
  initSessionContext(agentMode, selectedModel);
120
125
  await askLoop(engine, options, selectedProvider, selectedModel, cvApiKey, systemInstruction, tools);
121
126
  });
@@ -0,0 +1,35 @@
1
+ /**
2
+ * repl/engineLoop.ts
3
+ *
4
+ * Runs the AI response loop for both engine types:
5
+ * - CareerVividProxyEngine (cv_live_ API key → cloud proxy)
6
+ * - BYO providers (OpenAI, Anthropic, OpenRouter, custom)
7
+ *
8
+ * Returns the full accumulated text response for TTS / audit.
9
+ */
10
+ import { CareerVividProxyEngine } from "../../../agent/CareerVividProxyEngine.js";
11
+ import { QueryEngine } from "../../../agent/QueryEngine.js";
12
+ import { type LLMProvider } from "../../../config.js";
13
+ import { ToolHandlerState } from "./toolHandlers.js";
14
+ export interface EngineRunOptions {
15
+ engine: QueryEngine | CareerVividProxyEngine | null;
16
+ userInput: string;
17
+ selectedProvider: LLMProvider;
18
+ selectedModel: string;
19
+ currentModel: string;
20
+ byoHistory: any[];
21
+ tools: any[];
22
+ systemInstruction: string;
23
+ verbose: boolean;
24
+ sessionTurns: number;
25
+ apiKey: string | undefined;
26
+ baseUrl: string | undefined;
27
+ handleSigInt: () => void;
28
+ toolState: ToolHandlerState;
29
+ onCreditInfo: (remaining: number | null, limit: number | null) => void;
30
+ }
31
+ /**
32
+ * Run one full agent turn. Returns the accumulated text response.
33
+ */
34
+ export declare function runEngineLoop(opts: EngineRunOptions): Promise<string>;
35
+ //# sourceMappingURL=engineLoop.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engineLoop.d.ts","sourceRoot":"","sources":["../../../../src/commands/agent/repl/engineLoop.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,EAAE,sBAAsB,EAAE,MAAM,0CAA0C,CAAC;AAClF,OAAO,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAC5D,OAAO,EAA8B,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAElF,OAAO,EAA4B,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAc/E,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,WAAW,GAAG,sBAAsB,GAAG,IAAI,CAAC;IACpD,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,EAAE,WAAW,CAAC;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,GAAG,EAAE,CAAC;IAClB,KAAK,EAAE,GAAG,EAAE,CAAC;IACb,iBAAiB,EAAE,MAAM,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,SAAS,EAAE,gBAAgB,CAAC;IAC5B,YAAY,EAAE,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;CACxE;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CA6K3E"}
@@ -0,0 +1,168 @@
1
+ /**
2
+ * repl/engineLoop.ts
3
+ *
4
+ * Runs the AI response loop for both engine types:
5
+ * - CareerVividProxyEngine (cv_live_ API key → cloud proxy)
6
+ * - BYO providers (OpenAI, Anthropic, OpenRouter, custom)
7
+ *
8
+ * Returns the full accumulated text response for TTS / audit.
9
+ */
10
+ import chalk from "chalk";
11
+ import ora from "ora";
12
+ import { CareerVividProxyEngine } from "../../../agent/CareerVividProxyEngine.js";
13
+ import { QueryEngine } from "../../../agent/QueryEngine.js";
14
+ import { loadConfig, getProviderKey } from "../../../config.js";
15
+ import { printCreditStatus } from "../repl.js";
16
+ import { onToolCall, onToolResult } from "./toolHandlers.js";
17
+ /** Timeout wrapper */
18
+ function withTimeout(p, ms, label) {
19
+ return new Promise((resolve, reject) => {
20
+ const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms / 1000}s. Press Ctrl+C if stuck.`)), ms);
21
+ p.then(v => { clearTimeout(timer); resolve(v); })
22
+ .catch(e => { clearTimeout(timer); reject(e); });
23
+ });
24
+ }
25
+ /**
26
+ * Run one full agent turn. Returns the accumulated text response.
27
+ */
28
+ export async function runEngineLoop(opts) {
29
+ const { engine, userInput, selectedProvider, currentModel, byoHistory, tools, systemInstruction, verbose, apiKey, baseUrl, handleSigInt, toolState, } = opts;
30
+ let responseAccumulator = "";
31
+ let firstChunk = true;
32
+ const thinkingSpinner = ora({
33
+ text: chalk.dim("Vivid is thinking…"),
34
+ color: "cyan",
35
+ spinner: "dots",
36
+ }).start();
37
+ const onChunk = (text) => {
38
+ if (firstChunk) {
39
+ thinkingSpinner.stop();
40
+ process.stdout.write("\n" + chalk.hex("#6366f1")("✦ "));
41
+ firstChunk = false;
42
+ }
43
+ process.stdout.write(text);
44
+ responseAccumulator += text;
45
+ };
46
+ const onThinking = (thought) => {
47
+ if (verbose) {
48
+ console.log(chalk.dim(`\n[thinking] ${thought.substring(0, 200)}...`));
49
+ }
50
+ };
51
+ const onError = (error) => {
52
+ thinkingSpinner.stop();
53
+ if (toolState.currentSpinner) {
54
+ toolState.currentSpinner.fail("Tool error");
55
+ toolState.currentSpinner = null;
56
+ }
57
+ console.log(chalk.red(`\n❌ Error: ${error.message}\n`));
58
+ };
59
+ const onCompacting = () => {
60
+ console.log(chalk.dim("\n📦 Compacting context window...\n"));
61
+ };
62
+ const boundOnToolCall = (name, args) => onToolCall(name, args, thinkingSpinner, toolState);
63
+ const boundOnToolResult = (name, result) => onToolResult(name, result, toolState);
64
+ // ── CareerVivid Cloud engine ──────────────────────────────────────────────
65
+ if (engine instanceof CareerVividProxyEngine) {
66
+ await engine.runLoopStreaming(userInput, {
67
+ onChunk,
68
+ onThinking,
69
+ onToolCall: boundOnToolCall,
70
+ onToolResult: boundOnToolResult,
71
+ onCompacting,
72
+ onError,
73
+ onResponse: async (creditInfo) => {
74
+ opts.onCreditInfo(creditInfo.creditsRemaining, creditInfo.monthlyLimit);
75
+ printCreditStatus(creditInfo.creditsRemaining, creditInfo.monthlyLimit);
76
+ },
77
+ onCreditLimitReached: (remaining) => {
78
+ console.log(chalk.red(`\n\n⚠️ Credit limit reached (${remaining} remaining).\n` +
79
+ chalk.dim(" Upgrade or top up at ") +
80
+ chalk.underline.blue("careervivid.app/developer")));
81
+ },
82
+ });
83
+ // ── QueryEngine (direct Gemini) ───────────────────────────────────────────
84
+ }
85
+ else if (engine instanceof QueryEngine) {
86
+ await engine.runLoopStreaming(userInput, {
87
+ onChunk,
88
+ onThinking,
89
+ onToolCall: boundOnToolCall,
90
+ onToolResult: boundOnToolResult,
91
+ onCompacting,
92
+ onError,
93
+ });
94
+ // ── BYO Provider (OpenAI / Anthropic / OpenRouter / custom) ──────────────
95
+ }
96
+ else {
97
+ const { createOpenAICompatibleProvider } = await import("../../../agent/providers/OpenAIProvider.js");
98
+ const { AnthropicProvider } = await import("../../../agent/providers/AnthropicProvider.js");
99
+ const byoApiKey = apiKey || getProviderKey(selectedProvider) || loadConfig().llmApiKey || "";
100
+ const resolvedBaseUrl = baseUrl || loadConfig().llmBaseUrl;
101
+ let provider;
102
+ if (selectedProvider === "anthropic") {
103
+ provider = new AnthropicProvider({ apiKey: byoApiKey });
104
+ }
105
+ else {
106
+ const sub = selectedProvider === "openrouter" ? "openrouter" :
107
+ selectedProvider === "custom" ? "custom" : "openai";
108
+ provider = createOpenAICompatibleProvider(sub, byoApiKey, resolvedBaseUrl);
109
+ }
110
+ let userTurn = { role: "user", parts: [{ text: userInput }] };
111
+ let round = 0;
112
+ while (round < 10) {
113
+ const result = await withTimeout(provider.generate({ model: currentModel, history: byoHistory, userTurn, tools, systemInstruction }), 45_000, "LLM generate()");
114
+ if (round === 0) {
115
+ thinkingSpinner.stop();
116
+ process.stdout.write("\n" + chalk.hex("#6366f1")("✦ "));
117
+ firstChunk = false;
118
+ }
119
+ if (result.text) {
120
+ process.stdout.write(result.text);
121
+ responseAccumulator += result.text;
122
+ }
123
+ byoHistory.push(userTurn);
124
+ byoHistory.push({ role: "model", parts: result.rawParts || [{ text: result.text }] });
125
+ if (!result.functionCalls?.length)
126
+ break;
127
+ const fnResponses = [];
128
+ for (const fc of result.functionCalls) {
129
+ const allow = await boundOnToolCall(fc.name, fc.args);
130
+ if (!allow) {
131
+ fnResponses.push({
132
+ functionResponse: { id: fc.id, name: fc.name, response: { error: "User denied execution." } },
133
+ });
134
+ continue;
135
+ }
136
+ const tool = tools.find((t) => t.name === fc.name);
137
+ let out;
138
+ try {
139
+ if (fc.name === "start_interview") {
140
+ process.removeListener("SIGINT", handleSigInt);
141
+ try {
142
+ out = tool ? await tool.execute(fc.args) : { error: "Tool not found" };
143
+ }
144
+ finally {
145
+ process.on("SIGINT", handleSigInt);
146
+ }
147
+ }
148
+ else {
149
+ out = tool
150
+ ? await withTimeout(tool.execute(fc.args), 45_000, `tool:${fc.name}`)
151
+ : { error: "Tool not found" };
152
+ }
153
+ }
154
+ catch (e) {
155
+ out = e.message?.includes("No API key configured")
156
+ ? { error: "CareerVivid API key not found. Run 'cv login' to authenticate." }
157
+ : { error: e.message };
158
+ }
159
+ boundOnToolResult(fc.name, out);
160
+ fnResponses.push({ functionResponse: { id: fc.id, name: fc.name, response: out } });
161
+ }
162
+ userTurn = { role: "user", parts: fnResponses };
163
+ round++;
164
+ }
165
+ }
166
+ process.stdout.write("\n" + chalk.dim("─".repeat(48)) + "\n");
167
+ return responseAccumulator;
168
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * repl/input.ts
3
+ *
4
+ * Handles all user input collection:
5
+ * - First-turn quick-action menu (autocomplete)
6
+ * - Subsequent turn free-text input
7
+ * - Multi-line paste mode (<<<)
8
+ * - Fast-paste buffer accumulation
9
+ */
10
+ /** Read the first-turn menu selection. Returns the cleaned user intent string. */
11
+ export declare function readFirstTurnInput(): Promise<string>;
12
+ /** Reads a single multi-line paste block. User ends with an empty Enter. */
13
+ export declare function readMultiLineInput(prefix: string): Promise<string>;
14
+ export interface InputResult {
15
+ text: string;
16
+ /** True when the input arrived in < 150ms (possible paste line) */
17
+ isFastLine: boolean;
18
+ }
19
+ /** Read a normal subsequent-turn line. Returns the raw text and timing flag. */
20
+ export declare function readNormalInput(showContinuation: boolean): Promise<InputResult>;
21
+ //# sourceMappingURL=input.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../../../../src/commands/agent/repl/input.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAiBH,kFAAkF;AAClF,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,MAAM,CAAC,CAyB1D;AAED,4EAA4E;AAC5E,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAoBxE;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,mEAAmE;IACnE,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,gFAAgF;AAChF,wBAAsB,eAAe,CAAC,gBAAgB,EAAE,OAAO,GAAG,OAAO,CAAC,WAAW,CAAC,CAUrF"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * repl/input.ts
3
+ *
4
+ * Handles all user input collection:
5
+ * - First-turn quick-action menu (autocomplete)
6
+ * - Subsequent turn free-text input
7
+ * - Multi-line paste mode (<<<)
8
+ * - Fast-paste buffer accumulation
9
+ */
10
+ import chalk from "chalk";
11
+ import pkg from "enquirer";
12
+ const { prompt } = pkg;
13
+ const MENU_ITEMS = [
14
+ "📄 View or update my resume",
15
+ "🔍 Search for job opportunities",
16
+ "📊 Check my job pipeline / tracker",
17
+ "✉️ Draft a cover letter or tailor my resume",
18
+ "🎙 Start an AI mock interview (voice or text)",
19
+ "📈 Get an overview of my job search progress",
20
+ "🗓️ Pick up where we left off",
21
+ ];
22
+ /** Read the first-turn menu selection. Returns the cleaned user intent string. */
23
+ export async function readFirstTurnInput() {
24
+ console.log(chalk.dim(" What would you like to do today?\n"));
25
+ for (const item of MENU_ITEMS) {
26
+ console.log(chalk.dim(` ${item}`));
27
+ }
28
+ console.log("");
29
+ const firstResp = await prompt({
30
+ type: "autocomplete",
31
+ name: "choice",
32
+ message: chalk.bold.hex("#6366f1")("❯") + chalk.dim(" ·"),
33
+ limit: 7,
34
+ suggest(input, choices) {
35
+ if (!input)
36
+ return choices;
37
+ return choices.filter((c) => c.value.toLowerCase().includes(input.toLowerCase()));
38
+ },
39
+ choices: MENU_ITEMS.map(item => ({ name: item, value: item })),
40
+ footer: chalk.dim(" ↑↓ to navigate · type to filter · Enter to send"),
41
+ });
42
+ const raw = firstResp.choice?.trim() || "";
43
+ // Strip emoji prefixes so the agent gets clean text
44
+ return raw.replace(/^[\p{Emoji}\s]+/u, "").trim() || raw;
45
+ }
46
+ /** Reads a single multi-line paste block. User ends with an empty Enter. */
47
+ export async function readMultiLineInput(prefix) {
48
+ console.log(chalk.dim(" 📋 Multi-line mode: paste your text, then press Enter twice to submit.\n"));
49
+ const lines = prefix ? [prefix] : [];
50
+ let emptyCount = 0;
51
+ while (emptyCount < 1) {
52
+ const lineResp = await prompt({
53
+ type: "input",
54
+ name: "line",
55
+ message: chalk.dim(" │"),
56
+ });
57
+ if (lineResp.line === "") {
58
+ emptyCount++;
59
+ }
60
+ else {
61
+ emptyCount = 0;
62
+ lines.push(lineResp.line);
63
+ }
64
+ }
65
+ return lines.join("\n").trim();
66
+ }
67
+ /** Read a normal subsequent-turn line. Returns the raw text and timing flag. */
68
+ export async function readNormalInput(showContinuation) {
69
+ const t0 = Date.now();
70
+ const response = await prompt({
71
+ type: "input",
72
+ name: "query",
73
+ message: showContinuation
74
+ ? chalk.dim("... ")
75
+ : chalk.bold.hex("#6366f1")("❯") + chalk.dim(" ·"),
76
+ });
77
+ return { text: response.query, isFastLine: Date.now() - t0 < 150 };
78
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * repl/slashCommands.ts
3
+ *
4
+ * Handles all slash (/) commands typed in the REPL:
5
+ * /help, /voice, /speak, /models, /model
6
+ *
7
+ * Returns `true` if the command was handled (caller should re-prompt),
8
+ * returns `false` if the input was not a slash command.
9
+ */
10
+ interface ModelSwitchContext {
11
+ currentModel: string;
12
+ cvApiKey: string | undefined;
13
+ engine: any;
14
+ systemInstruction: string;
15
+ tools: any[];
16
+ options: {
17
+ think?: number;
18
+ };
19
+ }
20
+ export interface SlashCommandResult {
21
+ /** If model was switched, contains the new values */
22
+ modelSwitch?: {
23
+ newModel: string;
24
+ newEngine: any;
25
+ };
26
+ }
27
+ /**
28
+ * Dispatch a slash command. Returns the result or null if not a slash command.
29
+ * Always returns non-null for slash inputs — caller should re-prompt after.
30
+ */
31
+ export declare function handleSlashCommand(input: string, currentModel: string, modelCtx: Omit<ModelSwitchContext, "currentModel">): Promise<SlashCommandResult | null>;
32
+ export {};
33
+ //# sourceMappingURL=slashCommands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slashCommands.d.ts","sourceRoot":"","sources":["../../../../src/commands/agent/repl/slashCommands.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA6IH,UAAU,kBAAkB;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,MAAM,EAAE,GAAG,CAAC;IACZ,iBAAiB,EAAE,MAAM,CAAC;IAC1B,KAAK,EAAE,GAAG,EAAE,CAAC;IACb,OAAO,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC7B;AAyCD,MAAM,WAAW,kBAAkB;IACjC,qDAAqD;IACrD,WAAW,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,GAAG,CAAA;KAAE,CAAC;CACpD;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,IAAI,CAAC,kBAAkB,EAAE,cAAc,CAAC,GACjD,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAkCpC"}
@@ -0,0 +1,193 @@
1
+ /**
2
+ * repl/slashCommands.ts
3
+ *
4
+ * Handles all slash (/) commands typed in the REPL:
5
+ * /help, /voice, /speak, /models, /model
6
+ *
7
+ * Returns `true` if the command was handled (caller should re-prompt),
8
+ * returns `false` if the input was not a slash command.
9
+ */
10
+ import chalk from "chalk";
11
+ import pkg from "enquirer";
12
+ import { CV_MODELS } from "../configurator.js";
13
+ import { CareerVividProxyEngine } from "../../../agent/CareerVividProxyEngine.js";
14
+ import { isVoiceEnabled, setVoiceEnabled, stopPlayback, getLastResponse, speakText, getCurrentVoice, setCurrentVoice, getCurrentTtsModel, setCurrentTtsModel, AVAILABLE_VOICES, AVAILABLE_TTS_MODELS, } from "../../../lib/tts.js";
15
+ const { prompt } = pkg;
16
+ // ── /help ─────────────────────────────────────────────────────────────────────
17
+ function handleHelp() {
18
+ console.log(chalk.cyan("\n Slash commands:"));
19
+ console.log(chalk.dim(" /model <name> — Switch to a different model mid-session"));
20
+ console.log(chalk.dim(" /models — List all available CareerVivid models"));
21
+ console.log(chalk.dim(" /voice — Voice / TTS settings (interactive)"));
22
+ console.log(chalk.dim(" /voice on|off — Quick toggle"));
23
+ console.log(chalk.dim(" /speak — Read the last agent response aloud"));
24
+ console.log(chalk.dim(" /help — Show this help message"));
25
+ console.log(chalk.dim(" exit — End the session"));
26
+ console.log(chalk.cyan("\n Shell escape (run terminal commands without leaving the agent):"));
27
+ console.log(chalk.dim(" !<command> — e.g. !ls -la or !git status\n"));
28
+ console.log(chalk.cyan(" Paste long content (job descriptions, cover letters):"));
29
+ console.log(chalk.dim(" <<< — Open multi-line paste mode; press Enter twice when done"));
30
+ console.log(chalk.dim(" <<<your text — Start with text directly after <<<\n"));
31
+ }
32
+ // ── /voice (interactive select) ───────────────────────────────────────────────
33
+ async function handleVoice(arg) {
34
+ // Quick text shortcuts (scriptable / muscle-memory)
35
+ if (arg === "on") {
36
+ setVoiceEnabled(true);
37
+ console.log(chalk.green(`\n 🔊 Voice on (${getCurrentVoice()} · ${getCurrentTtsModel()})\n`));
38
+ return;
39
+ }
40
+ if (arg === "off") {
41
+ setVoiceEnabled(false);
42
+ stopPlayback();
43
+ console.log(chalk.yellow("\n 🔇 Voice off\n"));
44
+ return;
45
+ }
46
+ // Interactive top-level menu
47
+ const topChoice = await prompt({
48
+ type: "select",
49
+ name: "action",
50
+ message: `Voice settings (${chalk.dim(`voice: ${getCurrentVoice()} model: ${getCurrentTtsModel()}}`)})`,
51
+ choices: [
52
+ { name: "toggle", message: isVoiceEnabled() ? "🔇 Turn voice off" : "🔊 Turn voice on" },
53
+ { name: "set-voice", message: "🎵 Pick a voice" },
54
+ { name: "set-model", message: "⚙️ Pick a TTS model" },
55
+ { name: "speak", message: "▶️ Replay last response" },
56
+ { name: "cancel", message: chalk.dim("Cancel") },
57
+ ],
58
+ });
59
+ if (topChoice.action === "toggle") {
60
+ const newState = !isVoiceEnabled();
61
+ setVoiceEnabled(newState);
62
+ if (!newState)
63
+ stopPlayback();
64
+ console.log(newState
65
+ ? chalk.green(`\n 🔊 Voice on (${getCurrentVoice()} · ${getCurrentTtsModel()})\n`)
66
+ : chalk.yellow("\n 🔇 Voice off\n"));
67
+ }
68
+ else if (topChoice.action === "set-voice") {
69
+ const { voice } = await prompt({
70
+ type: "select",
71
+ name: "voice",
72
+ message: "Choose a voice:",
73
+ choices: AVAILABLE_VOICES.map((v) => ({
74
+ name: v,
75
+ message: v === getCurrentVoice() ? chalk.green(`${v} ← active`) : v,
76
+ })),
77
+ });
78
+ setCurrentVoice(voice);
79
+ console.log(chalk.green(`\n 🎵 Voice set to ${chalk.bold(voice)}\n`));
80
+ }
81
+ else if (topChoice.action === "set-model") {
82
+ const MODEL_LABELS = {
83
+ "gemini-3.1-flash-tts-preview": "Gemini 3.1 Flash (latest, fast)",
84
+ "gemini-2.5-flash-preview-tts": "Gemini 2.5 Flash (previous gen, fast)",
85
+ "gemini-2.5-pro-preview-tts": "Gemini 2.5 Pro (previous gen, high quality)",
86
+ };
87
+ const { model } = await prompt({
88
+ type: "select",
89
+ name: "model",
90
+ message: "Choose a TTS model:",
91
+ choices: AVAILABLE_TTS_MODELS.map((m) => ({
92
+ name: m,
93
+ message: m === getCurrentTtsModel()
94
+ ? chalk.green(`${MODEL_LABELS[m] ?? m} ← active`)
95
+ : (MODEL_LABELS[m] ?? m),
96
+ })),
97
+ });
98
+ setCurrentTtsModel(model);
99
+ console.log(chalk.green(`\n ⚙️ TTS model set to ${chalk.bold(model)}\n`));
100
+ }
101
+ else if (topChoice.action === "speak") {
102
+ const last = getLastResponse();
103
+ if (!last) {
104
+ console.log(chalk.dim("\n Nothing to speak yet.\n"));
105
+ }
106
+ else {
107
+ speakText(last).catch(() => { });
108
+ console.log(chalk.dim("\n 🔊 Speaking...\n"));
109
+ }
110
+ }
111
+ }
112
+ // ── /speak ────────────────────────────────────────────────────────────────────
113
+ function handleSpeak() {
114
+ const last = getLastResponse();
115
+ if (!last) {
116
+ console.log(chalk.dim("\n Nothing to speak yet. Ask the agent something first.\n"));
117
+ }
118
+ else {
119
+ speakText(last).catch(() => { });
120
+ console.log(chalk.dim("\n 🔊 Speaking last response...\n"));
121
+ }
122
+ }
123
+ // ── /models ───────────────────────────────────────────────────────────────────
124
+ function handleModels(currentModel) {
125
+ console.log(chalk.cyan("\n Available CareerVivid models:"));
126
+ for (const m of CV_MODELS) {
127
+ const active = m.value === currentModel ? chalk.green(" ← active") : "";
128
+ console.log(` ${m.name}${active}`);
129
+ }
130
+ console.log(chalk.dim("\n Usage: /model gemini-2.5-flash\n"));
131
+ }
132
+ function handleModel(arg, ctx) {
133
+ if (!arg) {
134
+ console.log(chalk.yellow(`\n Current model: ${chalk.bold(ctx.currentModel)}`));
135
+ console.log(chalk.dim(" Usage: /model <name> e.g. /model gemini-3.1-pro-preview"));
136
+ console.log(chalk.dim(" Run /models to see all available options.\n"));
137
+ return null;
138
+ }
139
+ const known = CV_MODELS.find(m => m.value === arg);
140
+ if (!known && !arg.includes("/") && !arg.includes("-")) {
141
+ console.log(chalk.red(`\n Unknown model: ${arg}`));
142
+ console.log(chalk.dim(" Run /models to see available options.\n"));
143
+ return null;
144
+ }
145
+ let newEngine = ctx.engine;
146
+ if (ctx.cvApiKey && ctx.engine instanceof CareerVividProxyEngine) {
147
+ newEngine = new CareerVividProxyEngine({
148
+ cvApiKey: ctx.cvApiKey,
149
+ model: arg,
150
+ systemInstruction: ctx.systemInstruction,
151
+ tools: ctx.tools,
152
+ thinkingBudget: arg.includes("pro") ? (ctx.options.think ?? 8192) : 0,
153
+ maxHistoryLength: 40,
154
+ });
155
+ }
156
+ const creditInfo = known ? chalk.dim(` (${known.cost} credit/turn)`) : "";
157
+ console.log(chalk.green(`\n ✔ Switched to ${chalk.bold(arg)}${creditInfo}`));
158
+ console.log(chalk.dim(" Conversation history has been reset.\n"));
159
+ return { newModel: arg, newEngine };
160
+ }
161
+ /**
162
+ * Dispatch a slash command. Returns the result or null if not a slash command.
163
+ * Always returns non-null for slash inputs — caller should re-prompt after.
164
+ */
165
+ export async function handleSlashCommand(input, currentModel, modelCtx) {
166
+ if (!input.startsWith("/"))
167
+ return null;
168
+ const [cmd, ...rest] = input.slice(1).split(" ");
169
+ const arg = rest.join(" ").trim();
170
+ switch (cmd) {
171
+ case "help":
172
+ handleHelp();
173
+ break;
174
+ case "voice":
175
+ await handleVoice(arg);
176
+ break;
177
+ case "speak":
178
+ handleSpeak();
179
+ break;
180
+ case "models":
181
+ handleModels(currentModel);
182
+ break;
183
+ case "model": {
184
+ const result = handleModel(arg, { ...modelCtx, currentModel });
185
+ if (result)
186
+ return { modelSwitch: result };
187
+ break;
188
+ }
189
+ default:
190
+ console.log(chalk.yellow(`\n Unknown command: /${cmd}. Type /help for available commands.\n`));
191
+ }
192
+ return {};
193
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * repl/toolHandlers.ts
3
+ *
4
+ * Tool call confirmation, spinner lifecycle, mutation budgets,
5
+ * circuit breaker, and audit logging for the REPL.
6
+ */
7
+ import ora from "ora";
8
+ export declare const WRITE_TOOLS: Set<string>;
9
+ export declare const SESSION_MAX_MUTATIONS = 25;
10
+ export declare const TURN_MAX_MUTATIONS = 10;
11
+ export interface ToolHandlerState {
12
+ sessionMutations: number;
13
+ turnMutations: number;
14
+ trustAllCommands: boolean;
15
+ trustAllWrites: boolean;
16
+ currentSpinner: ReturnType<typeof ora> | null;
17
+ lastToolCall: {
18
+ name: string;
19
+ argsHash: string;
20
+ count: number;
21
+ };
22
+ }
23
+ export declare function createToolHandlerState(): ToolHandlerState;
24
+ /**
25
+ * Called before a tool executes. Returns true to allow, false to deny.
26
+ * Manages confirmation prompts, spinner start, and mutation budgets.
27
+ */
28
+ export declare function onToolCall(name: string, args: any, thinkingSpinner: ReturnType<typeof ora>, state: ToolHandlerState): Promise<boolean>;
29
+ /**
30
+ * Called after a tool completes. Stops spinner, logs audit entry.
31
+ */
32
+ export declare function onToolResult(name: string, result: any, state: ToolHandlerState): void;
33
+ //# sourceMappingURL=toolHandlers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toolHandlers.d.ts","sourceRoot":"","sources":["../../../../src/commands/agent/repl/toolHandlers.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,GAAG,MAAM,KAAK,CAAC;AAQtB,eAAO,MAAM,WAAW,aAItB,CAAC;AACH,eAAO,MAAM,qBAAqB,KAAK,CAAC;AACxC,eAAO,MAAM,kBAAkB,KAAK,CAAC;AA6CrC,MAAM,WAAW,gBAAgB;IAC/B,gBAAgB,EAAE,MAAM,CAAC;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,cAAc,EAAE,OAAO,CAAC;IACxB,cAAc,EAAE,UAAU,CAAC,OAAO,GAAG,CAAC,GAAG,IAAI,CAAC;IAC9C,YAAY,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CACjE;AAED,wBAAgB,sBAAsB,IAAI,gBAAgB,CASzD;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,GAAG,EACT,eAAe,EAAE,UAAU,CAAC,OAAO,GAAG,CAAC,EACvC,KAAK,EAAE,gBAAgB,GACtB,OAAO,CAAC,OAAO,CAAC,CAmGlB;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,gBAAgB,QAe9E"}