offerprinter 0.2.0__tar.gz → 0.4.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 (77) hide show
  1. {offerprinter-0.2.0 → offerprinter-0.4.0}/.gitignore +3 -1
  2. offerprinter-0.4.0/CHANGELOG.md +217 -0
  3. {offerprinter-0.2.0 → offerprinter-0.4.0}/PKG-INFO +261 -7
  4. {offerprinter-0.2.0 → offerprinter-0.4.0}/README.md +260 -6
  5. {offerprinter-0.2.0 → offerprinter-0.4.0}/config.example.toml +23 -0
  6. {offerprinter-0.2.0 → offerprinter-0.4.0}/docs/skill/SKILL.md +33 -0
  7. offerprinter-0.4.0/evals/README.md +75 -0
  8. offerprinter-0.4.0/extension/README.md +57 -0
  9. {offerprinter-0.2.0 → offerprinter-0.4.0}/offerprinter/__init__.py +1 -1
  10. offerprinter-0.4.0/offerprinter/cli.py +1140 -0
  11. {offerprinter-0.2.0 → offerprinter-0.4.0}/offerprinter/config.py +5 -0
  12. {offerprinter-0.2.0 → offerprinter-0.4.0}/offerprinter/controllers/pipeline.py +44 -6
  13. offerprinter-0.4.0/offerprinter/mcp_server.py +345 -0
  14. {offerprinter-0.2.0 → offerprinter-0.4.0}/offerprinter/models/schemas.py +124 -0
  15. offerprinter-0.4.0/offerprinter/prompts/__init__.py +58 -0
  16. {offerprinter-0.2.0 → offerprinter-0.4.0}/offerprinter/prompts/templates.py +273 -0
  17. offerprinter-0.4.0/offerprinter/services/cache.py +152 -0
  18. offerprinter-0.4.0/offerprinter/services/differ.py +193 -0
  19. {offerprinter-0.2.0 → offerprinter-0.4.0}/offerprinter/services/generator.py +77 -0
  20. offerprinter-0.4.0/offerprinter/services/jd_fetcher.py +216 -0
  21. offerprinter-0.4.0/offerprinter/services/linkedin.py +267 -0
  22. {offerprinter-0.2.0 → offerprinter-0.4.0}/offerprinter/services/pdf_writer.py +109 -95
  23. offerprinter-0.4.0/offerprinter/services/ranker.py +95 -0
  24. offerprinter-0.4.0/offerprinter/services/redactor.py +174 -0
  25. offerprinter-0.4.0/offerprinter/services/themes.py +232 -0
  26. offerprinter-0.4.0/offerprinter/services/verifier.py +367 -0
  27. {offerprinter-0.2.0 → offerprinter-0.4.0}/offerprinter/services/writer.py +28 -4
  28. {offerprinter-0.2.0 → offerprinter-0.4.0}/pyproject.toml +1 -1
  29. offerprinter-0.4.0/tests/test_new_services.py +400 -0
  30. offerprinter-0.4.0/tests/test_templates_and_linkedin.py +329 -0
  31. offerprinter-0.4.0/tests/test_verifier.py +240 -0
  32. offerprinter-0.2.0/CHANGELOG.md +0 -104
  33. offerprinter-0.2.0/offerprinter/cli.py +0 -523
  34. offerprinter-0.2.0/offerprinter/prompts/__init__.py +0 -32
  35. offerprinter-0.2.0/offerprinter/services/jd_fetcher.py +0 -86
  36. {offerprinter-0.2.0 → offerprinter-0.4.0}/LICENSE +0 -0
  37. {offerprinter-0.2.0 → offerprinter-0.4.0}/app.py +0 -0
  38. {offerprinter-0.2.0 → offerprinter-0.4.0}/cli.py +0 -0
  39. {offerprinter-0.2.0 → offerprinter-0.4.0}/deploy/README.md +0 -0
  40. {offerprinter-0.2.0 → offerprinter-0.4.0}/deploy/huggingface/README.md +0 -0
  41. {offerprinter-0.2.0 → offerprinter-0.4.0}/examples/README.md +0 -0
  42. {offerprinter-0.2.0 → offerprinter-0.4.0}/examples/output/northbank-senior-product-analyst/ats-keyword-report.docx +0 -0
  43. {offerprinter-0.2.0 → offerprinter-0.4.0}/examples/output/northbank-senior-product-analyst/ats-keyword-report.md +0 -0
  44. {offerprinter-0.2.0 → offerprinter-0.4.0}/examples/output/northbank-senior-product-analyst/cover-letter.docx +0 -0
  45. {offerprinter-0.2.0 → offerprinter-0.4.0}/examples/output/northbank-senior-product-analyst/cover-letter.md +0 -0
  46. {offerprinter-0.2.0 → offerprinter-0.4.0}/examples/output/northbank-senior-product-analyst/fit-memo.docx +0 -0
  47. {offerprinter-0.2.0 → offerprinter-0.4.0}/examples/output/northbank-senior-product-analyst/fit-memo.md +0 -0
  48. {offerprinter-0.2.0 → offerprinter-0.4.0}/examples/output/northbank-senior-product-analyst/full-package.docx +0 -0
  49. {offerprinter-0.2.0 → offerprinter-0.4.0}/examples/output/northbank-senior-product-analyst/full-package.md +0 -0
  50. {offerprinter-0.2.0 → offerprinter-0.4.0}/examples/output/northbank-senior-product-analyst/interview-prep-pack.docx +0 -0
  51. {offerprinter-0.2.0 → offerprinter-0.4.0}/examples/output/northbank-senior-product-analyst/interview-prep-pack.md +0 -0
  52. {offerprinter-0.2.0 → offerprinter-0.4.0}/examples/output/northbank-senior-product-analyst/tailored-cv.docx +0 -0
  53. {offerprinter-0.2.0 → offerprinter-0.4.0}/examples/output/northbank-senior-product-analyst/tailored-cv.md +0 -0
  54. {offerprinter-0.2.0 → offerprinter-0.4.0}/examples/sample_cv.md +0 -0
  55. {offerprinter-0.2.0 → offerprinter-0.4.0}/examples/sample_jd.md +0 -0
  56. {offerprinter-0.2.0 → offerprinter-0.4.0}/offerprinter/controllers/__init__.py +0 -0
  57. {offerprinter-0.2.0 → offerprinter-0.4.0}/offerprinter/llm/__init__.py +0 -0
  58. {offerprinter-0.2.0 → offerprinter-0.4.0}/offerprinter/llm/anthropic_provider.py +0 -0
  59. {offerprinter-0.2.0 → offerprinter-0.4.0}/offerprinter/llm/base.py +0 -0
  60. {offerprinter-0.2.0 → offerprinter-0.4.0}/offerprinter/llm/factory.py +0 -0
  61. {offerprinter-0.2.0 → offerprinter-0.4.0}/offerprinter/llm/gemini_provider.py +0 -0
  62. {offerprinter-0.2.0 → offerprinter-0.4.0}/offerprinter/llm/kimi_provider.py +0 -0
  63. {offerprinter-0.2.0 → offerprinter-0.4.0}/offerprinter/llm/ollama_provider.py +0 -0
  64. {offerprinter-0.2.0 → offerprinter-0.4.0}/offerprinter/llm/openai_provider.py +0 -0
  65. {offerprinter-0.2.0 → offerprinter-0.4.0}/offerprinter/models/__init__.py +0 -0
  66. {offerprinter-0.2.0 → offerprinter-0.4.0}/offerprinter/pricing.py +0 -0
  67. {offerprinter-0.2.0 → offerprinter-0.4.0}/offerprinter/services/__init__.py +0 -0
  68. {offerprinter-0.2.0 → offerprinter-0.4.0}/offerprinter/services/cv_parser.py +0 -0
  69. {offerprinter-0.2.0 → offerprinter-0.4.0}/offerprinter/services/tracker.py +0 -0
  70. {offerprinter-0.2.0 → offerprinter-0.4.0}/offerprinter/ui/__init__.py +0 -0
  71. {offerprinter-0.2.0 → offerprinter-0.4.0}/offerprinter/ui/printer.py +0 -0
  72. {offerprinter-0.2.0 → offerprinter-0.4.0}/tests/__init__.py +0 -0
  73. {offerprinter-0.2.0 → offerprinter-0.4.0}/tests/conftest.py +0 -0
  74. {offerprinter-0.2.0 → offerprinter-0.4.0}/tests/test_offerprinter.py +0 -0
  75. {offerprinter-0.2.0 → offerprinter-0.4.0}/tests/test_pdf_writer.py +0 -0
  76. {offerprinter-0.2.0 → offerprinter-0.4.0}/tests/test_pricing_and_retries.py +0 -0
  77. {offerprinter-0.2.0 → offerprinter-0.4.0}/tests/test_tracker.py +0 -0
@@ -1,6 +1,8 @@
1
1
  # --- OfferPrinter ---
2
2
  # Never commit secrets or generated user output.
3
- config.toml
3
+ # Anchored to the repo root: an unanchored "config.toml" also matches nested
4
+ # files like .streamlit/config.toml, which is checked in on purpose.
5
+ /config.toml
4
6
  .env
5
7
  output/
6
8
  !examples/output/
@@ -0,0 +1,217 @@
1
+ # Changelog
2
+
3
+ All notable changes to OfferPrinter are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.4.0] — 2026-08-25
11
+
12
+ The "it has to look like something" release. v0.3.0 made the output provable;
13
+ this one makes it presentable, and makes getting your history in a two-minute job
14
+ rather than an afternoon.
15
+
16
+ ### Added
17
+
18
+ - **Six PDF templates**: `classic`, `compact`, `serif`, `rule`, `accent`, `mono`.
19
+ Choose with `--template`, list with `offerprinter templates`, and see them with
20
+ `offerprinter templates --sample ./samples`. Set a default under `[output]` in
21
+ `config.toml`, or `OFFERPRINTER_TEMPLATE`.
22
+ - **LinkedIn import** (`--linkedin`). Takes either export LinkedIn offers: the PDF
23
+ from *More → Save to PDF*, or the CSV archive from *Get a copy of your data*.
24
+ Nothing is scraped and nothing is fetched — you export your own data and this
25
+ reads the file.
26
+
27
+ ### Notes on the templates
28
+
29
+ Every template stays inside what an ATS can parse: one of the 14 PDF base fonts,
30
+ no columns, no tables, no images, no text boxes. What varies is typeface, spacing,
31
+ rules, capitalisation and one accent colour. The test suite checks, per template,
32
+ that the PDF embeds no font program, contains no `/Image` or `/XObject`, and that
33
+ a naive text extraction still returns the name, the email and every bullet.
34
+
35
+ `accent` is labelled *email only*: it extracts cleanly, but carries enough colour
36
+ that a strict parser could lose a heading.
37
+
38
+ The LinkedIn importer is held to the no-fabrication guarantee too. A test takes
39
+ every word of its output and asserts it appeared in the input, because an importer
40
+ that quietly tidied up your history would break the guarantee before the generator
41
+ ever ran.
42
+
43
+ ### Unchanged
44
+
45
+ `classic` renders byte-for-byte what v0.3.0 rendered. There is a test for that, so
46
+ upgrading changes nothing unless you ask it to.
47
+
48
+ ## [0.3.0] — 2026-08-20
49
+
50
+ The "prove it" release. v0.2.0 made OfferPrinter installable; this one makes its
51
+ central claim checkable, and extends the tool across the rest of a job hunt.
52
+
53
+ ### Added
54
+
55
+ **Proving the guarantee**
56
+ - **Fabrication verifier.** Every number, date and named entity in the tailored
57
+ CV and cover letter is checked against your source CV, and the tailored CV is
58
+ cross-checked against the ATS report's own list of gaps — so a package that
59
+ contradicts itself is caught automatically. Writes `fabrication-check.md`.
60
+ `--strict` exits 3 if anything is unverified, which makes it usable as a gate.
61
+ Until now "never fabricate" was an instruction in a prompt; now it is an
62
+ assertion the tool makes about its own output.
63
+ - **`offerprinter verify`** re-checks an already-generated package — after you
64
+ have hand-edited it, or in CI. Makes no API calls, so it is free and offline.
65
+ - **Tailoring diff.** `tailoring-diff.md` shows exactly what changed: which
66
+ bullets were reworded, what was added or dropped, and which vocabulary is new.
67
+ The verifier proves nothing was invented; the diff shows what was done.
68
+ - **Eval harness** (`evals/`, `scripts/run_evals.py`). Golden cases with
69
+ per-case assertions about which gaps must be named and which terms must never
70
+ be claimed, plus an LLM-as-judge pass scoring grounding, specificity, honesty,
71
+ usefulness and format. The deterministic half runs in CI on every PR; the
72
+ judge runs on demand. Prompt changes are no longer a guess.
73
+
74
+ **New commands**
75
+ - **`offerprinter rank`** — score a folder of adverts and rank them by fit
76
+ without generating a single document. Two cheap calls per job; triage twenty
77
+ roles for a couple of pence, then print packages only for the ones worth it.
78
+ - **`offerprinter followup`** — thank-you email, recruiter message, LinkedIn
79
+ note (hard-capped at 300 characters) or a polite nudge. Written from your CV
80
+ and your notes; it will not invent a conversation that did not happen.
81
+ - **`offerprinter practice`** — an interactive mock interview. It asks, you
82
+ answer, it critiques and rewrites your answer using only your real experience,
83
+ then summarises the patterns worth fixing.
84
+ - **`offerprinter mcp`** — run as an MCP server on stdio, so Claude Desktop,
85
+ Claude Code and other agents can call OfferPrinter natively and get structured
86
+ results back. Hand-rolled JSON-RPC; no new dependencies.
87
+ - **`offerprinter cache stats` / `cache clear`**.
88
+
89
+ **Privacy and job-board reality**
90
+ - **PII redaction** (`--redact`). Your name, email, phone and profile links are
91
+ replaced with placeholders before the provider sees anything, and restored in
92
+ the output. The model keeps every employer, date and metric it needs.
93
+ - **`--jd-clipboard`** and a **browser extension** (`extension/`). Job boards
94
+ are the most common reason a run fails; the advert is already rendered in your
95
+ tab, so read it from there. The extension has no host permissions and makes no
96
+ network requests.
97
+ - **schema.org `JobPosting` JSON-LD extraction.** Many boards embed the whole
98
+ advert as structured data — cleaner than anything scraped, and it supplies the
99
+ company and job title for free.
100
+
101
+ **Cost**
102
+ - **Response caching.** Keyed on provider, model, temperature and both prompts,
103
+ so anything that could change the answer correctly misses. Re-running after a
104
+ CV tweak is now mostly free.
105
+
106
+ ### Fixed
107
+
108
+ - Multi-word entities could match across a line break, producing nonsense claims
109
+ glued together from two sentences.
110
+ - A level-two heading (`## Missing keywords`) was not recognised as a sentence
111
+ start, so its first word was treated as a named entity.
112
+ - A capitalised word at the very start of a bullet ("Used SQL daily") was read
113
+ as an entity, because the sentence-start pattern only handled bullets that
114
+ followed a newline, not one at the start of the text.
115
+
116
+ ### Changed
117
+
118
+ - Config gains `[output] redact`, `[generation] verify`, `diff` and `cache`.
119
+ - The test suite grew from 73 to 143 tests, still fully offline.
120
+
121
+
122
+ ## [0.2.0] — 2026-08-18
123
+
124
+ The "actually installable, and quite a lot faster" release.
125
+
126
+ ### Added
127
+
128
+ **Distribution**
129
+ - Published to PyPI: `pipx install offerprinter`, `uvx offerprinter`, or
130
+ `pip install offerprinter`.
131
+ - `offerprinter` and `opr` console scripts, so it works from anywhere after
132
+ install rather than only from a git clone.
133
+ - Standalone binaries for macOS, Linux and Windows attached to each release —
134
+ no Python installation required.
135
+ - Multi-arch Docker image published to GHCR on every push and tag:
136
+ `ghcr.io/mohitagw15856/offerprinter:latest`.
137
+ - Homebrew formula in `packaging/homebrew/` for a `brew install` tap.
138
+ - Deployment guides for Streamlit Community Cloud, Hugging Face Spaces, Docker
139
+ and container platforms, in `deploy/`.
140
+
141
+ **Features**
142
+ - **Fit score** — every run ends with a 0-100 score, a band, and honest
143
+ strengths and gaps. Written to `fit-score.md` and shown in the CLI and web UI.
144
+ - **PDF output** — real, selectable, ATS-friendly Helvetica text, written by a
145
+ dependency-free PDF writer rather than a rendering engine. Enable with
146
+ `--formats md,docx,pdf`.
147
+ - **Roast mode** — `offerprinter roast --cv cv.pdf`, or `--roast` during a run.
148
+ Blunt, funny, opt-in critique of your CV's *writing*. Still never dishonest.
149
+ - **Application tracker** — every run is recorded locally in
150
+ `~/.offerprinter/applications.json`. New commands: `offerprinter list`,
151
+ `offerprinter stats`, `offerprinter status <slug> <status>`.
152
+ - **Achievements** — eleven small milestones, computed from your local history.
153
+ - **Batch mode** — `--jd-dir ./jobs` prints a package for every job description
154
+ in a folder.
155
+ - **Cost and token reporting** — every run reports calls, tokens and estimated
156
+ spend. `--dry-run` forecasts the cost without calling the API at all.
157
+ - **Ollama provider** — run entirely on your own machine with no API key and no
158
+ data leaving your laptop: `--provider ollama`.
159
+ - **Printer animation** — an ASCII dot-matrix printer feeding a sheet per
160
+ document. Degrades to plain status lines when not a terminal, or with
161
+ `--no-animation`.
162
+ - **Demo mode in the web UI** — with no API key configured, the app shows the
163
+ bundled example package instead of an error, which is what makes a public
164
+ hosted demo possible.
165
+
166
+ **Reliability**
167
+ - Automatic retries with exponential backoff and `Retry-After` support on 429s
168
+ and 5xx errors. A single rate limit no longer kills a five-document run.
169
+ - `[pricing]` config table to override list prices with your actual rates.
170
+
171
+ **Project**
172
+ - `CONTRIBUTING.md`, `SECURITY.md`, `CODE_OF_CONDUCT.md`, this changelog, issue
173
+ and pull request templates.
174
+ - CI now runs the test suite across Python 3.11/3.12/3.13 with macOS and Windows
175
+ spot-checks, plus coverage, a packaging build, and a console-script smoke test.
176
+
177
+ ### Changed
178
+
179
+ - **Artifacts now generate in parallel.** They are independent of each other, so
180
+ a full run takes about as long as its slowest single document instead of the
181
+ sum of all five. Use `--sequential` for the old behaviour.
182
+ - The CLI moved to `offerprinter/cli.py` so it ships inside the installed
183
+ package. `python cli.py …` still works from a clone.
184
+ - The web UI (`streamlit`) is now an optional extra, keeping
185
+ `pipx install offerprinter` small. Install it with `offerprinter[web]`.
186
+ - Default output formats remain `md` and `docx`; `pdf` is opt-in via config
187
+ or `--formats`.
188
+
189
+ ### Fixed
190
+
191
+ - The `offerprinter` console script pointed at a root-level `cli` module that
192
+ was never included in the wheel, so it could not resolve after an install.
193
+ - The README claimed CI ran `pytest`. It didn't. Now it does.
194
+
195
+ ## [0.1.0] — 2026-08-18
196
+
197
+ First release — "First Print".
198
+
199
+ ### Added
200
+
201
+ - Five generated artifacts from one CV and one job description: tailored CV,
202
+ cover letter, fit memo, ATS keyword report, interview prep pack.
203
+ - The no-fabrication guarantee, enforced in every prompt.
204
+ - Four providers behind one interface: Anthropic (default), OpenAI, Gemini,
205
+ Moonshot Kimi.
206
+ - CLI (Typer), web UI (Streamlit), and an agent skill at `docs/skill/SKILL.md`.
207
+ - CV input as `.pdf`, `.docx`, `.md`, `.txt` or pasted text; job description as a
208
+ URL or text.
209
+ - Markdown and Word output, plus a combined full package.
210
+ - UK and US English.
211
+ - Config via env vars, `config.toml`, or defaults.
212
+ - Dockerfile and docker-compose, offline test suite, ruff lint.
213
+
214
+ [Unreleased]: https://github.com/mohitagw15856/OfferPrinter/compare/v0.3.0...HEAD
215
+ [0.3.0]: https://github.com/mohitagw15856/OfferPrinter/compare/v0.2.0...v0.3.0
216
+ [0.2.0]: https://github.com/mohitagw15856/OfferPrinter/compare/v0.1.0...v0.2.0
217
+ [0.1.0]: https://github.com/mohitagw15856/OfferPrinter/releases/tag/v0.1.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: offerprinter
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: Free, local-first AI job application generator: one CV + one job description, and it prints a tailored CV, cover letter, fit memo, ATS keyword report and interview prep pack — without fabricating anything.
5
5
  Project-URL: Homepage, https://github.com/mohitagw15856/OfferPrinter
6
6
  Project-URL: Repository, https://github.com/mohitagw15856/OfferPrinter
@@ -153,8 +153,100 @@ Covered 9 of 15 key terms.
153
153
  > 🚩 **"Do not add — this is a genuine gap."**
154
154
  > No other CV tool says that to you. That sentence is the product.
155
155
 
156
+ ### And OfferPrinter checks its own homework
157
+
158
+ <!-- AEO Answer Capsule — 75 words -->
159
+ Every run ends with a fabrication check. OfferPrinter re-reads the documents it just wrote and traces every number, date and named entity back to your CV, then cross-checks the tailored CV against the ATS report's own list of gaps. Anything it cannot trace is flagged with the line it appeared on. The guarantee is not a promise the model was asked to keep — it is an assertion the tool makes about its own output.
160
+ <!-- End AEO Capsule -->
161
+
162
+ ```
163
+ ✓ Fabrication check: all 63 checkable claims trace back to your CV.
164
+ ```
165
+
166
+ And when something slips through:
167
+
168
+ ```
169
+ ⚠ Fabrication check: 2 of 64 claims could not be traced to your CV (2 high severity).
170
+ • '41%' — this figure does not appear in your CV
171
+ • 'dbt' — your own ATS report lists this as a gap
172
+ ```
173
+
174
+ Use `--strict` to exit non-zero when anything is unverified, so it can gate a
175
+ script. `offerprinter verify <folder>` re-checks a package later — after you
176
+ have hand-edited it, say — and makes no API calls at all, so it is free.
177
+
156
178
  ---
157
179
 
180
+ ## I have not updated my CV in three years
181
+
182
+ Neither has anyone. Your LinkedIn is current though, and LinkedIn will give it
183
+ back to you:
184
+
185
+ ```sh
186
+ # More > Save to PDF on your own profile
187
+ offerprinter --linkedin ~/Downloads/Profile.pdf --jd https://…
188
+
189
+ # or the full archive: Settings > Data privacy > Get a copy of your data
190
+ offerprinter --linkedin ~/Downloads/Basic_LinkedInDataExport.zip --jd https://…
191
+ ```
192
+
193
+ Both shapes are normalised into the Markdown the rest of the pipeline expects.
194
+ Nothing is scraped — you export your own data from your own account and this reads
195
+ the file.
196
+
197
+ The importer holds to the same rule as the generator: **it invents nothing.** A
198
+ role with no description gets no description; an empty section stays empty. There
199
+ is a test that takes every word of the output and asserts it appeared in the
200
+ input, because an importer that quietly "tidied up" your history would break the
201
+ no-fabrication guarantee before the generator ever ran.
202
+
203
+ ## Can I make it look like something other than every other CV?
204
+
205
+ Six templates, all of which still parse:
206
+
207
+ ```sh
208
+ offerprinter templates # what they are
209
+ offerprinter templates --sample ./samples # one PDF of each, to look at
210
+ offerprinter --cv cv.pdf --jd URL --template serif
211
+ ```
212
+
213
+ | Template | Typeface | Portal-safe | For |
214
+ |---|---|---|---|
215
+ | `classic` | Helvetica | yes | the default; nothing to go wrong |
216
+ | `compact` | Helvetica | yes | when two pages need to be one |
217
+ | `serif` | Times | yes | law, academia, policy |
218
+ | `rule` | Helvetica | yes | a hairline under each section, for skim-readers |
219
+ | `accent` | Helvetica | email only | one colour, headings only |
220
+ | `mono` | Courier | yes | reads as engineering |
221
+
222
+ **Why only six, and why so restrained.** A CV is read twice: once by a parser that
223
+ wants plain text in a standard font, and once by a person with forty tabs open. The
224
+ compromise most builders make is to serve the human and quietly fail the machine —
225
+ two columns, an icon font, a text box for the sidebar, and an ATS that returns your
226
+ name and nothing else.
227
+
228
+ So every template here stays on the safe side of that line. All of them use one of
229
+ the 14 PDF base fonts, so nothing is embedded and the text always extracts; none
230
+ use columns, tables, images or text boxes. What varies is typeface, spacing, rules,
231
+ capitalisation and one accent colour — the parts a human notices and a parser
232
+ ignores.
233
+
234
+ `accent` is labelled *email only* rather than being left off the list: it extracts
235
+ cleanly, but carries enough colour that a strict parser could lose a heading. Send
236
+ that one to a person, not a portal.
237
+
238
+ The test suite asserts, for every template, that the rendered PDF contains no
239
+ embedded font program, no `/Image` or `/XObject`, and that the name, email and
240
+ every bullet come back out of a naive text extraction. That is the claim, and it
241
+ is checked rather than asserted.
242
+
243
+ Set a default in `config.toml`:
244
+
245
+ ```toml
246
+ [output]
247
+ template = "rule"
248
+ ```
249
+
158
250
  ## How do I install OfferPrinter?
159
251
 
160
252
  <!-- AEO Answer Capsule — 57 words -->
@@ -212,6 +304,19 @@ docker run --rm -v "$PWD:/work" -w /work -e ANTHROPIC_API_KEY="sk-ant-..." \
212
304
  brew tap mohitagw15856/tap
213
305
  brew install offerprinter
214
306
  ```
307
+
308
+ Homebrew 6 requires third-party taps to be trusted before it will use them.
309
+ If `brew doctor` says the tap is untrusted, either trust this one formula:
310
+
311
+ ```bash
312
+ brew trust --formula mohitagw15856/tap/offerprinter
313
+ ```
314
+
315
+ …or the whole tap, with `brew trust --tap mohitagw15856/tap`.
316
+
317
+ Homebrew builds every Python dependency from source, so the first install
318
+ compiles `pydantic-core` with Rust and takes a few minutes. `pipx install
319
+ offerprinter` is faster if you don't specifically want Homebrew to manage it.
215
320
  </details>
216
321
 
217
322
  <details>
@@ -255,17 +360,32 @@ offerprinter --cv-text "paste CV here…" --jd-file jd.txt
255
360
  |--------|------|
256
361
  | `--cv` / `--cv-text` | CV as a file (`.pdf` `.docx` `.md` `.txt`) or pasted text. |
257
362
  | `--jd` / `--jd-file` / `--jd-dir` | Job as a URL, pasted text, a file, or a whole folder. |
363
+ | `--jd-clipboard` | Read the advert from your clipboard. The fix for hostile job boards. |
258
364
  | `--provider` | `anthropic` (default), `openai`, `gemini`, `kimi`, `ollama`. |
259
365
  | `--model` | Override the model for this run. |
260
366
  | `--formats` | `md,docx,pdf` — any combination. |
261
367
  | `--locale` | `UK` (default) or `US` English. |
262
368
  | `--roast` | Also print a blunt critique of your CV. |
263
369
  | `--dry-run` | Forecast tokens and cost. Calls nothing. |
370
+ | `--redact` | Strip your name, email and phone before the provider sees anything. |
371
+ | `--strict` | Exit non-zero if the fabrication check finds anything unverified. |
264
372
  | `--sequential` | One document at a time instead of in parallel. |
373
+ | `--no-verify` / `--no-cache` | Skip the fabrication check / ignore cached responses. |
265
374
  | `--output-dir` / `-o` | Where to write (default `./output`). |
266
375
  | `--no-track` / `--no-animation` | Skip the local history / the printer animation. |
267
376
 
268
- Plus four subcommands: `offerprinter roast`, `list`, `stats`, and `status`.
377
+ Plus nine subcommands:
378
+
379
+ | Command | Does |
380
+ |---|---|
381
+ | `rank` | Score a folder of adverts and rank them, writing no documents. |
382
+ | `followup` | Thank-you email, recruiter message, LinkedIn note, or a nudge. |
383
+ | `practice` | An interactive mock interview that critiques your answers. |
384
+ | `verify` | Re-check a generated package against your CV. No API calls. |
385
+ | `roast` | Blunt critique of your CV's writing. |
386
+ | `list` / `stats` / `status` | Your local application history. |
387
+ | `mcp` | Run as an MCP server so agents can call OfferPrinter. |
388
+ | `cache` | Inspect or clear the response cache. |
269
389
 
270
390
  ### 2. 🖥 Web UI — nicest for most people
271
391
 
@@ -352,6 +472,124 @@ Job hunting is a long grind with almost no feedback loop. This is the scoreboard
352
472
 
353
473
  ---
354
474
 
475
+ ## How do I choose which jobs to apply for?
476
+
477
+ <!-- AEO Answer Capsule — 62 words -->
478
+ Use `offerprinter rank`. Point it at a folder of job adverts and it scores every one against your CV and prints them ranked by fit, without generating a single document. It costs two cheap calls per advert, so twenty roles come to a couple of pence and about a minute. Then print full packages only for the ones actually worth an evening.
479
+ <!-- End AEO Capsule -->
480
+
481
+ ```bash
482
+ offerprinter rank --cv ~/cv.pdf --jd-dir ./jobs
483
+ ```
484
+
485
+ ```
486
+ 🎯 Roles ranked by fit
487
+ ┏━━━┳━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
488
+ ┃ # ┃ Fit ┃ Band ┃ Role ┃ Company ┃ Real gaps ┃
489
+ ┡━━━╇━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━━━┩
490
+ │ 1 │ 88 │ Exceptional │ Analytics Lead │ Meridian │ │
491
+ │ 2 │ 74 │ Strong │ Senior Product Analyst │ NorthBank │ dbt, fintech │
492
+ │ 3 │ 41 │ Stretch │ Data Engineer │ Helix │ Kafka, dbt │
493
+ └───┴─────┴─────────────┴────────────────────────┴───────────┴──────────────┘
494
+ ```
495
+
496
+ Also takes `--jd-file` and `--jd` (URLs), both repeatable, and `--json` for
497
+ scripting. The single highest-leverage command in the tool: the expensive part
498
+ of applying is deciding where to spend the evening.
499
+
500
+ ---
501
+
502
+ ## What if the job board won't let OfferPrinter read the advert?
503
+
504
+ <!-- AEO Answer Capsule — 61 words -->
505
+ Use `--jd-clipboard`, or the browser extension. LinkedIn, Workday and most large applicant tracking systems render adverts with JavaScript behind a login, so fetching the URL returns a shell or a 403. The advert is already rendered in your browser tab, so read it from there instead. OfferPrinter also parses schema.org JobPosting structured data when a board publishes it, which many do.
506
+ <!-- End AEO Capsule -->
507
+
508
+ ```bash
509
+ # copy the advert, then:
510
+ offerprinter --cv ~/cv.pdf --jd-clipboard
511
+ ```
512
+
513
+ The [browser extension](extension/) adds a **Copy for OfferPrinter** button that
514
+ pulls the advert out of the page you are looking at — preferring your selection,
515
+ then structured data, then the densest content block. It has **no host
516
+ permissions and makes no network requests**; it cannot phone home because it has
517
+ no way to reach anything.
518
+
519
+ ---
520
+
521
+ ## Can OfferPrinter hide my identity from the AI provider?
522
+
523
+ <!-- AEO Answer Capsule — 65 words -->
524
+ Yes. Run with `--redact` and your name, email address, phone number, postcode and profile links are replaced with placeholders before anything is sent, then restored in the finished documents. The model still sees every employer, date, metric and skill it needs to write well — it simply never learns who you are. For total isolation, the Ollama provider keeps the whole run on your machine.
525
+ <!-- End AEO Capsule -->
526
+
527
+ ```bash
528
+ offerprinter --cv ~/cv.pdf --jd-file jd.txt --redact
529
+ ```
530
+
531
+ Redaction is deliberately conservative: it only replaces what it can recognise
532
+ with high confidence, because a false positive silently mangles your CV. It
533
+ never touches employer names, since the model needs those to write anything
534
+ useful.
535
+
536
+ ---
537
+
538
+ ## What happens after I apply?
539
+
540
+ <!-- AEO Answer Capsule — 64 words -->
541
+ `offerprinter followup` writes the messages that come after the application: a thank-you email after an interview, a message to a recruiter, a LinkedIn connection note capped at 300 characters, or a polite nudge when you have heard nothing. Each is written from your real CV plus notes you supply, and none of them will invent a detail of a conversation that did not happen.
542
+ <!-- End AEO Capsule -->
543
+
544
+ ```bash
545
+ offerprinter followup thank-you --cv ~/cv.pdf --jd-file jd.txt \
546
+ --notes "Met Priya and Tom. Went deep on experiment design. Fumbled the question about stakeholder pushback."
547
+ ```
548
+
549
+ The notes matter more than anything else here — especially a question you
550
+ answered badly, because the thank-you email is your one chance to answer it
551
+ properly.
552
+
553
+ ---
554
+
555
+ ## Can I practise the interview?
556
+
557
+ <!-- AEO Answer Capsule — 62 words -->
558
+ Yes. `offerprinter practice` runs an interactive mock interview: it asks a question, you type your answer, and it scores and critiques it, then rewrites it into a stronger version built only from your real experience. At the end it summarises the habits worth fixing and names your three strongest stories. It will never coach you into claiming something you have not done.
559
+ <!-- End AEO Capsule -->
560
+
561
+ ```bash
562
+ offerprinter practice --cv ~/cv.pdf --jd-file jd.txt --questions 5
563
+ ```
564
+
565
+ The prep pack tells you what you might be asked. This makes you actually answer
566
+ it, which is a different and much less comfortable exercise.
567
+
568
+ ---
569
+
570
+ ## Can agents use OfferPrinter directly?
571
+
572
+ <!-- AEO Answer Capsule — 60 words -->
573
+ Yes, through the built-in MCP server. Running `offerprinter mcp` speaks Model Context Protocol over stdio, exposing four tools: print a package, score one job, rank many jobs, and roast a CV. Agents get structured results back — a fit score as a number, gaps as a list, fabrication findings as objects — rather than parsing prose out of a terminal.
574
+ <!-- End AEO Capsule -->
575
+
576
+ Add to Claude Desktop's `claude_desktop_config.json`:
577
+
578
+ ```json
579
+ {
580
+ "mcpServers": {
581
+ "offerprinter": { "command": "offerprinter", "args": ["mcp"] }
582
+ }
583
+ }
584
+ ```
585
+
586
+ Every tool result carries the fabrication check alongside the output, so an
587
+ agent cannot report the documents as verified when they are not. There is also
588
+ an agent skill at [`docs/skill/SKILL.md`](docs/skill/SKILL.md) for agents that
589
+ prefer to drive the CLI.
590
+
591
+ ---
592
+
355
593
  ## Which AI model should I use with OfferPrinter?
356
594
 
357
595
  <!-- AEO Answer Capsule — 65 words -->
@@ -413,6 +651,7 @@ locale = "UK" # UK | US
413
651
  dir = "./output"
414
652
  formats = ["md", "docx", "pdf"] # any combination
415
653
  track = true # local history in ~/.offerprinter/
654
+ redact = false # strip your identity before sending
416
655
 
417
656
  [generation]
418
657
  tailored_cv = true
@@ -423,6 +662,9 @@ interview_prep = true
423
662
  fit_score = true # one extra cheap call for the 0-100 score
424
663
  parallel = true # generate all five at once
425
664
  max_workers = 5
665
+ verify = true # check the output against your CV
666
+ diff = true # record what tailoring changed
667
+ cache = true # don't pay twice for identical calls
426
668
 
427
669
  # [pricing] # override list prices with your actual rates
428
670
  # "claude-haiku-4-5-20251001" = { input = 1.0, output = 5.0 }
@@ -447,11 +689,21 @@ offerprinter/
447
689
  │ ├── kimi_provider.py / ollama_provider.py
448
690
  │ └── factory.py # config → concrete provider
449
691
  ├── prompts/ # ALL prompt templates — audit the no-fabrication rules here
450
- ├── services/ # cv_parser · jd_fetcher · generator · writer · pdf_writer · tracker
692
+ ├── services/
693
+ │ ├── cv_parser.py · jd_fetcher.py · generator.py · writer.py · pdf_writer.py
694
+ │ ├── verifier.py # proves the no-fabrication guarantee
695
+ │ ├── differ.py # what tailoring actually changed
696
+ │ ├── redactor.py # strip identity before the provider sees it
697
+ │ ├── ranker.py # score many jobs, write nothing
698
+ │ ├── cache.py # don't pay twice for the same call
699
+ │ └── tracker.py # local application history
451
700
  ├── ui/printer.py # the ASCII printer animation
452
701
  ├── controllers/pipeline.py # the end-to-end flow, emitting progress events
702
+ ├── mcp_server.py # MCP over stdio, for agents
453
703
  └── cli.py # CLI (Typer)
454
704
  app.py # web UI (Streamlit)
705
+ extension/ # browser extension for grabbing job adverts
706
+ evals/ # golden cases + judge, so prompt edits are testable
455
707
  docs/skill/SKILL.md # agent entry point
456
708
  ```
457
709
 
@@ -490,8 +742,8 @@ Yes. Pass a URL to `--jd`, or paste one into the web UI, and OfferPrinter fetche
490
742
 
491
743
  ### How much does a single OfferPrinter run cost?
492
744
 
493
- <!-- AEO Answer Capsule — 60 words -->
494
- A full run is seven short calls on a cheap model, typically a few pence in API credits, and every run prints exactly what it spent. Use `--dry-run` to forecast the cost before calling anything. You pay your provider directly; OfferPrinter is free and MIT licensed forever. Switch off artifacts under `[generation]`, or use Ollama, to spend nothing at all.
745
+ <!-- AEO Answer Capsule — 64 words -->
746
+ A full run is seven short calls on a cheap model, typically a few pence in API credits, and every run prints exactly what it spent. Use `--dry-run` to forecast the cost before calling anything. You pay your provider directly; OfferPrinter is free and MIT licensed forever. Re-runs hit the response cache and cost nothing, and `offerprinter rank` triages a whole shortlist for pence.
495
747
  <!-- End AEO Capsule -->
496
748
 
497
749
  ### How long does a run take?
@@ -516,8 +768,8 @@ That depends entirely on the LLM provider you choose, so check their API data po
516
768
 
517
769
  ## How can I contribute to OfferPrinter?
518
770
 
519
- <!-- AEO Answer Capsule — 57 words -->
520
- Issues and pull requests are welcome — see CONTRIBUTING for the full guide. Run `ruff check .`, `ruff format .` and `pytest` before pushing, since CI enforces all three across Python 3.11, 3.12 and 3.13. Good first contributions include a new LLM provider subclass, a new output format, or better job description extraction from hostile job boards.
771
+ <!-- AEO Answer Capsule — 54 words -->
772
+ Issues and pull requests are welcome — see CONTRIBUTING for the full guide. Run `ruff check .`, `ruff format .`, `pytest` and `python scripts/run_evals.py --offline` before pushing, since CI enforces all four. Good first contributions include a new LLM provider subclass, a new output format, or better job advert extraction from hostile job boards.
521
773
  <!-- End AEO Capsule -->
522
774
 
523
775
  Especially wanted:
@@ -526,6 +778,8 @@ Especially wanted:
526
778
  - 🌍 **Better JD extraction** — job boards are hostile; make the fetcher smarter.
527
779
  - 📄 **New output formats** — LaTeX, plain-text ATS mode, ODT.
528
780
  - 🧪 **Tests** — the suite is offline and runs in under a second; keep it that way.
781
+ - 📊 **Eval cases** — add a CV/JD pair to [`evals/cases/`](evals/) that catches a
782
+ failure mode the suite misses. Synthetic and anonymised only.
529
783
 
530
784
  Read [CONTRIBUTING.md](CONTRIBUTING.md) · [SECURITY.md](SECURITY.md) · [CHANGELOG.md](CHANGELOG.md) · [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)
531
785