resuml 3.0.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,686 @@
1
+ import {
2
+ analyzeAts,
3
+ loadConfig
4
+ } from "./chunk-C2JG5KF4.js";
5
+ import {
6
+ getSkillIndex
7
+ } from "./chunk-QR77BRMN.js";
8
+
9
+ // src/ats/seniority.ts
10
+ var SENIORITY_ORDER = [
11
+ "intern",
12
+ "junior",
13
+ "mid",
14
+ "senior",
15
+ "staff",
16
+ "principal"
17
+ ];
18
+ var SENIORITY_PATTERNS = [
19
+ { level: "principal", rx: /\b(principal|distinguished|fellow)\b/i },
20
+ { level: "staff", rx: /\b(staff|architect|head\s+of|director|vp\b|chief)\b/i },
21
+ { level: "senior", rx: /\b(senior|sr\.?|lead|tech(?:nical)?\s*lead|tl\b|manager)\b/i },
22
+ { level: "mid", rx: /\b(mid|ii\b|iii\b|engineer\s*ii)\b/i },
23
+ { level: "junior", rx: /\b(junior|jr\.?|associate|entry)\b/i },
24
+ { level: "intern", rx: /\b(intern|trainee|apprentice)\b/i }
25
+ ];
26
+ function detectSeniorityFromTitle(title) {
27
+ for (const { level, rx } of SENIORITY_PATTERNS) {
28
+ if (rx.test(title)) return level;
29
+ }
30
+ return null;
31
+ }
32
+ function seniorityFromYoe(yoe) {
33
+ if (yoe < 1) return "junior";
34
+ if (yoe < 3) return "mid";
35
+ if (yoe < 8) return "senior";
36
+ if (yoe < 12) return "staff";
37
+ return "principal";
38
+ }
39
+
40
+ // src/jobs/query.ts
41
+ function maxSeniority(a, b) {
42
+ return SENIORITY_ORDER.indexOf(a) > SENIORITY_ORDER.indexOf(b) ? a : b;
43
+ }
44
+ function parseDate(raw) {
45
+ if (!raw) return null;
46
+ const d = new Date(raw);
47
+ return isNaN(d.getTime()) ? null : d;
48
+ }
49
+ function computeYearsExperience(resume) {
50
+ const now = /* @__PURE__ */ new Date();
51
+ let totalMs = 0;
52
+ for (const w of resume.work ?? []) {
53
+ const start = parseDate(w.startDate);
54
+ if (!start) continue;
55
+ const end = parseDate(w.endDate) ?? now;
56
+ if (end <= start) continue;
57
+ totalMs += end.getTime() - start.getTime();
58
+ }
59
+ return Math.round(totalMs / (1e3 * 60 * 60 * 24 * 365.25) * 10) / 10;
60
+ }
61
+ function extractResumeText(resume) {
62
+ const parts = [];
63
+ if (resume.basics?.summary) parts.push(resume.basics.summary);
64
+ if (resume.basics?.label) parts.push(resume.basics.label);
65
+ for (const w of resume.work ?? []) {
66
+ if (w.position) parts.push(w.position);
67
+ if (w.summary) parts.push(w.summary);
68
+ parts.push(...w.highlights ?? []);
69
+ }
70
+ for (const s of resume.skills ?? []) {
71
+ if (s.name) parts.push(s.name);
72
+ parts.push(...s.keywords ?? []);
73
+ }
74
+ for (const p of resume.projects ?? []) {
75
+ if (p.name) parts.push(p.name);
76
+ if (p.description) parts.push(p.description);
77
+ parts.push(...p.highlights ?? []);
78
+ parts.push(...p.keywords ?? []);
79
+ }
80
+ return parts.join(" ");
81
+ }
82
+ function deriveSearchQuery(resume, overrides = {}) {
83
+ const text = extractResumeText(resume);
84
+ const matches = getSkillIndex().scan(text);
85
+ const skills = matches.slice(0, 15).map((m) => m.skill.canonical);
86
+ const titles = (resume.work ?? []).map((w) => w.position).filter((p) => typeof p === "string" && p.length > 0);
87
+ const yoe = computeYearsExperience(resume);
88
+ let seniority = seniorityFromYoe(yoe);
89
+ for (const title of titles) {
90
+ const fromTitle = detectSeniorityFromTitle(title);
91
+ if (fromTitle) seniority = maxSeniority(seniority, fromTitle);
92
+ }
93
+ if (resume.basics?.label) {
94
+ const fromLabel = detectSeniorityFromTitle(resume.basics.label);
95
+ if (fromLabel) seniority = maxSeniority(seniority, fromLabel);
96
+ }
97
+ const city = resume.basics?.location?.city;
98
+ const countryCode = resume.basics?.location?.countryCode;
99
+ const headTitle = titles[0] ?? resume.basics?.label ?? "";
100
+ const terms = Array.from(
101
+ new Set(
102
+ [headTitle, ...skills.slice(0, 5)].filter(Boolean).map((t) => t.toLowerCase()).flatMap((t) => t.split(/\s+/)).filter((t) => t.length > 2)
103
+ )
104
+ );
105
+ return {
106
+ skills,
107
+ seniority,
108
+ yearsExperience: yoe,
109
+ titles,
110
+ city,
111
+ countryCode,
112
+ remoteOnly: overrides.remoteOnly ?? false,
113
+ terms
114
+ };
115
+ }
116
+
117
+ // src/jobs/geo.ts
118
+ var COUNTRY_INCLUSIVE = {
119
+ // Switzerland: European, EMEA, EEA/EFTA, DACH, or explicitly Switzerland.
120
+ CH: /\b(switzerland|schweiz|suisse|svizzera|europe|european|emea|eea|efta|dach|cet|cest)\b/i
121
+ };
122
+ var WORLDWIDE = /\b(worldwide|world\s?wide|anywhere|global|globally|international|fully remote)\b/i;
123
+ var COUNTRY_EXCLUSIVE = {
124
+ CH: /\b(united states|u\.?s\.?a?|us[-\s]?based|us[-\s]?only|north america|americas|canada|canadian|latam|latin america|apac|asia|australia|india|united kingdom|uk[-\s]?only|brazil|mexico|nigeria|philippines)\b/i
125
+ };
126
+ function isRemoteEligible(location, body, countryCode) {
127
+ const cc = (countryCode ?? "").toUpperCase();
128
+ const inclusive = COUNTRY_INCLUSIVE[cc];
129
+ const exclusive = COUNTRY_EXCLUSIVE[cc];
130
+ if (!inclusive || !exclusive) return true;
131
+ const scope = `${location ?? ""} ${(body ?? "").slice(0, 400)}`;
132
+ if (WORLDWIDE.test(scope)) return true;
133
+ if (inclusive.test(scope)) return true;
134
+ if (exclusive.test(scope)) return false;
135
+ return true;
136
+ }
137
+
138
+ // src/jobs/normalize.ts
139
+ function stripHtml(input) {
140
+ if (!input) return "";
141
+ const noScript = input.replace(/<(script|style)[^>]*>[\s\S]*?<\/\1>/gi, " ");
142
+ const noTags = noScript.replace(/<[^>]+>/g, " ");
143
+ const decoded = noTags.replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&#(\d+);/g, (_, code) => String.fromCharCode(parseInt(code, 10)));
144
+ return decoded.replace(/\s+/g, " ").trim();
145
+ }
146
+ var REMOTE_PATTERNS = /\b(remote|anywhere|distributed|wfh|work[\s-]from[\s-]home)\b/i;
147
+ function looksRemote(location, body) {
148
+ if (location && REMOTE_PATTERNS.test(location)) return true;
149
+ if (body && REMOTE_PATTERNS.test(body.slice(0, 500))) return true;
150
+ return false;
151
+ }
152
+ function dedupeKey(posting) {
153
+ const norm = (s) => (s ?? "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim().replace(/\s+/g, "-");
154
+ return `${norm(posting.company)}::${norm(posting.title)}::${norm(posting.location)}`;
155
+ }
156
+ function extractLocationCountryCode(location) {
157
+ if (!location) return null;
158
+ const parts = location.split(",");
159
+ const last = parts[parts.length - 1]?.trim().toUpperCase();
160
+ return last && last.length === 2 ? last : null;
161
+ }
162
+ function applyQueryFilters(postings, query) {
163
+ let result = postings;
164
+ if (query.remoteOnly) {
165
+ result = result.filter((p) => p.remote && isRemoteEligible(p.location, p.body, query.countryCode));
166
+ }
167
+ let wrongLocationCount = 0;
168
+ if (query.countryCode) {
169
+ const before = result.length;
170
+ result = result.filter((p) => {
171
+ if (p.remote) return true;
172
+ const loc = extractLocationCountryCode(p.location);
173
+ return !loc || loc === query.countryCode;
174
+ });
175
+ wrongLocationCount = before - result.length;
176
+ }
177
+ return { postings: result, wrongLocationCount };
178
+ }
179
+
180
+ // src/jobs/providers/http.ts
181
+ var DEFAULT_UA = "resuml-jobs/1.0 (+https://github.com/phoinixi/resuml)";
182
+ async function fetchText(url, opts) {
183
+ const controller = new AbortController();
184
+ const timer = setTimeout(() => {
185
+ controller.abort();
186
+ }, opts.timeoutMs);
187
+ try {
188
+ const res = await fetch(url, {
189
+ signal: controller.signal,
190
+ headers: {
191
+ "User-Agent": DEFAULT_UA,
192
+ Accept: opts.accept ?? "application/json, text/plain;q=0.9, */*;q=0.5"
193
+ }
194
+ });
195
+ if (!res.ok) throw new Error(`${url} \u2192 HTTP ${res.status}`);
196
+ return await res.text();
197
+ } finally {
198
+ clearTimeout(timer);
199
+ }
200
+ }
201
+ async function fetchJson(url, opts) {
202
+ const text = await fetchText(url, { ...opts, accept: "application/json" });
203
+ return JSON.parse(text);
204
+ }
205
+
206
+ // src/jobs/providers/greenhouse.ts
207
+ var greenhouseProvider = {
208
+ id: "greenhouse",
209
+ needsAllowlist: true,
210
+ async fetch(_query, { companies = [], timeoutMs }) {
211
+ const results = await Promise.allSettled(
212
+ companies.map((slug) => fetchCompany(slug, timeoutMs))
213
+ );
214
+ return results.flatMap((r) => r.status === "fulfilled" ? r.value : []);
215
+ }
216
+ };
217
+ async function fetchCompany(slug, timeoutMs) {
218
+ const url = `https://boards-api.greenhouse.io/v1/boards/${encodeURIComponent(slug)}/jobs?content=true`;
219
+ const data = await fetchJson(url, { timeoutMs });
220
+ const jobs = data.jobs ?? [];
221
+ return jobs.map((j) => {
222
+ const location = j.location?.name;
223
+ const body = stripHtml(j.content ?? "");
224
+ return {
225
+ id: `greenhouse:${slug}:${j.id}`,
226
+ source: "greenhouse",
227
+ company: slug,
228
+ title: j.title,
229
+ location,
230
+ remote: looksRemote(location, body),
231
+ url: j.absolute_url,
232
+ body,
233
+ postedAt: j.updated_at
234
+ };
235
+ });
236
+ }
237
+
238
+ // src/jobs/providers/lever.ts
239
+ var leverProvider = {
240
+ id: "lever",
241
+ needsAllowlist: true,
242
+ async fetch(_query, { companies = [], timeoutMs }) {
243
+ const results = await Promise.allSettled(
244
+ companies.map((slug) => fetchCompany2(slug, timeoutMs))
245
+ );
246
+ return results.flatMap((r) => r.status === "fulfilled" ? r.value : []);
247
+ }
248
+ };
249
+ function assembleBody(p) {
250
+ const parts = [];
251
+ if (p.descriptionPlain) parts.push(p.descriptionPlain);
252
+ else if (p.description) parts.push(stripHtml(p.description));
253
+ for (const list of p.lists ?? []) {
254
+ if (list.text) parts.push(list.text);
255
+ if (list.content) parts.push(stripHtml(list.content));
256
+ }
257
+ if (p.additionalPlain) parts.push(p.additionalPlain);
258
+ return parts.join("\n\n");
259
+ }
260
+ async function fetchCompany2(slug, timeoutMs) {
261
+ const url = `https://api.lever.co/v0/postings/${encodeURIComponent(slug)}?mode=json`;
262
+ const data = await fetchJson(url, { timeoutMs });
263
+ return data.map((p) => {
264
+ const location = p.categories?.location;
265
+ const body = assembleBody(p);
266
+ return {
267
+ id: `lever:${slug}:${p.id}`,
268
+ source: "lever",
269
+ company: slug,
270
+ title: p.text,
271
+ location,
272
+ remote: looksRemote(location, body),
273
+ url: p.hostedUrl,
274
+ body,
275
+ postedAt: p.createdAt ? new Date(p.createdAt).toISOString() : void 0
276
+ };
277
+ });
278
+ }
279
+
280
+ // src/jobs/providers/ashby.ts
281
+ var ashbyProvider = {
282
+ id: "ashby",
283
+ needsAllowlist: true,
284
+ async fetch(_query, { companies = [], timeoutMs }) {
285
+ const results = await Promise.allSettled(
286
+ companies.map((slug) => fetchCompany3(slug, timeoutMs))
287
+ );
288
+ return results.flatMap((r) => r.status === "fulfilled" ? r.value : []);
289
+ }
290
+ };
291
+ async function fetchCompany3(slug, timeoutMs) {
292
+ const url = `https://api.ashbyhq.com/posting-api/job-board/${encodeURIComponent(slug)}?includeCompensation=true`;
293
+ const data = await fetchJson(url, { timeoutMs });
294
+ const jobs = data.jobs ?? [];
295
+ return jobs.map((j) => {
296
+ const location = j.locationName;
297
+ const body = j.descriptionPlain ?? stripHtml(j.descriptionHtml ?? "");
298
+ return {
299
+ id: `ashby:${slug}:${j.id}`,
300
+ source: "ashby",
301
+ company: slug,
302
+ title: j.title,
303
+ location,
304
+ remote: j.isRemote ?? looksRemote(location, body),
305
+ url: j.jobUrl ?? `https://jobs.ashbyhq.com/${slug}/${j.id}`,
306
+ body,
307
+ postedAt: j.publishedDate
308
+ };
309
+ });
310
+ }
311
+
312
+ // src/jobs/providers/workable.ts
313
+ var workableProvider = {
314
+ id: "workable",
315
+ needsAllowlist: true,
316
+ async fetch(_query, { companies = [], timeoutMs }) {
317
+ const results = await Promise.allSettled(
318
+ companies.map((slug) => fetchCompany4(slug, timeoutMs))
319
+ );
320
+ return results.flatMap((r) => r.status === "fulfilled" ? r.value : []);
321
+ }
322
+ };
323
+ function locationFor(j) {
324
+ const parts = [j.city, j.state, j.country].filter(Boolean);
325
+ return parts.length ? parts.join(", ") : void 0;
326
+ }
327
+ async function fetchCompany4(slug, timeoutMs) {
328
+ const url = `https://apply.workable.com/api/v1/widget/accounts/${encodeURIComponent(slug)}`;
329
+ const data = await fetchJson(url, { timeoutMs });
330
+ const jobs = data.jobs ?? data.results ?? [];
331
+ return jobs.map((j) => {
332
+ const location = locationFor(j);
333
+ const bodyParts = [j.description, j.requirements, j.benefits].filter(Boolean).map((s) => stripHtml(s));
334
+ const body = bodyParts.join("\n\n");
335
+ const postingUrl = j.url ?? j.application_url ?? `https://apply.workable.com/${slug}/j/${j.shortcode ?? j.id}/`;
336
+ return {
337
+ id: `workable:${slug}:${j.id}`,
338
+ source: "workable",
339
+ company: slug,
340
+ title: j.title,
341
+ location,
342
+ remote: j.remote ?? looksRemote(location, body),
343
+ url: postingUrl,
344
+ body,
345
+ postedAt: j.published_on
346
+ };
347
+ });
348
+ }
349
+
350
+ // src/jobs/providers/remoteok.ts
351
+ var remoteokProvider = {
352
+ id: "remoteok",
353
+ needsAllowlist: false,
354
+ async fetch(_query, { timeoutMs }) {
355
+ const url = "https://remoteok.com/api";
356
+ const entries = await fetchJson(url, { timeoutMs });
357
+ return entries.filter((e) => e.id && e.position && e.company).map((e) => {
358
+ const body = stripHtml(e.description ?? "");
359
+ const comp = e.salary_min && e.salary_max ? `$${e.salary_min}\u2013$${e.salary_max}` : void 0;
360
+ return {
361
+ id: `remoteok:${e.id}`,
362
+ source: "remoteok",
363
+ company: e.company,
364
+ title: e.position,
365
+ location: e.location,
366
+ remote: true,
367
+ url: e.url ?? `https://remoteok.com/remote-jobs/${e.slug ?? e.id}`,
368
+ body,
369
+ postedAt: e.date,
370
+ tags: e.tags,
371
+ compensation: comp
372
+ };
373
+ });
374
+ }
375
+ };
376
+
377
+ // src/jobs/providers/remotive.ts
378
+ var remotiveProvider = {
379
+ id: "remotive",
380
+ needsAllowlist: false,
381
+ async fetch(_query, { timeoutMs }) {
382
+ const url = "https://remotive.com/api/remote-jobs";
383
+ const data = await fetchJson(url, { timeoutMs });
384
+ const jobs = data.jobs ?? [];
385
+ return jobs.map((j) => ({
386
+ id: `remotive:${j.id}`,
387
+ source: "remotive",
388
+ company: j.company_name,
389
+ title: j.title,
390
+ location: j.candidate_required_location,
391
+ remote: true,
392
+ url: j.url,
393
+ body: stripHtml(j.description ?? ""),
394
+ postedAt: j.publication_date,
395
+ tags: j.tags,
396
+ compensation: j.salary
397
+ }));
398
+ }
399
+ };
400
+
401
+ // src/jobs/providers/wwr.ts
402
+ var WWR_CATEGORIES = [
403
+ "programming",
404
+ "design",
405
+ "devops",
406
+ "management",
407
+ "product",
408
+ "customer-support",
409
+ "sales-and-marketing"
410
+ ];
411
+ var wwrProvider = {
412
+ id: "wwr",
413
+ needsAllowlist: false,
414
+ async fetch(_query, { timeoutMs }) {
415
+ const results = await Promise.allSettled(
416
+ WWR_CATEGORIES.map((cat) => fetchCategory(cat, timeoutMs))
417
+ );
418
+ return results.flatMap((r) => r.status === "fulfilled" ? r.value : []);
419
+ }
420
+ };
421
+ async function fetchCategory(category, timeoutMs) {
422
+ const url = `https://weworkremotely.com/categories/remote-${category}-jobs.rss`;
423
+ const xml = await fetchText(url, { timeoutMs, accept: "application/rss+xml, application/xml" });
424
+ return parseRss(xml, category);
425
+ }
426
+ var ITEM_RE = /<item>([\s\S]*?)<\/item>/g;
427
+ var CDATA_RE = /<!\[CDATA\[([\s\S]*?)\]\]>/;
428
+ function pickTag(block, tag) {
429
+ const re = new RegExp(`<${tag}>([\\s\\S]*?)<\\/${tag}>`);
430
+ const m = re.exec(block);
431
+ if (!m) return void 0;
432
+ const raw = m[1] ?? "";
433
+ const cdata = CDATA_RE.exec(raw);
434
+ return (cdata && cdata[1] !== void 0 ? cdata[1] : raw).trim();
435
+ }
436
+ function parseRss(xml, category) {
437
+ const items = [];
438
+ let match;
439
+ while ((match = ITEM_RE.exec(xml)) !== null) {
440
+ const block = match[1] ?? "";
441
+ const title = pickTag(block, "title") ?? "";
442
+ const link = pickTag(block, "link") ?? "";
443
+ const guid = pickTag(block, "guid") ?? link;
444
+ const pubDate = pickTag(block, "pubDate");
445
+ const descriptionHtml = pickTag(block, "description") ?? "";
446
+ const body = stripHtml(descriptionHtml);
447
+ const segments = title.split(":").map((s) => s.trim());
448
+ const companyRaw = segments[0] ?? "";
449
+ const titleRest = segments.slice(1);
450
+ const company = companyRaw || "Unknown";
451
+ const cleanTitle = titleRest.length ? titleRest.join(": ") : title;
452
+ items.push({
453
+ id: `wwr:${guid}`,
454
+ source: "wwr",
455
+ company,
456
+ title: cleanTitle,
457
+ location: "Remote",
458
+ remote: true,
459
+ url: link,
460
+ body,
461
+ postedAt: pubDate ? new Date(pubDate).toISOString() : void 0,
462
+ tags: [category]
463
+ });
464
+ }
465
+ return items;
466
+ }
467
+
468
+ // src/jobs/providers/hn-whoishiring.ts
469
+ var hnWhoisHiringProvider = {
470
+ id: "hn-whoishiring",
471
+ needsAllowlist: false,
472
+ async fetch(query, { timeoutMs }) {
473
+ const storyId = await findLatestThread(timeoutMs);
474
+ if (!storyId) return [];
475
+ return fetchPostings(storyId, query, timeoutMs);
476
+ }
477
+ };
478
+ async function findLatestThread(timeoutMs) {
479
+ const url = "https://hn.algolia.com/api/v1/search?tags=story,author_whoishiring&query=who%20is%20hiring&restrictSearchableAttributes=title&hitsPerPage=5";
480
+ const data = await fetchJson(url, { timeoutMs });
481
+ for (const hit of data.hits) {
482
+ const title = (hit.story_title ?? hit.title ?? "").toLowerCase();
483
+ if (title.includes("who is hiring")) {
484
+ return hit.story_id ?? Number(hit.objectID);
485
+ }
486
+ }
487
+ return null;
488
+ }
489
+ async function fetchPostings(storyId, query, timeoutMs) {
490
+ const q = encodeURIComponent(query.terms.slice(0, 3).join(" ") || "remote");
491
+ const url = `https://hn.algolia.com/api/v1/search?tags=comment,story_${storyId}&hitsPerPage=40&query=${q}`;
492
+ const data = await fetchJson(url, { timeoutMs });
493
+ return data.hits.filter((h) => typeof h.comment_text === "string" && h.comment_text.length > 80).map((h) => {
494
+ const body = stripHtml(h.comment_text);
495
+ const { company, title, location } = parseHeader(body);
496
+ return {
497
+ id: `hn-whoishiring:${h.objectID}`,
498
+ source: "hn-whoishiring",
499
+ company,
500
+ title,
501
+ location,
502
+ remote: looksRemote(location, body),
503
+ url: `https://news.ycombinator.com/item?id=${h.objectID}`,
504
+ body,
505
+ postedAt: h.created_at
506
+ };
507
+ });
508
+ }
509
+ function parseHeader(body) {
510
+ const firstLine = body.split("\n")[0] ?? body.slice(0, 200);
511
+ const segments = firstLine.split("|").map((s) => s.trim());
512
+ const head = segments[0] ?? "";
513
+ const locMatch = /\(([^)]+)\)/.exec(head);
514
+ const company = head.replace(/\s*\([^)]+\)/, "").trim() || "HN posting";
515
+ const location = locMatch ? locMatch[1] : void 0;
516
+ const title = segments[1] ?? segments[2] ?? "See description";
517
+ return { company, title, location };
518
+ }
519
+
520
+ // src/jobs/providers/index.ts
521
+ var PROVIDERS = [
522
+ greenhouseProvider,
523
+ leverProvider,
524
+ ashbyProvider,
525
+ workableProvider,
526
+ remoteokProvider,
527
+ remotiveProvider,
528
+ wwrProvider,
529
+ hnWhoisHiringProvider
530
+ ];
531
+ function listProviders() {
532
+ return PROVIDERS;
533
+ }
534
+ function getProvider(id) {
535
+ return PROVIDERS.find((p) => p.id === id);
536
+ }
537
+
538
+ // src/jobs/rank.ts
539
+ function rankPostings(resume, postings) {
540
+ const cfg = loadConfig();
541
+ const seen = /* @__PURE__ */ new Map();
542
+ for (const posting of postings) {
543
+ const key = dedupeKey(posting);
544
+ const ats = analyzeAts(resume, {
545
+ jobDescription: posting.body,
546
+ jobTitle: posting.title,
547
+ language: cfg.locale,
548
+ config: cfg
549
+ });
550
+ const ranked = { ...posting, ats, dedupeKey: key };
551
+ const existing = seen.get(key);
552
+ if (!existing || ranked.ats.score > existing.ats.score) {
553
+ seen.set(key, ranked);
554
+ }
555
+ }
556
+ return Array.from(seen.values()).sort((a, b) => b.ats.score - a.ats.score);
557
+ }
558
+
559
+ // src/jobs/companies.ts
560
+ import fs from "fs";
561
+ import path from "path";
562
+ import { fileURLToPath } from "url";
563
+ var currentDir = path.dirname(fileURLToPath(import.meta.url));
564
+ var CANDIDATE_PATHS = [
565
+ path.resolve(currentDir, "../../data/jobs/companies.json"),
566
+ path.resolve(currentDir, "../data/jobs/companies.json")
567
+ ];
568
+ var cached = null;
569
+ function loadCompaniesFile() {
570
+ if (cached) return cached;
571
+ for (const p of CANDIDATE_PATHS) {
572
+ if (fs.existsSync(p)) {
573
+ cached = JSON.parse(fs.readFileSync(p, "utf-8"));
574
+ return cached;
575
+ }
576
+ }
577
+ cached = {};
578
+ return cached;
579
+ }
580
+ function getSeedCompanies(provider) {
581
+ const file = loadCompaniesFile();
582
+ return file[provider] ?? [];
583
+ }
584
+
585
+ // src/jobs/index.ts
586
+ var DEFAULT_LIMIT = 20;
587
+ var DEFAULT_MIN_SCORE = 85;
588
+ var DEFAULT_TIMEOUT_MS = 8e3;
589
+ async function searchJobs(resume, options = {}) {
590
+ const baseQuery = deriveSearchQuery(resume, { remoteOnly: options.remoteOnly });
591
+ const locationCc = options.location ? options.location.split(",").pop()?.trim().toUpperCase() : void 0;
592
+ const query = locationCc && locationCc.length === 2 ? { ...baseQuery, countryCode: locationCc } : baseQuery;
593
+ const enabled = options.providers ? options.providers.map(getProvider).filter((p) => !!p) : listProviders();
594
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
595
+ const limit = options.limit ?? DEFAULT_LIMIT;
596
+ const minScore = options.minScore ?? DEFAULT_MIN_SCORE;
597
+ const extraCompanies = options.extraCompanies ?? {};
598
+ const providerResults = await Promise.all(
599
+ enabled.map(async (provider) => {
600
+ const started = Date.now();
601
+ try {
602
+ const companies = provider.needsAllowlist ? Array.from(
603
+ /* @__PURE__ */ new Set([
604
+ ...getSeedCompanies(provider.id),
605
+ ...extraCompanies[provider.id] ?? []
606
+ ])
607
+ ) : void 0;
608
+ const postings = await provider.fetch(query, {
609
+ companies,
610
+ timeoutMs
611
+ });
612
+ return {
613
+ providerId: provider.id,
614
+ postings,
615
+ durationMs: Date.now() - started
616
+ };
617
+ } catch (e) {
618
+ return {
619
+ providerId: provider.id,
620
+ postings: [],
621
+ durationMs: Date.now() - started,
622
+ error: e instanceof Error ? e.message : String(e)
623
+ };
624
+ }
625
+ })
626
+ );
627
+ const allPostings = providerResults.flatMap((r) => r.postings);
628
+ const fetchedCount = allPostings.length;
629
+ const { postings: filtered, wrongLocationCount } = applyQueryFilters(allPostings, query);
630
+ const ranked = rankPostings(resume, filtered);
631
+ const offSpecialtyCount = ranked.filter(
632
+ (r) => r.ats.tiers.match?.checks.some((c) => c.id === "role-family-match" && c.status === "fail")
633
+ ).length;
634
+ const passing = ranked.filter((r) => r.ats.score >= minScore);
635
+ const filteredCount = ranked.length - passing.length;
636
+ const jobs = passing.slice(0, limit);
637
+ return {
638
+ query,
639
+ jobs,
640
+ providers: providerResults,
641
+ filteredCount,
642
+ fetchedCount,
643
+ offSpecialtyCount,
644
+ wrongLocationCount
645
+ };
646
+ }
647
+ function scorePosting(resume, posting) {
648
+ const ranked = rankPostings(resume, [
649
+ {
650
+ ...posting,
651
+ tags: void 0,
652
+ postedAt: void 0,
653
+ compensation: void 0
654
+ }
655
+ ]);
656
+ return ranked[0];
657
+ }
658
+ function buildTailorPrompt(posting) {
659
+ return [
660
+ `Tailor my resume for the following job posting at ${posting.company}.`,
661
+ "",
662
+ `# ${posting.title}`,
663
+ posting.location ? `Location: ${posting.location}` : "",
664
+ `URL: ${posting.url}`,
665
+ "",
666
+ "## Job description",
667
+ posting.body,
668
+ "",
669
+ "Use the resuml MCP tools:",
670
+ " 1. resuml_validate to confirm the tailored YAML parses.",
671
+ " 2. resuml_ats_check with the job description above. Target score >= 80.",
672
+ " 3. Show the changes you made and the final score."
673
+ ].filter(Boolean).join("\n");
674
+ }
675
+
676
+ export {
677
+ deriveSearchQuery,
678
+ stripHtml,
679
+ dedupeKey,
680
+ listProviders,
681
+ getProvider,
682
+ searchJobs,
683
+ scorePosting,
684
+ buildTailorPrompt
685
+ };
686
+ //# sourceMappingURL=chunk-QBCXFLW6.js.map