@synmux/claude-commit 0.1.0 → 0.1.2
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/LICENSE +21 -0
- package/README.md +18 -16
- package/package.json +14 -7
- package/src/agent.ts +2 -2
- package/src/config.ts +3 -3
- package/src/diff.ts +1 -1
- package/src/generate.ts +2 -2
- package/src/git.ts +2 -2
- package/src/prompts.ts +3 -3
- package/src/types.ts +2 -2
- package/src/ui/interactive.ts +4 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 syn <4ibo6o2n2@mozmail.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `claude-commit`
|
|
2
2
|
|
|
3
|
-
Generate high-quality git commit messages with Claude
|
|
3
|
+
Generate high-quality git commit messages with Claude - using your **Claude Code
|
|
4
4
|
subscription**, not an API key.
|
|
5
5
|
|
|
6
6
|
`claude-commit` reads your staged diff, has a strong model summarize it, and a fast
|
|
@@ -20,14 +20,16 @@ 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
|
|
23
|
+
staged diff ──split──▶ [chunk, …] ──sonnet──▶ summaries ──sonnet──▶ commit message
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
1. **Summarize**
|
|
27
|
-
each chunk is summarized by a strong model (`sonnet
|
|
28
|
-
context). Diffs larger than 1M tokens simply produce more chunks.
|
|
29
|
-
2. **Write**
|
|
30
|
-
final commit message according to your formatting rules.
|
|
26
|
+
1. **Summarize** - the diff is split into chunks that fit the context window and
|
|
27
|
+
each chunk is summarized by a strong model (`sonnet`, which carries a native
|
|
28
|
+
1M-token context). Diffs larger than 1M tokens simply produce more chunks.
|
|
29
|
+
2. **Write** - the summaries are handed to the same model (`sonnet`) to write the
|
|
30
|
+
final commit message according to your formatting rules. The message is the
|
|
31
|
+
whole point of the tool, and its input is tiny, so a strong model here costs
|
|
32
|
+
almost nothing extra.
|
|
31
33
|
|
|
32
34
|
Both stages run through the [Claude Agent SDK](https://code.claude.com/docs/en/agent-sdk/overview).
|
|
33
35
|
|
|
@@ -50,7 +52,7 @@ bun run bin/cco.ts --help
|
|
|
50
52
|
|
|
51
53
|
`claude-commit` uses the Claude Agent SDK and, by default, always authenticates
|
|
52
54
|
with your Claude Code subscription session (run `claude login` once). Usage is
|
|
53
|
-
bundled with your Claude Code usage
|
|
55
|
+
bundled with your Claude Code usage - no separate API bill.
|
|
54
56
|
|
|
55
57
|
To protect you from surprise pay-as-you-go charges, API credentials in your
|
|
56
58
|
environment (`ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN`) are **ignored by
|
|
@@ -84,8 +86,8 @@ and asks for confirmation before committing. Pass `-y` to skip the prompt, or
|
|
|
84
86
|
| `-m, --multiline` / `--no-multiline` | Write a multi-line commit (subject + body), or force a single line |
|
|
85
87
|
| `-t, --template <tpl>` | Template for the first line, e.g. `"[PROJ-1] {message}"` |
|
|
86
88
|
| `-p, --prompt <text>` | Extra instructions appended to the prompt |
|
|
87
|
-
| `--model-summary <model>` | Model used to summarize the diff (default `sonnet
|
|
88
|
-
| `--model-final <model>` | Model used to write the message (default `
|
|
89
|
+
| `--model-summary <model>` | Model used to summarize the diff (default `sonnet`) |
|
|
90
|
+
| `--model-final <model>` | Model used to write the message (default `sonnet`) |
|
|
89
91
|
| `-d, --dry-run` | Print the message to stdout without committing |
|
|
90
92
|
| `-y, --yes` | Commit without asking for confirmation |
|
|
91
93
|
| `--no-spinner` | Disable the progress spinner |
|
|
@@ -103,7 +105,7 @@ cco --dry-run | cat # print a message without committing (TUI-free, pipe-sa
|
|
|
103
105
|
git commit -F <(cco -d) # use the message with your own git invocation
|
|
104
106
|
```
|
|
105
107
|
|
|
106
|
-
In a pipe (no TTY) there is no spinner and no confirmation prompt
|
|
108
|
+
In a pipe (no TTY) there is no spinner and no confirmation prompt - `cco` just
|
|
107
109
|
generates and commits (or prints, with `--dry-run`).
|
|
108
110
|
|
|
109
111
|
## Interactive mode
|
|
@@ -116,8 +118,8 @@ cancel.
|
|
|
116
118
|
|
|
117
119
|
To make interactive mode the default without typing `-i` every time, set
|
|
118
120
|
`"interactive": true` in your config (see below); opt out of a single run with
|
|
119
|
-
`--no-interactive`. When there is no interactive terminal
|
|
120
|
-
or with `--dry-run`
|
|
121
|
+
`--no-interactive`. When there is no interactive terminal - in a pipe, a CI job,
|
|
122
|
+
or with `--dry-run` - `cco` ignores the setting and falls back to the
|
|
121
123
|
non-interactive flow rather than failing.
|
|
122
124
|
|
|
123
125
|
## Configuration
|
|
@@ -126,7 +128,7 @@ Configuration is layered, from lowest to highest precedence:
|
|
|
126
128
|
|
|
127
129
|
1. Built-in defaults.
|
|
128
130
|
2. **A global user config** at `~/.config/claude-commit/config.json` (or
|
|
129
|
-
`$XDG_CONFIG_HOME/claude-commit/config.json`)
|
|
131
|
+
`$XDG_CONFIG_HOME/claude-commit/config.json`) - your personal defaults across
|
|
130
132
|
every project. The `.claude-commit.json` / `.claude-commitrc.json` /
|
|
131
133
|
`.claude-commitrc` names are also accepted in that directory.
|
|
132
134
|
3. A `claude-commit` key in the repo's `package.json`.
|
|
@@ -150,8 +152,8 @@ keys are valid at every level:
|
|
|
150
152
|
"interactiveCount": 3,
|
|
151
153
|
"interactiveTemperature": 1,
|
|
152
154
|
"models": {
|
|
153
|
-
"summary": "sonnet
|
|
154
|
-
"final": "
|
|
155
|
+
"summary": "sonnet",
|
|
156
|
+
"final": "sonnet"
|
|
155
157
|
},
|
|
156
158
|
"maxChunkTokens": 600000,
|
|
157
159
|
"charsPerToken": 3.5,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synmux/claude-commit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Generate git commit messages with Claude, using your Claude Code subscription.",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"module": "index.ts",
|
|
@@ -25,15 +25,15 @@
|
|
|
25
25
|
"interactiveCount": 3,
|
|
26
26
|
"interactiveTemperature": 1,
|
|
27
27
|
"models": {
|
|
28
|
-
"summary": "sonnet
|
|
29
|
-
"final": "
|
|
28
|
+
"summary": "sonnet",
|
|
29
|
+
"final": "sonnet"
|
|
30
30
|
},
|
|
31
31
|
"maxChunkTokens": 600000,
|
|
32
32
|
"charsPerToken": 3.5,
|
|
33
33
|
"allowApiKey": false
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@anthropic-ai/claude-code": "^2.1.
|
|
36
|
+
"@anthropic-ai/claude-code": "^2.1.205",
|
|
37
37
|
"@trunkio/launcher": "^1.3.4",
|
|
38
38
|
"@types/bun": "^1.3.14",
|
|
39
39
|
"prettier": "3.9.4",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"typescript": "^6.0.3"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@anthropic-ai/claude-agent-sdk": "^0.3.
|
|
47
|
-
"@opentui/core": "^0.4.
|
|
46
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.205",
|
|
47
|
+
"@opentui/core": "^0.4.3",
|
|
48
48
|
"commander": "^15.0.0"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
|
@@ -55,5 +55,12 @@
|
|
|
55
55
|
"lint:fix": "bun run trunk check -a --fix",
|
|
56
56
|
"typecheck": "bun run tsc --noEmit",
|
|
57
57
|
"prepare": "bun run skilld prepare || true"
|
|
58
|
-
}
|
|
58
|
+
},
|
|
59
|
+
"trustedDependencies": [
|
|
60
|
+
"@anthropic-ai/claude-code",
|
|
61
|
+
"@google/genai",
|
|
62
|
+
"onnxruntime-node",
|
|
63
|
+
"protobufjs",
|
|
64
|
+
"sharp"
|
|
65
|
+
]
|
|
59
66
|
}
|
package/src/agent.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { ClaudeCommitError } from "./errors";
|
|
|
21
21
|
import type { ModelResult } from "./types";
|
|
22
22
|
|
|
23
23
|
export interface RunPromptOptions {
|
|
24
|
-
/** Model string (alias like `
|
|
24
|
+
/** Model string (alias like `sonnet`, `haiku`, or a full model id). */
|
|
25
25
|
model: string;
|
|
26
26
|
/** Full custom system prompt. */
|
|
27
27
|
system: string;
|
|
@@ -92,7 +92,7 @@ export interface SubprocessEnvOptions {
|
|
|
92
92
|
*
|
|
93
93
|
* Unless `allowApiKey` is set, API credential variables are removed so the
|
|
94
94
|
* spawned `claude` binary always authenticates with the user's subscription
|
|
95
|
-
* session
|
|
95
|
+
* session - an exported `ANTHROPIC_API_KEY` must never silently switch
|
|
96
96
|
* billing to pay-as-you-go.
|
|
97
97
|
*/
|
|
98
98
|
export function buildSubprocessEnv(
|
package/src/config.ts
CHANGED
|
@@ -22,8 +22,8 @@ export const DEFAULT_CONFIG: Config = {
|
|
|
22
22
|
interactiveCount: 3,
|
|
23
23
|
interactiveTemperature: 1,
|
|
24
24
|
models: {
|
|
25
|
-
summary: "sonnet
|
|
26
|
-
final: "
|
|
25
|
+
summary: "sonnet",
|
|
26
|
+
final: "sonnet",
|
|
27
27
|
},
|
|
28
28
|
maxChunkTokens: 600_000,
|
|
29
29
|
charsPerToken: 3.5,
|
|
@@ -197,7 +197,7 @@ export async function loadFileConfig(
|
|
|
197
197
|
|
|
198
198
|
// package.json#claude-commit at the repo root (above the global config, below
|
|
199
199
|
// project config files). A malformed package.json is not cco's concern to
|
|
200
|
-
// enforce
|
|
200
|
+
// enforce - skip it rather than blocking the commit (the user may even be
|
|
201
201
|
// committing its fix).
|
|
202
202
|
let pkg: unknown;
|
|
203
203
|
try {
|
package/src/diff.ts
CHANGED
|
@@ -16,7 +16,7 @@ const HUNK_HEADER = "@@";
|
|
|
16
16
|
* which we stop trying to subdivide a section. This guards against a
|
|
17
17
|
* misconfigured tiny `maxChars`, or a pathologically large file header,
|
|
18
18
|
* driving the line-splitter's budget to zero and shattering a hunk into
|
|
19
|
-
* one-character pieces
|
|
19
|
+
* one-character pieces - which would otherwise spawn a model request per
|
|
20
20
|
* character.
|
|
21
21
|
*/
|
|
22
22
|
const MIN_SPLIT_BUDGET = 64;
|
package/src/generate.ts
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* diff ──split──▶ [chunk, chunk, ...] ──summary model──▶ [summary, ...]
|
|
5
5
|
* ──final model──▶ commit message(s)
|
|
6
6
|
*
|
|
7
|
-
* The summary model (default `sonnet
|
|
7
|
+
* The summary model (default `sonnet`) reads each diff chunk and writes a
|
|
8
8
|
* factual summary; chunking keeps each request within the model's context
|
|
9
|
-
* window. The final model (default `
|
|
9
|
+
* window. The final model (default `sonnet`) turns the summaries into the commit
|
|
10
10
|
* message(s), applying the configured formatting rules.
|
|
11
11
|
*/
|
|
12
12
|
import { runPrompt } from "./agent";
|
package/src/git.ts
CHANGED
|
@@ -37,7 +37,7 @@ export async function isGitRepo(): Promise<boolean> {
|
|
|
37
37
|
const res = await $`git rev-parse --is-inside-work-tree`.quiet().nothrow();
|
|
38
38
|
return res.exitCode === 0 && res.stdout.toString().trim() === "true";
|
|
39
39
|
} catch {
|
|
40
|
-
// git missing or unrunnable
|
|
40
|
+
// git missing or unrunnable - treat as "not a usable repo".
|
|
41
41
|
return false;
|
|
42
42
|
}
|
|
43
43
|
}
|
|
@@ -81,7 +81,7 @@ export async function getStagedStat(): Promise<string> {
|
|
|
81
81
|
/**
|
|
82
82
|
* Create a commit with the given message. The message is piped to
|
|
83
83
|
* `git commit -F -` over stdin, so arbitrary content (leading dashes, multiple
|
|
84
|
-
* lines, special characters) is handled safely
|
|
84
|
+
* lines, special characters) is handled safely - and nothing touches disk, so
|
|
85
85
|
* there is no temp file to be raced or read by another user.
|
|
86
86
|
*/
|
|
87
87
|
export async function commit(message: string): Promise<void> {
|
package/src/prompts.ts
CHANGED
|
@@ -147,7 +147,7 @@ export function buildFinalSystem(config: Config, structured = false): string {
|
|
|
147
147
|
|
|
148
148
|
rules.push(
|
|
149
149
|
structured
|
|
150
|
-
? "Each commit message must be the raw message text only
|
|
150
|
+
? "Each commit message must be the raw message text only - no surrounding quotes, no markdown, and no code fences."
|
|
151
151
|
: "Output ONLY the commit message itself: no surrounding quotes, no markdown, no code fences, no preamble, and no explanation.",
|
|
152
152
|
);
|
|
153
153
|
|
|
@@ -157,7 +157,7 @@ export function buildFinalSystem(config: Config, structured = false): string {
|
|
|
157
157
|
/**
|
|
158
158
|
* The shared instruction for requesting several distinct options.
|
|
159
159
|
*
|
|
160
|
-
* It insists each option be a COMPLETE message obeying the formatting rules
|
|
160
|
+
* It insists each option be a COMPLETE message obeying the formatting rules -
|
|
161
161
|
* crucially the body when `multiline` is on. The previous wording asked the
|
|
162
162
|
* model to "vary the structure" of the options, which let it drop bodies to
|
|
163
163
|
* manufacture variety, so `multiline` appeared to be ignored in interactive
|
|
@@ -166,7 +166,7 @@ export function buildFinalSystem(config: Config, structured = false): string {
|
|
|
166
166
|
function multiOptionInstruction(count: number): string {
|
|
167
167
|
return (
|
|
168
168
|
`Produce exactly ${count} distinct commit-message options for this change. ` +
|
|
169
|
-
`Each option must be a complete commit message that independently obeys all the formatting rules above
|
|
169
|
+
`Each option must be a complete commit message that independently obeys all the formatting rules above - ` +
|
|
170
170
|
`including the blank line and body when those rules ask for one. ` +
|
|
171
171
|
`Make the options genuinely different in wording and emphasis, but never drop the subject or a required body just to create variety.`
|
|
172
172
|
);
|
package/src/types.ts
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
/** Which models to use for each stage of the pipeline. */
|
|
6
6
|
export interface ModelConfig {
|
|
7
|
-
/** Model used to read diffs and write summaries. Defaults to `sonnet
|
|
7
|
+
/** Model used to read diffs and write summaries. Defaults to `sonnet`. */
|
|
8
8
|
summary: string;
|
|
9
|
-
/** Model used to turn summaries into the final commit message. Defaults to `
|
|
9
|
+
/** Model used to turn summaries into the final commit message. Defaults to `sonnet`. */
|
|
10
10
|
final: string;
|
|
11
11
|
}
|
|
12
12
|
|
package/src/ui/interactive.ts
CHANGED
|
@@ -51,7 +51,7 @@ export async function runInteractive(
|
|
|
51
51
|
try {
|
|
52
52
|
selection = await selectWithTui(messages);
|
|
53
53
|
} catch {
|
|
54
|
-
// TUI failed to initialize (unusual terminal, etc.)
|
|
54
|
+
// TUI failed to initialize (unusual terminal, etc.) - degrade gracefully.
|
|
55
55
|
selection = await selectWithReadline(messages);
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -98,7 +98,7 @@ const PICKER_CHROME_ROWS = 4;
|
|
|
98
98
|
* The picker's height, sized to its content (two rows per candidate) but capped
|
|
99
99
|
* to the terminal so a large `interactiveCount` still fits; past the cap the list
|
|
100
100
|
* scrolls internally (see `showScrollIndicator` in {@link buildPickerScene}). An
|
|
101
|
-
* explicit height keeps the picker compact
|
|
101
|
+
* explicit height keeps the picker compact - only as tall as the options need -
|
|
102
102
|
* rather than stretching to fill the screen.
|
|
103
103
|
*/
|
|
104
104
|
export function pickerHeight(count: number, terminalRows: number): number {
|
|
@@ -163,7 +163,7 @@ export function buildPickerScene(
|
|
|
163
163
|
showDescription: true,
|
|
164
164
|
wrapSelection: true,
|
|
165
165
|
// A calm slate highlight (OpenTUI's own default background) with soft
|
|
166
|
-
// near-white text
|
|
166
|
+
// near-white text - the previous bright cyan/black fill was too harsh.
|
|
167
167
|
// The default description greys (#888888 / #CCCCCC) read fine on the slate,
|
|
168
168
|
// so they are left untouched.
|
|
169
169
|
selectedBackgroundColor: "#334455",
|
|
@@ -246,7 +246,7 @@ async function selectWithTui(messages: string[]): Promise<Selection> {
|
|
|
246
246
|
} catch {
|
|
247
247
|
// A renderable method threw unexpectedly. Rather than let the error
|
|
248
248
|
// escape the key handler and leave the terminal stuck in raw mode,
|
|
249
|
-
// cancel cleanly
|
|
249
|
+
// cancel cleanly - finish() restores the terminal via cleanup().
|
|
250
250
|
finish({ action: "cancel" });
|
|
251
251
|
}
|
|
252
252
|
};
|