Skip to main content
Glama
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).