@synmux/claude-commit 1.0.3 → 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.
- package/CHANGELOG.md +238 -0
- package/README.md +92 -38
- package/bin/cco.js +16 -0
- package/bin/cco.ts +3 -6
- package/dist/bin/cco.js +2347 -0
- package/dist/index.js +1642 -0
- package/dist/types/index.d.ts +22 -0
- package/dist/types/src/agent.d.ts +93 -0
- package/dist/types/src/config.d.ts +36 -0
- package/dist/types/src/diff.d.ts +115 -0
- package/{src/errors.ts → dist/types/src/errors.d.ts} +3 -6
- package/dist/types/src/generate.d.ts +113 -0
- package/dist/types/src/git.d.ts +31 -0
- package/dist/types/src/models.d.ts +42 -0
- package/dist/types/src/ollama.d.ts +89 -0
- package/dist/types/src/paths.d.ts +9 -0
- package/dist/types/src/prompts.d.ts +59 -0
- package/dist/types/src/tokens.d.ts +58 -0
- package/dist/types/src/types.d.ts +234 -0
- package/{src/ui/colors.ts → dist/types/src/ui/colors.d.ts} +2 -5
- package/dist/types/src/ui/spinner.d.ts +23 -0
- package/package.json +45 -32
- package/index.ts +0 -69
- package/src/agent.ts +0 -280
- package/src/cli.ts +0 -442
- package/src/config.ts +0 -337
- package/src/diff.ts +0 -571
- package/src/generate.ts +0 -478
- package/src/git.ts +0 -145
- package/src/models.ts +0 -95
- package/src/ollama.ts +0 -502
- package/src/paths.ts +0 -139
- package/src/prompts.ts +0 -364
- package/src/tokens.ts +0 -147
- package/src/types.ts +0 -238
- package/src/ui/editor.ts +0 -89
- package/src/ui/interactive.ts +0 -313
- package/src/ui/spinner.ts +0 -79
- package/src/utils.ts +0 -5
|
@@ -0,0 +1,22 @@
|
|
|
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.ts";
|
|
8
|
+
export type { GenerateOptions, GenerateProgress, GenerateResult, IgnoreStats, LowPriorityStats, OllamaContextWindow, } from "./src/generate.ts";
|
|
9
|
+
export { runClaudePrompt, runPrompt } from "./src/agent.ts";
|
|
10
|
+
export type { RunPromptOptions } from "./src/agent.ts";
|
|
11
|
+
export { probeOllamaContext, resolveOllamaContext, resolveOllamaHost, runOllamaPrompt, } from "./src/ollama.ts";
|
|
12
|
+
export { DEFAULT_OLLAMA_CONTEXT, DEFAULT_OLLAMA_CONTEXT_TOKENS, DEFAULT_OLLAMA_HOST, isOllamaModel, OLLAMA_PREFIX, parseModelRef, } from "./src/models.ts";
|
|
13
|
+
export type { ModelProvider, ModelRef } from "./src/models.ts";
|
|
14
|
+
export { applyIgnorePatterns, diffPaths, partitionDiff, sectionPaths, splitDiff, } from "./src/diff.ts";
|
|
15
|
+
export type { DiffPartition, IgnoreResult } from "./src/diff.ts";
|
|
16
|
+
export { createPathMatcher, matchesPathPatterns } from "./src/paths.ts";
|
|
17
|
+
export type { PathMatcher } from "./src/paths.ts";
|
|
18
|
+
export { DEFAULT_CONFIG, loadFileConfig, resolveConfig, mergeConfig, mergePartial, sanitizePartial, } from "./src/config.ts";
|
|
19
|
+
export { buildSummarySystem, buildSummaryUser, buildFinalSystem, buildFinalUser, buildFilenamesUser, parseOptions, cleanMessage, } from "./src/prompts.ts";
|
|
20
|
+
export * as git from "./src/git.ts";
|
|
21
|
+
export { ClaudeCommitError } from "./src/errors.ts";
|
|
22
|
+
export type { ChangePriority, Config, DiffSummary, ModelConfig, OllamaConfig, PartialConfig, ModelResult, FileChange, } from "./src/types.ts";
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model-call layer: {@link runPrompt} turns one prompt into one text
|
|
3
|
+
* completion, routing to whichever backend the model name asks for.
|
|
4
|
+
*
|
|
5
|
+
* A bare model name goes to Claude, through the Agent SDK, below. An
|
|
6
|
+
* `ollama:`-prefixed one goes to `src/ollama.ts` instead. Both return the
|
|
7
|
+
* same {@link ModelResult}, so the pipeline in `src/generate.ts` - and the
|
|
8
|
+
* injectable runner its tests use - never learns which provider ran.
|
|
9
|
+
*
|
|
10
|
+
* The Claude path, in detail:
|
|
11
|
+
*
|
|
12
|
+
* The Agent SDK spawns a bundled `claude` binary, so authentication follows
|
|
13
|
+
* Claude Code's own resolution order over the environment we hand it. By
|
|
14
|
+
* default the API credential variables (`ANTHROPIC_API_KEY` /
|
|
15
|
+
* `ANTHROPIC_AUTH_TOKEN`) are stripped from that environment, forcing the
|
|
16
|
+
* `claude login` subscription session so the cost is bundled with Claude Code
|
|
17
|
+
* usage; the `allowApiKey` config option passes them through for explicit
|
|
18
|
+
* pay-as-you-go billing. Every request runs fully isolated from the user's
|
|
19
|
+
* Claude Code configuration - no tools, skills, MCP servers, plugins, or
|
|
20
|
+
* settings (see {@link buildQueryOptions}) - so the request contains nothing
|
|
21
|
+
* beyond the prompt we build.
|
|
22
|
+
*/
|
|
23
|
+
import { type Options } from "@anthropic-ai/claude-agent-sdk";
|
|
24
|
+
import type { ModelResult, RunPromptOptions } from "./types.ts";
|
|
25
|
+
export type { RunPromptOptions } from "./types.ts";
|
|
26
|
+
export declare const GATED_CREDENTIAL_VARS: readonly ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"];
|
|
27
|
+
/**
|
|
28
|
+
* Names of the gated credential variables present in `env`. An empty string
|
|
29
|
+
* counts as present, since presence alone perturbs credential resolution.
|
|
30
|
+
*/
|
|
31
|
+
export declare function presentCredentialVars(env: Record<string, string | undefined>): string[];
|
|
32
|
+
export interface SubprocessEnvOptions {
|
|
33
|
+
/** Environment to derive the subprocess environment from (usually `process.env`). */
|
|
34
|
+
baseEnv: Record<string, string | undefined>;
|
|
35
|
+
/**
|
|
36
|
+
* Pass API credential variables through instead of stripping them.
|
|
37
|
+
* Defaults to false so the gate fails safe when a caller omits it.
|
|
38
|
+
*/
|
|
39
|
+
allowApiKey?: boolean;
|
|
40
|
+
/** Sampling temperature to inject via `CLAUDE_CODE_EXTRA_BODY`, preserving any existing extra body. */
|
|
41
|
+
temperature?: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Build the environment for the Claude Agent SDK subprocess, or return
|
|
45
|
+
* `undefined` when the parent environment can be inherited unchanged.
|
|
46
|
+
*
|
|
47
|
+
* Unless `allowApiKey` is set, API credential variables are removed so the
|
|
48
|
+
* spawned `claude` binary always authenticates with the user's subscription
|
|
49
|
+
* session - an exported `ANTHROPIC_API_KEY` must never silently switch
|
|
50
|
+
* billing to pay-as-you-go.
|
|
51
|
+
*/
|
|
52
|
+
export declare function buildSubprocessEnv(opts: SubprocessEnvOptions): Record<string, string | undefined> | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* Build the Agent SDK options for one isolated, single-turn text completion.
|
|
55
|
+
*
|
|
56
|
+
* Isolation is layered because the SDK gates each context source separately:
|
|
57
|
+
*
|
|
58
|
+
* - `settingSources: []` disables settings files and `CLAUDE.md` - and only
|
|
59
|
+
* those. It does NOT stop MCP servers or skills from loading.
|
|
60
|
+
* - `mcpServers: {}` + `strictMcpConfig: true` ignore every MCP server
|
|
61
|
+
* configured in `~/.claude.json`, project `.mcp.json`, and plugins.
|
|
62
|
+
* - `skills: []` disables skill discovery, which the CLI otherwise performs
|
|
63
|
+
* even when the `skills` option is omitted entirely.
|
|
64
|
+
* - `tools: []` and `plugins: []` drop all built-in tools and plugins.
|
|
65
|
+
*
|
|
66
|
+
* Omitting any of these lets the user's global Claude Code configuration
|
|
67
|
+
* (MCP tool schemas, skill listings - easily hundreds of thousands of tokens
|
|
68
|
+
* on a busy setup) into every request; with all of them set, live probes
|
|
69
|
+
* measure ~170 input tokens per request. Historical note: the 2026-07
|
|
70
|
+
* "Prompt is too long" failures were ultimately caused by underestimating
|
|
71
|
+
* the token density of armored diff content (see `estimateDiffTokens`), not
|
|
72
|
+
* by this leak - the isolation is hygiene and cost control, not the fix for
|
|
73
|
+
* that bug.
|
|
74
|
+
*/
|
|
75
|
+
export declare function buildQueryOptions(opts: RunPromptOptions, subprocessEnv?: Record<string, string | undefined>): Options;
|
|
76
|
+
/**
|
|
77
|
+
* Run a single prompt against a Claude model via the Agent SDK.
|
|
78
|
+
*
|
|
79
|
+
* Throws {@link ClaudeCommitError} on any model/authentication/quota failure.
|
|
80
|
+
*/
|
|
81
|
+
export declare function runClaudePrompt(prompt: string, opts: RunPromptOptions): Promise<ModelResult>;
|
|
82
|
+
/**
|
|
83
|
+
* Run a single prompt against whichever provider `opts.model` names, and
|
|
84
|
+
* return its text response.
|
|
85
|
+
*
|
|
86
|
+
* This is the single seam every caller uses; `generate.ts` accepts a
|
|
87
|
+
* replacement of exactly this shape so the pipeline can be tested without a
|
|
88
|
+
* model of either kind.
|
|
89
|
+
*
|
|
90
|
+
* Throws {@link ClaudeCommitError} on any model, authentication, transport
|
|
91
|
+
* or quota failure.
|
|
92
|
+
*/
|
|
93
|
+
export declare function runPrompt(prompt: string, opts: RunPromptOptions): Promise<ModelResult>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Config, PartialConfig } from "./types.ts";
|
|
2
|
+
export declare const DEFAULT_CONFIG: Config;
|
|
3
|
+
/**
|
|
4
|
+
* The user-level config directory, `$XDG_CONFIG_HOME/claude-commit` (falling back
|
|
5
|
+
* to `~/.config/claude-commit`). Per the XDG Base Directory spec, `XDG_CONFIG_HOME`
|
|
6
|
+
* is honoured only when it is set to an absolute path.
|
|
7
|
+
*/
|
|
8
|
+
export declare function globalConfigDir(env?: Record<string, string | undefined>): string;
|
|
9
|
+
/**
|
|
10
|
+
* Deep-ish merge of a partial config over a base config: `models` and
|
|
11
|
+
* `ollama` are merged key by key; the path lists (`lowPriorityPaths`,
|
|
12
|
+
* `ignore`) are replaced whole - a higher layer's list wins outright, so a
|
|
13
|
+
* project can drop a global pattern - and copied so the result never
|
|
14
|
+
* aliases the base's array.
|
|
15
|
+
*/
|
|
16
|
+
export declare function mergeConfig(base: Config, override: PartialConfig): Config;
|
|
17
|
+
/** Validate and normalize a parsed partial config, ignoring unknown keys. */
|
|
18
|
+
export declare function sanitizePartial(raw: unknown): PartialConfig;
|
|
19
|
+
/**
|
|
20
|
+
* Load and merge the file-based configuration layers that sit below CLI flags,
|
|
21
|
+
* lowest first: the global user config, then `package.json`'s `claude-commit` key
|
|
22
|
+
* at the repo root, then the nearest project config file (searched from `cwd` up
|
|
23
|
+
* to `repoRoot`). An explicit `configPath` short-circuits the project-file
|
|
24
|
+
* discovery; the global and `package.json` layers still apply beneath it. `env`
|
|
25
|
+
* supplies `XDG_CONFIG_HOME` for locating the global config (defaults to
|
|
26
|
+
* `process.env`).
|
|
27
|
+
*/
|
|
28
|
+
export declare function loadFileConfig(cwd: string, repoRoot: string, configPath?: string, env?: Record<string, string | undefined>): Promise<PartialConfig>;
|
|
29
|
+
/**
|
|
30
|
+
* Merge two partial configs: `models` and `ollama` are merged key by key;
|
|
31
|
+
* every other key, including both path lists, is taken whole from the
|
|
32
|
+
* override when present.
|
|
33
|
+
*/
|
|
34
|
+
export declare function mergePartial(base: PartialConfig, override: PartialConfig): PartialConfig;
|
|
35
|
+
/** Produce a fully-resolved config from file config and CLI-flag overrides. */
|
|
36
|
+
export declare function resolveConfig(fileConfig: PartialConfig, flagConfig: PartialConfig): Config;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Splitting a unified git diff into chunks that fit a character budget, and
|
|
3
|
+
* partitioning it by path priority.
|
|
4
|
+
*
|
|
5
|
+
* The packer is structure-aware: it prefers to break on file boundaries, then
|
|
6
|
+
* on hunk (`@@`) boundaries, and only falls back to raw line splitting for a
|
|
7
|
+
* single hunk that is itself larger than the budget. When a file section is
|
|
8
|
+
* split, its header (`diff --git ... / --- / +++`) is repeated at the top of
|
|
9
|
+
* every piece so each chunk remains a self-contained, interpretable diff.
|
|
10
|
+
*
|
|
11
|
+
* `applyIgnorePatterns` drops whole file sections outright (see `ignore` in
|
|
12
|
+
* the config) and `partitionDiff` sorts what remains into a primary and a
|
|
13
|
+
* low-priority diff (see `lowPriorityPaths`), both using the paths
|
|
14
|
+
* `sectionPaths` recovers from each section's header lines.
|
|
15
|
+
*/
|
|
16
|
+
import type { PathMatcher } from "./paths.ts";
|
|
17
|
+
/**
|
|
18
|
+
* Split a unified diff into chunks no larger than `maxChars` characters.
|
|
19
|
+
*
|
|
20
|
+
* Returns an empty array for an empty diff, and a single-element array when the
|
|
21
|
+
* whole diff already fits.
|
|
22
|
+
*/
|
|
23
|
+
export declare function splitDiff(diff: string, maxChars: number): string[];
|
|
24
|
+
/**
|
|
25
|
+
* Replace each run of opaque (armored/encoded) lines with a single marker
|
|
26
|
+
* line. The marker keeps the surrounding diff structure interpretable and
|
|
27
|
+
* tells the summary model what was elided, so it can still report that an
|
|
28
|
+
* encrypted file changed - without paying ~1 token per character to send
|
|
29
|
+
* ciphertext the model cannot read anyway.
|
|
30
|
+
*/
|
|
31
|
+
export declare function redactOpaqueRuns(diff: string): string;
|
|
32
|
+
/**
|
|
33
|
+
* The staged diff split by path priority. `primary` drives the commit
|
|
34
|
+
* message; `lowPriority` holds the sections whose every path matched a
|
|
35
|
+
* `lowPriorityPaths` pattern. Either may be `""`. The counts make the
|
|
36
|
+
* matcher observable (`--verbose`): a pattern that matches nothing and one
|
|
37
|
+
* that matches everything (and is therefore promoted) produce the same
|
|
38
|
+
* message otherwise.
|
|
39
|
+
*/
|
|
40
|
+
export interface DiffPartition {
|
|
41
|
+
primary: string;
|
|
42
|
+
lowPriority: string;
|
|
43
|
+
/** File sections whose paths all matched. */
|
|
44
|
+
matchedFiles: number;
|
|
45
|
+
/** File sections with at least one recognisable path. */
|
|
46
|
+
totalFiles: number;
|
|
47
|
+
/** Every file matched, so the low-priority sections were promoted to primary. */
|
|
48
|
+
promoted: boolean;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The repository-relative paths a file section touches: one for an ordinary
|
|
52
|
+
* change, two for a rename or copy. Paths are read from the `---`/`+++`
|
|
53
|
+
* marker lines and `rename`/`copy from`/`to` lines in the section header
|
|
54
|
+
* (before the first hunk, so a removed line that happens to start with
|
|
55
|
+
* `-- ` is never mistaken for a marker), falling back to the
|
|
56
|
+
* `diff --git a/X b/Y` line for sections that have none. Returns `[]` for
|
|
57
|
+
* content that is not a file section at all.
|
|
58
|
+
*/
|
|
59
|
+
export declare function sectionPaths(section: string): string[];
|
|
60
|
+
/**
|
|
61
|
+
* Unique repository-relative filenames touched by a diff, in encounter
|
|
62
|
+
* order. Includes both paths of renames and copies. Uses the same header
|
|
63
|
+
* parser as ignore/priority matching, so hunk contents cannot become names.
|
|
64
|
+
*/
|
|
65
|
+
export declare function diffPaths(diff: string): string[];
|
|
66
|
+
/**
|
|
67
|
+
* Sort a diff's file sections into a primary and a low-priority diff.
|
|
68
|
+
*
|
|
69
|
+
* A section is low priority only when it names at least one path and every
|
|
70
|
+
* path it names matches - so a rename into or out of a low-priority area
|
|
71
|
+
* stays primary, as does anything whose path cannot be recognised. Sections
|
|
72
|
+
* keep their original order within each partition and are joined back with
|
|
73
|
+
* newlines, so each partition is itself a valid unified diff.
|
|
74
|
+
*
|
|
75
|
+
* When nothing is primary, the low-priority sections are promoted: with no
|
|
76
|
+
* other change to yield to, they *are* the change and should be described
|
|
77
|
+
* in full, exactly as if no patterns were configured.
|
|
78
|
+
*/
|
|
79
|
+
export declare function partitionDiff(diff: string, isLowPriority: PathMatcher): DiffPartition;
|
|
80
|
+
/** What `ignore` removed from a diff. */
|
|
81
|
+
export interface IgnoreResult {
|
|
82
|
+
/** The diff with every ignored file section removed. May be `""`. */
|
|
83
|
+
diff: string;
|
|
84
|
+
/** File sections dropped because all of their paths matched. */
|
|
85
|
+
ignoredFiles: number;
|
|
86
|
+
/** File sections in the original diff with at least one recognisable path. */
|
|
87
|
+
totalFiles: number;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Drop the file sections whose every path matches, returning the rest.
|
|
91
|
+
*
|
|
92
|
+
* The rule is `partitionDiff`'s: a section is removed only when it names at
|
|
93
|
+
* least one path and all of them match, so a rename out of an ignored
|
|
94
|
+
* directory - which is news - survives, as does anything whose path cannot
|
|
95
|
+
* be read. Surviving sections keep their order and are joined with
|
|
96
|
+
* newlines, so the result is itself a valid unified diff.
|
|
97
|
+
*
|
|
98
|
+
* This runs before everything else in the pipeline, so ignored content is
|
|
99
|
+
* never chunked, never sent, and never paid for.
|
|
100
|
+
*/
|
|
101
|
+
export declare function applyIgnorePatterns(diff: string, isIgnored: PathMatcher): IgnoreResult;
|
|
102
|
+
/**
|
|
103
|
+
* Split a diff so that every chunk's *classified token estimate* fits
|
|
104
|
+
* `maxTokens`.
|
|
105
|
+
*
|
|
106
|
+
* `splitDiff` budgets in characters, but token density varies wildly by
|
|
107
|
+
* content: prose and code sit near the configured `charsPerToken` (~3.5)
|
|
108
|
+
* while base64/armor lines measure near 1 char/token. Sizing every chunk
|
|
109
|
+
* with one blended ratio lets an armor-heavy region overflow, so after an
|
|
110
|
+
* initial blended-density split, any chunk still over budget is re-split
|
|
111
|
+
* using its own (denser) ratio until everything fits or no further split is
|
|
112
|
+
* possible. An unsplittable oversized chunk is kept - the overflow retry in
|
|
113
|
+
* the generation pipeline is the backstop for that case.
|
|
114
|
+
*/
|
|
115
|
+
export declare function splitDiffToFit(diff: string, maxTokens: number, charsPerToken: number): string[];
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/** Error type for user-facing, expected failures (printed without a stack trace). */
|
|
2
|
-
export class ClaudeCommitError extends Error {
|
|
3
|
-
|
|
2
|
+
export declare class ClaudeCommitError extends Error {
|
|
3
|
+
name: string;
|
|
4
4
|
}
|
|
5
|
-
|
|
6
5
|
/**
|
|
7
6
|
* True when `error` is the backend rejecting a request for exceeding the
|
|
8
7
|
* model's context window. Matched on message text because the Agent SDK
|
|
@@ -10,6 +9,4 @@ export class ClaudeCommitError extends Error {
|
|
|
10
9
|
* re-split-and-retry: the rejection happens before the model runs, so it is
|
|
11
10
|
* not billed.
|
|
12
11
|
*/
|
|
13
|
-
export function isPromptTooLongError(error: unknown): boolean
|
|
14
|
-
return error instanceof Error && /prompt is too long/i.test(error.message);
|
|
15
|
-
}
|
|
12
|
+
export declare function isPromptTooLongError(error: unknown): boolean;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The commit-message pipeline:
|
|
3
|
+
*
|
|
4
|
+
* diff ──ignore──▶ ──partition──▶ primary diff, low-priority diff
|
|
5
|
+
* ──split──▶ [chunk, chunk, ...] ──summary model──▶ [summary, ...]
|
|
6
|
+
* ──final model──▶ commit message(s)
|
|
7
|
+
*
|
|
8
|
+
* The `ignore` patterns run first and remove file sections outright, so
|
|
9
|
+
* ignored content is never chunked, never sent and never paid for. Those
|
|
10
|
+
* files are still committed - `ignore` governs what the model reads, not
|
|
11
|
+
* what git stages - but when it matches *everything* there is nothing left
|
|
12
|
+
* to describe and the run stops rather than inventing a message.
|
|
13
|
+
*
|
|
14
|
+
* The remaining diff is partitioned by the configured `lowPriorityPaths`: file
|
|
15
|
+
* sections under those paths (generated docs, lockfiles, ...) form a
|
|
16
|
+
* low-priority partition that is summarised after, and more briefly than,
|
|
17
|
+
* the primary one, and the final model is told which is which so the
|
|
18
|
+
* subject line describes the primary changes. When every file is low
|
|
19
|
+
* priority the partition is promoted and the run is identical to one with
|
|
20
|
+
* no patterns configured.
|
|
21
|
+
*
|
|
22
|
+
* The summary model (default `sonnet`) reads each diff chunk and writes a
|
|
23
|
+
* factual summary; chunks are sized by a content-classified token estimate
|
|
24
|
+
* (`splitDiffToFit`) so each request fits the model's context window, and a
|
|
25
|
+
* chunk the backend still rejects as too long is re-split with a halved
|
|
26
|
+
* budget and retried - the rejection happens before the model runs and is
|
|
27
|
+
* not billed, so the API acts as the final arbiter of token counts. The
|
|
28
|
+
* final model (default `sonnet`) turns the summaries into the commit
|
|
29
|
+
* message(s), applying the configured formatting rules.
|
|
30
|
+
* With filenamesOnly, the summary stage is skipped entirely and the final
|
|
31
|
+
* model receives only paths from the filtered, priority-grouped diff.
|
|
32
|
+
*/
|
|
33
|
+
import { runPrompt } from "./agent.ts";
|
|
34
|
+
import { resolveOllamaContext } from "./ollama.ts";
|
|
35
|
+
import type { Config, DiffSummary } from "./types.ts";
|
|
36
|
+
export interface GenerateProgress {
|
|
37
|
+
/** Called when a new phase of work begins (for spinner labels). */
|
|
38
|
+
onPhase?: (label: string) => void;
|
|
39
|
+
/** Receives streamed text of the final message as it is produced. */
|
|
40
|
+
onText?: (delta: string) => void;
|
|
41
|
+
}
|
|
42
|
+
export interface GenerateOptions {
|
|
43
|
+
/** Number of candidate messages to produce (interactive mode uses > 1). */
|
|
44
|
+
count?: number;
|
|
45
|
+
progress?: GenerateProgress;
|
|
46
|
+
abortController?: AbortController;
|
|
47
|
+
/**
|
|
48
|
+
* Model runner used for every prompt; injectable so tests can exercise the
|
|
49
|
+
* pipeline (including overflow retries) without real model calls.
|
|
50
|
+
* Defaults to {@link runPrompt}.
|
|
51
|
+
*/
|
|
52
|
+
runner?: typeof runPrompt;
|
|
53
|
+
/**
|
|
54
|
+
* Resolves an `ollama:` model's context window, called once per model
|
|
55
|
+
* per run before any chunk is sized; injectable so tests can exercise an
|
|
56
|
+
* `"auto"` configuration without a server. Defaults to
|
|
57
|
+
* {@link resolveOllamaContext}.
|
|
58
|
+
*/
|
|
59
|
+
resolveOllamaContext?: typeof resolveOllamaContext;
|
|
60
|
+
}
|
|
61
|
+
/** The context window one Ollama model ran with during this run. */
|
|
62
|
+
export interface OllamaContextWindow {
|
|
63
|
+
/** The model string as configured, prefix included. */
|
|
64
|
+
model: string;
|
|
65
|
+
tokens: number;
|
|
66
|
+
/** Whether the number was configured or chosen by the server (`"auto"`). */
|
|
67
|
+
source: "config" | "auto";
|
|
68
|
+
}
|
|
69
|
+
/** How the `ignore` patterns applied to this diff (for `--verbose`). */
|
|
70
|
+
export interface IgnoreStats {
|
|
71
|
+
/** File sections dropped before any model saw them. */
|
|
72
|
+
ignoredFiles: number;
|
|
73
|
+
/** File sections in the staged diff with a recognisable path. */
|
|
74
|
+
totalFiles: number;
|
|
75
|
+
}
|
|
76
|
+
/** How the `lowPriorityPaths` patterns applied to this diff (for `--verbose`). */
|
|
77
|
+
export interface LowPriorityStats {
|
|
78
|
+
/** File sections whose paths all matched a pattern. */
|
|
79
|
+
matchedFiles: number;
|
|
80
|
+
/** File sections in the diff with a recognisable path. */
|
|
81
|
+
totalFiles: number;
|
|
82
|
+
/** Every file matched, so the changes were treated as primary after all. */
|
|
83
|
+
promoted: boolean;
|
|
84
|
+
}
|
|
85
|
+
export interface GenerateResult {
|
|
86
|
+
/** Candidate commit messages (length 1 in non-interactive mode). */
|
|
87
|
+
messages: string[];
|
|
88
|
+
/** Intermediate summaries, primary first. Empty when filenamesOnly is enabled. */
|
|
89
|
+
summaries: DiffSummary[];
|
|
90
|
+
/** Number of diff chunks the summary stage processed, across both partitions. */
|
|
91
|
+
chunkCount: number;
|
|
92
|
+
/** Total cost across all model calls, in USD. */
|
|
93
|
+
costUsd: number;
|
|
94
|
+
/** How the low-priority patterns applied to this diff. */
|
|
95
|
+
lowPriority: LowPriorityStats;
|
|
96
|
+
/** How the ignore patterns applied to this diff. */
|
|
97
|
+
ignored: IgnoreStats;
|
|
98
|
+
/** The context window each Ollama model ran with, in order of first use. */
|
|
99
|
+
ollamaContexts: OllamaContextWindow[];
|
|
100
|
+
}
|
|
101
|
+
/** Run the full pipeline over a staged diff. */
|
|
102
|
+
export declare function generateCommit(diff: string, config: Config, options?: GenerateOptions): Promise<GenerateResult>;
|
|
103
|
+
/**
|
|
104
|
+
* The error for a commit whose every changed file matched `ignore`.
|
|
105
|
+
*
|
|
106
|
+
* There is no sensible fallback here. Describing the ignored files anyway
|
|
107
|
+
* would contradict the directive the user wrote; committing an empty or
|
|
108
|
+
* invented message would be worse. Naming the directive and the count makes
|
|
109
|
+
* the cause obvious, because the alternative - a run that mysteriously
|
|
110
|
+
* reports no staged changes when `git status` plainly disagrees - is the
|
|
111
|
+
* kind of bug people spend an afternoon on.
|
|
112
|
+
*/
|
|
113
|
+
export declare function describeFullyIgnored(stats: IgnoreStats): string;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ClaudeCommitError } from "./errors.ts";
|
|
2
|
+
import type { FileChange } from "./types.ts";
|
|
3
|
+
/**
|
|
4
|
+
* A git command failed. Subclasses {@link ClaudeCommitError} so the CLI prints
|
|
5
|
+
* it as a clean, user-facing error rather than a stack trace.
|
|
6
|
+
*/
|
|
7
|
+
export declare class GitError extends ClaudeCommitError {
|
|
8
|
+
name: string;
|
|
9
|
+
}
|
|
10
|
+
/** True if the current working directory is inside a git work tree. */
|
|
11
|
+
export declare function isGitRepo(): Promise<boolean>;
|
|
12
|
+
/** Absolute path to the repository root. */
|
|
13
|
+
export declare function getRepoRoot(): Promise<string>;
|
|
14
|
+
/** The unified diff of staged changes (`git diff --cached`), repository-root-relative. */
|
|
15
|
+
export declare function getStagedDiff(): Promise<string>;
|
|
16
|
+
/** Parsed list of staged files with their status codes. */
|
|
17
|
+
export declare function getStagedFiles(): Promise<FileChange[]>;
|
|
18
|
+
/** Stage every change in the work tree (`git add -A`). */
|
|
19
|
+
export declare function stageAll(): Promise<void>;
|
|
20
|
+
/** A short one-line stat summary of staged changes (for display). */
|
|
21
|
+
export declare function getStagedStat(): Promise<string>;
|
|
22
|
+
/**
|
|
23
|
+
* Create a commit with the given message. The message is piped to
|
|
24
|
+
* `git commit -F -` over stdin, so arbitrary content (leading dashes, multiple
|
|
25
|
+
* lines, special characters) is handled safely - and nothing touches disk, so
|
|
26
|
+
* there is no temp file to be raced or read by another user. Git's own
|
|
27
|
+
* stdout summary is discarded: the CLI prints its own confirmation.
|
|
28
|
+
*/
|
|
29
|
+
export declare function commit(message: string): Promise<void>;
|
|
30
|
+
/** The current branch name (or `HEAD` when detached). */
|
|
31
|
+
export declare function getCurrentBranch(): Promise<string>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Marks a model name as belonging to an Ollama server. Case-insensitive. */
|
|
2
|
+
export declare const OLLAMA_PREFIX = "ollama:";
|
|
3
|
+
/** Which backend serves a model. */
|
|
4
|
+
export type ModelProvider = "claude" | "ollama";
|
|
5
|
+
/** A model string resolved into a provider and the name that provider expects. */
|
|
6
|
+
export interface ModelRef {
|
|
7
|
+
provider: ModelProvider;
|
|
8
|
+
/** The model name to send to the provider, with any cco prefix removed. */
|
|
9
|
+
name: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Default Ollama base URL, used when neither the config nor `$OLLAMA_HOST`
|
|
13
|
+
* names one. This is Ollama's own default listen address.
|
|
14
|
+
*/
|
|
15
|
+
export declare const DEFAULT_OLLAMA_HOST = "http://localhost:11434";
|
|
16
|
+
/**
|
|
17
|
+
* Default `ollama.context`: ask the server what window it would run the
|
|
18
|
+
* model with on this machine, rather than guess (see `probeOllamaContext`
|
|
19
|
+
* in `src/ollama.ts`). Ollama's choice is made from available VRAM and
|
|
20
|
+
* capped at the model's trained maximum, so it is the largest window the
|
|
21
|
+
* server believes it can actually load.
|
|
22
|
+
*/
|
|
23
|
+
export declare const DEFAULT_OLLAMA_CONTEXT: "auto";
|
|
24
|
+
/**
|
|
25
|
+
* The window assumed for an `ollama:` model when nothing better is known:
|
|
26
|
+
* the synchronous fallback in `contextWindowTokens` for callers that size
|
|
27
|
+
* chunks without first resolving the context. The pipeline never relies on
|
|
28
|
+
* it - it resolves `"auto"` to a real number before sizing - so this only
|
|
29
|
+
* matters to direct library use. 32768 is Ollama's middle VRAM tier.
|
|
30
|
+
*/
|
|
31
|
+
export declare const DEFAULT_OLLAMA_CONTEXT_TOKENS = 32768;
|
|
32
|
+
/** Whether `model` names an Ollama model (i.e. carries the `ollama:` prefix). */
|
|
33
|
+
export declare function isOllamaModel(model: string): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Resolve a configured model string into its provider and provider-side name.
|
|
36
|
+
*
|
|
37
|
+
* Throws {@link ClaudeCommitError} for a name that no provider could serve:
|
|
38
|
+
* an empty string, or an `ollama:` prefix with nothing after it.
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseModelRef(model: string): ModelRef;
|
|
41
|
+
/** A model string as it should appear in an error or a `--verbose` line. */
|
|
42
|
+
export declare function describeModel(model: string): string;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { ModelResult, OllamaConfig, RunPromptOptions } from "./types.ts";
|
|
2
|
+
/** Ollama settings with every default filled in; the context may still be `"auto"`. */
|
|
3
|
+
export interface ResolvedOllama {
|
|
4
|
+
host: string;
|
|
5
|
+
context: number | "auto";
|
|
6
|
+
keepAlive: string | number | null;
|
|
7
|
+
}
|
|
8
|
+
/** {@link ResolvedOllama} after `"auto"` has been turned into a number. */
|
|
9
|
+
export interface OllamaRequestSettings {
|
|
10
|
+
host: string;
|
|
11
|
+
contextTokens: number;
|
|
12
|
+
keepAlive: string | number | null;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Normalise a base URL: add a scheme to a bare `host:port` and drop any
|
|
16
|
+
* trailing slash. Ollama's own `OLLAMA_HOST` convention allows the bare
|
|
17
|
+
* form, so `127.0.0.1:11434` has to mean what a user expects it to.
|
|
18
|
+
*/
|
|
19
|
+
export declare function normaliseOllamaHost(host: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* The Ollama base URL for this run: the configured `ollama.host`, else
|
|
22
|
+
* `$OLLAMA_HOST`, else {@link DEFAULT_OLLAMA_HOST}.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveOllamaHost(configured?: string, env?: Record<string, string | undefined>): string;
|
|
25
|
+
/**
|
|
26
|
+
* Fill in defaults for any Ollama setting the config left out. A missing
|
|
27
|
+
* or unusable `context` becomes `"auto"`; turning that into a number is
|
|
28
|
+
* {@link resolveOllamaContext}'s job, because it takes a round trip.
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolveOllamaConfig(config: Partial<OllamaConfig> | undefined, env?: Record<string, string | undefined>): ResolvedOllama;
|
|
31
|
+
/**
|
|
32
|
+
* Ask Ollama what context window it would run `model` with on this machine.
|
|
33
|
+
*
|
|
34
|
+
* Two calls. The first is a chat request with no messages, which loads the
|
|
35
|
+
* model (a no-op if it is already resident) *without* a `num_ctx` - so the
|
|
36
|
+
* server applies its own choice, made from available VRAM (4k / 32k / 256k
|
|
37
|
+
* tiers, capped at the model's trained maximum). The second reads that
|
|
38
|
+
* choice back from `/api/ps`, which reports the window each loaded model is
|
|
39
|
+
* actually running with. The load was going to happen on the first real
|
|
40
|
+
* request anyway, so the only added cost is the `ps` round trip.
|
|
41
|
+
*
|
|
42
|
+
* This is deliberately not `/api/show`'s `context_length`, which is the
|
|
43
|
+
* *trained* maximum regardless of hardware - 131072 for a model this
|
|
44
|
+
* machine may only be able to run at 32768. The number the server picked
|
|
45
|
+
* is the one it can actually load.
|
|
46
|
+
*/
|
|
47
|
+
export declare function probeOllamaContext(model: string, settings: {
|
|
48
|
+
host: string;
|
|
49
|
+
keepAlive: string | number | null;
|
|
50
|
+
}, signal?: AbortSignal): Promise<number>;
|
|
51
|
+
/**
|
|
52
|
+
* The context window to use for `model`: the configured number, or the
|
|
53
|
+
* server's own choice when the config says `"auto"` (see
|
|
54
|
+
* {@link probeOllamaContext}). Callers that make several requests to the
|
|
55
|
+
* same model should resolve once and reuse the result.
|
|
56
|
+
*/
|
|
57
|
+
export declare function resolveOllamaContext(model: string, config: Partial<OllamaConfig> | undefined, signal?: AbortSignal): Promise<number>;
|
|
58
|
+
/** The body of a native `/api/chat` request. */
|
|
59
|
+
export interface OllamaChatRequest {
|
|
60
|
+
model: string;
|
|
61
|
+
messages: Array<{
|
|
62
|
+
role: "system" | "user";
|
|
63
|
+
content: string;
|
|
64
|
+
}>;
|
|
65
|
+
stream: boolean;
|
|
66
|
+
format?: Record<string, unknown>;
|
|
67
|
+
keep_alive?: string | number;
|
|
68
|
+
options: Record<string, unknown>;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Build the `/api/chat` body for one prompt.
|
|
72
|
+
*
|
|
73
|
+
* Two things are deliberate. Sampling parameters go inside `options` - at the
|
|
74
|
+
* top level Ollama accepts and silently ignores them, so a misplaced
|
|
75
|
+
* `temperature` would look like a model that refuses to vary. And `think` is
|
|
76
|
+
* never sent at all: models disagree on whether reasoning can be switched
|
|
77
|
+
* off (gpt-oss cannot, and takes only a level; Granite uses its own field
|
|
78
|
+
* entirely), so asking is a needless way to earn a 400. Any `thinking` that
|
|
79
|
+
* comes back is dropped on the floor instead.
|
|
80
|
+
*/
|
|
81
|
+
export declare function buildChatRequest(prompt: string, opts: RunPromptOptions, settings: OllamaRequestSettings): OllamaChatRequest;
|
|
82
|
+
/**
|
|
83
|
+
* Run a single prompt against an Ollama model and return its response.
|
|
84
|
+
*
|
|
85
|
+
* Throws {@link ClaudeCommitError} on any transport, model or truncation
|
|
86
|
+
* failure. `costUsd` is always zero: local inference is not billed, so a
|
|
87
|
+
* mixed-provider run's reported cost is exactly its Claude half.
|
|
88
|
+
*/
|
|
89
|
+
export declare function runOllamaPrompt(prompt: string, opts: RunPromptOptions): Promise<ModelResult>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** A predicate over repository-relative paths. */
|
|
2
|
+
export type PathMatcher = (path: string) => boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Build a matcher for a list of gitignore-style patterns. Compile once per
|
|
5
|
+
* run and reuse it across every path in the diff.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createPathMatcher(patterns: string[]): PathMatcher;
|
|
8
|
+
/** Whether `path` matches any of the gitignore-style `patterns`. */
|
|
9
|
+
export declare function matchesPathPatterns(path: string, patterns: string[]): boolean;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt construction for the two-stage pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Stage 1 (summary model): read a diff chunk and describe the change factually.
|
|
5
|
+
* Stage 2 (final model): turn the summaries into a commit message that obeys the
|
|
6
|
+
* configured formatting rules (conventional commits, gitmoji, template, body).
|
|
7
|
+
*/
|
|
8
|
+
import type { ChangePriority, Config, DiffSummary } from "./types.ts";
|
|
9
|
+
/** Sentinel separating candidate messages in interactive mode. */
|
|
10
|
+
export declare const OPTION_DELIMITER = "===OPTION===";
|
|
11
|
+
/**
|
|
12
|
+
* System prompt for the diff-summarization stage. The low-priority variant
|
|
13
|
+
* asks for a deliberately short summary: the final model only needs to know
|
|
14
|
+
* which areas changed and how, so the churn cannot crowd out the primary
|
|
15
|
+
* changes when the summaries are combined.
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildSummarySystem(priority?: ChangePriority): string;
|
|
18
|
+
/** User prompt for a single diff chunk in the summarization stage. */
|
|
19
|
+
export declare function buildSummaryUser(chunk: string, index: number, total: number, priority?: ChangePriority): string;
|
|
20
|
+
/**
|
|
21
|
+
* JSON schema for the final stage's structured output: a list of candidate
|
|
22
|
+
* commit messages. Requesting this makes parsing robust regardless of how the
|
|
23
|
+
* model chooses to format its prose.
|
|
24
|
+
*/
|
|
25
|
+
export declare const MESSAGES_SCHEMA: Record<string, unknown>;
|
|
26
|
+
/** Pull the message list out of a structured-output object, or return null if malformed. */
|
|
27
|
+
export declare function extractMessages(structured: unknown): string[] | null;
|
|
28
|
+
/**
|
|
29
|
+
* System prompt for the final commit-message stage, encoding all formatting
|
|
30
|
+
* rules. When `structured` is true, the model returns its messages as JSON, so
|
|
31
|
+
* the "no markdown" guidance is scoped to each message's own text. When
|
|
32
|
+
* `hasLowPriority` is true the summaries come in two priority groups and the
|
|
33
|
+
* weighting rules ({@link lowPriorityWeightingRules}) are added between the
|
|
34
|
+
* subject-line rules and the body rule, in the order the constraints apply.
|
|
35
|
+
*/
|
|
36
|
+
export declare function buildFinalSystem(config: Config, structured?: boolean, hasLowPriority?: boolean): string;
|
|
37
|
+
/** Whether the summaries span both priority groups (the only case that needs the weighting rules). */
|
|
38
|
+
export declare function hasLowPrioritySummaries(summaries: DiffSummary[]): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* User prompt for the final stage.
|
|
41
|
+
*
|
|
42
|
+
* Summaries are presented by priority group (see {@link describeSummaries}).
|
|
43
|
+
* In `structured` mode the candidates are returned via {@link MESSAGES_SCHEMA}'s
|
|
44
|
+
* `messages` array. Otherwise, when `count` > 1, they are separated by
|
|
45
|
+
* {@link OPTION_DELIMITER} for text parsing.
|
|
46
|
+
*/
|
|
47
|
+
export declare function buildFinalUser(summaries: DiffSummary[], count?: number, structured?: boolean): string;
|
|
48
|
+
/**
|
|
49
|
+
* Final-stage input for filenamesOnly. JSON-quoted paths keep embedded
|
|
50
|
+
* newlines and quotes inside a single list item. No diff content is included.
|
|
51
|
+
*/
|
|
52
|
+
export declare function buildFilenamesUser(filenames: {
|
|
53
|
+
primary: string[];
|
|
54
|
+
lowPriority: string[];
|
|
55
|
+
}, count?: number, structured?: boolean): string;
|
|
56
|
+
/** Parse the multi-option response from the final stage into individual messages. */
|
|
57
|
+
export declare function parseOptions(text: string): string[];
|
|
58
|
+
/** Strip stray formatting a model may add despite instructions (fences, wrapping quotes). */
|
|
59
|
+
export declare function cleanMessage(text: string): string;
|