@servation/job-search-mcp 0.1.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,282 @@
1
+ /**
2
+ * find_jobs orchestration: refresh the registry, run all scrapers in parallel,
3
+ * convert to our Job model, structurally filter + dedup URLs, optionally
4
+ * network-verify, persist unscored jobs to the store, and return them.
5
+ *
6
+ * Scoring is NOT done here — jobs come back unscored (matchScore = -1). Claude
7
+ * evaluates them later (evaluate_jobs, Phase 4) and the server scores via the
8
+ * ported computeMatchScore.
9
+ */
10
+ import crypto from "node:crypto";
11
+ import { globalState } from "./config.js";
12
+ import { readDb, writeDb } from "./db.js";
13
+ import { updateCompanyDirectoriesFromRegistry, fetchGreenhouseJobs, fetchLeverJobs, fetchAshbyJobs, fetchWorkdayJobs, fetchSmartRecruitersJobs, fetchRemoteOKJobs, fetchRemotiveJobs, fetchHackerNewsJobs, fetchLinkedInJobs, fetchLinkedInJobDetail, linkedInJobIdFromUrl, } from "./sourcing.js";
14
+ import { extractRoleKeywords, isSpecificJobPost, normalizeJobUrl, verifyJobUrl, asyncMapConcurrent } from "./utils.js";
15
+ const DEFAULT_LIMIT = 15;
16
+ const cleanStr = (s) => s.toLowerCase().trim();
17
+ // Recency ledger: a job shown within this window is not re-surfaced even after it ages
18
+ // off the board or is cleared. Past the window it may resurface (catches genuine re-posts).
19
+ const SEEN_WINDOW_MS = 182 * 24 * 60 * 60 * 1000; // ~6 months
20
+ const SEEN_MAX = 8000;
21
+ /** Drop ledger entries older than `cutoff` (ms epoch) and cap to the most recent `max`. */
22
+ export function capLedger(m, cutoff, max) {
23
+ let entries = Object.entries(m).filter(([, ts]) => Date.parse(ts) >= cutoff);
24
+ if (entries.length > max) {
25
+ entries.sort((a, b) => Date.parse(b[1]) - Date.parse(a[1]));
26
+ entries = entries.slice(0, max);
27
+ }
28
+ return Object.fromEntries(entries);
29
+ }
30
+ function normType(t) {
31
+ const s = (t || "").toLowerCase();
32
+ if (s.includes("contract"))
33
+ return "Contract";
34
+ if (s.includes("part"))
35
+ return "Part-Time";
36
+ return "Full-Time";
37
+ }
38
+ /** Stable id from the normalized URL (falls back to title|company), prefixed by source. */
39
+ function stableId(r) {
40
+ const basis = normalizeJobUrl(r.url) || `${cleanStr(r.title)}|${cleanStr(r.company)}`;
41
+ return `${r.source}_${crypto.createHash("sha1").update(basis).digest("hex").slice(0, 12)}`;
42
+ }
43
+ function rawToJob(r) {
44
+ return {
45
+ id: stableId(r),
46
+ title: r.title,
47
+ company: r.company,
48
+ location: r.location,
49
+ salary: r.salary,
50
+ type: normType(r.type),
51
+ isW2: true,
52
+ description: r.description,
53
+ url: r.url,
54
+ postedAt: r.postedAt,
55
+ matchScore: -1, // unscored until Claude evaluates it
56
+ isDuplicate: false,
57
+ status: "discovered",
58
+ scannedAt: new Date().toISOString(),
59
+ isRemote: r.isRemote,
60
+ sourceTag: r.source,
61
+ };
62
+ }
63
+ /**
64
+ * Round-robin across sources (one per source per cycle) and skip a posting whose
65
+ * company already hit the cap. maxPerCompany <= 0 disables the cap.
66
+ */
67
+ function interleaveAndCap(raws, maxPerCompany) {
68
+ const bySource = new Map();
69
+ for (const r of raws) {
70
+ const arr = bySource.get(r.source) ?? [];
71
+ arr.push(r);
72
+ bySource.set(r.source, arr);
73
+ }
74
+ const queues = [...bySource.values()];
75
+ const companyCount = new Map();
76
+ const out = [];
77
+ let active = true;
78
+ while (active) {
79
+ active = false;
80
+ for (const q of queues) {
81
+ while (q.length) {
82
+ active = true;
83
+ const cand = q.shift();
84
+ const c = cleanStr(cand.company);
85
+ const n = companyCount.get(c) ?? 0;
86
+ if (maxPerCompany > 0 && n >= maxPerCompany)
87
+ continue; // over cap, drop and try next in this source
88
+ companyCount.set(c, n + 1);
89
+ out.push(cand);
90
+ break; // move to next source for round-robin variety
91
+ }
92
+ }
93
+ }
94
+ return out;
95
+ }
96
+ /** Map candidate years of experience to a LinkedIn seniority level (undefined = unknown). */
97
+ export function yoeToLinkedInLevel(years) {
98
+ if (years < 1)
99
+ return undefined;
100
+ if (years < 2)
101
+ return "entry level";
102
+ if (years <= 4)
103
+ return "associate";
104
+ if (years <= 9)
105
+ return "senior";
106
+ if (years <= 14)
107
+ return "director";
108
+ return "executive";
109
+ }
110
+ /** Effective search inputs: stored profile, with criteria overrides; synthesized if no profile yet. */
111
+ function searchInputs(profile, criteria) {
112
+ const targetRoles = criteria.query
113
+ ? [criteria.query, ...(profile?.targetRoles ?? [])]
114
+ : profile?.targetRoles?.length
115
+ ? profile.targetRoles
116
+ : ["Software Engineer"];
117
+ const keywords = extractRoleKeywords(targetRoles);
118
+ const searchLocation = criteria.location ?? profile?.searchLocation ?? "";
119
+ const prefersRemote = criteria.remote ? criteria.remote.toLowerCase() === "remote" : profile?.prefersRemote ?? true;
120
+ // max_years overrides the profile's years for this search (drives both seniority blocklisting and level mapping).
121
+ const yoe = criteria.maxYears ?? profile?.yearsOfExperience ?? 0;
122
+ const skills = profile?.parsedSkills ?? [];
123
+ // LinkedIn does its own keyword search, so it wants a phrase, not our tokenized list.
124
+ const keyword = criteria.query || profile?.targetRoles?.[0] || "Software Engineer";
125
+ // Default the LinkedIn seniority filter from the candidate's years when not given explicitly.
126
+ const experienceLevel = criteria.experienceLevel ?? yoeToLinkedInLevel(yoe);
127
+ return { targetRoles, keywords, searchLocation, prefersRemote, yoe, skills, keyword, experienceLevel };
128
+ }
129
+ /**
130
+ * Run all scrapers, dedup, optionally verify, persist unscored jobs, and return the fresh ones.
131
+ */
132
+ export async function sourceJobs(criteria) {
133
+ await updateCompanyDirectoriesFromRegistry();
134
+ const db = readDb();
135
+ const profile = db.profile;
136
+ const { targetRoles, keywords, searchLocation, prefersRemote, yoe, skills, keyword, experienceLevel } = searchInputs(profile, criteria);
137
+ const limit = criteria.limit ?? profile?.maxDiscoveredJobs ?? DEFAULT_LIMIT;
138
+ const liQuery = {
139
+ keyword,
140
+ location: searchLocation,
141
+ targetRoles,
142
+ yearsOfExperience: yoe,
143
+ prefersRemote,
144
+ datePosted: criteria.datePosted,
145
+ remote: criteria.remote,
146
+ jobType: criteria.jobType,
147
+ experienceLevel,
148
+ salaryMin: criteria.salaryMin,
149
+ sortBy: criteria.sortBy,
150
+ maxApplicants: criteria.maxApplicants,
151
+ limit,
152
+ };
153
+ // Build the source fan-out, honoring the optional `sources` filter. An unset OR
154
+ // empty array means "all 9 sources" (empty must not select zero).
155
+ const srcFilter = criteria.sources && criteria.sources.length ? criteria.sources : undefined;
156
+ const wantSrc = (name) => !srcFilter || srcFilter.includes(name);
157
+ const tasks = [];
158
+ if (wantSrc("linkedin"))
159
+ tasks.push(fetchLinkedInJobs(liQuery));
160
+ if (wantSrc("greenhouse"))
161
+ tasks.push(fetchGreenhouseJobs(globalState.cachedGreenhouseSlugs, keywords, targetRoles, searchLocation, prefersRemote, yoe));
162
+ if (wantSrc("lever"))
163
+ tasks.push(fetchLeverJobs(globalState.cachedLeverSlugs, keywords, targetRoles, searchLocation, prefersRemote, yoe));
164
+ if (wantSrc("ashby"))
165
+ tasks.push(fetchAshbyJobs(globalState.cachedAshbySlugs, keywords, targetRoles, searchLocation, prefersRemote, yoe));
166
+ if (wantSrc("workday"))
167
+ tasks.push(fetchWorkdayJobs(globalState.cachedWorkdayDirectory, keywords, targetRoles, searchLocation, prefersRemote, yoe));
168
+ if (wantSrc("smartrecruiters"))
169
+ tasks.push(fetchSmartRecruitersJobs(globalState.cachedSmartRecruitersDirectory, keywords, targetRoles, searchLocation, prefersRemote, yoe));
170
+ if (wantSrc("hackernews"))
171
+ tasks.push(fetchHackerNewsJobs(keywords, skills, targetRoles, searchLocation, prefersRemote, yoe));
172
+ if (wantSrc("remoteok"))
173
+ tasks.push(fetchRemoteOKJobs(keywords, skills, targetRoles, searchLocation, prefersRemote, yoe));
174
+ if (wantSrc("remotive"))
175
+ tasks.push(fetchRemotiveJobs(keywords, skills, targetRoles, searchLocation, prefersRemote, yoe));
176
+ // Each scraper already swallows its own errors and returns []; allSettled is a backstop.
177
+ const settled = await Promise.allSettled(tasks);
178
+ const raw = settled.flatMap((s) => (s.status === "fulfilled" ? s.value : []));
179
+ const sourced = raw.length;
180
+ // Structural URL filter (no network): drop generic career/root pages.
181
+ const structurallyOk = raw.filter((r) => r.url && isSpecificJobPost(r.url));
182
+ // Recency ledger of jobs shown within the last ~6 months (survives board age-out / clear_jobs).
183
+ const cutoff = Date.now() - SEEN_WINDOW_MS;
184
+ const ledger = db.seen ?? {};
185
+ const ledgerActive = new Set();
186
+ for (const [k, ts] of Object.entries(ledger))
187
+ if (Date.parse(ts) >= cutoff)
188
+ ledgerActive.add(k);
189
+ // Dedup vs the store (board + tracker + dismissed), the recency ledger, and within this batch.
190
+ const existing = new Set();
191
+ for (const j of [...db.scannedJobs, ...db.savedJobs, ...db.dismissedJobs]) {
192
+ existing.add(`${cleanStr(j.title)}|${cleanStr(j.company)}`);
193
+ if (j.url)
194
+ existing.add(normalizeJobUrl(j.url));
195
+ }
196
+ const batchSeen = new Set();
197
+ const freshRaw = [];
198
+ for (const r of structurallyOk) {
199
+ const kTitle = `${cleanStr(r.title)}|${cleanStr(r.company)}`;
200
+ const kUrl = normalizeJobUrl(r.url);
201
+ if (existing.has(kTitle) ||
202
+ existing.has(kUrl) ||
203
+ ledgerActive.has(kTitle) ||
204
+ ledgerActive.has(kUrl) ||
205
+ batchSeen.has(kTitle) ||
206
+ batchSeen.has(kUrl))
207
+ continue;
208
+ batchSeen.add(kTitle);
209
+ batchSeen.add(kUrl);
210
+ freshRaw.push(r);
211
+ }
212
+ // Interleave round-robin across sources and enforce the per-company cap, so the
213
+ // returned set is diverse instead of dominated by whichever source is largest.
214
+ const limitCompany = profile?.limitCompanyMatches !== false;
215
+ const maxPerCompany = limitCompany ? profile?.maxMatchesPerCompany ?? 3 : 0;
216
+ const ordered = interleaveAndCap(freshRaw, maxPerCompany);
217
+ // When filtering by applicant count, over-fetch candidates (bounded) so we can drop
218
+ // over-cap LinkedIn jobs and still return ~limit.
219
+ const candidateCount = criteria.maxApplicants !== undefined ? Math.min(limit * 3, 40) : limit;
220
+ let candidates = ordered.slice(0, candidateCount).map(rawToJob);
221
+ // LinkedIn detail pass: full JD + applicant count (their search results carry neither).
222
+ // Bounded to the LinkedIn candidates, so only a few detail requests.
223
+ const liCandidates = candidates.filter((j) => j.sourceTag === "linkedin");
224
+ const earlyIds = new Set();
225
+ if (liCandidates.length) {
226
+ const details = await asyncMapConcurrent(liCandidates, 5, async (j) => {
227
+ const id = linkedInJobIdFromUrl(j.url);
228
+ const d = id ? await fetchLinkedInJobDetail(id) : { description: "", applicants: null, early: false };
229
+ return { id: j.id, d };
230
+ });
231
+ const byId = new Map(details.map((x) => [x.id, x.d]));
232
+ for (const x of details)
233
+ if (x.d.early)
234
+ earlyIds.add(x.id);
235
+ candidates = candidates.map((j) => {
236
+ const d = byId.get(j.id);
237
+ if (!d)
238
+ return j;
239
+ return {
240
+ ...j,
241
+ description: d.description.length > (j.description?.length ?? 0) ? d.description : j.description,
242
+ applicants: d.applicants ?? j.applicants,
243
+ isFullDescriptionFetched: d.description ? true : j.isFullDescriptionFetched,
244
+ };
245
+ });
246
+ }
247
+ // Applicant cap: drop a LinkedIn job only when its EXACT count is known and over the
248
+ // cap. "Early applicant" jobs (upper-bound counts) and unknown counts are kept — those
249
+ // are the low-competition jobs the cap is meant to surface.
250
+ if (criteria.maxApplicants !== undefined) {
251
+ const cap = criteria.maxApplicants;
252
+ candidates = candidates.filter((j) => j.applicants == null || earlyIds.has(j.id) || j.applicants <= cap);
253
+ }
254
+ let kept = candidates.slice(0, limit);
255
+ // Optional network verification (off by default to keep find_jobs fast). Non-fatal:
256
+ // we mark isUrlVerified but never drop a job for a blocked/slow fetch.
257
+ if (criteria.verifyUrls) {
258
+ kept = await asyncMapConcurrent(kept, 8, async (j) => {
259
+ const { isValid, resolvedUrl } = await verifyJobUrl(j.url);
260
+ return { ...j, isUrlVerified: isValid, url: resolvedUrl || j.url };
261
+ });
262
+ }
263
+ else {
264
+ kept = kept.map((j) => ({ ...j, isUrlVerified: true })); // passed the structural check
265
+ }
266
+ // Persist: prepend the new unscored jobs; writeDb() dedups vs saved/dismissed and caps per company.
267
+ const maxTotal = Math.max(limit, profile?.maxDiscoveredJobs ?? 100);
268
+ db.scannedJobs = [...kept, ...db.scannedJobs].slice(0, maxTotal);
269
+ db.stats.totalSourced += sourced;
270
+ db.stats.totalScanned += kept.length;
271
+ // Record the jobs we're showing into the recency ledger; prune >6mo and cap.
272
+ const nowIso = new Date().toISOString();
273
+ for (const j of kept) {
274
+ ledger[`${cleanStr(j.title)}|${cleanStr(j.company)}`] = nowIso;
275
+ if (j.url)
276
+ ledger[normalizeJobUrl(j.url)] = nowIso;
277
+ }
278
+ db.seen = capLedger(ledger, cutoff, SEEN_MAX);
279
+ writeDb(db);
280
+ return { jobs: kept, profile, sourced, fresh: freshRaw.length, kept: kept.length };
281
+ }
282
+ //# sourceMappingURL=pipeline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipeline.js","sourceRoot":"","sources":["../pipeline.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,73 @@
1
+ const SCORE_PENALTY_PER_YEAR = 7; // points off per year of experience short
2
+ const SCORE_MAX_EXP_PENALTY = 21; // cap on the experience penalty
3
+ const SCORE_MUSTHAVE_CAP = 55; // ceiling when an explicit must-have skill is missing
4
+ const SCORE_SPECIALIST_CAP = 40; // ceiling when a required formal credential is missing
5
+ const SCORE_CEIL = 97; // no posting is a literal 100% match
6
+ /**
7
+ * Deterministically computes a 0-100 match score from extracted facts.
8
+ * Reproducible (no LLM arithmetic), never negative or saturated, and self-explaining.
9
+ */
10
+ export function computeMatchScore(f, yoe) {
11
+ const matched = Array.isArray(f?.matched) ? f.matched : [];
12
+ const missing = Array.isArray(f?.missing) ? f.missing : [];
13
+ const core = Array.isArray(f?.coreRequirements) ? f.coreRequirements : [];
14
+ const matchedN = matched.length;
15
+ const totalN = Math.max(core.length, matchedN + missing.length, 1);
16
+ const base = Math.round((100 * matchedN) / totalN);
17
+ const expGap = Math.max(0, (Number(f?.requiredYears) || 0) - yoe);
18
+ const expPenalty = Math.min(expGap * SCORE_PENALTY_PER_YEAR, SCORE_MAX_EXP_PENALTY);
19
+ let score = base - expPenalty;
20
+ if (f?.mustHaveMissing)
21
+ score = Math.min(score, SCORE_MUSTHAVE_CAP);
22
+ if (f?.specialistGapMissing)
23
+ score = Math.min(score, SCORE_SPECIALIST_CAP);
24
+ score = Math.max(0, Math.min(SCORE_CEIL, score));
25
+ const reason = `Met ${matchedN}/${totalN} core` +
26
+ (missing.length ? ` (missing: ${missing.slice(0, 3).join(", ")})` : "") +
27
+ "." +
28
+ (expGap > 0 ? ` Exp: needs ${f.requiredYears}y, has ${yoe}y (-${expPenalty}).` : "") +
29
+ (f?.mustHaveMissing ? " Must-have gap." : "") +
30
+ (f?.specialistGapMissing ? " Specialist gap." : "");
31
+ return { score, reason };
32
+ }
33
+ /** Apply Claude's extracted facts to a job: score it and copy through the extracted fields. */
34
+ export function applyFactsToJob(job, facts, yoe) {
35
+ const { score, reason } = computeMatchScore(facts, yoe);
36
+ const updated = {
37
+ ...job,
38
+ matchScore: score,
39
+ matchReason: reason,
40
+ skillsRequired: facts.coreRequirements,
41
+ industry: facts.industry ?? job.industry,
42
+ experienceLevel: facts.experienceLevel ?? job.experienceLevel,
43
+ salaryNum: facts.salaryNum ?? job.salaryNum,
44
+ };
45
+ if (!updated.salary && facts.salaryNum) {
46
+ updated.salary = `$${Math.round(facts.salaryNum / 1000)}k`;
47
+ }
48
+ return updated;
49
+ }
50
+ /**
51
+ * ACTIVE scoring path: apply Claude's HOLISTIC score + reason to a job (clamped 0-100).
52
+ * The host model (Claude) judges fit directly — it's far better calibrated than the
53
+ * local LLMs the deterministic computeMatchScore above was built to guard against.
54
+ * computeMatchScore/applyFactsToJob are kept as the deterministic fallback (rewire
55
+ * evaluate_jobs to them if ever scoring with a weaker model that over-ranks).
56
+ */
57
+ export function applyHolisticScore(job, ev) {
58
+ const score = Math.max(0, Math.min(100, Math.round(ev.score)));
59
+ const updated = { ...job, matchScore: score, matchReason: ev.reason };
60
+ if (ev.experienceLevel !== undefined)
61
+ updated.experienceLevel = ev.experienceLevel;
62
+ if (ev.industry !== undefined)
63
+ updated.industry = ev.industry;
64
+ if (ev.salaryNum !== undefined) {
65
+ updated.salaryNum = ev.salaryNum;
66
+ if (!updated.salary)
67
+ updated.salary = `$${Math.round(ev.salaryNum / 1000)}k`;
68
+ }
69
+ if (ev.skills !== undefined)
70
+ updated.skillsRequired = ev.skills;
71
+ return updated;
72
+ }
73
+ //# sourceMappingURL=scoring.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scoring.js","sourceRoot":"","sources":["../scoring.ts"],"names":[],"mappings":"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"}