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 +10 -1
- package/dist/agent/instructions.d.ts.map +1 -1
- package/dist/agent/instructions.js +12 -1
- package/dist/agent/tools/fetchInterviewContext.d.ts +14 -0
- package/dist/agent/tools/fetchInterviewContext.d.ts.map +1 -0
- package/dist/agent/tools/fetchInterviewContext.js +114 -0
- package/dist/agent/tools/interview.d.ts.map +1 -1
- package/dist/agent/tools/interview.js +39 -4
- package/dist/commands/agent/repl.d.ts.map +1 -1
- package/dist/commands/agent/repl.js +15 -1
- package/dist/commands/agent/toolRegistry.d.ts.map +1 -1
- package/dist/commands/agent/toolRegistry.js +4 -0
- package/dist/commands/interview.d.ts.map +1 -1
- package/dist/commands/interview.js +66 -25
- package/dist/index.js +3 -1
- 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)
|
|
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,
|
|
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;
|
|
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
|
-
|
|
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
|
|
48
|
-
`This
|
|
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
|
-
|
|
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,
|
|
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
|
-
?
|
|
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,
|
|
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;
|
|
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(
|
|
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
|
-
|
|
547
|
-
|
|
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
|
-
|
|
636
|
-
printReport(
|
|
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
|
-
|
|
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