careervivid 1.12.4 → 1.12.6

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.
@@ -1 +1 @@
1
- {"version":3,"file":"QueryEngine.d.ts","sourceRoot":"","sources":["../../src/agent/QueryEngine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,uBAAuB,EAAE,OAAO,EAAQ,MAAM,eAAe,CAAC;AACpF,OAAO,EAAE,IAAI,EAAsB,MAAM,WAAW,CAAC;AAMrD,eAAO,MAAM,0BAA0B,QA8B/B,CAAC;AAKT,eAAO,MAAM,kBAAkB,QAgFI,CAAC;AAMpC,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8EAA8E;IAC9E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8EAA8E;IAC9E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gEAAgE;IAChE,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,wEAAwE;IACxE,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,0FAA0F;IAC1F,UAAU,CAAC,EAAE,CAAC,QAAQ,CAAC,EAAE,uBAAuB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1E,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IACtE,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC;IACvD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,kDAAkD;IAClD,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,wEAAwE;IACxE,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,gDAAgD;IAChD,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;CAC3B;AAyCD;;;;GAIG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,EAAE,CAAc;IACxB,OAAO,CAAC,OAAO,CAAiB;IAChC,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,OAAO,CAAoB;IACnC,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,eAAe,CAAU;IACjC,OAAO,CAAC,gBAAgB,CAAS;gBAErB,OAAO,GAAE,kBAAuB;IAyBrC,UAAU,IAAI,OAAO,EAAE;IAIvB,UAAU,CAAC,OAAO,EAAE,OAAO,EAAE;IAIpC,8CAA8C;IACvC,QAAQ,CAAC,KAAK,EAAE,IAAI,EAAE;IAW7B,OAAO,CAAC,mBAAmB;YAcb,YAAY;YAOZ,gBAAgB;IA6DjB,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC;IAqG5E;;;;OAIG;IACU,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC;CAoHtF"}
1
+ {"version":3,"file":"QueryEngine.d.ts","sourceRoot":"","sources":["../../src/agent/QueryEngine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,uBAAuB,EAAE,OAAO,EAAQ,MAAM,eAAe,CAAC;AACpF,OAAO,EAAE,IAAI,EAAsB,MAAM,WAAW,CAAC;AAMrD,eAAO,MAAM,0BAA0B,QA8B/B,CAAC;AAKT,eAAO,MAAM,kBAAkB,QA+FI,CAAC;AAMpC,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8EAA8E;IAC9E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8EAA8E;IAC9E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gEAAgE;IAChE,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,wEAAwE;IACxE,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,0FAA0F;IAC1F,UAAU,CAAC,EAAE,CAAC,QAAQ,CAAC,EAAE,uBAAuB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1E,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IACtE,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC;IACvD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,kDAAkD;IAClD,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,wEAAwE;IACxE,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,gDAAgD;IAChD,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;CAC3B;AAyCD;;;;GAIG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,EAAE,CAAc;IACxB,OAAO,CAAC,OAAO,CAAiB;IAChC,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,OAAO,CAAoB;IACnC,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,eAAe,CAAU;IACjC,OAAO,CAAC,gBAAgB,CAAS;gBAErB,OAAO,GAAE,kBAAuB;IAyBrC,UAAU,IAAI,OAAO,EAAE;IAIvB,UAAU,CAAC,OAAO,EAAE,OAAO,EAAE;IAIpC,8CAA8C;IACvC,QAAQ,CAAC,KAAK,EAAE,IAAI,EAAE;IAW7B,OAAO,CAAC,mBAAmB;YAcb,YAAY;YAOZ,gBAAgB;IA6DjB,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC;IAqG5E;;;;OAIG;IACU,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC;CAoHtF"}
@@ -103,6 +103,21 @@ Priority Score formula: 40% attention + 30% excitement + 20% fit + 10% recency
103
103
  - Always show [ID], company, role, status, and priority score when discussing specific jobs
104
104
  - Be proactive: if you notice stale jobs or low apply velocity, mention it unprompted
105
105
 
106
+ ## URL Verification — HARNESS ENGINEERING RULE (Mandatory, No Exceptions)
107
+
108
+ You must NEVER present a job application URL or any company link to the user without first verifying it is alive.
109
+ Think like a user clicking that link — a 404 or hallucinated URL destroys trust instantly.
110
+
111
+ **Rules:**
112
+ 1. After 'search_jobs' returns results, call 'verify_search_results' with all URLs before presenting them.
113
+ 2. Before saying "here is the link to apply", call 'verify_url' on that URL first.
114
+ 3. If a URL fails verification, do NOT show it. Instead say: "I couldn't confirm the application link — please search for this role directly on [Company]'s careers page or LinkedIn."
115
+ 4. If a URL looks made-up (unfamiliar company domain, no ATS path like /careers/ or /jobs/), call 'verify_url' immediately — do not present it without checking.
116
+ 5. Never bypass this rule with phrases like "the link should be..." or "visit...". Check first, then share.
117
+
118
+ This rule exists because AI models can confidently produce plausible-sounding URLs that do not exist.
119
+ Your credibility depends on only sharing links that work.
120
+
106
121
  ## Greeting Protocol
107
122
  When the user sends a generic greeting (e.g., "hey", "hello", "hi", "start"), you MUST return exactly this standardized routing menu, word-for-word, and do not call any tools:
108
123
 
@@ -1 +1 @@
1
- {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../../../src/agent/tools/browser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAysBlC,eAAO,MAAM,iBAAiB,EAAE,IAAI,EAanC,CAAC"}
1
+ {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../../../src/agent/tools/browser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAmtBlC,eAAO,MAAM,iBAAiB,EAAE,IAAI,EAanC,CAAC"}
@@ -499,7 +499,11 @@ const BrowserCloseTool = {
499
499
  */
500
500
  function getSidecarPath() {
501
501
  const __dirname = dirname(fileURLToPath(import.meta.url));
502
- return resolve(__dirname, "../../apply/browser_sidecar.py");
502
+ // Compiled path: dist/agent/tools/ → three levels up → project root → src/apply/
503
+ const fromDist = resolve(__dirname, "../../../src/apply/browser_sidecar.py");
504
+ // Source path (ts-node): src/agent/tools/ → two levels up → src/apply/
505
+ const fromSrc = resolve(__dirname, "../../apply/browser_sidecar.py");
506
+ return existsSync(fromDist) ? fromDist : fromSrc;
503
507
  }
504
508
  const BrowserUseAgentTool = {
505
509
  name: "browser_use_agent",
@@ -535,48 +539,56 @@ Example: "Go to https://jobs.example.com/apply and fill out the application form
535
539
  },
536
540
  execute: async (args) => {
537
541
  const { spawn } = await import("child_process");
538
- const { loadConfig, getGeminiKey } = await import("../../config.js");
539
- // ── Locate Python 3.11 ────────────────────────────────────────────────
542
+ // ── Locate Python — venv first, then system python 3.11 ────────────────────────
540
543
  const PYTHON_CANDIDATES = [
544
+ join(homedir(), "careervivid", "browser-use", ".venv", "bin", "python"),
541
545
  "/opt/homebrew/bin/python3.11",
542
546
  "/usr/local/bin/python3.11",
543
547
  "/usr/bin/python3.11",
544
548
  ];
545
549
  const pythonBin = PYTHON_CANDIDATES.find(existsSync);
546
550
  if (!pythonBin) {
547
- return " Python 3.11 not found. Install with: brew install python@3.11";
551
+ return "\u274c Python 3.11 not found. Install with: brew install python@3.11";
548
552
  }
549
- // ── Locate sidecar script ─────────────────────────────────────────────
553
+ // ── Locate sidecar script ───────────────────────────────────────────
550
554
  const sidecarPath = getSidecarPath();
551
555
  if (!existsSync(sidecarPath)) {
552
556
  return `❌ browser_sidecar.py not found at: ${sidecarPath}`;
553
557
  }
554
- // ── Get Gemini API key ────────────────────────────────────────────────
558
+ // ── Resolve LLM config ───────────────────────────────────────────
559
+ const { loadConfig, getGeminiKey, getLlmConfig } = await import("../../config.js");
555
560
  const cfg = loadConfig();
556
- const apiKey = cfg.geminiKey ||
557
- cfg.llmApiKey ||
558
- getGeminiKey() ||
559
- process.env.GOOGLE_API_KEY ||
560
- process.env.GEMINI_API_KEY ||
561
- "";
562
- if (!apiKey) {
561
+ const llmCfg = getLlmConfig();
562
+ // For the agent tool, default to using the active LLM config from cv agent session
563
+ const llmConfig = {
564
+ provider: llmCfg.provider,
565
+ model: args.model || llmCfg.model || "gemini-3.1-flash-lite-preview",
566
+ apiKey: llmCfg.apiKey || cfg.geminiKey || getGeminiKey() || process.env.GEMINI_API_KEY || "",
567
+ baseUrl: llmCfg.baseUrl,
568
+ };
569
+ if (!llmConfig.apiKey && llmConfig.provider !== "careervivid") {
563
570
  return [
564
- "❌ No Gemini API key found for browser-use agent.",
565
- " Set one via: cv agent config → choose 'Gemini (personal key)'",
566
- " Or set env: export GEMINI_API_KEY=AIza...",
571
+ `\u274c No API key found for provider: ${llmConfig.provider}`,
572
+ " Set it via: cv agent config",
567
573
  ].join("\n");
568
574
  }
575
+ // For CareerVivid Cloud, use the CV API key as the identifier
576
+ if (llmConfig.provider === "careervivid") {
577
+ llmConfig.apiKey = cfg.apiKey || process.env.CV_API_KEY || "";
578
+ }
569
579
  const profileDir = join(homedir(), ".careervivid", "browser-session");
570
- const model = args.model || "gemini-3.1-flash-lite-preview";
571
- // ── Build input payload ───────────────────────────────────────────────
580
+ // ── Build input payload (new llm_config format) ────────────────────────
581
+ const model = llmConfig.model;
572
582
  const inputPayload = JSON.stringify({
573
- task: args.task,
574
- api_key: apiKey,
575
- model,
583
+ url: "", // browser_use_agent sends task_override instead of url+profile
584
+ llm_config: llmConfig,
585
+ resume_pdf_path: "",
586
+ profile: {},
576
587
  profile_dir: profileDir,
588
+ task_override: args.task,
577
589
  });
578
- // ── Spawn sidecar ─────────────────────────────────────────────────────
579
- console.log(chalk.cyan(`\n🤖 Launching browser-use agent (${model})...\n`));
590
+ // ── Spawn sidecar ───────────────────────────────────────────
591
+ console.log(chalk.cyan(`\n\ud83e\udd16 Launching browser-use agent (${model} via ${llmConfig.provider})...\n`));
580
592
  return new Promise((resolve) => {
581
593
  const steps = [];
582
594
  let finalResult = "";
@@ -584,8 +596,6 @@ Example: "Go to https://jobs.example.com/apply and fill out the application form
584
596
  stdio: ["pipe", "pipe", "pipe"],
585
597
  env: {
586
598
  ...process.env,
587
- GOOGLE_API_KEY: apiKey,
588
- GEMINI_API_KEY: apiKey,
589
599
  PYTHONUNBUFFERED: "1",
590
600
  },
591
601
  });
@@ -1 +1 @@
1
- {"version":3,"file":"jobs.d.ts","sourceRoot":"","sources":["../../../src/agent/tools/jobs.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAmBlC,eAAO,MAAM,cAAc,EAAE,IA0F5B,CAAC;AAMF,eAAO,MAAM,WAAW,EAAE,IA4EzB,CAAC;AAMF,eAAO,MAAM,YAAY,EAAE,IAgE1B,CAAC;AAMF,eAAO,MAAM,mBAAmB,EAAE,IAwDjC,CAAC;AAMF,eAAO,MAAM,aAAa,EAAE,IAgC3B,CAAC;AAMF,eAAO,MAAM,eAAe,EAAE,IA6B7B,CAAC;AAMF,eAAO,MAAM,gBAAgB,EAAE,IAmE9B,CAAC;AAMF,eAAO,MAAM,gBAAgB,EAAE,IAwB9B,CAAC;AAMF,eAAO,MAAM,cAAc,EAAE,IAkO5B,CAAC;AAMF,eAAO,MAAM,aAAa,EAAE,IAAI,EAU/B,CAAC"}
1
+ {"version":3,"file":"jobs.d.ts","sourceRoot":"","sources":["../../../src/agent/tools/jobs.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAmBlC,eAAO,MAAM,cAAc,EAAE,IA0F5B,CAAC;AAMF,eAAO,MAAM,WAAW,EAAE,IA4EzB,CAAC;AAMF,eAAO,MAAM,YAAY,EAAE,IAgE1B,CAAC;AAMF,eAAO,MAAM,mBAAmB,EAAE,IAwDjC,CAAC;AAMF,eAAO,MAAM,aAAa,EAAE,IAgC3B,CAAC;AAMF,eAAO,MAAM,eAAe,EAAE,IA6B7B,CAAC;AAMF,eAAO,MAAM,gBAAgB,EAAE,IAmE9B,CAAC;AAMF,eAAO,MAAM,gBAAgB,EAAE,IAwB9B,CAAC;AAMF,eAAO,MAAM,cAAc,EAAE,IAyO5B,CAAC;AAMF,eAAO,MAAM,aAAa,EAAE,IAAI,EAU/B,CAAC"}
@@ -513,61 +513,71 @@ FILLING RULES:
513
513
  if (args.dry_run) {
514
514
  return `[DRY RUN] Would launch browser-use with:\n\n${task.substring(0, 600)}...`;
515
515
  }
516
- // ── 4. Resolve Python 3.11 and sidecar path ────────────────────────────
516
+ // ── 4. Resolve Python and sidecar path ─────────────────────────────────
517
517
  const { existsSync } = await import("fs");
518
518
  const { spawn } = await import("child_process");
519
519
  const pathMod = await import("path");
520
520
  const { fileURLToPath } = await import("url");
521
521
  const PYTHON_CANDIDATES = [
522
+ pathMod.join(pathMod.dirname(fileURLToPath(import.meta.url)), "../../../..", "browser-use", ".venv", "bin", "python"),
523
+ "/Users/jiawenzhu/careervivid/browser-use/.venv/bin/python",
522
524
  "/opt/homebrew/bin/python3.11",
523
525
  "/usr/local/bin/python3.11",
524
526
  "/usr/bin/python3.11",
525
527
  ];
526
528
  const pythonBin = PYTHON_CANDIDATES.find(existsSync);
527
529
  if (!pythonBin) {
528
- return " Python 3.11 not found.\n Install: brew install python@3.11\n Then: pip3.11 install browser-use";
530
+ return "\u274c Python 3.11 not found.\n Install: brew install python@3.11\n Then: pip3.11 install browser-use";
529
531
  }
530
532
  const __dirname = pathMod.dirname(fileURLToPath(import.meta.url));
531
- const sidecarPath = pathMod.resolve(__dirname, "../../apply/browser_sidecar.py");
533
+ // dist/agent/tools/ ../../../ → project root → src/apply/
534
+ const sidecarFromDist = pathMod.resolve(__dirname, "../../../src/apply/browser_sidecar.py");
535
+ const sidecarFromSrc = pathMod.resolve(__dirname, "../../apply/browser_sidecar.py");
536
+ const sidecarPath = existsSync(sidecarFromDist) ? sidecarFromDist : sidecarFromSrc;
532
537
  if (!existsSync(sidecarPath)) {
533
- return `❌ browser_sidecar.py not found at expected path: ${sidecarPath}`;
538
+ return `\u274c browser_sidecar.py not found at expected path: ${sidecarPath}`;
534
539
  }
535
- // ── 5. Resolve Gemini API key ──────────────────────────────────────────
536
- const { loadConfig, getGeminiKey } = await import("../../config.js");
540
+ // ── 5. Resolve LLM config ─────────────────────────────────────────
541
+ const { loadConfig, getGeminiKey, getLlmConfig } = await import("../../config.js");
537
542
  const cfg = loadConfig();
538
- const apiKey = cfg.geminiKey ||
539
- cfg.llmApiKey ||
540
- getGeminiKey() ||
541
- process.env.GOOGLE_API_KEY ||
542
- process.env.GEMINI_API_KEY ||
543
- "";
544
- if (!apiKey) {
543
+ const llmCfg = getLlmConfig();
544
+ const llmConfig = {
545
+ provider: llmCfg.provider,
546
+ model: args.model || llmCfg.model || "gemini-3.1-flash-lite-preview",
547
+ apiKey: llmCfg.apiKey || cfg.geminiKey || cfg.llmApiKey || getGeminiKey() || process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY || "",
548
+ baseUrl: llmCfg.baseUrl,
549
+ };
550
+ // For CareerVivid Cloud, use the CV account key for the proxy — no personal LLM key needed
551
+ if (llmConfig.provider === "careervivid") {
552
+ llmConfig.apiKey = cfg.apiKey || process.env.CV_API_KEY || "";
553
+ }
554
+ else if (!llmConfig.apiKey) {
545
555
  return [
546
- "❌ No Gemini API key found browser-use agent cannot start.",
547
- " Fix options:",
548
- " 1. Run `cv agent` → Settings → choose 'Gemini (personal key)' → enter your key",
549
- " 2. Or: export GEMINI_API_KEY=AIzaSy... in your terminal before running cv agent",
556
+ `❌ No API key found for provider: ${llmConfig.provider}`,
557
+ " Run `cv agent config` and choose your provider to set a key.",
550
558
  ].join("\n");
551
559
  }
552
560
  const { homedir } = await import("os");
553
561
  const profileDir = pathMod.join(homedir(), ".careervivid", "browser-session");
554
- // ── 6. Spawn browser-use sidecar ───────────────────────────────────────
562
+ // ── 6. Spawn browser-use sidecar ─────────────────────────────────────────
555
563
  const chalk = (await import("chalk")).default;
556
564
  console.log(chalk.cyan(`\n🤖 Launching browser-use agent → ${args.job_url}\n`));
557
565
  console.log(chalk.dim(" Chrome will open and fill the form autonomously."));
558
566
  console.log(chalk.dim(" It will stop BEFORE the Submit button — you review and submit.\n"));
559
- const model = args.model || "gemini-3.1-flash-lite-preview";
567
+ // Build input payload using the new llm_config format
560
568
  const inputPayload = JSON.stringify({
561
- task,
562
- api_key: apiKey,
563
- model,
569
+ url: args.job_url,
570
+ llm_config: llmConfig,
571
+ resume_pdf_path: "",
572
+ profile: {},
564
573
  profile_dir: profileDir,
574
+ task_override: task, // sidecar uses this full task string
565
575
  });
566
576
  return new Promise((resolve) => {
567
577
  let finalResult = "";
568
578
  const child = spawn(pythonBin, [sidecarPath], {
569
579
  stdio: ["pipe", "pipe", "pipe"],
570
- env: { ...process.env, GOOGLE_API_KEY: apiKey, GEMINI_API_KEY: apiKey, PYTHONUNBUFFERED: "1" },
580
+ env: { ...process.env, PYTHONUNBUFFERED: "1" },
571
581
  });
572
582
  child.stdin.write(inputPayload);
573
583
  child.stdin.end();
@@ -0,0 +1,41 @@
1
+ /**
2
+ * urlVerifier.ts — URL safety & reachability checker for the CareerVivid Job Agent.
3
+ *
4
+ * Harness Engineering Mindset:
5
+ * The agent must think like a real user clicking a link. A 98%-match job means
6
+ * nothing if the URL is broken, hallucinated, or redirects to a homepage.
7
+ * Every URL we return to the user must pass this verification harness BEFORE
8
+ * we present it.
9
+ *
10
+ * The harness performs layered checks:
11
+ * 1. Structural validity — is this even a parseable URL?
12
+ * 2. Domain plausibility — does this look like a real company domain?
13
+ * 3. HTTP reachability — does it respond with 200/301/302?
14
+ * 4. Content sanity — does the final page look like a real job listing?
15
+ * 5. ATS legitimacy — is it on a known ATS (Ashby, Greenhouse, Lever, etc.)?
16
+ */
17
+ import { Tool } from "../Tool.js";
18
+ export interface UrlVerificationResult {
19
+ url: string;
20
+ ok: boolean;
21
+ status?: number;
22
+ finalUrl?: string;
23
+ isTrustedAts: boolean;
24
+ redirected: boolean;
25
+ reason: string;
26
+ warning?: string;
27
+ }
28
+ /**
29
+ * Verify a single URL is reachable and looks like a legitimate job posting.
30
+ * Uses harness-engineering thinking: emulate a real user clicking the link.
31
+ */
32
+ export declare function verifyUrl(url: string): Promise<UrlVerificationResult>;
33
+ /**
34
+ * Verify a batch of URLs in parallel (max 5 concurrent).
35
+ * Returns results in the same order as input.
36
+ */
37
+ export declare function verifyUrlBatch(urls: string[]): Promise<UrlVerificationResult[]>;
38
+ export declare const VerifyUrlTool: Tool;
39
+ export declare const VerifySearchResultsTool: Tool;
40
+ export declare const ALL_URL_VERIFIER_TOOLS: Tool[];
41
+ //# sourceMappingURL=urlVerifier.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"urlVerifier.d.ts","sourceRoot":"","sources":["../../../src/agent/tools/urlVerifier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AA8ClC,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,OAAO,CAAC;IACtB,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CA4I3E;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,MAAM,EAAE,GACb,OAAO,CAAC,qBAAqB,EAAE,CAAC,CAalC;AAID,eAAO,MAAM,aAAa,EAAE,IAkD3B,CAAC;AAIF,eAAO,MAAM,uBAAuB,EAAE,IAiDrC,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,IAAI,EAGxC,CAAC"}
@@ -0,0 +1,293 @@
1
+ /**
2
+ * urlVerifier.ts — URL safety & reachability checker for the CareerVivid Job Agent.
3
+ *
4
+ * Harness Engineering Mindset:
5
+ * The agent must think like a real user clicking a link. A 98%-match job means
6
+ * nothing if the URL is broken, hallucinated, or redirects to a homepage.
7
+ * Every URL we return to the user must pass this verification harness BEFORE
8
+ * we present it.
9
+ *
10
+ * The harness performs layered checks:
11
+ * 1. Structural validity — is this even a parseable URL?
12
+ * 2. Domain plausibility — does this look like a real company domain?
13
+ * 3. HTTP reachability — does it respond with 200/301/302?
14
+ * 4. Content sanity — does the final page look like a real job listing?
15
+ * 5. ATS legitimacy — is it on a known ATS (Ashby, Greenhouse, Lever, etc.)?
16
+ */
17
+ import { Type } from "@google/genai";
18
+ // ── Known ATS domains that are always trustworthy ─────────────────────────────
19
+ const TRUSTED_ATS_DOMAINS = [
20
+ "greenhouse.io",
21
+ "lever.co",
22
+ "ashbyhq.com",
23
+ "workday.com",
24
+ "workdayjobs.com",
25
+ "myworkdayjobs.com",
26
+ "icims.com",
27
+ "smartrecruiters.com",
28
+ "jobvite.com",
29
+ "taleo.net",
30
+ "oracle.com",
31
+ "brassring.com",
32
+ "successfactors.com",
33
+ "linkedin.com/jobs",
34
+ "indeed.com",
35
+ "careers.google.com",
36
+ "jobs.microsoft.com",
37
+ "amazon.jobs",
38
+ "meta.com/careers",
39
+ "apple.com/jobs",
40
+ "openai.com/careers",
41
+ "anthropic.com/careers",
42
+ "jobs.ashbyhq.com",
43
+ "boards.greenhouse.io",
44
+ "apply.workable.com",
45
+ "recruitee.com",
46
+ "bamboohr.com",
47
+ "rippling.com",
48
+ "ripplinghq.com",
49
+ ];
50
+ // ── Suspicious patterns — usually hallucinated or spam ────────────────────────
51
+ const SUSPICIOUS_PATTERNS = [
52
+ /localhost/i,
53
+ /127\.0\.0\./,
54
+ /example\.(com|org|net)/i,
55
+ /test\.(com|org)/i,
56
+ /careers\.(io|app|xyz|online|site|info|biz)$/i, // generic TLDs on "careers" domains
57
+ /jobs\.(io|app|xyz|online|site|info|biz)$/i,
58
+ ];
59
+ /**
60
+ * Verify a single URL is reachable and looks like a legitimate job posting.
61
+ * Uses harness-engineering thinking: emulate a real user clicking the link.
62
+ */
63
+ export async function verifyUrl(url) {
64
+ // ── 1. Structural check ───────────────────────────────────────────────────
65
+ let parsed;
66
+ try {
67
+ parsed = new URL(url);
68
+ }
69
+ catch {
70
+ return {
71
+ url, ok: false, isTrustedAts: false, redirected: false,
72
+ reason: `❌ Malformed URL — not a valid link: "${url}"`,
73
+ };
74
+ }
75
+ if (!["http:", "https:"].includes(parsed.protocol)) {
76
+ return {
77
+ url, ok: false, isTrustedAts: false, redirected: false,
78
+ reason: `❌ Non-HTTP URL — cannot open in browser: "${url}"`,
79
+ };
80
+ }
81
+ // ── 2. Suspicious pattern check ───────────────────────────────────────────
82
+ for (const pattern of SUSPICIOUS_PATTERNS) {
83
+ if (pattern.test(url)) {
84
+ return {
85
+ url, ok: false, isTrustedAts: false, redirected: false,
86
+ reason: `❌ URL looks hallucinated or is a placeholder domain: "${url}"`,
87
+ warning: "This URL matches a known fake/test domain pattern.",
88
+ };
89
+ }
90
+ }
91
+ // ── 3. Trusted ATS domain shortcut ───────────────────────────────────────
92
+ const isTrustedAts = TRUSTED_ATS_DOMAINS.some((d) => parsed.hostname.includes(d) || url.includes(d));
93
+ // ── 4. HTTP reachability (HEAD → GET fallback) ────────────────────────────
94
+ const controller = new AbortController();
95
+ const timeout = setTimeout(() => controller.abort(), 8000); // 8s timeout
96
+ try {
97
+ let res;
98
+ let finalUrl = url;
99
+ try {
100
+ res = await fetch(url, {
101
+ method: "HEAD",
102
+ redirect: "follow",
103
+ signal: controller.signal,
104
+ headers: {
105
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
106
+ },
107
+ });
108
+ finalUrl = res.url;
109
+ }
110
+ catch {
111
+ // HEAD blocked — try GET (some servers reject HEAD)
112
+ res = await fetch(url, {
113
+ method: "GET",
114
+ redirect: "follow",
115
+ signal: controller.signal,
116
+ headers: {
117
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
118
+ },
119
+ });
120
+ finalUrl = res.url;
121
+ }
122
+ const status = res.status;
123
+ const redirected = finalUrl !== url;
124
+ // ── 5. Content sanity — did we land on a real page? ────────────────────
125
+ if (status === 404) {
126
+ return {
127
+ url, ok: false, status, finalUrl, isTrustedAts, redirected,
128
+ reason: `❌ Page not found (404) — this job posting may have been removed or the URL is incorrect.`,
129
+ };
130
+ }
131
+ if (status === 403 || status === 401) {
132
+ // Could be a valid page behind auth — treat as uncertain but not broken
133
+ return {
134
+ url, ok: true, status, finalUrl, isTrustedAts, redirected,
135
+ reason: `⚠️ Page requires authentication (${status}) — link may be valid but access restricted.`,
136
+ warning: "User may need to log in to view this posting.",
137
+ };
138
+ }
139
+ if (status >= 500) {
140
+ return {
141
+ url, ok: false, status, finalUrl, isTrustedAts, redirected,
142
+ reason: `❌ Server error (${status}) — the job site is having issues. Try again later.`,
143
+ };
144
+ }
145
+ // Check if we got redirected to a homepage (common when job is removed)
146
+ if (redirected) {
147
+ const finalParsed = new URL(finalUrl);
148
+ const landedOnHomepage = finalParsed.pathname === "/" ||
149
+ finalParsed.pathname === "" ||
150
+ finalParsed.pathname === "/careers" ||
151
+ finalParsed.pathname === "/jobs";
152
+ if (landedOnHomepage && !isTrustedAts) {
153
+ return {
154
+ url, ok: false, status, finalUrl, isTrustedAts, redirected,
155
+ reason: `❌ URL redirects to homepage (${finalUrl}) — job posting likely no longer exists.`,
156
+ warning: "This job posting may have been removed.",
157
+ };
158
+ }
159
+ }
160
+ if (status >= 200 && status < 400) {
161
+ const verdict = isTrustedAts
162
+ ? `✅ Verified — reachable on trusted ATS (${parsed.hostname})`
163
+ : `✅ Reachable (status ${status})${redirected ? ` → redirected to ${finalUrl}` : ""}`;
164
+ return {
165
+ url, ok: true, status, finalUrl, isTrustedAts, redirected,
166
+ reason: verdict,
167
+ };
168
+ }
169
+ return {
170
+ url, ok: false, status, finalUrl, isTrustedAts, redirected,
171
+ reason: `❌ Unexpected HTTP status: ${status}`,
172
+ };
173
+ }
174
+ catch (err) {
175
+ const msg = err instanceof Error ? err.message : String(err);
176
+ const isTimeout = msg.includes("abort") || msg.includes("timeout");
177
+ return {
178
+ url, ok: false, isTrustedAts, redirected: false,
179
+ reason: isTimeout
180
+ ? `❌ Connection timed out (>8s) — site may be down or the URL is unreachable.`
181
+ : `❌ Network error: ${msg}`,
182
+ };
183
+ }
184
+ finally {
185
+ clearTimeout(timeout);
186
+ }
187
+ }
188
+ /**
189
+ * Verify a batch of URLs in parallel (max 5 concurrent).
190
+ * Returns results in the same order as input.
191
+ */
192
+ export async function verifyUrlBatch(urls) {
193
+ const CONCURRENCY = 5;
194
+ const results = new Array(urls.length);
195
+ for (let i = 0; i < urls.length; i += CONCURRENCY) {
196
+ const batch = urls.slice(i, i + CONCURRENCY);
197
+ const batchResults = await Promise.all(batch.map((u) => verifyUrl(u)));
198
+ batchResults.forEach((r, j) => {
199
+ results[i + j] = r;
200
+ });
201
+ }
202
+ return results;
203
+ }
204
+ // ── Tool: verify_url ─────────────────────────────────────────────────────────
205
+ export const VerifyUrlTool = {
206
+ name: "verify_url",
207
+ description: `Verify that a URL is actually reachable and looks like a real job posting before showing it to the user.
208
+
209
+ HARNESS ENGINEERING RULE: You MUST call this tool before presenting any job URL to the user.
210
+ Think like a user clicking a link — a broken or hallucinated URL wastes their time and destroys trust.
211
+
212
+ Use this tool when:
213
+ - You have a job URL from search_jobs or any other source
214
+ - You are about to tell the user "here is the link to apply"
215
+ - You suspect a URL might be invalid, outdated, or hallucinated
216
+ - You want to confirm a job is still accepting applications
217
+
218
+ This tool checks: URL validity, HTTP reachability, redirect detection, homepage-redirect detection,
219
+ and whether the URL is on a trusted ATS (Ashby, Greenhouse, Lever, etc.).`,
220
+ parameters: {
221
+ type: Type.OBJECT,
222
+ properties: {
223
+ url: {
224
+ type: Type.STRING,
225
+ description: "The full URL to verify (must start with http:// or https://).",
226
+ },
227
+ },
228
+ required: ["url"],
229
+ },
230
+ execute: async (args) => {
231
+ const result = await verifyUrl(args.url);
232
+ const lines = [result.reason];
233
+ if (result.redirected && result.finalUrl) {
234
+ lines.push(` Redirected to: ${result.finalUrl}`);
235
+ }
236
+ if (result.isTrustedAts) {
237
+ lines.push(` ✓ Trusted ATS domain — application form should be available`);
238
+ }
239
+ if (result.warning) {
240
+ lines.push(` ⚠️ ${result.warning}`);
241
+ }
242
+ if (!result.ok) {
243
+ lines.push(`\nAgent Instruction: Do NOT show this URL to the user — it is broken or unreachable.`, `Instead, tell the user you couldn't verify the application link and suggest they`, `search for the job directly on the company's careers page or LinkedIn.`);
244
+ }
245
+ return lines.join("\n");
246
+ },
247
+ };
248
+ // ── Tool: verify_search_results ──────────────────────────────────────────────
249
+ export const VerifySearchResultsTool = {
250
+ name: "verify_search_results",
251
+ description: `Verify a batch of job URLs returned from search_jobs are all reachable.
252
+ Use this after search_jobs to filter out dead or hallucinated links before showing results to the user.
253
+ Returns a summary of which URLs passed and which failed, so you can present only working links.`,
254
+ parameters: {
255
+ type: Type.OBJECT,
256
+ properties: {
257
+ urls: {
258
+ type: Type.ARRAY,
259
+ items: { type: Type.STRING },
260
+ description: "Array of job URLs to verify in parallel.",
261
+ },
262
+ },
263
+ required: ["urls"],
264
+ },
265
+ execute: async (args) => {
266
+ if (!args.urls || args.urls.length === 0) {
267
+ return "No URLs provided to verify.";
268
+ }
269
+ const results = await verifyUrlBatch(args.urls);
270
+ const passed = results.filter((r) => r.ok);
271
+ const failed = results.filter((r) => !r.ok);
272
+ const lines = [
273
+ `URL Verification Results (${passed.length}/${results.length} passed):\n`,
274
+ ];
275
+ for (const r of results) {
276
+ lines.push(`${r.ok ? "✅" : "❌"} ${r.url}`);
277
+ lines.push(` ${r.reason}`);
278
+ if (r.warning)
279
+ lines.push(` ⚠️ ${r.warning}`);
280
+ }
281
+ if (failed.length > 0) {
282
+ lines.push(`\nAgent Instruction: Only show the ${passed.length} passing URLs to the user.`, `For the ${failed.length} failed URL(s), do NOT include them in your response.`, `If too many failed, tell the user you couldn't verify all links and suggest`, `they search directly on LinkedIn or the company careers page.`);
283
+ }
284
+ else {
285
+ lines.push(`\nAll URLs verified successfully — safe to show to the user.`);
286
+ }
287
+ return lines.join("\n");
288
+ },
289
+ };
290
+ export const ALL_URL_VERIFIER_TOOLS = [
291
+ VerifyUrlTool,
292
+ VerifySearchResultsTool,
293
+ ];
@@ -0,0 +1,42 @@
1
+ /**
2
+ * adapter-generator.ts — AI-powered ATS adapter generator
3
+ *
4
+ * When cv jobs apply encounters a URL from an unknown ATS platform, this module:
5
+ * 1. Scrapes the HTML skeleton of the form (headless Playwright)
6
+ * 2. Calls Gemini to generate a TypeScript ATSAdapter implementation
7
+ * 3. Saves the generated adapter to ~/.careervivid/adapters/<hostname>.js (runtime)
8
+ * 4. Optionally syncs to Firebase Storage (user's "memory space") so it's
9
+ * available across devices/reinstalls without needing a CLI rebuild
10
+ *
11
+ * Generated adapters are saved as pre-transpiled JavaScript (not TypeScript)
12
+ * so they can be loaded via dynamic import() at runtime without a build step.
13
+ *
14
+ * Firebase Storage path: gs://<bucket>/users/<uid>/adapters/<hostname>.js
15
+ */
16
+ export interface GeneratedAdapter {
17
+ hostname: string;
18
+ filePath: string;
19
+ source: "cache" | "generated" | "firebase";
20
+ }
21
+ /**
22
+ * Try to load or generate a TypeScript adapter for the given URL.
23
+ *
24
+ * @param url The job application URL
25
+ * @param apiKey Gemini API key for generating the adapter
26
+ * @param model Gemini model to use (e.g. "gemini-3-flash-preview")
27
+ * @returns Path to the generated .js adapter file, or null on failure
28
+ */
29
+ export declare function getOrGenerateAdapter(url: string, apiKey: string, model: string): Promise<GeneratedAdapter | null>;
30
+ /**
31
+ * Dynamically load a generated adapter from a .mjs file.
32
+ * Returns an ATSAdapter-like object or null on failure.
33
+ */
34
+ export declare function loadGeneratedAdapter(adapterPath: string): Promise<any | null>;
35
+ /**
36
+ * List all locally cached adapters (for debugging / cv agent --jobs info).
37
+ */
38
+ export declare function listCachedAdapters(): Array<{
39
+ hostname: string;
40
+ path: string;
41
+ }>;
42
+ //# sourceMappingURL=adapter-generator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter-generator.d.ts","sourceRoot":"","sources":["../../src/apply/adapter-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAyDH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,OAAO,GAAG,WAAW,GAAG,UAAU,CAAC;CAC5C;AAED;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAoClC;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC,CAanF;AAiMD;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAS9E"}