# commitcraft

AI commit messages that follow your repo's conventions. `commitcraft` reads your staged diff, does the heavy lifting deterministically (filtering lockfiles and binaries, ranking hunks, fitting a token budget, learning your history's style) and then asks Claude Haiku 5.5 for one small structured answer, which it validates and formats locally into a Conventional Commits message.

## Features

- Parses `git diff --staged` into files and hunks; lockfiles, generated and binary files become one-line summaries (configurable globs).
- Ranks hunks by information (new functions/classes, signature changes, deleted public symbols; Python, Go, Rust, JS/TS and a generic fallback) and fits them into a token budget with `… N more lines` elisions.
- Infers scope candidates from paths (monorepo `packages/<name>`, top-level module).
- Learns style from the last 50 commits: conventional or not, common types and scopes, capitalization, ticket ids from the branch name.
- Structured JSON output (type, scope, subject, body, breaking, footer), then local validation: 72-char header, wrapped body, `BREAKING CHANGE:` footer and `!`, past-tense subjects auto-fixed to imperative ("added" becomes "add").
- `-c` to commit through your editor, `--hook install` for a `prepare-commit-msg` hook, `--alternatives N`, `--dry-run`, `--lang`, `--max-diff-tokens`.
- Responses cached on disk by hash of (model, prompt); `--verbose` shows tokens and cost.

## Install

```sh
uv venv .venv && uv pip install -p .venv/bin/python -e '.[dev]'
export ANTHROPIC_API_KEY=sk-ant-...   # or: ant auth login
```

Credentials come only from the environment. Without them, commitcraft prints setup instructions; `--dry-run` never needs a key.

## Quickstart

```sh
git add -p
commitcraft              # print a message
commitcraft -c           # edit in $EDITOR, then commit
commitcraft -n 3         # three alternatives
commitcraft --hook install
```

## Usage

```
commitcraft [-c] [-n 1-5] [--dry-run] [--lang TEXT] [--max-diff-tokens N]
            [--model ID] [--no-cache] [-v] [--hook install|uninstall [--force]]
```

`--dry-run` output (real, from `examples/demo.sh --dry-run`, a throwaway repo with a lockfile change, a new binary and a new function; the fixed system prompt is omitted here):

```
Number of alternatives: 1
Language: English
Allowed types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
Scope candidates inferred from paths: api
Branch: feature/ABC-123-users
Repository style: 100% of the last 3 commits are Conventional Commits; common types: fix, docs, feat; common scopes: web, api; subjects start lowercase; branch ticket: ABC-123

Staged changes:
### packages/api/server.py (modified, +3 -0)
@@ -1,2 +1,5 @@
 def health():
     return "ok"
+
+def list_users(limit=50):
+    return []
### Skipped files
package-lock.json: generated/lock file modified (+1 -1), content omitted
packages/web/logo.png: binary file added

--- estimate ---
model: claude-haiku-5-5
estimated input tokens: ~366
hunks omitted: 0, skipped files: 2
```

Example of a generated message (needs a real key; shape shown from the formatter, not a captured live call):

```
feat(api): add list_users endpoint

Clients need to page through users instead of fetching them one by one;
limit defaults to 50 to keep responses small.

Refs: ABC-123
```

### Hook

`commitcraft --hook install` writes `.git/hooks/prepare-commit-msg`. It fills the message only when it is empty (ignoring comments) and the commit is not a merge, squash, amend, `-m` or `-F`/template commit. Failures never block the commit. Installing twice is a no-op; a foreign hook is left alone unless `--force` (it is backed up to `.bak`). `--hook uninstall` removes it.

## Configuration

`.commitcraft.toml` at the repo root (see `examples/.commitcraft.toml`):

| key | meaning |
| --- | --- |
| `ignore` | extra globs summarized instead of sent (added to the defaults) |
| `types` | allowed Conventional Commits types |
| `scopes` | allowed scopes; anything else is dropped with a warning |
| `ticket_pattern` | regex for ticket ids in the branch name; adds a `Refs:` footer |
| `max_diff_tokens` | default diff budget (6000) |

Environment: `ANTHROPIC_API_KEY`, `COMMITCRAFT_MODEL`, `COMMITCRAFT_CACHE_DIR` (default `~/.cache/commitcraft`).

## Cost

Haiku 5.5 costs $0.10 per 1M input and $0.50 per 1M output tokens. A typical commit sends 500 to 3000 input tokens and gets about 150 back, so roughly $0.0001 to $0.0004 per message (the 6000-token cap bounds the worst case near $0.0007). Repeating a command on the same staged diff is free thanks to the cache.

## Architecture

| module | role |
| --- | --- |
| `diff.py` | unified diff parser, ignore globs, hunk scoring |
| `budget.py` | filtering, summaries, budget fitting, elision |
| `scope.py` | scope candidates from paths |
| `style.py` | history/branch style learning |
| `config.py` | `.commitcraft.toml` |
| `llm.py` | prompt, structured call, disk cache, cost |
| `message.py` | validation and formatting |
| `hook.py` | hook install/uninstall/fill rules |
| `git.py`, `cli.py` | git plumbing and the click CLI |

See `docs/design.md` for the algorithms.

## Tests

```sh
.venv/bin/pytest
```

All model calls use an injected fake client. Live API calls were not exercised when this was written (no key available).

## Limitations

- Token counts are a 4 chars/token estimate.
- Symbol detection is regex based and line oriented.
- The API call needs network and a key; there is no offline fallback.
- Staged diff only; `commit --amend` messages are not regenerated.
