careervivid 2.1.0 → 2.1.12
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/README.md +1 -1
- package/dist/agent/instructions.d.ts +2 -1
- package/dist/agent/instructions.d.ts.map +1 -1
- package/dist/agent/instructions.js +31 -5
- package/dist/commands/agent/engineResolver.d.ts +1 -1
- package/dist/commands/agent/engineResolver.d.ts.map +1 -1
- package/dist/commands/agent/engineResolver.js +6 -20
- package/dist/commands/agent/repl.d.ts.map +1 -1
- package/dist/commands/agent/repl.js +122 -65
- package/dist/commands/interview.d.ts.map +1 -1
- package/dist/commands/interview.js +97 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/careervivid)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](https://nodejs.org)
|
|
8
|
-
[](https://www.npmjs.com/package/careervivid)
|
|
8
|
+
[](https://www.npmjs.com/package/careervivid)
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
*
|
|
11
11
|
* DO NOT scatter instructions across QueryEngine.ts, engineResolver.ts, or anywhere else.
|
|
12
12
|
*/
|
|
13
|
-
|
|
13
|
+
/** @deprecated Keep for any external callers that import baseIdentity directly. */
|
|
14
|
+
export declare const baseIdentity: string;
|
|
14
15
|
export declare const RESUME_SECTION: string;
|
|
15
16
|
export declare const CODING_SECTION: string;
|
|
16
17
|
export declare const JOBS_TOOLS_SECTION: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"instructions.d.ts","sourceRoot":"","sources":["../../src/agent/instructions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;
|
|
1
|
+
{"version":3,"file":"instructions.d.ts","sourceRoot":"","sources":["../../src/agent/instructions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAsCH,mFAAmF;AACnF,eAAO,MAAM,YAAY,QAAsB,CAAC;AAMhD,eAAO,MAAM,cAAc,QAcnB,CAAC;AAMT,eAAO,MAAM,cAAc,QAqCnB,CAAC;AAMT,eAAO,MAAM,kBAAkB,QA0DvB,CAAC;AAMT,eAAO,MAAM,YAAY,QA6CjB,CAAC;AAMT,eAAO,MAAM,iBAAiB,QAuBtB,CAAC;AAOT;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE;IACzC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,GAAG,MAAM,CAqCT"}
|
|
@@ -11,10 +11,26 @@
|
|
|
11
11
|
* DO NOT scatter instructions across QueryEngine.ts, engineResolver.ts, or anywhere else.
|
|
12
12
|
*/
|
|
13
13
|
import { buildMemoryBlock, MEMORY_SECTION } from "./memory.js";
|
|
14
|
-
|
|
14
|
+
/** Returns today's date as YYYY-MM-DD in the local timezone. */
|
|
15
|
+
function todayStr() {
|
|
16
|
+
const d = new Date();
|
|
17
|
+
return [
|
|
18
|
+
d.getFullYear(),
|
|
19
|
+
String(d.getMonth() + 1).padStart(2, '0'),
|
|
20
|
+
String(d.getDate()).padStart(2, '0'),
|
|
21
|
+
].join('-');
|
|
22
|
+
}
|
|
23
|
+
/** @internal — call via buildSystemPrompt(), not directly */
|
|
24
|
+
function buildBaseIdentity() {
|
|
25
|
+
return `
|
|
15
26
|
You are CareerVivid AI — an autonomous career intelligence agent built into the CareerVivid CLI.
|
|
16
27
|
You help users manage their resume, track job applications, find new opportunities, prep for interviews, and grow their career.
|
|
17
28
|
|
|
29
|
+
## Today's Date
|
|
30
|
+
**TODAY IS: ${todayStr()}**
|
|
31
|
+
Use this exact date whenever you need "today", "now", or the current date.
|
|
32
|
+
NEVER guess, infer, or assume a date — always use the value above.
|
|
33
|
+
|
|
18
34
|
## Core Behavioral Rules
|
|
19
35
|
|
|
20
36
|
1. **ANSWER FIRST** — If the user asks a direct question (e.g., "Did you find X?", "What is Y?"), answer it explicitly and immediately. Do not ignore the question or bury it behind a massive multi-step automated tangent.
|
|
@@ -25,6 +41,9 @@ You help users manage their resume, track job applications, find new opportuniti
|
|
|
25
41
|
6. **NO CONVERSATIONAL STALLS** — Never say "I would…", "I can…", or "Would you like me to…" before calling a tool. Just call it.
|
|
26
42
|
7. **TRANSPARENCY** — If uncertain, list options and your recommendation. Never silently choose.
|
|
27
43
|
`.trim();
|
|
44
|
+
}
|
|
45
|
+
/** @deprecated Keep for any external callers that import baseIdentity directly. */
|
|
46
|
+
export const baseIdentity = buildBaseIdentity();
|
|
28
47
|
// ---------------------------------------------------------------------------
|
|
29
48
|
// §2 — Resume section (appended in --resume and --jobs modes)
|
|
30
49
|
// ---------------------------------------------------------------------------
|
|
@@ -215,7 +234,12 @@ When the user sends a generic greeting ("hey", "hi", "hello", "start"), respond
|
|
|
215
234
|
|
|
216
235
|
Just tell me what you need!"
|
|
217
236
|
|
|
218
|
-
If the user says "pick up where we left off" or similar
|
|
237
|
+
If the user says "pick up where we left off" or similar:
|
|
238
|
+
1. First check the Career Context block below for a "Last session" line and reference it.
|
|
239
|
+
2. If the Career Context block is empty or has no "Last session", do NOT say "I have no memory."
|
|
240
|
+
Instead, immediately call tracker_list_jobs (or tracker_dashboard) + list_resumes in parallel
|
|
241
|
+
to reconstruct the user's current state from live data, then summarize what you find.
|
|
242
|
+
3. Only say "nothing saved yet" if BOTH the memory block AND all tool calls return empty results.
|
|
219
243
|
`.trim();
|
|
220
244
|
// ---------------------------------------------------------------------------
|
|
221
245
|
// §7 — Assembled system prompts per mode (the public API)
|
|
@@ -226,6 +250,8 @@ If the user says "pick up where we left off" or similar, immediately reference t
|
|
|
226
250
|
* This is the ONLY function that engineResolver.ts should call.
|
|
227
251
|
*/
|
|
228
252
|
export function buildSystemPrompt(options) {
|
|
253
|
+
// Always build fresh so today's date is injected at call time, not module-load time.
|
|
254
|
+
const baseIdentity = buildBaseIdentity();
|
|
229
255
|
// Load memory block (empty string if no memory file exists yet)
|
|
230
256
|
const memoryBlock = buildMemoryBlock();
|
|
231
257
|
const memorySections = memoryBlock
|
|
@@ -233,7 +259,7 @@ export function buildSystemPrompt(options) {
|
|
|
233
259
|
: [];
|
|
234
260
|
if (options.jobs) {
|
|
235
261
|
return [
|
|
236
|
-
|
|
262
|
+
baseIdentity,
|
|
237
263
|
RESUME_SECTION,
|
|
238
264
|
JOBS_TOOLS_SECTION,
|
|
239
265
|
JOBS_HARNESS,
|
|
@@ -243,7 +269,7 @@ export function buildSystemPrompt(options) {
|
|
|
243
269
|
}
|
|
244
270
|
if (options.resume) {
|
|
245
271
|
return [
|
|
246
|
-
|
|
272
|
+
baseIdentity,
|
|
247
273
|
RESUME_SECTION,
|
|
248
274
|
...memorySections,
|
|
249
275
|
GREETING_PROTOCOL,
|
|
@@ -251,7 +277,7 @@ export function buildSystemPrompt(options) {
|
|
|
251
277
|
}
|
|
252
278
|
// Default: coding / general mode
|
|
253
279
|
return [
|
|
254
|
-
|
|
280
|
+
baseIdentity,
|
|
255
281
|
CODING_SECTION,
|
|
256
282
|
...memorySections,
|
|
257
283
|
GREETING_PROTOCOL,
|
|
@@ -21,5 +21,5 @@ export declare function printBanner(options: {
|
|
|
21
21
|
resume?: boolean;
|
|
22
22
|
pro?: boolean;
|
|
23
23
|
think?: number;
|
|
24
|
-
}, selectedProvider: string, selectedModel: string,
|
|
24
|
+
}, selectedProvider: string, selectedModel: string, _thinkingBudget: number): void;
|
|
25
25
|
//# sourceMappingURL=engineResolver.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engineResolver.d.ts","sourceRoot":"","sources":["../../../src/commands/agent/engineResolver.ts"],"names":[],"mappings":"AACA,OAAO,EAAyC,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC1F,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAE,sBAAsB,EAAE,MAAM,uCAAuC,CAAC;AAC/E,OAAO,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAI3C;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,MAAM,CAE5G;AAED,wBAAgB,WAAW,CACzB,gBAAgB,EAAE,WAAW,EAC7B,aAAa,EAAE,MAAM,EACrB,iBAAiB,EAAE,MAAM,EACzB,KAAK,EAAE,IAAI,EAAE,EACb,cAAc,EAAE,MAAM,EACtB,eAAe,EAAE,OAAO,EACxB,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,OAAO,EAAE,MAAM,GAAG,SAAS,GAC1B,WAAW,GAAG,sBAAsB,GAAG,IAAI,CA2B7C;AAED,wBAAgB,WAAW,CACzB,OAAO,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,GAAG,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,EAC9F,gBAAgB,EAAE,MAAM,EACxB,aAAa,EAAE,MAAM,EACrB,
|
|
1
|
+
{"version":3,"file":"engineResolver.d.ts","sourceRoot":"","sources":["../../../src/commands/agent/engineResolver.ts"],"names":[],"mappings":"AACA,OAAO,EAAyC,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC1F,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAE,sBAAsB,EAAE,MAAM,uCAAuC,CAAC;AAC/E,OAAO,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAI3C;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,MAAM,CAE5G;AAED,wBAAgB,WAAW,CACzB,gBAAgB,EAAE,WAAW,EAC7B,aAAa,EAAE,MAAM,EACrB,iBAAiB,EAAE,MAAM,EACzB,KAAK,EAAE,IAAI,EAAE,EACb,cAAc,EAAE,MAAM,EACtB,eAAe,EAAE,OAAO,EACxB,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,OAAO,EAAE,MAAM,GAAG,SAAS,GAC1B,WAAW,GAAG,sBAAsB,GAAG,IAAI,CA2B7C;AAED,wBAAgB,WAAW,CACzB,OAAO,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,GAAG,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,EAC9F,gBAAgB,EAAE,MAAM,EACxB,aAAa,EAAE,MAAM,EACrB,eAAe,EAAE,MAAM,QAcxB"}
|
|
@@ -40,30 +40,16 @@ export function buildEngine(selectedProvider, selectedModel, systemInstruction,
|
|
|
40
40
|
}
|
|
41
41
|
return engine;
|
|
42
42
|
}
|
|
43
|
-
export function printBanner(options, selectedProvider, selectedModel,
|
|
44
|
-
console.log(chalk.bold.
|
|
43
|
+
export function printBanner(options, selectedProvider, selectedModel, _thinkingBudget) {
|
|
44
|
+
console.log(chalk.bold.hex("#6366f1")("\n CareerVivid Agent"));
|
|
45
45
|
if (selectedProvider === "careervivid") {
|
|
46
46
|
const cost = MODEL_CREDIT_COST[selectedModel] ?? 1;
|
|
47
47
|
console.log(chalk.dim(` Model: ${selectedModel}`) +
|
|
48
|
-
chalk.
|
|
48
|
+
chalk.dim(` [${cost} credit${cost !== 1 ? "s" : ""}/turn via CareerVivid Cloud]`));
|
|
49
49
|
}
|
|
50
50
|
else {
|
|
51
|
-
console.log(chalk.
|
|
51
|
+
console.log(chalk.dim(` Provider: ${selectedProvider} · Model: ${selectedModel} [0 credits]`));
|
|
52
52
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (options.jobs) {
|
|
56
|
-
console.log(chalk.cyan(" ✔ Job mode: search, score, apply_to_job, openings_scan tools active"));
|
|
57
|
-
console.log(chalk.magenta(" ✔ Browser mode: navigate, click, type, select, scroll, screenshot tools active"));
|
|
58
|
-
console.log(chalk.yellow(" ✔ Local tracker: tracker_list_jobs · tracker_update_job · tracker_add_job"));
|
|
59
|
-
console.log(chalk.yellow(" + tracker_rank_priority · tracker_dashboard · tracker_find_stale"));
|
|
60
|
-
console.log(chalk.yellow(" + tracker_recheck_urls · openings_scan · openings_list (jobs.csv v2)"));
|
|
61
|
-
}
|
|
62
|
-
else if (options.resume)
|
|
63
|
-
console.log(chalk.cyan(" ✔ Resume mode: get_resume tool active"));
|
|
64
|
-
if (options.pro)
|
|
65
|
-
console.log(chalk.magenta(` ✔ Pro mode: ${selectedModel} + thinking (${thinkingBudget} tokens)`));
|
|
66
|
-
else if (thinkingBudget > 0)
|
|
67
|
-
console.log(chalk.yellow(` ✔ Thinking mode: ${thinkingBudget} token budget`));
|
|
68
|
-
console.log(chalk.gray(` Type 'exit' to quit · /model to switch models · /help for commands.\n`));
|
|
53
|
+
const modeLabel = options.jobs ? "Jobs & Applications" : options.resume ? "Resume" : "General";
|
|
54
|
+
console.log(chalk.dim(` Mode: ${modeLabel} · Type 'exit' to quit · /help for commands.\n`));
|
|
69
55
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"repl.d.ts","sourceRoot":"","sources":["../../../src/commands/agent/repl.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,sBAAsB,EAAE,MAAM,uCAAuC,CAAC;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAEzD,OAAO,EAA4D,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAK7G,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,GAAE,MAAM,GAAG,IAAW,QAwBtF;AAED,wBAAsB,OAAO,CAC3B,MAAM,EAAE,WAAW,GAAG,sBAAsB,GAAG,IAAI,EACnD,OAAO,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,EAC9K,gBAAgB,EAAE,WAAW,EAC7B,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,iBAAiB,EAAE,MAAM,EACzB,KAAK,EAAE,GAAG,EAAE,GACX,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"repl.d.ts","sourceRoot":"","sources":["../../../src/commands/agent/repl.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,sBAAsB,EAAE,MAAM,uCAAuC,CAAC;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAEzD,OAAO,EAA4D,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAK7G,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,GAAE,MAAM,GAAG,IAAW,QAwBtF;AAED,wBAAsB,OAAO,CAC3B,MAAM,EAAE,WAAW,GAAG,sBAAsB,GAAG,IAAI,EACnD,OAAO,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,EAC9K,gBAAgB,EAAE,WAAW,EAC7B,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,iBAAiB,EAAE,MAAM,EACzB,KAAK,EAAE,GAAG,EAAE,GACX,OAAO,CAAC,IAAI,CAAC,CA+oBf"}
|
|
@@ -67,65 +67,97 @@ export async function askLoop(engine, options, selectedProvider, selectedModel,
|
|
|
67
67
|
.catch(e => { clearTimeout(timer); reject(e); });
|
|
68
68
|
});
|
|
69
69
|
}
|
|
70
|
-
|
|
70
|
+
// ── First-turn menu items ────────────────────────────────────────────────
|
|
71
|
+
const MENU_ITEMS = [
|
|
72
|
+
"📄 View or update my resume",
|
|
73
|
+
"🔍 Search for job opportunities",
|
|
74
|
+
"📊 Check my job pipeline / tracker",
|
|
75
|
+
"✉️ Draft a cover letter or tailor my resume",
|
|
76
|
+
"🎙 Start an AI mock interview (voice or text)",
|
|
77
|
+
"📈 Get an overview of my job search progress",
|
|
78
|
+
"🗓️ Pick up where we left off",
|
|
79
|
+
];
|
|
80
|
+
const ask = async (isFirstTurn = false) => {
|
|
71
81
|
try {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
: chalk.bold.cyan("❯") + chalk.dim(" ·"),
|
|
79
|
-
});
|
|
80
|
-
const duration = Date.now() - promptStartTime;
|
|
81
|
-
let userInput = response.query;
|
|
82
|
-
// ── Multi-line paste mode: user typed <<< (or <<<paste) ─────────────
|
|
83
|
-
// Allows pasting arbitrarily long content (e.g. full JD) without truncation.
|
|
84
|
-
if (userInput.trim() === "<<<" || userInput.trim().toLowerCase().startsWith("<<<")) {
|
|
85
|
-
const prefix = userInput.trim().slice(3).trim(); // text after <<<
|
|
86
|
-
console.log(chalk.dim(" 📋 Multi-line mode: paste your text, then press Enter twice to submit.\n"));
|
|
87
|
-
const lines = prefix ? [prefix] : [];
|
|
88
|
-
let emptyCount = 0;
|
|
89
|
-
while (emptyCount < 1) {
|
|
90
|
-
const lineResp = await prompt({
|
|
91
|
-
type: "input",
|
|
92
|
-
name: "line",
|
|
93
|
-
message: chalk.dim(" │"),
|
|
94
|
-
});
|
|
95
|
-
if (lineResp.line === "") {
|
|
96
|
-
emptyCount++;
|
|
97
|
-
}
|
|
98
|
-
else {
|
|
99
|
-
emptyCount = 0;
|
|
100
|
-
lines.push(lineResp.line);
|
|
101
|
-
}
|
|
82
|
+
let userInput;
|
|
83
|
+
if (isFirstTurn) {
|
|
84
|
+
// ── Hybrid menu: arrow-key select OR free type ─────────────────
|
|
85
|
+
console.log(chalk.dim(" What would you like to do today?\n"));
|
|
86
|
+
for (const item of MENU_ITEMS) {
|
|
87
|
+
console.log(chalk.dim(` ${item}`));
|
|
102
88
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
89
|
+
console.log("");
|
|
90
|
+
const firstResp = await prompt({
|
|
91
|
+
type: "autocomplete",
|
|
92
|
+
name: "choice",
|
|
93
|
+
message: chalk.bold.hex("#6366f1")("❯") + chalk.dim(" ·"),
|
|
94
|
+
// @ts-ignore — enquirer autocomplete supports limit
|
|
95
|
+
limit: 7,
|
|
96
|
+
suggest(input, choices) {
|
|
97
|
+
if (!input)
|
|
98
|
+
return choices;
|
|
99
|
+
return choices.filter((c) => c.value.toLowerCase().includes(input.toLowerCase()));
|
|
100
|
+
},
|
|
101
|
+
choices: MENU_ITEMS.map(item => ({ name: item, value: item })),
|
|
102
|
+
footer: chalk.dim(" ↑↓ to navigate · type to filter · Enter to send"),
|
|
103
|
+
});
|
|
104
|
+
userInput = firstResp.choice?.trim() || "";
|
|
105
|
+
// Strip emoji prefixes so the agent gets clean text
|
|
106
|
+
userInput = userInput.replace(/^[\p{Emoji}\s]+/u, "").trim() || firstResp.choice?.trim() || "";
|
|
111
107
|
}
|
|
112
108
|
else {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
109
|
+
// ── Normal text input for subsequent turns ──────────────────────
|
|
110
|
+
const promptStartTime = Date.now();
|
|
111
|
+
const response = await prompt({
|
|
112
|
+
type: "input",
|
|
113
|
+
name: "query",
|
|
114
|
+
message: pasteBuffer.length > 0
|
|
115
|
+
? chalk.dim("... ")
|
|
116
|
+
: chalk.bold.hex("#6366f1")("❯") + chalk.dim(" ·"),
|
|
117
|
+
});
|
|
118
|
+
userInput = response.query;
|
|
119
|
+
const duration = Date.now() - promptStartTime;
|
|
120
|
+
// ── Multi-line paste mode ──────────────────────────────────────
|
|
121
|
+
if (userInput.trim() === "<<<" || userInput.trim().toLowerCase().startsWith("<<<")) {
|
|
122
|
+
const prefix = userInput.trim().slice(3).trim();
|
|
123
|
+
console.log(chalk.dim(" 📋 Multi-line mode: paste your text, then press Enter twice to submit.\n"));
|
|
124
|
+
const lines = prefix ? [prefix] : [];
|
|
125
|
+
let emptyCount = 0;
|
|
126
|
+
while (emptyCount < 1) {
|
|
127
|
+
const lineResp = await prompt({
|
|
128
|
+
type: "input",
|
|
129
|
+
name: "line",
|
|
130
|
+
message: chalk.dim(" │"),
|
|
131
|
+
});
|
|
132
|
+
if (lineResp.line === "") {
|
|
133
|
+
emptyCount++;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
emptyCount = 0;
|
|
137
|
+
lines.push(lineResp.line);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
userInput = lines.join("\n").trim();
|
|
118
141
|
pasteBuffer = [];
|
|
119
142
|
}
|
|
120
|
-
|
|
143
|
+
else if (duration < 150) {
|
|
144
|
+
pasteBuffer.push(userInput);
|
|
145
|
+
return ask();
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
if (pasteBuffer.length > 0) {
|
|
149
|
+
if (userInput)
|
|
150
|
+
pasteBuffer.push(userInput);
|
|
151
|
+
userInput = pasteBuffer.join("\n");
|
|
152
|
+
pasteBuffer = [];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
} // end else (non-first turn)
|
|
121
156
|
userInput = userInput.trim();
|
|
122
157
|
if (!userInput)
|
|
123
158
|
return ask();
|
|
124
159
|
// ── Input length guard ──────────────────────────────────────────
|
|
125
|
-
|
|
126
|
-
// pasting very long job descriptions gets silently truncated mid-word.
|
|
127
|
-
// Detect this early and guide the user to <<< mode instead.
|
|
128
|
-
const MAX_INPUT_CHARS = 20_000; // ~3,000 words — safe above typical JD length
|
|
160
|
+
const MAX_INPUT_CHARS = 20_000;
|
|
129
161
|
if (userInput.length > MAX_INPUT_CHARS) {
|
|
130
162
|
console.log(chalk.yellow("\n⚠️ Input is too long (" + userInput.length + " chars).") +
|
|
131
163
|
chalk.dim("\n Use <<< mode for long job descriptions so nothing gets cut off:") +
|
|
@@ -207,7 +239,9 @@ export async function askLoop(engine, options, selectedProvider, selectedModel,
|
|
|
207
239
|
}
|
|
208
240
|
// Reset per-turn mutation counter at the start of each user message
|
|
209
241
|
turnMutations = 0;
|
|
210
|
-
|
|
242
|
+
// ── Clear previous spinner residue then show thinking indicator ──
|
|
243
|
+
process.stdout.write(chalk.dim("\n"));
|
|
244
|
+
const thinkingSpinner = ora({ text: chalk.dim("Vivid is thinking…"), color: "cyan", spinner: "dots" }).start();
|
|
211
245
|
let firstChunk = true;
|
|
212
246
|
let currentSpinner = null;
|
|
213
247
|
let trustAllCommands = false;
|
|
@@ -255,6 +289,11 @@ export async function askLoop(engine, options, selectedProvider, selectedModel,
|
|
|
255
289
|
get_profile: "👤 Loading profile...",
|
|
256
290
|
};
|
|
257
291
|
const handleToolCall = async (name, args) => {
|
|
292
|
+
// Stop the thinking spinner the moment we start a tool — prevents duplication
|
|
293
|
+
if (thinkingSpinner.isSpinning) {
|
|
294
|
+
thinkingSpinner.stop();
|
|
295
|
+
process.stdout.write("\r\x1b[K"); // clear spinner line
|
|
296
|
+
}
|
|
258
297
|
// #9 Circuit breaker: abort if same tool called 5+ times consecutively with same args
|
|
259
298
|
const argsHash = JSON.stringify(args).slice(0, 100);
|
|
260
299
|
if (lastToolCall.name === name && lastToolCall.argsHash === argsHash) {
|
|
@@ -283,11 +322,9 @@ export async function askLoop(engine, options, selectedProvider, selectedModel,
|
|
|
283
322
|
console.log(chalk.yellow(`\n💡 Heads up: ${SESSION_MAX_MUTATIONS - sessionMutations} writes remaining this session.`));
|
|
284
323
|
}
|
|
285
324
|
}
|
|
286
|
-
//
|
|
325
|
+
// Print compact tool label — no blank lines, stays tight between steps
|
|
287
326
|
const label = TOOL_LABELS[name] ?? `⚙️ Working...`;
|
|
288
|
-
process.stdout.write(chalk.dim(`
|
|
289
|
-
${label}
|
|
290
|
-
`));
|
|
327
|
+
process.stdout.write(chalk.dim(` ${label}\n`));
|
|
291
328
|
if (name === "run_command") {
|
|
292
329
|
if (trustAllCommands || isSafeCommand(args.command)) {
|
|
293
330
|
return true;
|
|
@@ -361,7 +398,7 @@ ${label}
|
|
|
361
398
|
};
|
|
362
399
|
const handleToolResult = (name, result) => {
|
|
363
400
|
if (currentSpinner) {
|
|
364
|
-
currentSpinner.succeed(chalk.dim(
|
|
401
|
+
currentSpinner.succeed(chalk.dim("Done"));
|
|
365
402
|
currentSpinner = null;
|
|
366
403
|
}
|
|
367
404
|
if (name === "start_interview") {
|
|
@@ -383,12 +420,15 @@ ${label}
|
|
|
383
420
|
sessionTurns++;
|
|
384
421
|
const sharedOnChunk = (text) => {
|
|
385
422
|
if (firstChunk) {
|
|
386
|
-
|
|
423
|
+
thinkingSpinner.stop();
|
|
424
|
+
// Print a subtle gutter marker so AI response is visually distinct
|
|
425
|
+
process.stdout.write("\n" + chalk.hex("#6366f1")("✦ "));
|
|
387
426
|
firstChunk = false;
|
|
388
427
|
}
|
|
389
|
-
process.stdout.write(
|
|
428
|
+
process.stdout.write(text);
|
|
390
429
|
};
|
|
391
430
|
const sharedOnError = (error) => {
|
|
431
|
+
thinkingSpinner.stop();
|
|
392
432
|
if (currentSpinner) {
|
|
393
433
|
currentSpinner.fail("Tool error");
|
|
394
434
|
currentSpinner = null;
|
|
@@ -436,6 +476,8 @@ ${label}
|
|
|
436
476
|
onError: sharedOnError,
|
|
437
477
|
});
|
|
438
478
|
}
|
|
479
|
+
// ── Clean turn separator after every AI reply ─────────────────────────────
|
|
480
|
+
process.stdout.write("\n" + chalk.dim("─".repeat(48)) + "\n");
|
|
439
481
|
}
|
|
440
482
|
else {
|
|
441
483
|
sessionTurns++;
|
|
@@ -458,10 +500,11 @@ ${label}
|
|
|
458
500
|
while (round < 10) {
|
|
459
501
|
const result = await withTimeout(provider.generate({ model: currentModel, history: byoHistory, userTurn, tools, systemInstruction }), 45_000, "LLM generate()");
|
|
460
502
|
if (round === 0) {
|
|
461
|
-
|
|
503
|
+
thinkingSpinner.stop();
|
|
504
|
+
process.stdout.write("\n" + chalk.hex("#6366f1")("\u2726 "));
|
|
462
505
|
}
|
|
463
506
|
if (result.text) {
|
|
464
|
-
|
|
507
|
+
process.stdout.write(result.text);
|
|
465
508
|
}
|
|
466
509
|
byoHistory.push(userTurn);
|
|
467
510
|
byoHistory.push({ role: "model", parts: result.rawParts || [{ text: result.text }] });
|
|
@@ -479,11 +522,23 @@ ${label}
|
|
|
479
522
|
let out;
|
|
480
523
|
try {
|
|
481
524
|
// start_interview is an interactive long-running session — never apply a timeout to it.
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
525
|
+
// Also temporarily remove the REPL's SIGINT handler so the interview's own
|
|
526
|
+
// Ctrl+C handler can run cleanly (generate report) without racing against
|
|
527
|
+
// the REPL's "Goodbye! 👋" / process.exit path.
|
|
528
|
+
if (fc.name === "start_interview") {
|
|
529
|
+
process.removeListener("SIGINT", handleSigInt);
|
|
530
|
+
try {
|
|
531
|
+
out = tool ? await tool.execute(fc.args) : { error: "Tool not found" };
|
|
532
|
+
}
|
|
533
|
+
finally {
|
|
534
|
+
process.on("SIGINT", handleSigInt); // always restore, even on throw
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
out = tool
|
|
539
|
+
? await withTimeout(tool.execute(fc.args), 45_000, `tool:${fc.name}`)
|
|
540
|
+
: { error: "Tool not found" };
|
|
541
|
+
}
|
|
487
542
|
}
|
|
488
543
|
catch (e) {
|
|
489
544
|
if (e.message?.includes("No API key configured")) {
|
|
@@ -499,6 +554,8 @@ ${label}
|
|
|
499
554
|
userTurn = { role: "user", parts: fnResponses };
|
|
500
555
|
round++;
|
|
501
556
|
}
|
|
557
|
+
// ── Clean turn separator after every AI reply ─────────────────────────────
|
|
558
|
+
process.stdout.write("\n" + chalk.dim("─".repeat(48)) + "\n");
|
|
502
559
|
}
|
|
503
560
|
return ask();
|
|
504
561
|
}
|
|
@@ -571,5 +628,5 @@ ${label}
|
|
|
571
628
|
return ask();
|
|
572
629
|
}
|
|
573
630
|
};
|
|
574
|
-
return ask();
|
|
631
|
+
return ask(true);
|
|
575
632
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"interview.d.ts","sourceRoot":"","sources":["../../src/commands/interview.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"interview.d.ts","sourceRoot":"","sources":["../../src/commands/interview.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAyzBpC,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAmG/D"}
|
|
@@ -133,7 +133,7 @@ function printReport(report) {
|
|
|
133
133
|
console.log("\n" + chalk.yellow.bold(" 💡 Areas for Improvement"));
|
|
134
134
|
wordWrap(report.areasForImprovement, 72).split("\n").forEach(l => console.log(` ${chalk.yellow(l)}`));
|
|
135
135
|
console.log("\n" + chalk.dim(" ─────────────────────────────────────────────────────────────"));
|
|
136
|
-
console.log(chalk.dim(" View full history at: https://careervivid.app/interview"));
|
|
136
|
+
console.log(chalk.dim(" View full history at: https://careervivid.app/interview-studio"));
|
|
137
137
|
console.log("");
|
|
138
138
|
}
|
|
139
139
|
// ─── sox audio check ──────────────────────────────────────────────────────────
|
|
@@ -381,12 +381,17 @@ async function runVoiceSession(opts) {
|
|
|
381
381
|
const systemInstruction = buildSystemPrompt(role, questions, resumeContext);
|
|
382
382
|
const transcript = [];
|
|
383
383
|
let ended = false;
|
|
384
|
-
let outputBuf = ""; // accumulates AI transcription
|
|
385
|
-
let inputBuf = ""; // accumulates user transcription
|
|
386
|
-
|
|
387
|
-
// to prevent the mic picking up speaker output (echo loop).
|
|
384
|
+
let outputBuf = ""; // accumulates AI output transcription for current turn
|
|
385
|
+
let inputBuf = ""; // accumulates user input transcription for current turn
|
|
386
|
+
let streamColPos = 0; // current col position while streaming Vivid's text
|
|
388
387
|
let vividSpeaking = false;
|
|
389
388
|
let muteTimer = null;
|
|
389
|
+
// Track whether we're currently showing live user speech in the terminal
|
|
390
|
+
let userSpeechLineActive = false;
|
|
391
|
+
// Detect END_TOKEN in the raw chunk BEFORE it's stripped from the display buffer.
|
|
392
|
+
// (outputBuf never contains END_TOKEN because chunkClean strips it, so the
|
|
393
|
+
// turnComplete check on outputBuf would never fire without this flag.)
|
|
394
|
+
let endTokenSeen = false;
|
|
390
395
|
// ── Audio processes ──────────────────────────────────────────────────
|
|
391
396
|
const micProc = startMic(soxPath);
|
|
392
397
|
const speakerProc = startSpeaker(soxPath);
|
|
@@ -399,9 +404,14 @@ async function runVoiceSession(opts) {
|
|
|
399
404
|
model: LIVE_MODEL,
|
|
400
405
|
callbacks: {
|
|
401
406
|
onopen: () => {
|
|
402
|
-
connectSpinner.succeed(chalk.green("✅ Vivid is
|
|
403
|
-
|
|
404
|
-
|
|
407
|
+
connectSpinner.succeed(chalk.green("✅ Vivid is ready!"));
|
|
408
|
+
// Show a professional prompt so the user knows to speak first
|
|
409
|
+
console.log("");
|
|
410
|
+
console.log(chalk.bold.white(" 🎙 Greet Vivid to begin your interview."));
|
|
411
|
+
console.log(chalk.dim(' Say \'Hello\' or \'Hi, I\'m ready\' to get started.'));
|
|
412
|
+
console.log("");
|
|
413
|
+
process.stdout.write(chalk.green(" ● Listening...\r"));
|
|
414
|
+
// Pipe mic PCM → Gemini (muted while Vivid is speaking)
|
|
405
415
|
micProc.stdout.on("data", (chunk) => {
|
|
406
416
|
if (ended || chunk.length === 0 || vividSpeaking)
|
|
407
417
|
return;
|
|
@@ -420,48 +430,109 @@ async function runVoiceSession(opts) {
|
|
|
420
430
|
// ── Audio output (Vivid speaking) → sox speaker ───
|
|
421
431
|
const audioPart = msg.serverContent?.modelTurn?.parts?.[0]?.inlineData?.data;
|
|
422
432
|
if (audioPart) {
|
|
423
|
-
// Mute mic: cancel any pending unmute and stay muted
|
|
424
433
|
vividSpeaking = true;
|
|
425
434
|
if (muteTimer) {
|
|
426
435
|
clearTimeout(muteTimer);
|
|
427
436
|
muteTimer = null;
|
|
428
437
|
}
|
|
429
|
-
process.stdout.write(chalk.blue(" ◈ Vivid speaking...\r"));
|
|
430
438
|
const pcmBuf = Buffer.from(audioPart, "base64");
|
|
431
439
|
speakerProc.stdin.write(pcmBuf);
|
|
432
440
|
}
|
|
433
|
-
// ── Output transcription (
|
|
441
|
+
// ── Output transcription (Vivid's words) — stream in real-time ──
|
|
434
442
|
const outText = msg.serverContent?.outputTranscription?.text;
|
|
435
|
-
if (outText)
|
|
436
|
-
|
|
437
|
-
|
|
443
|
+
if (outText) {
|
|
444
|
+
// Detect END_TOKEN in raw chunk BEFORE stripping
|
|
445
|
+
if (outText.includes(END_TOKEN))
|
|
446
|
+
endTokenSeen = true;
|
|
447
|
+
// Phrase-based fallback: model sometimes omits the token
|
|
448
|
+
if (outText.toLowerCase().includes("feedback report is being generated"))
|
|
449
|
+
endTokenSeen = true;
|
|
450
|
+
const chunkClean = outText.replace(END_TOKEN, "");
|
|
451
|
+
if (chunkClean) {
|
|
452
|
+
if (!outputBuf) {
|
|
453
|
+
// First chunk: print the speaker header on a fresh line
|
|
454
|
+
process.stdout.write("\n" + chalk.cyan.bold(" Vivid ❯") + "\n");
|
|
455
|
+
streamColPos = 0;
|
|
456
|
+
}
|
|
457
|
+
outputBuf += chunkClean;
|
|
458
|
+
// Stream words inline with soft word-wrap
|
|
459
|
+
for (const word of chunkClean.split(/(\s+)/)) {
|
|
460
|
+
if (!word)
|
|
461
|
+
continue;
|
|
462
|
+
const isWhitespace = /^\s+$/.test(word);
|
|
463
|
+
if (isWhitespace) {
|
|
464
|
+
if (streamColPos > 0) {
|
|
465
|
+
process.stdout.write(chalk.cyan(' '));
|
|
466
|
+
streamColPos += 1;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
if (streamColPos > 0 && streamColPos + word.length > WRAP_WIDTH) {
|
|
471
|
+
process.stdout.write('\n');
|
|
472
|
+
streamColPos = 0;
|
|
473
|
+
}
|
|
474
|
+
const prefix = streamColPos === 0 ? ' ' : '';
|
|
475
|
+
process.stdout.write(chalk.cyan(prefix + word));
|
|
476
|
+
streamColPos += prefix.length + word.length;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
// ── Input transcription (user's live speech) — stream in real-time ──
|
|
438
482
|
const inText = msg.serverContent?.inputTranscription?.text;
|
|
439
|
-
if (inText)
|
|
483
|
+
if (inText) {
|
|
484
|
+
if (!userSpeechLineActive) {
|
|
485
|
+
// First chunk of user speech: clear the Listening indicator and
|
|
486
|
+
// start a "[You] ❯" prefix on a fresh line
|
|
487
|
+
process.stdout.write("\r\x1B[2K"); // clear current line
|
|
488
|
+
process.stdout.write(chalk.dim(" [You] ❯ ") + chalk.white(""));
|
|
489
|
+
userSpeechLineActive = true;
|
|
490
|
+
}
|
|
440
491
|
inputBuf += inText;
|
|
492
|
+
// Rewrite the whole user line so far (keeps it clean as chunks arrive)
|
|
493
|
+
process.stdout.write("\r\x1B[2K");
|
|
494
|
+
process.stdout.write(chalk.dim(" [You] ❯ ") + chalk.white(inputBuf.trim()));
|
|
495
|
+
}
|
|
441
496
|
// ── Turn complete ─────────────────────────────────
|
|
442
497
|
if (msg.serverContent?.turnComplete) {
|
|
443
498
|
if (outputBuf.trim()) {
|
|
444
499
|
const aiText = outputBuf.trim();
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
500
|
+
// Close the streamed line cleanly
|
|
501
|
+
if (streamColPos > 0)
|
|
502
|
+
process.stdout.write("\n");
|
|
503
|
+
process.stdout.write("\n"); // blank line after Vivid's turn
|
|
504
|
+
transcript.push({ speaker: "ai", text: aiText });
|
|
505
|
+
// Use the pre-strip flag — NOT aiText.includes(END_TOKEN)
|
|
506
|
+
if (endTokenSeen)
|
|
448
507
|
ended = true;
|
|
449
508
|
outputBuf = "";
|
|
509
|
+
streamColPos = 0;
|
|
510
|
+
endTokenSeen = false; // reset for next turn
|
|
450
511
|
}
|
|
451
512
|
if (inputBuf.trim()) {
|
|
452
|
-
|
|
513
|
+
// Finalize the user speech line with a newline
|
|
514
|
+
process.stdout.write("\n");
|
|
515
|
+
userSpeechLineActive = false;
|
|
453
516
|
transcript.push({ speaker: "user", text: inputBuf.trim() });
|
|
454
517
|
inputBuf = "";
|
|
455
518
|
}
|
|
456
519
|
if (!ended) {
|
|
457
|
-
// Unmute mic after a short delay so the speaker
|
|
458
|
-
// tail doesn't get captured (echo suppression buffer)
|
|
459
520
|
muteTimer = setTimeout(() => {
|
|
460
521
|
vividSpeaking = false;
|
|
461
522
|
muteTimer = null;
|
|
462
|
-
|
|
523
|
+
if (!userSpeechLineActive) {
|
|
524
|
+
process.stdout.write(chalk.green("\n ● Listening...\r"));
|
|
525
|
+
}
|
|
463
526
|
}, 800);
|
|
464
527
|
}
|
|
528
|
+
else {
|
|
529
|
+
// Interview ended via END_TOKEN — cancel pending mute timer
|
|
530
|
+
// so '● Listening...' never appears after the interview concludes
|
|
531
|
+
if (muteTimer) {
|
|
532
|
+
clearTimeout(muteTimer);
|
|
533
|
+
muteTimer = null;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
465
536
|
}
|
|
466
537
|
},
|
|
467
538
|
onerror: (e) => {
|
|
@@ -582,7 +653,10 @@ async function runVoiceSession(opts) {
|
|
|
582
653
|
log.warn("billing_timeout", { sessionId, sessionDurationMs });
|
|
583
654
|
}
|
|
584
655
|
if (report) {
|
|
585
|
-
console.log(chalk.dim("\n 💡
|
|
656
|
+
console.log(chalk.dim("\n 💡 Report saved! Continue your prep with:"));
|
|
657
|
+
console.log(chalk.dim(" • ") + chalk.white("`cv agent`") + chalk.dim(" or ") + chalk.white("`cv agent --jobs`") + chalk.dim(" → coach me on my answers"));
|
|
658
|
+
console.log(chalk.dim(" • Rewrite resume, draft cover letter, or start another round"));
|
|
659
|
+
console.log(chalk.dim(" • View report: ") + chalk.cyan("https://careervivid.app/interview-studio"));
|
|
586
660
|
}
|
|
587
661
|
await log.dispose();
|
|
588
662
|
}
|
package/package.json
CHANGED