jd-intel 0.5.0 → 0.6.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.
- package/package.json +1 -1
- package/src/cli.js +55 -8
- package/src/index.js +27 -7
- package/src/registry.js +10 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jd-intel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Fetch and normalize job descriptions across every 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",
|
package/src/cli.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* jd-intel CLI
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
|
-
* jd-intel fetch <company> [--ats
|
|
7
|
+
* jd-intel fetch <company> [--ats <platform>] [--filter keyword|pattern]
|
|
8
8
|
* jd-intel detect <company>
|
|
9
9
|
* jd-intel registry search <query>
|
|
10
10
|
*/
|
|
@@ -18,12 +18,12 @@ async function main() {
|
|
|
18
18
|
switch (command) {
|
|
19
19
|
case 'fetch': {
|
|
20
20
|
const company = args[0];
|
|
21
|
-
if (!company) { console.error('Usage: jd-intel fetch <company> [--ats
|
|
21
|
+
if (!company) { console.error('Usage: jd-intel fetch <company> [--ats <platform>] (omit --ats to auto-detect; run "jd-intel" for the platform list)'); process.exit(1); }
|
|
22
22
|
const getArg = (flag) => {
|
|
23
23
|
const idx = args.indexOf(flag);
|
|
24
24
|
return idx >= 0 ? args[idx + 1] : undefined;
|
|
25
25
|
};
|
|
26
|
-
|
|
26
|
+
let ats = getArg('--ats');
|
|
27
27
|
const titleFilter = getArg('--title-filter');
|
|
28
28
|
const filter = getArg('--filter');
|
|
29
29
|
const postedWithinRaw = getArg('--posted-within-days');
|
|
@@ -35,6 +35,28 @@ async function main() {
|
|
|
35
35
|
const limitRaw = getArg('--limit');
|
|
36
36
|
const limit = limitRaw !== undefined ? Number(limitRaw) : undefined;
|
|
37
37
|
|
|
38
|
+
// Workday is keyed by a {tenant, env, site} triple, not a slug.
|
|
39
|
+
// Supplying it here makes a Workday board reachable without a
|
|
40
|
+
// registry entry; presence of the flags infers --ats workday.
|
|
41
|
+
const wdTenant = getArg('--workday-tenant');
|
|
42
|
+
const wdEnv = getArg('--workday-env');
|
|
43
|
+
const wdSite = getArg('--workday-site');
|
|
44
|
+
let config;
|
|
45
|
+
if (wdTenant || wdEnv || wdSite) {
|
|
46
|
+
if (!wdTenant || !wdEnv || !wdSite) {
|
|
47
|
+
console.error('Workday needs all three: --workday-tenant, --workday-env, --workday-site.');
|
|
48
|
+
console.error('Find them in the careers URL: https://{tenant}.{env}.myworkdayjobs.com/{site}');
|
|
49
|
+
console.error('e.g. https://expedia.wd108.myworkdayjobs.com/search -> --workday-tenant expedia --workday-env wd108 --workday-site search');
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
if (ats && ats !== 'workday') {
|
|
53
|
+
console.error(`--ats ${ats} conflicts with the --workday-* flags (workday is inferred). Drop one.`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
config = { tenant: wdTenant, env: wdEnv, site: wdSite };
|
|
57
|
+
ats = 'workday';
|
|
58
|
+
}
|
|
59
|
+
|
|
38
60
|
const parts = [];
|
|
39
61
|
if (titleFilter) parts.push(`title: ${titleFilter}`);
|
|
40
62
|
if (filter) parts.push(`topic: ${filter}`);
|
|
@@ -43,10 +65,23 @@ async function main() {
|
|
|
43
65
|
if (locationExcludes) parts.push(`loc-: ${locationExcludes.join('|')}`);
|
|
44
66
|
const suffix = parts.length ? ` [${parts.join(', ')}]` : '';
|
|
45
67
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
});
|
|
68
|
+
const atsLabel = config
|
|
69
|
+
? ` (workday: ${config.tenant}/${config.env}/${config.site})`
|
|
70
|
+
: ats ? ` (${ats})` : ' (auto-detect)';
|
|
71
|
+
console.log(`Fetching jobs from ${company}${atsLabel}${suffix}...`);
|
|
72
|
+
let jobs;
|
|
73
|
+
try {
|
|
74
|
+
jobs = await fetchJobs({
|
|
75
|
+
company, ats, config, titleFilter, filter, postedWithinDays, locationIncludes, locationExcludes, limit,
|
|
76
|
+
});
|
|
77
|
+
} catch (err) {
|
|
78
|
+
if (config) {
|
|
79
|
+
console.error(`Could not reach that Workday board (${config.tenant}/${config.env}/${config.site}): ${err.message}`);
|
|
80
|
+
console.error('Verify the triple against the careers URL: https://{tenant}.{env}.myworkdayjobs.com/{site}');
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
50
85
|
console.log(`Found ${jobs.length} jobs\n`);
|
|
51
86
|
|
|
52
87
|
for (const job of jobs.slice(0, 20)) {
|
|
@@ -112,7 +147,18 @@ Usage:
|
|
|
112
147
|
jd-intel registry search <query>
|
|
113
148
|
|
|
114
149
|
Fetch options:
|
|
115
|
-
--ats
|
|
150
|
+
--ats <platform> Skip auto-detect. One of: greenhouse, lever,
|
|
151
|
+
ashby, smartrecruiters, teamtailor, recruitee,
|
|
152
|
+
workday. Omit to auto-detect (registry-backed).
|
|
153
|
+
--workday-tenant T Workday is keyed by a {tenant, env, site}
|
|
154
|
+
--workday-env wdN triple, not a slug. Registered Workday
|
|
155
|
+
--workday-site S companies work via auto-detect or --ats
|
|
156
|
+
workday; for any other Workday board pass
|
|
157
|
+
all three, read from the careers URL
|
|
158
|
+
https://{tenant}.{env}.myworkdayjobs.com/{site}
|
|
159
|
+
e.g. https://expedia.wd108.myworkdayjobs.com/search
|
|
160
|
+
-> --workday-tenant expedia --workday-env wd108
|
|
161
|
+
--workday-site search
|
|
116
162
|
--title-filter pattern Regex matched against TITLE only (role identity)
|
|
117
163
|
--filter pattern Regex matched across title, department, description (topic/scope)
|
|
118
164
|
--posted-within-days N Only jobs posted in the last N days
|
|
@@ -132,6 +178,7 @@ Examples:
|
|
|
132
178
|
jd-intel fetch stripe --title-filter "product manager" --filter "growth|platform"
|
|
133
179
|
jd-intel fetch ramp --location-include "United States,US,Remote - US" --location-exclude "London,Dublin"
|
|
134
180
|
jd-intel fetch notion --ats ashby --title-filter engineer --posted-within-days 14
|
|
181
|
+
jd-intel fetch expedia --workday-tenant expedia --workday-env wd108 --workday-site search
|
|
135
182
|
jd-intel detect figma
|
|
136
183
|
jd-intel registry search fintech`);
|
|
137
184
|
}
|
package/src/index.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* jd-intel — JD intelligence toolkit: fetch, normalize, and search job descriptions across every major ATS.
|
|
3
3
|
*
|
|
4
4
|
* Fetches, normalizes, and enriches job data from public ATS APIs
|
|
5
|
-
* (Greenhouse, Lever, Ashby
|
|
5
|
+
* (Greenhouse, Lever, Ashby, SmartRecruiters, Teamtailor, Recruitee,
|
|
6
|
+
* Workday) into a unified schema.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import { ADAPTERS, ATS_NAMES } from './adapters/index.js';
|
|
@@ -15,6 +16,7 @@ import { applyFilters } from './filters.js';
|
|
|
15
16
|
* @param {Object} options
|
|
16
17
|
* @param {string} options.company - Company slug or name
|
|
17
18
|
* @param {string} [options.ats] - Specific ATS platform. If omitted, auto-detects.
|
|
19
|
+
* @param {object} [options.config] - Adapter-specific config (e.g. Workday {tenant, env, site}). Bypasses the registry; the only way to reach a Workday company not in the registry.
|
|
18
20
|
* @param {string} [options.titleFilter] - Regex matched against title only. Use for role identity ("product manager", "staff engineer").
|
|
19
21
|
* @param {string} [options.filter] - Regex matched across title, department, description. Use for topic/scope.
|
|
20
22
|
* @param {number} [options.postedWithinDays] - Only return jobs posted within N days.
|
|
@@ -26,6 +28,7 @@ import { applyFilters } from './filters.js';
|
|
|
26
28
|
export async function fetchJobs({
|
|
27
29
|
company,
|
|
28
30
|
ats,
|
|
31
|
+
config,
|
|
29
32
|
titleFilter,
|
|
30
33
|
filter,
|
|
31
34
|
postedWithinDays,
|
|
@@ -48,16 +51,33 @@ export async function fetchJobs({
|
|
|
48
51
|
if (ats) {
|
|
49
52
|
const adapter = ADAPTERS[ats];
|
|
50
53
|
if (!adapter) throw new Error(`Unknown ATS: ${ats}. Supported: ${ATS_NAMES.join(', ')}`);
|
|
51
|
-
|
|
54
|
+
// Explicit ATS: an explicitly passed config wins (the only path that
|
|
55
|
+
// can reach a Workday company not in the registry). With no explicit
|
|
56
|
+
// config, fall back to the registry so config-keyed adapters
|
|
57
|
+
// (Workday) and canonically-cased registry slugs (SmartRecruiters
|
|
58
|
+
// "Visa") also work on the explicit path, not just under auto-detect.
|
|
59
|
+
let fetchSlug = slug;
|
|
60
|
+
let cfg = config;
|
|
61
|
+
let companyName;
|
|
62
|
+
if (!cfg) {
|
|
63
|
+
const hit = await findEntryBySlug(slug);
|
|
64
|
+
if (hit && hit.ats === ats) {
|
|
65
|
+
fetchSlug = hit.entry.slug;
|
|
66
|
+
cfg = hit.entry.config;
|
|
67
|
+
companyName = hit.entry.name;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
jobs = await adapter.fetch(fetchSlug, { config: cfg, companyName, filterContext });
|
|
52
71
|
} else {
|
|
53
72
|
// Consult registry first — if we know which ATS this company uses,
|
|
54
73
|
// skip probing the others (saves API calls, clearer error semantics).
|
|
55
|
-
// The
|
|
56
|
-
//
|
|
74
|
+
// The registry entry carries the canonical slug (so the adapter is
|
|
75
|
+
// called with the ATS's own casing, e.g. SmartRecruiters "Visa") and
|
|
76
|
+
// any adapter-specific config (the Workday {tenant,env,site} triple).
|
|
57
77
|
const hit = await findEntryBySlug(slug);
|
|
58
78
|
if (hit) {
|
|
59
|
-
jobs = await ADAPTERS[hit.ats].fetch(slug, {
|
|
60
|
-
config: hit.entry.config,
|
|
79
|
+
jobs = await ADAPTERS[hit.ats].fetch(hit.entry.slug, {
|
|
80
|
+
config: config || hit.entry.config,
|
|
61
81
|
companyName: hit.entry.name,
|
|
62
82
|
filterContext,
|
|
63
83
|
});
|
|
@@ -92,7 +112,7 @@ export { detectAts } from './registry.js';
|
|
|
92
112
|
|
|
93
113
|
/**
|
|
94
114
|
* Look up which ATS a slug belongs to in the registry (cached, no network).
|
|
95
|
-
* Returns the ATS name ("greenhouse"
|
|
115
|
+
* Returns the ATS name (e.g. "greenhouse", "workday") or null if not in registry.
|
|
96
116
|
*/
|
|
97
117
|
export { findAtsBySlug } from './registry.js';
|
|
98
118
|
|
package/src/registry.js
CHANGED
|
@@ -58,14 +58,21 @@ export async function searchRegistry(query) {
|
|
|
58
58
|
return results;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// Slug match is case/punctuation-insensitive: registry slugs are stored
|
|
62
|
+
// in each ATS's canonical form (SmartRecruiters uses PascalCase, e.g.
|
|
63
|
+
// "Visa"), but callers pass a lowercased/alnum-stripped slug. Comparing
|
|
64
|
+
// normalized forms keeps registry-first routing working for those.
|
|
65
|
+
const normSlug = (s) => String(s).toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
66
|
+
|
|
61
67
|
/**
|
|
62
68
|
* Look up which ATS a slug belongs to in the registry.
|
|
63
69
|
* Returns the ATS name (e.g., "greenhouse") or null if not in registry.
|
|
64
70
|
*/
|
|
65
71
|
export async function findAtsBySlug(slug) {
|
|
66
72
|
const all = await loadRegistry();
|
|
73
|
+
const key = normSlug(slug);
|
|
67
74
|
for (const [ats, companies] of Object.entries(all)) {
|
|
68
|
-
if (companies.some(c => c.slug ===
|
|
75
|
+
if (companies.some(c => normSlug(c.slug) === key)) return ats;
|
|
69
76
|
}
|
|
70
77
|
return null;
|
|
71
78
|
}
|
|
@@ -81,8 +88,9 @@ export async function findAtsBySlug(slug) {
|
|
|
81
88
|
*/
|
|
82
89
|
export async function findEntryBySlug(slug) {
|
|
83
90
|
const all = await loadRegistry();
|
|
91
|
+
const key = normSlug(slug);
|
|
84
92
|
for (const [ats, companies] of Object.entries(all)) {
|
|
85
|
-
const entry = companies.find(c => c.slug ===
|
|
93
|
+
const entry = companies.find(c => normSlug(c.slug) === key);
|
|
86
94
|
if (entry) return { ats, entry };
|
|
87
95
|
}
|
|
88
96
|
return null;
|