openings 0.1.49 → 0.1.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/package.json +1 -1
- package/src/crawler.ts +32 -11
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Openings
|
|
2
2
|
|
|
3
|
+
[](https://smithery.ai/servers/avagama/openings)
|
|
4
|
+
|
|
3
5
|
A free, candidate-safe job search for AI agents.
|
|
4
6
|
|
|
5
7
|
Openings indexes public company job boards into a private index on your machine and exposes it to any MCP client. Your agent can find roles that fit a resume, explain the fit with evidence, and propose truthful resume improvements. Installed this way there are no accounts and no API keys; there are never model calls and never a way to submit an application.
|
package/package.json
CHANGED
package/src/crawler.ts
CHANGED
|
@@ -45,26 +45,47 @@ export function createCrawler(options: CrawlerOptions): Crawler {
|
|
|
45
45
|
const pacingSleep = options.pacingSleep ?? ((delayMs: number) => new Promise<void>((resolve) => setTimeout(resolve, delayMs)));
|
|
46
46
|
let previousStart: number | undefined;
|
|
47
47
|
let pacingGate = Promise.resolve();
|
|
48
|
-
|
|
48
|
+
/** Ordered, because the budget below is spent in this order: the first country named is served first. */
|
|
49
|
+
const describeOrder = options.describeCountries ?? [];
|
|
50
|
+
const describeCountries = new Set(describeOrder);
|
|
51
|
+
/** Where a job sits in the country priority; lower is sooner. Jobs outside the list are never fetched. */
|
|
52
|
+
function describeRank(job: Job): number {
|
|
53
|
+
let best = describeOrder.length;
|
|
54
|
+
for (const code of job.eligibleCountries) {
|
|
55
|
+
const rank = describeOrder.indexOf(code);
|
|
56
|
+
if (rank >= 0 && rank < best) best = rank;
|
|
57
|
+
}
|
|
58
|
+
return best;
|
|
59
|
+
}
|
|
49
60
|
|
|
50
61
|
/** Required experience for every job: from its description, the last crawl's reading, or a paced detail fetch for recent roles in describeCountries. */
|
|
51
62
|
async function withExperience(source: Company, jobs: Job[], previous: Job[] | undefined): Promise<Job[]> {
|
|
52
63
|
const known = new Map((previous ?? []).map(normalizeJobExperience).filter((job) => job.experienceVersion === EXPERIENCE_VERSION && job.experience !== undefined).map((job) => [job.id, job]));
|
|
53
64
|
const since = now().getTime() - 30 * 86_400_000;
|
|
54
|
-
const deadline = Date.now() + DESCRIBE_BUDGET_MS;
|
|
55
|
-
let budget = DESCRIBE_LIMIT;
|
|
56
65
|
const out: Job[] = [];
|
|
66
|
+
const pending: number[] = [];
|
|
57
67
|
for (const job of jobs) {
|
|
58
68
|
if (job.description.trim() || job.experienceVersion === EXPERIENCE_VERSION) out.push(normalizeJobExperience(job));
|
|
59
69
|
else if (known.has(job.id)) out.push({ ...job, description: known.get(job.id)!.description, experience: known.get(job.id)!.experience, experienceVersion: EXPERIENCE_VERSION });
|
|
60
|
-
else
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
else {
|
|
71
|
+
if (options.describe && describeRank(job) < describeOrder.length && Date.parse(job.updatedAt ?? "") >= since) pending.push(out.length);
|
|
72
|
+
out.push(normalizeJobExperience(job));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// The budget is small and shared, so it is spent by country priority rather than in whatever order the board
|
|
76
|
+
// listed its jobs. Without this, an employer with 4 India roles among 11,581 American ones never reads the four.
|
|
77
|
+
// Ties keep the board's own order, which is newest first on every provider that dates its postings.
|
|
78
|
+
pending.sort((left, right) => describeRank(jobs[left]!) - describeRank(jobs[right]!) || left - right);
|
|
79
|
+
const deadline = Date.now() + DESCRIBE_BUDGET_MS;
|
|
80
|
+
let budget = DESCRIBE_LIMIT;
|
|
81
|
+
for (const index of pending) {
|
|
82
|
+
if (budget <= 0 || Date.now() >= deadline) break;
|
|
83
|
+
budget -= 1;
|
|
84
|
+
try {
|
|
85
|
+
if (options.workdayPageDelayMs) await pacingSleep(options.workdayPageDelayMs);
|
|
86
|
+
const description = await options.describe!(source, jobs[index]!);
|
|
87
|
+
out[index] = normalizeJobExperience({ ...jobs[index]!, description });
|
|
88
|
+
} catch { /* unread; the next crawl tries again */ }
|
|
68
89
|
}
|
|
69
90
|
return out;
|
|
70
91
|
}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.1.
|
|
1
|
+
export const VERSION = "0.1.51";
|