careervivid 2.1.38 → 2.1.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/Tool.d.ts +1 -1
- package/dist/agent/Tool.d.ts.map +1 -1
- package/dist/agent/tools/interview.d.ts.map +1 -1
- package/dist/agent/tools/interview.js +5 -2
- package/dist/agent/tools/urlVerifier.d.ts +1 -0
- package/dist/agent/tools/urlVerifier.d.ts.map +1 -1
- package/dist/agent/tools/urlVerifier.js +294 -86
- package/dist/api.d.ts +7 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +4 -0
- package/dist/apply/browser.d.ts +5 -0
- package/dist/apply/browser.d.ts.map +1 -1
- package/dist/apply/browser.js +1 -1
- package/dist/commands/agent/configurator.d.ts.map +1 -1
- package/dist/commands/agent/configurator.js +14 -0
- package/dist/commands/agent/index.d.ts.map +1 -1
- package/dist/commands/agent/index.js +3 -0
- package/dist/commands/agent/repl/engineLoop.d.ts.map +1 -1
- package/dist/commands/agent/repl/engineLoop.js +121 -116
- package/dist/commands/agent/repl/input.d.ts +2 -7
- package/dist/commands/agent/repl/input.d.ts.map +1 -1
- package/dist/commands/agent/repl/input.js +226 -17
- package/dist/commands/agent/repl.d.ts +3 -0
- package/dist/commands/agent/repl.d.ts.map +1 -1
- package/dist/commands/agent/repl.js +28 -1
- package/dist/commands/apply.d.ts.map +1 -1
- package/dist/commands/apply.js +24 -5
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +5 -4
- package/dist/commands/interview.d.ts.map +1 -1
- package/dist/commands/interview.js +76 -27
- package/dist/commands/jobs.d.ts.map +1 -1
- package/dist/commands/jobs.js +147 -4
- package/dist/commands/resumes.js +6 -6
- package/dist/index.js +4 -2
- package/package.json +1 -1
package/dist/commands/apply.js
CHANGED
|
@@ -28,6 +28,7 @@ import { detectPlatform, getAdapter } from "../apply/index.js";
|
|
|
28
28
|
import { loadProfile, saveProfile } from "../apply/gemini-agent.js";
|
|
29
29
|
import { resolveResumePdf } from "../apply/resume-pdf.js";
|
|
30
30
|
import { getOrGenerateAdapter, loadGeneratedAdapter } from "../apply/adapter-generator.js";
|
|
31
|
+
import { verifyUrl } from "../agent/tools/urlVerifier.js";
|
|
31
32
|
const { prompt } = pkg;
|
|
32
33
|
// ── Config helpers ────────────────────────────────────────────────────────────
|
|
33
34
|
function resolveModel(modelFlag) {
|
|
@@ -266,9 +267,27 @@ export function registerApplyCommand(program) {
|
|
|
266
267
|
console.log(chalk.dim(" Example: cv jobs apply https://boards.greenhouse.io/stripe/jobs/7788088"));
|
|
267
268
|
process.exit(1);
|
|
268
269
|
}
|
|
269
|
-
// ── 3.
|
|
270
|
+
// ── 3. Verify direct apply URL before any browser automation ──────────
|
|
271
|
+
console.log(chalk.dim("\n Verifying apply link..."));
|
|
272
|
+
const verification = await verifyUrl(resolvedUrl);
|
|
273
|
+
if (!verification.ok) {
|
|
274
|
+
console.error(chalk.red(`\n❌ Apply link rejected: ${verification.reason}`));
|
|
275
|
+
if (verification.finalUrl && verification.finalUrl !== resolvedUrl) {
|
|
276
|
+
console.error(chalk.dim(` Final URL: ${verification.finalUrl}`));
|
|
277
|
+
}
|
|
278
|
+
console.error(chalk.dim(" Search for a current direct apply page before running the harness.\n"));
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
if (verification.warning) {
|
|
282
|
+
console.log(chalk.yellow(` ⚠️ ${verification.warning}`));
|
|
283
|
+
}
|
|
284
|
+
if (verification.finalUrl && verification.finalUrl !== resolvedUrl) {
|
|
285
|
+
console.log(chalk.dim(` Redirect resolved: ${verification.finalUrl}`));
|
|
286
|
+
resolvedUrl = verification.finalUrl;
|
|
287
|
+
}
|
|
288
|
+
// ── 4. Resolve resume PDF ────────────────────────────────────────────
|
|
270
289
|
const resumePdf = await resolveResumePdf();
|
|
271
|
-
// ──
|
|
290
|
+
// ── 5. Detect platform ───────────────────────────────────────────────
|
|
272
291
|
const platform = detectPlatform(resolvedUrl);
|
|
273
292
|
const platformLabel = {
|
|
274
293
|
greenhouse: "Greenhouse 🌱", lever: "Lever", ashby: "Ashby",
|
|
@@ -279,7 +298,7 @@ export function registerApplyCommand(program) {
|
|
|
279
298
|
if (platform === "linkedin") {
|
|
280
299
|
console.log(chalk.yellow("\n⚠️ LinkedIn requires manual apply — opening browser-use agent.\n"));
|
|
281
300
|
}
|
|
282
|
-
// ──
|
|
301
|
+
// ── 6. Dry run ───────────────────────────────────────────────────────
|
|
283
302
|
if (options.dryRun) {
|
|
284
303
|
console.log(chalk.yellow("\n🔍 DRY RUN — nothing will open or be filled.\n"));
|
|
285
304
|
const profileKeys = Object.keys(profile).filter((k) => profile[k]);
|
|
@@ -290,13 +309,13 @@ export function registerApplyCommand(program) {
|
|
|
290
309
|
console.log(chalk.dim(" Re-run without --dry-run to execute.\n"));
|
|
291
310
|
process.exit(0);
|
|
292
311
|
}
|
|
293
|
-
// ──
|
|
312
|
+
// ── 7. Verification ──────────────────────────────────────────────────
|
|
294
313
|
if (!llmConfig.apiKey && llmConfig.provider !== "careervivid") {
|
|
295
314
|
console.log(chalk.red(`\n❌ No API key found for provider: ${llmConfig.provider}`));
|
|
296
315
|
console.log(chalk.dim(" Set it via: cv agent config\n"));
|
|
297
316
|
process.exit(1);
|
|
298
317
|
}
|
|
299
|
-
// ──
|
|
318
|
+
// ── 8. HARNESS ROUTING ───────────────────────────────────────────────
|
|
300
319
|
//
|
|
301
320
|
// Route A: Known platform → TypeScript adapter (fast, no LLM)
|
|
302
321
|
// Route B: Unknown platform → AI-generate new adapter
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAcpC,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAcpC,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAsM1D"}
|
package/dist/commands/auth.js
CHANGED
|
@@ -57,7 +57,7 @@ export function registerAuthCommand(program) {
|
|
|
57
57
|
spinner?.stop();
|
|
58
58
|
if (isApiError(result)) {
|
|
59
59
|
if (jsonMode) {
|
|
60
|
-
console.log(JSON.stringify({ success: false, error: result.message }));
|
|
60
|
+
console.log(JSON.stringify({ success: false, ok: false, error: result.message }));
|
|
61
61
|
}
|
|
62
62
|
else {
|
|
63
63
|
console.error(`\n ${chalk.red("✖")} ${chalk.bold.red("Invalid key:")} ${result.message}`);
|
|
@@ -81,6 +81,7 @@ export function registerAuthCommand(program) {
|
|
|
81
81
|
else {
|
|
82
82
|
console.log(JSON.stringify({
|
|
83
83
|
success: true,
|
|
84
|
+
ok: true,
|
|
84
85
|
name: result.name,
|
|
85
86
|
email: result.email,
|
|
86
87
|
role: result.role,
|
|
@@ -110,7 +111,7 @@ export function registerAuthCommand(program) {
|
|
|
110
111
|
`\n ${chalk.dim("Your key may have been revoked. Run")} ${chalk.cyan("cv login")} ${chalk.dim("to re-authenticate.\n")}`);
|
|
111
112
|
}
|
|
112
113
|
else {
|
|
113
|
-
console.log(JSON.stringify({ success: false, error: result.message }));
|
|
114
|
+
console.log(JSON.stringify({ success: false, ok: false, error: result.message }));
|
|
114
115
|
}
|
|
115
116
|
process.exit(1);
|
|
116
117
|
}
|
|
@@ -127,7 +128,7 @@ export function registerAuthCommand(program) {
|
|
|
127
128
|
}));
|
|
128
129
|
}
|
|
129
130
|
else {
|
|
130
|
-
console.log(JSON.stringify({ success: true, ...result }));
|
|
131
|
+
console.log(JSON.stringify({ success: true, ok: true, ...result }));
|
|
131
132
|
}
|
|
132
133
|
});
|
|
133
134
|
// ── whoami ─────────────────────────────────────────────────────────────────
|
|
@@ -179,7 +180,7 @@ export function registerAuthCommand(program) {
|
|
|
179
180
|
const { saveConfig } = await import("../config.js");
|
|
180
181
|
saveConfig(config);
|
|
181
182
|
if (jsonMode) {
|
|
182
|
-
console.log(JSON.stringify({ success: true }));
|
|
183
|
+
console.log(JSON.stringify({ success: true, ok: true }));
|
|
183
184
|
}
|
|
184
185
|
else {
|
|
185
186
|
console.log();
|
|
@@ -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;AAg6BpC,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAoG/D"}
|
|
@@ -252,9 +252,14 @@ async function callAgentProxy(opts) {
|
|
|
252
252
|
const apiKey = getApiKey();
|
|
253
253
|
if (!apiKey)
|
|
254
254
|
throw new Error("No API key. Run: cv login");
|
|
255
|
+
// agentProxy backend only supports Gemini models (e.g., gemini-2.5-flash, gemini-3.5-flash)
|
|
256
|
+
// If a BYO model is passed that is not Gemini (e.g., openai/gpt-oss-120b:free), we fall back to FEEDBACK_MODEL.
|
|
257
|
+
const resolvedModel = opts.model && opts.model.toLowerCase().includes("gemini")
|
|
258
|
+
? opts.model
|
|
259
|
+
: FEEDBACK_MODEL;
|
|
255
260
|
const body = {
|
|
256
261
|
apiKey,
|
|
257
|
-
model:
|
|
262
|
+
model: resolvedModel,
|
|
258
263
|
contents: opts.contents,
|
|
259
264
|
};
|
|
260
265
|
if (opts.systemInstruction)
|
|
@@ -306,7 +311,7 @@ ${questions.map((q, i) => `${i + 1}. ${q}`).join("\n")}
|
|
|
306
311
|
return prompt;
|
|
307
312
|
}
|
|
308
313
|
// ─── Generate Questions (via agentProxy) ─────────────────────────────────────
|
|
309
|
-
async function generateQuestions(role, numQuestions) {
|
|
314
|
+
async function generateQuestions(role, numQuestions, model) {
|
|
310
315
|
const spinner = ora(chalk.dim("Generating interview questions...")).start();
|
|
311
316
|
try {
|
|
312
317
|
const prompt = `Based on the following role, generate ${numQuestions} insightful interview questions covering technical skills, behavioral competencies, and role-specific scenarios. Return ONLY a valid JSON array of strings.\n\nRole: "${role}"`;
|
|
@@ -314,6 +319,7 @@ async function generateQuestions(role, numQuestions) {
|
|
|
314
319
|
contents: [{ role: "user", parts: [{ text: prompt }] }],
|
|
315
320
|
responseSchema: { type: "ARRAY", items: { type: "STRING" } },
|
|
316
321
|
responseMimeType: "application/json",
|
|
322
|
+
model,
|
|
317
323
|
});
|
|
318
324
|
let clean = text.trim().replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "");
|
|
319
325
|
const questions = JSON.parse(clean);
|
|
@@ -326,7 +332,7 @@ async function generateQuestions(role, numQuestions) {
|
|
|
326
332
|
}
|
|
327
333
|
}
|
|
328
334
|
// ─── Analyze Transcript (via agentProxy) ─────────────────────────────────────
|
|
329
|
-
async function analyzeTranscript(transcript, role) {
|
|
335
|
+
async function analyzeTranscript(transcript, role, model) {
|
|
330
336
|
const spinner = ora(chalk.dim("Generating feedback report...")).start();
|
|
331
337
|
try {
|
|
332
338
|
const formatted = transcript
|
|
@@ -347,6 +353,7 @@ async function analyzeTranscript(transcript, role) {
|
|
|
347
353
|
},
|
|
348
354
|
required: ["overallScore", "communicationScore", "confidenceScore", "relevanceScore", "strengths", "areasForImprovement"],
|
|
349
355
|
},
|
|
356
|
+
model,
|
|
350
357
|
});
|
|
351
358
|
let clean = text.trim().replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "");
|
|
352
359
|
const report = JSON.parse(clean);
|
|
@@ -360,7 +367,7 @@ async function analyzeTranscript(transcript, role) {
|
|
|
360
367
|
}
|
|
361
368
|
// ─── VOICE SESSION ────────────────────────────────────────────────────────────
|
|
362
369
|
async function runVoiceSession(opts) {
|
|
363
|
-
const { role, questions, resumeContext, soxPath } = opts;
|
|
370
|
+
const { role, questions, resumeContext, soxPath, model } = opts;
|
|
364
371
|
printBanner(role, "voice");
|
|
365
372
|
// Create logger (sessionId not yet known — will be set after token vend)
|
|
366
373
|
const log = createLogger("interview", {
|
|
@@ -662,7 +669,7 @@ async function runVoiceSession(opts) {
|
|
|
662
669
|
console.log(chalk.dim("\n Generating your personalized feedback report..."));
|
|
663
670
|
let report = null;
|
|
664
671
|
try {
|
|
665
|
-
report = await analyzeTranscript(transcript, role);
|
|
672
|
+
report = await analyzeTranscript(transcript, role, model);
|
|
666
673
|
printReport(report);
|
|
667
674
|
log.info("feedback_complete", { sessionId, overallScore: report.overallScore });
|
|
668
675
|
}
|
|
@@ -701,42 +708,79 @@ async function runVoiceSession(opts) {
|
|
|
701
708
|
}
|
|
702
709
|
// ─── TEXT SESSION (fallback) ──────────────────────────────────────────────────
|
|
703
710
|
async function runTextSession(opts) {
|
|
704
|
-
const { role, questions, resumeContext } = opts;
|
|
711
|
+
const { role, questions, resumeContext, model } = opts;
|
|
705
712
|
const systemInstruction = buildSystemPrompt(role, questions, resumeContext);
|
|
706
713
|
printBanner(role, "text");
|
|
707
714
|
const history = [];
|
|
708
715
|
const transcript = [];
|
|
709
716
|
let ended = false;
|
|
710
717
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false });
|
|
718
|
+
let sigintReceived = false;
|
|
719
|
+
let resolvePending = null;
|
|
720
|
+
const sigintHandler = () => {
|
|
721
|
+
if (sigintReceived)
|
|
722
|
+
return;
|
|
723
|
+
sigintReceived = true;
|
|
724
|
+
printSystem("Interview ended by user.");
|
|
725
|
+
rl.close();
|
|
726
|
+
if (resolvePending) {
|
|
727
|
+
resolvePending(null);
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
process.on("SIGINT", sigintHandler);
|
|
731
|
+
rl.on("SIGINT", sigintHandler);
|
|
711
732
|
const askUser = () => new Promise(resolve => {
|
|
733
|
+
resolvePending = resolve;
|
|
712
734
|
if (process.stdin.isTTY)
|
|
713
735
|
process.stdout.write(chalk.white.bold("\n you ❯ "));
|
|
714
|
-
rl.once("line", line =>
|
|
715
|
-
|
|
736
|
+
rl.once("line", line => {
|
|
737
|
+
resolvePending = null;
|
|
738
|
+
resolve(line.trim());
|
|
739
|
+
});
|
|
740
|
+
rl.once("close", () => {
|
|
741
|
+
resolvePending = null;
|
|
742
|
+
resolve(null);
|
|
743
|
+
});
|
|
716
744
|
});
|
|
745
|
+
const runAgentCall = async (contents) => {
|
|
746
|
+
return new Promise((resolve, reject) => {
|
|
747
|
+
resolvePending = (val) => resolve(null);
|
|
748
|
+
callAgentProxy({ contents, systemInstruction, model }).then(res => {
|
|
749
|
+
resolvePending = null;
|
|
750
|
+
resolve(res);
|
|
751
|
+
}, err => {
|
|
752
|
+
resolvePending = null;
|
|
753
|
+
reject(err);
|
|
754
|
+
});
|
|
755
|
+
});
|
|
756
|
+
};
|
|
717
757
|
const spinner = ora(chalk.dim("Vivid is connecting...")).start();
|
|
718
758
|
try {
|
|
719
|
-
const greeting = await
|
|
720
|
-
contents: [{ role: "user", parts: [{ text: "Hello" }] }],
|
|
721
|
-
systemInstruction,
|
|
722
|
-
});
|
|
759
|
+
const greeting = await runAgentCall([{ role: "user", parts: [{ text: "Hello" }] }]);
|
|
723
760
|
spinner.stop();
|
|
724
|
-
|
|
725
|
-
history.push({ role: "model", parts: [{ text: greeting }] });
|
|
726
|
-
transcript.push({ speaker: "user", text: "Hello" });
|
|
727
|
-
transcript.push({ speaker: "ai", text: greeting });
|
|
728
|
-
if (greeting.includes(END_TOKEN))
|
|
761
|
+
if (sigintReceived || greeting === null) {
|
|
729
762
|
ended = true;
|
|
730
|
-
|
|
763
|
+
}
|
|
764
|
+
else {
|
|
765
|
+
history.push({ role: "user", parts: [{ text: "Hello" }] });
|
|
766
|
+
history.push({ role: "model", parts: [{ text: greeting }] });
|
|
767
|
+
transcript.push({ speaker: "user", text: "Hello" });
|
|
768
|
+
transcript.push({ speaker: "ai", text: greeting });
|
|
769
|
+
if (greeting.includes(END_TOKEN))
|
|
770
|
+
ended = true;
|
|
771
|
+
printAI(greeting);
|
|
772
|
+
}
|
|
731
773
|
}
|
|
732
774
|
catch (err) {
|
|
733
775
|
spinner.fail(chalk.red("Failed to connect to AI interviewer."));
|
|
734
776
|
throw err;
|
|
735
777
|
}
|
|
736
|
-
while (!ended) {
|
|
778
|
+
while (!ended && !sigintReceived) {
|
|
737
779
|
const input = await askUser();
|
|
738
|
-
if (input === null || input.toLowerCase() === "exit" || input.toLowerCase() === "q") {
|
|
739
|
-
|
|
780
|
+
if (sigintReceived || input === null || input.toLowerCase() === "exit" || input.toLowerCase() === "q") {
|
|
781
|
+
if (!sigintReceived) {
|
|
782
|
+
printSystem("Interview ended early.");
|
|
783
|
+
}
|
|
740
784
|
break;
|
|
741
785
|
}
|
|
742
786
|
if (input === "")
|
|
@@ -745,8 +789,11 @@ async function runTextSession(opts) {
|
|
|
745
789
|
transcript.push({ speaker: "user", text: input });
|
|
746
790
|
const aiSpinner = ora({ text: "" }).start();
|
|
747
791
|
try {
|
|
748
|
-
const aiResponse = await
|
|
792
|
+
const aiResponse = await runAgentCall(history);
|
|
749
793
|
aiSpinner.stop();
|
|
794
|
+
if (sigintReceived || aiResponse === null) {
|
|
795
|
+
break;
|
|
796
|
+
}
|
|
750
797
|
history.push({ role: "model", parts: [{ text: aiResponse }] });
|
|
751
798
|
transcript.push({ speaker: "ai", text: aiResponse.replace(END_TOKEN, "").trim() });
|
|
752
799
|
if (aiResponse.includes(END_TOKEN))
|
|
@@ -758,6 +805,7 @@ async function runTextSession(opts) {
|
|
|
758
805
|
console.log(chalk.red(`\n Error: ${err.message}\n`));
|
|
759
806
|
}
|
|
760
807
|
}
|
|
808
|
+
process.off("SIGINT", sigintHandler);
|
|
761
809
|
rl.close();
|
|
762
810
|
const userTurns = transcript.filter(t => t.speaker === "user").length;
|
|
763
811
|
if (userTurns < 2) {
|
|
@@ -767,7 +815,7 @@ async function runTextSession(opts) {
|
|
|
767
815
|
console.log(chalk.dim("\n Generating your personalized feedback report..."));
|
|
768
816
|
let textReport = null;
|
|
769
817
|
try {
|
|
770
|
-
textReport = await analyzeTranscript(transcript, role);
|
|
818
|
+
textReport = await analyzeTranscript(transcript, role, model);
|
|
771
819
|
printReport(textReport);
|
|
772
820
|
}
|
|
773
821
|
catch (err) {
|
|
@@ -802,6 +850,7 @@ export function registerInterviewCommand(program) {
|
|
|
802
850
|
.option("-q, --questions <n>", "Number of interview questions to generate", "5")
|
|
803
851
|
.option("--resume <id>", "Load a specific resume ID for context (from cv resumes list)")
|
|
804
852
|
.option("--text", "Use text-only mode (no audio required)")
|
|
853
|
+
.option("--model <model>", "Specify a custom model to use for the interview session")
|
|
805
854
|
.addHelpText("after", `
|
|
806
855
|
Examples:
|
|
807
856
|
cv interview
|
|
@@ -858,7 +907,7 @@ Voice mode setup (one-time):
|
|
|
858
907
|
// ── Generate questions ───────────────────────────────────────────
|
|
859
908
|
let questions;
|
|
860
909
|
try {
|
|
861
|
-
questions = await generateQuestions(role, numQuestions);
|
|
910
|
+
questions = await generateQuestions(role, numQuestions, opts.model);
|
|
862
911
|
}
|
|
863
912
|
catch (err) {
|
|
864
913
|
console.error(chalk.red(`\n Failed to generate questions: ${err.message}\n`));
|
|
@@ -866,7 +915,7 @@ Voice mode setup (one-time):
|
|
|
866
915
|
}
|
|
867
916
|
// ── Determine mode ───────────────────────────────────────────────
|
|
868
917
|
if (opts.text) {
|
|
869
|
-
await runTextSession({ role, questions, resumeContext });
|
|
918
|
+
await runTextSession({ role, questions, resumeContext, model: opts.model });
|
|
870
919
|
return;
|
|
871
920
|
}
|
|
872
921
|
// Probe for sox
|
|
@@ -877,12 +926,12 @@ Voice mode setup (one-time):
|
|
|
877
926
|
" macOS: brew install sox\n" +
|
|
878
927
|
" Linux: sudo apt install sox\n" +
|
|
879
928
|
"\n Or run in text mode: cv interview --text\n"));
|
|
880
|
-
await runTextSession({ role, questions, resumeContext });
|
|
929
|
+
await runTextSession({ role, questions, resumeContext, model: opts.model });
|
|
881
930
|
return;
|
|
882
931
|
}
|
|
883
932
|
// Voice mode
|
|
884
933
|
try {
|
|
885
|
-
await runVoiceSession({ role, questions, resumeContext, soxPath });
|
|
934
|
+
await runVoiceSession({ role, questions, resumeContext, soxPath, model: opts.model });
|
|
886
935
|
}
|
|
887
936
|
catch (err) {
|
|
888
937
|
console.error(chalk.red(`\n Interview error: ${err.message}\n`));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jobs.d.ts","sourceRoot":"","sources":["../../src/commands/jobs.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"jobs.d.ts","sourceRoot":"","sources":["../../src/commands/jobs.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAmFpC,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,QAmtBnD"}
|
package/dist/commands/jobs.js
CHANGED
|
@@ -11,7 +11,7 @@ import { resolve } from "path";
|
|
|
11
11
|
import chalk from "chalk";
|
|
12
12
|
import ora from "ora";
|
|
13
13
|
import boxen from "boxen";
|
|
14
|
-
import { isApiError, resumeGet, jobsHunt, jobsCreate, jobsUpdate, jobsList, resumesList, generateCoverLetter, listCoverLetters, } from "../api.js";
|
|
14
|
+
import { isApiError, resumeGet, jobsHunt, jobsCreate, jobsUpdate, jobsList, jobsDelete, resumesList, generateCoverLetter, listCoverLetters, } from "../api.js";
|
|
15
15
|
import { checkGwsReady, runGwsCommand } from "../utils/gws-runner.js";
|
|
16
16
|
import { printError } from "../output.js";
|
|
17
17
|
import { registerApplyCommand } from "./apply.js";
|
|
@@ -323,15 +323,15 @@ export function registerJobsCommand(program) {
|
|
|
323
323
|
.option("--json", "Output raw JSON")
|
|
324
324
|
.action(async (opts) => {
|
|
325
325
|
const isJson = opts.json ?? process.argv.includes("--json");
|
|
326
|
-
const spinner = ora("Fetching your job tracker…").start();
|
|
326
|
+
const spinner = isJson ? undefined : ora("Fetching your job tracker…").start();
|
|
327
327
|
const result = await jobsList(opts.status);
|
|
328
328
|
if (isApiError(result)) {
|
|
329
|
-
spinner
|
|
329
|
+
spinner?.fail("Failed to fetch tracker.");
|
|
330
330
|
printError(result.message, undefined, isJson);
|
|
331
331
|
process.exit(1);
|
|
332
332
|
}
|
|
333
333
|
const { jobs, total } = result;
|
|
334
|
-
spinner
|
|
334
|
+
spinner?.succeed(`${total} job(s)${opts.status ? ` with status "${opts.status}"` : ""}`);
|
|
335
335
|
if (isJson) {
|
|
336
336
|
console.log(JSON.stringify(result));
|
|
337
337
|
return;
|
|
@@ -354,6 +354,149 @@ export function registerJobsCommand(program) {
|
|
|
354
354
|
console.log(`\n ${statusSummary}\n`);
|
|
355
355
|
console.log(chalk.dim(` View on web: https://careervivid.app/job-tracker\n`));
|
|
356
356
|
});
|
|
357
|
+
// ── cv jobs validate-links ───────────────────────────────────────────────
|
|
358
|
+
jobsCmd
|
|
359
|
+
.command("validate-links")
|
|
360
|
+
.description("Scan job tracker for duplicates, missing, generic, or broken links")
|
|
361
|
+
.option("--remove-stale", "Delete duplicates (keep oldest) and broken/missing/generic URL entries")
|
|
362
|
+
.option("--json", "Output raw JSON")
|
|
363
|
+
.action(async (opts) => {
|
|
364
|
+
const isJson = opts.json ?? process.argv.includes("--json");
|
|
365
|
+
const spinner = isJson ? undefined : ora("Fetching your job tracker…").start();
|
|
366
|
+
const listResult = await jobsList();
|
|
367
|
+
if (isApiError(listResult)) {
|
|
368
|
+
spinner?.fail("Failed to fetch tracker.");
|
|
369
|
+
printError(listResult.message, undefined, isJson);
|
|
370
|
+
process.exit(1);
|
|
371
|
+
}
|
|
372
|
+
const { jobs } = listResult;
|
|
373
|
+
spinner?.succeed(`Retrieved ${jobs.length} job(s) from tracker`);
|
|
374
|
+
if (jobs.length === 0) {
|
|
375
|
+
if (isJson) {
|
|
376
|
+
console.log(JSON.stringify({ jobs: [], duplicates: [], missing: [], generic: [], broken: [], removed: [], failed: [] }));
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
console.log(chalk.yellow("\n Your tracker is empty.\n"));
|
|
380
|
+
}
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const duplicates = [];
|
|
384
|
+
const missing = [];
|
|
385
|
+
const generic = [];
|
|
386
|
+
const broken = [];
|
|
387
|
+
const normalizeString = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
388
|
+
const isFuzzyRoleMatch = (t1, t2) => {
|
|
389
|
+
const n1 = normalizeString(t1);
|
|
390
|
+
const n2 = normalizeString(t2);
|
|
391
|
+
if (n1 === n2)
|
|
392
|
+
return true;
|
|
393
|
+
if (n1.includes(n2) || n2.includes(n1))
|
|
394
|
+
return true;
|
|
395
|
+
// Fuzzy role check based on words of length >= 4
|
|
396
|
+
const w1 = t1.toLowerCase().split(/\s+/).filter(w => w.length >= 4);
|
|
397
|
+
const w2 = t2.toLowerCase().split(/\s+/).filter(w => w.length >= 4);
|
|
398
|
+
const intersection = w1.filter(w => w2.includes(w));
|
|
399
|
+
return intersection.length >= 2;
|
|
400
|
+
};
|
|
401
|
+
// Keep oldest entry: sort by updatedAt/createdAt asc (earliest first)
|
|
402
|
+
const sortedJobs = [...jobs].sort((a, b) => {
|
|
403
|
+
const t1 = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
|
|
404
|
+
const t2 = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
|
|
405
|
+
return t1 - t2;
|
|
406
|
+
});
|
|
407
|
+
const uniqueJobs = [];
|
|
408
|
+
for (const job of sortedJobs) {
|
|
409
|
+
const isDup = uniqueJobs.find(u => normalizeString(u.companyName) === normalizeString(job.companyName) &&
|
|
410
|
+
isFuzzyRoleMatch(u.jobTitle, job.jobTitle));
|
|
411
|
+
if (isDup) {
|
|
412
|
+
duplicates.push(job);
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
uniqueJobs.push(job);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const { verifyUrl, isGenericJobLanding } = await import("../agent/tools/urlVerifier.js");
|
|
419
|
+
const checkSpinner = isJson ? undefined : ora("Verifying links…").start();
|
|
420
|
+
for (const job of uniqueJobs) {
|
|
421
|
+
const url = job.jobPostURL;
|
|
422
|
+
if (!url || url.trim() === "") {
|
|
423
|
+
missing.push(job);
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
if (isGenericJobLanding(url)) {
|
|
427
|
+
generic.push(job);
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
// Verify reachability
|
|
431
|
+
const ver = await verifyUrl(url);
|
|
432
|
+
if (!ver.ok) {
|
|
433
|
+
broken.push(job);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
checkSpinner?.stop();
|
|
437
|
+
const toDelete = new Set();
|
|
438
|
+
duplicates.forEach(j => toDelete.add(j.id));
|
|
439
|
+
missing.forEach(j => toDelete.add(j.id));
|
|
440
|
+
generic.forEach(j => toDelete.add(j.id));
|
|
441
|
+
broken.forEach(j => toDelete.add(j.id));
|
|
442
|
+
const removed = [];
|
|
443
|
+
const failed = [];
|
|
444
|
+
if (opts.removeStale && toDelete.size > 0) {
|
|
445
|
+
const delSpinner = isJson ? undefined : ora(`Removing ${toDelete.size} stale/duplicate entries…`).start();
|
|
446
|
+
for (const id of toDelete) {
|
|
447
|
+
const res = await jobsDelete({ jobId: id });
|
|
448
|
+
if (isApiError(res)) {
|
|
449
|
+
failed.push(`${id}: ${res.message}`);
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
removed.push(id);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
delSpinner?.succeed(`Removed ${removed.length} entries (${failed.length} failed)`);
|
|
456
|
+
}
|
|
457
|
+
if (isJson) {
|
|
458
|
+
console.log(JSON.stringify({
|
|
459
|
+
totalFound: jobs.length,
|
|
460
|
+
duplicates: duplicates.map(j => ({ id: j.id, title: j.jobTitle, company: j.companyName })),
|
|
461
|
+
missing: missing.map(j => ({ id: j.id, title: j.jobTitle, company: j.companyName })),
|
|
462
|
+
generic: generic.map(j => ({ id: j.id, title: j.jobTitle, company: j.companyName, url: j.jobPostURL })),
|
|
463
|
+
broken: broken.map(j => ({ id: j.id, title: j.jobTitle, company: j.companyName, url: j.jobPostURL })),
|
|
464
|
+
removed,
|
|
465
|
+
failed
|
|
466
|
+
}));
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
console.log(`\n${chalk.bold(" Job Tracker Audit Summary")}\n`);
|
|
470
|
+
if (duplicates.length > 0) {
|
|
471
|
+
console.log(chalk.yellow(` 👯 Duplicates (${duplicates.length}):`));
|
|
472
|
+
duplicates.forEach(j => console.log(chalk.dim(` - ${j.jobTitle} @ ${j.companyName} (ID: ${j.id})`)));
|
|
473
|
+
}
|
|
474
|
+
if (missing.length > 0) {
|
|
475
|
+
console.log(chalk.yellow(` 🔗 Missing Apply Links (${missing.length}):`));
|
|
476
|
+
missing.forEach(j => console.log(chalk.dim(` - ${j.jobTitle} @ ${j.companyName} (ID: ${j.id})`)));
|
|
477
|
+
}
|
|
478
|
+
if (generic.length > 0) {
|
|
479
|
+
console.log(chalk.yellow(` 🌐 Generic/Careers Landing URLs (${generic.length}):`));
|
|
480
|
+
generic.forEach(j => console.log(chalk.dim(` - ${j.jobTitle} @ ${j.companyName}: ${j.jobPostURL} (ID: ${j.id})`)));
|
|
481
|
+
}
|
|
482
|
+
if (broken.length > 0) {
|
|
483
|
+
console.log(chalk.yellow(` ❌ Broken/Expired URLs (${broken.length}):`));
|
|
484
|
+
broken.forEach(j => console.log(chalk.dim(` - ${j.jobTitle} @ ${j.companyName}: ${j.jobPostURL} (ID: ${j.id})`)));
|
|
485
|
+
}
|
|
486
|
+
const totalStale = toDelete.size;
|
|
487
|
+
if (totalStale === 0) {
|
|
488
|
+
console.log(chalk.green(" ✔ No stale or duplicate job listings found in your tracker!\n"));
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
console.log(`\n Total stale entries found: ${chalk.bold.red(totalStale)}`);
|
|
492
|
+
if (opts.removeStale) {
|
|
493
|
+
console.log(chalk.green(` ✔ Cleaned ${removed.length} entries successfully.\n`));
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
console.log(chalk.cyan(` 💡 Run with ${chalk.bold("--remove-stale")} to automatically delete these entries.\n`));
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
});
|
|
357
500
|
// ── cv jobs sync-gmail (legacy) ───────────────────────────────────────────
|
|
358
501
|
jobsCmd
|
|
359
502
|
.command("sync-gmail")
|
package/dist/commands/resumes.js
CHANGED
|
@@ -59,15 +59,15 @@ export function registerResumesCommand(program) {
|
|
|
59
59
|
.option("--json", "Output raw JSON")
|
|
60
60
|
.action(async (opts) => {
|
|
61
61
|
const isJson = opts.json ?? process.argv.includes("--json");
|
|
62
|
-
const spinner = ora("Fetching resumes…").start();
|
|
62
|
+
const spinner = isJson ? undefined : ora("Fetching resumes…").start();
|
|
63
63
|
const result = await resumesList();
|
|
64
64
|
if (isApiError(result)) {
|
|
65
|
-
spinner
|
|
65
|
+
spinner?.fail("Failed to list resumes.");
|
|
66
66
|
printError(result.message, undefined, isJson);
|
|
67
67
|
process.exit(1);
|
|
68
68
|
}
|
|
69
69
|
const { resumes, total } = result;
|
|
70
|
-
spinner
|
|
70
|
+
spinner?.succeed(`Found ${total} resume(s)`);
|
|
71
71
|
if (isJson) {
|
|
72
72
|
console.log(JSON.stringify(result));
|
|
73
73
|
return;
|
|
@@ -90,14 +90,14 @@ export function registerResumesCommand(program) {
|
|
|
90
90
|
.option("--json", "Output raw JSON")
|
|
91
91
|
.action(async (id, opts) => {
|
|
92
92
|
const isJson = opts.json ?? process.argv.includes("--json");
|
|
93
|
-
const spinner = ora("Fetching resume…").start();
|
|
93
|
+
const spinner = isJson ? undefined : ora("Fetching resume…").start();
|
|
94
94
|
const result = await resumeGet(id);
|
|
95
95
|
if (isApiError(result)) {
|
|
96
|
-
spinner
|
|
96
|
+
spinner?.fail("Failed to get resume.");
|
|
97
97
|
printError(result.message, undefined, isJson);
|
|
98
98
|
process.exit(1);
|
|
99
99
|
}
|
|
100
|
-
spinner
|
|
100
|
+
spinner?.succeed(`Resume loaded: ${result.title}`);
|
|
101
101
|
if (isJson) {
|
|
102
102
|
console.log(JSON.stringify(result));
|
|
103
103
|
return;
|
package/dist/index.js
CHANGED
|
@@ -98,10 +98,12 @@ async function main() {
|
|
|
98
98
|
return;
|
|
99
99
|
}
|
|
100
100
|
// ── Session expiry check (firebase-cli style) ──────────────────────────────
|
|
101
|
-
// Skip for auth/login/logout/config/help commands — they don’t need a live session
|
|
101
|
+
// Skip for auth/login/logout/config/help commands — they don’t need a live session.
|
|
102
|
+
// Subcommand help must work before login, e.g. `cv agent --help`.
|
|
102
103
|
const SESSION_EXEMPT = ["login", "logout", "auth", "config", "upgrade", "-v", "--version", "-h", "--help", "help"];
|
|
103
104
|
const subcommand = process.argv[2] ?? "";
|
|
104
|
-
const
|
|
105
|
+
const wantsHelp = process.argv.includes("-h") || process.argv.includes("--help") || subcommand === "help";
|
|
106
|
+
const isExempt = wantsHelp || SESSION_EXEMPT.includes(subcommand);
|
|
105
107
|
if (!isExempt && !process.env.CV_API_KEY && !isSessionValid()) {
|
|
106
108
|
const { loadConfig } = await import("./config.js");
|
|
107
109
|
const hasKey = !!loadConfig().apiKey;
|
package/package.json
CHANGED