@synmux/claude-commit 0.1.2 → 0.1.3
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 +1 -1
- package/README.md +6 -0
- package/package.json +9 -5
- package/src/agent.ts +44 -15
- package/src/generate.ts +9 -2
- package/src/tokens.ts +42 -0
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -161,6 +161,12 @@ keys are valid at every level:
|
|
|
161
161
|
}
|
|
162
162
|
```
|
|
163
163
|
|
|
164
|
+
`maxChunkTokens` is a cap, not a promise: at run time it is clamped to the
|
|
165
|
+
summary model's context window minus a fixed reserve (1M-window models such as
|
|
166
|
+
current Sonnet/Opus keep the full budget; Haiku, older pinned model ids, and
|
|
167
|
+
unrecognised models are floored at 200k), so a single chunk can never overflow
|
|
168
|
+
the model.
|
|
169
|
+
|
|
164
170
|
## Development
|
|
165
171
|
|
|
166
172
|
```sh
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synmux/claude-commit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Generate git commit messages with Claude, using your Claude Code subscription.",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"module": "index.ts",
|
|
@@ -33,20 +33,24 @@
|
|
|
33
33
|
"allowApiKey": false
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@anthropic-ai/claude-code": "^2.1.
|
|
36
|
+
"@anthropic-ai/claude-code": "^2.1.218",
|
|
37
37
|
"@trunkio/launcher": "^1.3.4",
|
|
38
38
|
"@types/bun": "^1.3.14",
|
|
39
39
|
"prettier": "3.9.4",
|
|
40
40
|
"skilld": "^2.0.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"typescript": "^
|
|
43
|
+
"typescript": "^7.0.2"
|
|
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.218",
|
|
47
|
+
"@opentui/core": "^0.4.5",
|
|
48
48
|
"commander": "^15.0.0"
|
|
49
49
|
},
|
|
50
|
+
"repository": {
|
|
51
|
+
"type": "git",
|
|
52
|
+
"url": "git+https://github.com/synmux/claude-commit"
|
|
53
|
+
},
|
|
50
54
|
"scripts": {
|
|
51
55
|
"start": "bun run bin/cco.ts",
|
|
52
56
|
"test": "bun test",
|
package/src/agent.ts
CHANGED
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
* `ANTHROPIC_AUTH_TOKEN`) are stripped from that environment, forcing the
|
|
9
9
|
* `claude login` subscription session so the cost is bundled with Claude Code
|
|
10
10
|
* usage; the `allowApiKey` config option passes them through for explicit
|
|
11
|
-
* pay-as-you-go billing.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* pay-as-you-go billing. Every request runs fully isolated from the user's
|
|
12
|
+
* Claude Code configuration - no tools, skills, MCP servers, plugins, or
|
|
13
|
+
* settings (see {@link buildQueryOptions}) - so the request contains nothing
|
|
14
|
+
* beyond the prompt we build.
|
|
14
15
|
*/
|
|
15
16
|
import {
|
|
16
17
|
query,
|
|
@@ -150,31 +151,59 @@ function describeAssistantError(code: string): string {
|
|
|
150
151
|
}
|
|
151
152
|
|
|
152
153
|
/**
|
|
153
|
-
*
|
|
154
|
+
* Build the Agent SDK options for one isolated, single-turn text completion.
|
|
154
155
|
*
|
|
155
|
-
*
|
|
156
|
+
* Isolation is layered because the SDK gates each context source separately:
|
|
157
|
+
*
|
|
158
|
+
* - `settingSources: []` disables settings files and `CLAUDE.md` - and only
|
|
159
|
+
* those. It does NOT stop MCP servers or skills from loading.
|
|
160
|
+
* - `mcpServers: {}` + `strictMcpConfig: true` ignore every MCP server
|
|
161
|
+
* configured in `~/.claude.json`, project `.mcp.json`, and plugins.
|
|
162
|
+
* - `skills: []` disables skill discovery, which the CLI otherwise performs
|
|
163
|
+
* even when the `skills` option is omitted entirely.
|
|
164
|
+
* - `tools: []` and `plugins: []` drop all built-in tools and plugins.
|
|
165
|
+
*
|
|
166
|
+
* Omitting any of these leaks the user's global Claude Code configuration
|
|
167
|
+
* into the request - observed at ~850k tokens of MCP tool definitions, which
|
|
168
|
+
* overflows the context window before the diff is even counted.
|
|
156
169
|
*/
|
|
157
|
-
export
|
|
158
|
-
prompt: string,
|
|
170
|
+
export function buildQueryOptions(
|
|
159
171
|
opts: RunPromptOptions,
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
allowApiKey: opts.allowApiKey ?? false,
|
|
164
|
-
...(opts.temperature != null ? { temperature: opts.temperature } : {}),
|
|
165
|
-
});
|
|
166
|
-
const options: Options = {
|
|
172
|
+
subprocessEnv?: Record<string, string | undefined>,
|
|
173
|
+
): Options {
|
|
174
|
+
return {
|
|
167
175
|
model: opts.model,
|
|
168
176
|
systemPrompt: opts.system,
|
|
169
177
|
tools: [], // pure text completion: no Bash/Read/Edit/etc.
|
|
178
|
+
skills: [],
|
|
179
|
+
mcpServers: {},
|
|
180
|
+
strictMcpConfig: true,
|
|
181
|
+
plugins: [],
|
|
182
|
+
settingSources: [],
|
|
170
183
|
maxTurns: 1,
|
|
171
|
-
settingSources: [], // ignore user/project/local settings, CLAUDE.md, MCP, plugins
|
|
172
184
|
includePartialMessages: Boolean(opts.onText),
|
|
173
185
|
...(opts.abortController ? { abortController: opts.abortController } : {}),
|
|
174
186
|
...(opts.onStderr ? { stderr: opts.onStderr } : {}),
|
|
175
187
|
...(subprocessEnv ? { env: subprocessEnv } : {}),
|
|
176
188
|
...(opts.outputFormat ? { outputFormat: opts.outputFormat } : {}),
|
|
177
189
|
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Run a single prompt and return the model's text response.
|
|
194
|
+
*
|
|
195
|
+
* Throws {@link ClaudeCommitError} on any model/authentication/quota failure.
|
|
196
|
+
*/
|
|
197
|
+
export async function runPrompt(
|
|
198
|
+
prompt: string,
|
|
199
|
+
opts: RunPromptOptions,
|
|
200
|
+
): Promise<ModelResult> {
|
|
201
|
+
const subprocessEnv = buildSubprocessEnv({
|
|
202
|
+
baseEnv: process.env,
|
|
203
|
+
allowApiKey: opts.allowApiKey ?? false,
|
|
204
|
+
...(opts.temperature != null ? { temperature: opts.temperature } : {}),
|
|
205
|
+
});
|
|
206
|
+
const options = buildQueryOptions(opts, subprocessEnv);
|
|
178
207
|
|
|
179
208
|
let resultText: string | null = null;
|
|
180
209
|
let costUsd = 0;
|
package/src/generate.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { runPrompt } from "./agent";
|
|
13
13
|
import { splitDiff } from "./diff";
|
|
14
|
-
import { tokensToChars } from "./tokens";
|
|
14
|
+
import { clampChunkTokens, tokensToChars } from "./tokens";
|
|
15
15
|
import { ClaudeCommitError } from "./errors";
|
|
16
16
|
import {
|
|
17
17
|
buildFinalSystem,
|
|
@@ -58,7 +58,14 @@ export async function generateCommit(
|
|
|
58
58
|
): Promise<GenerateResult> {
|
|
59
59
|
const { count = 1, progress = {}, abortController } = options;
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
// The configured chunk budget is clamped to the summary model's context
|
|
62
|
+
// window so a single chunk (plus prompt scaffolding and response headroom)
|
|
63
|
+
// can never overflow it, whatever `maxChunkTokens` says.
|
|
64
|
+
const chunkTokens = clampChunkTokens(
|
|
65
|
+
config.models.summary,
|
|
66
|
+
config.maxChunkTokens,
|
|
67
|
+
);
|
|
68
|
+
const maxChars = tokensToChars(chunkTokens, config.charsPerToken);
|
|
62
69
|
const chunks = splitDiff(diff, maxChars);
|
|
63
70
|
if (chunks.length === 0) {
|
|
64
71
|
throw new ClaudeCommitError("There are no staged changes to summarize.");
|
package/src/tokens.ts
CHANGED
|
@@ -18,3 +18,45 @@ export function estimateTokens(text: string, charsPerToken: number): number {
|
|
|
18
18
|
export function tokensToChars(tokens: number, charsPerToken: number): number {
|
|
19
19
|
return Math.floor(tokens * charsPerToken);
|
|
20
20
|
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Models with a native (or explicitly requested) 1M-token context window:
|
|
24
|
+
* current-generation Sonnet/Opus (4.6 and later), Fable/Mythos, the bare
|
|
25
|
+
* `sonnet` / `opus` aliases (which resolve to current-generation models), and
|
|
26
|
+
* any id carrying the legacy `[1m]` long-context suffix.
|
|
27
|
+
*
|
|
28
|
+
* Everything else - Haiku, pre-4.6 pinned ids, unknown or custom models -
|
|
29
|
+
* gets the conservative 200k floor. Worst case we split the diff into more
|
|
30
|
+
* chunks than strictly necessary, which is always safe; assuming 1M for a
|
|
31
|
+
* 200k model would instead fail the whole run with "Prompt is too long".
|
|
32
|
+
*/
|
|
33
|
+
const MILLION_TOKEN_CONTEXT_MODELS =
|
|
34
|
+
/\[1m\]|^(claude-)?(sonnet|opus)$|sonnet-5|sonnet-4-6|opus-4-[678]|fable|mythos/i;
|
|
35
|
+
|
|
36
|
+
/** The context window (input token capacity) for a model name or alias. */
|
|
37
|
+
export function contextWindowTokens(model: string): number {
|
|
38
|
+
return MILLION_TOKEN_CONTEXT_MODELS.test(model) ? 1_000_000 : 200_000;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Tokens reserved out of the context window before sizing diff chunks: the
|
|
43
|
+
* system prompt, the Agent SDK's scaffolding, and room for the response.
|
|
44
|
+
* Generous on purpose - `charsPerToken` is an estimate, and a chunk that
|
|
45
|
+
* overflows the window fails the whole run.
|
|
46
|
+
*/
|
|
47
|
+
export const CONTEXT_RESERVE_TOKENS = 32_000;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Clamp a configured per-chunk token budget so that one chunk plus overhead
|
|
51
|
+
* always fits the given model's context window. The configured
|
|
52
|
+
* `maxChunkTokens` remains the user-facing cap; this only ever lowers it.
|
|
53
|
+
*/
|
|
54
|
+
export function clampChunkTokens(
|
|
55
|
+
model: string,
|
|
56
|
+
maxChunkTokens: number,
|
|
57
|
+
): number {
|
|
58
|
+
return Math.min(
|
|
59
|
+
maxChunkTokens,
|
|
60
|
+
contextWindowTokens(model) - CONTEXT_RESERVE_TOKENS,
|
|
61
|
+
);
|
|
62
|
+
}
|