jd-intel 0.3.2 → 0.4.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 +18 -6
- package/package.json +1 -1
- package/registry/recruitee.json +13 -0
- package/src/adapters/index.js +2 -0
- package/src/adapters/recruitee.js +69 -0
package/README.md
CHANGED
|
@@ -34,6 +34,17 @@ Done.
|
|
|
34
34
|
|
|
35
35
|
---
|
|
36
36
|
|
|
37
|
+
## Why not just scrape?
|
|
38
|
+
|
|
39
|
+
Because scraping breaks where jd-intel doesn't:
|
|
40
|
+
|
|
41
|
+
- **Full JDs when browsing fails.** SPA-rendered boards, slow loads, auth walls, and geo-restrictions block a browser. They don't block a public API call.
|
|
42
|
+
- **Structured data, not HTML soup.** Salary, location type, department, and clean markdown, normalized across every ATS.
|
|
43
|
+
- **No keys, no browser.** Public APIs only. Runs anywhere your AI does.
|
|
44
|
+
- **One schema, every platform.** Greenhouse, Lever, Ashby, SmartRecruiters, TeamTailor, Recruitee return the same shape.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
37
48
|
## What you can do with it
|
|
38
49
|
|
|
39
50
|
- Look up open roles at any company directly from your AI, no copy-paste
|
|
@@ -197,8 +208,9 @@ No custom parsing per company.
|
|
|
197
208
|
| Lever | Shipped | Common at mid-stage companies |
|
|
198
209
|
| SmartRecruiters | Shipped | Enterprise and mid-market |
|
|
199
210
|
| TeamTailor | Shipped | European startups and scale-ups |
|
|
200
|
-
|
|
|
201
|
-
|
|
|
211
|
+
| Recruitee | Shipped | Dutch / EU SMBs and scale-ups |
|
|
212
|
+
| Personio | Planned | German / EU mid-market |
|
|
213
|
+
| Workday | Planned | Large enterprises (scoped scraper) |
|
|
202
214
|
|
|
203
215
|
Adding a new ATS is a single adapter file. See [Contributing](#contributing).
|
|
204
216
|
|
|
@@ -223,17 +235,17 @@ All filters AND together. Deep dive on patterns and gotchas: [docs/filters.md](d
|
|
|
223
235
|
|
|
224
236
|
**Shipped**
|
|
225
237
|
- Library, CLI, and MCP server (three surfaces of one toolkit)
|
|
226
|
-
- Greenhouse, Ashby, Lever, SmartRecruiters, TeamTailor adapters
|
|
238
|
+
- Greenhouse, Ashby, Lever, SmartRecruiters, TeamTailor, Recruitee adapters
|
|
227
239
|
- Title, topic, location, and date filters
|
|
228
240
|
- Salary extraction from JD text
|
|
229
|
-
- Verified company registry (
|
|
241
|
+
- Verified company registry (155+ companies)
|
|
230
242
|
|
|
231
243
|
**Next**
|
|
244
|
+
- Personio adapter (German / EU mid-market)
|
|
232
245
|
- Anthropic MCP marketplace submission
|
|
233
|
-
- BambooHR and Workable adapters
|
|
234
246
|
|
|
235
247
|
**Planned**
|
|
236
|
-
-
|
|
248
|
+
- Workable adapter (parked — needs SPA shortcode resolution)
|
|
237
249
|
- Workday support (scoped scraper — large enterprise universe)
|
|
238
250
|
- Temporal tracking (when roles open, close, reopen)
|
|
239
251
|
- Change detection
|
package/package.json
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
[
|
|
2
|
+
{"slug": "incentro", "name": "Incentro", "sector": "it consultancy"},
|
|
3
|
+
{"slug": "channable", "name": "Channable", "sector": "ecommerce / feed management"},
|
|
4
|
+
{"slug": "vandebron", "name": "Vandebron", "sector": "energy"},
|
|
5
|
+
{"slug": "effectory", "name": "Effectory", "sector": "hr tech"},
|
|
6
|
+
{"slug": "nmbrs", "name": "Nmbrs", "sector": "hr / payroll"},
|
|
7
|
+
{"slug": "shypple", "name": "Shypple", "sector": "logistics / freight"},
|
|
8
|
+
{"slug": "floryn", "name": "Floryn", "sector": "fintech / financing"},
|
|
9
|
+
{"slug": "bridgefund", "name": "BridgeFund", "sector": "fintech / sme lending"},
|
|
10
|
+
{"slug": "sendcloud", "name": "Sendcloud", "sector": "shipping saas"},
|
|
11
|
+
{"slug": "brenger", "name": "Brenger", "sector": "logistics / delivery"},
|
|
12
|
+
{"slug": "youwe", "name": "Youwe", "sector": "digital commerce agency"}
|
|
13
|
+
]
|
package/src/adapters/index.js
CHANGED
|
@@ -3,6 +3,7 @@ export { fetchLever, hasLever } from './lever.js';
|
|
|
3
3
|
export { fetchAshby, hasAshby } from './ashby.js';
|
|
4
4
|
export { fetchSmartrecruiters, hasSmartrecruiters } from './smartrecruiters.js';
|
|
5
5
|
export { fetchTeamtailor, hasTeamtailor } from './teamtailor.js';
|
|
6
|
+
export { fetchRecruitee, hasRecruitee } from './recruitee.js';
|
|
6
7
|
|
|
7
8
|
export const ADAPTERS = {
|
|
8
9
|
greenhouse: { fetch: (...args) => import('./greenhouse.js').then(m => m.fetchGreenhouse(...args)), has: (...args) => import('./greenhouse.js').then(m => m.hasGreenhouse(...args)) },
|
|
@@ -10,6 +11,7 @@ export const ADAPTERS = {
|
|
|
10
11
|
ashby: { fetch: (...args) => import('./ashby.js').then(m => m.fetchAshby(...args)), has: (...args) => import('./ashby.js').then(m => m.hasAshby(...args)) },
|
|
11
12
|
smartrecruiters: { fetch: (...args) => import('./smartrecruiters.js').then(m => m.fetchSmartrecruiters(...args)), has: (...args) => import('./smartrecruiters.js').then(m => m.hasSmartrecruiters(...args)) },
|
|
12
13
|
teamtailor: { fetch: (...args) => import('./teamtailor.js').then(m => m.fetchTeamtailor(...args)), has: (...args) => import('./teamtailor.js').then(m => m.hasTeamtailor(...args)) },
|
|
14
|
+
recruitee: { fetch: (...args) => import('./recruitee.js').then(m => m.fetchRecruitee(...args)), has: (...args) => import('./recruitee.js').then(m => m.hasRecruitee(...args)) },
|
|
13
15
|
};
|
|
14
16
|
|
|
15
17
|
export const ATS_NAMES = Object.keys(ADAPTERS);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { normalize, stripHtml } from '../normalizer.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fetch jobs from a Recruitee career site.
|
|
5
|
+
* Public API, no auth required.
|
|
6
|
+
* Docs: https://docs.recruitee.com/reference/offers
|
|
7
|
+
*
|
|
8
|
+
* Single GET returns every offer with the full HTML description
|
|
9
|
+
* inline — no N+1 (unlike SmartRecruiters), no XML (unlike
|
|
10
|
+
* TeamTailor/Personio). The simplest adapter shape in the toolkit.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} slug - Recruitee company subdomain (e.g., 'vandebron')
|
|
13
|
+
* @returns {Promise<Array>} Normalized job objects
|
|
14
|
+
*/
|
|
15
|
+
export async function fetchRecruitee(slug) {
|
|
16
|
+
const url = `https://${slug}.recruitee.com/api/offers/`;
|
|
17
|
+
const resp = await fetch(url);
|
|
18
|
+
|
|
19
|
+
if (!resp.ok) {
|
|
20
|
+
if (resp.status === 404) return []; // No Recruitee site for this slug
|
|
21
|
+
throw new Error(`Recruitee API error for ${slug}: ${resp.status}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const data = await resp.json();
|
|
25
|
+
const offers = data.offers || [];
|
|
26
|
+
|
|
27
|
+
return offers.map(offer => {
|
|
28
|
+
const place = [offer.city, offer.country].filter(Boolean).join(', ');
|
|
29
|
+
let location = place;
|
|
30
|
+
if (offer.remote) location = place ? `Remote - ${place}` : 'Remote';
|
|
31
|
+
|
|
32
|
+
let postedAt = null;
|
|
33
|
+
if (offer.created_at) {
|
|
34
|
+
// Recruitee returns "2026-05-13 07:38:11 UTC"; coerce to ISO.
|
|
35
|
+
const iso = offer.created_at.replace(' UTC', 'Z').replace(' ', 'T');
|
|
36
|
+
const d = new Date(iso);
|
|
37
|
+
if (!Number.isNaN(d.getTime())) postedAt = d.toISOString();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return normalize({
|
|
41
|
+
companySlug: slug,
|
|
42
|
+
company: offer.company_name || slug,
|
|
43
|
+
title: offer.title || '',
|
|
44
|
+
department: offer.department || '',
|
|
45
|
+
location,
|
|
46
|
+
description: stripHtml(offer.description || ''),
|
|
47
|
+
url: offer.careers_url || offer.careers_apply_url || '',
|
|
48
|
+
postedAt,
|
|
49
|
+
salary: null, // No structured salary; normalizer parses from text
|
|
50
|
+
metadata: {
|
|
51
|
+
recruiteeId: offer.guid || offer.id,
|
|
52
|
+
employmentType: offer.employment_type_code || '',
|
|
53
|
+
category: offer.category_code || '',
|
|
54
|
+
},
|
|
55
|
+
}, 'recruitee');
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check if a company has a Recruitee career site.
|
|
61
|
+
*/
|
|
62
|
+
export async function hasRecruitee(slug) {
|
|
63
|
+
try {
|
|
64
|
+
const resp = await fetch(`https://${slug}.recruitee.com/api/offers/`);
|
|
65
|
+
return resp.ok;
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|