careervivid 1.12.36 → 1.12.38

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":"instructions.d.ts","sourceRoot":"","sources":["../../src/agent/instructions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,eAAO,MAAM,aAAa,QAalB,CAAC;AAMT,eAAO,MAAM,cAAc,QAcnB,CAAC;AAMT,eAAO,MAAM,cAAc,QAgBnB,CAAC;AAMT,eAAO,MAAM,kBAAkB,QAqCvB,CAAC;AAMT,eAAO,MAAM,YAAY,QAuCjB,CAAC;AAMT,eAAO,MAAM,iBAAiB,QActB,CAAC;AAMT;;;GAGG;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,CAyBT"}
1
+ {"version":3,"file":"instructions.d.ts","sourceRoot":"","sources":["../../src/agent/instructions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,eAAO,MAAM,aAAa,QAalB,CAAC;AAMT,eAAO,MAAM,cAAc,QAcnB,CAAC;AAMT,eAAO,MAAM,cAAc,QAgBnB,CAAC;AAMT,eAAO,MAAM,kBAAkB,QAwCvB,CAAC;AAMT,eAAO,MAAM,YAAY,QA2CjB,CAAC;AAMT,eAAO,MAAM,iBAAiB,QActB,CAAC;AAMT;;;GAGG;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,CAyBT"}
@@ -75,7 +75,10 @@ export const JOBS_TOOLS_SECTION = `
75
75
  - **delete_resume** — Permanently delete a resume (ask for confirmation first).
76
76
 
77
77
  ### Job Search
78
- - **search_jobs** — Search for jobs scored against the user's resume. Returns results for review (dry_run by default — does NOT auto-save).
78
+ - **search_jobs** — Search for NEW companies/roles NOT yet in the tracker. Returns results for review (dry_run by default — does NOT auto-save). Use for discovery only.
79
+ - **openings_scan** ⭐ — Drill into companies ALREADY in jobs.csv to find their SPECIFIC open roles with direct apply links (uses Greenhouse/Ashby/Lever APIs). Use this instead of search_jobs when the user wants roles at known tracked companies.
80
+ - **openings_list** — List saved job openings found by openings_scan.
81
+ - **openings_apply** — Mark a specific opening as Applied (requires explicit date).
79
82
 
80
83
  ### CSV Pipeline Tracker (tracker_*)
81
84
  These tools read/write jobs.csv — the local career-ops pipeline spreadsheet.
@@ -110,9 +113,10 @@ export const JOBS_HARNESS = `
110
113
  ## Autonomy Rules (Non-Negotiable)
111
114
 
112
115
  ### What you may do freely (no confirmation needed)
113
- - Call any read-only tool (tracker_list_jobs, tracker_dashboard, tracker_rank_priority, tracker_find_stale, tracker_inspect_quality, tracker_recheck_urls, search_jobs, get_resume, etc.)
116
+ - Call any read-only tool (tracker_list_jobs, tracker_dashboard, tracker_rank_priority, tracker_find_stale, tracker_inspect_quality, tracker_recheck_urls, openings_scan, openings_list, search_jobs, get_resume, etc.)
114
117
  - Add a new company/job entry via tracker_add_job (when the user explicitly names a company or approves a search result)
115
118
  - Update non-status fields (attention_score, excitement, notes, follow_up_date, etc.) via tracker_update_job
119
+ - Run openings_scan to fetch real job postings from tracked companies
116
120
 
117
121
  ### What requires explicit user confirmation
118
122
  - Changing status to "Applied", "Rejected", or "Ghosted" — present the proposed change and wait for "yes"
@@ -144,7 +148,10 @@ If it does, call tracker_update_job instead — never create a duplicate row.
144
148
  | updating status, marking applied, follow-up | tracker_update_job |
145
149
  | Kanban board, web tracker | kanban_list_jobs |
146
150
  | resume, background, skills, experience | get_resume |
147
- | find jobs, search roles | get_resume search_jobs |
151
+ | specific roles at tracked companies | openings_scan |
152
+ | view saved openings | openings_list |
153
+ | applied to a specific opening | openings_apply |
154
+ | find NEW companies/roles not yet in tracker | get_resume → search_jobs |
148
155
  `.trim();
149
156
  // ---------------------------------------------------------------------------
150
157
  // §6 — Greeting protocol (shared across modes)
@@ -0,0 +1,21 @@
1
+ /**
2
+ * jobOpenings.ts — Job Openings Drill-Down Tools
3
+ *
4
+ * Bridges jobs.csv (company pipeline) → specific open roles with direct apply links.
5
+ *
6
+ * Architecture:
7
+ * jobs.csv → openings_scan → job_openings.csv
8
+ * (company-level) (ATS fetcher) (posting-level)
9
+ *
10
+ * ATS Support:
11
+ * Greenhouse — Free public REST API (boards-api.greenhouse.io)
12
+ * Ashby — HTML parse (JavaScript SPA; titles extracted from embedded JSON)
13
+ * Lever — Public REST API (api.lever.co/v0/postings/{slug})
14
+ * Direct — JSON-LD schema.org/JobPosting + heuristic <a> tag parsing
15
+ */
16
+ import { Tool } from "../Tool.js";
17
+ export declare const OpeningsScanTool: Tool;
18
+ export declare const OpeningsListTool: Tool;
19
+ export declare const OpeningsApplyTool: Tool;
20
+ export declare const ALL_JOB_OPENINGS_TOOLS: Tool[];
21
+ //# sourceMappingURL=jobOpenings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jobOpenings.d.ts","sourceRoot":"","sources":["../../../src/agent/tools/jobOpenings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAgclC,eAAO,MAAM,gBAAgB,EAAE,IA6O9B,CAAC;AAMF,eAAO,MAAM,gBAAgB,EAAE,IA4G9B,CAAC;AAQF,eAAO,MAAM,iBAAiB,EAAE,IA8F/B,CAAC;AAMF,eAAO,MAAM,sBAAsB,EAAE,IAAI,EAIxC,CAAC"}
@@ -0,0 +1,795 @@
1
+ /**
2
+ * jobOpenings.ts — Job Openings Drill-Down Tools
3
+ *
4
+ * Bridges jobs.csv (company pipeline) → specific open roles with direct apply links.
5
+ *
6
+ * Architecture:
7
+ * jobs.csv → openings_scan → job_openings.csv
8
+ * (company-level) (ATS fetcher) (posting-level)
9
+ *
10
+ * ATS Support:
11
+ * Greenhouse — Free public REST API (boards-api.greenhouse.io)
12
+ * Ashby — HTML parse (JavaScript SPA; titles extracted from embedded JSON)
13
+ * Lever — Public REST API (api.lever.co/v0/postings/{slug})
14
+ * Direct — JSON-LD schema.org/JobPosting + heuristic <a> tag parsing
15
+ */
16
+ import { Type } from "@google/genai";
17
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync, renameSync, } from "fs";
18
+ import { resolve, dirname } from "path";
19
+ import { fileURLToPath } from "url";
20
+ import { homedir } from "os";
21
+ const __dirname = dirname(fileURLToPath(import.meta.url));
22
+ // ---------------------------------------------------------------------------
23
+ // CSV Schema — job_openings.csv
24
+ // ---------------------------------------------------------------------------
25
+ const OPENING_HEADERS = [
26
+ "id", // VER-OPN-001
27
+ "company_id", // FK → jobs.csv id (e.g. VER-001)
28
+ "company", // Human name
29
+ "posting_title", // Exact title from ATS
30
+ "apply_url", // Direct apply link
31
+ "ats", // Greenhouse | Ashby | Lever | Direct
32
+ "location", // Remote / SF / etc.
33
+ "match_score", // 0–100 keyword match vs resume
34
+ "match_keywords", // comma-separated matched keywords
35
+ "status", // Found | Reviewing | Applied | Rejected | Ghosted
36
+ "date_found", // ISO date
37
+ "date_applied",
38
+ "notes",
39
+ ];
40
+ // ---------------------------------------------------------------------------
41
+ // File path
42
+ // ---------------------------------------------------------------------------
43
+ function getOpeningsPath() {
44
+ // Mirror jobs.csv location: career-ops/data/job_openings.csv
45
+ const careerOpsPath = resolve(__dirname, "..", "..", "..", "..", "career-ops", "data");
46
+ if (existsSync(careerOpsPath))
47
+ return resolve(careerOpsPath, "job_openings.csv");
48
+ // Fallback: ~/.careervivid/
49
+ const dir = resolve(homedir(), ".careervivid");
50
+ if (!existsSync(dir))
51
+ mkdirSync(dir, { recursive: true });
52
+ return resolve(dir, "job_openings.csv");
53
+ }
54
+ function loadOpenings() {
55
+ const path = getOpeningsPath();
56
+ if (!existsSync(path)) {
57
+ writeFileSync(path, OPENING_HEADERS.join(",") + "\n", "utf-8");
58
+ return { rows: [], path };
59
+ }
60
+ const raw = readFileSync(path, "utf-8");
61
+ const lines = raw.trim().split("\n");
62
+ if (lines.length <= 1)
63
+ return { rows: [], path };
64
+ const fileHeaders = lines[0].split(",");
65
+ const rows = lines.slice(1).filter(l => l.trim()).map(line => {
66
+ const cols = parseCsvLine(line);
67
+ const row = {};
68
+ OPENING_HEADERS.forEach(h => {
69
+ const idx = fileHeaders.indexOf(h);
70
+ row[h] = idx >= 0 ? (cols[idx] ?? "").trim() : "";
71
+ });
72
+ return row;
73
+ });
74
+ return { rows, path };
75
+ }
76
+ function saveOpenings(rows, csvPath) {
77
+ if (existsSync(csvPath))
78
+ copyFileSync(csvPath, csvPath + ".bak");
79
+ const header = OPENING_HEADERS.join(",");
80
+ const data = rows.map(row => OPENING_HEADERS.map(h => {
81
+ const val = row[h] ?? "";
82
+ return val.includes(",") || val.includes('"')
83
+ ? `"${val.replace(/"/g, '""')}"`
84
+ : val;
85
+ }).join(","));
86
+ const tmpPath = csvPath + ".tmp";
87
+ writeFileSync(tmpPath, [header, ...data].join("\n") + "\n", "utf-8");
88
+ renameSync(tmpPath, csvPath);
89
+ }
90
+ /** Minimal CSV line parser that handles quoted fields. */
91
+ function parseCsvLine(line) {
92
+ const cols = [];
93
+ let cur = "";
94
+ let inQuote = false;
95
+ for (let i = 0; i < line.length; i++) {
96
+ const ch = line[i];
97
+ if (ch === '"') {
98
+ if (inQuote && line[i + 1] === '"') {
99
+ cur += '"';
100
+ i++;
101
+ }
102
+ else {
103
+ inQuote = !inQuote;
104
+ }
105
+ }
106
+ else if (ch === "," && !inQuote) {
107
+ cols.push(cur);
108
+ cur = "";
109
+ }
110
+ else {
111
+ cur += ch;
112
+ }
113
+ }
114
+ cols.push(cur);
115
+ return cols;
116
+ }
117
+ function today() {
118
+ return new Date().toISOString().slice(0, 10);
119
+ }
120
+ // ---------------------------------------------------------------------------
121
+ // ATS Slug Extractor
122
+ // ---------------------------------------------------------------------------
123
+ function extractSlug(careersUrl, ats) {
124
+ try {
125
+ const url = new URL(careersUrl);
126
+ const pathname = url.pathname;
127
+ if (ats === "Greenhouse") {
128
+ // boards.greenhouse.io/vercel OR job-boards.greenhouse.io/vercel
129
+ const match = pathname.match(/^\/([^/]+)/);
130
+ return match?.[1] ?? null;
131
+ }
132
+ if (ats === "Ashby") {
133
+ // jobs.ashbyhq.com/supabase
134
+ const match = pathname.match(/^\/([^/]+)/);
135
+ return match?.[1] ?? null;
136
+ }
137
+ if (ats === "Lever") {
138
+ // jobs.lever.co/wandb
139
+ const match = pathname.match(/^\/([^/]+)/);
140
+ return match?.[1] ?? null;
141
+ }
142
+ return null;
143
+ }
144
+ catch {
145
+ return null;
146
+ }
147
+ }
148
+ // ---------------------------------------------------------------------------
149
+ // ATS Fetchers
150
+ // ---------------------------------------------------------------------------
151
+ /** Greenhouse: free public API — returns real titles + direct apply URLs */
152
+ async function fetchGreenhouseJobs(slug) {
153
+ const url = `https://boards-api.greenhouse.io/v1/boards/${slug}/jobs`;
154
+ const res = await fetch(url, {
155
+ headers: { "User-Agent": "CareerVivid-Agent/1.0" },
156
+ signal: AbortSignal.timeout(15_000),
157
+ });
158
+ if (!res.ok)
159
+ throw new Error(`Greenhouse API returned ${res.status}`);
160
+ const data = await res.json();
161
+ return (data.jobs ?? []).map(j => ({
162
+ title: j.title,
163
+ applyUrl: j.absolute_url,
164
+ location: j.location?.name ?? "",
165
+ department: j.departments?.[0]?.name ?? "",
166
+ }));
167
+ }
168
+ /** Ashby: SPA — parse the embedded JSON from the rendered HTML */
169
+ async function fetchAshbyJobs(slug) {
170
+ const url = `https://jobs.ashbyhq.com/${slug}`;
171
+ const res = await fetch(url, {
172
+ headers: {
173
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
174
+ },
175
+ signal: AbortSignal.timeout(15_000),
176
+ });
177
+ if (!res.ok)
178
+ throw new Error(`Ashby page returned ${res.status}`);
179
+ const html = await res.text();
180
+ // Ashby embeds job data in Next.js __NEXT_DATA__ JSON
181
+ const nextDataMatch = html.match(/<script id="__NEXT_DATA__" type="application\/json">([^<]+)<\/script>/);
182
+ if (nextDataMatch) {
183
+ try {
184
+ const nextData = JSON.parse(nextDataMatch[1]);
185
+ // Location: props.pageProps.jobPostings or props.pageProps.jobBoard.jobPostings
186
+ const postings = nextData?.props?.pageProps?.jobPostings ??
187
+ nextData?.props?.pageProps?.jobBoard?.jobPostings ??
188
+ [];
189
+ if (Array.isArray(postings) && postings.length > 0) {
190
+ return postings
191
+ .filter((p) => p.jobPostingState === "Listed" || !p.jobPostingState)
192
+ .map((p) => ({
193
+ title: p.title ?? p.name ?? "Unknown",
194
+ applyUrl: p.externalLink ?? p.applyUrl ?? `https://jobs.ashbyhq.com/${slug}/${p.id}`,
195
+ location: p.locationName ?? p.location ?? "",
196
+ department: p.departmentName ?? p.team ?? "",
197
+ }));
198
+ }
199
+ }
200
+ catch { /* fall through to regex */ }
201
+ }
202
+ // Fallback: regex-extract title fields from the embedded JSON blobs
203
+ const titleMatches = [...html.matchAll(/"title":"([^"]{5,80})"/g)].map(m => m[1]);
204
+ const dedupedTitles = [...new Set(titleMatches)];
205
+ return dedupedTitles
206
+ .filter(t => /engineer|designer|product|manager|analyst|developer|scientist|operations/i.test(t))
207
+ .map(title => ({
208
+ title,
209
+ applyUrl: `https://jobs.ashbyhq.com/${slug}`,
210
+ location: "",
211
+ }));
212
+ }
213
+ /** Lever: public REST API with HTML fallback */
214
+ async function fetchLeverJobs(slug, careersUrl) {
215
+ // Try public API first
216
+ try {
217
+ const res = await fetch(`https://api.lever.co/v0/postings/${slug}?mode=json&limit=200`, {
218
+ headers: { "User-Agent": "CareerVivid-Agent/1.0" },
219
+ signal: AbortSignal.timeout(10_000),
220
+ });
221
+ if (res.ok) {
222
+ const data = await res.json();
223
+ if (Array.isArray(data) && data.length > 0) {
224
+ return data.map((p) => ({
225
+ title: p.text ?? p.title,
226
+ applyUrl: p.hostedUrl ?? p.applyUrl ?? careersUrl,
227
+ location: p.categories?.location ?? p.location ?? "",
228
+ department: p.categories?.team ?? p.categories?.department ?? "",
229
+ }));
230
+ }
231
+ }
232
+ }
233
+ catch { /* fall through */ }
234
+ // HTML fallback: lever hosted job pages have structured JSON
235
+ const res = await fetch(`https://jobs.lever.co/${slug}`, {
236
+ headers: {
237
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
238
+ },
239
+ signal: AbortSignal.timeout(15_000),
240
+ });
241
+ if (!res.ok)
242
+ throw new Error(`Lever page ${res.status}`);
243
+ const html = await res.text();
244
+ // Lever pages embed jobs in <script type="application/ld+json">
245
+ const ldMatches = [...html.matchAll(/<script type="application\/ld\+json">([^<]+)<\/script>/g)];
246
+ const results = [];
247
+ for (const m of ldMatches) {
248
+ try {
249
+ const obj = JSON.parse(m[1]);
250
+ if (obj["@type"] === "JobPosting" || obj.title) {
251
+ results.push({
252
+ title: obj.title ?? obj.name,
253
+ applyUrl: obj.url ?? obj.sameAs ?? `https://jobs.lever.co/${slug}`,
254
+ location: obj.jobLocation?.address?.addressLocality ?? obj.jobLocation?.name ?? "",
255
+ });
256
+ }
257
+ }
258
+ catch { /* skip malformed */ }
259
+ }
260
+ // Also try extracting from data-qa attributes Lever uses
261
+ const qaMatches = [...html.matchAll(/data-qa="posting-name"[^>]*>([^<]+)</g)];
262
+ const urlMatches = [...html.matchAll(/href="(https:\/\/jobs\.lever\.co\/[^"]+)"/g)];
263
+ qaMatches.forEach((m, i) => {
264
+ const title = m[1].trim();
265
+ if (title && !results.find(r => r.title === title)) {
266
+ results.push({
267
+ title,
268
+ applyUrl: urlMatches[i]?.[1] ?? `https://jobs.lever.co/${slug}`,
269
+ location: "",
270
+ });
271
+ }
272
+ });
273
+ return results;
274
+ }
275
+ /** Direct: JSON-LD schema.org/JobPosting parse + heuristic <a> tag extraction */
276
+ async function fetchDirectJobs(careersUrl) {
277
+ const res = await fetch(careersUrl, {
278
+ headers: {
279
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
280
+ },
281
+ signal: AbortSignal.timeout(15_000),
282
+ });
283
+ if (!res.ok)
284
+ throw new Error(`Careers page returned ${res.status}`);
285
+ const html = await res.text();
286
+ const results = [];
287
+ // 1. JSON-LD schema.org/JobPosting
288
+ const ldMatches = [...html.matchAll(/<script type="application\/ld\+json">([^<]+)<\/script>/g)];
289
+ for (const m of ldMatches) {
290
+ try {
291
+ const obj = JSON.parse(m[1]);
292
+ const items = Array.isArray(obj) ? obj : [obj];
293
+ for (const item of items) {
294
+ if (item["@type"] === "JobPosting") {
295
+ results.push({
296
+ title: item.title,
297
+ applyUrl: item.url ?? item.sameAs ?? careersUrl,
298
+ location: item.jobLocation?.address?.addressLocality ?? item.jobLocation?.name ?? "",
299
+ description: item.description?.slice(0, 200),
300
+ });
301
+ }
302
+ }
303
+ }
304
+ catch { /* skip malformed */ }
305
+ }
306
+ if (results.length > 0)
307
+ return results;
308
+ // 2. Heuristic: find <a> tags that look like job posting links
309
+ const baseUrl = new URL(careersUrl);
310
+ const linkPattern = /<a\s[^>]*href="([^"]+)"[^>]*>([^<]{10,100})<\/a>/gi;
311
+ const linkMatches = [...html.matchAll(linkPattern)];
312
+ const JOB_TITLE_SIGNALS = /engineer|developer|designer|product manager|analyst|architect|scientist|devrel|advocate|solutions|forward.?deployed|technical/i;
313
+ const SKIP_PATTERNS = /privacy|terms|blog|about|home|login|signup|contact|docs/i;
314
+ for (const m of linkMatches) {
315
+ const href = m[1];
316
+ const linkText = m[2].trim().replace(/\s+/g, " ");
317
+ if (!JOB_TITLE_SIGNALS.test(linkText))
318
+ continue;
319
+ if (SKIP_PATTERNS.test(linkText))
320
+ continue;
321
+ if (linkText.length > 100)
322
+ continue;
323
+ // Resolve relative URLs
324
+ let fullUrl = href;
325
+ try {
326
+ fullUrl = new URL(href, baseUrl.origin).toString();
327
+ }
328
+ catch {
329
+ continue;
330
+ }
331
+ if (!results.find(r => r.title === linkText)) {
332
+ results.push({ title: linkText, applyUrl: fullUrl, location: "" });
333
+ }
334
+ }
335
+ return results;
336
+ }
337
+ // ---------------------------------------------------------------------------
338
+ // Resume Keyword Scoring
339
+ // ---------------------------------------------------------------------------
340
+ const RESUME_KEYWORD_CACHE = {
341
+ keywords: [],
342
+ timestamp: 0,
343
+ };
344
+ /**
345
+ * Returns a curated keyword list for the current user.
346
+ * First tries to pull from CareerVivid API; falls back to a hardcoded set
347
+ * of common AI/engineering keywords if API is unavailable.
348
+ */
349
+ async function getResumeKeywords() {
350
+ // Cache for 5 minutes within a session
351
+ if (RESUME_KEYWORD_CACHE.keywords.length > 0 &&
352
+ Date.now() - RESUME_KEYWORD_CACHE.timestamp < 300_000) {
353
+ return RESUME_KEYWORD_CACHE.keywords;
354
+ }
355
+ // Curated engineering + AI keyword set that matches the user's profile.
356
+ // These are weighted for Solutions Engineer / Forward Deployed Engineer roles.
357
+ const defaults = [
358
+ // Roles
359
+ "Solutions Engineer", "Forward Deployed Engineer", "Developer Advocate",
360
+ "Software Engineer", "Full-stack", "Backend", "Frontend", "DevRel",
361
+ // Tech stack
362
+ "TypeScript", "JavaScript", "React", "Next.js", "Node.js",
363
+ "Python", "PostgreSQL", "Firebase", "Supabase", "REST API", "GraphQL",
364
+ // AI/ML
365
+ "LLM", "AI", "Agentic", "RAG", "Embeddings", "OpenAI", "Anthropic", "Gemini",
366
+ "Machine Learning", "GPT", "Claude", "agent", "automation",
367
+ // DevTools
368
+ "Docker", "AWS", "GCP", "Vercel", "CLI", "SDK", "developer tools",
369
+ "developer experience", "DX", "open source",
370
+ // Traits (commonly in JDs matched to your background)
371
+ "Remote", "startup", "SaaS", "platform", "integration",
372
+ ];
373
+ RESUME_KEYWORD_CACHE.keywords = defaults;
374
+ RESUME_KEYWORD_CACHE.timestamp = Date.now();
375
+ return defaults;
376
+ }
377
+ function scorePosting(keywords, posting) {
378
+ const text = [posting.title, posting.description ?? "", posting.department ?? ""]
379
+ .join(" ")
380
+ .toLowerCase();
381
+ const matched = keywords.filter(k => text.includes(k.toLowerCase()));
382
+ // Title match is worth more — double-count title hits
383
+ const titleText = posting.title.toLowerCase();
384
+ const titleMatched = keywords.filter(k => titleText.includes(k.toLowerCase()));
385
+ const weightedMatches = new Set([...matched, ...titleMatched, ...titleMatched]); // title 2x
386
+ const score = Math.min(100, Math.round((weightedMatches.size / Math.max(keywords.length * 0.3, 8)) * 100));
387
+ return { score, matched };
388
+ }
389
+ // ---------------------------------------------------------------------------
390
+ // ID generator for openings
391
+ // ---------------------------------------------------------------------------
392
+ function generateOpeningId(companyId, existingRows) {
393
+ const prefix = companyId.replace(/-\d+$/, ""); // VER-001 → VER
394
+ const existing = existingRows.filter(r => r.id.startsWith(prefix + "-OPN-"));
395
+ const maxN = existing.reduce((max, r) => {
396
+ const n = parseInt(r.id.split("-OPN-")[1] ?? "0", 10);
397
+ return Math.max(max, isNaN(n) ? 0 : n);
398
+ }, 0);
399
+ return `${prefix}-OPN-${String(maxN + 1).padStart(3, "0")}`;
400
+ }
401
+ // ---------------------------------------------------------------------------
402
+ // Tool: openings_scan
403
+ // ---------------------------------------------------------------------------
404
+ export const OpeningsScanTool = {
405
+ name: "openings_scan",
406
+ description: `Scans companies already in the user's job tracker (jobs.csv) to find their ACTUAL open roles with direct apply links.
407
+
408
+ This is the CORRECT way to find specific job postings — it uses the company's real ATS (Greenhouse, Ashby, Lever) to return verified, clickable apply URLs instead of hallucinating jobs.
409
+
410
+ Use this when the user asks:
411
+ - "What specific roles are open at my tracked companies?"
412
+ - "Find me specific jobs I can apply to"
413
+ - "Drill into openings at Vercel / LangChain / Supabase"
414
+ - "What's open at my Tier 1 companies?"
415
+ - "Show me real job postings with direct links"
416
+
417
+ The tool reads careers_url + ats from jobs.csv, fetches live postings,
418
+ scores them against the user's resume, and returns a ranked table.
419
+
420
+ DO NOT use search_jobs for this — search_jobs is for discovering new companies.
421
+ openings_scan is for drilling into companies the user has already vetted.`,
422
+ parameters: {
423
+ type: Type.OBJECT,
424
+ properties: {
425
+ companies: {
426
+ type: Type.ARRAY,
427
+ items: { type: Type.STRING },
428
+ description: "Optional. Company names to scan (e.g. ['Vercel', 'LangChain']). Default: all Tier 1 companies with status 'To Apply' or 'Applied'.",
429
+ },
430
+ tier: {
431
+ type: Type.NUMBER,
432
+ description: "Optional. Filter by tier (1, 2, or 3). Default: 1.",
433
+ },
434
+ role_filter: {
435
+ type: Type.STRING,
436
+ description: "Optional. Keyword to filter job titles (e.g. 'engineer', 'solutions'). Case-insensitive.",
437
+ },
438
+ min_score: {
439
+ type: Type.NUMBER,
440
+ description: "Optional. Minimum resume match score to include (0–100). Default: 40.",
441
+ },
442
+ save: {
443
+ type: Type.BOOLEAN,
444
+ description: "Optional. Save discovered openings to job_openings.csv. Default: true.",
445
+ },
446
+ max_companies: {
447
+ type: Type.NUMBER,
448
+ description: "Optional. Max number of companies to scan per call. Default: 10 (to avoid long waits).",
449
+ },
450
+ },
451
+ required: [],
452
+ },
453
+ execute: async (args) => {
454
+ try {
455
+ // Load companies from jobs.csv
456
+ const jobsCsvPath = resolve(__dirname, "..", "..", "..", "..", "career-ops", "data", "jobs.csv");
457
+ const altPath = resolve(homedir(), ".careervivid", "jobs.csv");
458
+ const csvPath = existsSync(jobsCsvPath) ? jobsCsvPath : altPath;
459
+ if (!existsSync(csvPath)) {
460
+ return "❌ No jobs.csv found. Add some companies to your tracker first with tracker_add_job.";
461
+ }
462
+ const raw = readFileSync(csvPath, "utf-8");
463
+ const lines = raw.trim().split("\n");
464
+ const headers = lines[0].split(",");
465
+ const allJobs = lines.slice(1).filter(l => l.trim()).map(line => {
466
+ const cols = parseCsvLine(line);
467
+ const row = {};
468
+ headers.forEach((h, i) => { row[h] = (cols[i] ?? "").trim(); });
469
+ return row;
470
+ });
471
+ // Filter companies to scan
472
+ let candidates = allJobs;
473
+ if (args.companies && args.companies.length > 0) {
474
+ const names = args.companies.map(c => c.toLowerCase());
475
+ candidates = allJobs.filter(j => names.some(n => j.company?.toLowerCase().includes(n)));
476
+ }
477
+ else {
478
+ // Default: Tier 1 (or specified tier) with non-terminal status
479
+ const targetTier = String(args.tier ?? 1);
480
+ const TERMINAL_STATUSES = ["Rejected", "Ghosted", "Withdrawn", "Closed"];
481
+ candidates = allJobs.filter(j => j.tier === targetTier &&
482
+ !TERMINAL_STATUSES.includes(j.status) &&
483
+ j.careers_url?.startsWith("http"));
484
+ }
485
+ const maxCompanies = Math.min(args.max_companies ?? 10, 20);
486
+ candidates = candidates.slice(0, maxCompanies);
487
+ if (candidates.length === 0) {
488
+ return "ℹ️ No matching companies found in your tracker. Add companies with tracker_add_job first, or broaden the filter.";
489
+ }
490
+ const minScore = args.min_score ?? 40;
491
+ const keywords = await getResumeKeywords();
492
+ const scanResults = [];
493
+ process.stderr.write(`[openings_scan] Scanning ${candidates.length} companies...\n`);
494
+ // Fetch from each ATS
495
+ for (const job of candidates) {
496
+ const { id, company, careers_url: careersUrl, ats } = job;
497
+ const slug = extractSlug(careersUrl, ats);
498
+ let postings = [];
499
+ let error;
500
+ try {
501
+ if (ats === "Greenhouse" && slug) {
502
+ postings = await fetchGreenhouseJobs(slug);
503
+ }
504
+ else if (ats === "Ashby" && slug) {
505
+ postings = await fetchAshbyJobs(slug);
506
+ }
507
+ else if (ats === "Lever" && slug) {
508
+ postings = await fetchLeverJobs(slug, careersUrl);
509
+ }
510
+ else {
511
+ // Direct or unknown ATS — try HTML parse
512
+ postings = await fetchDirectJobs(careersUrl);
513
+ }
514
+ }
515
+ catch (err) {
516
+ error = err.message?.slice(0, 100);
517
+ }
518
+ scanResults.push({ company, companyId: id, ats, postings, error });
519
+ process.stderr.write(` ${company}: ${postings.length} postings${error ? ` (⚠️ ${error})` : ""}\n`);
520
+ }
521
+ // Score + filter postings
522
+ const { rows: existingOpenings, path: openingsPath } = loadOpenings();
523
+ const newRows = [];
524
+ const reportSections = [];
525
+ for (const result of scanResults) {
526
+ const { company, companyId, ats, postings, error } = result;
527
+ if (error && postings.length === 0) {
528
+ reportSections.push(`\n⚠️ ${company} (${ats}): Could not fetch — ${error}`);
529
+ continue;
530
+ }
531
+ // Apply role filter
532
+ let filtered = postings;
533
+ if (args.role_filter) {
534
+ const kw = args.role_filter.toLowerCase();
535
+ filtered = postings.filter(p => p.title.toLowerCase().includes(kw));
536
+ }
537
+ // Score + sort
538
+ const scored = filtered
539
+ .map(p => ({ posting: p, ...scorePosting(keywords, p) }))
540
+ .filter(s => s.score >= minScore)
541
+ .sort((a, b) => b.score - a.score);
542
+ if (scored.length === 0) {
543
+ reportSections.push(`\n○ ${company} (${ats}) — ${postings.length} postings, none matched score ≥${minScore}%`);
544
+ continue;
545
+ }
546
+ const lines = [
547
+ `\n◆ ${company} (${ats}) — ${postings.length} open roles → ${scored.length} match above ${minScore}%`,
548
+ ];
549
+ for (const { posting, score, matched } of scored.slice(0, 8)) {
550
+ lines.push(` ★ [${score}%] ${posting.title}${posting.location ? ` [${posting.location}]` : ""}`, ` → ${posting.applyUrl}`);
551
+ if (matched.length > 0) {
552
+ lines.push(` ✓ Keywords: ${matched.slice(0, 5).join(", ")}`);
553
+ }
554
+ // Build new row for job_openings.csv
555
+ if (args.save !== false) {
556
+ const existing = existingOpenings.find(r => r.company_id === companyId && r.posting_title === posting.title);
557
+ if (!existing) {
558
+ const newId = generateOpeningId(companyId, [...existingOpenings, ...newRows]);
559
+ newRows.push({
560
+ id: newId,
561
+ company_id: companyId,
562
+ company,
563
+ posting_title: posting.title,
564
+ apply_url: posting.applyUrl,
565
+ ats,
566
+ location: posting.location ?? "",
567
+ match_score: String(score),
568
+ match_keywords: matched.slice(0, 10).join("; "),
569
+ status: "Found",
570
+ date_found: today(),
571
+ date_applied: "",
572
+ notes: "",
573
+ });
574
+ }
575
+ }
576
+ }
577
+ reportSections.push(lines.join("\n"));
578
+ }
579
+ // Persist new rows
580
+ let savedCount = 0;
581
+ if (args.save !== false && newRows.length > 0) {
582
+ const allRows = [...existingOpenings, ...newRows];
583
+ saveOpenings(allRows, openingsPath);
584
+ savedCount = newRows.length;
585
+ }
586
+ const totalFound = scanResults.reduce((s, r) => s + r.postings.length, 0);
587
+ const totalMatched = scanResults.reduce((s, r) => s + r.postings.filter(p => scorePosting(keywords, p).score >= minScore).length, 0);
588
+ return [
589
+ `🔍 Job Openings Scan — ${candidates.length} companies, ${totalFound} live postings, ${totalMatched} matches`,
590
+ "─".repeat(70),
591
+ ...reportSections,
592
+ "─".repeat(70),
593
+ savedCount > 0
594
+ ? `\n✅ Saved ${savedCount} new openings to job_openings.csv. Say "show my openings" to list them.`
595
+ : "\n(No new openings saved — all already tracked or save=false.)",
596
+ `\nTo apply: tell me which role interests you and I can help autofill the application.`,
597
+ ].join("\n");
598
+ }
599
+ catch (err) {
600
+ return `❌ openings_scan failed: ${err.message}`;
601
+ }
602
+ },
603
+ };
604
+ // ---------------------------------------------------------------------------
605
+ // Tool: openings_list
606
+ // ---------------------------------------------------------------------------
607
+ export const OpeningsListTool = {
608
+ name: "openings_list",
609
+ description: `List saved job openings from job_openings.csv.
610
+ Shows specific postings found by openings_scan, with match scores and direct apply URLs.
611
+ Use this to review what's been found and decide what to apply to.
612
+
613
+ Use when the user asks:
614
+ - "Show my job openings"
615
+ - "What openings have you found?"
616
+ - "List the jobs I should apply to"
617
+ - "What did the scan find?"`,
618
+ parameters: {
619
+ type: Type.OBJECT,
620
+ properties: {
621
+ status_filter: {
622
+ type: Type.STRING,
623
+ description: "Optional. Filter by status: Found | Reviewing | Applied | Rejected | Ghosted. Default: all.",
624
+ },
625
+ company_filter: {
626
+ type: Type.STRING,
627
+ description: "Optional. Filter by company name (partial match).",
628
+ },
629
+ min_score: {
630
+ type: Type.NUMBER,
631
+ description: "Optional. Minimum match score to show. Default: 0.",
632
+ },
633
+ sort_by: {
634
+ type: Type.STRING,
635
+ description: "Optional. Sort by: match_score | date_found | company. Default: match_score.",
636
+ },
637
+ },
638
+ required: [],
639
+ },
640
+ execute: async (args) => {
641
+ try {
642
+ const { rows } = loadOpenings();
643
+ if (rows.length === 0) {
644
+ return "ℹ️ No job openings saved yet. Run openings_scan to discover real job postings from your tracked companies.";
645
+ }
646
+ let filtered = rows;
647
+ if (args.status_filter) {
648
+ filtered = filtered.filter(r => r.status.toLowerCase() === args.status_filter.toLowerCase());
649
+ }
650
+ if (args.company_filter) {
651
+ const kw = args.company_filter.toLowerCase();
652
+ filtered = filtered.filter(r => r.company.toLowerCase().includes(kw));
653
+ }
654
+ if (args.min_score) {
655
+ filtered = filtered.filter(r => Number(r.match_score) >= args.min_score);
656
+ }
657
+ // Sort
658
+ const sortBy = args.sort_by ?? "match_score";
659
+ filtered.sort((a, b) => {
660
+ if (sortBy === "match_score")
661
+ return Number(b.match_score) - Number(a.match_score);
662
+ if (sortBy === "date_found")
663
+ return b.date_found.localeCompare(a.date_found);
664
+ if (sortBy === "company")
665
+ return a.company.localeCompare(b.company);
666
+ return 0;
667
+ });
668
+ const lines = [
669
+ `📋 Job Openings — ${filtered.length} of ${rows.length} total`,
670
+ "─".repeat(70),
671
+ ];
672
+ // Group by status
673
+ const byStatus = new Map();
674
+ for (const r of filtered) {
675
+ const s = r.status || "Found";
676
+ if (!byStatus.has(s))
677
+ byStatus.set(s, []);
678
+ byStatus.get(s).push(r);
679
+ }
680
+ const STATUS_ICONS = {
681
+ Found: "🔍", Reviewing: "📖", Applied: "✅", Rejected: "❌", Ghosted: "👻",
682
+ };
683
+ for (const [status, group] of byStatus) {
684
+ lines.push(`\n${STATUS_ICONS[status] ?? "○"} ${status} (${group.length})`);
685
+ for (const r of group) {
686
+ lines.push(` [${String(r.match_score).padStart(3)}%] ${r.company} — ${r.posting_title}`, ` 📍 ${r.location || "Location not specified"}`, ` 🔗 ${r.apply_url}`);
687
+ if (r.date_applied)
688
+ lines.push(` Applied: ${r.date_applied}`);
689
+ }
690
+ }
691
+ lines.push("─".repeat(70));
692
+ lines.push(`\nTo apply: say "mark [opening-id] as applied on [date]" or "help me apply to [company] [role]".`);
693
+ lines.push(`Opening IDs: ${filtered.slice(0, 5).map(r => r.id).join(", ")}${filtered.length > 5 ? ", ..." : ""}`);
694
+ return lines.join("\n");
695
+ }
696
+ catch (err) {
697
+ return `❌ openings_list failed: ${err.message}`;
698
+ }
699
+ },
700
+ };
701
+ // ---------------------------------------------------------------------------
702
+ // Tool: openings_apply
703
+ // ---------------------------------------------------------------------------
704
+ const OPENING_VALID_STATUSES = ["Found", "Reviewing", "Applied", "Rejected", "Ghosted"];
705
+ export const OpeningsApplyTool = {
706
+ name: "openings_apply",
707
+ description: `Mark a specific job opening in job_openings.csv as Applied (or update its status).
708
+ REQUIRES explicit date_applied when marking as Applied — never infer the date autonomously.
709
+
710
+ Use when the user says:
711
+ - "I applied to the Vercel Developer Success Engineer role today"
712
+ - "Mark VER-OPN-001 as applied on 2026-04-14"
713
+ - "Update the LangChain opening status to Reviewing"`,
714
+ parameters: {
715
+ type: Type.OBJECT,
716
+ properties: {
717
+ opening_id: {
718
+ type: Type.STRING,
719
+ description: "The opening ID from job_openings.csv (e.g. VER-OPN-001). Required.",
720
+ },
721
+ status: {
722
+ type: Type.STRING,
723
+ description: `New status: Found | Reviewing | Applied | Rejected | Ghosted.`,
724
+ },
725
+ date_applied: {
726
+ type: Type.STRING,
727
+ description: "ISO date of application (e.g. 2026-04-14). REQUIRED when status = Applied.",
728
+ },
729
+ notes: {
730
+ type: Type.STRING,
731
+ description: "Optional notes to append (not replace).",
732
+ },
733
+ },
734
+ required: ["opening_id"],
735
+ },
736
+ execute: async (args) => {
737
+ try {
738
+ const { rows, path } = loadOpenings();
739
+ const idx = rows.findIndex(r => r.id.toLowerCase() === args.opening_id.toLowerCase());
740
+ if (idx === -1) {
741
+ const ids = rows.slice(0, 10).map(r => r.id).join(", ");
742
+ return `❌ Opening ID "${args.opening_id}" not found.\nAvailable IDs: ${ids}`;
743
+ }
744
+ const row = { ...rows[idx] };
745
+ const changes = [];
746
+ if (args.status) {
747
+ if (!OPENING_VALID_STATUSES.includes(args.status)) {
748
+ return `❌ Invalid status "${args.status}". Valid: ${OPENING_VALID_STATUSES.join(", ")}`;
749
+ }
750
+ // Gate: Applied requires explicit date
751
+ if (args.status === "Applied" && !args.date_applied && row.status !== "Applied") {
752
+ return (`⚠️ CONFIRMATION REQUIRED\n` +
753
+ `To mark "${row.company} — ${row.posting_title}" as Applied, provide the date:\n` +
754
+ ` Example: "Yes, mark ${args.opening_id} as Applied on ${today()}"`);
755
+ }
756
+ changes.push(`status: ${row.status} → ${args.status}`);
757
+ row.status = args.status;
758
+ }
759
+ if (args.date_applied) {
760
+ row.date_applied = args.date_applied;
761
+ changes.push(`date_applied: ${args.date_applied}`);
762
+ }
763
+ else if (row.status === "Applied" && !row.date_applied) {
764
+ row.date_applied = today();
765
+ changes.push(`date_applied: ${today()} (auto)`);
766
+ }
767
+ if (args.notes) {
768
+ const existing = row.notes ? row.notes + "; " : "";
769
+ row.notes = (existing + args.notes).slice(0, 500);
770
+ changes.push(`notes: appended`);
771
+ }
772
+ rows[idx] = row;
773
+ saveOpenings(rows, path);
774
+ return [
775
+ `✅ Updated opening ${args.opening_id}:`,
776
+ ...changes.map(c => ` • ${c}`),
777
+ ``,
778
+ `${row.company} — ${row.posting_title}`,
779
+ `Status: ${row.status}${row.date_applied ? ` (applied ${row.date_applied})` : ""}`,
780
+ `🔗 ${row.apply_url}`,
781
+ ].join("\n");
782
+ }
783
+ catch (err) {
784
+ return `❌ openings_apply failed: ${err.message}`;
785
+ }
786
+ },
787
+ };
788
+ // ---------------------------------------------------------------------------
789
+ // Export
790
+ // ---------------------------------------------------------------------------
791
+ export const ALL_JOB_OPENINGS_TOOLS = [
792
+ OpeningsScanTool,
793
+ OpeningsListTool,
794
+ OpeningsApplyTool,
795
+ ];
@@ -1 +1 @@
1
- {"version":3,"file":"repl.d.ts","sourceRoot":"","sources":["../../../src/commands/agent/repl.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,sBAAsB,EAAE,MAAM,uCAAuC,CAAC;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAEzD,OAAO,EAA4D,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAK7G,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,GAAE,MAAM,GAAG,IAAW,QAwBtF;AAED,wBAAsB,OAAO,CAC3B,MAAM,EAAE,WAAW,GAAG,sBAAsB,GAAG,IAAI,EACnD,OAAO,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,EAC9K,gBAAgB,EAAE,WAAW,EAC7B,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,iBAAiB,EAAE,MAAM,EACzB,KAAK,EAAE,GAAG,EAAE,GACX,OAAO,CAAC,IAAI,CAAC,CAmkBf"}
1
+ {"version":3,"file":"repl.d.ts","sourceRoot":"","sources":["../../../src/commands/agent/repl.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,sBAAsB,EAAE,MAAM,uCAAuC,CAAC;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAEzD,OAAO,EAA4D,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAK7G,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,GAAE,MAAM,GAAG,IAAW,QAwBtF;AAED,wBAAsB,OAAO,CAC3B,MAAM,EAAE,WAAW,GAAG,sBAAsB,GAAG,IAAI,EACnD,OAAO,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,EAC9K,gBAAgB,EAAE,WAAW,EAC7B,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,iBAAiB,EAAE,MAAM,EACzB,KAAK,EAAE,GAAG,EAAE,GACX,OAAO,CAAC,IAAI,CAAC,CAskBf"}
@@ -34,7 +34,7 @@ export async function askLoop(engine, options, selectedProvider, selectedModel,
34
34
  const WRITE_TOOLS = new Set([
35
35
  "tracker_add_job", "tracker_update_job", "kanban_add_job", "kanban_update_status",
36
36
  "save_cover_letter", "delete_cover_letter", "write_file", "patch_file",
37
- "tracker_recheck_urls",
37
+ "tracker_recheck_urls", "openings_apply",
38
38
  ]);
39
39
  const SESSION_MAX_MUTATIONS = 25;
40
40
  const TURN_MAX_MUTATIONS = 10;
@@ -247,6 +247,9 @@ export async function askLoop(engine, options, selectedProvider, selectedModel,
247
247
  verify_url: "🔍 Verifying URL...",
248
248
  verify_job_urls: "🔍 Verifying job URLs...",
249
249
  search_jobs: "🔍 Searching jobs...",
250
+ openings_scan: "🎯 Scanning companies for open roles...",
251
+ openings_list: "📋 Loading saved openings...",
252
+ openings_apply: "✅ Marking opening as applied...",
250
253
  get_resume: "📄 Loading resume...",
251
254
  list_resumes: "📄 Loading resumes...",
252
255
  get_profile: "👤 Loading profile...",
@@ -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;AAuO3C,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,CA2ChG"}
1
+ {"version":3,"file":"toolRegistry.d.ts","sourceRoot":"","sources":["../../../src/commands/agent/toolRegistry.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAwO3C,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,CA8ChG"}
@@ -6,6 +6,7 @@ import { ALL_LOCAL_TRACKER_TOOLS } from "../../agent/tools/local-tracker.js";
6
6
  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
+ import { ALL_JOB_OPENINGS_TOOLS } from "../../agent/tools/jobOpenings.js";
9
10
  import { publishSingleFile } from "../publish.js";
10
11
  // ── Publish tools ─────────────────────────────────────────────────────────────
11
12
  const PublishArticleTool = {
@@ -237,6 +238,10 @@ export function getTools(options) {
237
238
  if (!tools.find((x) => x.name === t.name))
238
239
  tools.push(t);
239
240
  }
241
+ for (const t of ALL_JOB_OPENINGS_TOOLS) {
242
+ if (!tools.find((x) => x.name === t.name))
243
+ tools.push(t);
244
+ }
240
245
  return tools;
241
246
  }
242
247
  if (options.resume) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "careervivid",
3
- "version": "1.12.36",
3
+ "version": "1.12.38",
4
4
  "description": "Official CLI for CareerVivid — publish articles, diagrams, and portfolio updates from your terminal or AI agent",
5
5
  "type": "module",
6
6
  "bin": {