applyfit
by bayuh85
README.md
# applyfit
Tailoring a CV by hand for every posting is slow, and pasting your work history into a random chat box leaves you with no record of what you sent. Applyfit keeps the profile, the posting, the fit score, and the drafts as plain files on your machine. A model is called only when you ask for one.
## What applyfit does
You keep a profile (skills, roles, salary floor, location rules) and one folder per application. From there the loop is:
1. **add** a posting from a file or a URL.
2. **score** it from 0 to 100, with must-haves met and missing, nice-to-haves, red flags, and deal-breakers.
3. **tailor** a CV that reorders your existing bullets and names the gaps.
4. **letter** a short cover letter.
5. **prep** an interview sheet: likely questions, STAR prompts tied to real bullets, and questions to ask them.
6. **board** a status table (`saved`, `applied`, `interview`, `offer`, `rejected`).
The offline score needs no API key. It is a deterministic keyword-overlap baseline: requirement bullets are checked against your skills and the words in your profile, then mixed with salary, remote, and deal-breaker rules. If a deal-breaker in your preferences shows up in the posting, the score is capped at 25. `score --llm` asks a model for a second opinion and still prints the offline baseline beside it.
`tailor`, `letter`, and `prep` use the model when a key is set. Without a key they write an offline draft that only copies and reorders facts already in the profile. The model prompt tells it to never fabricate experience and to flag gaps instead.
| Command | What it writes |
| --- | --- |
| `applyfit init` | `.applyfit/profile.yaml` |
| `applyfit add <file-or-url>` | `.applyfit/applications/<id>/posting.md` |
| `applyfit score <id>` | `score.md` and `score.json` |
| `applyfit tailor <id>` | `cv.md` |
| `applyfit letter <id>` | `letter.md` |
| `applyfit prep <id>` | `prep.md` |
| `applyfit status <id> <state>` | updates `record.json` |
| `applyfit board` | prints the table |
| `applyfit mcp` | MCP stdio server |
## Quickstart
From a checkout of this repo, on Python 3.10 or newer:
```bash
python -m venv .venv
source .venv/bin/activate
pip install -e .
applyfit init --profile examples/profile.yaml
applyfit add examples/postings/northline-platform.md
applyfit add examples/postings/bramble-mobile.md
applyfit score northline-platform
applyfit score bramble-mobile
applyfit board
```
Northline is a remote platform role that lines up with the sample profile. Bramble is an on-site mobile contract under the salary floor, with deal-breakers called out in the breakdown. Both scores run offline. No network and no key.
Offline drafts of the other artifacts:
```bash
applyfit tailor northline-platform --offline
applyfit letter northline-platform --offline
applyfit prep northline-platform --offline
applyfit status northline-platform applied
applyfit show northline-platform
```
To use a model, copy the env file, set a real key, and drop `--offline`:
```bash
cp .env.example .env
# edit ATMOROUTER_API_KEY
applyfit score northline-platform --llm
applyfit tailor northline-platform
applyfit letter northline-platform
applyfit prep northline-platform
```
Each model call prints a cost line on stderr: tokens in, tokens out, and an estimated dollar amount.
## Powered by AtmoRouter
A job search is a pile of short calls: one score, one CV, one letter, one prep sheet, then the same four again for the next posting. A pay-per-token cheap AI API gateway fits that pattern. You are not keeping a seat warm on a monthly plan for a handful of paragraphs.
Applyfit's default provider is [AtmoRouter](https://atmorouter.dev), an OpenRouter alternative. The OpenAI-compatible base is `https://atmorouter.dev/v1`. Set `APPLYFIT_API_STYLE=anthropic` and the same client calls the Messages API at `https://atmorouter.dev/v1/messages`. One `ar-` key reaches the catalog, including the default model `atmo/deepseek-v4.1-flash`. The base URL, key, and model are all overridable, so any other OpenAI-compatible endpoint works the same way.
After every call, applyfit estimates dollars from a small built-in price table (USD per million tokens). Override a model in `.applyfit/pricing.yaml` when the catalog moves:
```yaml
default:
input: 0.50
output: 1.50
models:
atmo/deepseek-v4.1-flash:
input: 0.15
output: 0.60
```
Treat the printed figure as a budget check. Confirm live rates at https://atmorouter.dev/models.
Credit: Applyfit uses [AtmoRouter](https://atmorouter.dev) as its default LLM gateway.
## Configuration
Environment variables override `.env` in the current directory. The shell wins over `.env`.
| Variable | Default | Purpose |
| --- | --- | --- |
| `ATMOROUTER_API_KEY` | empty | Bearer key. `APPLYFIT_API_KEY` and `OPENAI_API_KEY` are fallbacks. |
| `APPLYFIT_BASE_URL` | `https://atmorouter.dev/v1` | OpenAI-compatible base URL. |
| `APPLYFIT_API_STYLE` | `openai` | `openai` or `anthropic`. |
| `APPLYFIT_MODEL` | `atmo/deepseek-v4.1-flash` | Model id sent on every call. |
| `APPLYFIT_HOME` | `.applyfit` | Workspace directory. |
| `APPLYFIT_TIMEOUT` | `60` | HTTP timeout in seconds. |
| `APPLYFIT_PRICE_INPUT_PER_MILLION` | table | Override the input price for every model. |
| `APPLYFIT_PRICE_OUTPUT_PER_MILLION` | table | Override the output price for every model. |
`applyfit config` prints the resolved base URL, style, model, and a masked key.
Profile shape (also written by `applyfit init`):
```yaml
name: Your Name
headline: The work you want
location: City, Country
salary:
floor: 150000
currency: USD
period: year
preferences:
remote: required # required, preferred, office-ok, office-only
locations:
allow: [Remote, United States]
deny: []
strict: false # when true, an unmatched location line is a red flag
employment: [full-time]
deal_breakers:
- unpaid trial
red_flags:
- relocation required
skills:
- name: Python
level: expert
years: 8
aliases: []
experience:
- company: Example
title: Engineer
start: 2021-04
end: null
bullets:
- A fact you can defend in an interview.
```
An application folder looks like this:
```text
.applyfit/applications/northline-platform/
posting.md
record.json
score.md
score.json
cv.md
letter.md
prep.md
```
`record.json` stores status, the last score, and every model call's token counts. The files are plain markdown and JSON, so you can diff them. `.applyfit/` is gitignored by default so a real profile is not committed by accident. Remove that line if you want the board in git.
Offline score weights, renormalized over the parts the posting actually has:
- 0.60 must-have coverage
- 0.10 nice-to-have coverage
- 0.20 keyword overlap (share of requirement terms that already appear in the profile)
- 0.10 preferences (each red flag subtracts 0.20 from this part)
Deal-breakers cap the final number at 25. A bullet counts as met when a skill name or alias appears in it, or when at least 60% of its distinctive words appear in the profile.
## Using it from Claude Code / Cursor
`applyfit mcp` speaks MCP over stdio (newline-delimited JSON-RPC, plus Content-Length frames). Tools: `add_job`, `score_job`, `tailor_cv`, `draft_letter`, `prep_interview`, `list_applications`, `set_status`, `show_profile`.
Claude Code (`.mcp.json`) or Cursor (MCP settings):
```json
{
"mcpServers": {
"applyfit": {
"command": "applyfit",
"args": ["mcp"],
"env": {
"APPLYFIT_HOME": "/absolute/path/to/.applyfit",
"ATMOROUTER_API_KEY": "ar-your-key-here",
"APPLYFIT_MODEL": "atmo/deepseek-v4.1-flash"
}
}
}
}
```
Point `APPLYFIT_HOME` at the workspace you already use from the CLI so the agent and the terminal share one board. Leave the key unset if you only want the offline tools.
## Privacy
The profile, postings, scores, and drafts stay in the workspace directory on the machine where you run the command. Offline `score`, `tailor`, `letter`, and `prep` do not open a network connection. `add` fetches a URL only when you pass one. LLM commands send the profile fact sheet and the posting text to whatever host `APPLYFIT_BASE_URL` names, and nothing else. There is no account, no telemetry, and the API key is read from the environment and never written into `record.json`.
## License
MIT. See [LICENSE](LICENSE).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues