@synmux/claude-commit 1.0.4 → 1.1.0

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.
@@ -0,0 +1,58 @@
1
+ /** Estimate the number of tokens in `text` given a chars-per-token ratio. */
2
+ export declare function estimateTokens(text: string, charsPerToken: number): number;
3
+ /** Convert a token budget into an approximate character budget. */
4
+ export declare function tokensToChars(tokens: number, charsPerToken: number): number;
5
+ /**
6
+ * The context window (input token capacity) for a model name or alias.
7
+ *
8
+ * For an `ollama:` model the window is not a property of the name at all -
9
+ * it is whatever `options.num_ctx` the request asks for, which cco pins to
10
+ * `ollama.contextTokens` so that chunk sizing and the request agree. Pass
11
+ * that value as `ollamaContextTokens`; the default matches
12
+ * {@link DEFAULT_OLLAMA_CONTEXT_TOKENS}.
13
+ */
14
+ export declare function contextWindowTokens(model: string, ollamaContextTokens?: number): number;
15
+ /**
16
+ * Ceiling on the tokens reserved out of the context window before sizing
17
+ * diff chunks: the system prompt, the backend's scaffolding, and room for
18
+ * the response. Generous on purpose - `charsPerToken` is an estimate, and a
19
+ * chunk that overflows the window fails the whole run.
20
+ */
21
+ export declare const CONTEXT_RESERVE_TOKENS = 32000;
22
+ /**
23
+ * Tokens to hold back from `contextWindow` when sizing chunks: the flat
24
+ * reserve, or a quarter of the window when that is smaller. Both Claude
25
+ * tiers (200k and 1M) are far above the crossover, so they reserve the full
26
+ * 32k exactly as before; only windows under 128k - which in practice means
27
+ * Ollama - scale down.
28
+ */
29
+ export declare function contextReserveTokens(contextWindow: number): number;
30
+ /**
31
+ * Clamp a configured per-chunk token budget so that one chunk plus overhead
32
+ * always fits the given model's context window. The configured
33
+ * `maxChunkTokens` remains the user-facing cap; this only ever lowers it.
34
+ * `ollamaContextTokens` supplies the window for an `ollama:` model.
35
+ */
36
+ export declare function clampChunkTokens(model: string, maxChunkTokens: number, ollamaContextTokens?: number): number;
37
+ /**
38
+ * Chars-per-token for "opaque" content: base64/base85 armor (age, gpg, git
39
+ * binary patches), long hashes, and similar high-entropy runs. Measured
40
+ * against the live API on age-armor diff content (2026-07-23): ~1.14
41
+ * chars/token - the current Claude tokenizer finds almost no merges in
42
+ * random base64. Note that generic BPE vocabularies (tiktoken-class)
43
+ * compress base64 roughly 3x better, so swapping in a third-party "real"
44
+ * tokenizer would underestimate this content class just like a plain
45
+ * chars/3.5 heuristic does. 1.0 leaves a small safety margin under the
46
+ * measured value.
47
+ */
48
+ export declare const OPAQUE_CHARS_PER_TOKEN = 1;
49
+ /** Whether a single diff line should be estimated at the opaque ratio. */
50
+ export declare function isOpaqueLine(line: string): boolean;
51
+ /**
52
+ * Estimate tokens for diff text with per-line content classification:
53
+ * opaque lines at {@link OPAQUE_CHARS_PER_TOKEN}, everything else at the
54
+ * configured `charsPerToken`. A single blended ratio underestimates
55
+ * armor-heavy diffs more than threefold, which is exactly how a chunk that
56
+ * looks within budget can overflow the model's real context window.
57
+ */
58
+ export declare function estimateDiffTokens(text: string, charsPerToken: number): number;
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Shared types for claude-commit.
3
+ */
4
+ /**
5
+ * Which models to use for each stage of the pipeline.
6
+ *
7
+ * A bare name (`sonnet`, `haiku`, a full `claude-*` id) runs through the
8
+ * Claude Agent SDK. An `ollama:`-prefixed name runs against a local or
9
+ * self-hosted Ollama server instead, with everything after the prefix taken
10
+ * as the Ollama model name verbatim - tag included, so
11
+ * `ollama:ornith-1.5:35b` means the model `ornith-1.5:35b`. The two stages
12
+ * are resolved independently, so mixing providers is normal.
13
+ */
14
+ export interface ModelConfig {
15
+ /** Model used to read diffs and write summaries. Defaults to `sonnet`. */
16
+ summary: string;
17
+ /** Model used to turn summaries into the final commit message. Defaults to `sonnet`. */
18
+ final: string;
19
+ }
20
+ /** Settings for the Ollama backend, used only by `ollama:`-prefixed models. */
21
+ export interface OllamaConfig {
22
+ /**
23
+ * Base URL of the Ollama server. Defaults to `$OLLAMA_HOST`, falling back
24
+ * to `http://localhost:11434`. A bare `host:port` (Ollama's own
25
+ * convention for that variable) is given an `http://` scheme.
26
+ */
27
+ host: string;
28
+ /**
29
+ * Context window requested for every Ollama call (`options.num_ctx`) and
30
+ * used to size diff chunks: a token count, or `"auto"` (the default) to
31
+ * take the window Ollama itself chooses for the model on this machine.
32
+ *
33
+ * The window is always sent explicitly, never left to the server: a
34
+ * prompt over it is truncated *silently* - HTTP 200, oldest content
35
+ * dropped, no flag on the response - and a summary written from half a
36
+ * diff is worse than an error, so cco pins the number it sized its chunks
37
+ * against and cross-checks the response's token counts.
38
+ *
39
+ * `"auto"` asks Ollama rather than guessing: the model is preloaded with
40
+ * no `num_ctx`, which makes the server pick from its VRAM tiers (4k / 32k
41
+ * / 256k, capped at the model's trained maximum), and the choice is read
42
+ * back from `/api/ps`. That is the largest window the server believes
43
+ * this machine can run, resolved once per model per run. A number pins
44
+ * the window instead - lower it when memory is tight (memory scales with
45
+ * it, multiplied by `OLLAMA_NUM_PARALLEL`), or raise it past the tier if
46
+ * you know better than the server does.
47
+ */
48
+ context: number | "auto";
49
+ /**
50
+ * How long the server keeps the model loaded after a request: a duration
51
+ * string (`"10m"`), seconds as a number, `0` to unload immediately, or a
52
+ * negative value to pin it. `null` leaves the server's own default (which
53
+ * is itself 5 minutes unless `OLLAMA_KEEP_ALIVE` says otherwise).
54
+ */
55
+ keepAlive: string | number | null;
56
+ }
57
+ /** Fully-resolved configuration after merging defaults, file config and CLI flags. */
58
+ export interface Config {
59
+ /** Format the subject line as a Conventional Commit (`type(scope): description`). */
60
+ conventionalCommits: boolean;
61
+ /** Prefix the subject line with a gitmoji. */
62
+ gitmoji: boolean;
63
+ /** Produce a multi-line commit (subject + body) instead of a single subject line. */
64
+ multiline: boolean;
65
+ /**
66
+ * Template for the first line. `{message}` is replaced with the generated
67
+ * subject. Useful for ticket prefixes, e.g. `"[PROJ-123] {message}"`.
68
+ */
69
+ template: string | null;
70
+ /** Extra instructions appended to the standard prompt. */
71
+ customPrompt: string | null;
72
+ /**
73
+ * Default to interactive mode (the `-i` selection TUI) on every run, without
74
+ * needing to pass `-i`. Override for a single run with `--no-interactive`.
75
+ * When there is no interactive terminal (a pipe, CI, etc.) this is ignored and
76
+ * cco falls back to the non-interactive flow rather than failing.
77
+ */
78
+ interactive: boolean;
79
+ /** How many candidate messages to generate in interactive mode. */
80
+ interactiveCount: number;
81
+ /**
82
+ * Sampling temperature for the final model when generating interactive
83
+ * options, to encourage variety between candidates. `null` leaves the model
84
+ * at its default. Only applied in interactive mode.
85
+ */
86
+ interactiveTemperature: number | null;
87
+ /**
88
+ * Name of the progress spinner animation: any spinner from the cli-spinners
89
+ * set bundled with ora (e.g. `"dots"`, `"moon"`, `"material"`). Unknown
90
+ * names are ignored and the default is used instead.
91
+ */
92
+ spinner: string;
93
+ /** Models for each pipeline stage. */
94
+ models: ModelConfig;
95
+ /**
96
+ * Skip diff summarisation and send only changed filenames to the final
97
+ * model. Uses less time and tokens at the cost of less useful messages.
98
+ * Defaults to false; ignore and lowPriorityPaths still apply.
99
+ */
100
+ filenamesOnly: boolean;
101
+ /**
102
+ * Approximate maximum number of tokens of diff to send to the summary model
103
+ * in a single request. Diffs larger than this are split across requests.
104
+ */
105
+ maxChunkTokens: number;
106
+ /** Approximate characters-per-token ratio used for chunk-size estimation. */
107
+ charsPerToken: number;
108
+ /**
109
+ * Replace runs of armored/encoded diff lines (age/gpg armor, base64 blobs,
110
+ * git binary patch bodies) with a one-line `[... lines omitted]` marker
111
+ * before summarizing. Ciphertext is unreadable to the model and tokenizes
112
+ * at roughly one token per character, so skipping it makes commits in
113
+ * encrypted-file repos (e.g. chezmoi with age) fast and cheap without
114
+ * losing anything a summary could actually use.
115
+ */
116
+ skipArmored: boolean;
117
+ /**
118
+ * Gitignore-style patterns for paths whose changes matter less than the
119
+ * rest of the commit: generated docs, lockfiles, vendored snapshots, build
120
+ * output. Diff sections under these paths are summarised separately and
121
+ * briefly, and the final model is told to describe the other changes in
122
+ * the subject line and to mention these only after them. When every
123
+ * changed file matches, the changes are described normally - there is
124
+ * nothing else for them to yield to. A pattern containing `/` matches a
125
+ * path or any ancestor directory; a bare pattern matches any path segment
126
+ * (see `src/paths.ts`).
127
+ */
128
+ lowPriorityPaths: string[];
129
+ /**
130
+ * Gitignore-style patterns - the same language as `lowPriorityPaths` - for
131
+ * paths whose changes should not be read at all: vendored dependency
132
+ * trees, generated clients, bulk data fixtures. Matching diff sections are
133
+ * dropped before anything else looks at the diff, so they cost no tokens
134
+ * and cannot influence the message.
135
+ *
136
+ * The files are still committed; this governs only what the model reads.
137
+ * When every changed file matches, there is nothing left to describe and
138
+ * the run stops with an error naming the directive - unlike
139
+ * `lowPriorityPaths`, which promotes its partition in that case, because
140
+ * "this matters less" can degrade gracefully and "do not look at this"
141
+ * cannot.
142
+ */
143
+ ignore: string[];
144
+ /** Settings for the Ollama backend (`ollama:`-prefixed models). */
145
+ ollama: OllamaConfig;
146
+ /**
147
+ * Allow API credentials from the environment (`ANTHROPIC_API_KEY` /
148
+ * `ANTHROPIC_AUTH_TOKEN`) to be used, billing pay-as-you-go instead of the
149
+ * Claude subscription. When false (the default) those variables are
150
+ * stripped from the environment passed to the Claude Agent SDK subprocess,
151
+ * so an exported key can never silently switch billing.
152
+ */
153
+ allowApiKey: boolean;
154
+ }
155
+ /** Partial config as it may appear in a config file or be produced by flags. */
156
+ export type PartialConfig = {
157
+ [K in keyof Config]?: K extends "models" ? Partial<ModelConfig> : K extends "ollama" ? Partial<OllamaConfig> : Config[K];
158
+ };
159
+ /**
160
+ * How much weight a slice of the diff carries in the commit message.
161
+ * `primary` changes define the commit; `low` changes - those under the
162
+ * configured `lowPriorityPaths` - are summarised briefly and mentioned only
163
+ * after the primary ones.
164
+ */
165
+ export type ChangePriority = "primary" | "low";
166
+ /** The summary of one diff chunk, tagged with the priority of the partition it came from. */
167
+ export interface DiffSummary {
168
+ priority: ChangePriority;
169
+ text: string;
170
+ }
171
+ /** Result of a single model invocation. */
172
+ export interface ModelResult {
173
+ /** The text the model produced. */
174
+ text: string;
175
+ /** Cost of the call in USD, if reported. */
176
+ costUsd: number;
177
+ /** The model that actually served the request, if reported. */
178
+ model?: string;
179
+ /** Parsed structured output, when a JSON-schema `outputFormat` was requested. */
180
+ structured?: unknown;
181
+ }
182
+ /** A staged change as seen by `git`. */
183
+ export interface FileChange {
184
+ /** Status code from `git diff --name-status` (e.g. `A`, `M`, `D`, `R100`). */
185
+ status: string;
186
+ /** Path of the file (the destination path for renames). */
187
+ path: string;
188
+ }
189
+ /**
190
+ * One prompt to one model, whichever provider serves it.
191
+ *
192
+ * `model` carries the provider: bare names go to Claude, `ollama:`-prefixed
193
+ * ones to Ollama (see {@link ModelConfig}). Some options only apply to one
194
+ * provider - `allowApiKey` gates Claude credentials, `ollama` supplies the
195
+ * host and context window - and each is simply ignored by the other.
196
+ */
197
+ export interface RunPromptOptions {
198
+ /** Model string: an alias (`sonnet`), a full id, or `ollama:<name>[:<tag>]`. */
199
+ model: string;
200
+ /** Full custom system prompt. */
201
+ system: string;
202
+ /** Receives assistant text as it streams in (enables partial messages). */
203
+ onText?: (delta: string) => void;
204
+ /** Abort the in-flight request. */
205
+ abortController?: AbortController;
206
+ /** Receives the underlying CLI's stderr (for `--verbose`). Claude only. */
207
+ onStderr?: (data: string) => void;
208
+ /**
209
+ * Sampling temperature. Used to add variety when generating several
210
+ * interactive options. Models that don't accept a temperature override
211
+ * will reject the request, so the caller should be prepared to retry
212
+ * without it.
213
+ */
214
+ temperature?: number;
215
+ /**
216
+ * Request a structured JSON response matching this schema. The parsed object
217
+ * is returned on {@link ModelResult.structured}. Models that don't support
218
+ * structured outputs will reject the request or return unparseable content,
219
+ * so the caller should be prepared to retry without it.
220
+ */
221
+ outputFormat?: {
222
+ type: "json_schema";
223
+ schema: Record<string, unknown>;
224
+ };
225
+ /**
226
+ * Allow API credentials from the environment to reach the Claude Agent SDK
227
+ * subprocess. Defaults to false: `ANTHROPIC_API_KEY` /
228
+ * `ANTHROPIC_AUTH_TOKEN` are stripped so the run is billed to the Claude
229
+ * subscription. Has no meaning for Ollama, which takes no credential.
230
+ */
231
+ allowApiKey?: boolean;
232
+ /** Ollama host and context settings; required for an `ollama:` model. */
233
+ ollama?: OllamaConfig;
234
+ }
@@ -4,9 +4,6 @@
4
4
  * Color is emitted only when stderr is a TTY and `NO_COLOR` is unset, so escape
5
5
  * codes never leak into redirected logs or CI output.
6
6
  */
7
- export const useColor = Boolean(process.stderr.isTTY) && !process.env.NO_COLOR;
8
-
7
+ export declare const useColor: boolean;
9
8
  /** Wrap `text` in an ANSI SGR sequence when color is enabled, else return it unchanged. */
10
- export function color(code: string, text: string): string {
11
- return useColor ? `\x1b[${code}m${text}\x1b[0m` : text;
12
- }
9
+ export declare function color(code: string, text: string): string;
@@ -0,0 +1,23 @@
1
+ import { type Spinner as SpinnerAnimation } from "cli-spinners";
2
+ /** The spinner used when none (or an unknown one) is configured. */
3
+ export declare const DEFAULT_SPINNER = "material";
4
+ /** Whether `name` is one of the cli-spinners animations bundled with ora. */
5
+ export declare function isSpinnerName(name: string): boolean;
6
+ /**
7
+ * Look up a spinner animation by name, falling back to {@link DEFAULT_SPINNER}
8
+ * for unknown names (ora itself throws on those, and a cosmetic option must
9
+ * never be able to break a commit).
10
+ */
11
+ export declare function resolveSpinner(name: string): SpinnerAnimation;
12
+ export declare class Spinner {
13
+ private instance;
14
+ private readonly enabled;
15
+ private readonly animation;
16
+ constructor(enabled?: boolean, spinnerName?: string);
17
+ start(label: string): void;
18
+ update(label: string): void;
19
+ /** Stop and clear the spinner line, optionally printing a final status line. */
20
+ stop(finalLine?: string): void;
21
+ succeed(label: string): void;
22
+ fail(label: string): void;
23
+ }
package/package.json CHANGED
@@ -1,19 +1,25 @@
1
1
  {
2
2
  "name": "@synmux/claude-commit",
3
- "version": "1.0.4",
3
+ "version": "1.1.0",
4
4
  "description": "Generate git commit messages with Claude, using your Claude Code subscription and/or Ollama.",
5
- "main": "index.ts",
6
- "module": "index.ts",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/types/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/types/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ },
12
+ "./package.json": "./package.json"
13
+ },
7
14
  "type": "module",
8
15
  "private": false,
9
16
  "bin": {
10
- "cco": "bin/cco.ts",
11
- "claude-commit": "bin/cco.ts"
17
+ "cco": "bin/cco.js",
18
+ "claude-commit": "bin/cco.js"
12
19
  },
13
20
  "files": [
14
21
  "bin",
15
- "src",
16
- "index.ts",
22
+ "dist",
17
23
  "CHANGELOG.md"
18
24
  ],
19
25
  "claude-commit": {
@@ -30,12 +36,13 @@
30
36
  ".agents/**",
31
37
  ".claude/**",
32
38
  "bun.lock",
39
+ "pnpm-lock.yaml",
33
40
  ".serena"
34
41
  ],
35
- "maxChunkTokens": 32768,
42
+ "maxChunkTokens": 750000,
36
43
  "models": {
37
- "summary": "ollama:gemma4:e4b-mlx",
38
- "final": "ollama:gemma4:e4b-mlx"
44
+ "summary": "opus",
45
+ "final": "opus"
39
46
  },
40
47
  "multiline": true,
41
48
  "ollama": {
@@ -48,40 +55,44 @@
48
55
  "template": null
49
56
  },
50
57
  "devDependencies": {
51
- "@anthropic-ai/claude-code": "^2.1.241",
58
+ "@anthropic-ai/claude-code": "^2.1.263",
52
59
  "@trunkio/launcher": "^1.3.4",
53
- "@types/bun": "^1.4.0",
60
+ "@types/node": "^24.13.3",
61
+ "@types/picomatch": "^4.0.3",
62
+ "esbuild": "^0.27.7",
54
63
  "prettier": "3.9.4",
55
- "skilld": "^2.3.0"
64
+ "skilld": "^2.3.0",
65
+ "vitest": "^4.1.11"
66
+ },
67
+ "engines": {
68
+ "node": "^24.20.0"
56
69
  },
57
70
  "peerDependencies": {
58
71
  "typescript": "^6.0.3"
59
72
  },
60
73
  "dependencies": {
61
- "@anthropic-ai/claude-agent-sdk": "^0.3.241",
62
- "@opentui/core": "^0.5.7",
74
+ "@anthropic-ai/claude-agent-sdk": "^0.3.263",
75
+ "@clack/core": "^1.4.3",
76
+ "@clack/prompts": "^1.7.0",
63
77
  "cli-spinners": "^3.4.0",
64
78
  "commander": "^15.0.0",
65
- "ora": "^9.4.1"
79
+ "ora": "^9.4.1",
80
+ "picomatch": "^4.0.7"
66
81
  },
67
82
  "repository": {
68
83
  "type": "git",
69
- "url": "git+https://github.com/synmux/claude-commit"
84
+ "url": "git+https://github.com/synmux/claude-commit.git"
70
85
  },
71
86
  "scripts": {
72
- "start": "bun run bin/cco.ts",
73
- "test": "bun test",
74
- "format": "bun run prettier --write . && bun run trunk fmt -a",
75
- "lint": "bun run trunk check -a",
76
- "lint:fix": "bun run trunk check -a --fix",
77
- "typecheck": "bun run tsc --noEmit",
78
- "prepare": "bun run skilld prepare || true"
87
+ "start": "node bin/cco.js",
88
+ "build": "esbuild bin/cco.ts index.ts --bundle --platform=node --format=esm --packages=external --outbase=. --outdir=dist && tsc -p tsconfig.build.json",
89
+ "test": "vitest run",
90
+ "format": "prettier --write . && trunk fmt -a",
91
+ "lint": "trunk check -a",
92
+ "lint:fix": "trunk check -a --fix",
93
+ "lint:types": "tsc --noEmit",
94
+ "prepare": "skilld prepare || true",
95
+ "prepublishOnly": "pnpm run build"
79
96
  },
80
- "trustedDependencies": [
81
- "@anthropic-ai/claude-code",
82
- "@google/genai",
83
- "onnxruntime-node",
84
- "protobufjs",
85
- "sharp"
86
- ]
97
+ "packageManager": "pnpm@12.4.1+sha512.2e81e399d73fe8390dab25e06aa788ab7a5908248d2f5a370f82b481147a6a7a367bf8048f9a6fdb6460f21a66f0542dedb8b94ca2c8723596741920b1656d4c"
87
98
  }
package/index.ts DELETED
@@ -1,71 +0,0 @@
1
- /**
2
- * Library entry point for `claude-commit`.
3
- *
4
- * Re-exports the building blocks so the commit-message pipeline can be used
5
- * programmatically. The CLI lives in `bin/cco.ts` (`src/cli.ts`).
6
- */
7
- export { generateCommit } from "./src/generate";
8
- export type {
9
- GenerateOptions,
10
- GenerateProgress,
11
- GenerateResult,
12
- IgnoreStats,
13
- LowPriorityStats,
14
- OllamaContextWindow,
15
- } from "./src/generate";
16
- export { runClaudePrompt, runPrompt } from "./src/agent";
17
- export type { RunPromptOptions } from "./src/agent";
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";
43
- export {
44
- DEFAULT_CONFIG,
45
- loadFileConfig,
46
- resolveConfig,
47
- mergeConfig,
48
- mergePartial,
49
- sanitizePartial,
50
- } from "./src/config";
51
- export {
52
- buildSummarySystem,
53
- buildSummaryUser,
54
- buildFinalSystem,
55
- buildFinalUser,
56
- buildFilenamesUser,
57
- parseOptions,
58
- cleanMessage,
59
- } from "./src/prompts";
60
- export * as git from "./src/git";
61
- export { ClaudeCommitError } from "./src/errors";
62
- export type {
63
- ChangePriority,
64
- Config,
65
- DiffSummary,
66
- ModelConfig,
67
- OllamaConfig,
68
- PartialConfig,
69
- ModelResult,
70
- FileChange,
71
- } from "./src/types";