careervivid 2.1.21 → 2.1.29
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/agent/QueryEngine.d.ts.map +1 -1
- package/dist/agent/QueryEngine.js +36 -4
- package/dist/agent/agentAuditLog.d.ts.map +1 -1
- package/dist/agent/agentAuditLog.js +45 -1
- package/dist/agent/instructions.d.ts +1 -0
- package/dist/agent/instructions.d.ts.map +1 -1
- package/dist/agent/instructions.js +96 -26
- package/dist/agent/memory.d.ts +28 -0
- package/dist/agent/memory.d.ts.map +1 -1
- package/dist/agent/memory.js +59 -0
- package/dist/agent/tools/coding.d.ts.map +1 -1
- package/dist/agent/tools/coding.js +27 -34
- package/dist/commands/agent/engineResolver.js +1 -1
- package/dist/commands/agent/index.d.ts.map +1 -1
- package/dist/commands/agent/index.js +11 -4
- package/dist/commands/agent/personas.d.ts +41 -0
- package/dist/commands/agent/personas.d.ts.map +1 -0
- package/dist/commands/agent/personas.js +323 -0
- 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 +30 -0
- package/dist/commands/agent/repl/input.d.ts.map +1 -0
- package/dist/commands/agent/repl/input.js +86 -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 +12 -1
- package/dist/commands/agent/repl.d.ts.map +1 -1
- package/dist/commands/agent/repl.js +134 -636
- package/dist/commands/agent/toolRegistry.d.ts.map +1 -1
- package/dist/commands/agent/toolRegistry.js +4 -2
- 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
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* personas.ts — Extensible Persona Registry for cv agent
|
|
3
|
+
*
|
|
4
|
+
* Each persona defines:
|
|
5
|
+
* - id: unique key (matches the CLI flag name)
|
|
6
|
+
* - label: human-readable name shown in the banner
|
|
7
|
+
* - menuItems: first-turn autocomplete menu entries
|
|
8
|
+
* - pickUpPrompt: the system message injected when the user chooses
|
|
9
|
+
* "Pick up where we left off" — workspace-aware, not job-aware
|
|
10
|
+
* - contextGather: async function returning a markdown context snapshot
|
|
11
|
+
* of the current environment relevant to this persona
|
|
12
|
+
*
|
|
13
|
+
* ── Adding a new persona ──────────────────────────────────────────────────────
|
|
14
|
+
* 1. Add a new PersonaDefinition entry in PERSONAS below.
|
|
15
|
+
* 2. Register the matching --flag in index.ts (one line).
|
|
16
|
+
* 3. Done. The menu, context, and system prompt routing all work automatically.
|
|
17
|
+
*/
|
|
18
|
+
import { execSync } from "child_process";
|
|
19
|
+
import { existsSync, readdirSync, statSync } from "fs";
|
|
20
|
+
import { join } from "path";
|
|
21
|
+
import { loadCodingSession } from "../../agent/memory.js";
|
|
22
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
23
|
+
// Shared context helpers
|
|
24
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
function tryExec(cmd) {
|
|
26
|
+
try {
|
|
27
|
+
return execSync(cmd, { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return "";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function getRecentlyModifiedFiles(dir, maxFiles = 8) {
|
|
34
|
+
const results = [];
|
|
35
|
+
function walk(current, depth = 0) {
|
|
36
|
+
if (depth > 3)
|
|
37
|
+
return;
|
|
38
|
+
let entries;
|
|
39
|
+
try {
|
|
40
|
+
entries = readdirSync(current);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
for (const name of entries) {
|
|
46
|
+
if (name.startsWith(".") || name === "node_modules" || name === "__pycache__" || name === ".git")
|
|
47
|
+
continue;
|
|
48
|
+
const full = join(current, name);
|
|
49
|
+
try {
|
|
50
|
+
const st = statSync(full);
|
|
51
|
+
if (st.isDirectory()) {
|
|
52
|
+
walk(full, depth + 1);
|
|
53
|
+
}
|
|
54
|
+
else if (/\.(ts|tsx|js|jsx|py|go|rs|java|cs|cpp|c|md|json|yaml|yml|sh|toml)$/.test(name)) {
|
|
55
|
+
results.push({ path: full.replace(dir + "/", ""), mtime: st.mtimeMs });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// skip
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
walk(dir);
|
|
64
|
+
return results
|
|
65
|
+
.sort((a, b) => b.mtime - a.mtime)
|
|
66
|
+
.slice(0, maxFiles)
|
|
67
|
+
.map((f) => f.path);
|
|
68
|
+
}
|
|
69
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
70
|
+
// Coding Persona
|
|
71
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
72
|
+
async function gatherCodingContext() {
|
|
73
|
+
const cwd = process.cwd();
|
|
74
|
+
const lines = [];
|
|
75
|
+
// Git branch + last commit
|
|
76
|
+
const branch = tryExec("git branch --show-current");
|
|
77
|
+
const lastCommit = tryExec("git log --oneline -1");
|
|
78
|
+
const gitStatus = tryExec("git status --short").split("\n").slice(0, 10).join("\n");
|
|
79
|
+
if (branch)
|
|
80
|
+
lines.push(`**Git branch:** \`${branch}\``);
|
|
81
|
+
if (lastCommit)
|
|
82
|
+
lines.push(`**Last commit:** ${lastCommit}`);
|
|
83
|
+
if (gitStatus)
|
|
84
|
+
lines.push(`**Uncommitted changes:**\n\`\`\`\n${gitStatus}\n\`\`\``);
|
|
85
|
+
// Recent files
|
|
86
|
+
const recentFiles = getRecentlyModifiedFiles(cwd);
|
|
87
|
+
if (recentFiles.length > 0) {
|
|
88
|
+
lines.push(`**Recently modified files:**\n${recentFiles.map((f) => ` • \`${f}\``).join("\n")}`);
|
|
89
|
+
}
|
|
90
|
+
// Package.json / pyproject.toml project name
|
|
91
|
+
const pkgPath = join(cwd, "package.json");
|
|
92
|
+
const pyPath = join(cwd, "pyproject.toml");
|
|
93
|
+
if (existsSync(pkgPath)) {
|
|
94
|
+
try {
|
|
95
|
+
const pkg = JSON.parse(require("fs").readFileSync(pkgPath, "utf-8"));
|
|
96
|
+
lines.push(`**Project:** ${pkg.name ?? "unknown"} v${pkg.version ?? "?"} (Node.js)`);
|
|
97
|
+
}
|
|
98
|
+
catch { /* ok */ }
|
|
99
|
+
}
|
|
100
|
+
else if (existsSync(pyPath)) {
|
|
101
|
+
const name = tryExec("grep '^name' pyproject.toml | head -1");
|
|
102
|
+
if (name)
|
|
103
|
+
lines.push(`**Project (Python):** ${name}`);
|
|
104
|
+
}
|
|
105
|
+
return lines.length > 0 ? lines.join("\n") : "No git repo or recognised project found in current directory.";
|
|
106
|
+
}
|
|
107
|
+
const CODING_PERSONA = {
|
|
108
|
+
id: "coding",
|
|
109
|
+
label: "Coding",
|
|
110
|
+
menuItems: [
|
|
111
|
+
"🔍 Analyze the current codebase or a specific file",
|
|
112
|
+
"🛠️ Debug an error or failing test",
|
|
113
|
+
"🏗️ Scaffold a new feature or module into an existing project",
|
|
114
|
+
"🔄 Refactor or optimize existing code",
|
|
115
|
+
"📖 Explain how a piece of code works",
|
|
116
|
+
"🧪 Write or improve tests",
|
|
117
|
+
"🆕 Create a brand-new project or app in a new folder",
|
|
118
|
+
"🌐 Debug or test in the browser",
|
|
119
|
+
"🗓️ Pick up where we left off",
|
|
120
|
+
],
|
|
121
|
+
menuPrompts: {
|
|
122
|
+
"🔍 Analyze the current codebase or a specific file": [
|
|
123
|
+
`The user wants to analyze their codebase. Current working directory: \`${process.cwd()}\`.`,
|
|
124
|
+
`1. Run get_file_tree on the current directory to show the project structure.`,
|
|
125
|
+
`2. Ask the user: "Which file or directory would you like me to analyze? Or should I give you an overview of the whole project?"`,
|
|
126
|
+
`3. Wait for their answer before reading any files.`,
|
|
127
|
+
].join("\n"),
|
|
128
|
+
"🛠️ Debug an error or failing test": [
|
|
129
|
+
`The user wants to debug an error or failing test. Current working directory: \`${process.cwd()}\`.`,
|
|
130
|
+
`1. Ask the user: "Please paste the full error message or test output, and tell me which file it's coming from."`,
|
|
131
|
+
`2. Once you have the error, read the relevant file(s) and identify the root cause.`,
|
|
132
|
+
`3. Apply a targeted fix with patch_file. Verify with run_command (e.g., npm test or tsc --noEmit).`,
|
|
133
|
+
].join("\n"),
|
|
134
|
+
"🏗️ Scaffold a new feature or module into an existing project": [
|
|
135
|
+
`The user wants to scaffold a feature or module into their EXISTING project. Current working directory: \`${process.cwd()}\`.`,
|
|
136
|
+
``,
|
|
137
|
+
`Before writing ANY files, ask the user these two questions in a single message:`,
|
|
138
|
+
`1. "What feature or module would you like to add?" — e.g., authentication, API route, React component, database model`,
|
|
139
|
+
`2. "Which directory should I add it to?" — e.g., src/components, src/api, lib/`,
|
|
140
|
+
``,
|
|
141
|
+
`Do NOT write any files until you have both answers.`,
|
|
142
|
+
`Once you have the answers: read the relevant existing files first → Plan → write_file/patch_file → run_command to verify → Report all changed paths.`,
|
|
143
|
+
].join("\n"),
|
|
144
|
+
"🆕 Create a brand-new project or app in a new folder": [
|
|
145
|
+
`The user wants to create a BRAND-NEW standalone project in a new folder. Current working directory: \`${process.cwd()}\`.`,
|
|
146
|
+
``,
|
|
147
|
+
`Ask the user ONE message with these two questions:`,
|
|
148
|
+
`1. "What would you like to build?"`,
|
|
149
|
+
` Give them examples to spark ideas:`,
|
|
150
|
+
` • A modern HTML/CSS/JS dashboard visualising my CareerVivid job pipeline`,
|
|
151
|
+
` • A React + Vite app with TypeScript`,
|
|
152
|
+
` • A FastAPI Python REST API`,
|
|
153
|
+
` • A Next.js full-stack app`,
|
|
154
|
+
` • A Node.js CLI tool`,
|
|
155
|
+
` • A static landing page`,
|
|
156
|
+
` • Or describe anything else`,
|
|
157
|
+
`2. "What should the folder be called?" (e.g., job-dashboard, my-api, cool-app)`,
|
|
158
|
+
``,
|
|
159
|
+
`Do NOT create any files or run any commands until you have both answers.`,
|
|
160
|
+
``,
|
|
161
|
+
`Once you have the answers, follow this execution loop:`,
|
|
162
|
+
` a. Plan: list every file you will create with a one-line description of each`,
|
|
163
|
+
` b. Create the folder and write EVERY file using write_file`,
|
|
164
|
+
` — For HTML/CSS/JS projects: write index.html, style.css, app.js (and any sub-files) with REAL, complete, production-quality code — NO placeholders`,
|
|
165
|
+
` — For Node/Python projects: write package.json/pyproject.toml, README.md, and all source files`,
|
|
166
|
+
` — For React/Next/Vite apps: write all config files AND source components`,
|
|
167
|
+
` c. Run run_command to verify (e.g., \`tsc --noEmit\`, \`python -m py_compile\`, or \`open index.html\`)`,
|
|
168
|
+
` d. Report: list every file created with its absolute path so the user can open them immediately`,
|
|
169
|
+
``,
|
|
170
|
+
`Special case — Job Pipeline Dashboard:`,
|
|
171
|
+
`If the user asks for a dashboard based on their job application pipeline or CareerVivid data:`,
|
|
172
|
+
` 1. Run run_command with \`cat ~/career-vivid/jobs.csv\` to read their real job data`,
|
|
173
|
+
` 2. Parse the CSV and embed the actual data as a JavaScript array in the dashboard`,
|
|
174
|
+
` 3. Build a beautiful, modern HTML/CSS/JS single-file dashboard with:`,
|
|
175
|
+
` — Kanban-style status columns (To Apply / Applied / Interview / Offer / Rejected)`,
|
|
176
|
+
` — Cards for each job showing company, role, status, excitement score`,
|
|
177
|
+
` — Summary stats bar (total jobs, applied %, top companies)`,
|
|
178
|
+
` — A color-coded priority chart`,
|
|
179
|
+
` — Responsive design that works on mobile`,
|
|
180
|
+
` 4. Write it to \`job-dashboard/index.html\` as a self-contained file (inline CSS + JS)`,
|
|
181
|
+
` 5. Run \`open job-dashboard/index.html\` so the user sees it immediately in their browser`,
|
|
182
|
+
].join("\n"),
|
|
183
|
+
"🔄 Refactor or optimize existing code": [
|
|
184
|
+
`The user wants to refactor or optimize code. Current working directory: \`${process.cwd()}\`.`,
|
|
185
|
+
`1. Ask: "Which file or function would you like me to refactor? What's the goal — performance, readability, type safety, or something else?"`,
|
|
186
|
+
`2. Wait for their answer. Then read the file, propose a brief Plan, and apply changes with patch_file.`,
|
|
187
|
+
].join("\n"),
|
|
188
|
+
"📖 Explain how a piece of code works": [
|
|
189
|
+
`The user wants an explanation of some code. Current working directory: \`${process.cwd()}\`.`,
|
|
190
|
+
`Ask: "Which file, function, or class would you like me to explain?"`,
|
|
191
|
+
`Once they answer, read the file and give a clear, structured explanation.`,
|
|
192
|
+
].join("\n"),
|
|
193
|
+
"🧪 Write or improve tests": [
|
|
194
|
+
`The user wants to write or improve tests. Current working directory: \`${process.cwd()}\`.`,
|
|
195
|
+
`1. Ask: "Which file or function should I write tests for? And do you want unit tests, integration tests, or browser-based E2E tests?"`,
|
|
196
|
+
`2. Once they answer:`,
|
|
197
|
+
` • For unit/integration tests: read the source file, detect the test framework (check package.json), write test files to disk with write_file, run with run_command.`,
|
|
198
|
+
` • For browser E2E tests: use browser_navigate to open the app, browser_state to inspect, browser_click/browser_type to simulate user actions, browser_screenshot to verify results.`,
|
|
199
|
+
`3. Report all test files created and test results.`,
|
|
200
|
+
].join("\n"),
|
|
201
|
+
"🌐 Debug or test in the browser": [
|
|
202
|
+
`The user wants to debug a visual/runtime issue OR test a feature directly in the browser. Current working directory: \`${process.cwd()}\`.`,
|
|
203
|
+
``,
|
|
204
|
+
`First, ask ONE clarifying question: "What would you like to do?"`,
|
|
205
|
+
`Give these options:`,
|
|
206
|
+
` • Debug a visual or layout bug in my running app`,
|
|
207
|
+
` • Debug a JavaScript runtime error`,
|
|
208
|
+
` • Smoke test / manually test a feature in the browser`,
|
|
209
|
+
` • Run a browser-based E2E test on my app`,
|
|
210
|
+
` • Verify my generated HTML/CSS/JS file looks correct`,
|
|
211
|
+
``,
|
|
212
|
+
`Once you know what they want, follow the browser debug/test loop:`,
|
|
213
|
+
`1. Ask for the URL if not provided (e.g., http://localhost:3000 or a file path like ./index.html)`,
|
|
214
|
+
`2. Call browser_navigate(url) to open it`,
|
|
215
|
+
`3. Call browser_state() to get the page structure`,
|
|
216
|
+
`4. Call browser_screenshot() to visually inspect the page`,
|
|
217
|
+
`5. For debugging: identify the root cause, patch_file to fix it, browser_navigate to reload and verify`,
|
|
218
|
+
`6. For testing: browser_click / browser_type to simulate user interactions, browser_screenshot to confirm the expected result`,
|
|
219
|
+
`7. Report what was found and fixed/verified`,
|
|
220
|
+
``,
|
|
221
|
+
`If the user's dev server is not running, offer to start it: run_command with \`npm run dev\` or equivalent.`,
|
|
222
|
+
].join("\n"),
|
|
223
|
+
},
|
|
224
|
+
pickUpPrompt: (() => {
|
|
225
|
+
// Read the coding session file synchronously — this is O(1), no tool calls.
|
|
226
|
+
const last = loadCodingSession();
|
|
227
|
+
if (last) {
|
|
228
|
+
const fileList = last.filesChanged.length > 0
|
|
229
|
+
? `Files changed last session:\n${last.filesChanged.slice(0, 10).map(f => ` • \`${f}\``).join("\n")}`
|
|
230
|
+
: "";
|
|
231
|
+
return [
|
|
232
|
+
`## Resuming Last Coding Session`,
|
|
233
|
+
``,
|
|
234
|
+
`Here is the context from the last coding session (read from local memory — no tool calls needed):`,
|
|
235
|
+
``,
|
|
236
|
+
`- **Directory:** \`${last.cwd}\``,
|
|
237
|
+
`- **Git branch:** ${last.branch || "(no git repo)"}`,
|
|
238
|
+
`- **Last built:** ${last.lastBuilt || "(unknown)"}`,
|
|
239
|
+
`- **Session saved:** ${new Date(last.savedAt).toLocaleString()}`,
|
|
240
|
+
``,
|
|
241
|
+
`**What happened last time:**`,
|
|
242
|
+
last.summary,
|
|
243
|
+
``,
|
|
244
|
+
fileList,
|
|
245
|
+
``,
|
|
246
|
+
`The current working directory is now: \`${process.cwd()}\``,
|
|
247
|
+
``,
|
|
248
|
+
`Greet the user with a ONE-LINE recap of what was worked on last time and ask what they want to continue or start next.`,
|
|
249
|
+
`Do NOT run any git commands or scan the filesystem — you already have the context above.`,
|
|
250
|
+
`Only call tools if the user asks for something new.`,
|
|
251
|
+
].filter(Boolean).join("\n");
|
|
252
|
+
}
|
|
253
|
+
// No previous session — do a lightweight orientation
|
|
254
|
+
return [
|
|
255
|
+
`No previous coding session found in memory.`,
|
|
256
|
+
`Current working directory: \`${process.cwd()}\`.`,
|
|
257
|
+
``,
|
|
258
|
+
`Ask the user: "What would you like to work on today?" — give them the menu options as a reminder.`,
|
|
259
|
+
`Do NOT run any commands or scan files until they answer.`,
|
|
260
|
+
].join("\n");
|
|
261
|
+
})(),
|
|
262
|
+
contextGather: gatherCodingContext,
|
|
263
|
+
};
|
|
264
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
265
|
+
// Jobs Persona (default)
|
|
266
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
267
|
+
const JOBS_PERSONA = {
|
|
268
|
+
id: "jobs",
|
|
269
|
+
label: "Jobs & Applications",
|
|
270
|
+
menuItems: [
|
|
271
|
+
"📄 View or update my resume",
|
|
272
|
+
"🔍 Search for job opportunities",
|
|
273
|
+
"📊 Check my job pipeline / tracker",
|
|
274
|
+
"✉️ Draft a cover letter or tailor my resume",
|
|
275
|
+
"🎙 Start an AI mock interview (voice or text)",
|
|
276
|
+
"📈 Get an overview of my job search progress",
|
|
277
|
+
"🗓️ Pick up where we left off",
|
|
278
|
+
],
|
|
279
|
+
pickUpPrompt: "The user wants to pick up their last job-search session. Check their resume list (cv tracker resume list), review their job tracker pipeline (cv tracker list), and provide a concise overview of where they left off. Suggest the most impactful next action.",
|
|
280
|
+
contextGather: async () => "",
|
|
281
|
+
};
|
|
282
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
283
|
+
// Resume Persona
|
|
284
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
285
|
+
const RESUME_PERSONA = {
|
|
286
|
+
id: "resume",
|
|
287
|
+
label: "Resume Builder",
|
|
288
|
+
menuItems: [
|
|
289
|
+
"📄 View or load my current resume",
|
|
290
|
+
"✏️ Update a section of my resume",
|
|
291
|
+
"🎯 Tailor my resume for a specific job",
|
|
292
|
+
"🔗 Sync resume to my portfolio",
|
|
293
|
+
"💡 Get suggestions to improve my resume",
|
|
294
|
+
"🗓️ Pick up where we left off",
|
|
295
|
+
],
|
|
296
|
+
pickUpPrompt: "The user wants to pick up their last resume session. List their resumes with get_resume or list_resumes, identify the most recently updated one, and summarise what was last worked on. Suggest the next improvement.",
|
|
297
|
+
contextGather: async () => "",
|
|
298
|
+
};
|
|
299
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
300
|
+
// Persona registry
|
|
301
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
302
|
+
const PERSONA_MAP = {
|
|
303
|
+
coding: CODING_PERSONA,
|
|
304
|
+
jobs: JOBS_PERSONA,
|
|
305
|
+
resume: RESUME_PERSONA,
|
|
306
|
+
// Future:
|
|
307
|
+
// ceo: CEO_PERSONA,
|
|
308
|
+
// finance: FINANCE_PERSONA,
|
|
309
|
+
// marketing: MARKETING_PERSONA,
|
|
310
|
+
};
|
|
311
|
+
/**
|
|
312
|
+
* Resolve the active PersonaDefinition from the CLI options object.
|
|
313
|
+
* Falls back to the jobs persona.
|
|
314
|
+
*/
|
|
315
|
+
export function resolvePersona(options) {
|
|
316
|
+
for (const key of Object.keys(PERSONA_MAP)) {
|
|
317
|
+
if (options[key])
|
|
318
|
+
return PERSONA_MAP[key];
|
|
319
|
+
}
|
|
320
|
+
return JOBS_PERSONA;
|
|
321
|
+
}
|
|
322
|
+
/** All registered personas — useful for dynamic --help generation. */
|
|
323
|
+
export const ALL_PERSONAS = Object.values(PERSONA_MAP);
|
|
@@ -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,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* repl/input.ts
|
|
3
|
+
*
|
|
4
|
+
* Handles all user input collection:
|
|
5
|
+
* - First-turn quick-action menu (autocomplete) — persona-aware
|
|
6
|
+
* - Subsequent turn free-text input
|
|
7
|
+
* - Multi-line paste mode (<<<)
|
|
8
|
+
* - Fast-paste buffer accumulation
|
|
9
|
+
*/
|
|
10
|
+
import type { PersonaDefinition } from "../personas.js";
|
|
11
|
+
/**
|
|
12
|
+
* Read the first-turn menu selection.
|
|
13
|
+
* Returns the cleaned user-intent string ready to send to the agent.
|
|
14
|
+
*
|
|
15
|
+
* When the user picks "Pick up where we left off" the persona's
|
|
16
|
+
* `pickUpPrompt` is returned instead of the raw menu label — so
|
|
17
|
+
* the agent receives a rich context-gathering instruction specific
|
|
18
|
+
* to the active mode (coding, jobs, resume, …).
|
|
19
|
+
*/
|
|
20
|
+
export declare function readFirstTurnInput(persona: PersonaDefinition): Promise<string>;
|
|
21
|
+
/** Reads a single multi-line paste block. User ends with an empty Enter. */
|
|
22
|
+
export declare function readMultiLineInput(prefix: string): Promise<string>;
|
|
23
|
+
export interface InputResult {
|
|
24
|
+
text: string;
|
|
25
|
+
/** True when the input arrived in < 150ms (possible paste line) */
|
|
26
|
+
isFastLine: boolean;
|
|
27
|
+
}
|
|
28
|
+
/** Read a normal subsequent-turn line. Returns the raw text and timing flag. */
|
|
29
|
+
export declare function readNormalInput(showContinuation: boolean): Promise<InputResult>;
|
|
30
|
+
//# 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;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAMxD;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CAoCpF;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,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* repl/input.ts
|
|
3
|
+
*
|
|
4
|
+
* Handles all user input collection:
|
|
5
|
+
* - First-turn quick-action menu (autocomplete) — persona-aware
|
|
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 PICK_UP_LABEL = "🗓️ Pick up where we left off";
|
|
14
|
+
/**
|
|
15
|
+
* Read the first-turn menu selection.
|
|
16
|
+
* Returns the cleaned user-intent string ready to send to the agent.
|
|
17
|
+
*
|
|
18
|
+
* When the user picks "Pick up where we left off" the persona's
|
|
19
|
+
* `pickUpPrompt` is returned instead of the raw menu label — so
|
|
20
|
+
* the agent receives a rich context-gathering instruction specific
|
|
21
|
+
* to the active mode (coding, jobs, resume, …).
|
|
22
|
+
*/
|
|
23
|
+
export async function readFirstTurnInput(persona) {
|
|
24
|
+
console.log(chalk.dim(" What would you like to do today?\n"));
|
|
25
|
+
for (const item of persona.menuItems) {
|
|
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: persona.menuItems.length,
|
|
34
|
+
suggest(input, choices) {
|
|
35
|
+
if (!input)
|
|
36
|
+
return choices;
|
|
37
|
+
return choices.filter((c) => c.value.toLowerCase().includes(input.toLowerCase()));
|
|
38
|
+
},
|
|
39
|
+
choices: persona.menuItems.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
|
+
// 1. "Pick up where we left off" → persona-specific context-gathering prompt
|
|
44
|
+
if (raw === PICK_UP_LABEL || raw.toLowerCase().includes("pick up")) {
|
|
45
|
+
return persona.pickUpPrompt;
|
|
46
|
+
}
|
|
47
|
+
// 2. Per-item prompt override (e.g. scaffold, debug) → rich targeted prompt
|
|
48
|
+
if (persona.menuPrompts?.[raw]) {
|
|
49
|
+
return persona.menuPrompts[raw];
|
|
50
|
+
}
|
|
51
|
+
// 3. Default: strip emoji prefix and send clean text
|
|
52
|
+
return raw.replace(/^[\p{Emoji}\s]+/u, "").trim() || raw;
|
|
53
|
+
}
|
|
54
|
+
/** Reads a single multi-line paste block. User ends with an empty Enter. */
|
|
55
|
+
export async function readMultiLineInput(prefix) {
|
|
56
|
+
console.log(chalk.dim(" 📋 Multi-line mode: paste your text, then press Enter twice to submit.\n"));
|
|
57
|
+
const lines = prefix ? [prefix] : [];
|
|
58
|
+
let emptyCount = 0;
|
|
59
|
+
while (emptyCount < 1) {
|
|
60
|
+
const lineResp = await prompt({
|
|
61
|
+
type: "input",
|
|
62
|
+
name: "line",
|
|
63
|
+
message: chalk.dim(" │"),
|
|
64
|
+
});
|
|
65
|
+
if (lineResp.line === "") {
|
|
66
|
+
emptyCount++;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
emptyCount = 0;
|
|
70
|
+
lines.push(lineResp.line);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return lines.join("\n").trim();
|
|
74
|
+
}
|
|
75
|
+
/** Read a normal subsequent-turn line. Returns the raw text and timing flag. */
|
|
76
|
+
export async function readNormalInput(showContinuation) {
|
|
77
|
+
const t0 = Date.now();
|
|
78
|
+
const response = await prompt({
|
|
79
|
+
type: "input",
|
|
80
|
+
name: "query",
|
|
81
|
+
message: showContinuation
|
|
82
|
+
? chalk.dim("... ")
|
|
83
|
+
: chalk.bold.hex("#6366f1")("❯") + chalk.dim(" ·"),
|
|
84
|
+
});
|
|
85
|
+
return { text: response.query, isFastLine: Date.now() - t0 < 150 };
|
|
86
|
+
}
|