careervivid 2.0.0 → 2.1.1

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 CHANGED
@@ -5,7 +5,7 @@
5
5
  [![npm version](https://img.shields.io/npm/v/careervivid?color=0ea5e9&label=careervivid)](https://www.npmjs.com/package/careervivid)
6
6
  [![License: MIT](https://img.shields.io/badge/license-MIT-brightgreen)](LICENSE)
7
7
  [![Node ≥18](https://img.shields.io/badge/node-%3E%3D18-blue)](https://nodejs.org)
8
- [![v2.0](https://img.shields.io/badge/version-2.0-6366f1?logo=sparkles)](https://www.npmjs.com/package/careervivid)
8
+ [![v2.1.1](https://img.shields.io/badge/version-2.1.1-6366f1?logo=sparkles)](https://www.npmjs.com/package/careervivid)
9
9
 
10
10
  ---
11
11
 
@@ -94,6 +94,15 @@ cv interview --questions 7 # custom question count (default
94
94
  - Overall, communication, confidence, and relevance scores (0–100)
95
95
  - Specific strengths and areas for improvement
96
96
 
97
+ **Interview History & Coaching:**
98
+
99
+ Every session (transcript + report) is automatically persisted to your CareerVivid account. This enables the **AI Agent** to provide post-interview coaching:
100
+
101
+ 1. Complete an interview via `cv interview`.
102
+ 2. Start the agent: `cv agent`.
103
+ 3. Ask: *"How can I improve my answers from my last interview?"*
104
+ 4. The agent retrieves your actual transcript and suggests **STAR-method** improvements for your specific responses.
105
+
97
106
  **Credit cost:** **2 credits/minute** (minimum 2, maximum 60 per session)
98
107
 
99
108
  | Session length | Credits |
@@ -1 +1 @@
1
- {"version":3,"file":"instructions.d.ts","sourceRoot":"","sources":["../../src/agent/instructions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,eAAO,MAAM,aAAa,QAalB,CAAC;AAMT,eAAO,MAAM,cAAc,QAcnB,CAAC;AAMT,eAAO,MAAM,cAAc,QAqCnB,CAAC;AAMT,eAAO,MAAM,kBAAkB,QAgDvB,CAAC;AAMT,eAAO,MAAM,YAAY,QA4CjB,CAAC;AAMT,eAAO,MAAM,iBAAiB,QAkBtB,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,CAkCT"}
1
+ {"version":3,"file":"instructions.d.ts","sourceRoot":"","sources":["../../src/agent/instructions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,eAAO,MAAM,aAAa,QAalB,CAAC;AAMT,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,QAkBtB,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,CAkCT"}
@@ -135,6 +135,16 @@ NEVER share a link without verifying it first.
135
135
  - Feedback report auto-generated at the end with scores and improvement tips.
136
136
  - **Credit cost:** 2 credits/minute (min 2, max 60). Text mode ~1 credit flat.
137
137
  - Use when user says: "practice interview", "mock interview", "interview me for [role]", "I have an interview at [company]", etc.
138
+ - ⚠️ **PRE-FLIGHT REQUIRED:** Before calling this tool, ALWAYS ask the user:
139
+ 1. What role (if not already stated)
140
+ 2. Voice or Text mode? — Never assume. Ask every time unless user already specified.
141
+ - **fetch_interview_context** — Retrieve the user's recent mock interview sessions (transcript + scores) for coaching.
142
+ - Use when user says: "improve my answers", "review my interview", "how did I do?",
143
+ "coach me on my [company] interview", "what did I say", "help me with STAR stories",
144
+ or any post-interview coaching request.
145
+ - Returns: scores, strengths, areasForImprovement, and the full Q&A transcript.
146
+ - After fetching, provide specific STAR-method rewrites for weak answers.
147
+ - ⭐ After start_interview completes, ALWAYS offer: "Would you like me to review your answers and give coaching tips?"
138
148
  `.trim();
139
149
  // ---------------------------------------------------------------------------
140
150
  // §5 — Autonomous execution harness (appended in --jobs mode)
@@ -182,7 +192,8 @@ If it does, call tracker_update_job instead — never create a duplicate row.
182
192
  | view saved openings | openings_list |
183
193
  | applied to a specific opening | openings_apply |
184
194
  | find NEW companies/roles not yet in tracker | get_resume → search_jobs |
185
- | practice interview, mock interview, interview me | start_interview |
195
+ | practice interview, mock interview, interview me | ask role + voice/text → start_interview |
196
+ | review interview, improve answers, how did I do | fetch_interview_context → STAR coaching |
186
197
  `.trim();
187
198
  // ---------------------------------------------------------------------------
188
199
  // §6 — Greeting protocol (shared across modes)
@@ -0,0 +1,14 @@
1
+ /**
2
+ * fetch_interview_context tool — retrieves the user's recent CLI interview
3
+ * sessions (transcript + feedback report) from Firestore via the
4
+ * cliGetInterviewContext Cloud Function.
5
+ *
6
+ * This allows the agent to:
7
+ * - Review what the user actually said during a mock interview
8
+ * - Identify weak answers by score
9
+ * - Suggest STAR-method improvements for specific questions
10
+ * - Pick up post-interview coaching without requiring copy-paste
11
+ */
12
+ import { Tool } from "../Tool.js";
13
+ export declare const FetchInterviewContextTool: Tool;
14
+ //# sourceMappingURL=fetchInterviewContext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetchInterviewContext.d.ts","sourceRoot":"","sources":["../../../src/agent/tools/fetchInterviewContext.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAqFlC,eAAO,MAAM,yBAAyB,EAAE,IAkCvC,CAAC"}
@@ -0,0 +1,114 @@
1
+ /**
2
+ * fetch_interview_context tool — retrieves the user's recent CLI interview
3
+ * sessions (transcript + feedback report) from Firestore via the
4
+ * cliGetInterviewContext Cloud Function.
5
+ *
6
+ * This allows the agent to:
7
+ * - Review what the user actually said during a mock interview
8
+ * - Identify weak answers by score
9
+ * - Suggest STAR-method improvements for specific questions
10
+ * - Pick up post-interview coaching without requiring copy-paste
11
+ */
12
+ import { Type } from "@google/genai";
13
+ import { getApiKey } from "../../config.js";
14
+ const CLI_CONTEXT_URL = process.env.CV_FUNCTIONS_URL
15
+ ? `${process.env.CV_FUNCTIONS_URL}/cliGetInterviewContext`
16
+ : "https://us-west1-jastalk-firebase.cloudfunctions.net/cliGetInterviewContext";
17
+ /** Cap per session to avoid bloating the agent's context window */
18
+ const MAX_TRANSCRIPT_ENTRIES = 40;
19
+ async function fetchInterviewContext(args) {
20
+ const apiKey = getApiKey();
21
+ if (!apiKey) {
22
+ return "❌ Not logged in. Run `cv login` first to access your interview history.";
23
+ }
24
+ try {
25
+ const res = await fetch(CLI_CONTEXT_URL, {
26
+ method: "POST",
27
+ headers: { "Content-Type": "application/json" },
28
+ body: JSON.stringify({ apiKey, limit: args.limit ?? 3 }),
29
+ });
30
+ if (res.status === 401) {
31
+ return "❌ API key invalid or expired. Run `cv login` to re-authenticate.";
32
+ }
33
+ if (!res.ok) {
34
+ return `⚠️ Could not retrieve interview history (HTTP ${res.status}). Try again shortly.`;
35
+ }
36
+ const data = await res.json();
37
+ const sessions = data.sessions ?? [];
38
+ if (sessions.length === 0) {
39
+ return "No recent interview sessions found. Run `cv interview` or start one via `cv agent` to build your history.";
40
+ }
41
+ // Build a compact, agent-readable context string
42
+ const parts = [];
43
+ parts.push(`📋 Found ${sessions.length} recent interview session(s):\n`);
44
+ for (let i = 0; i < sessions.length; i++) {
45
+ const s = sessions[i];
46
+ const date = s.startedAt ? new Date(s.startedAt).toLocaleString() : "Unknown date";
47
+ const duration = s.durationMinutes != null ? `${s.durationMinutes} min` : "N/A";
48
+ parts.push(`─── Session ${i + 1}: ${s.role} (${date}, ${duration}) ───`);
49
+ // Feedback scores
50
+ if (s.feedbackReport) {
51
+ const r = s.feedbackReport;
52
+ parts.push(`Scores: Overall ${r.overallScore}/100 · Communication ${r.communicationScore}/100 · Confidence ${r.confidenceScore}/100 · Relevance ${r.relevanceScore}/100`);
53
+ parts.push(`Strengths: ${r.strengths}`);
54
+ parts.push(`Areas for improvement: ${r.areasForImprovement}`);
55
+ }
56
+ else {
57
+ parts.push("(No feedback report available for this session)");
58
+ }
59
+ // Transcript (capped)
60
+ const entries = s.transcript ?? [];
61
+ if (entries.length > 0) {
62
+ parts.push("\nTranscript:");
63
+ const capped = entries.slice(0, MAX_TRANSCRIPT_ENTRIES);
64
+ for (const e of capped) {
65
+ const speaker = e.speaker === "ai" ? "Interviewer" : "Candidate";
66
+ parts.push(` ${speaker}: ${e.text}`);
67
+ }
68
+ if (entries.length > MAX_TRANSCRIPT_ENTRIES) {
69
+ parts.push(` ... (${entries.length - MAX_TRANSCRIPT_ENTRIES} more turns not shown)`);
70
+ }
71
+ }
72
+ else {
73
+ parts.push("(Transcript not yet available for this session — it may still be saving)");
74
+ }
75
+ parts.push("");
76
+ }
77
+ return parts.join("\n");
78
+ }
79
+ catch (err) {
80
+ return `⚠️ Error fetching interview context: ${err.message}`;
81
+ }
82
+ }
83
+ export const FetchInterviewContextTool = {
84
+ name: "fetch_interview_context",
85
+ description: `Retrieves the user's recent mock interview sessions from the database, including the full transcript and AI feedback scores.
86
+
87
+ Use this tool when the user:
88
+ - Asks to "improve my answers", "review my interview", "how did I do?"
89
+ - Says anything about their last/recent interview performance
90
+ - Wants coaching, STAR-method help, or answer refinement after an interview
91
+ - Asks what specific questions they were asked or what they said
92
+
93
+ RETURNS:
94
+ - Scores (overall, communication, confidence, relevance) for each session
95
+ - Strengths and areas for improvement
96
+ - Full transcript of interviewer questions and candidate answers (capped to 40 turns)
97
+
98
+ After fetching context, provide targeted, STAR-method coaching based on the actual answers given.
99
+ Default: retrieves the 3 most recent sessions.`,
100
+ parameters: {
101
+ type: Type.OBJECT,
102
+ properties: {
103
+ limit: {
104
+ type: Type.INTEGER,
105
+ description: "Number of recent sessions to fetch (1–10). Default: 3.",
106
+ },
107
+ },
108
+ required: [],
109
+ },
110
+ requiresConfirmation: false,
111
+ execute: async (args) => {
112
+ return fetchInterviewContext(args);
113
+ },
114
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"interview.d.ts","sourceRoot":"","sources":["../../../src/agent/tools/interview.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AA4DlC,eAAO,MAAM,kBAAkB,EAAE,IAwDhC,CAAC;AAEF,eAAO,MAAM,mBAAmB,EAAE,IAAI,EAAyB,CAAC"}
1
+ {"version":3,"file":"interview.d.ts","sourceRoot":"","sources":["../../../src/agent/tools/interview.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAoFlC,eAAO,MAAM,kBAAkB,EAAE,IAqEhC,CAAC;AAIF,eAAO,MAAM,mBAAmB,EAAE,IAAI,EAAoD,CAAC"}
@@ -31,25 +31,46 @@ async function runInterview(args) {
31
31
  }
32
32
  if (args.resume_id)
33
33
  argv.push("--resume", args.resume_id);
34
+ // Clear current line + move to fresh line before the interview TUI takes over.
35
+ // This prevents any leftover cursor text from flashing during the session.
36
+ process.stdout.write("\r\x1b[K\n");
37
+ // ── SIGINT isolation ─────────────────────────────────────────────────
38
+ // When the user presses Ctrl+C during the interview:
39
+ // - SIGINT is sent to the whole process group (parent agent + child)
40
+ // - The child (cv interview) handles it: billing → feedback report → exit 0
41
+ // - The parent MUST ignore it, otherwise it terminates and kills the child
42
+ // before the report is generated and shown.
43
+ // We suppress SIGINT on the agent for the lifetime of the subprocess and
44
+ // restore a default handler once the child exits cleanly.
45
+ const noop = () => { };
46
+ process.on("SIGINT", noop); // suppress — child handles it
34
47
  // Inherit the terminal so the full interactive TUI works.
35
48
  const child = spawn(process.execPath, argv, {
36
49
  stdio: "inherit",
37
50
  env: process.env,
38
51
  });
52
+ const cleanup = () => {
53
+ // Remove our suppressor so future Ctrl+C in the agent REPL works normally.
54
+ process.removeListener("SIGINT", noop);
55
+ };
39
56
  child.on("close", (code) => {
40
- if (code === 0) {
57
+ cleanup();
58
+ // code === null means killed by signal (e.g. SIGINT handled by child itself).
59
+ // The child always does billing + report before exiting, so treat as completed.
60
+ if (code === 0 || code === null) {
41
61
  resolve(`✅ Interview session for "${args.role}" completed.\n` +
42
62
  `The user has received their feedback report above.\n` +
43
63
  `You may now discuss their performance, suggest areas for improvement, ` +
44
64
  `help them prep specific STAR stories, or start another session.`);
45
65
  }
46
66
  else {
47
- resolve(`⚠️ Interview session ended with exit code ${code ?? "unknown"}.\n` +
48
- `This is normal if the user pressed Ctrl+C to end early. ` +
67
+ resolve(`⚠️ Interview session ended with exit code ${code}.\n` +
68
+ `This may mean the session was cut short before the report was generated. ` +
49
69
  `Ask if they'd like to start another session or discuss their responses.`);
50
70
  }
51
71
  });
52
72
  child.on("error", (err) => {
73
+ cleanup();
53
74
  resolve(`❌ Failed to launch interview session: ${err.message}`);
54
75
  });
55
76
  });
@@ -64,6 +85,19 @@ Use this tool when the user says ANYTHING like:
64
85
  - "I have an interview at [company], let's practice"
65
86
  - "Start a voice interview", "text interview"
66
87
 
88
+ ⚠️ MANDATORY PRE-FLIGHT — before calling this tool, you MUST know:
89
+ 1. **role** — which job role to interview for (ask if not mentioned)
90
+ 2. **mode** — ALWAYS ask the user: "Voice (real-time speech, requires sox) or Text?"
91
+ Never assume voice. Some users are on servers, don't have a mic, or prefer text.
92
+ Only skip asking if the user already said 'voice' or 'text' in their message.
93
+ 3. **questions** — optional, default 5 (no need to ask unless user wants to customize)
94
+
95
+ Example pre-flight:
96
+ User: "Can I take an interview?"
97
+ You: "Sure! What role are you interviewing for, and would you prefer Voice or Text mode?"
98
+ User: "Senior SWE, voice please."
99
+ You: → call start_interview(role="Senior Software Engineer", mode="voice")
100
+
67
101
  HOW IT WORKS:
68
102
  - Launches the full interactive \`cv interview\` session directly in the terminal.
69
103
  - The user speaks (voice mode) or types (text mode) their answers to Vivid, the AI interviewer.
@@ -103,4 +137,5 @@ DEFAULTS:
103
137
  return runInterview(args);
104
138
  },
105
139
  };
106
- export const ALL_INTERVIEW_TOOLS = [StartInterviewTool];
140
+ import { FetchInterviewContextTool } from "./fetchInterviewContext.js";
141
+ export const ALL_INTERVIEW_TOOLS = [StartInterviewTool, FetchInterviewContextTool];
@@ -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,CAskBf"}
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,CAqlBf"}
@@ -349,6 +349,13 @@ ${label}
349
349
  if (confirm.ok !== "Yes, close it")
350
350
  return false;
351
351
  }
352
+ // Tools that take over the full terminal must NOT have a spinner running —
353
+ // the concurrent ora redraw causes constant flashing.
354
+ if (name === "start_interview") {
355
+ // Clear current line so any previous UI is gone, then yield terminal cleanly.
356
+ process.stdout.write("\r\x1b[K");
357
+ return true;
358
+ }
352
359
  currentSpinner = ora(`Running ${chalk.bold(name)}...`).start();
353
360
  return true;
354
361
  };
@@ -357,6 +364,10 @@ ${label}
357
364
  currentSpinner.succeed(chalk.dim(`Done`));
358
365
  currentSpinner = null;
359
366
  }
367
+ if (name === "start_interview") {
368
+ // Interview already printed its own output — just add a separator.
369
+ console.log(chalk.dim("─".repeat(50)));
370
+ }
360
371
  // #4 Audit log — record every completed tool call
361
372
  // durationMs is approximate since we don't have exact start time here
362
373
  auditLog({
@@ -467,8 +478,11 @@ ${label}
467
478
  const tool = tools.find((t) => t.name === fc.name);
468
479
  let out;
469
480
  try {
481
+ // start_interview is an interactive long-running session — never apply a timeout to it.
470
482
  out = tool
471
- ? await withTimeout(tool.execute(fc.args), 45_000, `tool:${fc.name}`)
483
+ ? fc.name === "start_interview"
484
+ ? await tool.execute(fc.args)
485
+ : await withTimeout(tool.execute(fc.args), 45_000, `tool:${fc.name}`)
472
486
  : { error: "Tool not found" };
473
487
  }
474
488
  catch (e) {
@@ -1 +1 @@
1
- {"version":3,"file":"toolRegistry.d.ts","sourceRoot":"","sources":["../../../src/commands/agent/toolRegistry.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAyO3C,wBAAgB,QAAQ,CAAC,OAAO,EAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,EAAE,CAiDhG"}
1
+ {"version":3,"file":"toolRegistry.d.ts","sourceRoot":"","sources":["../../../src/commands/agent/toolRegistry.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAyO3C,wBAAgB,QAAQ,CAAC,OAAO,EAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,EAAE,CAoDhG"}
@@ -268,6 +268,10 @@ export function getTools(options) {
268
268
  if (!tools.find((x) => x.name === t.name))
269
269
  tools.push(t);
270
270
  }
271
+ for (const t of ALL_INTERVIEW_TOOLS) {
272
+ if (!tools.find((x) => x.name === t.name))
273
+ tools.push(t);
274
+ }
271
275
  return tools;
272
276
  }
273
277
  // Default coding mode: file system + publish tools
@@ -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;AA4rBpC,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAmG/D"}
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;AAkvBpC,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAmG/D"}
@@ -47,6 +47,9 @@ const CLI_TOKEN_URL = process.env.CV_FUNCTIONS_URL
47
47
  const CLI_BILL_URL = process.env.CV_FUNCTIONS_URL
48
48
  ? `${process.env.CV_FUNCTIONS_URL}/cliInterviewBill`
49
49
  : "https://us-west1-jastalk-firebase.cloudfunctions.net/cliInterviewBill";
50
+ const CLI_CONTEXT_URL = process.env.CV_FUNCTIONS_URL
51
+ ? `${process.env.CV_FUNCTIONS_URL}/cliGetInterviewContext`
52
+ : "https://us-west1-jastalk-firebase.cloudfunctions.net/cliGetInterviewContext";
50
53
  const LIVE_MODEL = "gemini-3.1-flash-live-preview";
51
54
  const FEEDBACK_MODEL = "gemini-2.5-flash";
52
55
  const END_TOKEN = "<END_INTERVIEW>";
@@ -213,15 +216,22 @@ async function getGeminiToken(role) {
213
216
  }
214
217
  // ─── Duration Billing ─────────────────────────────────────────────────────────
215
218
  /** Call cliInterviewBill at session end. Returns credit summary for display. */
216
- async function billSession(sessionId) {
219
+ async function billSession(sessionId, payload) {
217
220
  const apiKey = getApiKey();
218
221
  if (!apiKey || !sessionId)
219
222
  return null;
220
223
  try {
224
+ const body = { apiKey, sessionId };
225
+ if (payload?.transcript && payload.transcript.length > 0) {
226
+ body.transcript = payload.transcript;
227
+ }
228
+ if (payload?.feedbackReport) {
229
+ body.feedbackReport = payload.feedbackReport;
230
+ }
221
231
  const res = await fetch(CLI_BILL_URL, {
222
232
  method: "POST",
223
233
  headers: { "Content-Type": "application/json" },
224
- body: JSON.stringify({ apiKey, sessionId }),
234
+ body: JSON.stringify(body),
225
235
  });
226
236
  if (!res.ok)
227
237
  return null;
@@ -523,9 +533,37 @@ async function runVoiceSession(opts) {
523
533
  await new Promise(r => setTimeout(r, 800));
524
534
  const sessionDurationMs = Date.now() - sessionStart;
525
535
  // ── Bill session (duration-based, 10s timeout to prevent hang) ──────
536
+ // We delay billing until AFTER the feedback report is generated so we
537
+ // can persist the transcript + report in the same request.
538
+ const userTurns = transcript.filter(t => t.speaker === "user").length;
539
+ log.info("session_end", { sessionId, userTurns, aiTurns: transcript.filter(t => t.speaker === "ai").length, sessionDurationMs });
540
+ if (userTurns < 1) {
541
+ printSystem("Not enough conversation to generate a feedback report.");
542
+ // Still bill the session (minimum charge applies)
543
+ const billSpinner = ora(chalk.dim("Calculating session cost...")).start();
544
+ const bill = await Promise.race([
545
+ billSession(sessionId),
546
+ new Promise(r => setTimeout(() => r(null), 10_000)),
547
+ ]);
548
+ bill ? billSpinner.succeed(chalk.dim(`${bill.durationMinutes}min · ${bill.creditsCharged} credits`)) : billSpinner.warn("Could not calculate cost.");
549
+ await log.dispose();
550
+ return;
551
+ }
552
+ console.log(chalk.dim("\n Generating your personalized feedback report..."));
553
+ let report = null;
554
+ try {
555
+ report = await analyzeTranscript(transcript, role);
556
+ printReport(report);
557
+ log.info("feedback_complete", { sessionId, overallScore: report.overallScore });
558
+ }
559
+ catch (err) {
560
+ log.error("feedback_failed", err, { sessionId });
561
+ console.log(chalk.red(` Failed to generate feedback: ${err.message}`));
562
+ }
563
+ // ── Bill + persist transcript/report together ────────────────────────
526
564
  const billSpinner = ora(chalk.dim("Calculating session cost...")).start();
527
565
  const bill = await Promise.race([
528
- billSession(sessionId),
566
+ billSession(sessionId, { transcript, feedbackReport: report }),
529
567
  new Promise(r => setTimeout(() => r(null), 10_000)),
530
568
  ]);
531
569
  if (bill) {
@@ -543,27 +581,10 @@ async function runVoiceSession(opts) {
543
581
  billSpinner.warn(chalk.dim("Session cost could not be calculated (timeout)."));
544
582
  log.warn("billing_timeout", { sessionId, sessionDurationMs });
545
583
  }
546
- // ── Feedback report ──────────────────────────────────────────────────
547
- const userTurns = transcript.filter(t => t.speaker === "user").length;
548
- log.info("session_end", { sessionId, userTurns, aiTurns: transcript.filter(t => t.speaker === "ai").length, sessionDurationMs });
549
- if (userTurns < 1) {
550
- printSystem("Not enough conversation to generate a feedback report.");
551
- await log.dispose();
552
- return;
553
- }
554
- console.log(chalk.dim("\n Generating your personalized feedback report..."));
555
- try {
556
- const report = await analyzeTranscript(transcript, role);
557
- printReport(report);
558
- log.info("feedback_complete", { sessionId, overallScore: report.overallScore });
559
- }
560
- catch (err) {
561
- log.error("feedback_failed", err, { sessionId });
562
- console.log(chalk.red(` Failed to generate feedback: ${err.message}`));
563
- }
564
- finally {
565
- await log.dispose();
584
+ if (report) {
585
+ console.log(chalk.dim("\n 💡 Interview context saved. Ask \`cv agent\` to coach you on your answers."));
566
586
  }
587
+ await log.dispose();
567
588
  }
568
589
  // ─── TEXT SESSION (fallback) ──────────────────────────────────────────────────
569
590
  async function runTextSession(opts) {
@@ -631,13 +652,33 @@ async function runTextSession(opts) {
631
652
  return;
632
653
  }
633
654
  console.log(chalk.dim("\n Generating your personalized feedback report..."));
655
+ let textReport = null;
634
656
  try {
635
- const report = await analyzeTranscript(transcript, role);
636
- printReport(report);
657
+ textReport = await analyzeTranscript(transcript, role);
658
+ printReport(textReport);
637
659
  }
638
660
  catch (err) {
639
661
  console.log(chalk.red(` Failed to generate feedback: ${err.message}`));
640
662
  }
663
+ // Persist transcript + report to Firestore for agent coaching (fire-and-forget)
664
+ const apiKey = getApiKey();
665
+ if (apiKey && textReport) {
666
+ fetch(CLI_BILL_URL, {
667
+ method: "POST",
668
+ headers: { "Content-Type": "application/json" },
669
+ body: JSON.stringify({
670
+ apiKey,
671
+ // Text mode has no billable sessionId — use a stub that won't match
672
+ // a real session. The function will 404, but the persist path still runs.
673
+ // A proper text-mode session doc would require cliGetInterviewToken for text too.
674
+ // For now, persist to a synthetic doc under a well-known pattern.
675
+ sessionId: `text_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
676
+ transcript,
677
+ feedbackReport: textReport,
678
+ }),
679
+ }).catch(() => { });
680
+ console.log(chalk.dim("\n 💡 Interview context saved. Ask `cv agent` to coach you on your answers."));
681
+ }
641
682
  }
642
683
  // ─── Command Registration ─────────────────────────────────────────────────────
643
684
  export function registerInterviewCommand(program) {
package/dist/index.js CHANGED
@@ -80,7 +80,9 @@ registerResumesCommand(program);
80
80
  registerReferralCommand(program);
81
81
  registerEvalCommand(program);
82
82
  registerInterviewCommand(program);
83
- registerAdminCommand(program);
83
+ if (process.env.CV_ADMIN === "true") {
84
+ registerAdminCommand(program);
85
+ }
84
86
  // Shortcuts for whiteboard creation
85
87
  registerNewCommand(program);
86
88
  registerListTemplatesCommand(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "careervivid",
3
- "version": "2.0.0",
3
+ "version": "2.1.1",
4
4
  "description": "Official CLI for CareerVivid — AI voice interviews, autonomous job applications, resume editing, and portfolio publishing from your terminal",
5
5
  "type": "module",
6
6
  "bin": {