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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jd-intel",
3
- "version": "0.5.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 greenhouse|lever|ashby] [--filter keyword|pattern]
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 greenhouse|lever|ashby]'); process.exit(1); }
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
- const ats = getArg('--ats');
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
- console.log(`Fetching jobs from ${company}${ats ? ` (${ats})` : ' (auto-detect)'}${suffix}...`);
47
- const jobs = await fetchJobs({
48
- company, ats, titleFilter, filter, postedWithinDays, locationIncludes, locationExcludes, limit,
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 greenhouse|lever|ashby Skip auto-detect
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) into a unified schema.
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
- jobs = await adapter.fetch(slug, { filterContext });
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 full entry is needed so adapter-specific config (e.g. the
56
- // Workday {tenant,env,site} triple) reaches the adapter.
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" | "lever" | "ashby") or null if not in registry.
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 === slug)) return ats;
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 === slug);
93
+ const entry = companies.find(c => normSlug(c.slug) === key);
86
94
  if (entry) return { ats, entry };
87
95
  }
88
96
  return null;