job-applier 0.2.0__tar.gz

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.
Files changed (82) hide show
  1. job_applier-0.2.0/LICENSE +25 -0
  2. job_applier-0.2.0/PKG-INFO +391 -0
  3. job_applier-0.2.0/README.md +353 -0
  4. job_applier-0.2.0/job_applier/__init__.py +3 -0
  5. job_applier-0.2.0/job_applier/actions.py +171 -0
  6. job_applier-0.2.0/job_applier/actiontools/__init__.py +24 -0
  7. job_applier-0.2.0/job_applier/actiontools/base.py +105 -0
  8. job_applier-0.2.0/job_applier/actiontools/playwright.py +179 -0
  9. job_applier-0.2.0/job_applier/actiontools/pydoll.py +297 -0
  10. job_applier-0.2.0/job_applier/apply_flow.py +476 -0
  11. job_applier-0.2.0/job_applier/apply_variations.py +360 -0
  12. job_applier-0.2.0/job_applier/batch.py +112 -0
  13. job_applier-0.2.0/job_applier/budget.py +79 -0
  14. job_applier-0.2.0/job_applier/chrome.py +207 -0
  15. job_applier-0.2.0/job_applier/cli.py +191 -0
  16. job_applier-0.2.0/job_applier/config.py +357 -0
  17. job_applier-0.2.0/job_applier/credentials.py +113 -0
  18. job_applier-0.2.0/job_applier/detector/__init__.py +11 -0
  19. job_applier-0.2.0/job_applier/detector/application.py +100 -0
  20. job_applier-0.2.0/job_applier/detector/bot_challenge.py +30 -0
  21. job_applier-0.2.0/job_applier/detector/captcha.py +58 -0
  22. job_applier-0.2.0/job_applier/detector/classify.py +90 -0
  23. job_applier-0.2.0/job_applier/detector/closed_job.py +11 -0
  24. job_applier-0.2.0/job_applier/detector/config.py +64 -0
  25. job_applier-0.2.0/job_applier/detector/constants.py +155 -0
  26. job_applier-0.2.0/job_applier/detector/errors.py +35 -0
  27. job_applier-0.2.0/job_applier/detector/login.py +29 -0
  28. job_applier-0.2.0/job_applier/detector/signals.py +52 -0
  29. job_applier-0.2.0/job_applier/doctor.py +218 -0
  30. job_applier-0.2.0/job_applier/errors.py +45 -0
  31. job_applier-0.2.0/job_applier/fileio.py +95 -0
  32. job_applier-0.2.0/job_applier/form_fields.py +224 -0
  33. job_applier-0.2.0/job_applier/form_filler.py +278 -0
  34. job_applier-0.2.0/job_applier/hr_company_applyflows/__init__.py +80 -0
  35. job_applier-0.2.0/job_applier/hr_company_applyflows/amazon.py +630 -0
  36. job_applier-0.2.0/job_applier/hr_company_applyflows/applytojobs.py +206 -0
  37. job_applier-0.2.0/job_applier/hr_company_applyflows/ashbyhq.py +534 -0
  38. job_applier-0.2.0/job_applier/hr_company_applyflows/bamboohr.py +350 -0
  39. job_applier-0.2.0/job_applier/hr_company_applyflows/generic.py +143 -0
  40. job_applier-0.2.0/job_applier/hr_company_applyflows/greenhouse.py +215 -0
  41. job_applier-0.2.0/job_applier/hr_company_applyflows/henryscheinone.py +176 -0
  42. job_applier-0.2.0/job_applier/hr_company_applyflows/jobvite.py +459 -0
  43. job_applier-0.2.0/job_applier/hr_company_applyflows/ripplingats.py +265 -0
  44. job_applier-0.2.0/job_applier/hr_company_applyflows/workday.py +800 -0
  45. job_applier-0.2.0/job_applier/init_cmd.py +180 -0
  46. job_applier-0.2.0/job_applier/ledger.py +81 -0
  47. job_applier-0.2.0/job_applier/llm.py +177 -0
  48. job_applier-0.2.0/job_applier/logger.py +118 -0
  49. job_applier-0.2.0/job_applier/logging_setup.py +85 -0
  50. job_applier-0.2.0/job_applier/paths.py +105 -0
  51. job_applier-0.2.0/job_applier/preferred_answers.py +92 -0
  52. job_applier-0.2.0/job_applier/qa_cache.py +55 -0
  53. job_applier-0.2.0/job_applier/risk.py +89 -0
  54. job_applier-0.2.0/job_applier/submit_gate.py +155 -0
  55. job_applier-0.2.0/job_applier/templates/human_prompt.md.example +8 -0
  56. job_applier-0.2.0/job_applier/templates/preferred_answers/amazon.md.example +144 -0
  57. job_applier-0.2.0/job_applier/templates/preferred_answers/general.md.example +33 -0
  58. job_applier-0.2.0/job_applier/templates/preferred_answers/henryscheinone.md.example +39 -0
  59. job_applier-0.2.0/job_applier/templates/preferred_answers/workday.md.example +58 -0
  60. job_applier-0.2.0/job_applier/templates/resume.md.example +18 -0
  61. job_applier-0.2.0/job_applier.egg-info/PKG-INFO +391 -0
  62. job_applier-0.2.0/job_applier.egg-info/SOURCES.txt +80 -0
  63. job_applier-0.2.0/job_applier.egg-info/dependency_links.txt +1 -0
  64. job_applier-0.2.0/job_applier.egg-info/entry_points.txt +2 -0
  65. job_applier-0.2.0/job_applier.egg-info/requires.txt +17 -0
  66. job_applier-0.2.0/job_applier.egg-info/top_level.txt +1 -0
  67. job_applier-0.2.0/pyproject.toml +80 -0
  68. job_applier-0.2.0/setup.cfg +4 -0
  69. job_applier-0.2.0/tests/test_actions.py +82 -0
  70. job_applier-0.2.0/tests/test_apply_flow.py +398 -0
  71. job_applier-0.2.0/tests/test_attach.py +226 -0
  72. job_applier-0.2.0/tests/test_commands.py +409 -0
  73. job_applier-0.2.0/tests/test_config.py +50 -0
  74. job_applier-0.2.0/tests/test_e2e_attach.py +112 -0
  75. job_applier-0.2.0/tests/test_form_fields.py +159 -0
  76. job_applier-0.2.0/tests/test_form_filler.py +282 -0
  77. job_applier-0.2.0/tests/test_llm.py +160 -0
  78. job_applier-0.2.0/tests/test_logger.py +32 -0
  79. job_applier-0.2.0/tests/test_preferred_answers.py +94 -0
  80. job_applier-0.2.0/tests/test_qa_cache.py +19 -0
  81. job_applier-0.2.0/tests/test_safety.py +422 -0
  82. job_applier-0.2.0/tests/test_settings_resolution.py +121 -0
@@ -0,0 +1,25 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Umer Khalid
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ Job Applier submits real applications on your behalf. You are responsible for
24
+ the accuracy of everything it submits and for complying with the terms of the
25
+ websites you use.
@@ -0,0 +1,391 @@
1
+ Metadata-Version: 2.4
2
+ Name: job-applier
3
+ Version: 0.2.0
4
+ Summary: Autonomously navigate to a job posting, fill out the application form using an LLM and your resume, and submit it -- in your own Chrome, with dry-run and review safeguards.
5
+ Author: Umer Khalid
6
+ License-Expression: MIT
7
+ Keywords: job,application,automation,playwright,llm,cli
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: End Users/Desktop
11
+ Classifier: Operating System :: Microsoft :: Windows
12
+ Classifier: Operating System :: MacOS
13
+ Classifier: Operating System :: POSIX :: Linux
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Utilities
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: playwright<2,>=1.40.0
25
+ Requires-Dist: beautifulsoup4<5,>=4.12.0
26
+ Requires-Dist: anthropic<1,>=0.40.0
27
+ Requires-Dist: openai<4,>=1.40.0
28
+ Requires-Dist: platformdirs<5,>=3.0
29
+ Requires-Dist: keyring<26,>=24.0
30
+ Requires-Dist: tomli>=2.0; python_version < "3.11"
31
+ Provides-Extra: pydoll
32
+ Requires-Dist: pydoll-python<3,>=2.0; extra == "pydoll"
33
+ Provides-Extra: dev
34
+ Requires-Dist: pytest>=8.0; extra == "dev"
35
+ Requires-Dist: ruff>=0.5; extra == "dev"
36
+ Requires-Dist: mypy>=1.10; extra == "dev"
37
+ Dynamic: license-file
38
+
39
+ # JobApplier
40
+
41
+ Give it a job posting URL. It opens the page in **your own Chrome** (a dedicated profile you log into
42
+ once), finds the application form, and fills it out field-by-field with an LLM grounded in your resume —
43
+ then, only if you allow it, submits and confirms success. Every attempt is logged with a full Q&A trail
44
+ and, on any snag, a screenshot.
45
+
46
+ > **You are responsible.** JobApplier submits real applications on your behalf. You are responsible for
47
+ > the accuracy of every answer it submits and for complying with the terms of the websites you use. It does
48
+ > not guarantee compliance with any third party's terms of service. Start with `--dry-run`.
49
+
50
+ ## Quick start
51
+
52
+ ```
53
+ pipx install job-applier # or: uv tool install job-applier
54
+
55
+ job-applier init # creates your config directory, stores your API key, imports your resume
56
+ job-applier chrome # starts a dedicated Chrome; log in to your job sites once
57
+ job-applier doctor # checks everything is ready
58
+ job-applier "https://company.example.com/careers/apply/123" --dry-run # fill, validate, don't submit
59
+ job-applier "https://company.example.com/careers/apply/123" # the real thing
60
+ ```
61
+
62
+ No browser is downloaded or bundled: attach mode uses the Chrome/Edge you already have.
63
+ (`playwright install chromium` is only needed if you use the old launch mode without `--cdp-url`.)
64
+
65
+ Requires Python 3.10+. Works on Windows, macOS and Linux.
66
+
67
+ The last line printed is the outcome:
68
+
69
+ ```
70
+ status: submitted
71
+ log written to: <your config dir>/results/2026-09-18_regular_job_applications.json
72
+ ```
73
+
74
+ ## Where your files live
75
+
76
+ Nothing application-owned is kept in a repository or the current directory. Everything is under one
77
+ per-user directory (`job-applier doctor` prints it; override with `JOBAPPLIER_HOME`):
78
+
79
+ | OS | Directory |
80
+ | --- | --- |
81
+ | Linux | `~/.config/job-applier` |
82
+ | macOS | `~/Library/Application Support/job-applier` |
83
+ | Windows | `%APPDATA%\job-applier` |
84
+
85
+ ```
86
+ config.toml settings (CLI flag > environment variable > config.toml > default)
87
+ .env API key fallback, only if no OS keyring is available (owner-only permissions)
88
+ resume.md / resume.pdf your resume: text the LLM reads / the PDF uploaded to resume fields
89
+ human_prompt.md persona / instructions wrapper for the LLM
90
+ preferred_answers/ fixed answers that override the resume (general.md, amazon.md, ...)
91
+ results/ <date>_regular_job_applications.json — the Q&A trail and outcome of every run
92
+ screenshots/ taken on any failed action
93
+ logs/ one structured JSON-lines log per run (no answers, no keys)
94
+ cache/qa_cache.json answer cache across runs
95
+ state/ duplicate-ledger and batch-queue state
96
+ chrome-profile/ the dedicated browser profile (see below)
97
+ ```
98
+
99
+ **Upgrading from the old checkout layout?** If the directory above doesn't exist yet and `./data/resume.md`
100
+ does, the old `./data/...` paths keep working. `job-applier init` offers to copy them into the new location.
101
+
102
+ ## The dedicated Chrome profile
103
+
104
+ `job-applier chrome` starts Chrome (or Edge) with:
105
+
106
+ ```
107
+ --remote-debugging-port=9222 --remote-debugging-address=127.0.0.1 --user-data-dir=<config dir>/chrome-profile
108
+ ```
109
+
110
+ - **It is a separate browser profile**, not your everyday one. JobApplier never reads, copies or touches your
111
+ normal Chrome profile, and refuses to use it.
112
+ - **Why a separate profile:** since Chrome 136, `--remote-debugging-port` is ignored on the default profile
113
+ (so nothing can attach to your real profile). A dedicated `--user-data-dir` is the supported route.
114
+ - **Login persistence:** log in to LinkedIn, Greenhouse, Lever, Workday, … in that window once; the sessions
115
+ persist in the profile between runs.
116
+ - **Attach mode** (`--cdp-url http://127.0.0.1:9222`, on by default after `job-applier init`): every run opens
117
+ a **new tab**, works only inside it, and closes **only that tab** — also on failure and Ctrl+C. It never
118
+ uses your existing tabs, never resizes the window, and never closes the browser.
119
+
120
+ ### CDP security
121
+
122
+ The DevTools port gives **full control of the browser, including your logged-in sessions**. Therefore:
123
+ it is bound to `127.0.0.1` only; JobApplier refuses any non-loopback `--cdp-url`; and `job-applier doctor`
124
+ fails if the port answers on a non-loopback address. Never expose port 9222 (no port forwarding, no
125
+ `0.0.0.0`, no tunnels).
126
+
127
+ ### Troubleshooting port 9222
128
+
129
+ | Symptom | Fix |
130
+ | --- | --- |
131
+ | `could not attach to a browser at http://127.0.0.1:9222` | Run `job-applier chrome`, then `job-applier doctor` |
132
+ | Chrome opened but nothing answers on 9222 | Another Chrome is already using the same profile — close it and re-run |
133
+ | Port already in use by something else | `job-applier chrome --port 9333`, then use `--cdp-url http://127.0.0.1:9333` (or set `chrome_port`/`cdp_url` in `config.toml`) |
134
+ | `answers, but does not look like a Chrome/Chromium DevTools server` | Another program owns the port; pick another port |
135
+ | Chrome/Edge not found | Set `JOBAPPLIER_CHROME_PATH` to the executable |
136
+
137
+ ## Safety features
138
+
139
+ | Feature | What it does |
140
+ | --- | --- |
141
+ | `--dry-run` | Opens the application, fills and validates it, writes the normal result — and **never submits** (status `dry_run`). |
142
+ | `--review` | Fills everything, then **pauses before the final Submit**, lists any sensitive answers, and asks you to type `submit`. Anything else cancels (status `review_declined`). Needs an interactive terminal. |
143
+ | Sensitive fields | One classification for every HR system: **SENSITIVE** (salary, sponsorship/work authorization, EEO/demographics, veteran, disability, criminal history…) and **ATTESTATION** (legal certifications/consents). Those without a configured [preferred answer](#preferred-answers-override-the-resume) are flagged and the application is **not submitted without `--review` confirmation** (status `review_required`, exit code 4). Set `sensitive_policy = "auto"` (or `--sensitive-policy auto`) to trust the LLM instead. |
144
+ | Duplicate protection | URLs already submitted are skipped (`skipped_duplicate`). URL variations are normalized (tracking parameters, fragment, trailing slash) but job-id parameters are kept. An unconfirmed Submit click also blocks a repeat until you check the site and pass `--force`. |
145
+ | No blind retries | Only transient browser/network failures are retried (with backoff, `--retries`), and **never after a Submit click**. Config errors, CAPTCHA/login walls, validation failures, cancellation and budget stops are never retried. |
146
+ | Cost limits | `--max-api-calls N` and `--max-spend USD` are hard limits per run. Spend needs `price_input_per_mtok` / `price_output_per_mtok` in `config.toml` (JobApplier ships no price table) unless the provider reports cost (OpenRouter). |
147
+ | Pacing | Non-zero defaults (startup buffer 2 s, 0.3–1.2 s between actions). Override with `--startup-buffer-ms`, `--step-delay-min-ms`, `--step-delay-max-ms` (0 disables). |
148
+
149
+ `--wait-for-submit` (you click Submit yourself, for invisible-reCAPTCHA sites) still works and is honored by
150
+ the ApplyToJobs flow; `--dry-run` takes precedence over it.
151
+
152
+ ## Commands
153
+
154
+ ```
155
+ job-applier init scaffold the config directory (--yes for non-interactive)
156
+ job-applier chrome start the dedicated Chrome (--port, --browser chrome|edge)
157
+ job-applier doctor verify config, Chrome/CDP, API key, resume, preferred answers (--offline)
158
+ job-applier run jobs.txt apply to many URLs, resumable
159
+ job-applier "<url>" [flags] apply to one URL
160
+ job-applier --version
161
+ ```
162
+
163
+ ### Batch mode
164
+
165
+ `jobs.txt` — one URL per line; optionally `URL | Company Name | HR System`; `#` comments allowed:
166
+
167
+ ```
168
+ https://boards.greenhouse.io/acme/jobs/123
169
+ https://acme.wd1.myworkdayjobs.com/en-US/careers/job/X | Acme Inc | Workday
170
+ ```
171
+
172
+ Progress is saved after every application. After a crash or Ctrl+C, run the same command again: finished
173
+ URLs are not redone; anything else is retried (the duplicate ledger still stops a second submission if a
174
+ Submit click had gone out). A `--dry-run` batch keeps separate state from a real one.
175
+
176
+ ## Exit codes
177
+
178
+ | Code | Meaning |
179
+ | --- | --- |
180
+ | 0 | Success: `submitted`, `skipped_duplicate`, or a `dry_run` that completed |
181
+ | 1 | Application failed: `blocked`, `bot_detected` (CAPTCHA/login wall), validation/submit failure |
182
+ | 2 | Configuration error (missing API key/resume, bad flag, bad config file, non-loopback `--cdp-url`) |
183
+ | 3 | Cancelled: Ctrl+C (`interrupted`) or declined at the `--review` prompt |
184
+ | 4 | Review required: sensitive answers with no preferred answer; nothing was submitted |
185
+ | 5 | Runtime/browser error (Chrome unreachable, crash, timeouts) |
186
+
187
+ `job-applier run` returns the most severe code among its applications (3 > 2 > 5 > 4 > 1 > 0).
188
+
189
+ ## CAPTCHAs and login walls
190
+
191
+ JobApplier never solves or bypasses a CAPTCHA. On a challenge or login wall it screenshots and stops with
192
+ `bot_detected` (exit 1). With `--wait-for-captcha` a CAPTCHA instead pauses for you to solve it in the
193
+ browser window, then the flow continues. Because the dedicated profile keeps your logins, most login walls
194
+ disappear after you sign in once.
195
+
196
+ ## Step by step: what happens
197
+
198
+ | # | Step | Detail |
199
+ | - | --- | --- |
200
+ | 1 | Navigate | Opens the URL in a **new tab** of the attached Chrome (or a launched browser) |
201
+ | 2 | Duplicate check | Skips URLs already submitted |
202
+ | 3 | Bot-check | Screenshots and stops if a captcha/login wall/challenge is detected |
203
+ | 4 | Find the form | Uses it if present, otherwise clicks an "Apply" button/link to open it |
204
+ | 5 | Fill fields | Asks the LLM (resume.md + human_prompt.md + preferred answers) for each field's answer and applies it, uploading resume.pdf for resume fields; leaves alone what is already set |
205
+ | 6 | Gate | Sensitive answers are flagged; `--dry-run` / `--review` / policy decide whether the final Submit is clicked |
206
+ | 7 | Advance | Clicks Next/Submit, repeats for each page |
207
+ | 8 | Confirm | Verifies a success message ("thank you" / "application received" / …) |
208
+ | 9 | Log | Writes the full record (Q&A, screenshots, status, timings, versions) throughout |
209
+
210
+ ## Preferred answers (override the resume)
211
+
212
+ The LLM answers from your resume by default. For questions where you want a **fixed answer** — relocation,
213
+ sponsorship, "how did you hear about this role", EEO self-identification — write the answer down once.
214
+ Preferred answers live in Markdown files in `<config dir>/preferred_answers/` (`job-applier init` copies
215
+ templates there):
216
+
217
+ | File | Used for |
218
+ | --- | --- |
219
+ | `general.md` | Every application |
220
+ | `amazon.md`, `henryscheinone.md`, `workday.md` … | One company — file named after the company, lowercase, letters and digits only |
221
+
222
+ Each entry is a question and an answer; **leave an answer empty to let the LLM use your resume**:
223
+
224
+ ```
225
+ - Q: Are you willing to relocate?
226
+ A: Yes
227
+ - Q: Salary expectations
228
+ A:
229
+ ```
230
+
231
+ Precedence: **company file > general file > resume**. To point somewhere else, set
232
+ `JOBAPPLIER_PREFERRED_ANSWERS_GENERAL` / `JOBAPPLIER_PREFERRED_ANSWERS_<COMPANY>` (company name upper-cased,
233
+ letters and digits only: `Henry Schein One` → `..._HENRYSCHEINONE`) or `preferred_answers_dir` in
234
+ `config.toml`. Sensitive fields (see above) with an answered entry are automated; those without one wait for
235
+ your `--review`. Changing a preferred-answers file automatically ignores older cached answers.
236
+
237
+ ## Configuration
238
+
239
+ Precedence, resolved in one place (`job_applier/config.py`): **CLI flag > environment variable > `config.toml`
240
+ > default.** `config.toml` keys are the setting names; each has a `JOBAPPLIER_<NAME>` environment variable
241
+ (see `.env.example`).
242
+
243
+ ```toml
244
+ cdp_url = "http://127.0.0.1:9222"
245
+ max_api_calls = 40
246
+ max_spend_usd = 0.50
247
+ price_input_per_mtok = 3.0 # your model's price, USD per million tokens
248
+ price_output_per_mtok = 15.0
249
+ step_delay_min_ms = 300
250
+ step_delay_max_ms = 1200
251
+ ```
252
+
253
+ **API key:** a real environment variable wins, then the **OS keyring** (`job-applier init` stores it there),
254
+ then a `.env` file. `doctor` reports only *where* the key came from, never the key.
255
+
256
+ ## Useful flags
257
+
258
+ | Flag | What it does |
259
+ | --- | --- |
260
+ | `--cdp-url http://127.0.0.1:9222` | Attach to the dedicated Chrome instead of launching a browser |
261
+ | `--dry-run` / `--review` | See [Safety features](#safety-features) |
262
+ | `--force` | Apply even if the URL was already submitted |
263
+ | `--retries N` | Extra attempts after a transient browser/network failure |
264
+ | `--max-api-calls N`, `--max-spend USD` | Hard per-run LLM limits |
265
+ | `--sensitive-policy review\|auto` | What to do with sensitive answers that have no preferred answer |
266
+ | `--no-headless`, `--slow-mo 250`, `--devtools`, `--browser-channel chrome` | Launch-mode browser options (not used when attached) |
267
+ | `--verbose`, `--playwright-debug` | Console progress; Playwright's own noisy log |
268
+ | `--pause-on-block` | On a blocked/error finish, pause in the Playwright Inspector (launch mode) |
269
+ | `--wait-for-captcha`, `--wait-for-submit` | Human-in-the-loop CAPTCHA / submit |
270
+ | `--action-tool pydoll` | Use the **experimental** Pydoll backend (`pip install "job-applier[pydoll]"`) |
271
+ | `--company-name`, `--hr-system-name` | Select a specialized flow, see below |
272
+ | `--resume-md`, `--resume-pdf`, `--human-prompt`, `--output-json` | Override the default paths |
273
+ | `--startup-buffer-ms`, `--step-delay-min-ms`, `--step-delay-max-ms` | Pacing |
274
+
275
+ ## Switching the browser tool
276
+
277
+ Every browser action goes through `job_applier/actions.py`, which forwards to the backend named by
278
+ `JOBAPPLIER_ACTION_TOOL` / `--action-tool`. Backends live in `job_applier/actiontools/` (`playwright.py`,
279
+ `pydoll.py`); flows never call a browser library directly. To add a tool, subclass `ActionTool`
280
+ (`actiontools/base.py`) and register it in `actiontools/__init__.py`.
281
+
282
+ **Playwright is the supported backend.** Pydoll is **experimental**: its attach mode and tab lifecycle are
283
+ verified against a real browser in the test suite (`tests/test_attach.py`), but the full apply flows have only
284
+ been exercised with Playwright, and it does not support request interception (Ashby's resume-autofill blocking
285
+ is skipped with a warning).
286
+
287
+ ## Per-company / per-HR-system flows
288
+
289
+ ```
290
+ job-applier "<url>" --hr-system-name "Greenhouse" --company-name "Acme Inc"
291
+ ```
292
+
293
+ | Given | JobApplier looks for |
294
+ | --- | --- |
295
+ | `--hr-system-name` | A matching module in `job_applier/hr_company_applyflows/` (tried first) |
296
+ | `--company-name` | Same folder, tried if no HR-system match |
297
+ | Neither, or no match | `generic.py` — the default flow |
298
+
299
+ **Currently supported:**
300
+
301
+ | Companies | HR systems |
302
+ | --- | --- |
303
+ | Henry Schein One, Amazon | Ashby, BambooHR, ApplyToJobs, Greenhouse, Rippling ATS, Jobvite, Workday |
304
+
305
+ **Adding one:** drop a module named after the normalized name (lowercase, no punctuation) in
306
+ `job_applier/hr_company_applyflows/`, exposing:
307
+
308
+ ```python
309
+ def run(page, settings, record, log, client_bundle, resume_text, human_prompt_text, job_url, qa_cache):
310
+ ...
311
+ ```
312
+
313
+ Reuse pieces from `job_applier/apply_flow.py` (`fail`, `fill_form_once`, `find_next_button`, `has_success_text`,
314
+ …) — see `generic.py`. Final Submit clicks must go through `apply_variations.click_with_retry_budget`, which is
315
+ where `--dry-run`/`--review` are enforced for every flow; a flow that submits some other way would bypass them.
316
+
317
+ ## Output
318
+
319
+ Each run appends/updates a record in `results/<date>_regular_job_applications.json`. The schema is
320
+ versioned (`schema_version`); fields are only ever added within a version.
321
+
322
+ ```json
323
+ {
324
+ "schema_version": 1,
325
+ "job_applier_version": "0.2.0",
326
+ "job_url": "https://company.example.com/careers/apply/123",
327
+ "normalized_url": "https://company.example.com/careers/apply/123",
328
+ "company_name": "Acme Inc",
329
+ "hr_system_name": "Greenhouse",
330
+ "backend": "playwright",
331
+ "browser": { "name": "chromium", "version": "150.0.0.0", "attached": true },
332
+ "started_at": "2026-09-17T10:00:00",
333
+ "finished_at": "2026-09-17T10:02:31",
334
+ "duration_seconds": 151.2,
335
+ "status": "submitted",
336
+ "error_class": null,
337
+ "dry_run": false, "review": false, "reviewed": false, "submit_attempted": true,
338
+ "attempts": 1, "retries": [],
339
+ "questions": [{ "label": "First name", "kind": "text", "answer": "Umer" }],
340
+ "flagged_fields": [],
341
+ "llm_usage": { "api_calls": 12, "input_tokens": 9400, "output_tokens": 310, "spend_usd": null },
342
+ "screenshot": null, "action_failures": [], "note": null
343
+ }
344
+ ```
345
+
346
+ | Field | Meaning |
347
+ | --- | --- |
348
+ | `status` | `submitted`, `dry_run`, `review_required`, `review_declined`, `interrupted`, `skipped_duplicate`, `blocked`, `bot_detected`, `error`, or `in_progress` |
349
+ | `error_class` | `config`, `browser`, `captcha_or_login`, `validation`, `submit_failed`, `cancelled`, `budget`, `llm`, `unknown` |
350
+ | `questions` | Every field the form asked, with the answer given |
351
+ | `flagged_fields` | Sensitive answers that had no preferred answer |
352
+
353
+ ## Privacy and personal data
354
+
355
+ - **Everything stays on your machine**, except what is sent to your LLM provider: field labels/options, the
356
+ job URL, and your resume text (`resume.md`) and preferred answers as prompt context. Nothing else is sent
357
+ anywhere. There is **no telemetry and no analytics**.
358
+ - Results, screenshots, the answer cache and logs contain **personal information** (your answers, page
359
+ screenshots). They live in your per-user directory; keep them out of repositories and do not attach them
360
+ to bug reports without redacting them.
361
+ - Logs are structured JSON lines, redact anything that looks like an API key, and never contain form answers
362
+ or resume text (only field labels). The results JSON does contain the Q&A trail by design.
363
+ - API keys are stored in the OS keyring when available; otherwise in `.env` with owner-only permissions
364
+ (Unix mode 600; on Windows the file inherits your user-profile permissions).
365
+
366
+ ## Limitations
367
+
368
+ - Supported flows are the HR systems and companies listed above; everything else uses the generic flow,
369
+ which may get stuck (`blocked`) on unusual forms.
370
+ - Submit detection is by control label (Submit / Send application / Finish / Apply after fields are filled).
371
+ A site that submits through an unlabeled control would bypass `--dry-run`/`--review` — please report it.
372
+ - `--review` needs an interactive terminal; batch runs with `--review` end as `review_required`.
373
+ - Two application URLs for the same job that differ in a meaningful query parameter are treated as different
374
+ applications, so the duplicate check can miss them.
375
+ - Chrome-specific: attach mode drives Chrome/Edge (Chromium) only.
376
+
377
+ ## Development
378
+
379
+ ```
380
+ pip install -e ".[dev,pydoll]"
381
+ python -m playwright install chromium # the test suite uses it (flow fixtures, real-CDP attach tests)
382
+ python -m pytest
383
+ python scripts/check.py # ruff + mypy + pytest (add --fast to skip the Chromium tests)
384
+ ```
385
+
386
+ CI runs on Windows, macOS and Linux with Python 3.10–3.14. A manual "Live smoke test" workflow loads a real
387
+ application form with `--dry-run`; live submissions are never part of CI.
388
+
389
+ Standalone — no runtime dependency on PlaywrightURLJsonExtractor or Linkedin-RegularApplyBot.
390
+ `job_applier/detector` is a vendored copy of PlaywrightURLJsonExtractor's bot-detection code; the LLM-calling
391
+ pattern is adapted from its `qa.py`.