jd-intel 0.1.0 → 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 +90 -46
- package/package.json +12 -2
- package/registry/ashby.json +14 -1
- package/registry/greenhouse.json +33 -1
- package/registry/lever.json +8 -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
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
[](LICENSE)
|
|
4
4
|
[](https://nodejs.org)
|
|
5
5
|
[](https://www.npmjs.com/package/jd-intel)
|
|
6
|
+
[](https://www.npmjs.com/package/jd-intel-mcp)
|
|
7
|
+
[](https://github.com/prPMDev/jd-intel/stargazers)
|
|
6
8
|
|
|
7
9
|
> **Stop pasting job descriptions into AI assistants. Let your AI fetch them directly.**
|
|
8
10
|
|
|
@@ -16,13 +18,17 @@ Your AI assistant already knows a lot about you. Your resume is in its memory. Y
|
|
|
16
18
|
|
|
17
19
|
So you copy-paste.
|
|
18
20
|
|
|
19
|
-
A JD from
|
|
21
|
+
A JD from one company. Another from the next. A half-dozen more from your target list. Half have broken HTML. Salary info dies in translation. Links get stripped. And for every role, the dance starts over.
|
|
20
22
|
|
|
21
23
|
You could wait for the job boards to ship their own MCPs. They'll get there eventually. On their timeline. Filtered through their priorities, not yours. Tied to their query abstractions.
|
|
22
24
|
|
|
23
25
|
jd-intel skips that wait. Raw JDs, fetched directly by your AI, on your terms. One level below the curated layer.
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
Try asking your AI:
|
|
28
|
+
|
|
29
|
+
> "Find AI/ML engineering jobs posted this week."
|
|
30
|
+
> "What product designer roles are open at fintechs right now?"
|
|
31
|
+
> "Pull the staff PM roles posted in the last 7 days."
|
|
26
32
|
|
|
27
33
|
Done.
|
|
28
34
|
|
|
@@ -30,10 +36,10 @@ Done.
|
|
|
30
36
|
|
|
31
37
|
## What you can do with it
|
|
32
38
|
|
|
33
|
-
-
|
|
39
|
+
- Look up open roles at any company directly from your AI, no copy-paste
|
|
34
40
|
- Tailor your resume across ten roles in one conversation
|
|
35
41
|
- Rank openings by fit with your background
|
|
36
|
-
- Scan a whole sector: "Pull
|
|
42
|
+
- Scan a whole sector: "Pull open roles at fintech companies posted this week"
|
|
37
43
|
- Research teams by reading their JDs in bulk
|
|
38
44
|
|
|
39
45
|
The toolkit fetches. Your AI thinks.
|
|
@@ -42,12 +48,63 @@ The toolkit fetches. Your AI thinks.
|
|
|
42
48
|
|
|
43
49
|
## Install
|
|
44
50
|
|
|
45
|
-
|
|
51
|
+
Works with MCP-aware AI clients: Claude Desktop, Claude Code, Cursor, Windsurf. ChatGPT, Gemini, and other non-MCP clients don't support this yet. They use different tool-calling systems. (We wish they did. The protocol works the same way regardless of which AI you talk to.)
|
|
52
|
+
|
|
53
|
+
You'll need [Node.js 18 or newer](https://nodejs.org/). To check: open a terminal and run `node --version`. If it's missing or older, install from nodejs.org first.
|
|
54
|
+
|
|
55
|
+
### For Claude Desktop (one command)
|
|
56
|
+
|
|
57
|
+
1. **Open a terminal.** It's just a text window. Nothing destructive happens here.
|
|
58
|
+
- **macOS:** Spotlight (`⌘ Space`), type "Terminal", hit Enter.
|
|
59
|
+
- **Windows:** Start menu, type "PowerShell", hit Enter.
|
|
60
|
+
|
|
61
|
+
2. **Paste this and hit Enter:**
|
|
62
|
+
```bash
|
|
63
|
+
npx jd-intel-mcp install
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
3. **Quit and reopen Claude Desktop.** The tools appear automatically.
|
|
67
|
+
|
|
68
|
+
Try: *"Find product roles at devtools companies."*
|
|
69
|
+
|
|
70
|
+
If something goes wrong or you'd rather edit the config file directly, see [Manual install](#manual-install-fallback) below.
|
|
71
|
+
|
|
72
|
+
### For Cursor and Windsurf
|
|
73
|
+
|
|
74
|
+
These clients have their own MCP setup flows. Follow their docs:
|
|
75
|
+
- Cursor: [docs.cursor.com](https://docs.cursor.com)
|
|
76
|
+
- Windsurf: [docs.windsurf.com](https://docs.windsurf.com)
|
|
77
|
+
|
|
78
|
+
Use this server config: `command: "npx"`, `args: ["-y", "jd-intel-mcp"]`.
|
|
46
79
|
|
|
47
|
-
|
|
80
|
+
### For developers
|
|
48
81
|
|
|
49
|
-
|
|
50
|
-
|
|
82
|
+
```bash
|
|
83
|
+
npm install jd-intel
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
import { fetchJobs } from 'jd-intel';
|
|
88
|
+
|
|
89
|
+
const jobs = await fetchJobs({
|
|
90
|
+
company: '<your-target-company>',
|
|
91
|
+
titleFilter: 'designer',
|
|
92
|
+
postedWithinDays: 14,
|
|
93
|
+
limit: 50,
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
CLI usage: `npx jd-intel fetch <company-slug> --title-filter "engineer" --posted-within-days 14`. Full filter reference [below](#filters-quick-reference).
|
|
98
|
+
|
|
99
|
+
Node.js 18+. No API keys. No configuration.
|
|
100
|
+
|
|
101
|
+
### Manual install (fallback)
|
|
102
|
+
|
|
103
|
+
If `npx jd-intel-mcp install` fails, edit the config directly.
|
|
104
|
+
|
|
105
|
+
**Config file location:**
|
|
106
|
+
- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
107
|
+
- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
|
|
51
108
|
|
|
52
109
|
```json
|
|
53
110
|
{
|
|
@@ -60,40 +117,26 @@ Add to your MCP config file:
|
|
|
60
117
|
}
|
|
61
118
|
```
|
|
62
119
|
|
|
63
|
-
Restart
|
|
120
|
+
Restart Claude Desktop.
|
|
64
121
|
|
|
65
|
-
|
|
66
|
-
```bash
|
|
67
|
-
npx jd-intel-mcp install
|
|
68
|
-
```
|
|
122
|
+
### Updating
|
|
69
123
|
|
|
70
|
-
|
|
124
|
+
`npx -y jd-intel-mcp` auto-updates within ~24 hours via npm's cache. To force an update immediately:
|
|
71
125
|
|
|
72
126
|
```bash
|
|
73
|
-
|
|
127
|
+
npx clear-npx-cache
|
|
74
128
|
```
|
|
75
129
|
|
|
76
|
-
|
|
130
|
+
Then quit and reopen Claude Desktop.
|
|
77
131
|
|
|
78
|
-
|
|
79
|
-
npx jd-intel fetch stripe --title-filter "product manager"
|
|
80
|
-
```
|
|
132
|
+
If you installed the library or CLI directly:
|
|
81
133
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const jobs = await fetchJobs({
|
|
88
|
-
company: 'ramp',
|
|
89
|
-
titleFilter: 'engineer',
|
|
90
|
-
postedWithinDays: 14,
|
|
91
|
-
limit: 50,
|
|
92
|
-
});
|
|
134
|
+
```bash
|
|
135
|
+
npm install jd-intel@latest # force latest
|
|
136
|
+
# or
|
|
137
|
+
npm update jd-intel # respect semver
|
|
93
138
|
```
|
|
94
139
|
|
|
95
|
-
Node.js 18+. No API keys. No configuration.
|
|
96
|
-
|
|
97
140
|
---
|
|
98
141
|
|
|
99
142
|
## MCP tools
|
|
@@ -115,14 +158,14 @@ Every job normalizes to one schema, across every platform:
|
|
|
115
158
|
```json
|
|
116
159
|
{
|
|
117
160
|
"id": "a1b2c3d4e5f6",
|
|
118
|
-
"company": "
|
|
119
|
-
"title": "Senior
|
|
120
|
-
"department": "
|
|
121
|
-
"location": "
|
|
122
|
-
"locationType": "
|
|
123
|
-
"salary": { "min": 180000, "max":
|
|
124
|
-
"description": "
|
|
125
|
-
"url": "https://boards.
|
|
161
|
+
"company": "Example Co",
|
|
162
|
+
"title": "Senior Software Engineer, Platform",
|
|
163
|
+
"department": "Engineering",
|
|
164
|
+
"location": "Remote - US",
|
|
165
|
+
"locationType": "remote",
|
|
166
|
+
"salary": { "min": 180000, "max": 240000, "currency": "USD" },
|
|
167
|
+
"description": "Design and build the API surface our customers integrate against...",
|
|
168
|
+
"url": "https://boards.example.com/jobs/12345",
|
|
126
169
|
"postedAt": "2026-04-10T14:30:00Z"
|
|
127
170
|
}
|
|
128
171
|
```
|
|
@@ -152,6 +195,7 @@ No custom parsing per company.
|
|
|
152
195
|
| Greenhouse | Shipped | Most widely used ATS in tech |
|
|
153
196
|
| Ashby | Shipped | Growing fast with startups |
|
|
154
197
|
| Lever | Shipped | Common at mid-stage companies |
|
|
198
|
+
| SmartRecruiters | Shipped | Enterprise and mid-market |
|
|
155
199
|
| BambooHR | Planned | Mid-market companies |
|
|
156
200
|
| Workday | Planned | Large enterprises |
|
|
157
201
|
|
|
@@ -178,18 +222,18 @@ All filters AND together. Deep dive on patterns and gotchas: [docs/filters.md](d
|
|
|
178
222
|
|
|
179
223
|
**Shipped**
|
|
180
224
|
- Library, CLI, and MCP server (three surfaces of one toolkit)
|
|
181
|
-
- Greenhouse, Ashby, Lever adapters
|
|
225
|
+
- Greenhouse, Ashby, Lever, SmartRecruiters adapters
|
|
182
226
|
- Title, topic, location, and date filters
|
|
183
227
|
- Salary extraction from JD text
|
|
184
|
-
- Verified company registry (
|
|
228
|
+
- Verified company registry (100+ companies)
|
|
185
229
|
|
|
186
230
|
**Next**
|
|
231
|
+
- TeamTailor adapter (European startup coverage)
|
|
187
232
|
- Anthropic MCP marketplace submission
|
|
188
|
-
- Setup guide with screenshots (non-technical walkthrough)
|
|
189
|
-
- Remote MCP transport (for Claude.ai Custom Connectors)
|
|
190
233
|
|
|
191
234
|
**Planned**
|
|
192
|
-
- BambooHR and
|
|
235
|
+
- BambooHR and Workable adapters
|
|
236
|
+
- Workday support (scoped scraper — large enterprise universe)
|
|
193
237
|
- Temporal tracking (when roles open, close, reopen)
|
|
194
238
|
- Change detection
|
|
195
239
|
- Resume-aware fit scoring
|
|
@@ -208,7 +252,7 @@ All filters AND together. Deep dive on patterns and gotchas: [docs/filters.md](d
|
|
|
208
252
|
|
|
209
253
|
## Built by
|
|
210
254
|
|
|
211
|
-
**[Prashant R](https://prashantrana.xyz)**. PM who builds. I
|
|
255
|
+
**[Prashant R](https://prashantrana.xyz)**. PM who builds. I try out and build what really matters below the AI hype.
|
|
212
256
|
|
|
213
257
|
- Portfolio and writing: [prashantrana.xyz](https://prashantrana.xyz)
|
|
214
258
|
- [LinkedIn](https://www.linkedin.com/in/prashant-rana)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jd-intel",
|
|
3
|
-
"version": "0.
|
|
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",
|
|
@@ -28,7 +28,17 @@
|
|
|
28
28
|
"ai",
|
|
29
29
|
"claude",
|
|
30
30
|
"hiring",
|
|
31
|
-
"careers"
|
|
31
|
+
"careers",
|
|
32
|
+
"model-context-protocol",
|
|
33
|
+
"anthropic",
|
|
34
|
+
"cursor",
|
|
35
|
+
"windsurf",
|
|
36
|
+
"ai-tools",
|
|
37
|
+
"ai-assistant",
|
|
38
|
+
"job-search",
|
|
39
|
+
"applicant-tracking-system",
|
|
40
|
+
"developer-tools",
|
|
41
|
+
"nodejs"
|
|
32
42
|
],
|
|
33
43
|
"author": "Prashant R",
|
|
34
44
|
"license": "MIT",
|
package/registry/ashby.json
CHANGED
|
@@ -12,5 +12,18 @@
|
|
|
12
12
|
{"slug": "shiftkey", "name": "ShiftKey", "sector": "healthcare staffing"},
|
|
13
13
|
{"slug": "gorgias", "name": "Gorgias", "sector": "customer support"},
|
|
14
14
|
{"slug": "zapier", "name": "Zapier", "sector": "integration platform"},
|
|
15
|
-
{"slug": "clickup", "name": "ClickUp", "sector": "productivity"}
|
|
15
|
+
{"slug": "clickup", "name": "ClickUp", "sector": "productivity"},
|
|
16
|
+
{"slug": "higharc", "name": "Higharc", "sector": "architecture tech"},
|
|
17
|
+
{"slug": "mural", "name": "Mural", "sector": "collaboration"},
|
|
18
|
+
{"slug": "ironcladhq", "name": "Ironclad", "sector": "legal tech"},
|
|
19
|
+
{"slug": "wrapbook", "name": "Wrapbook", "sector": "entertainment payroll"},
|
|
20
|
+
{"slug": "Tabs", "name": "Tabs", "sector": "fintech"},
|
|
21
|
+
{"slug": "vibe", "name": "Vibe", "sector": "saas"},
|
|
22
|
+
{"slug": "unicourt", "name": "UniCourt", "sector": "legal tech"},
|
|
23
|
+
{"slug": "meridianlink", "name": "MeridianLink", "sector": "fintech"},
|
|
24
|
+
{"slug": "Hippocratic AI", "name": "Hippocratic AI", "sector": "healthcare ai"},
|
|
25
|
+
{"slug": "scan-com", "name": "Scan.com", "sector": "healthcare imaging"},
|
|
26
|
+
{"slug": "hiive", "name": "Hiive", "sector": "marketplaces"},
|
|
27
|
+
{"slug": "virtuous", "name": "Virtuous", "sector": "nonprofit crm"},
|
|
28
|
+
{"slug": "jasper ai", "name": "Jasper", "sector": "ai writing"}
|
|
16
29
|
]
|
package/registry/greenhouse.json
CHANGED
|
@@ -43,5 +43,37 @@
|
|
|
43
43
|
{"slug": "braze", "name": "Braze", "sector": "marketing tech"},
|
|
44
44
|
{"slug": "appsflyer", "name": "AppsFlyer", "sector": "marketing tech"},
|
|
45
45
|
{"slug": "attentive", "name": "Attentive", "sector": "marketing tech"},
|
|
46
|
-
{"slug": "iterable", "name": "Iterable", "sector": "marketing tech"}
|
|
46
|
+
{"slug": "iterable", "name": "Iterable", "sector": "marketing tech"},
|
|
47
|
+
{"slug": "nextroll", "name": "NextRoll", "sector": "adtech"},
|
|
48
|
+
{"slug": "cloudbeds", "name": "Cloudbeds", "sector": "hospitality tech"},
|
|
49
|
+
{"slug": "headspacesourcing", "name": "Headspace (Sourcing)", "sector": "mental health"},
|
|
50
|
+
{"slug": "fleetio", "name": "Fleetio", "sector": "fleet management"},
|
|
51
|
+
{"slug": "engine", "name": "Engine", "sector": "travel tech"},
|
|
52
|
+
{"slug": "axon", "name": "Axon", "sector": "public safety"},
|
|
53
|
+
{"slug": "sparrow", "name": "Sparrow", "sector": "hr tech"},
|
|
54
|
+
{"slug": "remotecom", "name": "Remote", "sector": "hr tech"},
|
|
55
|
+
{"slug": "weedmaps77", "name": "Weedmaps", "sector": "cannabis"},
|
|
56
|
+
{"slug": "patterndata", "name": "Pattern", "sector": "ecommerce data"},
|
|
57
|
+
{"slug": "tubitv", "name": "Tubi", "sector": "streaming"},
|
|
58
|
+
{"slug": "future", "name": "Future", "sector": "fitness"},
|
|
59
|
+
{"slug": "eltropyinc", "name": "Eltropy", "sector": "fintech"},
|
|
60
|
+
{"slug": "ziprecruiter", "name": "ZipRecruiter", "sector": "recruiting tech"},
|
|
61
|
+
{"slug": "cognitiv", "name": "Cognitiv", "sector": "adtech"},
|
|
62
|
+
{"slug": "maintainx", "name": "MaintainX", "sector": "industrial saas"},
|
|
63
|
+
{"slug": "betterhelpcom", "name": "BetterHelp", "sector": "mental health"},
|
|
64
|
+
{"slug": "shopmonkey", "name": "Shopmonkey", "sector": "automotive saas"},
|
|
65
|
+
{"slug": "ezcaterinc", "name": "ezCater", "sector": "food tech"},
|
|
66
|
+
{"slug": "trivelta", "name": "TriVelta", "sector": "saas"},
|
|
67
|
+
{"slug": "firstconnectinsurance", "name": "First Connect Insurance", "sector": "insurtech"},
|
|
68
|
+
{"slug": "justworks", "name": "Justworks", "sector": "hr tech"},
|
|
69
|
+
{"slug": "starrez", "name": "StarRez", "sector": "property management"},
|
|
70
|
+
{"slug": "upstart", "name": "Upstart", "sector": "fintech"},
|
|
71
|
+
{"slug": "affirm", "name": "Affirm", "sector": "fintech"},
|
|
72
|
+
{"slug": "houseaccount", "name": "House Account", "sector": "saas"},
|
|
73
|
+
{"slug": "appdirect", "name": "AppDirect", "sector": "cloud commerce"},
|
|
74
|
+
{"slug": "rubrik", "name": "Rubrik", "sector": "security"},
|
|
75
|
+
{"slug": "asana", "name": "Asana", "sector": "productivity"},
|
|
76
|
+
{"slug": "instacart", "name": "Instacart", "sector": "grocery delivery"},
|
|
77
|
+
{"slug": "discord", "name": "Discord", "sector": "social"},
|
|
78
|
+
{"slug": "hs", "name": "Headspace", "sector": "mental health"}
|
|
47
79
|
]
|
package/registry/lever.json
CHANGED
|
@@ -6,5 +6,12 @@
|
|
|
6
6
|
{"slug": "clari", "name": "Clari", "sector": "sales tech"},
|
|
7
7
|
{"slug": "netflix", "name": "Netflix", "sector": "media"},
|
|
8
8
|
{"slug": "lever", "name": "Lever", "sector": "recruiting tech"},
|
|
9
|
-
{"slug": "postman",
|
|
9
|
+
{"slug": "postman", "name": "Postman", "sector": "developer tools"},
|
|
10
|
+
{"slug": "agiloft", "name": "Agiloft", "sector": "contract management"},
|
|
11
|
+
{"slug": "gohighlevel", "name": "HighLevel", "sector": "marketing saas"},
|
|
12
|
+
{"slug": "veeva", "name": "Veeva Systems", "sector": "life sciences"},
|
|
13
|
+
{"slug": "caremessage", "name": "CareMessage", "sector": "healthcare communications"},
|
|
14
|
+
{"slug": "rover", "name": "Rover", "sector": "pet care"},
|
|
15
|
+
{"slug": "LuminDigital", "name": "Lumin Digital", "sector": "fintech (banking)"},
|
|
16
|
+
{"slug": "redoxengine", "name": "Redox", "sector": "healthcare interop"}
|
|
10
17
|
]
|
|
@@ -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
|
+
}
|