jd-intel 0.7.0 → 0.8.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/README.md +52 -39
- package/package.json +60 -57
- package/registry/ashby.json +49 -49
- package/registry/greenhouse.json +131 -131
- package/registry/lever.json +32 -32
- package/registry/recruitee.json +25 -25
- package/registry/smartrecruiters.json +30 -30
- package/registry/teamtailor.json +33 -33
- package/registry/workday.json +29 -29
- package/src/adapters/ashby.js +2 -1
- package/src/adapters/greenhouse.js +2 -1
- package/src/adapters/lever.js +2 -1
- package/src/adapters/recruitee.js +2 -1
- package/src/adapters/smartrecruiters.js +8 -2
- package/src/adapters/teamtailor.js +2 -1
- package/src/adapters/workday.js +2 -1
- package/src/cli.js +190 -190
- package/src/errors.js +38 -0
- package/src/index.js +146 -136
- package/src/registry.js +164 -114
package/registry/workday.json
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
[
|
|
2
|
-
{"slug": "cisco", "name": "Cisco", "sector": "networking", "config": {"tenant": "cisco", "env": "wd5", "site": "Cisco_Careers"}},
|
|
3
|
-
{"slug": "salesforce", "name": "Salesforce", "sector": "crm / saas", "config": {"tenant": "salesforce", "env": "wd12", "site": "External_Career_Site"}},
|
|
4
|
-
{"slug": "bankofamerica", "name": "Bank of America", "sector": "banking", "config": {"tenant": "ghr", "env": "wd1", "site": "Lateral-US"}},
|
|
5
|
-
{"slug": "adobe", "name": "Adobe", "sector": "creative software", "config": {"tenant": "adobe", "env": "wd5", "site": "external_experienced"}},
|
|
6
|
-
{"slug": "nvidia", "name": "Nvidia", "sector": "semiconductors", "config": {"tenant": "nvidia", "env": "wd5", "site": "NVIDIAExternalCareerSite"}},
|
|
7
|
-
{"slug": "servicetitan", "name": "ServiceTitan", "sector": "vertical saas", "config": {"tenant": "servicetitan", "env": "wd1", "site": "ServiceTitan"}},
|
|
8
|
-
{"slug": "cvshealth", "name": "CVS Health", "sector": "healthcare / pharmacy", "config": {"tenant": "cvshealth", "env": "wd1", "site": "CVS_Health_Careers"}},
|
|
9
|
-
{"slug": "target", "name": "Target", "sector": "retail", "config": {"tenant": "target", "env": "wd5", "site": "targetcareers"}},
|
|
10
|
-
{"slug": "rbc", "name": "Royal Bank of Canada", "sector": "banking", "config": {"tenant": "rbc", "env": "wd3", "site": "RBCGLOBAL1"}},
|
|
11
|
-
{"slug": "capitalone", "name": "Capital One", "sector": "banking / fintech", "config": {"tenant": "capitalone", "env": "wd12", "site": "Capital_One"}},
|
|
12
|
-
{"slug": "bmo", "name": "Bank of Montreal", "sector": "banking", "config": {"tenant": "bmo", "env": "wd3", "site": "External"}},
|
|
13
|
-
{"slug": "hpe", "name": "Hewlett Packard Enterprise", "sector": "enterprise tech", "config": {"tenant": "hpe", "env": "wd5", "site": "Jobsathpe"}},
|
|
14
|
-
{"slug": "mastercard", "name": "Mastercard", "sector": "payments", "config": {"tenant": "mastercard", "env": "wd1", "site": "CorporateCareers"}},
|
|
15
|
-
{"slug": "hp", "name": "HP Inc.", "sector": "personal computing", "config": {"tenant": "hp", "env": "wd5", "site": "ExternalCareerSite"}},
|
|
16
|
-
{"slug": "wolterskluwer", "name": "Wolters Kluwer", "sector": "information services", "config": {"tenant": "wk", "env": "wd3", "site": "External"}},
|
|
17
|
-
{"slug": "mckesson", "name": "McKesson", "sector": "healthcare distribution", "config": {"tenant": "mckesson", "env": "wd3", "site": "External_Careers"}},
|
|
18
|
-
{"slug": "thehartford", "name": "The Hartford", "sector": "insurance", "config": {"tenant": "thehartford", "env": "wd5", "site": "Careers_External"}},
|
|
19
|
-
{"slug": "equifax", "name": "Equifax", "sector": "credit data", "config": {"tenant": "equifax", "env": "wd5", "site": "External"}},
|
|
20
|
-
{"slug": "zillow", "name": "Zillow Group", "sector": "real estate tech", "config": {"tenant": "zillow", "env": "wd5", "site": "Zillow_Group_External"}},
|
|
21
|
-
{"slug": "nationwide", "name": "Nationwide", "sector": "insurance / financial services", "config": {"tenant": "nationwide", "env": "wd1", "site": "Nationwide_Career"}},
|
|
22
|
-
{"slug": "toyota", "name": "Toyota Motor North America", "sector": "automotive", "config": {"tenant": "toyota", "env": "wd503", "site": "TMNA"}},
|
|
23
|
-
{"slug": "progressiveleasing", "name": "Progressive Leasing", "sector": "fintech / lease-to-own", "config": {"tenant": "progleasing", "env": "wd5", "site": "ProgLeasingCareers"}},
|
|
24
|
-
{"slug": "fico", "name": "FICO", "sector": "analytics / credit", "config": {"tenant": "fico", "env": "wd1", "site": "External"}},
|
|
25
|
-
{"slug": "sonyglobal", "name": "Sony Group", "sector": "electronics / entertainment", "config": {"tenant": "sonyglobal", "env": "wd1", "site": "SonyGlobalCareers"}},
|
|
26
|
-
{"slug": "sonypictures", "name": "Sony Pictures Entertainment", "sector": "media / entertainment", "config": {"tenant": "spe", "env": "wd1", "site": "SonyPicturesEntertainment"}},
|
|
27
|
-
{"slug": "merative", "name": "Merative", "sector": "health data", "config": {"tenant": "merative", "env": "wd12", "site": "External_Career_Site"}},
|
|
28
|
-
{"slug": "duckcreek", "name": "Duck Creek Technologies", "sector": "insurance software", "config": {"tenant": "duckcreek", "env": "wd1", "site": "duckcreekcareers"}}
|
|
29
|
-
]
|
|
1
|
+
[
|
|
2
|
+
{"slug": "cisco", "name": "Cisco", "sector": "networking", "config": {"tenant": "cisco", "env": "wd5", "site": "Cisco_Careers"}},
|
|
3
|
+
{"slug": "salesforce", "name": "Salesforce", "sector": "crm / saas", "config": {"tenant": "salesforce", "env": "wd12", "site": "External_Career_Site"}},
|
|
4
|
+
{"slug": "bankofamerica", "name": "Bank of America", "sector": "banking", "config": {"tenant": "ghr", "env": "wd1", "site": "Lateral-US"}},
|
|
5
|
+
{"slug": "adobe", "name": "Adobe", "sector": "creative software", "config": {"tenant": "adobe", "env": "wd5", "site": "external_experienced"}},
|
|
6
|
+
{"slug": "nvidia", "name": "Nvidia", "sector": "semiconductors", "config": {"tenant": "nvidia", "env": "wd5", "site": "NVIDIAExternalCareerSite"}},
|
|
7
|
+
{"slug": "servicetitan", "name": "ServiceTitan", "sector": "vertical saas", "config": {"tenant": "servicetitan", "env": "wd1", "site": "ServiceTitan"}},
|
|
8
|
+
{"slug": "cvshealth", "name": "CVS Health", "sector": "healthcare / pharmacy", "config": {"tenant": "cvshealth", "env": "wd1", "site": "CVS_Health_Careers"}},
|
|
9
|
+
{"slug": "target", "name": "Target", "sector": "retail", "config": {"tenant": "target", "env": "wd5", "site": "targetcareers"}},
|
|
10
|
+
{"slug": "rbc", "name": "Royal Bank of Canada", "sector": "banking", "config": {"tenant": "rbc", "env": "wd3", "site": "RBCGLOBAL1"}},
|
|
11
|
+
{"slug": "capitalone", "name": "Capital One", "sector": "banking / fintech", "config": {"tenant": "capitalone", "env": "wd12", "site": "Capital_One"}},
|
|
12
|
+
{"slug": "bmo", "name": "Bank of Montreal", "sector": "banking", "config": {"tenant": "bmo", "env": "wd3", "site": "External"}},
|
|
13
|
+
{"slug": "hpe", "name": "Hewlett Packard Enterprise", "sector": "enterprise tech", "config": {"tenant": "hpe", "env": "wd5", "site": "Jobsathpe"}},
|
|
14
|
+
{"slug": "mastercard", "name": "Mastercard", "sector": "payments", "config": {"tenant": "mastercard", "env": "wd1", "site": "CorporateCareers"}},
|
|
15
|
+
{"slug": "hp", "name": "HP Inc.", "sector": "personal computing", "config": {"tenant": "hp", "env": "wd5", "site": "ExternalCareerSite"}},
|
|
16
|
+
{"slug": "wolterskluwer", "name": "Wolters Kluwer", "sector": "information services", "config": {"tenant": "wk", "env": "wd3", "site": "External"}},
|
|
17
|
+
{"slug": "mckesson", "name": "McKesson", "sector": "healthcare distribution", "config": {"tenant": "mckesson", "env": "wd3", "site": "External_Careers"}},
|
|
18
|
+
{"slug": "thehartford", "name": "The Hartford", "sector": "insurance", "config": {"tenant": "thehartford", "env": "wd5", "site": "Careers_External"}},
|
|
19
|
+
{"slug": "equifax", "name": "Equifax", "sector": "credit data", "config": {"tenant": "equifax", "env": "wd5", "site": "External"}},
|
|
20
|
+
{"slug": "zillow", "name": "Zillow Group", "sector": "real estate tech", "config": {"tenant": "zillow", "env": "wd5", "site": "Zillow_Group_External"}},
|
|
21
|
+
{"slug": "nationwide", "name": "Nationwide", "sector": "insurance / financial services", "config": {"tenant": "nationwide", "env": "wd1", "site": "Nationwide_Career"}},
|
|
22
|
+
{"slug": "toyota", "name": "Toyota Motor North America", "sector": "automotive", "config": {"tenant": "toyota", "env": "wd503", "site": "TMNA"}},
|
|
23
|
+
{"slug": "progressiveleasing", "name": "Progressive Leasing", "sector": "fintech / lease-to-own", "config": {"tenant": "progleasing", "env": "wd5", "site": "ProgLeasingCareers"}},
|
|
24
|
+
{"slug": "fico", "name": "FICO", "sector": "analytics / credit", "config": {"tenant": "fico", "env": "wd1", "site": "External"}},
|
|
25
|
+
{"slug": "sonyglobal", "name": "Sony Group", "sector": "electronics / entertainment", "config": {"tenant": "sonyglobal", "env": "wd1", "site": "SonyGlobalCareers"}},
|
|
26
|
+
{"slug": "sonypictures", "name": "Sony Pictures Entertainment", "sector": "media / entertainment", "config": {"tenant": "spe", "env": "wd1", "site": "SonyPicturesEntertainment"}},
|
|
27
|
+
{"slug": "merative", "name": "Merative", "sector": "health data", "config": {"tenant": "merative", "env": "wd12", "site": "External_Career_Site"}},
|
|
28
|
+
{"slug": "duckcreek", "name": "Duck Creek Technologies", "sector": "insurance software", "config": {"tenant": "duckcreek", "env": "wd1", "site": "duckcreekcareers"}}
|
|
29
|
+
]
|
package/src/adapters/ashby.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalize } from '../normalizer.js';
|
|
2
|
+
import { atsErrorFromStatus } from '../errors.js';
|
|
2
3
|
|
|
3
4
|
const API_URL = 'https://jobs.ashbyhq.com/api/non-user-graphql';
|
|
4
5
|
const BOARD_URL = 'https://api.ashbyhq.com/posting-api/job-board';
|
|
@@ -28,7 +29,7 @@ async function fetchAshbyRest(slug) {
|
|
|
28
29
|
|
|
29
30
|
if (!resp.ok) {
|
|
30
31
|
if (resp.status === 404) return [];
|
|
31
|
-
throw
|
|
32
|
+
throw atsErrorFromStatus(resp.status, `Ashby REST API error for ${slug}: ${resp.status}`);
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
const data = await resp.json();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalize, stripHtml } from '../normalizer.js';
|
|
2
|
+
import { atsErrorFromStatus } from '../errors.js';
|
|
2
3
|
|
|
3
4
|
const BASE_URL = 'https://boards-api.greenhouse.io/v1/boards';
|
|
4
5
|
|
|
@@ -16,7 +17,7 @@ export async function fetchGreenhouse(slug) {
|
|
|
16
17
|
|
|
17
18
|
if (!resp.ok) {
|
|
18
19
|
if (resp.status === 404) return []; // Company not found or no jobs
|
|
19
|
-
throw
|
|
20
|
+
throw atsErrorFromStatus(resp.status, `Greenhouse API error for ${slug}: ${resp.status}`);
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
const data = await resp.json();
|
package/src/adapters/lever.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalize, stripHtml } from '../normalizer.js';
|
|
2
|
+
import { atsErrorFromStatus } from '../errors.js';
|
|
2
3
|
|
|
3
4
|
const BASE_URL = 'https://api.lever.co/v0/postings';
|
|
4
5
|
|
|
@@ -15,7 +16,7 @@ export async function fetchLever(slug) {
|
|
|
15
16
|
|
|
16
17
|
if (!resp.ok) {
|
|
17
18
|
if (resp.status === 404) return [];
|
|
18
|
-
throw
|
|
19
|
+
throw atsErrorFromStatus(resp.status, `Lever API error for ${slug}: ${resp.status}`);
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
const jobs = await resp.json();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalize, stripHtml } from '../normalizer.js';
|
|
2
|
+
import { atsErrorFromStatus } from '../errors.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Fetch jobs from a Recruitee career site.
|
|
@@ -18,7 +19,7 @@ export async function fetchRecruitee(slug) {
|
|
|
18
19
|
|
|
19
20
|
if (!resp.ok) {
|
|
20
21
|
if (resp.status === 404) return []; // No Recruitee site for this slug
|
|
21
|
-
throw
|
|
22
|
+
throw atsErrorFromStatus(resp.status, `Recruitee API error for ${slug}: ${resp.status}`);
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
const data = await resp.json();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalize, stripHtml } from '../normalizer.js';
|
|
2
|
+
import { atsErrorFromStatus } from '../errors.js';
|
|
2
3
|
|
|
3
4
|
const BASE_URL = 'https://api.smartrecruiters.com/v1/companies';
|
|
4
5
|
const PAGE_SIZE = 100;
|
|
@@ -30,7 +31,7 @@ export async function fetchSmartrecruiters(slug) {
|
|
|
30
31
|
|
|
31
32
|
if (!resp.ok) {
|
|
32
33
|
if (resp.status === 404) return []; // Company not found
|
|
33
|
-
throw
|
|
34
|
+
throw atsErrorFromStatus(resp.status, `SmartRecruiters API error for ${slug}: ${resp.status}`);
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
const data = await resp.json();
|
|
@@ -101,7 +102,12 @@ export async function fetchSmartrecruiters(slug) {
|
|
|
101
102
|
export async function hasSmartrecruiters(slug) {
|
|
102
103
|
try {
|
|
103
104
|
const resp = await fetch(`${BASE_URL}/${slug}/postings?limit=1`);
|
|
104
|
-
|
|
105
|
+
if (!resp.ok) return false;
|
|
106
|
+
// SmartRecruiters returns 200 with an empty page (not 404) for unknown
|
|
107
|
+
// companies, so resp.ok alone false-positives on any slug. Confirm at
|
|
108
|
+
// least one real posting exists before claiming a match.
|
|
109
|
+
const data = await resp.json();
|
|
110
|
+
return (data.totalFound || 0) > 0 || (data.content || []).length > 0;
|
|
105
111
|
} catch {
|
|
106
112
|
return false;
|
|
107
113
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalize, stripHtml } from '../normalizer.js';
|
|
2
|
+
import { atsErrorFromStatus } from '../errors.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Fetch jobs from a TeamTailor career site via its public RSS feed.
|
|
@@ -43,7 +44,7 @@ async function resolveFeed(slug, method = 'GET') {
|
|
|
43
44
|
});
|
|
44
45
|
if (resp.ok) return resp;
|
|
45
46
|
if (resp.status !== 404) {
|
|
46
|
-
throw
|
|
47
|
+
throw atsErrorFromStatus(resp.status, `TeamTailor RSS error for ${slug}: ${resp.status}`);
|
|
47
48
|
}
|
|
48
49
|
// 404 on this host — try the next region.
|
|
49
50
|
}
|
package/src/adapters/workday.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalize, stripHtml } from '../normalizer.js';
|
|
2
|
+
import { atsErrorFromStatus } from '../errors.js';
|
|
2
3
|
|
|
3
4
|
const MAX_DETAIL_FETCHES = 100;
|
|
4
5
|
const LIST_PAGE_SIZE = 20;
|
|
@@ -49,7 +50,7 @@ export async function fetchWorkday(slug, ctx = {}) {
|
|
|
49
50
|
if (!resp.ok) {
|
|
50
51
|
if (resp.status === 404) return []; // wrong site / no such board
|
|
51
52
|
if (offset === 0) {
|
|
52
|
-
throw
|
|
53
|
+
throw atsErrorFromStatus(resp.status, `Workday API error for ${slug} (${tenant}/${env}/${site}): ${resp.status}`);
|
|
53
54
|
}
|
|
54
55
|
break; // mid-paging failure: keep what we have
|
|
55
56
|
}
|
package/src/cli.js
CHANGED
|
@@ -1,190 +1,190 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* jd-intel CLI
|
|
5
|
-
*
|
|
6
|
-
* Usage:
|
|
7
|
-
* jd-intel fetch <company> [--ats <platform>] [--filter keyword|pattern]
|
|
8
|
-
* jd-intel detect <company>
|
|
9
|
-
* jd-intel registry search <query>
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { fetchJobs } from './index.js';
|
|
13
|
-
import { detectAts, searchRegistry } from './registry.js';
|
|
14
|
-
|
|
15
|
-
const [,, command, ...args] = process.argv;
|
|
16
|
-
|
|
17
|
-
async function main() {
|
|
18
|
-
switch (command) {
|
|
19
|
-
case 'fetch': {
|
|
20
|
-
const company = args[0];
|
|
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
|
-
const getArg = (flag) => {
|
|
23
|
-
const idx = args.indexOf(flag);
|
|
24
|
-
return idx >= 0 ? args[idx + 1] : undefined;
|
|
25
|
-
};
|
|
26
|
-
let ats = getArg('--ats');
|
|
27
|
-
const titleFilter = getArg('--title-filter');
|
|
28
|
-
const filter = getArg('--filter');
|
|
29
|
-
const postedWithinRaw = getArg('--posted-within-days');
|
|
30
|
-
const postedWithinDays = postedWithinRaw !== undefined ? Number(postedWithinRaw) : undefined;
|
|
31
|
-
const locIncludeRaw = getArg('--location-include');
|
|
32
|
-
const locationIncludes = locIncludeRaw ? locIncludeRaw.split(',').map(s => s.trim()).filter(Boolean) : undefined;
|
|
33
|
-
const locExcludeRaw = getArg('--location-exclude');
|
|
34
|
-
const locationExcludes = locExcludeRaw ? locExcludeRaw.split(',').map(s => s.trim()).filter(Boolean) : undefined;
|
|
35
|
-
const limitRaw = getArg('--limit');
|
|
36
|
-
const limit = limitRaw !== undefined ? Number(limitRaw) : undefined;
|
|
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
|
-
|
|
60
|
-
const parts = [];
|
|
61
|
-
if (titleFilter) parts.push(`title: ${titleFilter}`);
|
|
62
|
-
if (filter) parts.push(`topic: ${filter}`);
|
|
63
|
-
if (postedWithinDays !== undefined) parts.push(`within ${postedWithinDays}d`);
|
|
64
|
-
if (locationIncludes) parts.push(`loc+: ${locationIncludes.join('|')}`);
|
|
65
|
-
if (locationExcludes) parts.push(`loc-: ${locationExcludes.join('|')}`);
|
|
66
|
-
const suffix = parts.length ? ` [${parts.join(', ')}]` : '';
|
|
67
|
-
|
|
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
|
-
}
|
|
85
|
-
console.log(`Found ${jobs.length} jobs\n`);
|
|
86
|
-
|
|
87
|
-
for (const job of jobs.slice(0, 20)) {
|
|
88
|
-
const salary = job.salary ? ` | $${job.salary.min?.toLocaleString()}-$${job.salary.max?.toLocaleString()}` : '';
|
|
89
|
-
const loc = job.location ? ` | ${job.location}` : '';
|
|
90
|
-
const dept = job.department ? ` [${job.department}]` : '';
|
|
91
|
-
console.log(` ${job.title}${dept}${loc}${salary}`);
|
|
92
|
-
console.log(` ${job.url}`);
|
|
93
|
-
if (job.description) {
|
|
94
|
-
const preview = job.description.substring(0, 120).replace(/\n/g, ' ');
|
|
95
|
-
console.log(` ${preview}...`);
|
|
96
|
-
}
|
|
97
|
-
console.log();
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (jobs.length > 20) {
|
|
101
|
-
console.log(` ... and ${jobs.length - 20} more. Use --json for full output.`);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (args.includes('--json')) {
|
|
105
|
-
console.log(JSON.stringify(jobs, null, 2));
|
|
106
|
-
}
|
|
107
|
-
break;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
case 'detect': {
|
|
111
|
-
const company = args[0];
|
|
112
|
-
if (!company) { console.error('Usage: jd-intel detect <company>'); process.exit(1); }
|
|
113
|
-
console.log(`Detecting ATS for ${company}...`);
|
|
114
|
-
const results = await detectAts(company);
|
|
115
|
-
if (results.length === 0) {
|
|
116
|
-
console.log('No ATS board found for this company.');
|
|
117
|
-
} else {
|
|
118
|
-
for (const r of results) {
|
|
119
|
-
console.log(` Found: ${r.ats} (slug: ${r.slug})`);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
break;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
case 'registry': {
|
|
126
|
-
const subcommand = args[0];
|
|
127
|
-
if (subcommand === 'search') {
|
|
128
|
-
const query = args.slice(1).join(' ');
|
|
129
|
-
if (!query) { console.error('Usage: jd-intel registry search <query>'); process.exit(1); }
|
|
130
|
-
const results = await searchRegistry(query);
|
|
131
|
-
console.log(`Found ${results.length} companies matching "${query}":\n`);
|
|
132
|
-
for (const r of results) {
|
|
133
|
-
console.log(` ${r.name || r.slug} (${r.ats})${r.sector ? ` — ${r.sector}` : ''}`);
|
|
134
|
-
}
|
|
135
|
-
} else {
|
|
136
|
-
console.error('Usage: jd-intel registry search <query>');
|
|
137
|
-
}
|
|
138
|
-
break;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
default:
|
|
142
|
-
console.log(`jd-intel — JD intelligence toolkit for your AI assistant.
|
|
143
|
-
|
|
144
|
-
Usage:
|
|
145
|
-
jd-intel fetch <company> [options]
|
|
146
|
-
jd-intel detect <company>
|
|
147
|
-
jd-intel registry search <query>
|
|
148
|
-
|
|
149
|
-
Fetch options:
|
|
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
|
|
162
|
-
--title-filter pattern Regex matched against TITLE only (role identity)
|
|
163
|
-
--filter pattern Regex matched across title, department, description (topic/scope)
|
|
164
|
-
--posted-within-days N Only jobs posted in the last N days
|
|
165
|
-
--location-include "A,B,C" Keep jobs whose location contains any of these
|
|
166
|
-
--location-exclude "A,B,C" Drop jobs whose location contains any of these
|
|
167
|
-
--limit N Cap results (default 100)
|
|
168
|
-
--json Output full JSON
|
|
169
|
-
|
|
170
|
-
Filter guidance:
|
|
171
|
-
Use --title-filter for "what KIND of role" (PM, engineer, designer).
|
|
172
|
-
Use --filter for "what it's ABOUT" (integrations, growth, payments).
|
|
173
|
-
Both AND together. Avoid --filter "product manager" — description
|
|
174
|
-
mentions of PMs in other roles' JDs create false positives.
|
|
175
|
-
|
|
176
|
-
Examples:
|
|
177
|
-
jd-intel fetch stripe
|
|
178
|
-
jd-intel fetch stripe --title-filter "product manager" --filter "growth|platform"
|
|
179
|
-
jd-intel fetch ramp --location-include "United States,US,Remote - US" --location-exclude "London,Dublin"
|
|
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
|
|
182
|
-
jd-intel detect figma
|
|
183
|
-
jd-intel registry search fintech`);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
main().catch(err => {
|
|
188
|
-
console.error('Error:', err.message);
|
|
189
|
-
process.exit(1);
|
|
190
|
-
});
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* jd-intel CLI
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* jd-intel fetch <company> [--ats <platform>] [--filter keyword|pattern]
|
|
8
|
+
* jd-intel detect <company>
|
|
9
|
+
* jd-intel registry search <query>
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { fetchJobs } from './index.js';
|
|
13
|
+
import { detectAts, searchRegistry } from './registry.js';
|
|
14
|
+
|
|
15
|
+
const [,, command, ...args] = process.argv;
|
|
16
|
+
|
|
17
|
+
async function main() {
|
|
18
|
+
switch (command) {
|
|
19
|
+
case 'fetch': {
|
|
20
|
+
const company = args[0];
|
|
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
|
+
const getArg = (flag) => {
|
|
23
|
+
const idx = args.indexOf(flag);
|
|
24
|
+
return idx >= 0 ? args[idx + 1] : undefined;
|
|
25
|
+
};
|
|
26
|
+
let ats = getArg('--ats');
|
|
27
|
+
const titleFilter = getArg('--title-filter');
|
|
28
|
+
const filter = getArg('--filter');
|
|
29
|
+
const postedWithinRaw = getArg('--posted-within-days');
|
|
30
|
+
const postedWithinDays = postedWithinRaw !== undefined ? Number(postedWithinRaw) : undefined;
|
|
31
|
+
const locIncludeRaw = getArg('--location-include');
|
|
32
|
+
const locationIncludes = locIncludeRaw ? locIncludeRaw.split(',').map(s => s.trim()).filter(Boolean) : undefined;
|
|
33
|
+
const locExcludeRaw = getArg('--location-exclude');
|
|
34
|
+
const locationExcludes = locExcludeRaw ? locExcludeRaw.split(',').map(s => s.trim()).filter(Boolean) : undefined;
|
|
35
|
+
const limitRaw = getArg('--limit');
|
|
36
|
+
const limit = limitRaw !== undefined ? Number(limitRaw) : undefined;
|
|
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
|
+
|
|
60
|
+
const parts = [];
|
|
61
|
+
if (titleFilter) parts.push(`title: ${titleFilter}`);
|
|
62
|
+
if (filter) parts.push(`topic: ${filter}`);
|
|
63
|
+
if (postedWithinDays !== undefined) parts.push(`within ${postedWithinDays}d`);
|
|
64
|
+
if (locationIncludes) parts.push(`loc+: ${locationIncludes.join('|')}`);
|
|
65
|
+
if (locationExcludes) parts.push(`loc-: ${locationExcludes.join('|')}`);
|
|
66
|
+
const suffix = parts.length ? ` [${parts.join(', ')}]` : '';
|
|
67
|
+
|
|
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
|
+
}
|
|
85
|
+
console.log(`Found ${jobs.length} jobs\n`);
|
|
86
|
+
|
|
87
|
+
for (const job of jobs.slice(0, 20)) {
|
|
88
|
+
const salary = job.salary ? ` | $${job.salary.min?.toLocaleString()}-$${job.salary.max?.toLocaleString()}` : '';
|
|
89
|
+
const loc = job.location ? ` | ${job.location}` : '';
|
|
90
|
+
const dept = job.department ? ` [${job.department}]` : '';
|
|
91
|
+
console.log(` ${job.title}${dept}${loc}${salary}`);
|
|
92
|
+
console.log(` ${job.url}`);
|
|
93
|
+
if (job.description) {
|
|
94
|
+
const preview = job.description.substring(0, 120).replace(/\n/g, ' ');
|
|
95
|
+
console.log(` ${preview}...`);
|
|
96
|
+
}
|
|
97
|
+
console.log();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (jobs.length > 20) {
|
|
101
|
+
console.log(` ... and ${jobs.length - 20} more. Use --json for full output.`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (args.includes('--json')) {
|
|
105
|
+
console.log(JSON.stringify(jobs, null, 2));
|
|
106
|
+
}
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
case 'detect': {
|
|
111
|
+
const company = args[0];
|
|
112
|
+
if (!company) { console.error('Usage: jd-intel detect <company>'); process.exit(1); }
|
|
113
|
+
console.log(`Detecting ATS for ${company}...`);
|
|
114
|
+
const results = await detectAts(company);
|
|
115
|
+
if (results.length === 0) {
|
|
116
|
+
console.log('No ATS board found for this company.');
|
|
117
|
+
} else {
|
|
118
|
+
for (const r of results) {
|
|
119
|
+
console.log(` Found: ${r.ats} (slug: ${r.slug})`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
case 'registry': {
|
|
126
|
+
const subcommand = args[0];
|
|
127
|
+
if (subcommand === 'search') {
|
|
128
|
+
const query = args.slice(1).join(' ');
|
|
129
|
+
if (!query) { console.error('Usage: jd-intel registry search <query>'); process.exit(1); }
|
|
130
|
+
const results = await searchRegistry(query);
|
|
131
|
+
console.log(`Found ${results.length} companies matching "${query}":\n`);
|
|
132
|
+
for (const r of results) {
|
|
133
|
+
console.log(` ${r.name || r.slug} (${r.ats})${r.sector ? ` — ${r.sector}` : ''}`);
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
console.error('Usage: jd-intel registry search <query>');
|
|
137
|
+
}
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
default:
|
|
142
|
+
console.log(`jd-intel — JD intelligence toolkit for your AI assistant.
|
|
143
|
+
|
|
144
|
+
Usage:
|
|
145
|
+
jd-intel fetch <company> [options]
|
|
146
|
+
jd-intel detect <company>
|
|
147
|
+
jd-intel registry search <query>
|
|
148
|
+
|
|
149
|
+
Fetch options:
|
|
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
|
|
162
|
+
--title-filter pattern Regex matched against TITLE only (role identity)
|
|
163
|
+
--filter pattern Regex matched across title, department, description (topic/scope)
|
|
164
|
+
--posted-within-days N Only jobs posted in the last N days
|
|
165
|
+
--location-include "A,B,C" Keep jobs whose location contains any of these
|
|
166
|
+
--location-exclude "A,B,C" Drop jobs whose location contains any of these
|
|
167
|
+
--limit N Cap results (default 100)
|
|
168
|
+
--json Output full JSON
|
|
169
|
+
|
|
170
|
+
Filter guidance:
|
|
171
|
+
Use --title-filter for "what KIND of role" (PM, engineer, designer).
|
|
172
|
+
Use --filter for "what it's ABOUT" (integrations, growth, payments).
|
|
173
|
+
Both AND together. Avoid --filter "product manager" — description
|
|
174
|
+
mentions of PMs in other roles' JDs create false positives.
|
|
175
|
+
|
|
176
|
+
Examples:
|
|
177
|
+
jd-intel fetch stripe
|
|
178
|
+
jd-intel fetch stripe --title-filter "product manager" --filter "growth|platform"
|
|
179
|
+
jd-intel fetch ramp --location-include "United States,US,Remote - US" --location-exclude "London,Dublin"
|
|
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
|
|
182
|
+
jd-intel detect figma
|
|
183
|
+
jd-intel registry search fintech`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
main().catch(err => {
|
|
188
|
+
console.error('Error:', err.message);
|
|
189
|
+
process.exit(1);
|
|
190
|
+
});
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error taxonomy + typed error for the jd-intel toolkit.
|
|
3
|
+
*
|
|
4
|
+
* Codes are short and stable; the MCP tool descriptions teach the AI what each
|
|
5
|
+
* one means. Adapters throw AtsError with a code so callers map failures by
|
|
6
|
+
* `err.code` instead of parsing the message.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const ERROR_CODES = {
|
|
10
|
+
COMPANY_NOT_FOUND: 'company_not_found', // Slug not in registry and not detected
|
|
11
|
+
ATS_UNREACHABLE: 'ats_unreachable', // Known ATS failed (500, timeout)
|
|
12
|
+
PARTIAL_FAILURE: 'partial_failure', // Discovery mode; some adapters failed
|
|
13
|
+
INVALID_ARGS: 'invalid_args', // Missing required, wrong type, bad pattern
|
|
14
|
+
NO_RESULTS: 'no_results', // Query succeeded, filters returned nothing
|
|
15
|
+
RATE_LIMITED: 'rate_limited', // Upstream returned 429
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Thrown by adapters on an upstream HTTP failure. Carries a stable `code`
|
|
20
|
+
* (ats_unreachable / rate_limited) so the MCP layer maps it without parsing
|
|
21
|
+
* the message. Extends Error, so the message, stack, and `instanceof Error`
|
|
22
|
+
* all keep working for existing library consumers.
|
|
23
|
+
*/
|
|
24
|
+
export class AtsError extends Error {
|
|
25
|
+
constructor(code, message) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = 'AtsError';
|
|
28
|
+
this.code = code;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Helper for adapters: build an AtsError from an HTTP status (429 => rate
|
|
34
|
+
* limited, anything else => unreachable) with the given message.
|
|
35
|
+
*/
|
|
36
|
+
export function atsErrorFromStatus(status, message) {
|
|
37
|
+
return new AtsError(status === 429 ? ERROR_CODES.RATE_LIMITED : ERROR_CODES.ATS_UNREACHABLE, message);
|
|
38
|
+
}
|