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.
- 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 -635
- 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,95 +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 (
|
|
146
|
-
// Only buffer lines that are NOT commands — fixes ! and / getting swallowed by paste detection
|
|
136
|
+
else if (isFastLine && !userInput.startsWith("!") && !userInput.startsWith("/")) {
|
|
147
137
|
pasteBuffer.push(userInput);
|
|
148
138
|
return ask();
|
|
149
139
|
}
|
|
@@ -155,20 +145,19 @@ export async function askLoop(engine, options, selectedProvider, selectedModel,
|
|
|
155
145
|
pasteBuffer = [];
|
|
156
146
|
}
|
|
157
147
|
}
|
|
158
|
-
}
|
|
148
|
+
}
|
|
159
149
|
userInput = userInput.trim();
|
|
160
150
|
if (!userInput)
|
|
161
151
|
return ask();
|
|
162
|
-
// ── Input length guard
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
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).`) +
|
|
166
155
|
chalk.dim("\n Use <<< mode for long job descriptions so nothing gets cut off:") +
|
|
167
156
|
chalk.cyan("\n\n ❯ <<< ") +
|
|
168
157
|
chalk.dim("\n Then paste the job description, and press Enter twice to submit.\n"));
|
|
169
158
|
return ask();
|
|
170
159
|
}
|
|
171
|
-
// ── Subshell escape:
|
|
160
|
+
// ── Subshell escape: !command ──────────────────────────────────────
|
|
172
161
|
if (userInput.startsWith("!")) {
|
|
173
162
|
const shellCmd = userInput.slice(1).trim();
|
|
174
163
|
if (shellCmd) {
|
|
@@ -177,165 +166,24 @@ export async function askLoop(engine, options, selectedProvider, selectedModel,
|
|
|
177
166
|
}
|
|
178
167
|
return ask();
|
|
179
168
|
}
|
|
180
|
-
// ── Slash commands
|
|
169
|
+
// ── Slash commands ─────────────────────────────────────────────────
|
|
181
170
|
if (userInput.startsWith("/")) {
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
console.log(chalk.cyan("\n Shell escape (run terminal commands without leaving the agent):"));
|
|
193
|
-
console.log(chalk.dim(" !<command> — e.g. !ls -la or !git status\n"));
|
|
194
|
-
console.log(chalk.cyan(" Paste long content (job descriptions, cover letters):"));
|
|
195
|
-
console.log(chalk.dim(" <<< — Open multi-line paste mode; press Enter twice when done"));
|
|
196
|
-
console.log(chalk.dim(" <<<your text — Start with text directly after <<<\n"));
|
|
197
|
-
return ask();
|
|
198
|
-
}
|
|
199
|
-
if (cmd === "voice") {
|
|
200
|
-
// ── Interactive /voice menu using enquirer selects ──────────────────────────
|
|
201
|
-
if (!arg) {
|
|
202
|
-
// Top-level voice menu
|
|
203
|
-
const status = isVoiceEnabled() ? "on" : "off";
|
|
204
|
-
const topChoice = await prompt({
|
|
205
|
-
type: "select",
|
|
206
|
-
name: "action",
|
|
207
|
-
message: `Voice settings (${chalk.dim(`voice: ${getCurrentVoice()} model: ${getCurrentTtsModel()}`)})`,
|
|
208
|
-
choices: [
|
|
209
|
-
{ name: "toggle", message: `${isVoiceEnabled() ? "🔇 Turn voice off" : "🔊 Turn voice on"}` },
|
|
210
|
-
{ name: "set-voice", message: "🎵 Pick a voice" },
|
|
211
|
-
{ name: "set-model", message: "⚙️ Pick a TTS model" },
|
|
212
|
-
{ name: "speak", message: "▶️ Replay last response" },
|
|
213
|
-
{ name: "cancel", message: chalk.dim("Cancel") },
|
|
214
|
-
],
|
|
215
|
-
});
|
|
216
|
-
if (topChoice.action === "toggle") {
|
|
217
|
-
const newState = !isVoiceEnabled();
|
|
218
|
-
setVoiceEnabled(newState);
|
|
219
|
-
if (!newState)
|
|
220
|
-
stopPlayback();
|
|
221
|
-
console.log(newState
|
|
222
|
-
? chalk.green(`\n 🔊 Voice on (${getCurrentVoice()} · ${getCurrentTtsModel()})\n`)
|
|
223
|
-
: chalk.yellow("\n 🔇 Voice off\n"));
|
|
224
|
-
}
|
|
225
|
-
else if (topChoice.action === "set-voice") {
|
|
226
|
-
const voiceChoice = await prompt({
|
|
227
|
-
type: "select",
|
|
228
|
-
name: "voice",
|
|
229
|
-
message: "Choose a voice:",
|
|
230
|
-
choices: AVAILABLE_VOICES.map(v => ({
|
|
231
|
-
name: v,
|
|
232
|
-
message: v === getCurrentVoice() ? chalk.green(`${v} ← active`) : v,
|
|
233
|
-
})),
|
|
234
|
-
});
|
|
235
|
-
setCurrentVoice(voiceChoice.voice);
|
|
236
|
-
console.log(chalk.green(`\n 🎵 Voice set to ${chalk.bold(voiceChoice.voice)}\n`));
|
|
237
|
-
}
|
|
238
|
-
else if (topChoice.action === "set-model") {
|
|
239
|
-
const modelLabels = {
|
|
240
|
-
"gemini-3.1-flash-preview-tts": "Gemini 3.1 Flash (latest, fast)",
|
|
241
|
-
"gemini-3.1-pro-preview-tts": "Gemini 3.1 Pro (latest, highest quality)",
|
|
242
|
-
"gemini-2.5-flash-preview-tts": "Gemini 2.5 Flash (previous gen, fast)",
|
|
243
|
-
"gemini-2.5-pro-preview-tts": "Gemini 2.5 Pro (previous gen, high quality)",
|
|
244
|
-
};
|
|
245
|
-
const modelChoice = await prompt({
|
|
246
|
-
type: "select",
|
|
247
|
-
name: "model",
|
|
248
|
-
message: "Choose a TTS model:",
|
|
249
|
-
choices: AVAILABLE_TTS_MODELS.map(m => ({
|
|
250
|
-
name: m,
|
|
251
|
-
message: m === getCurrentTtsModel()
|
|
252
|
-
? chalk.green(`${modelLabels[m] ?? m} ← active`)
|
|
253
|
-
: (modelLabels[m] ?? m),
|
|
254
|
-
})),
|
|
255
|
-
});
|
|
256
|
-
setCurrentTtsModel(modelChoice.model);
|
|
257
|
-
console.log(chalk.green(`\n ⚙️ TTS model set to ${chalk.bold(modelChoice.model)}\n`));
|
|
258
|
-
}
|
|
259
|
-
else if (topChoice.action === "speak") {
|
|
260
|
-
const last = getLastResponse();
|
|
261
|
-
if (!last) {
|
|
262
|
-
console.log(chalk.dim("\n Nothing to speak yet.\n"));
|
|
263
|
-
}
|
|
264
|
-
else {
|
|
265
|
-
speakText(last).catch(() => { });
|
|
266
|
-
console.log(chalk.dim("\n 🔊 Speaking...\n"));
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
else {
|
|
271
|
-
// Still support direct text subcommands for scripting: /voice on, /voice off
|
|
272
|
-
if (arg === "on") {
|
|
273
|
-
setVoiceEnabled(true);
|
|
274
|
-
console.log(chalk.green(`\n 🔊 Voice on (${getCurrentVoice()} · ${getCurrentTtsModel()})\n`));
|
|
275
|
-
}
|
|
276
|
-
if (arg === "off") {
|
|
277
|
-
setVoiceEnabled(false);
|
|
278
|
-
stopPlayback();
|
|
279
|
-
console.log(chalk.yellow("\n 🔇 Voice off\n"));
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
return ask();
|
|
283
|
-
}
|
|
284
|
-
if (cmd === "speak") {
|
|
285
|
-
const last = getLastResponse();
|
|
286
|
-
if (!last) {
|
|
287
|
-
console.log(chalk.dim("\n Nothing to speak yet. Ask the agent something first.\n"));
|
|
288
|
-
}
|
|
289
|
-
else {
|
|
290
|
-
speakText(last).catch(() => { });
|
|
291
|
-
console.log(chalk.dim("\n 🔊 Speaking last response...\n"));
|
|
292
|
-
}
|
|
293
|
-
return ask();
|
|
294
|
-
}
|
|
295
|
-
if (cmd === "models") {
|
|
296
|
-
console.log(chalk.cyan("\n Available CareerVivid models:"));
|
|
297
|
-
for (const m of CV_MODELS) {
|
|
298
|
-
const active = m.value === currentModel ? chalk.green(" ← active") : "";
|
|
299
|
-
console.log(` ${m.name}${active}`);
|
|
300
|
-
}
|
|
301
|
-
console.log(chalk.dim("\n Usage: /model gemini-2.5-flash\n"));
|
|
302
|
-
return ask();
|
|
303
|
-
}
|
|
304
|
-
if (cmd === "model") {
|
|
305
|
-
if (!arg) {
|
|
306
|
-
console.log(chalk.yellow(`\n Current model: ${chalk.bold(currentModel)}`));
|
|
307
|
-
console.log(chalk.dim(" Usage: /model <name> e.g. /model gemini-3.1-pro-preview"));
|
|
308
|
-
console.log(chalk.dim(" Run /models to see all available options.\n"));
|
|
309
|
-
return ask();
|
|
310
|
-
}
|
|
311
|
-
const newModel = arg;
|
|
312
|
-
const known = CV_MODELS.find((m) => m.value === newModel);
|
|
313
|
-
if (!known && !newModel.includes("/") && !newModel.includes("-")) {
|
|
314
|
-
console.log(chalk.red(`\n Unknown model: ${newModel}`));
|
|
315
|
-
console.log(chalk.dim(" Run /models to see available options.\n"));
|
|
316
|
-
return ask();
|
|
317
|
-
}
|
|
318
|
-
currentModel = newModel;
|
|
319
|
-
if (cvApiKey && engine instanceof CareerVividProxyEngine) {
|
|
320
|
-
engine = new CareerVividProxyEngine({
|
|
321
|
-
cvApiKey,
|
|
322
|
-
model: currentModel,
|
|
323
|
-
systemInstruction,
|
|
324
|
-
tools,
|
|
325
|
-
thinkingBudget: newModel.includes("pro") ? (options.think ?? 8192) : 0,
|
|
326
|
-
maxHistoryLength: 40,
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
const creditInfo = known ? chalk.dim(` (${known.cost} credit/turn)`) : "";
|
|
330
|
-
console.log(chalk.green(`\n ✔ Switched to ${chalk.bold(currentModel)}${creditInfo}`));
|
|
331
|
-
console.log(chalk.dim(" Conversation history has been reset.\n"));
|
|
332
|
-
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;
|
|
333
181
|
}
|
|
334
|
-
console.log(chalk.yellow(`\n Unknown command: /${cmd}. Type /help for available commands.\n`));
|
|
335
182
|
return ask();
|
|
336
183
|
}
|
|
184
|
+
// ── Exit ───────────────────────────────────────────────────────────
|
|
337
185
|
if (userInput.toLowerCase() === "exit") {
|
|
338
|
-
const proxyEngine =
|
|
186
|
+
const proxyEngine = currentEngine instanceof CareerVividProxyEngine ? currentEngine : null;
|
|
339
187
|
if (proxyEngine && sessionTurns > 0) {
|
|
340
188
|
console.log(chalk.dim("\n─────────────────────────────────────────"));
|
|
341
189
|
console.log(chalk.dim(`Session: ${sessionTurns} turn${sessionTurns !== 1 ? "s" : ""} · `) +
|
|
@@ -349,402 +197,52 @@ export async function askLoop(engine, options, selectedProvider, selectedModel,
|
|
|
349
197
|
console.log(chalk.gray("\nGoodbye! 👋\n"));
|
|
350
198
|
process.exit(0);
|
|
351
199
|
}
|
|
352
|
-
//
|
|
353
|
-
|
|
354
|
-
|
|
200
|
+
// ── Run the agent turn ─────────────────────────────────────────────
|
|
201
|
+
sessionTurns++;
|
|
202
|
+
toolState.turnMutations = 0; // reset per-turn counter
|
|
355
203
|
process.stdout.write(chalk.dim("\n"));
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
save_cover_letter: "💾 Saving cover letter...",
|
|
381
|
-
delete_cover_letter: "🗑️ Deleting cover letter...",
|
|
382
|
-
browser_navigate: "🌐 Navigating to page...",
|
|
383
|
-
browser_click: "🖱️ Clicking element...",
|
|
384
|
-
browser_type: "⌨️ Typing input...",
|
|
385
|
-
browser_state: "🌐 Reading browser state...",
|
|
386
|
-
browser_screenshot: "📸 Taking screenshot...",
|
|
387
|
-
browser_scroll: "📜 Scrolling page...",
|
|
388
|
-
browser_wait: "⏳ Waiting...",
|
|
389
|
-
browser_close: "🔒 Closing browser...",
|
|
390
|
-
browser_select: "🖱️ Selecting option...",
|
|
391
|
-
tracker_recheck_urls: "🔗 Re-checking job URLs...",
|
|
392
|
-
browser_autofill_application: "📝 Auto-filling application...",
|
|
393
|
-
verify_url: "🔍 Verifying URL...",
|
|
394
|
-
verify_job_urls: "🔍 Verifying job URLs...",
|
|
395
|
-
search_jobs: "🔍 Searching jobs...",
|
|
396
|
-
openings_scan: "🎯 Scanning companies for open roles...",
|
|
397
|
-
openings_list: "📋 Loading saved openings...",
|
|
398
|
-
openings_apply: "✅ Marking opening as applied...",
|
|
399
|
-
get_resume: "📄 Loading resume...",
|
|
400
|
-
list_resumes: "📄 Loading resumes...",
|
|
401
|
-
get_profile: "👤 Loading profile...",
|
|
402
|
-
};
|
|
403
|
-
const handleToolCall = async (name, args) => {
|
|
404
|
-
// Stop the thinking spinner the moment we start a tool — prevents duplication
|
|
405
|
-
if (thinkingSpinner.isSpinning) {
|
|
406
|
-
thinkingSpinner.stop();
|
|
407
|
-
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(() => { });
|
|
408
228
|
}
|
|
409
|
-
// #9 Circuit breaker: abort if same tool called 5+ times consecutively with same args
|
|
410
|
-
const argsHash = JSON.stringify(args).slice(0, 100);
|
|
411
|
-
if (lastToolCall.name === name && lastToolCall.argsHash === argsHash) {
|
|
412
|
-
lastToolCall.count++;
|
|
413
|
-
if (lastToolCall.count >= 5) {
|
|
414
|
-
console.log(chalk.red(`\n⛔ Loop detected: "${name}" called ${lastToolCall.count} times with identical args. Aborting turn.`));
|
|
415
|
-
return false;
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
else {
|
|
419
|
-
lastToolCall = { name, argsHash, count: 1 };
|
|
420
|
-
}
|
|
421
|
-
// #3 Per-turn mutation budget
|
|
422
|
-
if (WRITE_TOOLS.has(name)) {
|
|
423
|
-
turnMutations++;
|
|
424
|
-
if (turnMutations > TURN_MAX_MUTATIONS) {
|
|
425
|
-
console.log(chalk.red(`\n⛔ Turn mutation limit (${TURN_MAX_MUTATIONS}) reached. The agent has made ${turnMutations} writes this turn.`));
|
|
426
|
-
return false;
|
|
427
|
-
}
|
|
428
|
-
sessionMutations++;
|
|
429
|
-
if (sessionMutations >= SESSION_MAX_MUTATIONS) {
|
|
430
|
-
console.log(chalk.yellow(`\n⚠️ Session mutation budget exhausted (${SESSION_MAX_MUTATIONS} writes). Restart the agent to continue writing.`));
|
|
431
|
-
return false;
|
|
432
|
-
}
|
|
433
|
-
else if (sessionMutations === SESSION_MAX_MUTATIONS - 5) {
|
|
434
|
-
console.log(chalk.yellow(`\n💡 Heads up: ${SESSION_MAX_MUTATIONS - sessionMutations} writes remaining this session.`));
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
// Print compact tool label — no blank lines, stays tight between steps
|
|
438
|
-
const label = TOOL_LABELS[name] ?? `⚙️ Working...`;
|
|
439
|
-
process.stdout.write(chalk.dim(` ${label}\n`));
|
|
440
|
-
if (name === "run_command") {
|
|
441
|
-
if (trustAllCommands || isSafeCommand(args.command)) {
|
|
442
|
-
return true;
|
|
443
|
-
}
|
|
444
|
-
const confirm = await prompt({
|
|
445
|
-
type: "select",
|
|
446
|
-
name: "ok",
|
|
447
|
-
message: `Allow running: ${chalk.bold(args.command)}?`,
|
|
448
|
-
choices: [
|
|
449
|
-
"Yes, run it",
|
|
450
|
-
"Yes, and trust all commands this session",
|
|
451
|
-
"No, skip it",
|
|
452
|
-
],
|
|
453
|
-
});
|
|
454
|
-
if (confirm.ok === "Yes, and trust all commands this session") {
|
|
455
|
-
trustAllCommands = true;
|
|
456
|
-
console.log(chalk.dim(" ✅ All commands will run automatically for the rest of this session."));
|
|
457
|
-
return true;
|
|
458
|
-
}
|
|
459
|
-
return confirm.ok === "Yes, run it";
|
|
460
|
-
}
|
|
461
|
-
if (name === "write_file" || name === "patch_file") {
|
|
462
|
-
if (trustAllWrites)
|
|
463
|
-
return true;
|
|
464
|
-
const target = args.path || "(unknown path)";
|
|
465
|
-
const confirm = await prompt({
|
|
466
|
-
type: "select",
|
|
467
|
-
name: "ok",
|
|
468
|
-
message: `Allow writing to: ${chalk.bold(target)}?`,
|
|
469
|
-
choices: [
|
|
470
|
-
"Yes, write it",
|
|
471
|
-
"Yes, and trust all writes this session",
|
|
472
|
-
"No, skip it",
|
|
473
|
-
],
|
|
474
|
-
});
|
|
475
|
-
if (confirm.ok === "Yes, and trust all writes this session") {
|
|
476
|
-
trustAllWrites = true;
|
|
477
|
-
console.log(chalk.dim(" ✅ All file writes will run automatically for the rest of this session."));
|
|
478
|
-
return true;
|
|
479
|
-
}
|
|
480
|
-
if (confirm.ok !== "Yes, write it")
|
|
481
|
-
return false;
|
|
482
|
-
}
|
|
483
|
-
if (["browser_state", "browser_screenshot", "browser_scroll", "browser_wait"].includes(name)) {
|
|
484
|
-
currentSpinner = ora(`Running ${chalk.bold(name)}...`).start();
|
|
485
|
-
return true;
|
|
486
|
-
}
|
|
487
|
-
if (["browser_navigate", "browser_click", "browser_type", "browser_select"].includes(name)) {
|
|
488
|
-
currentSpinner = ora(`Running ${chalk.bold(name)}...`).start();
|
|
489
|
-
return true;
|
|
490
|
-
}
|
|
491
|
-
if (name === "browser_close") {
|
|
492
|
-
const confirm = await prompt({
|
|
493
|
-
type: "select",
|
|
494
|
-
name: "ok",
|
|
495
|
-
message: `Close the browser?`,
|
|
496
|
-
choices: ["Yes, close it", "No, keep it open"],
|
|
497
|
-
});
|
|
498
|
-
if (confirm.ok !== "Yes, close it")
|
|
499
|
-
return false;
|
|
500
|
-
}
|
|
501
|
-
// Tools that take over the full terminal must NOT have a spinner running —
|
|
502
|
-
// the concurrent ora redraw causes constant flashing.
|
|
503
|
-
if (name === "start_interview") {
|
|
504
|
-
// Clear current line so any previous UI is gone, then yield terminal cleanly.
|
|
505
|
-
process.stdout.write("\r\x1b[K");
|
|
506
|
-
return true;
|
|
507
|
-
}
|
|
508
|
-
currentSpinner = ora(`Running ${chalk.bold(name)}...`).start();
|
|
509
|
-
return true;
|
|
510
|
-
};
|
|
511
|
-
const handleToolResult = (name, result) => {
|
|
512
|
-
if (currentSpinner) {
|
|
513
|
-
currentSpinner.succeed(chalk.dim("Done"));
|
|
514
|
-
currentSpinner = null;
|
|
515
|
-
}
|
|
516
|
-
if (name === "start_interview") {
|
|
517
|
-
// Interview already printed its own output — just add a separator.
|
|
518
|
-
console.log(chalk.dim("─".repeat(50)));
|
|
519
|
-
}
|
|
520
|
-
// #4 Audit log — record every completed tool call
|
|
521
|
-
// durationMs is approximate since we don't have exact start time here
|
|
522
|
-
auditLog({
|
|
523
|
-
sessionId: SESSION_ID,
|
|
524
|
-
tool: name,
|
|
525
|
-
args: typeof result?._args === "object" ? result._args : {},
|
|
526
|
-
result: typeof result === "string" ? result : JSON.stringify(result ?? ""),
|
|
527
|
-
durationMs: 0, // QueryEngine doesn't expose timing; repl.ts timing TBD
|
|
528
|
-
});
|
|
529
|
-
// Suppress raw output — the agent will summarize it in natural language
|
|
530
|
-
};
|
|
531
|
-
if (engine) {
|
|
532
|
-
sessionTurns++;
|
|
533
|
-
let responseAccumulator = "";
|
|
534
|
-
const sharedOnChunk = (text) => {
|
|
535
|
-
if (firstChunk) {
|
|
536
|
-
thinkingSpinner.stop();
|
|
537
|
-
// Print a subtle gutter marker so AI response is visually distinct
|
|
538
|
-
process.stdout.write("\n" + chalk.hex("#6366f1")("✦ "));
|
|
539
|
-
firstChunk = false;
|
|
540
|
-
}
|
|
541
|
-
process.stdout.write(text);
|
|
542
|
-
responseAccumulator += text; // Accumulate for TTS
|
|
543
|
-
};
|
|
544
|
-
const sharedOnError = (error) => {
|
|
545
|
-
thinkingSpinner.stop();
|
|
546
|
-
if (currentSpinner) {
|
|
547
|
-
currentSpinner.fail("Tool error");
|
|
548
|
-
currentSpinner = null;
|
|
549
|
-
}
|
|
550
|
-
console.log(chalk.red(`\n❌ Error: ${error.message}\n`));
|
|
551
|
-
};
|
|
552
|
-
if (engine instanceof CareerVividProxyEngine) {
|
|
553
|
-
await engine.runLoopStreaming(userInput, {
|
|
554
|
-
onChunk: sharedOnChunk,
|
|
555
|
-
onThinking: (thought) => {
|
|
556
|
-
if (options.verbose) {
|
|
557
|
-
console.log(chalk.dim(`\n[thinking] ${thought.substring(0, 200)}...`));
|
|
558
|
-
}
|
|
559
|
-
},
|
|
560
|
-
onToolCall: handleToolCall,
|
|
561
|
-
onToolResult: handleToolResult,
|
|
562
|
-
onCompacting: () => {
|
|
563
|
-
console.log(chalk.dim("\n📦 Compacting context window...\n"));
|
|
564
|
-
},
|
|
565
|
-
onError: sharedOnError,
|
|
566
|
-
onResponse: async (creditInfo) => {
|
|
567
|
-
sessionLimit = creditInfo.monthlyLimit;
|
|
568
|
-
printCreditStatus(creditInfo.creditsRemaining, sessionLimit);
|
|
569
|
-
},
|
|
570
|
-
onCreditLimitReached: (remaining) => {
|
|
571
|
-
console.log(chalk.red("\n\n⚠️ Credit limit reached (" + remaining + " remaining).\n" +
|
|
572
|
-
chalk.dim(" Upgrade or top up at ") +
|
|
573
|
-
chalk.underline.blue("careervivid.app/developer")));
|
|
574
|
-
},
|
|
575
|
-
});
|
|
576
|
-
}
|
|
577
|
-
else {
|
|
578
|
-
await engine.runLoopStreaming(userInput, {
|
|
579
|
-
onChunk: sharedOnChunk,
|
|
580
|
-
onThinking: (thought) => {
|
|
581
|
-
if (options.verbose) {
|
|
582
|
-
console.log(chalk.dim(`\n[thinking] ${thought.substring(0, 200)}...`));
|
|
583
|
-
}
|
|
584
|
-
},
|
|
585
|
-
onToolCall: handleToolCall,
|
|
586
|
-
onToolResult: handleToolResult,
|
|
587
|
-
onCompacting: () => {
|
|
588
|
-
console.log(chalk.dim("\n📦 Compacting context window...\n"));
|
|
589
|
-
},
|
|
590
|
-
onError: sharedOnError,
|
|
591
|
-
});
|
|
592
|
-
}
|
|
593
|
-
// ── TTS: store last response + auto-speak if voice enabled ──────
|
|
594
|
-
if (responseAccumulator) {
|
|
595
|
-
setLastResponse(responseAccumulator);
|
|
596
|
-
if (isVoiceEnabled()) {
|
|
597
|
-
speakText(responseAccumulator).catch(() => { });
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
// ── Clean turn separator after every AI reply ─────────────────────────────
|
|
601
|
-
process.stdout.write("\n" + chalk.dim("─".repeat(48)) + "\n");
|
|
602
|
-
}
|
|
603
|
-
else {
|
|
604
|
-
sessionTurns++;
|
|
605
|
-
const { createOpenAICompatibleProvider } = await import("../../agent/providers/OpenAIProvider.js");
|
|
606
|
-
const { AnthropicProvider } = await import("../../agent/providers/AnthropicProvider.js");
|
|
607
|
-
const byoApiKey = options["api-key"] || getProviderKey(selectedProvider) || loadConfig().llmApiKey;
|
|
608
|
-
const key = byoApiKey || "";
|
|
609
|
-
const baseUrl = options["base-url"] || loadConfig().llmBaseUrl;
|
|
610
|
-
let provider;
|
|
611
|
-
if (selectedProvider === "anthropic") {
|
|
612
|
-
provider = new AnthropicProvider({ apiKey: key });
|
|
613
|
-
}
|
|
614
|
-
else {
|
|
615
|
-
const subProvider = (selectedProvider === "openrouter" ? "openrouter" :
|
|
616
|
-
selectedProvider === "custom" ? "custom" : "openai");
|
|
617
|
-
provider = createOpenAICompatibleProvider(subProvider, key, baseUrl);
|
|
618
|
-
}
|
|
619
|
-
let userTurn = { role: "user", parts: [{ text: userInput }] };
|
|
620
|
-
let round = 0;
|
|
621
|
-
while (round < 10) {
|
|
622
|
-
const result = await withTimeout(provider.generate({ model: currentModel, history: byoHistory, userTurn, tools, systemInstruction }), 45_000, "LLM generate()");
|
|
623
|
-
if (round === 0) {
|
|
624
|
-
thinkingSpinner.stop();
|
|
625
|
-
process.stdout.write("\n" + chalk.hex("#6366f1")("\u2726 "));
|
|
626
|
-
}
|
|
627
|
-
if (result.text) {
|
|
628
|
-
process.stdout.write(result.text);
|
|
629
|
-
}
|
|
630
|
-
byoHistory.push(userTurn);
|
|
631
|
-
byoHistory.push({ role: "model", parts: result.rawParts || [{ text: result.text }] });
|
|
632
|
-
if (!result.functionCalls || result.functionCalls.length === 0) {
|
|
633
|
-
break;
|
|
634
|
-
}
|
|
635
|
-
let fnResponses = [];
|
|
636
|
-
for (const fc of result.functionCalls) {
|
|
637
|
-
const allow = await handleToolCall(fc.name, fc.args);
|
|
638
|
-
if (!allow) {
|
|
639
|
-
fnResponses.push({ functionResponse: { id: fc.id, name: fc.name, response: { error: "User denied execution." } } });
|
|
640
|
-
continue;
|
|
641
|
-
}
|
|
642
|
-
const tool = tools.find((t) => t.name === fc.name);
|
|
643
|
-
let out;
|
|
644
|
-
try {
|
|
645
|
-
// start_interview is an interactive long-running session — never apply a timeout to it.
|
|
646
|
-
// Also temporarily remove the REPL's SIGINT handler so the interview's own
|
|
647
|
-
// Ctrl+C handler can run cleanly (generate report) without racing against
|
|
648
|
-
// the REPL's "Goodbye! 👋" / process.exit path.
|
|
649
|
-
if (fc.name === "start_interview") {
|
|
650
|
-
process.removeListener("SIGINT", handleSigInt);
|
|
651
|
-
try {
|
|
652
|
-
out = tool ? await tool.execute(fc.args) : { error: "Tool not found" };
|
|
653
|
-
}
|
|
654
|
-
finally {
|
|
655
|
-
process.on("SIGINT", handleSigInt); // always restore, even on throw
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
else {
|
|
659
|
-
out = tool
|
|
660
|
-
? await withTimeout(tool.execute(fc.args), 45_000, `tool:${fc.name}`)
|
|
661
|
-
: { error: "Tool not found" };
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
catch (e) {
|
|
665
|
-
if (e.message?.includes("No API key configured")) {
|
|
666
|
-
out = { error: "CareerVivid API key not found. Run 'cv login' to authenticate." };
|
|
667
|
-
}
|
|
668
|
-
else {
|
|
669
|
-
out = { error: e.message };
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
handleToolResult(fc.name, out);
|
|
673
|
-
fnResponses.push({ functionResponse: { id: fc.id, name: fc.name, response: out } });
|
|
674
|
-
}
|
|
675
|
-
userTurn = { role: "user", parts: fnResponses };
|
|
676
|
-
round++;
|
|
677
|
-
}
|
|
678
|
-
// ── Clean turn separator after every AI reply ─────────────────────────────
|
|
679
|
-
process.stdout.write("\n" + chalk.dim("─".repeat(48)) + "\n");
|
|
680
229
|
}
|
|
681
230
|
return ask();
|
|
682
231
|
}
|
|
683
232
|
catch (err) {
|
|
684
233
|
const msg = err?.message ?? "";
|
|
685
|
-
// ── Clean exit on Ctrl+C / enquirer cancel ────────────────────────
|
|
686
234
|
if (!msg || msg.includes("cancelled") || msg.includes("User force closed")) {
|
|
687
235
|
console.log(chalk.gray("\nCancelled. Exiting.\n"));
|
|
688
236
|
process.exit(0);
|
|
689
237
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
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");
|
|
693
242
|
if (is401 && selectedProvider && selectedProvider !== "careervivid") {
|
|
694
|
-
|
|
695
|
-
openai: "OpenAI", anthropic: "Anthropic",
|
|
696
|
-
gemini: "Gemini", openrouter: "OpenRouter", custom: "Custom",
|
|
697
|
-
};
|
|
698
|
-
const providerKeyUrls = {
|
|
699
|
-
openai: "https://platform.openai.com/api-keys",
|
|
700
|
-
anthropic: "https://console.anthropic.com/settings/keys",
|
|
701
|
-
gemini: "https://aistudio.google.com/app/apikey",
|
|
702
|
-
openrouter: "https://openrouter.ai/settings/keys",
|
|
703
|
-
};
|
|
704
|
-
const label = providerLabels[selectedProvider] ?? selectedProvider;
|
|
705
|
-
console.log();
|
|
706
|
-
console.log(chalk.red(`❌ API key rejected by ${label} (401 Unauthorized).`));
|
|
707
|
-
console.log(chalk.dim(` The saved key may be expired or invalid.`));
|
|
708
|
-
if (providerKeyUrls[selectedProvider]) {
|
|
709
|
-
console.log(chalk.dim(` Get a new key at: `) + chalk.cyan(providerKeyUrls[selectedProvider]));
|
|
710
|
-
}
|
|
711
|
-
console.log();
|
|
712
|
-
try {
|
|
713
|
-
const resetAnswer = await prompt({
|
|
714
|
-
type: "select",
|
|
715
|
-
name: "action",
|
|
716
|
-
message: "What would you like to do?",
|
|
717
|
-
choices: [
|
|
718
|
-
{ name: "reset", message: `🔑 Enter a new ${label} API key` },
|
|
719
|
-
{ name: "continue", message: "⏭️ Continue anyway (will keep failing)" },
|
|
720
|
-
{ name: "exit", message: "🚪 Exit the agent" },
|
|
721
|
-
],
|
|
722
|
-
});
|
|
723
|
-
if (resetAnswer.action === "reset") {
|
|
724
|
-
const keyAnswer = await prompt({
|
|
725
|
-
type: "password",
|
|
726
|
-
name: "key",
|
|
727
|
-
message: `Enter your new ${label} API key:`,
|
|
728
|
-
});
|
|
729
|
-
const newKey = (keyAnswer?.key ?? "").trim();
|
|
730
|
-
if (newKey) {
|
|
731
|
-
setProviderKey(selectedProvider, newKey);
|
|
732
|
-
// Update the key used for subsequent turns this session
|
|
733
|
-
options["api-key"] = newKey;
|
|
734
|
-
console.log(chalk.green(`\n✔ New ${label} key saved. Resuming session...\n`));
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
else if (resetAnswer.action === "exit") {
|
|
738
|
-
console.log(chalk.gray("\nGoodbye! 👋\n"));
|
|
739
|
-
process.exit(0);
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
catch {
|
|
743
|
-
// User cancelled the reset prompt — just continue
|
|
744
|
-
}
|
|
243
|
+
await handle401Error(selectedProvider, options);
|
|
745
244
|
return ask();
|
|
746
245
|
}
|
|
747
|
-
// ── Generic error ────────────────────────────────────────────────
|
|
748
246
|
console.error(chalk.red(`\nAgent encountered an error: ${msg}`));
|
|
749
247
|
return ask();
|
|
750
248
|
}
|