jobspy-js 1.6.0 → 1.7.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/AGENTS.md +93 -0
- package/CHANGELOG.md +110 -0
- package/README.md +108 -2
- package/dist/cli/index.cjs +46 -6
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +46 -6
- package/dist/cli/index.js.map +1 -1
- package/dist/credentials.d.ts +8 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/mcp/index.cjs +1 -1
- package/dist/mcp/index.js +1 -1
- package/dist/{scraper--3KGfypo.cjs → scraper-975Qy6MB.cjs} +139 -4
- package/dist/scraper-975Qy6MB.cjs.map +1 -0
- package/dist/{scraper-DMYBxp8J.js → scraper-CC9KdZdU.js} +139 -4
- package/dist/scraper-CC9KdZdU.js.map +1 -0
- package/dist/scraper.d.ts +3 -1
- package/dist/scrapers/base.d.ts +9 -1
- package/dist/scrapers/linkedin/index.d.ts +11 -1
- package/dist/types.d.ts +43 -0
- package/package.json +1 -1
- package/src/cli/index.ts +54 -0
- package/src/credentials.ts +76 -0
- package/src/scraper.ts +9 -4
- package/src/scrapers/base.ts +14 -2
- package/src/scrapers/linkedin/index.ts +99 -2
- package/src/types.ts +49 -0
- package/dist/scraper--3KGfypo.cjs.map +0 -1
- package/dist/scraper-DMYBxp8J.js.map +0 -1
package/AGENTS.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# AGENTS.md — AI Agent Guidelines
|
|
2
|
+
|
|
3
|
+
This file instructs AI coding agents (GitHub Copilot, Cursor, Claude, etc.) on how to contribute to this project correctly.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Mandatory: Update README and CHANGELOG on Every Meaningful Change
|
|
8
|
+
|
|
9
|
+
Whenever you make a change that affects user-visible behaviour — a new feature, a bug fix, a breaking change, a new CLI flag, a new API parameter, or a change to an existing interface — you **must** update both files as part of the same task:
|
|
10
|
+
|
|
11
|
+
1. **[CHANGELOG.md](CHANGELOG.md)** — record the change under `[Unreleased]`.
|
|
12
|
+
2. **[README.md](README.md)** — update any affected section (feature list, tables, examples, structure diagram).
|
|
13
|
+
|
|
14
|
+
Do not skip these updates. Do not leave them as a separate follow-up task.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## CHANGELOG Rules
|
|
19
|
+
|
|
20
|
+
Follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) conventions:
|
|
21
|
+
|
|
22
|
+
- All unreleased work goes under `## [Unreleased]` at the top.
|
|
23
|
+
- Use these subsection headers: `Added`, `Changed`, `Fixed`, `Removed`, `Security`, `Deprecated`.
|
|
24
|
+
- Write entries from the **user's point of view** — describe what they can now do, not what lines changed.
|
|
25
|
+
- When a version is released, rename `[Unreleased]` to `## [x.y.z] — YYYY-MM-DD` and add a new empty `[Unreleased]` block above it.
|
|
26
|
+
- Update the comparison links at the bottom of the file whenever a new version is cut.
|
|
27
|
+
|
|
28
|
+
### Entry format
|
|
29
|
+
|
|
30
|
+
```markdown
|
|
31
|
+
## [Unreleased]
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
- **Short feature name** — one-sentence description of what was added and why it is useful.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
- Brief description of the bug and what was corrected.
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## README Rules
|
|
43
|
+
|
|
44
|
+
The README is the primary user-facing documentation. Keep it accurate and complete:
|
|
45
|
+
|
|
46
|
+
| Section to update | When |
|
|
47
|
+
|---|---|
|
|
48
|
+
| **Features** bullet list | new capability added |
|
|
49
|
+
| **SDK — Parameters table** (`scrapeJobs` params) | new/changed param |
|
|
50
|
+
| **Authentication / Credentials** section | credential-related changes |
|
|
51
|
+
| **CLI — Quick Start** examples | new common use-case |
|
|
52
|
+
| **CLI — All CLI Options** table | new/changed CLI flag |
|
|
53
|
+
| **Config File — Profile Options** table | new/changed profile key |
|
|
54
|
+
| **Project Structure** tree | new source file added |
|
|
55
|
+
| **Supported Sites** table | new scraper |
|
|
56
|
+
|
|
57
|
+
Do not add a new CLI flag or SDK parameter without adding it to the corresponding table in README.md.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Code Change Guidelines
|
|
62
|
+
|
|
63
|
+
### Types first
|
|
64
|
+
New user-facing features start in `src/types.ts`. Add interfaces and fields there before implementing them elsewhere, so TypeScript catches all callsites.
|
|
65
|
+
|
|
66
|
+
### Credential / auth changes
|
|
67
|
+
- Load credentials via `src/credentials.ts` — do not read `process.env` directly in scrapers.
|
|
68
|
+
- Credentials should only be *used* when `useCreds === true`. Never auto-login without explicit opt-in.
|
|
69
|
+
- Always test anonymous scraping first; authenticated fallback is a last resort.
|
|
70
|
+
|
|
71
|
+
### Scraper changes
|
|
72
|
+
- The constructor signature for scrapers is `{ proxies?, credentials?, useCreds? }` — maintain this shape.
|
|
73
|
+
- `SCRAPER_MAP` in `src/scraper.ts` must reflect the current constructor signature type.
|
|
74
|
+
- New scrapers must implement `scrape(input)` and should implement `fetchJob(id, format)` if single-job fetching is possible.
|
|
75
|
+
|
|
76
|
+
### CLI changes
|
|
77
|
+
- New options must be added in three places: the `program.option(...)` chain, the `o = { ... }` merge object, and the `scrapeJobs(...)` call.
|
|
78
|
+
- Profile config keys use `snake_case`; CLI flags use `--kebab-case`; the merge object uses `camelCase`.
|
|
79
|
+
|
|
80
|
+
### Testing
|
|
81
|
+
- Run `pnpm build` after changes to confirm TypeScript compiles cleanly.
|
|
82
|
+
- Add / update tests in `tests/` for any logic in `src/state.ts`, `src/credentials.ts`, or `src/utils.ts`.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Release Checklist
|
|
87
|
+
|
|
88
|
+
When cutting a new version:
|
|
89
|
+
|
|
90
|
+
1. Move `[Unreleased]` entries in `CHANGELOG.md` to a new dated version block.
|
|
91
|
+
2. Bump `version` in `package.json`.
|
|
92
|
+
3. Update CHANGELOG comparison links at the bottom.
|
|
93
|
+
4. Commit with message: `chore: release vX.Y.Z`.
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## [Unreleased]
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **Provider credentials** — per-provider username/password support for all scrapers via env vars (`LINKEDIN_USERNAME` / `LINKEDIN_PASSWORD`, `INDEED_USERNAME` / `INDEED_PASSWORD`, `GLASSDOOR_USERNAME` / `GLASSDOOR_PASSWORD`, `ZIPRECRUITER_*`, `BAYT_*`, `NAUKRI_*`, `BDJOBS_*`), CLI flags (`--linkedin-username`, `--linkedin-password`, …), or `credentials` object in `ScrapeJobsParams`.
|
|
13
|
+
- **`--creds` CLI flag** — opt-in authenticated scraping fallback (also `JOBSPY_CREDS=1` env var). Credentials are loaded but only *used* when this flag is set.
|
|
14
|
+
- **LinkedIn login fallback** — on 429 throttle or auth-wall redirect, the LinkedIn scraper automatically attempts a form-based session login and retries the blocked request when `--creds` is active.
|
|
15
|
+
- `src/credentials.ts` — new module that merges credentials from three priority layers: env vars → per-parameter fields → explicit `credentials` object.
|
|
16
|
+
- `ProviderCredentials` and `ProviderCreds` types exported from `types.ts`.
|
|
17
|
+
- `use_creds`, `credentials`, and all `*_username`/`*_password` fields added to `ScrapeJobsParams` and `ScraperInput`.
|
|
18
|
+
- `--init` profiles now include commented-out credential examples.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## [1.6.0] — 2026-03-02
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- **`fetchJobDetails(site, id, options)`** — fetch full job details by provider-specific ID for *any* supported site (`indeed`, `linkedin`, `glassdoor`, `zip_recruiter`, `bayt`, `naukri`, `bdjobs`). Equivalent to `fetchLinkedInJob` but provider-agnostic.
|
|
26
|
+
- **`--id <jobId>` CLI flag** — fetch full job details from any provider by ID (requires `-s/--site`).
|
|
27
|
+
- Abstract `fetchJob(id, format)` method on the base `Scraper` class; subclasses override per provider.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
- LinkedIn scraper now distinguishes between a true auth-wall redirect (no job content) and a page that merely references the signup URL — prevents false-positive empty results.
|
|
31
|
+
- LinkedIn auth-wall check uses `show-more-less-html__markup` presence as a page-content signal.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## [1.5.0] — 2026-02-27
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
- **`fetchLinkedInJob(idOrUrl, options)`** — fetch full details for a single LinkedIn job by numeric ID or full URL. Returns description, job level, job type, job function, company industry, company logo, and direct application URL.
|
|
39
|
+
- **`--describe <jobId>` CLI flag** — fetch and pretty-print LinkedIn job details from the command line.
|
|
40
|
+
- **Unified `jobspy.json` config file** — single file stores both search profiles (`config.profiles`) and dedup state (`state.profiles`). Replaces the previous separate state file.
|
|
41
|
+
- **`--init` CLI flag** — generates a `jobspy.json` with two sample profiles (`frontend`, `backend`).
|
|
42
|
+
- **`--list-profiles` CLI flag** — lists all profiles with last-run timestamp, sites, and search term.
|
|
43
|
+
- **`--profile <name>` CLI flag** — run a named search profile from `jobspy.json`; CLI flags override profile values.
|
|
44
|
+
- **`--all` CLI flag** — skip dedup filtering for one run while still updating state.
|
|
45
|
+
- **Dedup / incremental runs** — `scrapeJobs()` with a profile name automatically tracks seen URLs and date watermarks per provider. Only new jobs are returned on subsequent runs.
|
|
46
|
+
- `profile` and `skip_dedup` fields in `ScrapeJobsParams`.
|
|
47
|
+
- `ScrapeJobsResult` now includes `totalScraped`, `newCount`, and `profile` metadata.
|
|
48
|
+
- `src/state.ts` — state file I/O, `filterNewJobs()`, `updateProviderState()`, `mergeParams()`.
|
|
49
|
+
|
|
50
|
+
### Changed
|
|
51
|
+
- CLI option merging: profile config values are used as defaults; CLI flags always take precedence.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## [1.3.0] — 2026-02-21
|
|
56
|
+
|
|
57
|
+
### Changed
|
|
58
|
+
- **Dual ESM/CJS output** — Vite build now emits both `.js` (ESM) and `.cjs` (CommonJS) bundles for broad compatibility with Node.js consumers.
|
|
59
|
+
- Updated `package.json` exports map with `import`/`require` conditions.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## [1.2.0] — 2026-02-21
|
|
64
|
+
|
|
65
|
+
### Added
|
|
66
|
+
- **`SDK.md`** — comprehensive SDK reference covering all parameters, types, enums, output fields, proxy configuration, country support, and advanced examples.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## [1.1.0] — 2026-02-18
|
|
71
|
+
|
|
72
|
+
### Added
|
|
73
|
+
- **Google Careers scraper** (`google_careers`) — scrapes jobs posted at Google the company via plain HTTP; parses `AF_initDataCallback` JSON payload.
|
|
74
|
+
- **Playwright support for Google Jobs** (`google`) — headless Chromium execution via `@playwright/test` to handle JavaScript-rendered job listings.
|
|
75
|
+
|
|
76
|
+
### Fixed
|
|
77
|
+
- ZipRecruiter scraper — corrected JSON extraction and pagination.
|
|
78
|
+
- BDJobs scraper — fixed request parameters and result parsing.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## [1.0.1] — 2026-02-18
|
|
83
|
+
|
|
84
|
+
### Changed
|
|
85
|
+
- Refactored source structure for improved readability and maintainability (module split, consistent naming).
|
|
86
|
+
- Added `release` script to `package.json`.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## [1.0.0] — 2026-02-17
|
|
91
|
+
|
|
92
|
+
### Added
|
|
93
|
+
- Initial TypeScript port of [JobSpy](https://github.com/speedyapply/JobSpy) (Python).
|
|
94
|
+
- **9 scrapers**: LinkedIn (HTML), Indeed (GraphQL), Glassdoor (GraphQL), Google Jobs (Playwright), Google Careers (HTTP), ZipRecruiter, Bayt, Naukri (REST), BDJobs (REST).
|
|
95
|
+
- **Three interfaces**: SDK (`scrapeJobs()`), CLI (`jobspy`), MCP server.
|
|
96
|
+
- [wreq-js](https://github.com/nicehash/wreq-js) browser TLS fingerprint emulation (JA3/JA4, Chrome/Firefox/Safari).
|
|
97
|
+
- Concurrent multi-site scraping via `Promise.allSettled`.
|
|
98
|
+
- Proxy rotation support.
|
|
99
|
+
- Salary extraction from job descriptions.
|
|
100
|
+
- 60+ country support for Indeed and Glassdoor regional domains.
|
|
101
|
+
- `JobPost`, `ScraperInput`, `JobResponse`, and all supporting types.
|
|
102
|
+
|
|
103
|
+
[Unreleased]: https://github.com/borgius/jobspy-js/compare/v1.6.0...HEAD
|
|
104
|
+
[1.6.0]: https://github.com/borgius/jobspy-js/compare/v1.5.0...v1.6.0
|
|
105
|
+
[1.5.0]: https://github.com/borgius/jobspy-js/compare/v1.3.0...v1.5.0
|
|
106
|
+
[1.3.0]: https://github.com/borgius/jobspy-js/compare/v1.2.0...v1.3.0
|
|
107
|
+
[1.2.0]: https://github.com/borgius/jobspy-js/compare/v1.1.0...v1.2.0
|
|
108
|
+
[1.1.0]: https://github.com/borgius/jobspy-js/compare/v1.0.1...v1.1.0
|
|
109
|
+
[1.0.1]: https://github.com/borgius/jobspy-js/compare/v1.0.0...v1.0.1
|
|
110
|
+
[1.0.0]: https://github.com/borgius/jobspy-js/releases/tag/v1.0.0
|
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@ Uses [wreq-js](https://github.com/nicehash/wreq-js) for browser TLS fingerprint
|
|
|
15
15
|
- **Concurrent scraping** — all sites scraped in parallel
|
|
16
16
|
- **Salary extraction** — parses compensation from descriptions when not provided directly
|
|
17
17
|
- **60+ countries** — Indeed/Glassdoor regional domain support
|
|
18
|
+
- **Credential fallback** — optional per-provider login (env vars or CLI flags) when anonymous scraping is blocked
|
|
18
19
|
|
|
19
20
|
## Supported Sites
|
|
20
21
|
|
|
@@ -89,6 +90,14 @@ console.log(job.description);
|
|
|
89
90
|
| `enforce_annual_salary` | `boolean` | `false` | Convert all salaries to annual |
|
|
90
91
|
| `profile` | `string` | — | Named profile for dedup tracking |
|
|
91
92
|
| `skip_dedup` | `boolean` | `false` | Skip dedup filtering (still updates state) |
|
|
93
|
+
| `use_creds` | `boolean` | `false` | Enable credential fallback when anonymous scraping is blocked (also: `JOBSPY_CREDS=1`) |
|
|
94
|
+
| `credentials` | `ProviderCredentials` | — | Explicit credentials object (see [Authentication](#authentication--credentials)) |
|
|
95
|
+
| `linkedin_username` | `string` | — | LinkedIn username/email (also: `LINKEDIN_USERNAME`) |
|
|
96
|
+
| `linkedin_password` | `string` | — | LinkedIn password (also: `LINKEDIN_PASSWORD`) |
|
|
97
|
+
| `indeed_username` | `string` | — | Indeed username/email (also: `INDEED_USERNAME`) |
|
|
98
|
+
| `indeed_password` | `string` | — | Indeed password (also: `INDEED_PASSWORD`) |
|
|
99
|
+
| `glassdoor_username` | `string` | — | Glassdoor username/email (also: `GLASSDOOR_USERNAME`) |
|
|
100
|
+
| `glassdoor_password` | `string` | — | Glassdoor password (also: `GLASSDOOR_PASSWORD`) |
|
|
92
101
|
|
|
93
102
|
### fetchLinkedInJob()
|
|
94
103
|
|
|
@@ -135,8 +144,79 @@ console.log(job.company); // company name
|
|
|
135
144
|
Options: `{ format?: "markdown"|"html"|"plain", proxies?: string|string[], country?: string }`
|
|
136
145
|
|
|
137
146
|
> **Full reference:** See [SDK.md](https://github.com/borgius/jobspy-js/blob/master/SDK.md#fetchjobdetails) for all fields and examples.
|
|
147
|
+
## Authentication / Credentials
|
|
138
148
|
|
|
139
|
-
|
|
149
|
+
All providers support optional authenticated scraping as a fallback for when anonymous access is blocked (e.g. LinkedIn 429s or auth-wall redirects). Credentials are **never used unless explicitly enabled**.
|
|
150
|
+
|
|
151
|
+
### Enable credential fallback
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
# Via env var
|
|
155
|
+
JOBSPY_CREDS=1 jobspy -s linkedin -q "engineer"
|
|
156
|
+
|
|
157
|
+
# Via CLI flag
|
|
158
|
+
jobspy -s linkedin -q "engineer" --creds
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Or in the SDK:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
const result = await scrapeJobs({
|
|
165
|
+
site_name: ["linkedin"],
|
|
166
|
+
search_term: "engineer",
|
|
167
|
+
use_creds: true,
|
|
168
|
+
linkedin_username: process.env.LINKEDIN_USERNAME,
|
|
169
|
+
linkedin_password: process.env.LINKEDIN_PASSWORD,
|
|
170
|
+
});
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Setting credentials
|
|
174
|
+
|
|
175
|
+
Credentials are resolved in this priority order (highest wins):
|
|
176
|
+
|
|
177
|
+
1. **Explicit `credentials` object** in `ScrapeJobsParams`
|
|
178
|
+
2. **Per-field params** (`linkedin_username`, `linkedin_password`, …)
|
|
179
|
+
3. **Environment variables**
|
|
180
|
+
|
|
181
|
+
#### Environment variables
|
|
182
|
+
|
|
183
|
+
| Provider | Username env var | Password env var |
|
|
184
|
+
|----------|-----------------|------------------|
|
|
185
|
+
| LinkedIn | `LINKEDIN_USERNAME` | `LINKEDIN_PASSWORD` |
|
|
186
|
+
| Indeed | `INDEED_USERNAME` | `INDEED_PASSWORD` |
|
|
187
|
+
| Glassdoor | `GLASSDOOR_USERNAME` | `GLASSDOOR_PASSWORD` |
|
|
188
|
+
| ZipRecruiter | `ZIPRECRUITER_USERNAME` | `ZIPRECRUITER_PASSWORD` |
|
|
189
|
+
| Bayt | `BAYT_USERNAME` | `BAYT_PASSWORD` |
|
|
190
|
+
| Naukri | `NAUKRI_USERNAME` | `NAUKRI_PASSWORD` |
|
|
191
|
+
| BDJobs | `BDJOBS_USERNAME` | `BDJOBS_PASSWORD` |
|
|
192
|
+
|
|
193
|
+
#### CLI flags
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
jobspy -s linkedin -q "engineer" --creds \
|
|
197
|
+
--linkedin-username me@email.com \
|
|
198
|
+
--linkedin-password secret
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
#### `jobspy.json` profile
|
|
202
|
+
|
|
203
|
+
```json
|
|
204
|
+
{
|
|
205
|
+
"config": {
|
|
206
|
+
"profiles": {
|
|
207
|
+
"frontend": {
|
|
208
|
+
"site": ["linkedin", "indeed"],
|
|
209
|
+
"search_term": "react developer",
|
|
210
|
+
"creds": true,
|
|
211
|
+
"linkedin_username": "me@email.com",
|
|
212
|
+
"linkedin_password": "secret"
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
> **Security note:** Prefer environment variables over storing passwords in `jobspy.json`. The state section of that file is committed by some users.
|
|
140
220
|
|
|
141
221
|
### Quick Start
|
|
142
222
|
|
|
@@ -160,6 +240,10 @@ jobspy --describe https://www.linkedin.com/jobs/view/4127292817
|
|
|
160
240
|
# Fetch full job details by ID for any provider
|
|
161
241
|
jobspy -s indeed --id fdde406379455a1e
|
|
162
242
|
jobspy -s glassdoor --id 123456789
|
|
243
|
+
|
|
244
|
+
# Use credential fallback (anonymous scraping blocked)
|
|
245
|
+
JOBSPY_CREDS=1 LINKEDIN_USERNAME=me@email.com LINKEDIN_PASSWORD=secret jobspy -s linkedin -q "engineer"
|
|
246
|
+
jobspy -s linkedin -q "engineer" --creds --linkedin-username me@email.com --linkedin-password secret
|
|
163
247
|
```
|
|
164
248
|
|
|
165
249
|
### All CLI Options
|
|
@@ -193,6 +277,20 @@ jobspy -s glassdoor --id 123456789
|
|
|
193
277
|
| `--init` | | — | Generate a `jobspy.json` with sample profiles |
|
|
194
278
|
| `--describe <jobId>` | | — | Fetch full LinkedIn job details by ID or URL |
|
|
195
279
|
| `--id <jobId>` | | — | Fetch full job details by ID (requires `-s/--site`) |
|
|
280
|
+
| **Credentials** | | | |
|
|
281
|
+
| `--creds` | | `false` | Enable credential fallback when anonymous scraping is blocked (also: `JOBSPY_CREDS=1`) |
|
|
282
|
+
| `--linkedin-username <user>` | | — | LinkedIn username/email (also: `LINKEDIN_USERNAME`) |
|
|
283
|
+
| `--linkedin-password <pass>` | | — | LinkedIn password (also: `LINKEDIN_PASSWORD`) |
|
|
284
|
+
| `--indeed-username <user>` | | — | Indeed username/email (also: `INDEED_USERNAME`) |
|
|
285
|
+
| `--indeed-password <pass>` | | — | Indeed password (also: `INDEED_PASSWORD`) |
|
|
286
|
+
| `--glassdoor-username <user>` | | — | Glassdoor username/email (also: `GLASSDOOR_USERNAME`) |
|
|
287
|
+
| `--glassdoor-password <pass>` | | — | Glassdoor password (also: `GLASSDOOR_PASSWORD`) |
|
|
288
|
+
| `--ziprecruiter-username <user>` | | — | ZipRecruiter username/email (also: `ZIPRECRUITER_USERNAME`) |
|
|
289
|
+
| `--ziprecruiter-password <pass>` | | — | ZipRecruiter password (also: `ZIPRECRUITER_PASSWORD`) |
|
|
290
|
+
| `--bayt-username <user>` | | — | Bayt username/email (also: `BAYT_USERNAME`) |
|
|
291
|
+
| `--bayt-password <pass>` | | — | Bayt password (also: `BAYT_PASSWORD`) |
|
|
292
|
+
| `--naukri-username <user>` | | — | Naukri username/email (also: `NAUKRI_USERNAME`) |
|
|
293
|
+
| `--naukri-password <pass>` | | — | Naukri password (also: `NAUKRI_PASSWORD`) |
|
|
196
294
|
|
|
197
295
|
## Config File (`jobspy.json`)
|
|
198
296
|
|
|
@@ -275,6 +373,13 @@ Each profile in `config.profiles` supports the following keys:
|
|
|
275
373
|
| `enforce_annual_salary` | `boolean` | Normalize salaries to annual |
|
|
276
374
|
| `verbose` | `number` | Log verbosity level |
|
|
277
375
|
| `output` | `string` | Output file path |
|
|
376
|
+
| `creds` | `boolean` | Enable credential fallback (also: `JOBSPY_CREDS=1`) |
|
|
377
|
+
| `linkedin_username` | `string` | LinkedIn username/email (also: `LINKEDIN_USERNAME`) |
|
|
378
|
+
| `linkedin_password` | `string` | LinkedIn password (also: `LINKEDIN_PASSWORD`) |
|
|
379
|
+
| `indeed_username` | `string` | Indeed username/email (also: `INDEED_USERNAME`) |
|
|
380
|
+
| `indeed_password` | `string` | Indeed password (also: `INDEED_PASSWORD`) |
|
|
381
|
+
| `glassdoor_username` | `string` | Glassdoor username/email (also: `GLASSDOOR_USERNAME`) |
|
|
382
|
+
| `glassdoor_password` | `string` | Glassdoor password (also: `GLASSDOOR_PASSWORD`) |
|
|
278
383
|
|
|
279
384
|
### Running Profiles
|
|
280
385
|
|
|
@@ -441,6 +546,7 @@ npm test
|
|
|
441
546
|
src/
|
|
442
547
|
├── index.ts # SDK entry point
|
|
443
548
|
├── scraper.ts # Main scrapeJobs() orchestrator
|
|
549
|
+
├── credentials.ts # Credential loader (env → params → object merge)
|
|
444
550
|
├── state.ts # Profile state, dedup logic, file I/O
|
|
445
551
|
├── types.ts # All types, enums, country config
|
|
446
552
|
├── utils.ts # Logger, proxy rotation, HTML helpers
|
|
@@ -449,7 +555,7 @@ src/
|
|
|
449
555
|
└── scrapers/
|
|
450
556
|
├── base.ts # Abstract Scraper base class
|
|
451
557
|
├── indeed/ # GraphQL API
|
|
452
|
-
├── linkedin/ # HTML scraping
|
|
558
|
+
├── linkedin/ # HTML scraping + optional login fallback
|
|
453
559
|
├── glassdoor/ # GraphQL API
|
|
454
560
|
├── google/ # Playwright headless Chrome
|
|
455
561
|
├── google-careers/ # Plain HTTP; AF_initDataCallback JSON parsing
|
package/dist/cli/index.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
const commander = require("commander");
|
|
4
|
-
const state = require("../scraper
|
|
4
|
+
const state = require("../scraper-975Qy6MB.cjs");
|
|
5
5
|
const node_fs = require("node:fs");
|
|
6
6
|
const node_path = require("node:path");
|
|
7
7
|
const program = new commander.Command();
|
|
@@ -30,7 +30,7 @@ program.name("jobspy").description(
|
|
|
30
30
|
).option("--offset <offset>", "Start from offset", "0").option(
|
|
31
31
|
"--hours-old <hours>",
|
|
32
32
|
"Filter jobs posted within N hours"
|
|
33
|
-
).option("--enforce-annual-salary", "Convert all salaries to annual").option("-v, --verbose <level>", "Verbosity (0=errors, 1=warnings, 2=all)", "0").option("-o, --output <file>", "Output file path (JSON or CSV based on extension)").option("--profile <name>", "Named search profile from jobspy.json").option("--all", "Skip dedup for this run (still updates state)").option("--list-profiles", "List saved profiles and their last run time").option("--init", "Generate a jobspy.json with sample profiles").option("--describe <jobId>", "Fetch full LinkedIn job details by ID or URL").option("--id <jobId>", "Fetch full job details by ID (requires -s/--site)").action(async (opts) => {
|
|
33
|
+
).option("--enforce-annual-salary", "Convert all salaries to annual").option("-v, --verbose <level>", "Verbosity (0=errors, 1=warnings, 2=all)", "0").option("-o, --output <file>", "Output file path (JSON or CSV based on extension)").option("--profile <name>", "Named search profile from jobspy.json").option("--all", "Skip dedup for this run (still updates state)").option("--list-profiles", "List saved profiles and their last run time").option("--init", "Generate a jobspy.json with sample profiles").option("--describe <jobId>", "Fetch full LinkedIn job details by ID or URL").option("--id <jobId>", "Fetch full job details by ID (requires -s/--site)").option("--creds", "Use stored/env credentials as fallback when anonymous scraping is blocked (also: JOBSPY_CREDS=1)").option("--linkedin-username <user>", "LinkedIn username/email (also: LINKEDIN_USERNAME)").option("--linkedin-password <pass>", "LinkedIn password (also: LINKEDIN_PASSWORD)").option("--indeed-username <user>", "Indeed username/email (also: INDEED_USERNAME)").option("--indeed-password <pass>", "Indeed password (also: INDEED_PASSWORD)").option("--glassdoor-username <user>", "Glassdoor username/email (also: GLASSDOOR_USERNAME)").option("--glassdoor-password <pass>", "Glassdoor password (also: GLASSDOOR_PASSWORD)").option("--ziprecruiter-username <user>", "ZipRecruiter username/email (also: ZIPRECRUITER_USERNAME)").option("--ziprecruiter-password <pass>", "ZipRecruiter password (also: ZIPRECRUITER_PASSWORD)").option("--bayt-username <user>", "Bayt username/email (also: BAYT_USERNAME)").option("--bayt-password <pass>", "Bayt password (also: BAYT_PASSWORD)").option("--naukri-username <user>", "Naukri username/email (also: NAUKRI_USERNAME)").option("--naukri-password <pass>", "Naukri password (also: NAUKRI_PASSWORD)").action(async (opts) => {
|
|
34
34
|
if (opts.init) {
|
|
35
35
|
const filePath = node_path.resolve(process.cwd(), "jobspy.json");
|
|
36
36
|
if (node_fs.existsSync(filePath)) {
|
|
@@ -60,6 +60,12 @@ program.name("jobspy").description(
|
|
|
60
60
|
enforce_annual_salary: true,
|
|
61
61
|
verbose: 1,
|
|
62
62
|
output: "frontend-jobs.csv"
|
|
63
|
+
// Credentials (optional – enable with creds:true or env JOBSPY_CREDS=1)
|
|
64
|
+
// creds: false,
|
|
65
|
+
// linkedin_username: "",
|
|
66
|
+
// linkedin_password: "",
|
|
67
|
+
// indeed_username: "",
|
|
68
|
+
// indeed_password: "",
|
|
63
69
|
},
|
|
64
70
|
backend: {
|
|
65
71
|
site: ["linkedin", "indeed", "zip_recruiter", "glassdoor", "google", "bayt", "naukri", "bdjobs"],
|
|
@@ -81,6 +87,12 @@ program.name("jobspy").description(
|
|
|
81
87
|
enforce_annual_salary: true,
|
|
82
88
|
verbose: 1,
|
|
83
89
|
output: "backend-jobs.json"
|
|
90
|
+
// Credentials (optional – enable with creds:true or env JOBSPY_CREDS=1)
|
|
91
|
+
// creds: false,
|
|
92
|
+
// linkedin_username: "",
|
|
93
|
+
// linkedin_password: "",
|
|
94
|
+
// indeed_username: "",
|
|
95
|
+
// indeed_password: "",
|
|
84
96
|
}
|
|
85
97
|
}
|
|
86
98
|
},
|
|
@@ -130,7 +142,7 @@ program.name("jobspy").description(
|
|
|
130
142
|
return;
|
|
131
143
|
}
|
|
132
144
|
if (opts.listProfiles) {
|
|
133
|
-
const { findJobspyPath, loadFile } = await Promise.resolve().then(() => require("../scraper
|
|
145
|
+
const { findJobspyPath, loadFile } = await Promise.resolve().then(() => require("../scraper-975Qy6MB.cjs")).then((n) => n.state);
|
|
134
146
|
const filePath = findJobspyPath();
|
|
135
147
|
const file = loadFile(filePath);
|
|
136
148
|
const configNames = Object.keys(file.config.profiles);
|
|
@@ -153,7 +165,7 @@ program.name("jobspy").description(
|
|
|
153
165
|
}
|
|
154
166
|
let cfg = {};
|
|
155
167
|
if (opts.profile) {
|
|
156
|
-
const { findJobspyPath, loadFile } = await Promise.resolve().then(() => require("../scraper
|
|
168
|
+
const { findJobspyPath, loadFile } = await Promise.resolve().then(() => require("../scraper-975Qy6MB.cjs")).then((n) => n.state);
|
|
157
169
|
const filePath = findJobspyPath();
|
|
158
170
|
const file = loadFile(filePath);
|
|
159
171
|
const profileConfig = file.config.profiles[opts.profile];
|
|
@@ -184,7 +196,21 @@ program.name("jobspy").description(
|
|
|
184
196
|
verbose: cliSet.verbose !== void 0 ? opts.verbose : String(cfg.verbose ?? 0),
|
|
185
197
|
output: opts.output ?? cfg.output ?? void 0,
|
|
186
198
|
profile: opts.profile,
|
|
187
|
-
all: opts.all ?? false
|
|
199
|
+
all: opts.all ?? false,
|
|
200
|
+
// Credentials
|
|
201
|
+
creds: opts.creds ?? cfg.creds ?? false,
|
|
202
|
+
linkedinUsername: opts.linkedinUsername ?? cfg.linkedin_username ?? void 0,
|
|
203
|
+
linkedinPassword: opts.linkedinPassword ?? cfg.linkedin_password ?? void 0,
|
|
204
|
+
indeedUsername: opts.indeedUsername ?? cfg.indeed_username ?? void 0,
|
|
205
|
+
indeedPassword: opts.indeedPassword ?? cfg.indeed_password ?? void 0,
|
|
206
|
+
glassdoorUsername: opts.glassdoorUsername ?? cfg.glassdoor_username ?? void 0,
|
|
207
|
+
glassdoorPassword: opts.glassdoorPassword ?? cfg.glassdoor_password ?? void 0,
|
|
208
|
+
ziprecruiterUsername: opts.ziprecruiterUsername ?? cfg.ziprecruiter_username ?? void 0,
|
|
209
|
+
ziprecruiterPassword: opts.ziprecruiterPassword ?? cfg.ziprecruiter_password ?? void 0,
|
|
210
|
+
baytUsername: opts.baytUsername ?? cfg.bayt_username ?? void 0,
|
|
211
|
+
baytPassword: opts.baytPassword ?? cfg.bayt_password ?? void 0,
|
|
212
|
+
naukriUsername: opts.naukriUsername ?? cfg.naukri_username ?? void 0,
|
|
213
|
+
naukriPassword: opts.naukriPassword ?? cfg.naukri_password ?? void 0
|
|
188
214
|
};
|
|
189
215
|
if (o.all && !o.profile) {
|
|
190
216
|
console.warn("Warning: --all has no effect without --profile");
|
|
@@ -210,7 +236,21 @@ program.name("jobspy").description(
|
|
|
210
236
|
enforce_annual_salary: o.enforceAnnualSalary,
|
|
211
237
|
verbose: parseInt(o.verbose),
|
|
212
238
|
profile: o.profile,
|
|
213
|
-
skip_dedup: o.all
|
|
239
|
+
skip_dedup: o.all,
|
|
240
|
+
// Credentials
|
|
241
|
+
use_creds: o.creds || void 0,
|
|
242
|
+
linkedin_username: o.linkedinUsername,
|
|
243
|
+
linkedin_password: o.linkedinPassword,
|
|
244
|
+
indeed_username: o.indeedUsername,
|
|
245
|
+
indeed_password: o.indeedPassword,
|
|
246
|
+
glassdoor_username: o.glassdoorUsername,
|
|
247
|
+
glassdoor_password: o.glassdoorPassword,
|
|
248
|
+
ziprecruiter_username: o.ziprecruiterUsername,
|
|
249
|
+
ziprecruiter_password: o.ziprecruiterPassword,
|
|
250
|
+
bayt_username: o.baytUsername,
|
|
251
|
+
bayt_password: o.baytPassword,
|
|
252
|
+
naukri_username: o.naukriUsername,
|
|
253
|
+
naukri_password: o.naukriPassword
|
|
214
254
|
});
|
|
215
255
|
console.log(`Found ${result.jobs.length} jobs`);
|
|
216
256
|
if (result.profile) {
|
package/dist/cli/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":["../../src/cli/index.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { scrapeJobs, fetchLinkedInJob, fetchJobDetails } from \"../scraper\";\nimport { writeFileSync, existsSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\n\nconst program = new Command();\n\nprogram\n .name(\"jobspy\")\n .description(\n \"Job scraper for LinkedIn, Indeed, Glassdoor, Google, ZipRecruiter, Bayt, Naukri & BDJobs\",\n )\n .version(\"1.0.0\")\n .option(\n \"-s, --site <sites...>\",\n \"Job boards to scrape (linkedin, indeed, zip_recruiter, glassdoor, google, bayt, naukri, bdjobs)\",\n )\n .option(\"-q, --search-term <term>\", \"Search term\")\n .option(\"--google-search-term <term>\", \"Google-specific search term\")\n .option(\"-l, --location <location>\", \"Job location\")\n .option(\"-d, --distance <miles>\", \"Distance in miles\", \"50\")\n .option(\"-r, --remote\", \"Filter for remote jobs\")\n .option(\n \"-t, --job-type <type>\",\n \"Job type (fulltime, parttime, contract, internship)\",\n )\n .option(\"--easy-apply\", \"Filter for easy apply jobs\")\n .option(\"-n, --results <count>\", \"Number of results wanted (alias: --limit)\", \"15\")\n .option(\"--limit <count>\", \"Alias for --results\")\n .option(\n \"-c, --country <country>\",\n \"Country for Indeed/Glassdoor\",\n \"usa\",\n )\n .option(\n \"-p, --proxies <proxies...>\",\n \"Proxy servers (user:pass@host:port)\",\n )\n .option(\n \"--format <format>\",\n \"Description format (markdown, html, plain)\",\n \"markdown\",\n )\n .option(\"--linkedin-fetch-description\", \"Fetch full LinkedIn descriptions\")\n .option(\"--indeed-fetch-description\", \"Fetch full Indeed descriptions\")\n .option(\n \"--linkedin-company-ids <ids...>\",\n \"LinkedIn company IDs to filter\",\n )\n .option(\"--offset <offset>\", \"Start from offset\", \"0\")\n .option(\n \"--hours-old <hours>\",\n \"Filter jobs posted within N hours\",\n )\n .option(\"--enforce-annual-salary\", \"Convert all salaries to annual\")\n .option(\"-v, --verbose <level>\", \"Verbosity (0=errors, 1=warnings, 2=all)\", \"0\")\n .option(\"-o, --output <file>\", \"Output file path (JSON or CSV based on extension)\")\n .option(\"--profile <name>\", \"Named search profile from jobspy.json\")\n .option(\"--all\", \"Skip dedup for this run (still updates state)\")\n .option(\"--list-profiles\", \"List saved profiles and their last run time\")\n .option(\"--init\", \"Generate a jobspy.json with sample profiles\")\n .option(\"--describe <jobId>\", \"Fetch full LinkedIn job details by ID or URL\")\n .option(\"--id <jobId>\", \"Fetch full job details by ID (requires -s/--site)\")\n .action(async (opts) => {\n if (opts.init) {\n const filePath = resolve(process.cwd(), \"jobspy.json\");\n if (existsSync(filePath)) {\n console.error(`File already exists: ${filePath}`);\n process.exit(1);\n }\n const defaultFile = {\n config: {\n profiles: {\n frontend: {\n site: [\"linkedin\", \"indeed\", \"zip_recruiter\", \"glassdoor\", \"google\", \"bayt\", \"naukri\", \"bdjobs\"],\n search_term: \"react frontend developer\",\n google_search_term: \"react frontend developer jobs near New York NY\",\n location: \"New York, NY\",\n distance: 25,\n remote: false,\n job_type: \"fulltime\",\n easy_apply: false,\n results: 50,\n country: \"usa\",\n proxies: [],\n format: \"markdown\",\n linkedin_fetch_description: true,\n linkedin_company_ids: [],\n offset: 0,\n hours_old: 72,\n enforce_annual_salary: true,\n verbose: 1,\n output: \"frontend-jobs.csv\",\n },\n backend: {\n site: [\"linkedin\", \"indeed\", \"zip_recruiter\", \"glassdoor\", \"google\", \"bayt\", \"naukri\", \"bdjobs\"],\n search_term: \"node.js backend engineer\",\n google_search_term: \"node.js backend engineer jobs near New York NY\",\n location: \"New York, NY\",\n distance: 25,\n remote: true,\n job_type: \"fulltime\",\n easy_apply: false,\n results: 50,\n country: \"usa\",\n proxies: [],\n format: \"markdown\",\n linkedin_fetch_description: true,\n linkedin_company_ids: [],\n offset: 0,\n hours_old: 48,\n enforce_annual_salary: true,\n verbose: 1,\n output: \"backend-jobs.json\",\n },\n },\n },\n state: {\n version: 1,\n profiles: {},\n },\n };\n writeFileSync(filePath, JSON.stringify(defaultFile, null, 2) + \"\\n\");\n console.log(`Created ${filePath}`);\n return;\n }\n\n if (opts.describe) {\n try {\n const details = await fetchLinkedInJob(opts.describe, {\n format: opts.format,\n });\n console.log(JSON.stringify(details, null, 2));\n } catch (e: unknown) {\n console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);\n process.exit(1);\n }\n return;\n }\n\n if (opts.id) {\n const sites = opts.site;\n if (!sites || !sites.length) {\n console.error(\"Error: --id requires -s/--site (e.g. -s indeed --id abc123)\");\n process.exit(1);\n }\n const site = Array.isArray(sites) ? sites[0] : sites;\n try {\n const job = await fetchJobDetails(site, opts.id, {\n format: opts.format,\n proxies: opts.proxies,\n country: opts.country,\n });\n if (!job) {\n console.error(\"Job not found\");\n process.exit(1);\n }\n console.log(JSON.stringify(job, null, 2));\n } catch (e: unknown) {\n console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);\n process.exit(1);\n }\n return;\n }\n\n if (opts.listProfiles) {\n const { findJobspyPath, loadFile } = await import(\"../state\");\n const filePath = findJobspyPath();\n const file = loadFile(filePath);\n const configNames = Object.keys(file.config.profiles);\n const stateNames = Object.keys(file.state.profiles ?? {});\n const allNames = [...new Set([...configNames, ...stateNames])].sort();\n if (allNames.length === 0) {\n console.log(`No profiles in ${filePath}. Run --init to create one.`);\n } else {\n console.log(`Profiles in ${filePath}:`);\n for (const name of allNames) {\n const cfg = file.config.profiles[name];\n const st = file.state.profiles?.[name];\n const last = st?.lastRunAt ? new Date(st.lastRunAt).toLocaleString() : \"never\";\n const sites = cfg?.site\n ? (Array.isArray(cfg.site) ? cfg.site.join(\", \") : cfg.site)\n : \"all\";\n const term = cfg?.search_term ?? \"\";\n console.log(` ${name.padEnd(20)} last run: ${last} sites: ${sites} term: ${term}`);\n }\n }\n return;\n }\n\n // Load config profile defaults from jobspy.json (if profile specified)\n let cfg: Record<string, any> = {};\n if (opts.profile) {\n const { findJobspyPath, loadFile } = await import(\"../state\");\n const filePath = findJobspyPath();\n const file = loadFile(filePath);\n const profileConfig = file.config.profiles[opts.profile];\n if (profileConfig) {\n cfg = profileConfig;\n }\n }\n\n // Merge: CLI flags override config profile defaults\n const cliSet = program.opts();\n const o = {\n site: opts.site ?? cfg.site,\n searchTerm: opts.searchTerm ?? cfg.search_term ?? undefined,\n googleSearchTerm: opts.googleSearchTerm ?? cfg.google_search_term ?? undefined,\n location: opts.location ?? cfg.location ?? undefined,\n distance: cliSet.distance !== undefined ? opts.distance : String(cfg.distance ?? 50),\n remote: opts.remote ?? cfg.remote ?? false,\n jobType: opts.jobType ?? cfg.job_type ?? undefined,\n easyApply: opts.easyApply ?? cfg.easy_apply ?? false,\n results: cliSet.results !== undefined ? opts.results : String(cfg.results ?? 15),\n limit: opts.limit,\n country: cliSet.country !== undefined ? opts.country : (cfg.country ?? \"usa\"),\n proxies: opts.proxies ?? cfg.proxies ?? undefined,\n format: cliSet.format !== undefined ? opts.format : (cfg.format ?? \"markdown\"),\n linkedinFetchDescription: opts.linkedinFetchDescription ?? cfg.linkedin_fetch_description ?? false,\n linkedinCompanyIds: opts.linkedinCompanyIds ?? cfg.linkedin_company_ids ?? undefined,\n offset: cliSet.offset !== undefined ? opts.offset : String(cfg.offset ?? 0),\n hoursOld: opts.hoursOld ?? (cfg.hours_old != null ? String(cfg.hours_old) : undefined),\n enforceAnnualSalary: opts.enforceAnnualSalary ?? cfg.enforce_annual_salary ?? false,\n verbose: cliSet.verbose !== undefined ? opts.verbose : String(cfg.verbose ?? 0),\n output: opts.output ?? cfg.output ?? undefined,\n profile: opts.profile,\n all: opts.all ?? false,\n };\n\n if (o.all && !o.profile) {\n console.warn(\"Warning: --all has no effect without --profile\");\n }\n try {\n const result = await scrapeJobs({\n site_name: o.site,\n search_term: o.searchTerm,\n google_search_term: o.googleSearchTerm,\n location: o.location,\n distance: parseInt(o.distance),\n is_remote: o.remote,\n job_type: o.jobType,\n easy_apply: o.easyApply,\n results_wanted: parseInt(o.limit ?? o.results),\n country_indeed: o.country,\n proxies: o.proxies,\n description_format: o.format,\n linkedin_fetch_description: o.linkedinFetchDescription,\n linkedin_company_ids: o.linkedinCompanyIds?.map(Number),\n offset: parseInt(o.offset),\n hours_old: o.hoursOld ? parseInt(o.hoursOld) : undefined,\n enforce_annual_salary: o.enforceAnnualSalary,\n verbose: parseInt(o.verbose),\n profile: o.profile,\n skip_dedup: o.all,\n });\n\n console.log(`Found ${result.jobs.length} jobs`);\n if (result.profile) {\n const runLabel = result.profile.lastRunAt ? \"new since last run\" : \"first run\";\n console.log(` (${result.totalScraped} scraped, ${result.newCount} ${runLabel} — state: ${result.profile.stateFile})`);\n }\n\n if (o.output) {\n const outPath = o.output as string;\n if (outPath.endsWith(\".csv\")) {\n writeFileSync(outPath, jobsToCsv(result.jobs));\n console.log(`Results written to ${outPath}`);\n } else {\n writeFileSync(outPath, JSON.stringify(result.jobs, null, 2));\n console.log(`Results written to ${outPath}`);\n }\n } else {\n // Print summary table to stdout\n for (const job of result.jobs) {\n const line = [\n job.site?.padEnd(14),\n (job.title ?? \"\").slice(0, 40).padEnd(42),\n (job.company ?? \"\").slice(0, 20).padEnd(22),\n (job.location ?? \"\").slice(0, 25).padEnd(27),\n job.date_posted ?? \"\",\n ].join(\"\");\n console.log(line);\n }\n }\n } catch (e: unknown) {\n console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);\n process.exit(1);\n }\n });\n\nfunction jobsToCsv(jobs: any[]): string {\n if (jobs.length === 0) return \"\";\n const headers = Object.keys(jobs[0]);\n const escape = (val: any): string => {\n if (val == null) return \"\";\n const str = String(val);\n if (str.includes(\",\") || str.includes('\"') || str.includes(\"\\n\")) {\n return `\"${str.replace(/\"/g, '\"\"')}\"`;\n }\n return str;\n };\n const lines = [headers.join(\",\")];\n for (const job of jobs) {\n lines.push(headers.map((h) => escape(job[h])).join(\",\"));\n }\n return lines.join(\"\\n\");\n}\n\nprogram.parse();\n"],"names":["Command","resolve","existsSync","writeFileSync","fetchLinkedInJob","fetchJobDetails","cfg","scrapeJobs"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../../src/cli/index.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { scrapeJobs, fetchLinkedInJob, fetchJobDetails } from \"../scraper\";\nimport { writeFileSync, existsSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\n\nconst program = new Command();\n\nprogram\n .name(\"jobspy\")\n .description(\n \"Job scraper for LinkedIn, Indeed, Glassdoor, Google, ZipRecruiter, Bayt, Naukri & BDJobs\",\n )\n .version(\"1.0.0\")\n .option(\n \"-s, --site <sites...>\",\n \"Job boards to scrape (linkedin, indeed, zip_recruiter, glassdoor, google, bayt, naukri, bdjobs)\",\n )\n .option(\"-q, --search-term <term>\", \"Search term\")\n .option(\"--google-search-term <term>\", \"Google-specific search term\")\n .option(\"-l, --location <location>\", \"Job location\")\n .option(\"-d, --distance <miles>\", \"Distance in miles\", \"50\")\n .option(\"-r, --remote\", \"Filter for remote jobs\")\n .option(\n \"-t, --job-type <type>\",\n \"Job type (fulltime, parttime, contract, internship)\",\n )\n .option(\"--easy-apply\", \"Filter for easy apply jobs\")\n .option(\"-n, --results <count>\", \"Number of results wanted (alias: --limit)\", \"15\")\n .option(\"--limit <count>\", \"Alias for --results\")\n .option(\n \"-c, --country <country>\",\n \"Country for Indeed/Glassdoor\",\n \"usa\",\n )\n .option(\n \"-p, --proxies <proxies...>\",\n \"Proxy servers (user:pass@host:port)\",\n )\n .option(\n \"--format <format>\",\n \"Description format (markdown, html, plain)\",\n \"markdown\",\n )\n .option(\"--linkedin-fetch-description\", \"Fetch full LinkedIn descriptions\")\n .option(\"--indeed-fetch-description\", \"Fetch full Indeed descriptions\")\n .option(\n \"--linkedin-company-ids <ids...>\",\n \"LinkedIn company IDs to filter\",\n )\n .option(\"--offset <offset>\", \"Start from offset\", \"0\")\n .option(\n \"--hours-old <hours>\",\n \"Filter jobs posted within N hours\",\n )\n .option(\"--enforce-annual-salary\", \"Convert all salaries to annual\")\n .option(\"-v, --verbose <level>\", \"Verbosity (0=errors, 1=warnings, 2=all)\", \"0\")\n .option(\"-o, --output <file>\", \"Output file path (JSON or CSV based on extension)\")\n .option(\"--profile <name>\", \"Named search profile from jobspy.json\")\n .option(\"--all\", \"Skip dedup for this run (still updates state)\")\n .option(\"--list-profiles\", \"List saved profiles and their last run time\")\n .option(\"--init\", \"Generate a jobspy.json with sample profiles\")\n .option(\"--describe <jobId>\", \"Fetch full LinkedIn job details by ID or URL\")\n .option(\"--id <jobId>\", \"Fetch full job details by ID (requires -s/--site)\")\n // ── Credentials ──────────────────────────────────────────────────────────\n .option(\"--creds\", \"Use stored/env credentials as fallback when anonymous scraping is blocked (also: JOBSPY_CREDS=1)\")\n .option(\"--linkedin-username <user>\", \"LinkedIn username/email (also: LINKEDIN_USERNAME)\")\n .option(\"--linkedin-password <pass>\", \"LinkedIn password (also: LINKEDIN_PASSWORD)\")\n .option(\"--indeed-username <user>\", \"Indeed username/email (also: INDEED_USERNAME)\")\n .option(\"--indeed-password <pass>\", \"Indeed password (also: INDEED_PASSWORD)\")\n .option(\"--glassdoor-username <user>\", \"Glassdoor username/email (also: GLASSDOOR_USERNAME)\")\n .option(\"--glassdoor-password <pass>\", \"Glassdoor password (also: GLASSDOOR_PASSWORD)\")\n .option(\"--ziprecruiter-username <user>\", \"ZipRecruiter username/email (also: ZIPRECRUITER_USERNAME)\")\n .option(\"--ziprecruiter-password <pass>\", \"ZipRecruiter password (also: ZIPRECRUITER_PASSWORD)\")\n .option(\"--bayt-username <user>\", \"Bayt username/email (also: BAYT_USERNAME)\")\n .option(\"--bayt-password <pass>\", \"Bayt password (also: BAYT_PASSWORD)\")\n .option(\"--naukri-username <user>\", \"Naukri username/email (also: NAUKRI_USERNAME)\")\n .option(\"--naukri-password <pass>\", \"Naukri password (also: NAUKRI_PASSWORD)\")\n .action(async (opts) => {\n if (opts.init) {\n const filePath = resolve(process.cwd(), \"jobspy.json\");\n if (existsSync(filePath)) {\n console.error(`File already exists: ${filePath}`);\n process.exit(1);\n }\n const defaultFile = {\n config: {\n profiles: {\n frontend: {\n site: [\"linkedin\", \"indeed\", \"zip_recruiter\", \"glassdoor\", \"google\", \"bayt\", \"naukri\", \"bdjobs\"],\n search_term: \"react frontend developer\",\n google_search_term: \"react frontend developer jobs near New York NY\",\n location: \"New York, NY\",\n distance: 25,\n remote: false,\n job_type: \"fulltime\",\n easy_apply: false,\n results: 50,\n country: \"usa\",\n proxies: [],\n format: \"markdown\",\n linkedin_fetch_description: true,\n linkedin_company_ids: [],\n offset: 0,\n hours_old: 72,\n enforce_annual_salary: true,\n verbose: 1,\n output: \"frontend-jobs.csv\",\n // Credentials (optional – enable with creds:true or env JOBSPY_CREDS=1)\n // creds: false,\n // linkedin_username: \"\",\n // linkedin_password: \"\",\n // indeed_username: \"\",\n // indeed_password: \"\",\n },\n backend: {\n site: [\"linkedin\", \"indeed\", \"zip_recruiter\", \"glassdoor\", \"google\", \"bayt\", \"naukri\", \"bdjobs\"],\n search_term: \"node.js backend engineer\",\n google_search_term: \"node.js backend engineer jobs near New York NY\",\n location: \"New York, NY\",\n distance: 25,\n remote: true,\n job_type: \"fulltime\",\n easy_apply: false,\n results: 50,\n country: \"usa\",\n proxies: [],\n format: \"markdown\",\n linkedin_fetch_description: true,\n linkedin_company_ids: [],\n offset: 0,\n hours_old: 48,\n enforce_annual_salary: true,\n verbose: 1,\n output: \"backend-jobs.json\",\n // Credentials (optional – enable with creds:true or env JOBSPY_CREDS=1)\n // creds: false,\n // linkedin_username: \"\",\n // linkedin_password: \"\",\n // indeed_username: \"\",\n // indeed_password: \"\",\n },\n },\n },\n state: {\n version: 1,\n profiles: {},\n },\n };\n writeFileSync(filePath, JSON.stringify(defaultFile, null, 2) + \"\\n\");\n console.log(`Created ${filePath}`);\n return;\n }\n\n if (opts.describe) {\n try {\n const details = await fetchLinkedInJob(opts.describe, {\n format: opts.format,\n });\n console.log(JSON.stringify(details, null, 2));\n } catch (e: unknown) {\n console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);\n process.exit(1);\n }\n return;\n }\n\n if (opts.id) {\n const sites = opts.site;\n if (!sites || !sites.length) {\n console.error(\"Error: --id requires -s/--site (e.g. -s indeed --id abc123)\");\n process.exit(1);\n }\n const site = Array.isArray(sites) ? sites[0] : sites;\n try {\n const job = await fetchJobDetails(site, opts.id, {\n format: opts.format,\n proxies: opts.proxies,\n country: opts.country,\n });\n if (!job) {\n console.error(\"Job not found\");\n process.exit(1);\n }\n console.log(JSON.stringify(job, null, 2));\n } catch (e: unknown) {\n console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);\n process.exit(1);\n }\n return;\n }\n\n if (opts.listProfiles) {\n const { findJobspyPath, loadFile } = await import(\"../state\");\n const filePath = findJobspyPath();\n const file = loadFile(filePath);\n const configNames = Object.keys(file.config.profiles);\n const stateNames = Object.keys(file.state.profiles ?? {});\n const allNames = [...new Set([...configNames, ...stateNames])].sort();\n if (allNames.length === 0) {\n console.log(`No profiles in ${filePath}. Run --init to create one.`);\n } else {\n console.log(`Profiles in ${filePath}:`);\n for (const name of allNames) {\n const cfg = file.config.profiles[name];\n const st = file.state.profiles?.[name];\n const last = st?.lastRunAt ? new Date(st.lastRunAt).toLocaleString() : \"never\";\n const sites = cfg?.site\n ? (Array.isArray(cfg.site) ? cfg.site.join(\", \") : cfg.site)\n : \"all\";\n const term = cfg?.search_term ?? \"\";\n console.log(` ${name.padEnd(20)} last run: ${last} sites: ${sites} term: ${term}`);\n }\n }\n return;\n }\n\n // Load config profile defaults from jobspy.json (if profile specified)\n let cfg: Record<string, any> = {};\n if (opts.profile) {\n const { findJobspyPath, loadFile } = await import(\"../state\");\n const filePath = findJobspyPath();\n const file = loadFile(filePath);\n const profileConfig = file.config.profiles[opts.profile];\n if (profileConfig) {\n cfg = profileConfig;\n }\n }\n\n // Merge: CLI flags override config profile defaults\n const cliSet = program.opts();\n const o = {\n site: opts.site ?? cfg.site,\n searchTerm: opts.searchTerm ?? cfg.search_term ?? undefined,\n googleSearchTerm: opts.googleSearchTerm ?? cfg.google_search_term ?? undefined,\n location: opts.location ?? cfg.location ?? undefined,\n distance: cliSet.distance !== undefined ? opts.distance : String(cfg.distance ?? 50),\n remote: opts.remote ?? cfg.remote ?? false,\n jobType: opts.jobType ?? cfg.job_type ?? undefined,\n easyApply: opts.easyApply ?? cfg.easy_apply ?? false,\n results: cliSet.results !== undefined ? opts.results : String(cfg.results ?? 15),\n limit: opts.limit,\n country: cliSet.country !== undefined ? opts.country : (cfg.country ?? \"usa\"),\n proxies: opts.proxies ?? cfg.proxies ?? undefined,\n format: cliSet.format !== undefined ? opts.format : (cfg.format ?? \"markdown\"),\n linkedinFetchDescription: opts.linkedinFetchDescription ?? cfg.linkedin_fetch_description ?? false,\n linkedinCompanyIds: opts.linkedinCompanyIds ?? cfg.linkedin_company_ids ?? undefined,\n offset: cliSet.offset !== undefined ? opts.offset : String(cfg.offset ?? 0),\n hoursOld: opts.hoursOld ?? (cfg.hours_old != null ? String(cfg.hours_old) : undefined),\n enforceAnnualSalary: opts.enforceAnnualSalary ?? cfg.enforce_annual_salary ?? false,\n verbose: cliSet.verbose !== undefined ? opts.verbose : String(cfg.verbose ?? 0),\n output: opts.output ?? cfg.output ?? undefined,\n profile: opts.profile,\n all: opts.all ?? false,\n // Credentials\n creds: opts.creds ?? cfg.creds ?? false,\n linkedinUsername: opts.linkedinUsername ?? cfg.linkedin_username ?? undefined,\n linkedinPassword: opts.linkedinPassword ?? cfg.linkedin_password ?? undefined,\n indeedUsername: opts.indeedUsername ?? cfg.indeed_username ?? undefined,\n indeedPassword: opts.indeedPassword ?? cfg.indeed_password ?? undefined,\n glassdoorUsername: opts.glassdoorUsername ?? cfg.glassdoor_username ?? undefined,\n glassdoorPassword: opts.glassdoorPassword ?? cfg.glassdoor_password ?? undefined,\n ziprecruiterUsername: opts.ziprecruiterUsername ?? cfg.ziprecruiter_username ?? undefined,\n ziprecruiterPassword: opts.ziprecruiterPassword ?? cfg.ziprecruiter_password ?? undefined,\n baytUsername: opts.baytUsername ?? cfg.bayt_username ?? undefined,\n baytPassword: opts.baytPassword ?? cfg.bayt_password ?? undefined,\n naukriUsername: opts.naukriUsername ?? cfg.naukri_username ?? undefined,\n naukriPassword: opts.naukriPassword ?? cfg.naukri_password ?? undefined,\n };\n\n if (o.all && !o.profile) {\n console.warn(\"Warning: --all has no effect without --profile\");\n }\n try {\n const result = await scrapeJobs({\n site_name: o.site,\n search_term: o.searchTerm,\n google_search_term: o.googleSearchTerm,\n location: o.location,\n distance: parseInt(o.distance),\n is_remote: o.remote,\n job_type: o.jobType,\n easy_apply: o.easyApply,\n results_wanted: parseInt(o.limit ?? o.results),\n country_indeed: o.country,\n proxies: o.proxies,\n description_format: o.format,\n linkedin_fetch_description: o.linkedinFetchDescription,\n linkedin_company_ids: o.linkedinCompanyIds?.map(Number),\n offset: parseInt(o.offset),\n hours_old: o.hoursOld ? parseInt(o.hoursOld) : undefined,\n enforce_annual_salary: o.enforceAnnualSalary,\n verbose: parseInt(o.verbose),\n profile: o.profile,\n skip_dedup: o.all,\n // Credentials\n use_creds: o.creds || undefined,\n linkedin_username: o.linkedinUsername,\n linkedin_password: o.linkedinPassword,\n indeed_username: o.indeedUsername,\n indeed_password: o.indeedPassword,\n glassdoor_username: o.glassdoorUsername,\n glassdoor_password: o.glassdoorPassword,\n ziprecruiter_username: o.ziprecruiterUsername,\n ziprecruiter_password: o.ziprecruiterPassword,\n bayt_username: o.baytUsername,\n bayt_password: o.baytPassword,\n naukri_username: o.naukriUsername,\n naukri_password: o.naukriPassword,\n });\n\n console.log(`Found ${result.jobs.length} jobs`);\n if (result.profile) {\n const runLabel = result.profile.lastRunAt ? \"new since last run\" : \"first run\";\n console.log(` (${result.totalScraped} scraped, ${result.newCount} ${runLabel} — state: ${result.profile.stateFile})`);\n }\n\n if (o.output) {\n const outPath = o.output as string;\n if (outPath.endsWith(\".csv\")) {\n writeFileSync(outPath, jobsToCsv(result.jobs));\n console.log(`Results written to ${outPath}`);\n } else {\n writeFileSync(outPath, JSON.stringify(result.jobs, null, 2));\n console.log(`Results written to ${outPath}`);\n }\n } else {\n // Print summary table to stdout\n for (const job of result.jobs) {\n const line = [\n job.site?.padEnd(14),\n (job.title ?? \"\").slice(0, 40).padEnd(42),\n (job.company ?? \"\").slice(0, 20).padEnd(22),\n (job.location ?? \"\").slice(0, 25).padEnd(27),\n job.date_posted ?? \"\",\n ].join(\"\");\n console.log(line);\n }\n }\n } catch (e: unknown) {\n console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);\n process.exit(1);\n }\n });\n\nfunction jobsToCsv(jobs: any[]): string {\n if (jobs.length === 0) return \"\";\n const headers = Object.keys(jobs[0]);\n const escape = (val: any): string => {\n if (val == null) return \"\";\n const str = String(val);\n if (str.includes(\",\") || str.includes('\"') || str.includes(\"\\n\")) {\n return `\"${str.replace(/\"/g, '\"\"')}\"`;\n }\n return str;\n };\n const lines = [headers.join(\",\")];\n for (const job of jobs) {\n lines.push(headers.map((h) => escape(job[h])).join(\",\"));\n }\n return lines.join(\"\\n\");\n}\n\nprogram.parse();\n"],"names":["Command","resolve","existsSync","writeFileSync","fetchLinkedInJob","fetchJobDetails","cfg","scrapeJobs"],"mappings":"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"}
|