jd-intel 0.11.0 → 0.11.1
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/package.json +1 -2
- package/src/adapters/lever.js +2 -2
- package/src/adapters/recruitee.js +5 -11
- package/src/adapters/teamtailor.js +2 -8
- package/src/adapters/workday.js +5 -15
- package/src/normalizer.js +10 -0
- package/src/registry.js +8 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jd-intel",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "Fetch and normalize job descriptions across seven major ATS (Greenhouse, Lever, Ashby, Workday, and more), for your AI assistant. No copy-paste.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
"scripts": {
|
|
16
16
|
"test": "node --test test/*.test.js",
|
|
17
17
|
"fetch": "node src/cli.js fetch",
|
|
18
|
-
"search": "node src/cli.js search",
|
|
19
18
|
"verify:registry": "node scripts/verify-registry.mjs",
|
|
20
19
|
"sync:registry-pages": "node scripts/sync-pages-registry.mjs",
|
|
21
20
|
"pack:mcpb": "node scripts/build-mcpb.mjs",
|
package/src/adapters/lever.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { normalize, extractSalaryFromText } from '../normalizer.js';
|
|
1
|
+
import { normalize, extractSalaryFromText, toIso } from '../normalizer.js';
|
|
2
2
|
import { atsErrorFromStatus } from '../errors.js';
|
|
3
3
|
import { atsFetch, probeResult } from '../http.js';
|
|
4
4
|
|
|
@@ -44,7 +44,7 @@ export async function fetchLever(slug) {
|
|
|
44
44
|
workplace: job.workplaceType,
|
|
45
45
|
description: buildDescription(job),
|
|
46
46
|
url: job.hostedUrl || '',
|
|
47
|
-
postedAt:
|
|
47
|
+
postedAt: toIso(job.createdAt),
|
|
48
48
|
salary: parseLeverSalary(job.salaryRange, job.text),
|
|
49
49
|
metadata: {
|
|
50
50
|
leverId: job.id,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { normalize } from '../normalizer.js';
|
|
1
|
+
import { normalize, toIso } from '../normalizer.js';
|
|
2
2
|
import { atsErrorFromStatus } from '../errors.js';
|
|
3
3
|
import { atsFetch, probeResult } from '../http.js';
|
|
4
4
|
import { orgHost } from '../boards.js';
|
|
@@ -51,7 +51,7 @@ export async function fetchRecruitee(slug, ctx = {}) {
|
|
|
51
51
|
let location = place;
|
|
52
52
|
if (offer.remote) location = place ? `Remote - ${place}` : 'Remote';
|
|
53
53
|
|
|
54
|
-
const createdAt =
|
|
54
|
+
const createdAt = recruiteeDate(offer.created_at);
|
|
55
55
|
|
|
56
56
|
return normalize({
|
|
57
57
|
companySlug: slug,
|
|
@@ -65,7 +65,7 @@ export async function fetchRecruitee(slug, ctx = {}) {
|
|
|
65
65
|
url: offer.careers_url || offer.careers_apply_url || '',
|
|
66
66
|
// created_at can predate publication by years on long-lived offers,
|
|
67
67
|
// so it is not a posting date. published_at is.
|
|
68
|
-
postedAt:
|
|
68
|
+
postedAt: recruiteeDate(offer.published_at) || createdAt,
|
|
69
69
|
salary: parseRecruiteeSalary(offer.salary),
|
|
70
70
|
metadata: {
|
|
71
71
|
recruiteeId: offer.guid || offer.id,
|
|
@@ -77,14 +77,8 @@ export async function fetchRecruitee(slug, ctx = {}) {
|
|
|
77
77
|
});
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
*/
|
|
83
|
-
function toIso(ts) {
|
|
84
|
-
if (!ts) return null;
|
|
85
|
-
const d = new Date(ts.replace(' UTC', 'Z').replace(' ', 'T'));
|
|
86
|
-
return Number.isNaN(d.getTime()) ? null : d.toISOString();
|
|
87
|
-
}
|
|
80
|
+
// Recruitee returns "2026-05-13 07:38:11 UTC".
|
|
81
|
+
const recruiteeDate = (ts) => toIso(ts?.replace(' UTC', 'Z').replace(' ', 'T'));
|
|
88
82
|
|
|
89
83
|
/**
|
|
90
84
|
* Recruitee sends three booleans, not one enum. Hybrid wins when remote is
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { normalize, decodeEntities } from '../normalizer.js';
|
|
1
|
+
import { normalize, decodeEntities, toIso } from '../normalizer.js';
|
|
2
2
|
import { atsErrorFromStatus } from '../errors.js';
|
|
3
3
|
import { atsFetch } from '../http.js';
|
|
4
4
|
import { orgHost } from '../boards.js';
|
|
@@ -116,12 +116,6 @@ export async function fetchTeamtailor(slug, ctx = {}) {
|
|
|
116
116
|
location = location ? `Remote - ${location}` : 'Remote';
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
let postedAt = null;
|
|
120
|
-
if (pubDateRaw) {
|
|
121
|
-
const d = new Date(pubDateRaw);
|
|
122
|
-
if (!Number.isNaN(d.getTime())) postedAt = d.toISOString();
|
|
123
|
-
}
|
|
124
|
-
|
|
125
119
|
return normalize({
|
|
126
120
|
companySlug: slug,
|
|
127
121
|
company,
|
|
@@ -132,7 +126,7 @@ export async function fetchTeamtailor(slug, ctx = {}) {
|
|
|
132
126
|
workplace: REMOTE_STATUS[remoteStatus.toLowerCase()] || null,
|
|
133
127
|
description: decodeEntities(pick('description')),
|
|
134
128
|
url: link,
|
|
135
|
-
postedAt,
|
|
129
|
+
postedAt: toIso(pubDateRaw),
|
|
136
130
|
salary: null, // No structured salary; normalizer parses from text
|
|
137
131
|
metadata: {
|
|
138
132
|
teamtailorId: guid,
|
package/src/adapters/workday.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { normalize, missingContent } from '../normalizer.js';
|
|
1
|
+
import { normalize, missingContent, toIso } from '../normalizer.js';
|
|
2
2
|
import { atsErrorFromStatus } from '../errors.js';
|
|
3
3
|
import { prefilterRows } from '../filters.js';
|
|
4
4
|
import { atsFetch } from '../http.js';
|
|
@@ -151,7 +151,8 @@ export async function fetchWorkday(slug, ctx = {}) {
|
|
|
151
151
|
workplace: parseWorkdayRemoteType(info.remoteType),
|
|
152
152
|
description: info.jobDescription || '',
|
|
153
153
|
url: `https://${tenant}.${env}.myworkdayjobs.com/${site}${externalPath}`,
|
|
154
|
-
|
|
154
|
+
// startDate is "2026-05-01" or "May 1, 2026".
|
|
155
|
+
postedAt: toIso(info.startDate) || normalizePostedOn(p.postedOn),
|
|
155
156
|
salary: null, // normalizer extracts from description text
|
|
156
157
|
content,
|
|
157
158
|
metadata: {
|
|
@@ -223,23 +224,12 @@ function withinDays(postedOn, days) {
|
|
|
223
224
|
* so the library's postedWithinDays re-filter has a value to compare.
|
|
224
225
|
*/
|
|
225
226
|
function normalizePostedOn(v) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (Number.isFinite(direct.getTime())) return direct.toISOString();
|
|
227
|
+
const direct = toIso(v);
|
|
228
|
+
if (direct) return direct;
|
|
229
229
|
const n = daysAgo(v);
|
|
230
230
|
return n === null ? null : new Date(Date.now() - n * 86400000).toISOString();
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
-
/**
|
|
234
|
-
* Workday detail `startDate` ("2026-05-01" or "May 1, 2026"). Return
|
|
235
|
-
* ISO, or null if unparseable.
|
|
236
|
-
*/
|
|
237
|
-
function parseWorkdayDate(s) {
|
|
238
|
-
if (!s) return null;
|
|
239
|
-
const d = new Date(s);
|
|
240
|
-
return Number.isFinite(d.getTime()) ? d.toISOString() : null;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
233
|
/**
|
|
244
234
|
* Registry-only invariant: the {tenant,env,site} triple can't be
|
|
245
235
|
* probed from a company name. Always false so detect_ats never selects
|
package/src/normalizer.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* A date value (string or epoch ms) as an ISO string, or null when it is
|
|
5
|
+
* missing or does not parse.
|
|
6
|
+
*/
|
|
7
|
+
export function toIso(value) {
|
|
8
|
+
if (!value) return null;
|
|
9
|
+
const d = new Date(value);
|
|
10
|
+
return Number.isNaN(d.getTime()) ? null : d.toISOString();
|
|
11
|
+
}
|
|
12
|
+
|
|
3
13
|
/**
|
|
4
14
|
* Generate a stable ID for a job posting.
|
|
5
15
|
*/
|
package/src/registry.js
CHANGED
|
@@ -7,11 +7,6 @@ import { ADAPTERS, ATS_NAMES } from './adapters/index.js';
|
|
|
7
7
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
8
|
const REGISTRY_DIR = join(__dirname, '..', 'registry');
|
|
9
9
|
|
|
10
|
-
// The one order the registry is ever walked in. Lookups, detectAts and the
|
|
11
|
-
// loaded object all follow it, so which file answers for a slug does not
|
|
12
|
-
// depend on which file's load finished first (issue #87).
|
|
13
|
-
const PLATFORMS = ATS_NAMES;
|
|
14
|
-
|
|
15
10
|
// Network-first registry. A hosted copy lets installed bundles AND npx users
|
|
16
11
|
// pick up newly-added companies without reinstalling; the on-disk copy that
|
|
17
12
|
// ships with the package is the guaranteed offline fallback. The base URL is
|
|
@@ -73,8 +68,11 @@ async function loadPlatform(platform) {
|
|
|
73
68
|
*/
|
|
74
69
|
export async function loadRegistry(ats) {
|
|
75
70
|
if (ats) return loadPlatform(ats);
|
|
76
|
-
|
|
77
|
-
|
|
71
|
+
// ATS_NAMES is the one order the registry is ever walked in. Lookups,
|
|
72
|
+
// detectAts and this object all follow it, so which file answers for a
|
|
73
|
+
// slug does not depend on which file's load finished first (issue #87).
|
|
74
|
+
const lists = await Promise.all(ATS_NAMES.map(loadPlatform));
|
|
75
|
+
return Object.fromEntries(ATS_NAMES.map((platform, i) => [platform, lists[i]]));
|
|
78
76
|
}
|
|
79
77
|
|
|
80
78
|
/**
|
|
@@ -127,7 +125,7 @@ export async function searchRegistry(query) {
|
|
|
127
125
|
// normalized forms keeps registry-first routing working for those.
|
|
128
126
|
export const normSlug = (s) => String(s).toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
129
127
|
|
|
130
|
-
const byPlatform = (a, b) =>
|
|
128
|
+
const byPlatform = (a, b) => ATS_NAMES.indexOf(a.ats) - ATS_NAMES.indexOf(b.ats);
|
|
131
129
|
|
|
132
130
|
/**
|
|
133
131
|
* Look up which ATS a slug belongs to in the registry.
|
|
@@ -143,7 +141,7 @@ export async function findAtsBySlug(slug) {
|
|
|
143
141
|
* Unlike findAtsBySlug (returns just the ats name), this returns the
|
|
144
142
|
* whole entry so callers can read adapter-specific config (e.g. the
|
|
145
143
|
* Workday {tenant, env, site} triple). The files are searched in
|
|
146
|
-
*
|
|
144
|
+
* ATS_NAMES order, so the first match is the same on every call.
|
|
147
145
|
*
|
|
148
146
|
* @returns {Promise<{ats: string, entry: object}|null>}
|
|
149
147
|
*/
|
|
@@ -167,7 +165,7 @@ export async function findEntryBySlug(slug) {
|
|
|
167
165
|
* adds nothing, and an AtsError (429, 5xx, 401, network) goes to `failed`
|
|
168
166
|
* with its code, so a board the probe could not check never reads as
|
|
169
167
|
* absent (issue #55). Any other error is a bug and is rethrown. Both lists
|
|
170
|
-
* come back in
|
|
168
|
+
* come back in ATS_NAMES order, never in completion order.
|
|
171
169
|
*
|
|
172
170
|
* @returns {Promise<{
|
|
173
171
|
* boards: Array<{ ats: string, slug: string, source: 'registry'|'probe' }>,
|