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 +23 -4
- package/package.json +1 -1
- package/registry/smartrecruiters.json +13 -0
- package/src/adapters/index.js +2 -0
- package/src/adapters/smartrecruiters.js +108 -0
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
|
|
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
|
@@ -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
|
+
]
|
package/src/adapters/index.js
CHANGED
|
@@ -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
|
+
}
|