jd-intel 0.1.1 → 0.2.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/README.md CHANGED
@@ -119,6 +119,24 @@ If `npx jd-intel-mcp install` fails, edit the config directly.
119
119
 
120
120
  Restart Claude Desktop.
121
121
 
122
+ ### Updating
123
+
124
+ `npx -y jd-intel-mcp` auto-updates within ~24 hours via npm's cache. To force an update immediately:
125
+
126
+ ```bash
127
+ npx clear-npx-cache
128
+ ```
129
+
130
+ Then quit and reopen Claude Desktop.
131
+
132
+ If you installed the library or CLI directly:
133
+
134
+ ```bash
135
+ npm install jd-intel@latest # force latest
136
+ # or
137
+ npm update jd-intel # respect semver
138
+ ```
139
+
122
140
  ---
123
141
 
124
142
  ## MCP tools
@@ -177,6 +195,7 @@ No custom parsing per company.
177
195
  | Greenhouse | Shipped | Most widely used ATS in tech |
178
196
  | Ashby | Shipped | Growing fast with startups |
179
197
  | Lever | Shipped | Common at mid-stage companies |
198
+ | SmartRecruiters | Shipped | Enterprise and mid-market |
180
199
  | BambooHR | Planned | Mid-market companies |
181
200
  | Workday | Planned | Large enterprises |
182
201
 
@@ -203,18 +222,18 @@ All filters AND together. Deep dive on patterns and gotchas: [docs/filters.md](d
203
222
 
204
223
  **Shipped**
205
224
  - Library, CLI, and MCP server (three surfaces of one toolkit)
206
- - Greenhouse, Ashby, Lever adapters
225
+ - Greenhouse, Ashby, Lever, SmartRecruiters adapters
207
226
  - Title, topic, location, and date filters
208
227
  - Salary extraction from JD text
209
228
  - Verified company registry (100+ companies)
210
229
 
211
230
  **Next**
231
+ - TeamTailor adapter (European startup coverage)
212
232
  - Anthropic MCP marketplace submission
213
- - Setup guide with screenshots (non-technical walkthrough)
214
- - Remote MCP transport (for Claude.ai Custom Connectors)
215
233
 
216
234
  **Planned**
217
- - BambooHR and Workday adapters
235
+ - BambooHR and Workable adapters
236
+ - Workday support (scoped scraper — large enterprise universe)
218
237
  - Temporal tracking (when roles open, close, reopen)
219
238
  - Change detection
220
239
  - Resume-aware fit scoring
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jd-intel",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Fetch and normalize job descriptions across every major ATS (Greenhouse, Lever, Ashby) — for your AI assistant, no copy-paste.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -0,0 +1,13 @@
1
+ [
2
+ {"slug": "Visa", "name": "Visa", "sector": "payments"},
3
+ {"slug": "Wise", "name": "Wise", "sector": "fintech"},
4
+ {"slug": "Wabtec", "name": "Wabtec", "sector": "rail / industrial"},
5
+ {"slug": "Sutherland", "name": "Sutherland", "sector": "bpo / cx"},
6
+ {"slug": "AveryDennison", "name": "Avery Dennison", "sector": "materials"},
7
+ {"slug": "PublicStorage", "name": "Public Storage", "sector": "real estate"},
8
+ {"slug": "Sportradar", "name": "Sportradar", "sector": "sports data"},
9
+ {"slug": "Entain", "name": "Entain", "sector": "gaming"},
10
+ {"slug": "Picnic", "name": "Picnic", "sector": "grocery delivery"},
11
+ {"slug": "Hootsuite", "name": "Hootsuite", "sector": "social media management"},
12
+ {"slug": "BusinessWire", "name": "Business Wire", "sector": "pr / newswire"}
13
+ ]
@@ -1,11 +1,13 @@
1
1
  export { fetchGreenhouse, hasGreenhouse } from './greenhouse.js';
2
2
  export { fetchLever, hasLever } from './lever.js';
3
3
  export { fetchAshby, hasAshby } from './ashby.js';
4
+ export { fetchSmartrecruiters, hasSmartrecruiters } from './smartrecruiters.js';
4
5
 
5
6
  export const ADAPTERS = {
6
7
  greenhouse: { fetch: (...args) => import('./greenhouse.js').then(m => m.fetchGreenhouse(...args)), has: (...args) => import('./greenhouse.js').then(m => m.hasGreenhouse(...args)) },
7
8
  lever: { fetch: (...args) => import('./lever.js').then(m => m.fetchLever(...args)), has: (...args) => import('./lever.js').then(m => m.hasLever(...args)) },
8
9
  ashby: { fetch: (...args) => import('./ashby.js').then(m => m.fetchAshby(...args)), has: (...args) => import('./ashby.js').then(m => m.hasAshby(...args)) },
10
+ smartrecruiters: { fetch: (...args) => import('./smartrecruiters.js').then(m => m.fetchSmartrecruiters(...args)), has: (...args) => import('./smartrecruiters.js').then(m => m.hasSmartrecruiters(...args)) },
9
11
  };
10
12
 
11
13
  export const ATS_NAMES = Object.keys(ADAPTERS);
@@ -0,0 +1,108 @@
1
+ import { normalize, stripHtml } from '../normalizer.js';
2
+
3
+ const BASE_URL = 'https://api.smartrecruiters.com/v1/companies';
4
+ const PAGE_SIZE = 100;
5
+
6
+ /**
7
+ * Fetch all postings from a SmartRecruiters company.
8
+ * Public API, no auth required.
9
+ * Docs: https://developers.smartrecruiters.com/reference/postingsget-1
10
+ *
11
+ * Two-step flow (unavoidable N+1):
12
+ * - The postings LIST endpoint omits the job description entirely.
13
+ * - jd-intel's contract is "full JD text", so we must fetch each
14
+ * posting's DETAIL endpoint to get jobAd.sections.
15
+ * Large enterprise tenants with hundreds of openings will therefore be
16
+ * slow against SmartRecruiters specifically. This is the API's shape,
17
+ * not a bug here.
18
+ *
19
+ * @param {string} slug - SmartRecruiters company identifier (e.g., 'Visa')
20
+ * @returns {Promise<Array>} Normalized job objects
21
+ */
22
+ export async function fetchSmartrecruiters(slug) {
23
+ // 1. Page through the postings list.
24
+ const postings = [];
25
+ let offset = 0;
26
+
27
+ while (true) {
28
+ const listUrl = `${BASE_URL}/${slug}/postings?limit=${PAGE_SIZE}&offset=${offset}`;
29
+ const resp = await fetch(listUrl);
30
+
31
+ if (!resp.ok) {
32
+ if (resp.status === 404) return []; // Company not found
33
+ throw new Error(`SmartRecruiters API error for ${slug}: ${resp.status}`);
34
+ }
35
+
36
+ const data = await resp.json();
37
+ const content = data.content || [];
38
+ postings.push(...content);
39
+
40
+ offset += PAGE_SIZE;
41
+ if (content.length === 0 || offset >= (data.totalFound || 0)) break;
42
+ }
43
+
44
+ // 2. Fetch detail per posting for the description.
45
+ const jobs = await Promise.all(postings.map(async (p) => {
46
+ let sections = {};
47
+ let postingUrl = '';
48
+
49
+ try {
50
+ const detailResp = await fetch(`${BASE_URL}/${slug}/postings/${p.id}`);
51
+ if (detailResp.ok) {
52
+ const detail = await detailResp.json();
53
+ sections = detail.jobAd?.sections || {};
54
+ postingUrl = detail.postingUrl || detail.applyUrl || '';
55
+ }
56
+ } catch {
57
+ // Detail fetch failed: fall back to list-only fields (no description).
58
+ }
59
+
60
+ const description = [
61
+ sections.jobDescription?.text,
62
+ sections.qualifications?.text,
63
+ sections.additionalInformation?.text,
64
+ ].filter(Boolean).join('\n\n');
65
+
66
+ const loc = p.location || {};
67
+ const place = loc.fullLocation
68
+ || [loc.city, loc.region, loc.country].filter(Boolean).join(', ');
69
+ let location = place;
70
+ if (loc.remote) location = `Remote - ${place}`.replace(/ - $/, ' ');
71
+ else if (loc.hybrid) location = `Hybrid - ${place}`.replace(/ - $/, ' ');
72
+
73
+ return normalize({
74
+ companySlug: slug,
75
+ company: p.company?.name || slug,
76
+ title: p.name || '',
77
+ department: p.department?.label || p.function?.label || '',
78
+ location,
79
+ description: stripHtml(description),
80
+ url: postingUrl,
81
+ postedAt: p.releasedDate || null,
82
+ salary: null, // SmartRecruiters has no structured salary; normalizer parses text
83
+ metadata: {
84
+ smartRecruitersId: p.id,
85
+ refNumber: p.refNumber || '',
86
+ function: p.function?.label || '',
87
+ experienceLevel: p.experienceLevel?.label || '',
88
+ typeOfEmployment: p.typeOfEmployment?.label || '',
89
+ },
90
+ }, 'smartrecruiters');
91
+ }));
92
+
93
+ return jobs;
94
+ }
95
+
96
+ /**
97
+ * Check if a company exists on SmartRecruiters.
98
+ * (HEAD isn't reliably supported on the postings endpoint, so use a
99
+ * minimal GET.)
100
+ */
101
+ export async function hasSmartrecruiters(slug) {
102
+ try {
103
+ const resp = await fetch(`${BASE_URL}/${slug}/postings?limit=1`);
104
+ return resp.ok;
105
+ } catch {
106
+ return false;
107
+ }
108
+ }