@synmux/claude-commit 1.0.2 → 1.0.4
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.
- package/CHANGELOG.md +204 -0
- package/README.md +242 -6
- package/index.ts +33 -2
- package/package.json +32 -18
- package/src/agent.ts +36 -43
- package/src/cli.ts +137 -12
- package/src/config.ts +91 -6
- package/src/diff.ts +348 -14
- package/src/generate.ts +295 -49
- package/src/git.ts +38 -4
- package/src/models.ts +95 -0
- package/src/ollama.ts +502 -0
- package/src/paths.ts +139 -0
- package/src/prompts.ts +192 -28
- package/src/tokens.ts +47 -9
- package/src/types.ts +147 -3
- package/src/ui/spinner.ts +1 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `claude-commit` are recorded here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project
|
|
5
|
+
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
Each version also has a [GitHub release](https://github.com/synmux/claude-commit/releases)
|
|
8
|
+
carrying the same notes at greater length.
|
|
9
|
+
|
|
10
|
+
## [1.0.4] - 2026-09-11
|
|
11
|
+
|
|
12
|
+
### [1.0.4] - Added
|
|
13
|
+
|
|
14
|
+
- **Filenames-only mode.** Set `"filenamesOnly": true` in your config, or
|
|
15
|
+
pass `-f` / `--filenames-only`, to skip the summariser and send only
|
|
16
|
+
filenames to the final model. This reduces model work at the cost of
|
|
17
|
+
broader, less useful messages; the default remains `false`.
|
|
18
|
+
- Filename lists retain `ignore` filtering and `lowPriorityPaths` weighting,
|
|
19
|
+
including both paths of renames and copies. Formatting, templates, custom
|
|
20
|
+
instructions and interactive options still apply; the model is instructed
|
|
21
|
+
to avoid inventing specific edits or motivations from filenames alone.
|
|
22
|
+
- Verbose output identifies when the summariser was skipped. In this mode,
|
|
23
|
+
library results contain an empty `summaries` array and `chunkCount: 0`,
|
|
24
|
+
and the summary model is never called or preloaded.
|
|
25
|
+
|
|
26
|
+
## [1.0.3] - 2026-09-11
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **Ollama models.** Any model can run on a local or self-hosted
|
|
31
|
+
[Ollama](https://ollama.com) server by prefixing its name with `ollama:`;
|
|
32
|
+
everything after the prefix is the Ollama model name verbatim, tag
|
|
33
|
+
included. The two pipeline stages resolve independently, so the diff can be
|
|
34
|
+
summarised locally and free while the final message still goes to Claude.
|
|
35
|
+
`cco` uses the native `/api/chat` endpoint rather than either compatibility
|
|
36
|
+
layer, because only the native API can set a context length.
|
|
37
|
+
- **`ollama.context`**, defaulting to `"auto"`. Ollama truncates an oversized
|
|
38
|
+
prompt silently - HTTP 200, oldest content dropped, nothing in the response
|
|
39
|
+
to say so - so `cco` never lets the window stay implicit: it preloads the
|
|
40
|
+
model with no window set, reads the server's own VRAM-based choice back from
|
|
41
|
+
`/api/ps`, pins that number on every request, sizes chunks against it, and
|
|
42
|
+
checks the returned token counts to catch a truncation that happened anyway.
|
|
43
|
+
A number pins the window instead.
|
|
44
|
+
- **`ollama.host`** (defaults to `$OLLAMA_HOST`, then `http://localhost:11434`)
|
|
45
|
+
and **`ollama.keepAlive`**, plus the `--ollama-host` and `--ollama-context`
|
|
46
|
+
flags.
|
|
47
|
+
- **`lowPriorityPaths`.** Gitignore-style patterns for churn-heavy paths whose
|
|
48
|
+
changes are summarised separately, so they cannot crowd out the code in the
|
|
49
|
+
final message. The content is still read and still costs the same; only its
|
|
50
|
+
weight changes. `--no-low-priority-paths` disables it for one run.
|
|
51
|
+
- **`ignore`.** The same pattern syntax, but matching file sections are removed
|
|
52
|
+
from the diff before anything else looks at it - before the low-priority
|
|
53
|
+
partition, before chunking, before any model call. The files are still
|
|
54
|
+
committed; `ignore` governs what the model reads, never what git stages. When
|
|
55
|
+
a pattern matches everything, `cco` stops with an error naming the directive
|
|
56
|
+
rather than inventing a message about changes you told it not to read.
|
|
57
|
+
`--no-ignore` disables it for one run.
|
|
58
|
+
|
|
59
|
+
### Changed
|
|
60
|
+
|
|
61
|
+
- The default spinner is now `material`.
|
|
62
|
+
- `@opentui/core` bumped to 0.5.7, alongside SDK and skill dependency bumps.
|
|
63
|
+
- CI workflows bump `checkout`, `setup-node` and `claude-code-action`.
|
|
64
|
+
|
|
65
|
+
## [1.0.2] - 2026-07-27
|
|
66
|
+
|
|
67
|
+
### [1.0.2] - Changed
|
|
68
|
+
|
|
69
|
+
- The default spinner is now `dwarfFortress` instead of `bouncingBall`.
|
|
70
|
+
|
|
71
|
+
## [1.0.1] - 2026-07-27
|
|
72
|
+
|
|
73
|
+
### [1.0.1] - Added
|
|
74
|
+
|
|
75
|
+
- **A configurable spinner.** The `spinner` config key accepts any name from
|
|
76
|
+
the [cli-spinners](https://github.com/sindresorhus/cli-spinners) set bundled
|
|
77
|
+
with [ora](https://github.com/sindresorhus/ora). Unknown names fall back to
|
|
78
|
+
the default rather than throwing, because a cosmetic option must never be
|
|
79
|
+
able to break a commit.
|
|
80
|
+
|
|
81
|
+
### [1.0.1] - Changed
|
|
82
|
+
|
|
83
|
+
- The progress spinner is an ora instance rather than a hand-rolled frame
|
|
84
|
+
timer. Enablement is still decided solely by the existing TTY and
|
|
85
|
+
`--no-spinner` checks - ora's own CI auto-detection is bypassed - and a final
|
|
86
|
+
success or failure line still prints when the animation is disabled.
|
|
87
|
+
|
|
88
|
+
## [1.0.0] - 2026-07-24
|
|
89
|
+
|
|
90
|
+
### [1.0.0] - Added
|
|
91
|
+
|
|
92
|
+
- **`--skip-armored`** and the matching `skipArmored` config key, replacing
|
|
93
|
+
each run of armoured or encoded lines with a short marker. Runs of one or two
|
|
94
|
+
lines survive, because a lone URL or hash is content. Ciphertext is
|
|
95
|
+
unreadable to the model and re-encrypts nondeterministically on every
|
|
96
|
+
`chezmoi re-add`, so this is the recommended mode for repos holding encrypted
|
|
97
|
+
files.
|
|
98
|
+
|
|
99
|
+
### [1.0.0] - Fixed
|
|
100
|
+
|
|
101
|
+
- **Chunks are sized by real token density.** `Prompt is too long` returned,
|
|
102
|
+
and the isolation fix in 0.1.3 turned out to have treated a symptom. Armoured
|
|
103
|
+
and base64 content tokenises at roughly 1.14 chars per token on current
|
|
104
|
+
Claude models, while chunk budgets assumed the configured `charsPerToken` of
|
|
105
|
+
3.5: a 1,247,318-character diff was estimated at ~356k tokens and really
|
|
106
|
+
counted ~1.19M. Long unbroken runs are now priced at their own, much denser
|
|
107
|
+
ratio. A third-party "real tokeniser" would not fix this class of bug -
|
|
108
|
+
tiktoken-style vocabularies compress base64 about three times better than
|
|
109
|
+
Claude's actual tokeniser, so they underestimate the same way.
|
|
110
|
+
- **The summary stage is a work queue.** If the backend rejects a chunk anyway
|
|
111
|
+
(its rejection is free, unbilled, and the only authoritative count), the
|
|
112
|
+
budget is halved, that chunk is re-split, and processing continues in place.
|
|
113
|
+
|
|
114
|
+
### [1.0.0] - Changed
|
|
115
|
+
|
|
116
|
+
- The 0.1.3 documentation blaming these failures on MCP/skill leakage is
|
|
117
|
+
corrected. That isolation stays as hygiene and cost control, but density was
|
|
118
|
+
the bug.
|
|
119
|
+
|
|
120
|
+
## [0.1.4] - 2026-07-23
|
|
121
|
+
|
|
122
|
+
### [0.1.4] - Fixed
|
|
123
|
+
|
|
124
|
+
- `cco --version` reported a hardcoded string rather than the installed package
|
|
125
|
+
version.
|
|
126
|
+
|
|
127
|
+
## [0.1.3] - 2026-07-23
|
|
128
|
+
|
|
129
|
+
### [0.1.3] - Fixed
|
|
130
|
+
|
|
131
|
+
- **Requests no longer inherit your global Claude Code context.** Runs failed
|
|
132
|
+
reporting ~1,149k tokens against a conversation of only ~301k; the missing
|
|
133
|
+
~848k were MCP tool definitions and skills pulled from the user's global
|
|
134
|
+
configuration into what should be an isolated prompt-in, text-out request.
|
|
135
|
+
The Agent SDK gates each context source separately, and `settingSources: []`
|
|
136
|
+
disables only settings files and `CLAUDE.md` - MCP servers and plugins load
|
|
137
|
+
regardless, and the CLI performs skill discovery even when the `skills`
|
|
138
|
+
option is omitted. Every switch is now set explicitly in one tested
|
|
139
|
+
`buildQueryOptions()`.
|
|
140
|
+
- **The chunk budget is clamped to the summary model's context window.**
|
|
141
|
+
`maxChunkTokens` was converted straight into a character budget regardless of
|
|
142
|
+
the configured model, so on any 200k-context model a single chunk could
|
|
143
|
+
exceed the whole window. It is now a cap rather than a promise, clamped to
|
|
144
|
+
the model's window minus a 32k reserve.
|
|
145
|
+
|
|
146
|
+
### [0.1.3] - Added
|
|
147
|
+
|
|
148
|
+
- npm publishing on version tags via CI.
|
|
149
|
+
|
|
150
|
+
## [0.1.2] - 2026-07-09
|
|
151
|
+
|
|
152
|
+
### [0.1.2] - Changed
|
|
153
|
+
|
|
154
|
+
- **Both pipeline stages default to `sonnet`.** The final stage's input is a
|
|
155
|
+
handful of summaries and its output is the entire point of the tool, so a
|
|
156
|
+
strong model there costs almost nothing and writes a visibly better message.
|
|
157
|
+
|
|
158
|
+
### [0.1.2] - Added
|
|
159
|
+
|
|
160
|
+
- MIT licence.
|
|
161
|
+
|
|
162
|
+
## [0.1.1] - 2026-07-02
|
|
163
|
+
|
|
164
|
+
### [0.1.1] - Changed
|
|
165
|
+
|
|
166
|
+
- Minor tweaks.
|
|
167
|
+
|
|
168
|
+
## [0.1.0] - 2026-07-02
|
|
169
|
+
|
|
170
|
+
The first release, published as `@synmux/claude-commit` and providing the `cco`
|
|
171
|
+
and `claude-commit` binaries.
|
|
172
|
+
|
|
173
|
+
### [0.1.0] - Added
|
|
174
|
+
|
|
175
|
+
- **The two-stage pipeline.** The diff is split into chunks that fit the
|
|
176
|
+
context window, each chunk is summarised, and the summaries are handed back
|
|
177
|
+
to a model to write the final message. Diffs too large for a single context
|
|
178
|
+
window simply produce more chunks.
|
|
179
|
+
- **Message formatting.** Conventional Commits (`-c`), gitmoji (`-g`),
|
|
180
|
+
multi-line subject and body (`-m`), a first-line template (`-t`), and
|
|
181
|
+
free-form extra instructions (`-p`).
|
|
182
|
+
- **Interactive mode.** `cco -i` lists candidate messages in a TUI, generated
|
|
183
|
+
at a higher temperature for variety, with the scrollable diff alongside.
|
|
184
|
+
- **Layered configuration.** Built-in defaults, a global user config, a
|
|
185
|
+
`claude-commit` key in the repo's `package.json`, the nearest dotted config
|
|
186
|
+
file, then CLI flags.
|
|
187
|
+
- **Structured output.** The final message is requested as JSON against a
|
|
188
|
+
`{ messages: string[] }` schema rather than parsed out of free-form text,
|
|
189
|
+
degrading to a temperature-free request and then to delimiter parsing.
|
|
190
|
+
- **`allowApiKey`, defaulting to false.** An exported `ANTHROPIC_API_KEY` would
|
|
191
|
+
otherwise silently switch every generation from subscription auth to
|
|
192
|
+
pay-as-you-go billing, because the SDK subprocess inherits `process.env`.
|
|
193
|
+
Those credentials are stripped unless you opt in.
|
|
194
|
+
|
|
195
|
+
[1.0.4]: https://github.com/synmux/claude-commit/compare/1.0.3...1.0.4
|
|
196
|
+
[1.0.3]: https://github.com/synmux/claude-commit/compare/1.0.2...1.0.3
|
|
197
|
+
[1.0.2]: https://github.com/synmux/claude-commit/compare/1.0.1...1.0.2
|
|
198
|
+
[1.0.1]: https://github.com/synmux/claude-commit/compare/1.0.0...1.0.1
|
|
199
|
+
[1.0.0]: https://github.com/synmux/claude-commit/compare/0.1.4...1.0.0
|
|
200
|
+
[0.1.4]: https://github.com/synmux/claude-commit/compare/0.1.3...0.1.4
|
|
201
|
+
[0.1.3]: https://github.com/synmux/claude-commit/compare/0.1.2...0.1.3
|
|
202
|
+
[0.1.2]: https://github.com/synmux/claude-commit/compare/0.1.1...0.1.2
|
|
203
|
+
[0.1.1]: https://github.com/synmux/claude-commit/compare/0.1.0...0.1.1
|
|
204
|
+
[0.1.0]: https://github.com/synmux/claude-commit/releases/tag/0.1.0
|
package/README.md
CHANGED
|
@@ -20,18 +20,27 @@ feat(auth): add error handling and refresh token rotation to login
|
|
|
20
20
|
Want more details? See [WALKTHROUGH.md](WALKTHROUGH.md).
|
|
21
21
|
|
|
22
22
|
```plaintext
|
|
23
|
-
staged diff ──split──▶ [chunk, …] ──sonnet──▶ summaries ──sonnet──▶ commit message
|
|
23
|
+
staged diff ──ignore──▶ ──split──▶ [chunk, …] ──sonnet──▶ summaries ──sonnet──▶ commit message
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
1. **Summarize** - the diff is split into chunks that fit the context window and
|
|
27
27
|
each chunk is summarized by a strong model (`sonnet`, which carries a native
|
|
28
28
|
1M-token context). Diffs larger than 1M tokens simply produce more chunks.
|
|
29
|
+
Paths listed in [`ignore`](#ignoring-paths-entirely) are dropped first and
|
|
30
|
+
never read at all; changes under configured
|
|
31
|
+
[low-priority paths](#low-priority-paths) are summarized separately, so churn
|
|
32
|
+
cannot crowd out the code.
|
|
29
33
|
2. **Write** - the summaries are handed to the same model (`sonnet`) to write the
|
|
30
34
|
final commit message according to your formatting rules. The message is the
|
|
31
35
|
whole point of the tool, and its input is tiny, so a strong model here costs
|
|
32
36
|
almost nothing extra.
|
|
33
37
|
|
|
34
|
-
|
|
38
|
+
Either stage can run on a local [Ollama](#ollama-models) model instead; by
|
|
39
|
+
default both go through the
|
|
40
|
+
[Claude Agent SDK](https://code.claude.com/docs/en/agent-sdk/overview).
|
|
41
|
+
|
|
42
|
+
For less model work at the cost of less useful messages, enable
|
|
43
|
+
[`filenamesOnly`](#filenames-only-mode) to skip summarisation entirely.
|
|
35
44
|
|
|
36
45
|
## Install
|
|
37
46
|
|
|
@@ -64,6 +73,9 @@ instead (pay-as-you-go), opt in explicitly in your configuration:
|
|
|
64
73
|
{ "allowApiKey": true }
|
|
65
74
|
```
|
|
66
75
|
|
|
76
|
+
Ollama models sit outside all of this: they run on a server you control, over
|
|
77
|
+
plain HTTP, with no credential at all. See [Ollama models](#ollama-models).
|
|
78
|
+
|
|
67
79
|
## Usage
|
|
68
80
|
|
|
69
81
|
```sh
|
|
@@ -86,8 +98,14 @@ and asks for confirmation before committing. Pass `-y` to skip the prompt, or
|
|
|
86
98
|
| `-m, --multiline` / `--no-multiline` | Write a multi-line commit (subject + body), or force a single line |
|
|
87
99
|
| `-t, --template <tpl>` | Template for the first line, e.g. `"[PROJ-1] {message}"` |
|
|
88
100
|
| `-p, --prompt <text>` | Extra instructions appended to the prompt |
|
|
101
|
+
| `-f, --filenames-only` | Skip summarisation and send only filenames to the final model |
|
|
89
102
|
| `--model-summary <model>` | Model used to summarize the diff (default `sonnet`) |
|
|
90
103
|
| `--model-final <model>` | Model used to write the message (default `sonnet`) |
|
|
104
|
+
| `--skip-armored` | Omit armored/encoded lines (age/gpg armor, base64 blobs) from the summarized diff |
|
|
105
|
+
| `--no-low-priority-paths` | Ignore `lowPriorityPaths` for this run, so every change weighs the same |
|
|
106
|
+
| `--no-ignore` | Disregard `ignore` for this run, so every staged change is read |
|
|
107
|
+
| `--ollama-host <url>` | Base URL of the Ollama server for `ollama:` models |
|
|
108
|
+
| `--ollama-context <tokens>` | Context window requested from Ollama models |
|
|
91
109
|
| `-d, --dry-run` | Print the message to stdout without committing |
|
|
92
110
|
| `-y, --yes` | Commit without asking for confirmation |
|
|
93
111
|
| `--no-spinner` | Disable the progress spinner |
|
|
@@ -101,6 +119,7 @@ cco # generate, confirm, and commit staged changes
|
|
|
101
119
|
cco -a -c # stage everything and write a Conventional Commit
|
|
102
120
|
cco -c -g -m # conventional + gitmoji + a body
|
|
103
121
|
cco -i -n 5 # pick from 5 options interactively
|
|
122
|
+
cco -f --dry-run # generate from filenames only, without committing
|
|
104
123
|
cco --dry-run | cat # print a message without committing (TUI-free, pipe-safe)
|
|
105
124
|
git commit -F <(cco -d) # use the message with your own git invocation
|
|
106
125
|
```
|
|
@@ -108,6 +127,31 @@ git commit -F <(cco -d) # use the message with your own git invocation
|
|
|
108
127
|
In a pipe (no TTY) there is no spinner and no confirmation prompt - `cco` just
|
|
109
128
|
generates and commits (or prints, with `--dry-run`).
|
|
110
129
|
|
|
130
|
+
## Filenames-only mode
|
|
131
|
+
|
|
132
|
+
Set `"filenamesOnly": true` in any config layer, or pass `-f` /
|
|
133
|
+
`--filenames-only`, to send only the list of filenames touched by staged
|
|
134
|
+
changes to `models.final`. The default is `false`.
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{ "filenamesOnly": true }
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The summariser is skipped entirely: no diff chunks, summary calls, or
|
|
141
|
+
summary-model preload. The final model receives no file contents or diff
|
|
142
|
+
hunks, so expect broader, less useful messages. It is instructed to describe
|
|
143
|
+
the affected areas without inventing specific edits or reasons for them.
|
|
144
|
+
Normal formatting, custom instructions and interactive options still apply.
|
|
145
|
+
|
|
146
|
+
`ignore` still removes matching file sections, and `lowPriorityPaths` still
|
|
147
|
+
groups and weights the remaining filenames. Renames and copies include both
|
|
148
|
+
paths; additions, deletions, binary files and mode changes are included.
|
|
149
|
+
If every staged file is ignored, generation still stops with an error.
|
|
150
|
+
|
|
151
|
+
`models.summary`, `maxChunkTokens`, `charsPerToken` and `skipArmored` have no
|
|
152
|
+
effect in this mode. `--verbose` reports that the summariser was skipped;
|
|
153
|
+
library results have an empty `summaries` array and `chunkCount: 0`.
|
|
154
|
+
|
|
111
155
|
## Interactive mode
|
|
112
156
|
|
|
113
157
|
`cco -i` opens a TUI listing several candidate messages to choose from. The
|
|
@@ -151,14 +195,22 @@ keys are valid at every level:
|
|
|
151
195
|
"interactive": true,
|
|
152
196
|
"interactiveCount": 3,
|
|
153
197
|
"interactiveTemperature": 1,
|
|
154
|
-
"spinner": "
|
|
198
|
+
"spinner": "material",
|
|
155
199
|
"models": {
|
|
156
200
|
"summary": "sonnet",
|
|
157
201
|
"final": "sonnet"
|
|
158
202
|
},
|
|
159
203
|
"maxChunkTokens": 600000,
|
|
160
204
|
"charsPerToken": 3.5,
|
|
205
|
+
"filenamesOnly": false,
|
|
161
206
|
"skipArmored": false,
|
|
207
|
+
"lowPriorityPaths": [],
|
|
208
|
+
"ignore": [],
|
|
209
|
+
"ollama": {
|
|
210
|
+
"host": "http://localhost:11434",
|
|
211
|
+
"context": "auto",
|
|
212
|
+
"keepAlive": null
|
|
213
|
+
},
|
|
162
214
|
"allowApiKey": false
|
|
163
215
|
}
|
|
164
216
|
```
|
|
@@ -166,11 +218,11 @@ keys are valid at every level:
|
|
|
166
218
|
`spinner` chooses the progress animation: any name from the
|
|
167
219
|
[cli-spinners](https://github.com/sindresorhus/cli-spinners) set bundled with
|
|
168
220
|
[ora](https://github.com/sindresorhus/ora) (`"dots"`, `"moon"`, `"pong"`,
|
|
169
|
-
`"
|
|
170
|
-
`
|
|
221
|
+
`"material"`, ...). Unknown names are ignored and the default
|
|
222
|
+
`material` is used. `--no-spinner` disables the animated spinner, but final
|
|
171
223
|
status lines still print.
|
|
172
224
|
|
|
173
|
-
The `
|
|
225
|
+
The `material` spinner is chosen because it's fucking cool. Fight me.
|
|
174
226
|
|
|
175
227
|
`maxChunkTokens` is a cap, not a promise: at run time it is clamped to the
|
|
176
228
|
summary model's context window minus a fixed reserve (1M-window models such as
|
|
@@ -195,6 +247,186 @@ every `chezmoi re-add` re-encrypts nondeterministically and produces megabytes
|
|
|
195
247
|
of churned armor. Drop a `.claude-commit.json` with `{ "skipArmored": true }`
|
|
196
248
|
in the repo root to enable it per-repo.
|
|
197
249
|
|
|
250
|
+
### Low-priority paths
|
|
251
|
+
|
|
252
|
+
Some paths change a lot without meaning much - generated docs, lockfiles,
|
|
253
|
+
vendored snapshots, build output. Left alone, a commit that touches twenty
|
|
254
|
+
lines of code and regenerates two thousand lines of tooling gets a subject
|
|
255
|
+
line about the tooling. `lowPriorityPaths` lists gitignore-style patterns for
|
|
256
|
+
those paths:
|
|
257
|
+
|
|
258
|
+
```json
|
|
259
|
+
{
|
|
260
|
+
"lowPriorityPaths": [".agents/skills/*-skilld", "bun.lock", "!bun.lock.keep"]
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Changes under matching paths are summarised separately and briefly, and the
|
|
265
|
+
model is told that the subject line - and the commit type, scope and gitmoji
|
|
266
|
+
where you use them - comes from the _other_ changes, however small they are.
|
|
267
|
+
The low-priority changes are mentioned in the subject only if they fit, and in
|
|
268
|
+
the body (with `multiline`) only after the primary changes. When _every_
|
|
269
|
+
changed file is low priority there is nothing for it to yield to, so the
|
|
270
|
+
changes are described normally, exactly as if no patterns were configured.
|
|
271
|
+
|
|
272
|
+
Pattern rules follow `.gitignore` conventions, so trunk or gitignore lines can
|
|
273
|
+
usually be copied in:
|
|
274
|
+
|
|
275
|
+
- A pattern with a `/` in it is anchored at the repository root and matches a
|
|
276
|
+
path or any directory above it - `.agents/skills/*-skilld` covers every file
|
|
277
|
+
inside each matching directory.
|
|
278
|
+
- A pattern without a `/` matches any path segment at any depth - `bun.lock`
|
|
279
|
+
matches `packages/app/bun.lock`; `*-skilld` matches everything inside any
|
|
280
|
+
`*-skilld` directory.
|
|
281
|
+
- `*` matches dotfiles and does not cross `/`; `**` does; `{a,b}` expands. A
|
|
282
|
+
leading `/` or `./` anchors, a trailing `/` is ignored. The anchoring
|
|
283
|
+
decision looks at the whole pattern, so a `/` inside a brace group anchors
|
|
284
|
+
all of its alternatives - prefer one pattern per intent.
|
|
285
|
+
- A leading `!` negates, and the last matching pattern wins:
|
|
286
|
+
`["docs/**", "!docs/adr/**"]` deprioritises docs except the ADRs.
|
|
287
|
+
- Patterns are always matched against repository-root-relative paths with
|
|
288
|
+
`/` separators, whichever directory you run `cco` from. A backslash in a
|
|
289
|
+
pattern is an escape (`\[`, `\{`, `\!` for the literal characters), so
|
|
290
|
+
Windows-style `dist\**` matches nothing.
|
|
291
|
+
- Patterns are not validated: a typo such as an unbalanced `{` is parsed
|
|
292
|
+
rather than rejected and may match something unexpected, so check the
|
|
293
|
+
`--verbose` match counts when you add one.
|
|
294
|
+
|
|
295
|
+
A rename into or out of a low-priority path counts as primary (both sides
|
|
296
|
+
must match). The nearest config layer that sets the key wins outright - lists
|
|
297
|
+
are never merged - so `"lowPriorityPaths": []` in a project opts out of a
|
|
298
|
+
global list, and a project that wants the global patterns plus its own must
|
|
299
|
+
repeat them. `--no-low-priority-paths` switches the feature off for one run,
|
|
300
|
+
which is handy when the churn _is_ the story, or for comparing messages while
|
|
301
|
+
tuning patterns.
|
|
302
|
+
|
|
303
|
+
This changes how changes are _weighted_ in the message, not how much of the
|
|
304
|
+
diff is read: low-priority content is still summarised in full, at the same
|
|
305
|
+
cost. To skip content outright, see `skipArmored`. Under `--verbose`, `cco`
|
|
306
|
+
reports how many files matched (`low-priority paths: matched 3 of 41 files`),
|
|
307
|
+
which is the only way to tell a pattern that matched nothing from one that
|
|
308
|
+
matched everything and was promoted.
|
|
309
|
+
|
|
310
|
+
### Ignoring paths entirely
|
|
311
|
+
|
|
312
|
+
`lowPriorityPaths` still reads everything it deprioritises, and pays for it.
|
|
313
|
+
Some content is worth neither the tokens nor the time: a vendored dependency
|
|
314
|
+
tree, a generated API client, a data fixture that changes wholesale.
|
|
315
|
+
|
|
316
|
+
`ignore` takes the same gitignore-style patterns and removes those file
|
|
317
|
+
sections from the diff **before anything else looks at it** - before the
|
|
318
|
+
low-priority partition, before chunking, before any model call:
|
|
319
|
+
|
|
320
|
+
```json
|
|
321
|
+
{ "ignore": ["vendor/**", "**/__snapshots__", "*.generated.ts"] }
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
The stages compose in the order their names suggest:
|
|
325
|
+
|
|
326
|
+
```text
|
|
327
|
+
diff ─ ignore ─▶ ─ skipArmored ─▶ ─ lowPriorityPaths ─▶ chunks ─▶ summaries
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Two things worth being clear about:
|
|
331
|
+
|
|
332
|
+
- **The files are still committed.** `ignore` governs what the model reads,
|
|
333
|
+
never what git stages. `cco` is writing a message, not choosing a changeset.
|
|
334
|
+
- **When it matches _everything_, `cco` stops** with an error naming the
|
|
335
|
+
directive, rather than inventing a message about changes you told it not to
|
|
336
|
+
read. This is deliberately unlike `lowPriorityPaths`, which promotes its
|
|
337
|
+
partition in the same situation - "this matters less" can degrade
|
|
338
|
+
gracefully, "do not look at this" has nothing to degrade to. Pass
|
|
339
|
+
`--no-ignore` for that one commit.
|
|
340
|
+
|
|
341
|
+
As with `lowPriorityPaths`, a section is dropped only when it names at least
|
|
342
|
+
one path and _all_ of them match, so a rename out of an ignored directory
|
|
343
|
+
survives. `--verbose` reports the count
|
|
344
|
+
(`ignore: dropped 3 of 41 files before reading`).
|
|
345
|
+
|
|
346
|
+
## Ollama models
|
|
347
|
+
|
|
348
|
+
Any model can be run on a local (or self-hosted) [Ollama](https://ollama.com)
|
|
349
|
+
server instead of Claude, by prefixing its name with `ollama:`. Everything
|
|
350
|
+
after the prefix is the Ollama model name **verbatim**, tag included:
|
|
351
|
+
|
|
352
|
+
```json
|
|
353
|
+
{
|
|
354
|
+
"models": {
|
|
355
|
+
"summary": "ollama:ornith-1.5:35b",
|
|
356
|
+
"final": "sonnet"
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
The two stages resolve independently, so that mixed setup is the interesting
|
|
362
|
+
one: reading the diff is the bulk of the work and the most sensitive thing
|
|
363
|
+
`cco` touches, so it runs locally and free, while the final message - one
|
|
364
|
+
short, quality-sensitive call on a summary - still goes to Claude. The prefix
|
|
365
|
+
works anywhere a model name does, including the flags:
|
|
366
|
+
|
|
367
|
+
```sh
|
|
368
|
+
cco --model-summary ollama:ornith-1.5:35b --dry-run -v
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
The server needs no credential. `cco` talks to Ollama's native `/api/chat`
|
|
372
|
+
endpoint, not either of its OpenAI/Anthropic compatibility layers, because
|
|
373
|
+
only the native API can set a context length.
|
|
374
|
+
|
|
375
|
+
### Context length is the setting that matters
|
|
376
|
+
|
|
377
|
+
Ollama picks a context window for each model from available VRAM (4k / 32k /
|
|
378
|
+
256k tiers, capped at the model's trained maximum), and a prompt that
|
|
379
|
+
exceeds it is truncated **silently** - HTTP 200, oldest content dropped,
|
|
380
|
+
nothing on the response to say so. A summary written from half a diff is
|
|
381
|
+
worse than no summary, so `cco` never lets that number stay implicit: it
|
|
382
|
+
sends an explicit window on every request, sizes its diff chunks against the
|
|
383
|
+
same number, and checks the token counts afterwards to catch a truncation
|
|
384
|
+
that happened anyway (in which case it re-splits the chunk and retries,
|
|
385
|
+
exactly as it does for a Claude context overflow).
|
|
386
|
+
|
|
387
|
+
Where the number comes from is `ollama.context`:
|
|
388
|
+
|
|
389
|
+
```json
|
|
390
|
+
{
|
|
391
|
+
"ollama": {
|
|
392
|
+
"host": "http://localhost:11434",
|
|
393
|
+
"context": "auto",
|
|
394
|
+
"keepAlive": "10m"
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
- `context` - `"auto"` (the default) **asks the server** rather than
|
|
400
|
+
guessing: the model is preloaded with no window set, so Ollama applies
|
|
401
|
+
its own VRAM-based choice, and `cco` reads that choice back from
|
|
402
|
+
`/api/ps` before sizing anything. That is the largest window Ollama
|
|
403
|
+
believes this machine can actually run - 131072 for a Gemma model on a
|
|
404
|
+
large Mac, 4096 for the same model on a small laptop - resolved once per
|
|
405
|
+
model per run, and shown under `--verbose`. A number pins the window
|
|
406
|
+
instead: lower it when memory is tight (usage scales with it, multiplied
|
|
407
|
+
by `OLLAMA_NUM_PARALLEL`), or raise it past the tier if you know your
|
|
408
|
+
hardware better than the server does. A smaller window is never a
|
|
409
|
+
correctness problem - `cco` just splits the diff into more chunks.
|
|
410
|
+
- `host` - defaults to `$OLLAMA_HOST`, then `http://localhost:11434`. A bare
|
|
411
|
+
`box.local:11434` gains an `http://`, matching Ollama's own convention.
|
|
412
|
+
- `keepAlive` - how long the server keeps the model loaded after a request: a
|
|
413
|
+
duration string (`"10m"`), seconds as a number, `0` to unload immediately,
|
|
414
|
+
or negative to pin it. `null` leaves the server's own default. Pinning is
|
|
415
|
+
worth it if you commit often; a 35b model takes a while to load.
|
|
416
|
+
|
|
417
|
+
### What differs from a Claude model
|
|
418
|
+
|
|
419
|
+
- **Cost is reported as zero**, because local inference is not billed. In a
|
|
420
|
+
mixed run, `--verbose`'s total is exactly the Claude half.
|
|
421
|
+
- **Structured output** is requested through Ollama's `format` field. A model
|
|
422
|
+
or server that cannot honour it (Ollama Cloud does not support it at all)
|
|
423
|
+
falls back to plain-text parsing automatically.
|
|
424
|
+
- **A missing model is an error, not a download.** `cco` tells you to run
|
|
425
|
+
`ollama pull <model>` rather than pulling tens of gigabytes on your behalf.
|
|
426
|
+
- **Reasoning is never requested**, and any the model volunteers is
|
|
427
|
+
discarded - models disagree about whether thinking can even be switched
|
|
428
|
+
off, and asking is a good way to earn a 400.
|
|
429
|
+
|
|
198
430
|
## Development
|
|
199
431
|
|
|
200
432
|
```sh
|
|
@@ -202,6 +434,10 @@ bun test # run the test suite
|
|
|
202
434
|
bun run typecheck # tsc --noEmit
|
|
203
435
|
```
|
|
204
436
|
|
|
437
|
+
What changed between versions is in [CHANGELOG.md](CHANGELOG.md), and at
|
|
438
|
+
greater length on the
|
|
439
|
+
[releases page](https://github.com/synmux/claude-commit/releases).
|
|
440
|
+
|
|
205
441
|
## Did you vibe this?
|
|
206
442
|
|
|
207
443
|
I distinguish vibe coding and AI-assisted development by
|
package/index.ts
CHANGED
|
@@ -9,10 +9,37 @@ export type {
|
|
|
9
9
|
GenerateOptions,
|
|
10
10
|
GenerateProgress,
|
|
11
11
|
GenerateResult,
|
|
12
|
+
IgnoreStats,
|
|
13
|
+
LowPriorityStats,
|
|
14
|
+
OllamaContextWindow,
|
|
12
15
|
} from "./src/generate";
|
|
13
|
-
export { runPrompt } from "./src/agent";
|
|
16
|
+
export { runClaudePrompt, runPrompt } from "./src/agent";
|
|
14
17
|
export type { RunPromptOptions } from "./src/agent";
|
|
15
|
-
export {
|
|
18
|
+
export {
|
|
19
|
+
probeOllamaContext,
|
|
20
|
+
resolveOllamaContext,
|
|
21
|
+
resolveOllamaHost,
|
|
22
|
+
runOllamaPrompt,
|
|
23
|
+
} from "./src/ollama";
|
|
24
|
+
export {
|
|
25
|
+
DEFAULT_OLLAMA_CONTEXT,
|
|
26
|
+
DEFAULT_OLLAMA_CONTEXT_TOKENS,
|
|
27
|
+
DEFAULT_OLLAMA_HOST,
|
|
28
|
+
isOllamaModel,
|
|
29
|
+
OLLAMA_PREFIX,
|
|
30
|
+
parseModelRef,
|
|
31
|
+
} from "./src/models";
|
|
32
|
+
export type { ModelProvider, ModelRef } from "./src/models";
|
|
33
|
+
export {
|
|
34
|
+
applyIgnorePatterns,
|
|
35
|
+
diffPaths,
|
|
36
|
+
partitionDiff,
|
|
37
|
+
sectionPaths,
|
|
38
|
+
splitDiff,
|
|
39
|
+
} from "./src/diff";
|
|
40
|
+
export type { DiffPartition, IgnoreResult } from "./src/diff";
|
|
41
|
+
export { createPathMatcher, matchesPathPatterns } from "./src/paths";
|
|
42
|
+
export type { PathMatcher } from "./src/paths";
|
|
16
43
|
export {
|
|
17
44
|
DEFAULT_CONFIG,
|
|
18
45
|
loadFileConfig,
|
|
@@ -26,14 +53,18 @@ export {
|
|
|
26
53
|
buildSummaryUser,
|
|
27
54
|
buildFinalSystem,
|
|
28
55
|
buildFinalUser,
|
|
56
|
+
buildFilenamesUser,
|
|
29
57
|
parseOptions,
|
|
30
58
|
cleanMessage,
|
|
31
59
|
} from "./src/prompts";
|
|
32
60
|
export * as git from "./src/git";
|
|
33
61
|
export { ClaudeCommitError } from "./src/errors";
|
|
34
62
|
export type {
|
|
63
|
+
ChangePriority,
|
|
35
64
|
Config,
|
|
65
|
+
DiffSummary,
|
|
36
66
|
ModelConfig,
|
|
67
|
+
OllamaConfig,
|
|
37
68
|
PartialConfig,
|
|
38
69
|
ModelResult,
|
|
39
70
|
FileChange,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synmux/claude-commit",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Generate git commit messages with Claude, using your Claude Code subscription.",
|
|
3
|
+
"version": "1.0.4",
|
|
4
|
+
"description": "Generate git commit messages with Claude, using your Claude Code subscription and/or Ollama.",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"module": "index.ts",
|
|
7
7
|
"type": "module",
|
|
@@ -13,39 +13,53 @@
|
|
|
13
13
|
"files": [
|
|
14
14
|
"bin",
|
|
15
15
|
"src",
|
|
16
|
-
"index.ts"
|
|
16
|
+
"index.ts",
|
|
17
|
+
"CHANGELOG.md"
|
|
17
18
|
],
|
|
18
19
|
"claude-commit": {
|
|
20
|
+
"allowApiKey": false,
|
|
21
|
+
"charsPerToken": 3.5,
|
|
19
22
|
"conventionalCommits": true,
|
|
20
|
-
"gitmoji": true,
|
|
21
|
-
"multiline": true,
|
|
22
|
-
"template": null,
|
|
23
23
|
"customPrompt": null,
|
|
24
|
+
"filenamesOnly": false,
|
|
25
|
+
"gitmoji": true,
|
|
24
26
|
"interactive": false,
|
|
25
|
-
"interactiveCount":
|
|
27
|
+
"interactiveCount": 10,
|
|
26
28
|
"interactiveTemperature": 1,
|
|
27
|
-
"
|
|
29
|
+
"lowPriorityPaths": [
|
|
30
|
+
".agents/**",
|
|
31
|
+
".claude/**",
|
|
32
|
+
"bun.lock",
|
|
33
|
+
".serena"
|
|
34
|
+
],
|
|
35
|
+
"maxChunkTokens": 32768,
|
|
28
36
|
"models": {
|
|
29
|
-
"summary": "
|
|
30
|
-
"final": "
|
|
37
|
+
"summary": "ollama:gemma4:e4b-mlx",
|
|
38
|
+
"final": "ollama:gemma4:e4b-mlx"
|
|
31
39
|
},
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
|
|
40
|
+
"multiline": true,
|
|
41
|
+
"ollama": {
|
|
42
|
+
"host": "http://localhost:11434",
|
|
43
|
+
"context": "auto",
|
|
44
|
+
"keepAlive": "10m"
|
|
45
|
+
},
|
|
46
|
+
"skipArmored": true,
|
|
47
|
+
"spinner": "pong",
|
|
48
|
+
"template": null
|
|
35
49
|
},
|
|
36
50
|
"devDependencies": {
|
|
37
|
-
"@anthropic-ai/claude-code": "^2.1.
|
|
51
|
+
"@anthropic-ai/claude-code": "^2.1.241",
|
|
38
52
|
"@trunkio/launcher": "^1.3.4",
|
|
39
|
-
"@types/bun": "^1.
|
|
53
|
+
"@types/bun": "^1.4.0",
|
|
40
54
|
"prettier": "3.9.4",
|
|
41
|
-
"skilld": "^2.
|
|
55
|
+
"skilld": "^2.3.0"
|
|
42
56
|
},
|
|
43
57
|
"peerDependencies": {
|
|
44
58
|
"typescript": "^6.0.3"
|
|
45
59
|
},
|
|
46
60
|
"dependencies": {
|
|
47
|
-
"@anthropic-ai/claude-agent-sdk": "^0.3.
|
|
48
|
-
"@opentui/core": "^0.
|
|
61
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.241",
|
|
62
|
+
"@opentui/core": "^0.5.7",
|
|
49
63
|
"cli-spinners": "^3.4.0",
|
|
50
64
|
"commander": "^15.0.0",
|
|
51
65
|
"ora": "^9.4.1"
|