careervivid 2.1.18 → 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.
- package/dist/commands/agent/engineResolver.js +1 -1
- package/dist/commands/agent/index.d.ts.map +1 -1
- package/dist/commands/agent/index.js +8 -3
- package/dist/commands/agent/repl/engineLoop.d.ts +35 -0
- package/dist/commands/agent/repl/engineLoop.d.ts.map +1 -0
- package/dist/commands/agent/repl/engineLoop.js +168 -0
- package/dist/commands/agent/repl/input.d.ts +21 -0
- package/dist/commands/agent/repl/input.d.ts.map +1 -0
- package/dist/commands/agent/repl/input.js +78 -0
- package/dist/commands/agent/repl/slashCommands.d.ts +33 -0
- package/dist/commands/agent/repl/slashCommands.d.ts.map +1 -0
- package/dist/commands/agent/repl/slashCommands.js +193 -0
- package/dist/commands/agent/repl/toolHandlers.d.ts +33 -0
- package/dist/commands/agent/repl/toolHandlers.d.ts.map +1 -0
- package/dist/commands/agent/repl/toolHandlers.js +185 -0
- package/dist/commands/agent/repl.d.ts +10 -0
- package/dist/commands/agent/repl.d.ts.map +1 -1
- package/dist/commands/agent/repl.js +133 -609
- package/dist/lib/tts.d.ts +19 -9
- package/dist/lib/tts.d.ts.map +1 -1
- package/dist/lib/tts.js +129 -50
- package/package.json +1 -1
|
@@ -1,14 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* repl.ts — REPL orchestrator for the CareerVivid agent
|
|
3
|
+
*
|
|
4
|
+
* This file is intentionally thin. Each concern lives in its own module:
|
|
5
|
+
*
|
|
6
|
+
* repl/input.ts — User input: first-turn menu, paste buffer, <<<
|
|
7
|
+
* repl/slashCommands.ts — /help, /voice, /speak, /models, /model
|
|
8
|
+
* repl/toolHandlers.ts — Tool confirmation, spinner, mutation budget, audit
|
|
9
|
+
* repl/engineLoop.ts — CareerVivid & BYO provider run loops
|
|
10
|
+
*/
|
|
1
11
|
import chalk from "chalk";
|
|
2
12
|
import pkg from "enquirer";
|
|
3
|
-
import ora from "ora";
|
|
4
|
-
import { isSafeCommand } from "../../agent/tools/coding.js";
|
|
5
13
|
import { CareerVividProxyEngine } from "../../agent/CareerVividProxyEngine.js";
|
|
6
|
-
import {
|
|
7
|
-
import { loadConfig, getProviderKey, setProviderKey } from "../../config.js";
|
|
8
|
-
import { auditLog, writeSessionSummary, SESSION_ID } from "../../agent/agentAuditLog.js";
|
|
14
|
+
import { writeSessionSummary } from "../../agent/agentAuditLog.js";
|
|
9
15
|
import { runShellEscape } from "../../lib/shell.js";
|
|
10
|
-
import {
|
|
16
|
+
import { setLastResponse, isVoiceEnabled, speakText } from "../../lib/tts.js";
|
|
17
|
+
import { readFirstTurnInput, readMultiLineInput, readNormalInput } from "./repl/input.js";
|
|
18
|
+
import { handleSlashCommand } from "./repl/slashCommands.js";
|
|
19
|
+
import { createToolHandlerState } from "./repl/toolHandlers.js";
|
|
20
|
+
import { runEngineLoop } from "./repl/engineLoop.js";
|
|
11
21
|
const { prompt } = pkg;
|
|
22
|
+
// ── Credit status display (also exported for engineLoop.ts) ───────────────────
|
|
12
23
|
export function printCreditStatus(remaining, limit = null) {
|
|
13
24
|
if (remaining === null)
|
|
14
25
|
return;
|
|
@@ -28,25 +39,72 @@ export function printCreditStatus(remaining, limit = null) {
|
|
|
28
39
|
}
|
|
29
40
|
}
|
|
30
41
|
}
|
|
42
|
+
// ── 401 error handler ─────────────────────────────────────────────────────────
|
|
43
|
+
async function handle401Error(selectedProvider, options) {
|
|
44
|
+
const LABELS = {
|
|
45
|
+
openai: "OpenAI", anthropic: "Anthropic",
|
|
46
|
+
gemini: "Gemini", openrouter: "OpenRouter", custom: "Custom",
|
|
47
|
+
};
|
|
48
|
+
const KEY_URLS = {
|
|
49
|
+
openai: "https://platform.openai.com/api-keys",
|
|
50
|
+
anthropic: "https://console.anthropic.com/settings/keys",
|
|
51
|
+
gemini: "https://aistudio.google.com/app/apikey",
|
|
52
|
+
openrouter: "https://openrouter.ai/settings/keys",
|
|
53
|
+
};
|
|
54
|
+
const { setProviderKey } = await import("../../config.js");
|
|
55
|
+
const label = LABELS[selectedProvider] ?? selectedProvider;
|
|
56
|
+
console.log();
|
|
57
|
+
console.log(chalk.red(`❌ API key rejected by ${label} (401 Unauthorized).`));
|
|
58
|
+
console.log(chalk.dim(" The saved key may be expired or invalid."));
|
|
59
|
+
if (KEY_URLS[selectedProvider]) {
|
|
60
|
+
console.log(chalk.dim(" Get a new key at: ") + chalk.cyan(KEY_URLS[selectedProvider]));
|
|
61
|
+
}
|
|
62
|
+
console.log();
|
|
63
|
+
try {
|
|
64
|
+
const answer = await prompt({
|
|
65
|
+
type: "select",
|
|
66
|
+
name: "action",
|
|
67
|
+
message: "What would you like to do?",
|
|
68
|
+
choices: [
|
|
69
|
+
{ name: "reset", message: `🔑 Enter a new ${label} API key` },
|
|
70
|
+
{ name: "continue", message: "⏭️ Continue anyway (will keep failing)" },
|
|
71
|
+
{ name: "exit", message: "🚪 Exit the agent" },
|
|
72
|
+
],
|
|
73
|
+
});
|
|
74
|
+
if (answer.action === "reset") {
|
|
75
|
+
const { key } = await prompt({
|
|
76
|
+
type: "password",
|
|
77
|
+
name: "key",
|
|
78
|
+
message: `Enter your new ${label} API key:`,
|
|
79
|
+
});
|
|
80
|
+
const newKey = (key ?? "").trim();
|
|
81
|
+
if (newKey) {
|
|
82
|
+
setProviderKey(selectedProvider, newKey);
|
|
83
|
+
options["api-key"] = newKey;
|
|
84
|
+
console.log(chalk.green(`\n✔ New ${label} key saved. Resuming session...\n`));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else if (answer.action === "exit") {
|
|
88
|
+
console.log(chalk.gray("\nGoodbye! 👋\n"));
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// User cancelled — just continue
|
|
94
|
+
}
|
|
95
|
+
return true; // re-prompt
|
|
96
|
+
}
|
|
97
|
+
// ── Main REPL loop ────────────────────────────────────────────────────────────
|
|
31
98
|
export async function askLoop(engine, options, selectedProvider, selectedModel, cvApiKey, systemInstruction, tools) {
|
|
32
99
|
let sessionTurns = 0;
|
|
33
100
|
let sessionLimit = null;
|
|
34
101
|
let currentModel = selectedModel;
|
|
35
|
-
|
|
36
|
-
const WRITE_TOOLS = new Set([
|
|
37
|
-
"tracker_add_job", "tracker_update_job", "kanban_add_job", "kanban_update_status",
|
|
38
|
-
"save_cover_letter", "delete_cover_letter", "write_file", "patch_file",
|
|
39
|
-
"tracker_recheck_urls", "openings_apply",
|
|
40
|
-
]);
|
|
41
|
-
const SESSION_MAX_MUTATIONS = 25;
|
|
42
|
-
const TURN_MAX_MUTATIONS = 10;
|
|
102
|
+
let currentEngine = engine;
|
|
43
103
|
let sessionMutations = 0;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
let lastToolCall = { name: "", argsHash: "", count: 0 };
|
|
104
|
+
const toolState = createToolHandlerState();
|
|
105
|
+
const byoHistory = [];
|
|
47
106
|
let pasteBuffer = [];
|
|
48
|
-
|
|
49
|
-
// ── SIGINT handler: Ctrl+C cancels current operation and returns to prompt ──
|
|
107
|
+
// ── SIGINT: Ctrl+C cancels current op, second exits ──────────────────────
|
|
50
108
|
let activeAbort = null;
|
|
51
109
|
const handleSigInt = () => {
|
|
52
110
|
const ab = activeAbort;
|
|
@@ -55,94 +113,27 @@ export async function askLoop(engine, options, selectedProvider, selectedModel,
|
|
|
55
113
|
process.stdout.write("\n" + chalk.yellow("⚡ Interrupted. Press Ctrl+C again or type 'exit' to quit.\n"));
|
|
56
114
|
}
|
|
57
115
|
else {
|
|
58
|
-
// Second Ctrl+C exits
|
|
59
116
|
console.log(chalk.gray("\nGoodbye! 👋\n"));
|
|
60
117
|
process.exit(0);
|
|
61
118
|
}
|
|
62
119
|
};
|
|
63
120
|
process.on("SIGINT", handleSigInt);
|
|
64
|
-
/** Wraps a promise with a timeout. Rejects with a friendly timeout error. */
|
|
65
|
-
function withTimeout(p, ms, label) {
|
|
66
|
-
return new Promise((resolve, reject) => {
|
|
67
|
-
const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms / 1000}s. Press Ctrl+C if stuck.`)), ms);
|
|
68
|
-
p.then(v => { clearTimeout(timer); resolve(v); })
|
|
69
|
-
.catch(e => { clearTimeout(timer); reject(e); });
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
// ── First-turn menu items ────────────────────────────────────────────────
|
|
73
|
-
const MENU_ITEMS = [
|
|
74
|
-
"📄 View or update my resume",
|
|
75
|
-
"🔍 Search for job opportunities",
|
|
76
|
-
"📊 Check my job pipeline / tracker",
|
|
77
|
-
"✉️ Draft a cover letter or tailor my resume",
|
|
78
|
-
"🎙 Start an AI mock interview (voice or text)",
|
|
79
|
-
"📈 Get an overview of my job search progress",
|
|
80
|
-
"🗓️ Pick up where we left off",
|
|
81
|
-
];
|
|
82
121
|
const ask = async (isFirstTurn = false) => {
|
|
83
122
|
try {
|
|
84
123
|
let userInput;
|
|
124
|
+
// ── Collect user input ──────────────────────────────────────────────
|
|
85
125
|
if (isFirstTurn) {
|
|
86
|
-
|
|
87
|
-
console.log(chalk.dim(" What would you like to do today?\n"));
|
|
88
|
-
for (const item of MENU_ITEMS) {
|
|
89
|
-
console.log(chalk.dim(` ${item}`));
|
|
90
|
-
}
|
|
91
|
-
console.log("");
|
|
92
|
-
const firstResp = await prompt({
|
|
93
|
-
type: "autocomplete",
|
|
94
|
-
name: "choice",
|
|
95
|
-
message: chalk.bold.hex("#6366f1")("❯") + chalk.dim(" ·"),
|
|
96
|
-
// @ts-ignore — enquirer autocomplete supports limit
|
|
97
|
-
limit: 7,
|
|
98
|
-
suggest(input, choices) {
|
|
99
|
-
if (!input)
|
|
100
|
-
return choices;
|
|
101
|
-
return choices.filter((c) => c.value.toLowerCase().includes(input.toLowerCase()));
|
|
102
|
-
},
|
|
103
|
-
choices: MENU_ITEMS.map(item => ({ name: item, value: item })),
|
|
104
|
-
footer: chalk.dim(" ↑↓ to navigate · type to filter · Enter to send"),
|
|
105
|
-
});
|
|
106
|
-
userInput = firstResp.choice?.trim() || "";
|
|
107
|
-
// Strip emoji prefixes so the agent gets clean text
|
|
108
|
-
userInput = userInput.replace(/^[\p{Emoji}\s]+/u, "").trim() || firstResp.choice?.trim() || "";
|
|
126
|
+
userInput = await readFirstTurnInput();
|
|
109
127
|
}
|
|
110
128
|
else {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const response = await prompt({
|
|
114
|
-
type: "input",
|
|
115
|
-
name: "query",
|
|
116
|
-
message: pasteBuffer.length > 0
|
|
117
|
-
? chalk.dim("... ")
|
|
118
|
-
: chalk.bold.hex("#6366f1")("❯") + chalk.dim(" ·"),
|
|
119
|
-
});
|
|
120
|
-
userInput = response.query;
|
|
121
|
-
const duration = Date.now() - promptStartTime;
|
|
122
|
-
// ── Multi-line paste mode ──────────────────────────────────────
|
|
129
|
+
const { text, isFastLine } = await readNormalInput(pasteBuffer.length > 0);
|
|
130
|
+
userInput = text;
|
|
123
131
|
if (userInput.trim() === "<<<" || userInput.trim().toLowerCase().startsWith("<<<")) {
|
|
124
132
|
const prefix = userInput.trim().slice(3).trim();
|
|
125
|
-
|
|
126
|
-
const lines = prefix ? [prefix] : [];
|
|
127
|
-
let emptyCount = 0;
|
|
128
|
-
while (emptyCount < 1) {
|
|
129
|
-
const lineResp = await prompt({
|
|
130
|
-
type: "input",
|
|
131
|
-
name: "line",
|
|
132
|
-
message: chalk.dim(" │"),
|
|
133
|
-
});
|
|
134
|
-
if (lineResp.line === "") {
|
|
135
|
-
emptyCount++;
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
emptyCount = 0;
|
|
139
|
-
lines.push(lineResp.line);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
userInput = lines.join("\n").trim();
|
|
133
|
+
userInput = await readMultiLineInput(prefix);
|
|
143
134
|
pasteBuffer = [];
|
|
144
135
|
}
|
|
145
|
-
else if (
|
|
136
|
+
else if (isFastLine && !userInput.startsWith("!") && !userInput.startsWith("/")) {
|
|
146
137
|
pasteBuffer.push(userInput);
|
|
147
138
|
return ask();
|
|
148
139
|
}
|
|
@@ -154,20 +145,19 @@ export async function askLoop(engine, options, selectedProvider, selectedModel,
|
|
|
154
145
|
pasteBuffer = [];
|
|
155
146
|
}
|
|
156
147
|
}
|
|
157
|
-
}
|
|
148
|
+
}
|
|
158
149
|
userInput = userInput.trim();
|
|
159
150
|
if (!userInput)
|
|
160
151
|
return ask();
|
|
161
|
-
// ── Input length guard
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
console.log(chalk.yellow("\n⚠️ Input is too long (" + userInput.length + " chars).") +
|
|
152
|
+
// ── Input length guard ─────────────────────────────────────────────
|
|
153
|
+
if (userInput.length > 20_000) {
|
|
154
|
+
console.log(chalk.yellow(`\n⚠️ Input is too long (${userInput.length} chars).`) +
|
|
165
155
|
chalk.dim("\n Use <<< mode for long job descriptions so nothing gets cut off:") +
|
|
166
156
|
chalk.cyan("\n\n ❯ <<< ") +
|
|
167
157
|
chalk.dim("\n Then paste the job description, and press Enter twice to submit.\n"));
|
|
168
158
|
return ask();
|
|
169
159
|
}
|
|
170
|
-
// ── Subshell escape:
|
|
160
|
+
// ── Subshell escape: !command ──────────────────────────────────────
|
|
171
161
|
if (userInput.startsWith("!")) {
|
|
172
162
|
const shellCmd = userInput.slice(1).trim();
|
|
173
163
|
if (shellCmd) {
|
|
@@ -176,140 +166,24 @@ export async function askLoop(engine, options, selectedProvider, selectedModel,
|
|
|
176
166
|
}
|
|
177
167
|
return ask();
|
|
178
168
|
}
|
|
179
|
-
// ── Slash commands
|
|
169
|
+
// ── Slash commands ─────────────────────────────────────────────────
|
|
180
170
|
if (userInput.startsWith("/")) {
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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):"));
|
|
194
|
-
console.log(chalk.dim(" <<< — Open multi-line paste mode; press Enter twice when done"));
|
|
195
|
-
console.log(chalk.dim(" <<<your text — Start with text directly after <<<\n"));
|
|
196
|
-
return ask();
|
|
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
|
-
}
|
|
269
|
-
if (cmd === "models") {
|
|
270
|
-
console.log(chalk.cyan("\n Available CareerVivid models:"));
|
|
271
|
-
for (const m of CV_MODELS) {
|
|
272
|
-
const active = m.value === currentModel ? chalk.green(" ← active") : "";
|
|
273
|
-
console.log(` ${m.name}${active}`);
|
|
274
|
-
}
|
|
275
|
-
console.log(chalk.dim("\n Usage: /model gemini-2.5-flash\n"));
|
|
276
|
-
return ask();
|
|
277
|
-
}
|
|
278
|
-
if (cmd === "model") {
|
|
279
|
-
if (!arg) {
|
|
280
|
-
console.log(chalk.yellow(`\n Current model: ${chalk.bold(currentModel)}`));
|
|
281
|
-
console.log(chalk.dim(" Usage: /model <name> e.g. /model gemini-3.1-pro-preview"));
|
|
282
|
-
console.log(chalk.dim(" Run /models to see all available options.\n"));
|
|
283
|
-
return ask();
|
|
284
|
-
}
|
|
285
|
-
const newModel = arg;
|
|
286
|
-
const known = CV_MODELS.find((m) => m.value === newModel);
|
|
287
|
-
if (!known && !newModel.includes("/") && !newModel.includes("-")) {
|
|
288
|
-
console.log(chalk.red(`\n Unknown model: ${newModel}`));
|
|
289
|
-
console.log(chalk.dim(" Run /models to see available options.\n"));
|
|
290
|
-
return ask();
|
|
291
|
-
}
|
|
292
|
-
currentModel = newModel;
|
|
293
|
-
if (cvApiKey && engine instanceof CareerVividProxyEngine) {
|
|
294
|
-
engine = new CareerVividProxyEngine({
|
|
295
|
-
cvApiKey,
|
|
296
|
-
model: currentModel,
|
|
297
|
-
systemInstruction,
|
|
298
|
-
tools,
|
|
299
|
-
thinkingBudget: newModel.includes("pro") ? (options.think ?? 8192) : 0,
|
|
300
|
-
maxHistoryLength: 40,
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
const creditInfo = known ? chalk.dim(` (${known.cost} credit/turn)`) : "";
|
|
304
|
-
console.log(chalk.green(`\n ✔ Switched to ${chalk.bold(currentModel)}${creditInfo}`));
|
|
305
|
-
console.log(chalk.dim(" Conversation history has been reset.\n"));
|
|
306
|
-
return ask();
|
|
171
|
+
const result = await handleSlashCommand(userInput, currentModel, {
|
|
172
|
+
cvApiKey,
|
|
173
|
+
engine: currentEngine,
|
|
174
|
+
systemInstruction,
|
|
175
|
+
tools,
|
|
176
|
+
options,
|
|
177
|
+
});
|
|
178
|
+
if (result?.modelSwitch) {
|
|
179
|
+
currentModel = result.modelSwitch.newModel;
|
|
180
|
+
currentEngine = result.modelSwitch.newEngine;
|
|
307
181
|
}
|
|
308
|
-
console.log(chalk.yellow(`\n Unknown command: /${cmd}. Type /help for available commands.\n`));
|
|
309
182
|
return ask();
|
|
310
183
|
}
|
|
184
|
+
// ── Exit ───────────────────────────────────────────────────────────
|
|
311
185
|
if (userInput.toLowerCase() === "exit") {
|
|
312
|
-
const proxyEngine =
|
|
186
|
+
const proxyEngine = currentEngine instanceof CareerVividProxyEngine ? currentEngine : null;
|
|
313
187
|
if (proxyEngine && sessionTurns > 0) {
|
|
314
188
|
console.log(chalk.dim("\n─────────────────────────────────────────"));
|
|
315
189
|
console.log(chalk.dim(`Session: ${sessionTurns} turn${sessionTurns !== 1 ? "s" : ""} · `) +
|
|
@@ -323,402 +197,52 @@ export async function askLoop(engine, options, selectedProvider, selectedModel,
|
|
|
323
197
|
console.log(chalk.gray("\nGoodbye! 👋\n"));
|
|
324
198
|
process.exit(0);
|
|
325
199
|
}
|
|
326
|
-
//
|
|
327
|
-
|
|
328
|
-
|
|
200
|
+
// ── Run the agent turn ─────────────────────────────────────────────
|
|
201
|
+
sessionTurns++;
|
|
202
|
+
toolState.turnMutations = 0; // reset per-turn counter
|
|
329
203
|
process.stdout.write(chalk.dim("\n"));
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
save_cover_letter: "💾 Saving cover letter...",
|
|
355
|
-
delete_cover_letter: "🗑️ Deleting cover letter...",
|
|
356
|
-
browser_navigate: "🌐 Navigating to page...",
|
|
357
|
-
browser_click: "🖱️ Clicking element...",
|
|
358
|
-
browser_type: "⌨️ Typing input...",
|
|
359
|
-
browser_state: "🌐 Reading browser state...",
|
|
360
|
-
browser_screenshot: "📸 Taking screenshot...",
|
|
361
|
-
browser_scroll: "📜 Scrolling page...",
|
|
362
|
-
browser_wait: "⏳ Waiting...",
|
|
363
|
-
browser_close: "🔒 Closing browser...",
|
|
364
|
-
browser_select: "🖱️ Selecting option...",
|
|
365
|
-
tracker_recheck_urls: "🔗 Re-checking job URLs...",
|
|
366
|
-
browser_autofill_application: "📝 Auto-filling application...",
|
|
367
|
-
verify_url: "🔍 Verifying URL...",
|
|
368
|
-
verify_job_urls: "🔍 Verifying job URLs...",
|
|
369
|
-
search_jobs: "🔍 Searching jobs...",
|
|
370
|
-
openings_scan: "🎯 Scanning companies for open roles...",
|
|
371
|
-
openings_list: "📋 Loading saved openings...",
|
|
372
|
-
openings_apply: "✅ Marking opening as applied...",
|
|
373
|
-
get_resume: "📄 Loading resume...",
|
|
374
|
-
list_resumes: "📄 Loading resumes...",
|
|
375
|
-
get_profile: "👤 Loading profile...",
|
|
376
|
-
};
|
|
377
|
-
const handleToolCall = async (name, args) => {
|
|
378
|
-
// Stop the thinking spinner the moment we start a tool — prevents duplication
|
|
379
|
-
if (thinkingSpinner.isSpinning) {
|
|
380
|
-
thinkingSpinner.stop();
|
|
381
|
-
process.stdout.write("\r\x1b[K"); // clear spinner line
|
|
204
|
+
const response = await runEngineLoop({
|
|
205
|
+
engine: currentEngine,
|
|
206
|
+
userInput,
|
|
207
|
+
selectedProvider,
|
|
208
|
+
selectedModel,
|
|
209
|
+
currentModel,
|
|
210
|
+
byoHistory,
|
|
211
|
+
tools,
|
|
212
|
+
systemInstruction,
|
|
213
|
+
verbose: Boolean(options.verbose),
|
|
214
|
+
sessionTurns,
|
|
215
|
+
apiKey: options["api-key"] || options.apiKey,
|
|
216
|
+
baseUrl: options["base-url"] || options.baseUrl,
|
|
217
|
+
handleSigInt,
|
|
218
|
+
toolState,
|
|
219
|
+
onCreditInfo: (remaining, limit) => {
|
|
220
|
+
sessionLimit = limit;
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
// ── TTS: store + auto-speak ────────────────────────────────────────
|
|
224
|
+
if (response) {
|
|
225
|
+
setLastResponse(response);
|
|
226
|
+
if (isVoiceEnabled()) {
|
|
227
|
+
speakText(response).catch(() => { });
|
|
382
228
|
}
|
|
383
|
-
// #9 Circuit breaker: abort if same tool called 5+ times consecutively with same args
|
|
384
|
-
const argsHash = JSON.stringify(args).slice(0, 100);
|
|
385
|
-
if (lastToolCall.name === name && lastToolCall.argsHash === argsHash) {
|
|
386
|
-
lastToolCall.count++;
|
|
387
|
-
if (lastToolCall.count >= 5) {
|
|
388
|
-
console.log(chalk.red(`\n⛔ Loop detected: "${name}" called ${lastToolCall.count} times with identical args. Aborting turn.`));
|
|
389
|
-
return false;
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
else {
|
|
393
|
-
lastToolCall = { name, argsHash, count: 1 };
|
|
394
|
-
}
|
|
395
|
-
// #3 Per-turn mutation budget
|
|
396
|
-
if (WRITE_TOOLS.has(name)) {
|
|
397
|
-
turnMutations++;
|
|
398
|
-
if (turnMutations > TURN_MAX_MUTATIONS) {
|
|
399
|
-
console.log(chalk.red(`\n⛔ Turn mutation limit (${TURN_MAX_MUTATIONS}) reached. The agent has made ${turnMutations} writes this turn.`));
|
|
400
|
-
return false;
|
|
401
|
-
}
|
|
402
|
-
sessionMutations++;
|
|
403
|
-
if (sessionMutations >= SESSION_MAX_MUTATIONS) {
|
|
404
|
-
console.log(chalk.yellow(`\n⚠️ Session mutation budget exhausted (${SESSION_MAX_MUTATIONS} writes). Restart the agent to continue writing.`));
|
|
405
|
-
return false;
|
|
406
|
-
}
|
|
407
|
-
else if (sessionMutations === SESSION_MAX_MUTATIONS - 5) {
|
|
408
|
-
console.log(chalk.yellow(`\n💡 Heads up: ${SESSION_MAX_MUTATIONS - sessionMutations} writes remaining this session.`));
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
// Print compact tool label — no blank lines, stays tight between steps
|
|
412
|
-
const label = TOOL_LABELS[name] ?? `⚙️ Working...`;
|
|
413
|
-
process.stdout.write(chalk.dim(` ${label}\n`));
|
|
414
|
-
if (name === "run_command") {
|
|
415
|
-
if (trustAllCommands || isSafeCommand(args.command)) {
|
|
416
|
-
return true;
|
|
417
|
-
}
|
|
418
|
-
const confirm = await prompt({
|
|
419
|
-
type: "select",
|
|
420
|
-
name: "ok",
|
|
421
|
-
message: `Allow running: ${chalk.bold(args.command)}?`,
|
|
422
|
-
choices: [
|
|
423
|
-
"Yes, run it",
|
|
424
|
-
"Yes, and trust all commands this session",
|
|
425
|
-
"No, skip it",
|
|
426
|
-
],
|
|
427
|
-
});
|
|
428
|
-
if (confirm.ok === "Yes, and trust all commands this session") {
|
|
429
|
-
trustAllCommands = true;
|
|
430
|
-
console.log(chalk.dim(" ✅ All commands will run automatically for the rest of this session."));
|
|
431
|
-
return true;
|
|
432
|
-
}
|
|
433
|
-
return confirm.ok === "Yes, run it";
|
|
434
|
-
}
|
|
435
|
-
if (name === "write_file" || name === "patch_file") {
|
|
436
|
-
if (trustAllWrites)
|
|
437
|
-
return true;
|
|
438
|
-
const target = args.path || "(unknown path)";
|
|
439
|
-
const confirm = await prompt({
|
|
440
|
-
type: "select",
|
|
441
|
-
name: "ok",
|
|
442
|
-
message: `Allow writing to: ${chalk.bold(target)}?`,
|
|
443
|
-
choices: [
|
|
444
|
-
"Yes, write it",
|
|
445
|
-
"Yes, and trust all writes this session",
|
|
446
|
-
"No, skip it",
|
|
447
|
-
],
|
|
448
|
-
});
|
|
449
|
-
if (confirm.ok === "Yes, and trust all writes this session") {
|
|
450
|
-
trustAllWrites = true;
|
|
451
|
-
console.log(chalk.dim(" ✅ All file writes will run automatically for the rest of this session."));
|
|
452
|
-
return true;
|
|
453
|
-
}
|
|
454
|
-
if (confirm.ok !== "Yes, write it")
|
|
455
|
-
return false;
|
|
456
|
-
}
|
|
457
|
-
if (["browser_state", "browser_screenshot", "browser_scroll", "browser_wait"].includes(name)) {
|
|
458
|
-
currentSpinner = ora(`Running ${chalk.bold(name)}...`).start();
|
|
459
|
-
return true;
|
|
460
|
-
}
|
|
461
|
-
if (["browser_navigate", "browser_click", "browser_type", "browser_select"].includes(name)) {
|
|
462
|
-
currentSpinner = ora(`Running ${chalk.bold(name)}...`).start();
|
|
463
|
-
return true;
|
|
464
|
-
}
|
|
465
|
-
if (name === "browser_close") {
|
|
466
|
-
const confirm = await prompt({
|
|
467
|
-
type: "select",
|
|
468
|
-
name: "ok",
|
|
469
|
-
message: `Close the browser?`,
|
|
470
|
-
choices: ["Yes, close it", "No, keep it open"],
|
|
471
|
-
});
|
|
472
|
-
if (confirm.ok !== "Yes, close it")
|
|
473
|
-
return false;
|
|
474
|
-
}
|
|
475
|
-
// Tools that take over the full terminal must NOT have a spinner running —
|
|
476
|
-
// the concurrent ora redraw causes constant flashing.
|
|
477
|
-
if (name === "start_interview") {
|
|
478
|
-
// Clear current line so any previous UI is gone, then yield terminal cleanly.
|
|
479
|
-
process.stdout.write("\r\x1b[K");
|
|
480
|
-
return true;
|
|
481
|
-
}
|
|
482
|
-
currentSpinner = ora(`Running ${chalk.bold(name)}...`).start();
|
|
483
|
-
return true;
|
|
484
|
-
};
|
|
485
|
-
const handleToolResult = (name, result) => {
|
|
486
|
-
if (currentSpinner) {
|
|
487
|
-
currentSpinner.succeed(chalk.dim("Done"));
|
|
488
|
-
currentSpinner = null;
|
|
489
|
-
}
|
|
490
|
-
if (name === "start_interview") {
|
|
491
|
-
// Interview already printed its own output — just add a separator.
|
|
492
|
-
console.log(chalk.dim("─".repeat(50)));
|
|
493
|
-
}
|
|
494
|
-
// #4 Audit log — record every completed tool call
|
|
495
|
-
// durationMs is approximate since we don't have exact start time here
|
|
496
|
-
auditLog({
|
|
497
|
-
sessionId: SESSION_ID,
|
|
498
|
-
tool: name,
|
|
499
|
-
args: typeof result?._args === "object" ? result._args : {},
|
|
500
|
-
result: typeof result === "string" ? result : JSON.stringify(result ?? ""),
|
|
501
|
-
durationMs: 0, // QueryEngine doesn't expose timing; repl.ts timing TBD
|
|
502
|
-
});
|
|
503
|
-
// Suppress raw output — the agent will summarize it in natural language
|
|
504
|
-
};
|
|
505
|
-
if (engine) {
|
|
506
|
-
sessionTurns++;
|
|
507
|
-
let responseAccumulator = "";
|
|
508
|
-
const sharedOnChunk = (text) => {
|
|
509
|
-
if (firstChunk) {
|
|
510
|
-
thinkingSpinner.stop();
|
|
511
|
-
// Print a subtle gutter marker so AI response is visually distinct
|
|
512
|
-
process.stdout.write("\n" + chalk.hex("#6366f1")("✦ "));
|
|
513
|
-
firstChunk = false;
|
|
514
|
-
}
|
|
515
|
-
process.stdout.write(text);
|
|
516
|
-
responseAccumulator += text; // Accumulate for TTS
|
|
517
|
-
};
|
|
518
|
-
const sharedOnError = (error) => {
|
|
519
|
-
thinkingSpinner.stop();
|
|
520
|
-
if (currentSpinner) {
|
|
521
|
-
currentSpinner.fail("Tool error");
|
|
522
|
-
currentSpinner = null;
|
|
523
|
-
}
|
|
524
|
-
console.log(chalk.red(`\n❌ Error: ${error.message}\n`));
|
|
525
|
-
};
|
|
526
|
-
if (engine instanceof CareerVividProxyEngine) {
|
|
527
|
-
await engine.runLoopStreaming(userInput, {
|
|
528
|
-
onChunk: sharedOnChunk,
|
|
529
|
-
onThinking: (thought) => {
|
|
530
|
-
if (options.verbose) {
|
|
531
|
-
console.log(chalk.dim(`\n[thinking] ${thought.substring(0, 200)}...`));
|
|
532
|
-
}
|
|
533
|
-
},
|
|
534
|
-
onToolCall: handleToolCall,
|
|
535
|
-
onToolResult: handleToolResult,
|
|
536
|
-
onCompacting: () => {
|
|
537
|
-
console.log(chalk.dim("\n📦 Compacting context window...\n"));
|
|
538
|
-
},
|
|
539
|
-
onError: sharedOnError,
|
|
540
|
-
onResponse: async (creditInfo) => {
|
|
541
|
-
sessionLimit = creditInfo.monthlyLimit;
|
|
542
|
-
printCreditStatus(creditInfo.creditsRemaining, sessionLimit);
|
|
543
|
-
},
|
|
544
|
-
onCreditLimitReached: (remaining) => {
|
|
545
|
-
console.log(chalk.red("\n\n⚠️ Credit limit reached (" + remaining + " remaining).\n" +
|
|
546
|
-
chalk.dim(" Upgrade or top up at ") +
|
|
547
|
-
chalk.underline.blue("careervivid.app/developer")));
|
|
548
|
-
},
|
|
549
|
-
});
|
|
550
|
-
}
|
|
551
|
-
else {
|
|
552
|
-
await engine.runLoopStreaming(userInput, {
|
|
553
|
-
onChunk: sharedOnChunk,
|
|
554
|
-
onThinking: (thought) => {
|
|
555
|
-
if (options.verbose) {
|
|
556
|
-
console.log(chalk.dim(`\n[thinking] ${thought.substring(0, 200)}...`));
|
|
557
|
-
}
|
|
558
|
-
},
|
|
559
|
-
onToolCall: handleToolCall,
|
|
560
|
-
onToolResult: handleToolResult,
|
|
561
|
-
onCompacting: () => {
|
|
562
|
-
console.log(chalk.dim("\n📦 Compacting context window...\n"));
|
|
563
|
-
},
|
|
564
|
-
onError: sharedOnError,
|
|
565
|
-
});
|
|
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
|
-
}
|
|
574
|
-
// ── Clean turn separator after every AI reply ─────────────────────────────
|
|
575
|
-
process.stdout.write("\n" + chalk.dim("─".repeat(48)) + "\n");
|
|
576
|
-
}
|
|
577
|
-
else {
|
|
578
|
-
sessionTurns++;
|
|
579
|
-
const { createOpenAICompatibleProvider } = await import("../../agent/providers/OpenAIProvider.js");
|
|
580
|
-
const { AnthropicProvider } = await import("../../agent/providers/AnthropicProvider.js");
|
|
581
|
-
const byoApiKey = options["api-key"] || getProviderKey(selectedProvider) || loadConfig().llmApiKey;
|
|
582
|
-
const key = byoApiKey || "";
|
|
583
|
-
const baseUrl = options["base-url"] || loadConfig().llmBaseUrl;
|
|
584
|
-
let provider;
|
|
585
|
-
if (selectedProvider === "anthropic") {
|
|
586
|
-
provider = new AnthropicProvider({ apiKey: key });
|
|
587
|
-
}
|
|
588
|
-
else {
|
|
589
|
-
const subProvider = (selectedProvider === "openrouter" ? "openrouter" :
|
|
590
|
-
selectedProvider === "custom" ? "custom" : "openai");
|
|
591
|
-
provider = createOpenAICompatibleProvider(subProvider, key, baseUrl);
|
|
592
|
-
}
|
|
593
|
-
let userTurn = { role: "user", parts: [{ text: userInput }] };
|
|
594
|
-
let round = 0;
|
|
595
|
-
while (round < 10) {
|
|
596
|
-
const result = await withTimeout(provider.generate({ model: currentModel, history: byoHistory, userTurn, tools, systemInstruction }), 45_000, "LLM generate()");
|
|
597
|
-
if (round === 0) {
|
|
598
|
-
thinkingSpinner.stop();
|
|
599
|
-
process.stdout.write("\n" + chalk.hex("#6366f1")("\u2726 "));
|
|
600
|
-
}
|
|
601
|
-
if (result.text) {
|
|
602
|
-
process.stdout.write(result.text);
|
|
603
|
-
}
|
|
604
|
-
byoHistory.push(userTurn);
|
|
605
|
-
byoHistory.push({ role: "model", parts: result.rawParts || [{ text: result.text }] });
|
|
606
|
-
if (!result.functionCalls || result.functionCalls.length === 0) {
|
|
607
|
-
break;
|
|
608
|
-
}
|
|
609
|
-
let fnResponses = [];
|
|
610
|
-
for (const fc of result.functionCalls) {
|
|
611
|
-
const allow = await handleToolCall(fc.name, fc.args);
|
|
612
|
-
if (!allow) {
|
|
613
|
-
fnResponses.push({ functionResponse: { id: fc.id, name: fc.name, response: { error: "User denied execution." } } });
|
|
614
|
-
continue;
|
|
615
|
-
}
|
|
616
|
-
const tool = tools.find((t) => t.name === fc.name);
|
|
617
|
-
let out;
|
|
618
|
-
try {
|
|
619
|
-
// start_interview is an interactive long-running session — never apply a timeout to it.
|
|
620
|
-
// Also temporarily remove the REPL's SIGINT handler so the interview's own
|
|
621
|
-
// Ctrl+C handler can run cleanly (generate report) without racing against
|
|
622
|
-
// the REPL's "Goodbye! 👋" / process.exit path.
|
|
623
|
-
if (fc.name === "start_interview") {
|
|
624
|
-
process.removeListener("SIGINT", handleSigInt);
|
|
625
|
-
try {
|
|
626
|
-
out = tool ? await tool.execute(fc.args) : { error: "Tool not found" };
|
|
627
|
-
}
|
|
628
|
-
finally {
|
|
629
|
-
process.on("SIGINT", handleSigInt); // always restore, even on throw
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
else {
|
|
633
|
-
out = tool
|
|
634
|
-
? await withTimeout(tool.execute(fc.args), 45_000, `tool:${fc.name}`)
|
|
635
|
-
: { error: "Tool not found" };
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
catch (e) {
|
|
639
|
-
if (e.message?.includes("No API key configured")) {
|
|
640
|
-
out = { error: "CareerVivid API key not found. Run 'cv login' to authenticate." };
|
|
641
|
-
}
|
|
642
|
-
else {
|
|
643
|
-
out = { error: e.message };
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
handleToolResult(fc.name, out);
|
|
647
|
-
fnResponses.push({ functionResponse: { id: fc.id, name: fc.name, response: out } });
|
|
648
|
-
}
|
|
649
|
-
userTurn = { role: "user", parts: fnResponses };
|
|
650
|
-
round++;
|
|
651
|
-
}
|
|
652
|
-
// ── Clean turn separator after every AI reply ─────────────────────────────
|
|
653
|
-
process.stdout.write("\n" + chalk.dim("─".repeat(48)) + "\n");
|
|
654
229
|
}
|
|
655
230
|
return ask();
|
|
656
231
|
}
|
|
657
232
|
catch (err) {
|
|
658
233
|
const msg = err?.message ?? "";
|
|
659
|
-
// ── Clean exit on Ctrl+C / enquirer cancel ────────────────────────
|
|
660
234
|
if (!msg || msg.includes("cancelled") || msg.includes("User force closed")) {
|
|
661
235
|
console.log(chalk.gray("\nCancelled. Exiting.\n"));
|
|
662
236
|
process.exit(0);
|
|
663
237
|
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
msg.toLowerCase().includes("invalid api key") ||
|
|
238
|
+
const is401 = msg.includes("401") ||
|
|
239
|
+
msg.toLowerCase().includes("user not found") ||
|
|
240
|
+
msg.toLowerCase().includes("invalid api key") ||
|
|
241
|
+
msg.toLowerCase().includes("unauthorized");
|
|
667
242
|
if (is401 && selectedProvider && selectedProvider !== "careervivid") {
|
|
668
|
-
|
|
669
|
-
openai: "OpenAI", anthropic: "Anthropic",
|
|
670
|
-
gemini: "Gemini", openrouter: "OpenRouter", custom: "Custom",
|
|
671
|
-
};
|
|
672
|
-
const providerKeyUrls = {
|
|
673
|
-
openai: "https://platform.openai.com/api-keys",
|
|
674
|
-
anthropic: "https://console.anthropic.com/settings/keys",
|
|
675
|
-
gemini: "https://aistudio.google.com/app/apikey",
|
|
676
|
-
openrouter: "https://openrouter.ai/settings/keys",
|
|
677
|
-
};
|
|
678
|
-
const label = providerLabels[selectedProvider] ?? selectedProvider;
|
|
679
|
-
console.log();
|
|
680
|
-
console.log(chalk.red(`❌ API key rejected by ${label} (401 Unauthorized).`));
|
|
681
|
-
console.log(chalk.dim(` The saved key may be expired or invalid.`));
|
|
682
|
-
if (providerKeyUrls[selectedProvider]) {
|
|
683
|
-
console.log(chalk.dim(` Get a new key at: `) + chalk.cyan(providerKeyUrls[selectedProvider]));
|
|
684
|
-
}
|
|
685
|
-
console.log();
|
|
686
|
-
try {
|
|
687
|
-
const resetAnswer = await prompt({
|
|
688
|
-
type: "select",
|
|
689
|
-
name: "action",
|
|
690
|
-
message: "What would you like to do?",
|
|
691
|
-
choices: [
|
|
692
|
-
{ name: "reset", message: `🔑 Enter a new ${label} API key` },
|
|
693
|
-
{ name: "continue", message: "⏭️ Continue anyway (will keep failing)" },
|
|
694
|
-
{ name: "exit", message: "🚪 Exit the agent" },
|
|
695
|
-
],
|
|
696
|
-
});
|
|
697
|
-
if (resetAnswer.action === "reset") {
|
|
698
|
-
const keyAnswer = await prompt({
|
|
699
|
-
type: "password",
|
|
700
|
-
name: "key",
|
|
701
|
-
message: `Enter your new ${label} API key:`,
|
|
702
|
-
});
|
|
703
|
-
const newKey = (keyAnswer?.key ?? "").trim();
|
|
704
|
-
if (newKey) {
|
|
705
|
-
setProviderKey(selectedProvider, newKey);
|
|
706
|
-
// Update the key used for subsequent turns this session
|
|
707
|
-
options["api-key"] = newKey;
|
|
708
|
-
console.log(chalk.green(`\n✔ New ${label} key saved. Resuming session...\n`));
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
else if (resetAnswer.action === "exit") {
|
|
712
|
-
console.log(chalk.gray("\nGoodbye! 👋\n"));
|
|
713
|
-
process.exit(0);
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
catch {
|
|
717
|
-
// User cancelled the reset prompt — just continue
|
|
718
|
-
}
|
|
243
|
+
await handle401Error(selectedProvider, options);
|
|
719
244
|
return ask();
|
|
720
245
|
}
|
|
721
|
-
// ── Generic error ────────────────────────────────────────────────
|
|
722
246
|
console.error(chalk.red(`\nAgent encountered an error: ${msg}`));
|
|
723
247
|
return ask();
|
|
724
248
|
}
|