careervivid 1.12.50 → 2.0.0
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 +85 -19
- package/dist/agent/instructions.d.ts.map +1 -1
- package/dist/agent/instructions.js +10 -0
- package/dist/agent/tools/interview.d.ts +13 -0
- package/dist/agent/tools/interview.d.ts.map +1 -0
- package/dist/agent/tools/interview.js +106 -0
- package/dist/commands/admin.d.ts +18 -0
- package/dist/commands/admin.d.ts.map +1 -0
- package/dist/commands/admin.js +187 -0
- package/dist/commands/agent/toolRegistry.d.ts.map +1 -1
- package/dist/commands/agent/toolRegistry.js +6 -1
- package/dist/commands/interview.d.ts.map +1 -1
- package/dist/commands/interview.js +54 -6
- package/dist/index.js +2 -0
- package/dist/lib/logger.d.ts +108 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/logger.js +198 -0
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# careervivid · CLI
|
|
2
2
|
|
|
3
|
-
> **Your AI-powered career terminal — autonomous job applications, resume editing, job pipeline tracking, and portfolio publishing from the command line.**
|
|
3
|
+
> **Your AI-powered career terminal — voice mock interviews, autonomous job applications, resume editing, job pipeline tracking, and portfolio publishing from the command line.**
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/careervivid)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](https://nodejs.org)
|
|
8
|
+
[](https://www.npmjs.com/package/careervivid)
|
|
8
9
|
|
|
9
10
|
---
|
|
10
11
|
|
|
@@ -12,6 +13,7 @@
|
|
|
12
13
|
|
|
13
14
|
- [Quick Start](#quick-start)
|
|
14
15
|
- [Commands](#commands)
|
|
16
|
+
- [cv interview](#cv-interview) 🎙 **AI Voice Interview**
|
|
15
17
|
- [cv agent](#cv-agent) ⭐ **AI Agent**
|
|
16
18
|
- [cv agent --jobs](#cv-agent---jobs) 🤖 **Autonomous Job Applications**
|
|
17
19
|
- [cv agent --resume](#cv-agent---resume) 📄 **Resume CRUD**
|
|
@@ -40,18 +42,20 @@ npm install -g careervivid
|
|
|
40
42
|
|
|
41
43
|
# 2. Log in and get your free API key
|
|
42
44
|
cv login
|
|
43
|
-
# → opens careervivid.app
|
|
44
|
-
# →
|
|
45
|
-
|
|
45
|
+
# → opens careervivid.app in your browser
|
|
46
|
+
# → authenticate once, API key is saved automatically
|
|
47
|
+
|
|
48
|
+
# 3. Practice an interview with voice AI
|
|
49
|
+
cv interview
|
|
46
50
|
|
|
47
|
-
#
|
|
51
|
+
# 4. Start the AI agent
|
|
48
52
|
cv agent
|
|
49
53
|
|
|
50
|
-
#
|
|
54
|
+
# 5. Or jump straight into job-hunting mode
|
|
51
55
|
cv agent --jobs
|
|
52
56
|
```
|
|
53
57
|
|
|
54
|
-
> **Free tier includes
|
|
58
|
+
> **Free tier includes 10 AI credits/month** — no credit card required.
|
|
55
59
|
|
|
56
60
|
---
|
|
57
61
|
|
|
@@ -59,6 +63,50 @@ cv agent --jobs
|
|
|
59
63
|
|
|
60
64
|
---
|
|
61
65
|
|
|
66
|
+
### `cv interview`
|
|
67
|
+
|
|
68
|
+
**AI-powered voice mock interview** using the Gemini Live API. Vivid, your AI interviewer, conducts a real-time spoken interview tailored to the role you specify. After the session, you receive an auto-generated feedback report with scores and actionable improvement tips.
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
cv interview # interactive role prompt, voice mode
|
|
72
|
+
cv interview --role "Senior Software Engineer"
|
|
73
|
+
cv interview --role "Product Manager" --text # text-only fallback (no mic needed)
|
|
74
|
+
cv interview --role "SDE" --resume <id> # load your resume for context
|
|
75
|
+
cv interview --questions 7 # custom question count (default 5)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Requirements (voice mode):**
|
|
79
|
+
- `sox` for audio I/O — install once:
|
|
80
|
+
```bash
|
|
81
|
+
# macOS
|
|
82
|
+
brew install sox
|
|
83
|
+
# Ubuntu / Debian
|
|
84
|
+
sudo apt install sox
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**How it works:**
|
|
88
|
+
|
|
89
|
+
1. Vivid generates tailored interview questions using your role (and resume, if provided)
|
|
90
|
+
2. A real-time voice session opens — speak your answers naturally
|
|
91
|
+
3. Vivid asks follow-up questions and adapts to your responses
|
|
92
|
+
4. Press **Ctrl+C** at any time to end
|
|
93
|
+
5. A structured feedback report is generated covering:
|
|
94
|
+
- Overall, communication, confidence, and relevance scores (0–100)
|
|
95
|
+
- Specific strengths and areas for improvement
|
|
96
|
+
|
|
97
|
+
**Credit cost:** **2 credits/minute** (minimum 2, maximum 60 per session)
|
|
98
|
+
|
|
99
|
+
| Session length | Credits |
|
|
100
|
+
|---|---|
|
|
101
|
+
| < 1 min | 2 |
|
|
102
|
+
| 5 min | 10 |
|
|
103
|
+
| 10 min | 20 |
|
|
104
|
+
| 30 min+ | 60 (cap) |
|
|
105
|
+
|
|
106
|
+
> Credits are only charged after the session ends — never upfront.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
62
110
|
### `cv agent`
|
|
63
111
|
|
|
64
112
|
An **autonomous AI agent** that runs interactively in your terminal. Choose from CareerVivid Cloud (credits deducted from your account) or Bring Your Own API Key.
|
|
@@ -294,7 +342,7 @@ CV_API_KEY=cv_live_YOUR_KEY_HERE cv publish article.md
|
|
|
294
342
|
|
|
295
343
|
### `cv login`
|
|
296
344
|
|
|
297
|
-
Open the CareerVivid sign-in page and
|
|
345
|
+
Open the CareerVivid sign-in page and automatically save your API key.
|
|
298
346
|
|
|
299
347
|
```bash
|
|
300
348
|
cv login
|
|
@@ -336,17 +384,20 @@ cv config set llmModel gpt-4o
|
|
|
336
384
|
|
|
337
385
|
| Plan | Credits / Month | Price |
|
|
338
386
|
|---|---|---|
|
|
339
|
-
| **Free** |
|
|
387
|
+
| **Free** | 10 credits | $0 |
|
|
340
388
|
| **Pro** | 1,000 credits | Paid |
|
|
341
389
|
| **Max** | 10,000 credits | Paid |
|
|
342
390
|
|
|
343
|
-
**Credit costs
|
|
391
|
+
**Credit costs by feature:**
|
|
344
392
|
|
|
345
|
-
|
|
|
393
|
+
| Feature | Credits |
|
|
346
394
|
|---|---|
|
|
347
|
-
| `
|
|
348
|
-
| `gemini-
|
|
395
|
+
| `cv interview` (voice) | 2 credits/min (min 2, max 60) |
|
|
396
|
+
| `gemini-3.1-flash-lite-preview` agent turn | 0.5 cr |
|
|
397
|
+
| `gemini-2.5-flash` agent turn | 1 cr |
|
|
349
398
|
| `gemini-3.1-pro-preview` (`--pro`) | 2 cr |
|
|
399
|
+
| Resume tailor | 2 cr |
|
|
400
|
+
| Job evaluation | 3 cr |
|
|
350
401
|
|
|
351
402
|
> **Bring Your Own Key:** Using `--provider openai` (or any non-CareerVivid provider) deducts **zero credits** — you pay your provider directly.
|
|
352
403
|
|
|
@@ -462,12 +513,13 @@ echo "# My Architecture\n\nExplains the new service..." \
|
|
|
462
513
|
You have access to the `cv` CLI tool.
|
|
463
514
|
|
|
464
515
|
Available commands:
|
|
465
|
-
- cv
|
|
466
|
-
- cv
|
|
467
|
-
- cv agent --
|
|
468
|
-
- cv jobs
|
|
469
|
-
- cv jobs
|
|
470
|
-
- cv jobs
|
|
516
|
+
- cv interview AI voice interview (requires sox)
|
|
517
|
+
- cv publish <file> Publish an article (private by default, use --public to share)
|
|
518
|
+
- cv agent --resume Resume CRUD: read, update fields, tailor
|
|
519
|
+
- cv agent --jobs Job hunting + autonomous apply
|
|
520
|
+
- cv jobs hunt --role "..." AI job search
|
|
521
|
+
- cv jobs apply <url> Autonomous form filling (stops before submit)
|
|
522
|
+
- cv jobs list View job tracker
|
|
471
523
|
|
|
472
524
|
Rules:
|
|
473
525
|
1. Never include real API keys in published content.
|
|
@@ -500,6 +552,20 @@ cv auth check
|
|
|
500
552
|
CV_API_KEY=cv_live_YOUR_KEY cv publish article.md
|
|
501
553
|
```
|
|
502
554
|
|
|
555
|
+
**`cv interview` — no audio / can't hear Vivid**
|
|
556
|
+
```bash
|
|
557
|
+
# Install sox first:
|
|
558
|
+
brew install sox # macOS
|
|
559
|
+
sudo apt install sox # Ubuntu/Debian
|
|
560
|
+
|
|
561
|
+
# Then retry:
|
|
562
|
+
cv interview
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
**`cv interview` — hearing echo / Vivid repeating itself**
|
|
566
|
+
|
|
567
|
+
This can occur if your microphone picks up the speaker output. The CLI uses half-duplex mute suppression (mic is muted while Vivid is speaking). Use headphones for the best experience.
|
|
568
|
+
|
|
503
569
|
**`browser_sidecar.py not found`**
|
|
504
570
|
```bash
|
|
505
571
|
# browser-use is not set up. Follow the browser-use Setup section above.
|
|
@@ -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,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"}
|
|
@@ -127,6 +127,14 @@ These tools read/write the Firebase Kanban board at careervivid.app/job-tracker.
|
|
|
127
127
|
- **verify_url** — Verify a single link is alive before sharing it.
|
|
128
128
|
- **verify_job_urls** — Verify all URLs from a search_jobs result batch.
|
|
129
129
|
NEVER share a link without verifying it first.
|
|
130
|
+
|
|
131
|
+
### 🎙 AI Voice Interview
|
|
132
|
+
- **start_interview** ⭐ — Launch a live AI mock interview session directly in the terminal.
|
|
133
|
+
- Vivid (the AI interviewer) asks tailored questions based on the role and user's resume.
|
|
134
|
+
- User speaks answers (voice mode, requires sox) or types them (text mode).
|
|
135
|
+
- Feedback report auto-generated at the end with scores and improvement tips.
|
|
136
|
+
- **Credit cost:** 2 credits/minute (min 2, max 60). Text mode ~1 credit flat.
|
|
137
|
+
- Use when user says: "practice interview", "mock interview", "interview me for [role]", "I have an interview at [company]", etc.
|
|
130
138
|
`.trim();
|
|
131
139
|
// ---------------------------------------------------------------------------
|
|
132
140
|
// §5 — Autonomous execution harness (appended in --jobs mode)
|
|
@@ -174,6 +182,7 @@ If it does, call tracker_update_job instead — never create a duplicate row.
|
|
|
174
182
|
| view saved openings | openings_list |
|
|
175
183
|
| applied to a specific opening | openings_apply |
|
|
176
184
|
| find NEW companies/roles not yet in tracker | get_resume → search_jobs |
|
|
185
|
+
| practice interview, mock interview, interview me | start_interview |
|
|
177
186
|
`.trim();
|
|
178
187
|
// ---------------------------------------------------------------------------
|
|
179
188
|
// §6 — Greeting protocol (shared across modes)
|
|
@@ -189,6 +198,7 @@ When the user sends a generic greeting ("hey", "hi", "hello", "start"), respond
|
|
|
189
198
|
• 🔍 Search for job opportunities
|
|
190
199
|
• 📊 Check my job pipeline / tracker
|
|
191
200
|
• ✉️ Draft a cover letter or tailor my resume
|
|
201
|
+
• 🎙 Start an AI mock interview (voice or text)
|
|
192
202
|
• 📈 Get an overview of my job search progress
|
|
193
203
|
• 🗓️ Pick up where we left off
|
|
194
204
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* start_interview tool — launches a live `cv interview` session
|
|
3
|
+
* from within the agent REPL.
|
|
4
|
+
*
|
|
5
|
+
* The tool spawns the interview subprocess with the terminal attached
|
|
6
|
+
* (stdio: "inherit") so the user gets the full interactive voice/text
|
|
7
|
+
* experience without leaving the agent session. When the interview ends
|
|
8
|
+
* the agent picks up again and can discuss results, next steps, etc.
|
|
9
|
+
*/
|
|
10
|
+
import { Tool } from "../Tool.js";
|
|
11
|
+
export declare const StartInterviewTool: Tool;
|
|
12
|
+
export declare const ALL_INTERVIEW_TOOLS: Tool[];
|
|
13
|
+
//# sourceMappingURL=interview.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* start_interview tool — launches a live `cv interview` session
|
|
3
|
+
* from within the agent REPL.
|
|
4
|
+
*
|
|
5
|
+
* The tool spawns the interview subprocess with the terminal attached
|
|
6
|
+
* (stdio: "inherit") so the user gets the full interactive voice/text
|
|
7
|
+
* experience without leaving the agent session. When the interview ends
|
|
8
|
+
* the agent picks up again and can discuss results, next steps, etc.
|
|
9
|
+
*/
|
|
10
|
+
import { spawn } from "child_process";
|
|
11
|
+
import { Type } from "@google/genai";
|
|
12
|
+
/** Resolve the `cv` binary that is currently running this process. */
|
|
13
|
+
function getCvBin() {
|
|
14
|
+
// process.argv[1] is the entry-point JS file.
|
|
15
|
+
// When installed globally via npm, `cv` is a shell wrapper that calls
|
|
16
|
+
// the same binary — we can just re-use process.argv[0] (node) + argv[1].
|
|
17
|
+
// But the cleanest approach is to call the same node + script directly.
|
|
18
|
+
return process.argv[1]; // absolute path to dist/index.js
|
|
19
|
+
}
|
|
20
|
+
async function runInterview(args) {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
const argv = [
|
|
23
|
+
getCvBin(),
|
|
24
|
+
"interview",
|
|
25
|
+
"--role", args.role,
|
|
26
|
+
];
|
|
27
|
+
if (args.mode === "text")
|
|
28
|
+
argv.push("--text");
|
|
29
|
+
if (args.questions && args.questions > 0) {
|
|
30
|
+
argv.push("--questions", String(args.questions));
|
|
31
|
+
}
|
|
32
|
+
if (args.resume_id)
|
|
33
|
+
argv.push("--resume", args.resume_id);
|
|
34
|
+
// Inherit the terminal so the full interactive TUI works.
|
|
35
|
+
const child = spawn(process.execPath, argv, {
|
|
36
|
+
stdio: "inherit",
|
|
37
|
+
env: process.env,
|
|
38
|
+
});
|
|
39
|
+
child.on("close", (code) => {
|
|
40
|
+
if (code === 0) {
|
|
41
|
+
resolve(`✅ Interview session for "${args.role}" completed.\n` +
|
|
42
|
+
`The user has received their feedback report above.\n` +
|
|
43
|
+
`You may now discuss their performance, suggest areas for improvement, ` +
|
|
44
|
+
`help them prep specific STAR stories, or start another session.`);
|
|
45
|
+
}
|
|
46
|
+
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. ` +
|
|
49
|
+
`Ask if they'd like to start another session or discuss their responses.`);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
child.on("error", (err) => {
|
|
53
|
+
resolve(`❌ Failed to launch interview session: ${err.message}`);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
export const StartInterviewTool = {
|
|
58
|
+
name: "start_interview",
|
|
59
|
+
description: `Launch a live AI mock interview session for the user.
|
|
60
|
+
|
|
61
|
+
Use this tool when the user says ANYTHING like:
|
|
62
|
+
- "Start an interview", "practice interview", "mock interview"
|
|
63
|
+
- "Interview me for [role]", "prep me for [company] interview"
|
|
64
|
+
- "I have an interview at [company], let's practice"
|
|
65
|
+
- "Start a voice interview", "text interview"
|
|
66
|
+
|
|
67
|
+
HOW IT WORKS:
|
|
68
|
+
- Launches the full interactive \`cv interview\` session directly in the terminal.
|
|
69
|
+
- The user speaks (voice mode) or types (text mode) their answers to Vivid, the AI interviewer.
|
|
70
|
+
- At the end, a feedback report with scores and improvement tips is displayed automatically.
|
|
71
|
+
- You (the agent) can then discuss results, coach on weak areas, help build STAR stories, etc.
|
|
72
|
+
|
|
73
|
+
CREDIT COST: 2 credits/minute (minimum 2, capped at 60). Text mode uses ~1 credit flat.
|
|
74
|
+
|
|
75
|
+
DEFAULTS:
|
|
76
|
+
- mode defaults to "voice" (requires sox). Suggest "text" if user is on a server / no mic.
|
|
77
|
+
- questions defaults to 5.`,
|
|
78
|
+
parameters: {
|
|
79
|
+
type: Type.OBJECT,
|
|
80
|
+
properties: {
|
|
81
|
+
role: {
|
|
82
|
+
type: Type.STRING,
|
|
83
|
+
description: "The job role to interview for, e.g. 'Senior Software Engineer', 'Product Manager', 'Data Scientist'.",
|
|
84
|
+
},
|
|
85
|
+
mode: {
|
|
86
|
+
type: Type.STRING,
|
|
87
|
+
enum: ["voice", "text"],
|
|
88
|
+
description: "Interview mode. 'voice' = real-time speech (default, requires sox). 'text' = text-only fallback.",
|
|
89
|
+
},
|
|
90
|
+
questions: {
|
|
91
|
+
type: Type.INTEGER,
|
|
92
|
+
description: "Number of interview questions to generate (1–10). Default: 5.",
|
|
93
|
+
},
|
|
94
|
+
resume_id: {
|
|
95
|
+
type: Type.STRING,
|
|
96
|
+
description: "Optional. CareerVivid resume ID to load as context. The AI will tailor questions to the user's actual background.",
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
required: ["role"],
|
|
100
|
+
},
|
|
101
|
+
requiresConfirmation: false,
|
|
102
|
+
execute: async (args) => {
|
|
103
|
+
return runInterview(args);
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
export const ALL_INTERVIEW_TOOLS = [StartInterviewTool];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cv admin — Admin-only commands: log inspection, audit, diagnostics.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* cv admin logs Last 100 events across all features
|
|
6
|
+
* cv admin logs --feature interview Interview events only
|
|
7
|
+
* cv admin logs --feature interview --level error Errors only
|
|
8
|
+
* cv admin logs --uid <userId> Events for a specific user
|
|
9
|
+
* cv admin logs --since 2025-01-01 Events since a date
|
|
10
|
+
* cv admin logs --limit 50 Custom limit (max 500)
|
|
11
|
+
* cv admin logs --json Raw JSON output (pipe-friendly)
|
|
12
|
+
*
|
|
13
|
+
* Access: requires `role: "admin"` on the caller's Firestore user document.
|
|
14
|
+
* Feature logs are written by cli/src/lib/logger.ts and stored in Firestore cliLogs.
|
|
15
|
+
*/
|
|
16
|
+
import { Command } from "commander";
|
|
17
|
+
export declare function registerAdminCommand(program: Command): void;
|
|
18
|
+
//# sourceMappingURL=admin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin.d.ts","sourceRoot":"","sources":["../../src/commands/admin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA6FpC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAoH3D"}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cv admin — Admin-only commands: log inspection, audit, diagnostics.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* cv admin logs Last 100 events across all features
|
|
6
|
+
* cv admin logs --feature interview Interview events only
|
|
7
|
+
* cv admin logs --feature interview --level error Errors only
|
|
8
|
+
* cv admin logs --uid <userId> Events for a specific user
|
|
9
|
+
* cv admin logs --since 2025-01-01 Events since a date
|
|
10
|
+
* cv admin logs --limit 50 Custom limit (max 500)
|
|
11
|
+
* cv admin logs --json Raw JSON output (pipe-friendly)
|
|
12
|
+
*
|
|
13
|
+
* Access: requires `role: "admin"` on the caller's Firestore user document.
|
|
14
|
+
* Feature logs are written by cli/src/lib/logger.ts and stored in Firestore cliLogs.
|
|
15
|
+
*/
|
|
16
|
+
import chalk from "chalk";
|
|
17
|
+
import { getApiKey } from "../config.js";
|
|
18
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
19
|
+
const CLI_GET_LOGS_URL = process.env.CV_FUNCTIONS_URL
|
|
20
|
+
? `${process.env.CV_FUNCTIONS_URL}/cliGetLogs`
|
|
21
|
+
: "https://us-west1-jastalk-firebase.cloudfunctions.net/cliGetLogs";
|
|
22
|
+
// ─── Level styling ────────────────────────────────────────────────────────────
|
|
23
|
+
function levelBadge(level) {
|
|
24
|
+
switch (level) {
|
|
25
|
+
case "error": return chalk.bgRed.white.bold(" ERR ");
|
|
26
|
+
case "warn": return chalk.bgYellow.black.bold(" WRN ");
|
|
27
|
+
case "info": return chalk.bgBlue.white.bold(" INF ");
|
|
28
|
+
case "debug": return chalk.bgGray.white.bold(" DBG ");
|
|
29
|
+
default: return chalk.bgGray.white.bold(` ${level.toUpperCase().slice(0, 3)} `);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function featureBadge(feature) {
|
|
33
|
+
const colors = {
|
|
34
|
+
interview: (s) => chalk.hex("#4f46e5").bold(s),
|
|
35
|
+
agent: (s) => chalk.hex("#059669").bold(s),
|
|
36
|
+
resume: (s) => chalk.hex("#d97706").bold(s),
|
|
37
|
+
jobs: (s) => chalk.hex("#dc2626").bold(s),
|
|
38
|
+
};
|
|
39
|
+
const fn = colors[feature] ?? ((s) => chalk.gray.bold(s));
|
|
40
|
+
return fn(`[${feature}]`);
|
|
41
|
+
}
|
|
42
|
+
function dim(s) { return chalk.dim(s); }
|
|
43
|
+
// ─── Display helpers ──────────────────────────────────────────────────────────
|
|
44
|
+
function formatEvent(e, verbose) {
|
|
45
|
+
const ts = dim(new Date(e.clientTime ?? e.serverTime ?? 0).toISOString().replace("T", " ").slice(0, 19));
|
|
46
|
+
const badge = levelBadge(e.level ?? "info");
|
|
47
|
+
const feat = featureBadge(e.feature ?? "?");
|
|
48
|
+
const evt = chalk.white(e.event ?? "unknown");
|
|
49
|
+
const sid = e.sessionId ? dim(` sid:${String(e.sessionId).slice(0, 8)}…`) : "";
|
|
50
|
+
const ver = dim(` v${e.cliVersion ?? "?"}`);
|
|
51
|
+
console.log(` ${ts} ${badge} ${feat} ${evt}${sid}${ver}`);
|
|
52
|
+
if (e.errorMessage) {
|
|
53
|
+
console.log(` ${chalk.red("↳ " + e.errorMessage)}`);
|
|
54
|
+
if (verbose && e.errorStack) {
|
|
55
|
+
const stack = e.errorStack.split("\n").slice(1, 4).join("\n ");
|
|
56
|
+
console.log(chalk.red(` ${stack}`));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (verbose && e.metadata && Object.keys(e.metadata).length > 0) {
|
|
60
|
+
const json = JSON.stringify(e.metadata, null, 2)
|
|
61
|
+
.split("\n").map(l => ` ${chalk.dim(l)}`).join("\n");
|
|
62
|
+
console.log(json);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function printHeader(filters) {
|
|
66
|
+
console.log("\n" + chalk.bgHex("#4f46e5").white.bold(" CareerVivid — CLI Logs "));
|
|
67
|
+
const parts = [
|
|
68
|
+
filters.feature ? chalk.cyan(`feature=${filters.feature}`) : chalk.dim("feature=*"),
|
|
69
|
+
filters.level ? chalk.yellow(`level=${filters.level}`) : chalk.dim("level=*"),
|
|
70
|
+
filters.uid ? chalk.magenta(`uid=${filters.uid.slice(0, 8)}…`) : chalk.dim("uid=*"),
|
|
71
|
+
filters.since ? chalk.green(`since=${filters.since}`) : chalk.dim("since=all"),
|
|
72
|
+
chalk.dim(`limit=${filters.limit}`),
|
|
73
|
+
];
|
|
74
|
+
console.log(" " + parts.join(chalk.dim(" · ")));
|
|
75
|
+
console.log(chalk.dim(" " + "─".repeat(78)));
|
|
76
|
+
}
|
|
77
|
+
function printSummary(events) {
|
|
78
|
+
const counts = {};
|
|
79
|
+
for (const e of events)
|
|
80
|
+
counts[e.level ?? "info"] = (counts[e.level ?? "info"] || 0) + 1;
|
|
81
|
+
const parts = Object.entries(counts).map(([l, n]) => `${levelBadge(l)} ${n}`);
|
|
82
|
+
console.log(chalk.dim("\n " + "─".repeat(78)));
|
|
83
|
+
console.log(` ${chalk.bold("Total:")} ${events.length} events ${parts.join(" ")}`);
|
|
84
|
+
const errorCount = counts["error"] ?? 0;
|
|
85
|
+
if (errorCount > 0) {
|
|
86
|
+
console.log(chalk.red.bold(`\n ⚠ ${errorCount} error${errorCount > 1 ? "s" : ""} found. Run with --level error to filter.`));
|
|
87
|
+
}
|
|
88
|
+
console.log("");
|
|
89
|
+
}
|
|
90
|
+
// ─── Command registration ──────────────────────────────────────────────────────
|
|
91
|
+
export function registerAdminCommand(program) {
|
|
92
|
+
const admin = program
|
|
93
|
+
.command("admin", { hidden: true }) // hidden from public --help
|
|
94
|
+
.description("Admin-only commands — requires admin role");
|
|
95
|
+
admin
|
|
96
|
+
.command("logs")
|
|
97
|
+
.description("Fetch and inspect CLI event logs")
|
|
98
|
+
.option("-f, --feature <name>", "Filter by feature (interview, agent, resume, jobs, …)")
|
|
99
|
+
.option("-l, --level <level>", "Filter by level (error, warn, info)")
|
|
100
|
+
.option("--uid <userId>", "Filter by specific user UID")
|
|
101
|
+
.option("--since <date>", "Only show logs after this date (ISO format: 2025-01-01)")
|
|
102
|
+
.option("-n, --limit <n>", "Number of log batches to fetch (default 100, max 500)", "100")
|
|
103
|
+
.option("--verbose", "Show full metadata and stack traces")
|
|
104
|
+
.option("--json", "Output raw JSON (pipe-friendly)")
|
|
105
|
+
.addHelpText("after", `
|
|
106
|
+
Examples:
|
|
107
|
+
cv admin logs
|
|
108
|
+
cv admin logs --feature interview
|
|
109
|
+
cv admin logs --feature interview --level error
|
|
110
|
+
cv admin logs --level error --limit 50
|
|
111
|
+
cv admin logs --uid abc123def456
|
|
112
|
+
cv admin logs --since 2025-01-01
|
|
113
|
+
cv admin logs --feature interview --json | jq '.[] | select(.level=="error")'
|
|
114
|
+
`)
|
|
115
|
+
.action(async (opts) => {
|
|
116
|
+
const apiKey = getApiKey();
|
|
117
|
+
if (!apiKey) {
|
|
118
|
+
console.error(chalk.red("\n Not logged in. Run: cv login\n"));
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
const limit = Math.min(Math.max(parseInt(opts.limit, 10) || 100, 1), 500);
|
|
122
|
+
// ── Fetch logs ──────────────────────────────────────────────────
|
|
123
|
+
if (!opts.json) {
|
|
124
|
+
printHeader({ feature: opts.feature, level: opts.level, uid: opts.uid, since: opts.since, limit });
|
|
125
|
+
process.stdout.write(chalk.dim(" Fetching logs…\r"));
|
|
126
|
+
}
|
|
127
|
+
let result;
|
|
128
|
+
try {
|
|
129
|
+
const res = await fetch(CLI_GET_LOGS_URL, {
|
|
130
|
+
method: "POST",
|
|
131
|
+
headers: { "Content-Type": "application/json" },
|
|
132
|
+
body: JSON.stringify({
|
|
133
|
+
apiKey,
|
|
134
|
+
feature: opts.feature,
|
|
135
|
+
level: opts.level,
|
|
136
|
+
uid: opts.uid,
|
|
137
|
+
since: opts.since,
|
|
138
|
+
limit,
|
|
139
|
+
}),
|
|
140
|
+
});
|
|
141
|
+
result = await res.json();
|
|
142
|
+
if (res.status === 403) {
|
|
143
|
+
console.error(chalk.red("\n ✖ Admin access required.\n"));
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
if (!res.ok) {
|
|
147
|
+
throw new Error(result?.error ?? `HTTP ${res.status}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
console.error(chalk.red(`\n Failed to fetch logs: ${err.message}\n`));
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
// ── Flatten all events from all batches ─────────────────────────
|
|
155
|
+
const allEvents = [];
|
|
156
|
+
for (const batch of result.logs ?? []) {
|
|
157
|
+
for (const e of batch.events ?? []) {
|
|
158
|
+
allEvents.push({ ...e, _batchId: batch.batchId, _receivedAt: batch.receivedAt });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// ── Output ──────────────────────────────────────────────────────
|
|
162
|
+
if (opts.json) {
|
|
163
|
+
console.log(JSON.stringify(allEvents, null, 2));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (allEvents.length === 0) {
|
|
167
|
+
process.stdout.write(" \r");
|
|
168
|
+
console.log(chalk.dim(" No log events found with the current filters.\n"));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
process.stdout.write(" \r");
|
|
172
|
+
// Sort by clientTime ascending (oldest first for readability)
|
|
173
|
+
allEvents.sort((a, b) => {
|
|
174
|
+
const ta = new Date(a.clientTime || a._receivedAt || 0).getTime();
|
|
175
|
+
const tb = new Date(b.clientTime || b._receivedAt || 0).getTime();
|
|
176
|
+
return ta - tb;
|
|
177
|
+
});
|
|
178
|
+
for (const e of allEvents) {
|
|
179
|
+
formatEvent(e, !!opts.verbose);
|
|
180
|
+
}
|
|
181
|
+
printSummary(allEvents);
|
|
182
|
+
});
|
|
183
|
+
// Future subcommands can be added here:
|
|
184
|
+
// admin.command("sessions") — list interviewSessions
|
|
185
|
+
// admin.command("users") — user credit audit
|
|
186
|
+
// admin.command("billing") — billing reconciliation
|
|
187
|
+
}
|
|
@@ -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;
|
|
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"}
|
|
@@ -7,6 +7,7 @@ import { ALL_URL_VERIFIER_TOOLS } from "../../agent/tools/urlVerifier.js";
|
|
|
7
7
|
import { ALL_PORTFOLIO_TOOLS } from "../../agent/tools/portfolio.js";
|
|
8
8
|
import { ALL_COVERLETTER_TOOLS } from "../../agent/tools/coverLetter.js";
|
|
9
9
|
import { ALL_JOB_OPENINGS_TOOLS } from "../../agent/tools/jobOpenings.js";
|
|
10
|
+
import { ALL_INTERVIEW_TOOLS } from "../../agent/tools/interview.js";
|
|
10
11
|
import { publishSingleFile } from "../publish.js";
|
|
11
12
|
// ── Publish tools ─────────────────────────────────────────────────────────────
|
|
12
13
|
const PublishArticleTool = {
|
|
@@ -242,6 +243,10 @@ export function getTools(options) {
|
|
|
242
243
|
if (!tools.find((x) => x.name === t.name))
|
|
243
244
|
tools.push(t);
|
|
244
245
|
}
|
|
246
|
+
for (const t of ALL_INTERVIEW_TOOLS) {
|
|
247
|
+
if (!tools.find((x) => x.name === t.name))
|
|
248
|
+
tools.push(t);
|
|
249
|
+
}
|
|
245
250
|
return tools;
|
|
246
251
|
}
|
|
247
252
|
if (options.resume) {
|
|
@@ -266,5 +271,5 @@ export function getTools(options) {
|
|
|
266
271
|
return tools;
|
|
267
272
|
}
|
|
268
273
|
// Default coding mode: file system + publish tools
|
|
269
|
-
return [...ALL_CODING_TOOLS, PublishArticleTool, GenerateDiagramTool, ...ALL_COVERLETTER_TOOLS];
|
|
274
|
+
return [...ALL_CODING_TOOLS, PublishArticleTool, GenerateDiagramTool, ...ALL_COVERLETTER_TOOLS, ...ALL_INTERVIEW_TOOLS];
|
|
270
275
|
}
|
|
@@ -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;AA4rBpC,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAmG/D"}
|
|
@@ -20,9 +20,21 @@ import chalk from "chalk";
|
|
|
20
20
|
import readline from "readline";
|
|
21
21
|
import ora from "ora";
|
|
22
22
|
import { spawn } from "child_process";
|
|
23
|
+
import { readFileSync } from "fs";
|
|
24
|
+
import { join, dirname } from "path";
|
|
25
|
+
import { fileURLToPath } from "url";
|
|
23
26
|
import { GoogleGenAI, Modality } from "@google/genai";
|
|
24
27
|
import { getApiKey } from "../config.js";
|
|
25
28
|
import { isApiError, resumeGet } from "../api.js";
|
|
29
|
+
import { createLogger } from "../lib/logger.js";
|
|
30
|
+
/** Read CLI version from package.json (ESM-compatible) */
|
|
31
|
+
const __dirname_iv = dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
let _cliVersion = "unknown";
|
|
33
|
+
try {
|
|
34
|
+
_cliVersion = JSON.parse(readFileSync(join(__dirname_iv, "../../package.json"), "utf-8")).version ?? "unknown";
|
|
35
|
+
}
|
|
36
|
+
catch { /* ignore */ }
|
|
37
|
+
const CLI_VERSION = _cliVersion;
|
|
26
38
|
/** Strip ANSI escape codes for accurate string length measurement */
|
|
27
39
|
const stripAnsi = (s) => s.replace(/\x1B\[[0-9;]*m/g, "");
|
|
28
40
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
@@ -334,14 +346,24 @@ async function analyzeTranscript(transcript, role) {
|
|
|
334
346
|
async function runVoiceSession(opts) {
|
|
335
347
|
const { role, questions, resumeContext, soxPath } = opts;
|
|
336
348
|
printBanner(role, "voice");
|
|
349
|
+
// Create logger (sessionId not yet known — will be set after token vend)
|
|
350
|
+
const log = createLogger("interview", {
|
|
351
|
+
apiKey: getApiKey(),
|
|
352
|
+
version: CLI_VERSION,
|
|
353
|
+
});
|
|
354
|
+
const sessionStart = Date.now();
|
|
337
355
|
// Get Gemini token from Cloud Function
|
|
338
356
|
const connectSpinner = ora(chalk.dim("Connecting to Vivid...")).start();
|
|
339
357
|
let geminiKey;
|
|
340
358
|
let sessionId;
|
|
341
359
|
try {
|
|
342
360
|
({ geminiKey, sessionId } = await getGeminiToken(role));
|
|
361
|
+
log.setSessionId(sessionId);
|
|
362
|
+
log.info("session_start", { role, numQuestions: questions.length, mode: "voice", soxPath });
|
|
343
363
|
}
|
|
344
364
|
catch (err) {
|
|
365
|
+
log.error("token_vend_failed", err, { role });
|
|
366
|
+
await log.dispose();
|
|
345
367
|
connectSpinner.fail(chalk.red(err.message));
|
|
346
368
|
throw err;
|
|
347
369
|
}
|
|
@@ -456,12 +478,15 @@ async function runVoiceSession(opts) {
|
|
|
456
478
|
});
|
|
457
479
|
}
|
|
458
480
|
catch (err) {
|
|
481
|
+
log.error("live_connect_failed", err, { role, sessionId });
|
|
482
|
+
await log.dispose();
|
|
459
483
|
connectSpinner.fail(chalk.red(`Failed to connect: ${err.message}`));
|
|
460
484
|
micProc.kill();
|
|
461
485
|
speakerProc.kill();
|
|
462
486
|
throw err;
|
|
463
487
|
}
|
|
464
|
-
// ── Wait until interview ends or Ctrl+C
|
|
488
|
+
// ── Wait until interview ends or Ctrl+C (single-SIGINT guard) ───────
|
|
489
|
+
let shuttingDown = false;
|
|
465
490
|
await new Promise((resolve) => {
|
|
466
491
|
const check = setInterval(() => {
|
|
467
492
|
if (ended) {
|
|
@@ -469,12 +494,17 @@ async function runVoiceSession(opts) {
|
|
|
469
494
|
resolve();
|
|
470
495
|
}
|
|
471
496
|
}, 200);
|
|
472
|
-
|
|
497
|
+
const onSigInt = () => {
|
|
498
|
+
if (shuttingDown)
|
|
499
|
+
return; // ignore double Ctrl+C
|
|
500
|
+
shuttingDown = true;
|
|
473
501
|
clearInterval(check);
|
|
474
502
|
printSystem("Interview ended by user.");
|
|
503
|
+
log.info("session_interrupted", { role, sessionId, elapsedMs: Date.now() - sessionStart });
|
|
475
504
|
ended = true;
|
|
476
505
|
resolve();
|
|
477
|
-
}
|
|
506
|
+
};
|
|
507
|
+
process.once("SIGINT", onSigInt);
|
|
478
508
|
});
|
|
479
509
|
// ── Cleanup ──────────────────────────────────────────────────────────
|
|
480
510
|
try {
|
|
@@ -491,31 +521,49 @@ async function runVoiceSession(opts) {
|
|
|
491
521
|
catch { /* ignore */ }
|
|
492
522
|
// Wait a moment for final audio to drain
|
|
493
523
|
await new Promise(r => setTimeout(r, 800));
|
|
494
|
-
|
|
524
|
+
const sessionDurationMs = Date.now() - sessionStart;
|
|
525
|
+
// ── Bill session (duration-based, 10s timeout to prevent hang) ──────
|
|
495
526
|
const billSpinner = ora(chalk.dim("Calculating session cost...")).start();
|
|
496
|
-
const bill = await
|
|
527
|
+
const bill = await Promise.race([
|
|
528
|
+
billSession(sessionId),
|
|
529
|
+
new Promise(r => setTimeout(() => r(null), 10_000)),
|
|
530
|
+
]);
|
|
497
531
|
if (bill) {
|
|
498
532
|
billSpinner.succeed(chalk.dim(`Session: ${bill.durationMinutes}min · `) +
|
|
499
533
|
chalk.hex("#4f46e5").bold(`${bill.creditsCharged} credits used`) +
|
|
500
534
|
chalk.dim(` · ${bill.creditsRemaining} remaining`));
|
|
535
|
+
log.info("billing_complete", {
|
|
536
|
+
sessionId, durationMinutes: bill.durationMinutes,
|
|
537
|
+
creditsCharged: bill.creditsCharged, creditsRemaining: bill.creditsRemaining,
|
|
538
|
+
});
|
|
539
|
+
log.metric("credits_charged", bill.creditsCharged, { sessionId });
|
|
540
|
+
log.metric("session_duration_ms", sessionDurationMs, { sessionId });
|
|
501
541
|
}
|
|
502
542
|
else {
|
|
503
|
-
billSpinner.
|
|
543
|
+
billSpinner.warn(chalk.dim("Session cost could not be calculated (timeout)."));
|
|
544
|
+
log.warn("billing_timeout", { sessionId, sessionDurationMs });
|
|
504
545
|
}
|
|
505
546
|
// ── Feedback report ──────────────────────────────────────────────────
|
|
506
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 });
|
|
507
549
|
if (userTurns < 1) {
|
|
508
550
|
printSystem("Not enough conversation to generate a feedback report.");
|
|
551
|
+
await log.dispose();
|
|
509
552
|
return;
|
|
510
553
|
}
|
|
511
554
|
console.log(chalk.dim("\n Generating your personalized feedback report..."));
|
|
512
555
|
try {
|
|
513
556
|
const report = await analyzeTranscript(transcript, role);
|
|
514
557
|
printReport(report);
|
|
558
|
+
log.info("feedback_complete", { sessionId, overallScore: report.overallScore });
|
|
515
559
|
}
|
|
516
560
|
catch (err) {
|
|
561
|
+
log.error("feedback_failed", err, { sessionId });
|
|
517
562
|
console.log(chalk.red(` Failed to generate feedback: ${err.message}`));
|
|
518
563
|
}
|
|
564
|
+
finally {
|
|
565
|
+
await log.dispose();
|
|
566
|
+
}
|
|
519
567
|
}
|
|
520
568
|
// ─── TEXT SESSION (fallback) ──────────────────────────────────────────────────
|
|
521
569
|
async function runTextSession(opts) {
|
package/dist/index.js
CHANGED
|
@@ -54,6 +54,7 @@ import { registerResumesCommand } from "./commands/resumes.js";
|
|
|
54
54
|
import { registerReferralCommand } from "./commands/referral.js";
|
|
55
55
|
import { registerEvalCommand } from "./commands/eval.js";
|
|
56
56
|
import { registerInterviewCommand } from "./commands/interview.js";
|
|
57
|
+
import { registerAdminCommand } from "./commands/admin.js";
|
|
57
58
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
58
59
|
const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
|
|
59
60
|
const program = new Command();
|
|
@@ -79,6 +80,7 @@ registerResumesCommand(program);
|
|
|
79
80
|
registerReferralCommand(program);
|
|
80
81
|
registerEvalCommand(program);
|
|
81
82
|
registerInterviewCommand(program);
|
|
83
|
+
registerAdminCommand(program);
|
|
82
84
|
// Shortcuts for whiteboard creation
|
|
83
85
|
registerNewCommand(program);
|
|
84
86
|
registerListTemplatesCommand(program);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CVLogger — Generic structured logging module for the CareerVivid CLI.
|
|
3
|
+
*
|
|
4
|
+
* Design goals:
|
|
5
|
+
* - Zero-friction: never throws, never blocks the user — fire-and-forget
|
|
6
|
+
* - Extensible: one logger per command/feature, sessionId for event correlation
|
|
7
|
+
* - Dual-sink: (1) remote POST to cliLog Cloud Function, (2) local JSONL fallback
|
|
8
|
+
* - Buffered: batched remote flushes to minimize network calls
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* import { createLogger } from "../lib/logger.js";
|
|
12
|
+
* const log = createLogger("interview", { sessionId, apiKey, version: pkg.version });
|
|
13
|
+
* log.info("session_start", { role, numQuestions });
|
|
14
|
+
* log.error("connection_error", err, { phase: "websocket_open" });
|
|
15
|
+
* await log.dispose(); // flush before process exit
|
|
16
|
+
*
|
|
17
|
+
* Extending to new features — no changes to this file needed:
|
|
18
|
+
* const agentLog = createLogger("agent", { apiKey, version });
|
|
19
|
+
* const resumeLog = createLogger("resume", { sessionId: resumeId, apiKey, version });
|
|
20
|
+
*/
|
|
21
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
22
|
+
export interface LogEvent {
|
|
23
|
+
/** Severity level */
|
|
24
|
+
level: LogLevel;
|
|
25
|
+
/** Feature area: "interview" | "agent" | "resume" | ... */
|
|
26
|
+
feature: string;
|
|
27
|
+
/** Machine-readable event name: "session_start" | "billing_complete" | ... */
|
|
28
|
+
event: string;
|
|
29
|
+
/** Optional session/entity correlation ID */
|
|
30
|
+
sessionId?: string;
|
|
31
|
+
/** Arbitrary serializable metadata */
|
|
32
|
+
metadata?: Record<string, unknown>;
|
|
33
|
+
/** Error message (when level === "error") */
|
|
34
|
+
errorMessage?: string;
|
|
35
|
+
/** Error stack trace (when level === "error") */
|
|
36
|
+
errorStack?: string;
|
|
37
|
+
/** ISO 8601 timestamp from the client */
|
|
38
|
+
clientTime: string;
|
|
39
|
+
/** CLI package.json version */
|
|
40
|
+
cliVersion: string;
|
|
41
|
+
}
|
|
42
|
+
export declare class CVLogger {
|
|
43
|
+
private readonly _feature;
|
|
44
|
+
private _sessionId;
|
|
45
|
+
private readonly _flushUrl;
|
|
46
|
+
private readonly _apiKey;
|
|
47
|
+
private readonly _cliVersion;
|
|
48
|
+
private readonly _localLogDir;
|
|
49
|
+
private _buffer;
|
|
50
|
+
private _flushing;
|
|
51
|
+
private _disposed;
|
|
52
|
+
private _flushTimer;
|
|
53
|
+
private static readonly FLUSH_INTERVAL_MS;
|
|
54
|
+
private static readonly FLUSH_BUFFER_SIZE;
|
|
55
|
+
constructor(opts: {
|
|
56
|
+
feature: string;
|
|
57
|
+
sessionId?: string;
|
|
58
|
+
flushUrl: string;
|
|
59
|
+
apiKey?: string;
|
|
60
|
+
cliVersion: string;
|
|
61
|
+
localLogDir?: string;
|
|
62
|
+
});
|
|
63
|
+
/** Informational event. */
|
|
64
|
+
info(event: string, metadata?: Record<string, unknown>): void;
|
|
65
|
+
/** Warning event. */
|
|
66
|
+
warn(event: string, metadata?: Record<string, unknown>): void;
|
|
67
|
+
/**
|
|
68
|
+
* Error event. Accepts Error objects or any thrown value.
|
|
69
|
+
* Serializes the error — never rethrows.
|
|
70
|
+
*/
|
|
71
|
+
error(event: string, err?: unknown, metadata?: Record<string, unknown>): void;
|
|
72
|
+
/**
|
|
73
|
+
* Record a numeric metric (latency, token count, credit charge, etc.)
|
|
74
|
+
* Stored as an "info" event with metricName + metricValue in metadata.
|
|
75
|
+
*/
|
|
76
|
+
metric(name: string, value: number, metadata?: Record<string, unknown>): void;
|
|
77
|
+
/**
|
|
78
|
+
* Update the sessionId after construction.
|
|
79
|
+
* Use when the sessionId isn't available until after an async call.
|
|
80
|
+
*/
|
|
81
|
+
setSessionId(sessionId: string): void;
|
|
82
|
+
/**
|
|
83
|
+
* Flush buffered events to the remote endpoint.
|
|
84
|
+
* Safe to call multiple times — idempotent. Never throws.
|
|
85
|
+
*/
|
|
86
|
+
flush(): Promise<void>;
|
|
87
|
+
/**
|
|
88
|
+
* Dispose: stop auto-flush timer + flush remaining events.
|
|
89
|
+
* Always call before the command exits (use try/finally).
|
|
90
|
+
*/
|
|
91
|
+
dispose(): Promise<void>;
|
|
92
|
+
private _push;
|
|
93
|
+
private _writeLocal;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Create a feature-scoped logger. Pass this around; call dispose() at the end.
|
|
97
|
+
*
|
|
98
|
+
* @param feature "interview" | "agent" | "resume" | "jobs" | etc.
|
|
99
|
+
* @param opts.sessionId Correlation ID (interview sessionId, resumeId, …)
|
|
100
|
+
* @param opts.apiKey CareerVivid cv_live_ key (for remote attribution)
|
|
101
|
+
* @param opts.version CLI version from package.json
|
|
102
|
+
*/
|
|
103
|
+
export declare function createLogger(feature: string, opts?: {
|
|
104
|
+
sessionId?: string;
|
|
105
|
+
apiKey?: string;
|
|
106
|
+
version?: string;
|
|
107
|
+
}): CVLogger;
|
|
108
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/lib/logger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAQH,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAE3D,MAAM,WAAW,QAAQ;IACrB,qBAAqB;IACrB,KAAK,EAAE,QAAQ,CAAC;IAChB,2DAA2D;IAC3D,OAAO,EAAE,MAAM,CAAC;IAChB,8EAA8E;IAC9E,KAAK,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,6CAA6C;IAC7C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iDAAiD;IACjD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yCAAyC;IACzC,UAAU,EAAE,MAAM,CAAC;IACnB,+BAA+B;IAC/B,UAAU,EAAE,MAAM,CAAC;CACtB;AAID,qBAAa,QAAQ;IACjB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,UAAU,CAAqB;IACvC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IAEtC,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,WAAW,CAA+C;IAElE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAU;IACnD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAM;gBAEnC,IAAI,EAAE;QACd,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;QACnB,WAAW,CAAC,EAAE,MAAM,CAAC;KACxB;IAwBD,2BAA2B;IAC3B,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAI7D,qBAAqB;IACrB,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAI7D;;;OAGG;IACH,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAc7E;;;OAGG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAI7E;;;OAGG;IACH,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAIrC;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAmB5B;;;OAGG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAY9B,OAAO,CAAC,KAAK;IA+Bb,OAAO,CAAC,WAAW;CAOtB;AAWD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CACxB,OAAO,EAAE,MAAM,EACf,IAAI,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,GACrE,QAAQ,CAQV"}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CVLogger — Generic structured logging module for the CareerVivid CLI.
|
|
3
|
+
*
|
|
4
|
+
* Design goals:
|
|
5
|
+
* - Zero-friction: never throws, never blocks the user — fire-and-forget
|
|
6
|
+
* - Extensible: one logger per command/feature, sessionId for event correlation
|
|
7
|
+
* - Dual-sink: (1) remote POST to cliLog Cloud Function, (2) local JSONL fallback
|
|
8
|
+
* - Buffered: batched remote flushes to minimize network calls
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* import { createLogger } from "../lib/logger.js";
|
|
12
|
+
* const log = createLogger("interview", { sessionId, apiKey, version: pkg.version });
|
|
13
|
+
* log.info("session_start", { role, numQuestions });
|
|
14
|
+
* log.error("connection_error", err, { phase: "websocket_open" });
|
|
15
|
+
* await log.dispose(); // flush before process exit
|
|
16
|
+
*
|
|
17
|
+
* Extending to new features — no changes to this file needed:
|
|
18
|
+
* const agentLog = createLogger("agent", { apiKey, version });
|
|
19
|
+
* const resumeLog = createLogger("resume", { sessionId: resumeId, apiKey, version });
|
|
20
|
+
*/
|
|
21
|
+
import { appendFileSync, mkdirSync, existsSync } from "fs";
|
|
22
|
+
import { join } from "path";
|
|
23
|
+
import { homedir } from "os";
|
|
24
|
+
// ─── Logger class ─────────────────────────────────────────────────────────────
|
|
25
|
+
export class CVLogger {
|
|
26
|
+
_feature;
|
|
27
|
+
_sessionId;
|
|
28
|
+
_flushUrl;
|
|
29
|
+
_apiKey;
|
|
30
|
+
_cliVersion;
|
|
31
|
+
_localLogDir;
|
|
32
|
+
_buffer = [];
|
|
33
|
+
_flushing = false;
|
|
34
|
+
_disposed = false;
|
|
35
|
+
_flushTimer = null;
|
|
36
|
+
static FLUSH_INTERVAL_MS = 60_000;
|
|
37
|
+
static FLUSH_BUFFER_SIZE = 25;
|
|
38
|
+
constructor(opts) {
|
|
39
|
+
this._feature = opts.feature;
|
|
40
|
+
this._sessionId = opts.sessionId;
|
|
41
|
+
this._flushUrl = opts.flushUrl;
|
|
42
|
+
this._apiKey = opts.apiKey;
|
|
43
|
+
this._cliVersion = opts.cliVersion;
|
|
44
|
+
this._localLogDir = opts.localLogDir ?? join(homedir(), ".cv", "logs");
|
|
45
|
+
// Ensure local log directory exists
|
|
46
|
+
try {
|
|
47
|
+
if (!existsSync(this._localLogDir)) {
|
|
48
|
+
mkdirSync(this._localLogDir, { recursive: true });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch { /* non-fatal */ }
|
|
52
|
+
// Auto-flush timer — unref() so it doesn't prevent clean process exit
|
|
53
|
+
this._flushTimer = setInterval(() => {
|
|
54
|
+
if (this._buffer.length > 0)
|
|
55
|
+
this.flush().catch(() => { });
|
|
56
|
+
}, CVLogger.FLUSH_INTERVAL_MS);
|
|
57
|
+
this._flushTimer.unref?.();
|
|
58
|
+
}
|
|
59
|
+
// ── Public API ────────────────────────────────────────────────────────────
|
|
60
|
+
/** Informational event. */
|
|
61
|
+
info(event, metadata) {
|
|
62
|
+
this._push("info", event, metadata);
|
|
63
|
+
}
|
|
64
|
+
/** Warning event. */
|
|
65
|
+
warn(event, metadata) {
|
|
66
|
+
this._push("warn", event, metadata);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Error event. Accepts Error objects or any thrown value.
|
|
70
|
+
* Serializes the error — never rethrows.
|
|
71
|
+
*/
|
|
72
|
+
error(event, err, metadata) {
|
|
73
|
+
let errorMessage;
|
|
74
|
+
let errorStack;
|
|
75
|
+
if (err instanceof Error) {
|
|
76
|
+
errorMessage = err.message;
|
|
77
|
+
errorStack = err.stack;
|
|
78
|
+
}
|
|
79
|
+
else if (typeof err === "string") {
|
|
80
|
+
errorMessage = err;
|
|
81
|
+
}
|
|
82
|
+
else if (err != null) {
|
|
83
|
+
try {
|
|
84
|
+
errorMessage = JSON.stringify(err);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
errorMessage = String(err);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
this._push("error", event, metadata, errorMessage, errorStack);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Record a numeric metric (latency, token count, credit charge, etc.)
|
|
94
|
+
* Stored as an "info" event with metricName + metricValue in metadata.
|
|
95
|
+
*/
|
|
96
|
+
metric(name, value, metadata) {
|
|
97
|
+
this._push("info", `metric:${name}`, { ...metadata, metricName: name, metricValue: value });
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Update the sessionId after construction.
|
|
101
|
+
* Use when the sessionId isn't available until after an async call.
|
|
102
|
+
*/
|
|
103
|
+
setSessionId(sessionId) {
|
|
104
|
+
this._sessionId = sessionId;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Flush buffered events to the remote endpoint.
|
|
108
|
+
* Safe to call multiple times — idempotent. Never throws.
|
|
109
|
+
*/
|
|
110
|
+
async flush() {
|
|
111
|
+
if (this._flushing || this._buffer.length === 0 || !this._apiKey)
|
|
112
|
+
return;
|
|
113
|
+
this._flushing = true;
|
|
114
|
+
const batch = this._buffer.splice(0); // atomic drain
|
|
115
|
+
try {
|
|
116
|
+
await fetch(this._flushUrl, {
|
|
117
|
+
method: "POST",
|
|
118
|
+
headers: { "Content-Type": "application/json" },
|
|
119
|
+
body: JSON.stringify({ apiKey: this._apiKey, events: batch }),
|
|
120
|
+
signal: AbortSignal.timeout(8_000),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Remote flush failed: events are already in local JSONL file.
|
|
125
|
+
// Don't re-buffer — avoids memory growth on persistent failures.
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
this._flushing = false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Dispose: stop auto-flush timer + flush remaining events.
|
|
133
|
+
* Always call before the command exits (use try/finally).
|
|
134
|
+
*/
|
|
135
|
+
async dispose() {
|
|
136
|
+
if (this._disposed)
|
|
137
|
+
return;
|
|
138
|
+
this._disposed = true;
|
|
139
|
+
if (this._flushTimer) {
|
|
140
|
+
clearInterval(this._flushTimer);
|
|
141
|
+
this._flushTimer = null;
|
|
142
|
+
}
|
|
143
|
+
await this.flush();
|
|
144
|
+
}
|
|
145
|
+
// ── Internal ──────────────────────────────────────────────────────────────
|
|
146
|
+
_push(level, event, metadata, errorMessage, errorStack) {
|
|
147
|
+
if (this._disposed)
|
|
148
|
+
return;
|
|
149
|
+
const entry = {
|
|
150
|
+
level,
|
|
151
|
+
feature: this._feature,
|
|
152
|
+
event,
|
|
153
|
+
sessionId: this._sessionId,
|
|
154
|
+
metadata,
|
|
155
|
+
errorMessage,
|
|
156
|
+
errorStack,
|
|
157
|
+
clientTime: new Date().toISOString(),
|
|
158
|
+
cliVersion: this._cliVersion,
|
|
159
|
+
};
|
|
160
|
+
// Write to local JSONL immediately (synchronous, best-effort)
|
|
161
|
+
this._writeLocal(entry);
|
|
162
|
+
this._buffer.push(entry);
|
|
163
|
+
// Trigger remote flush if buffer is large enough
|
|
164
|
+
if (this._buffer.length >= CVLogger.FLUSH_BUFFER_SIZE) {
|
|
165
|
+
this.flush().catch(() => { });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
_writeLocal(entry) {
|
|
169
|
+
try {
|
|
170
|
+
const date = entry.clientTime.slice(0, 10); // YYYY-MM-DD
|
|
171
|
+
const logFile = join(this._localLogDir, `${this._feature}-${date}.jsonl`);
|
|
172
|
+
appendFileSync(logFile, JSON.stringify(entry) + "\n");
|
|
173
|
+
}
|
|
174
|
+
catch { /* local write failure is non-fatal */ }
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
178
|
+
const CLI_LOG_URL = process.env.CV_FUNCTIONS_URL
|
|
179
|
+
? `${process.env.CV_FUNCTIONS_URL}/cliLog`
|
|
180
|
+
: "https://us-west1-jastalk-firebase.cloudfunctions.net/cliLog";
|
|
181
|
+
// ─── Factory ──────────────────────────────────────────────────────────────────
|
|
182
|
+
/**
|
|
183
|
+
* Create a feature-scoped logger. Pass this around; call dispose() at the end.
|
|
184
|
+
*
|
|
185
|
+
* @param feature "interview" | "agent" | "resume" | "jobs" | etc.
|
|
186
|
+
* @param opts.sessionId Correlation ID (interview sessionId, resumeId, …)
|
|
187
|
+
* @param opts.apiKey CareerVivid cv_live_ key (for remote attribution)
|
|
188
|
+
* @param opts.version CLI version from package.json
|
|
189
|
+
*/
|
|
190
|
+
export function createLogger(feature, opts = {}) {
|
|
191
|
+
return new CVLogger({
|
|
192
|
+
feature,
|
|
193
|
+
sessionId: opts.sessionId,
|
|
194
|
+
flushUrl: CLI_LOG_URL,
|
|
195
|
+
apiKey: opts.apiKey,
|
|
196
|
+
cliVersion: opts.version ?? "unknown",
|
|
197
|
+
});
|
|
198
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "careervivid",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Official CLI for CareerVivid —
|
|
3
|
+
"version": "2.0.0",
|
|
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": {
|
|
7
7
|
"cv": "dist/index.js",
|
|
@@ -57,10 +57,15 @@
|
|
|
57
57
|
"keywords": [
|
|
58
58
|
"careervivid",
|
|
59
59
|
"cli",
|
|
60
|
+
"ai-interview",
|
|
61
|
+
"voice-interview",
|
|
62
|
+
"mock-interview",
|
|
63
|
+
"job-search",
|
|
60
64
|
"developer-tools",
|
|
61
65
|
"publish",
|
|
62
66
|
"portfolio",
|
|
63
67
|
"ai-agent",
|
|
68
|
+
"resume",
|
|
64
69
|
"mcp"
|
|
65
70
|
],
|
|
66
71
|
"author": "CareerVivid",
|