@synmux/claude-commit 1.0.1 → 1.0.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/README.md +208 -5
- package/index.ts +31 -2
- package/package.json +28 -16
- package/src/agent.ts +36 -43
- package/src/cli.ts +129 -10
- package/src/config.ts +89 -6
- package/src/diff.ts +339 -14
- package/src/generate.ts +270 -47
- 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 +148 -27
- package/src/tokens.ts +47 -9
- package/src/types.ts +141 -3
- package/src/ui/spinner.ts +1 -1
package/src/prompts.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Stage 2 (final model): turn the summaries into a commit message that obeys the
|
|
6
6
|
* configured formatting rules (conventional commits, gitmoji, template, body).
|
|
7
7
|
*/
|
|
8
|
-
import type { Config } from "./types";
|
|
8
|
+
import type { ChangePriority, Config, DiffSummary } from "./types";
|
|
9
9
|
|
|
10
10
|
/** Sentinel separating candidate messages in interactive mode. */
|
|
11
11
|
export const OPTION_DELIMITER = "===OPTION===";
|
|
@@ -31,12 +31,39 @@ const GITMOJI_GUIDE = [
|
|
|
31
31
|
const CONVENTIONAL_TYPES =
|
|
32
32
|
"feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert";
|
|
33
33
|
|
|
34
|
-
/**
|
|
35
|
-
|
|
34
|
+
/**
|
|
35
|
+
* What "low priority" means, phrased once for both stages so the summary
|
|
36
|
+
* model and the final model share the same picture of the content.
|
|
37
|
+
*/
|
|
38
|
+
const LOW_PRIORITY_DESCRIPTION =
|
|
39
|
+
"paths the user has marked as low priority - typically generated or vendored content such as " +
|
|
40
|
+
"tool-generated documentation, lockfiles, snapshots or build output - whose changes matter less " +
|
|
41
|
+
"than the rest of the commit";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* System prompt for the diff-summarization stage. The low-priority variant
|
|
45
|
+
* asks for a deliberately short summary: the final model only needs to know
|
|
46
|
+
* which areas changed and how, so the churn cannot crowd out the primary
|
|
47
|
+
* changes when the summaries are combined.
|
|
48
|
+
*/
|
|
49
|
+
export function buildSummarySystem(
|
|
50
|
+
priority: ChangePriority = "primary",
|
|
51
|
+
): string {
|
|
52
|
+
const role =
|
|
53
|
+
"You are an expert software engineer analyzing a git diff in preparation for writing a commit message.";
|
|
54
|
+
const guidance =
|
|
55
|
+
priority === "low"
|
|
56
|
+
? [
|
|
57
|
+
`The diff you are given comes from ${LOW_PRIORITY_DESCRIPTION}.`,
|
|
58
|
+
"Summarize it briefly: a few sentences at most, naming which files or areas changed and the nature of the change (regenerated, bumped, added, removed), without describing individual edits.",
|
|
59
|
+
]
|
|
60
|
+
: [
|
|
61
|
+
"Summarize the change factually and concisely: which files changed, what was added, removed or modified, and the apparent intent and impact of the change.",
|
|
62
|
+
"Focus on the substance of the change, not a line-by-line readout.",
|
|
63
|
+
];
|
|
36
64
|
return [
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"Focus on the substance of the change, not a line-by-line readout.",
|
|
65
|
+
role,
|
|
66
|
+
...guidance,
|
|
40
67
|
"Do not write a commit message. Do not include code fences or the raw diff.",
|
|
41
68
|
"If you are told this is one part of a larger change, summarize only the part you are given.",
|
|
42
69
|
].join(" ");
|
|
@@ -47,11 +74,13 @@ export function buildSummaryUser(
|
|
|
47
74
|
chunk: string,
|
|
48
75
|
index: number,
|
|
49
76
|
total: number,
|
|
77
|
+
priority: ChangePriority = "primary",
|
|
50
78
|
): string {
|
|
79
|
+
const subject = priority === "low" ? "low-priority diff" : "diff";
|
|
51
80
|
const preamble =
|
|
52
81
|
total > 1
|
|
53
|
-
? `This is part ${index + 1} of ${total} of a larger
|
|
54
|
-
:
|
|
82
|
+
? `This is part ${index + 1} of ${total} of a larger ${subject}. Summarize only this part:`
|
|
83
|
+
: `Summarize the following ${subject}:`;
|
|
55
84
|
return `${preamble}\n\n${chunk}`;
|
|
56
85
|
}
|
|
57
86
|
|
|
@@ -89,12 +118,49 @@ export function extractMessages(structured: unknown): string[] | null {
|
|
|
89
118
|
return null;
|
|
90
119
|
}
|
|
91
120
|
|
|
121
|
+
/**
|
|
122
|
+
* The weighting rules for a change with both primary and low-priority parts.
|
|
123
|
+
* They live in the system prompt next to the other subject-line rules so
|
|
124
|
+
* they carry the same authority, and every clause is branched on the config
|
|
125
|
+
* exactly as those rules are: a plain single-line setup is never told about
|
|
126
|
+
* a type, a gitmoji or a body it was not asked for. The wording is absolute
|
|
127
|
+
* and size-independent on purpose - a twenty-line fix next to thousands of
|
|
128
|
+
* regenerated lines must still read as a fix, however dull the fix is.
|
|
129
|
+
*/
|
|
130
|
+
function lowPriorityWeightingRules(config: Config): string[] {
|
|
131
|
+
const rules = [
|
|
132
|
+
`The summary is split into primary changes and low-priority changes (${LOW_PRIORITY_DESCRIPTION}). ` +
|
|
133
|
+
"The primary changes are what this commit is about.",
|
|
134
|
+
"The subject line describes the primary changes. This holds however small or routine the primary changes are " +
|
|
135
|
+
"and however many files or lines the low-priority changes touch: a one-line primary change still owns the subject. " +
|
|
136
|
+
"If the primary changes seem too small to fill a subject line, write a short subject about them anyway rather than " +
|
|
137
|
+
"reaching for the low-priority changes to pad it. " +
|
|
138
|
+
"Mention the low-priority changes in the subject only if they fit naturally without displacing anything about the primary changes.",
|
|
139
|
+
];
|
|
140
|
+
if (config.conventionalCommits) {
|
|
141
|
+
rules.push(
|
|
142
|
+
"Choose the commit type and scope from the primary changes alone.",
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
if (config.gitmoji) {
|
|
146
|
+
rules.push("Choose the gitmoji from the primary changes alone.");
|
|
147
|
+
}
|
|
148
|
+
return rules;
|
|
149
|
+
}
|
|
150
|
+
|
|
92
151
|
/**
|
|
93
152
|
* System prompt for the final commit-message stage, encoding all formatting
|
|
94
153
|
* rules. When `structured` is true, the model returns its messages as JSON, so
|
|
95
|
-
* the "no markdown" guidance is scoped to each message's own text.
|
|
154
|
+
* the "no markdown" guidance is scoped to each message's own text. When
|
|
155
|
+
* `hasLowPriority` is true the summaries come in two priority groups and the
|
|
156
|
+
* weighting rules ({@link lowPriorityWeightingRules}) are added between the
|
|
157
|
+
* subject-line rules and the body rule, in the order the constraints apply.
|
|
96
158
|
*/
|
|
97
|
-
export function buildFinalSystem(
|
|
159
|
+
export function buildFinalSystem(
|
|
160
|
+
config: Config,
|
|
161
|
+
structured = false,
|
|
162
|
+
hasLowPriority = false,
|
|
163
|
+
): string {
|
|
98
164
|
const rules: string[] = [
|
|
99
165
|
"You are an expert at writing clear, high-quality git commit messages.",
|
|
100
166
|
"You are given a summary of staged changes and must produce a commit message for them.",
|
|
@@ -132,10 +198,15 @@ export function buildFinalSystem(config: Config, structured = false): string {
|
|
|
132
198
|
);
|
|
133
199
|
}
|
|
134
200
|
|
|
201
|
+
if (hasLowPriority) rules.push(...lowPriorityWeightingRules(config));
|
|
202
|
+
|
|
135
203
|
if (config.multiline) {
|
|
136
204
|
rules.push(
|
|
137
205
|
"After the subject line, add one blank line and then a body that explains what changed and why. " +
|
|
138
|
-
'Use concise bullet points ("- ...") when there are several distinct changes. Wrap body lines at about 72 characters.'
|
|
206
|
+
'Use concise bullet points ("- ...") when there are several distinct changes. Wrap body lines at about 72 characters.' +
|
|
207
|
+
(hasLowPriority
|
|
208
|
+
? " Cover the primary changes first and in full, then reference the low-priority changes briefly after them."
|
|
209
|
+
: ""),
|
|
139
210
|
);
|
|
140
211
|
} else {
|
|
141
212
|
rules.push("Output only the single subject line. Do not include a body.");
|
|
@@ -163,51 +234,101 @@ export function buildFinalSystem(config: Config, structured = false): string {
|
|
|
163
234
|
* manufacture variety, so `multiline` appeared to be ignored in interactive
|
|
164
235
|
* mode even though the system prompt still required a body.
|
|
165
236
|
*/
|
|
166
|
-
function multiOptionInstruction(count: number): string {
|
|
237
|
+
function multiOptionInstruction(count: number, hasLowPriority = false): string {
|
|
238
|
+
// With two priority groups, "different in emphasis" would license one
|
|
239
|
+
// option to lead with the churn - and this is the last instruction the
|
|
240
|
+
// model reads - so the variety axis is scoped to the primary changes and
|
|
241
|
+
// the subject rule is re-anchored. With one group the wording is untouched.
|
|
242
|
+
const variety = hasLowPriority
|
|
243
|
+
? "Make the options genuinely different in wording and in which aspect of the primary changes they emphasise, " +
|
|
244
|
+
"but never drop the subject or a required body just to create variety. " +
|
|
245
|
+
"Each option's subject line describes the primary changes."
|
|
246
|
+
: "Make the options genuinely different in wording and emphasis, but never drop the subject or a required body just to create variety.";
|
|
167
247
|
return (
|
|
168
248
|
`Produce exactly ${count} distinct commit-message options for this change. ` +
|
|
169
249
|
`Each option must be a complete commit message that independently obeys all the formatting rules above - ` +
|
|
170
250
|
`including the blank line and body when those rules ask for one. ` +
|
|
171
|
-
|
|
251
|
+
variety
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Join a group of summaries, numbering them as parts when there are several. */
|
|
256
|
+
function joinSummaryTexts(texts: string[]): string {
|
|
257
|
+
return texts.length === 1
|
|
258
|
+
? texts[0]!
|
|
259
|
+
: texts.map((text, index) => `Part ${index + 1}:\n${text}`).join("\n\n");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Whether the summaries span both priority groups (the only case that needs the weighting rules). */
|
|
263
|
+
export function hasLowPrioritySummaries(summaries: DiffSummary[]): boolean {
|
|
264
|
+
return (
|
|
265
|
+
summaries.some((summary) => summary.priority === "low") &&
|
|
266
|
+
summaries.some((summary) => summary.priority === "primary")
|
|
172
267
|
);
|
|
173
268
|
}
|
|
174
269
|
|
|
270
|
+
/**
|
|
271
|
+
* Present the summaries to the final model. With a single priority group the
|
|
272
|
+
* layout is the plain one; with both groups present they are labelled,
|
|
273
|
+
* primary first, and closed with a one-line anchor back to the primary
|
|
274
|
+
* changes. The weighting rules themselves live in the system prompt
|
|
275
|
+
* ({@link buildFinalSystem}); the user turn only carries the data.
|
|
276
|
+
*/
|
|
277
|
+
function describeSummaries(summaries: DiffSummary[]): string {
|
|
278
|
+
const primaryTexts = summaries
|
|
279
|
+
.filter((summary) => summary.priority === "primary")
|
|
280
|
+
.map((summary) => summary.text);
|
|
281
|
+
const lowTexts = summaries
|
|
282
|
+
.filter((summary) => summary.priority === "low")
|
|
283
|
+
.map((summary) => summary.text);
|
|
284
|
+
|
|
285
|
+
if (!hasLowPrioritySummaries(summaries)) {
|
|
286
|
+
const texts = primaryTexts.length > 0 ? primaryTexts : lowTexts;
|
|
287
|
+
const header =
|
|
288
|
+
texts.length === 1
|
|
289
|
+
? "Here is the summary of the staged changes:"
|
|
290
|
+
: "Here are summaries of the parts of the staged changes:";
|
|
291
|
+
return `${header}\n\n${joinSummaryTexts(texts)}`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return [
|
|
295
|
+
"Here are summaries of the staged changes, in two groups.",
|
|
296
|
+
`Primary changes (what this commit is about):\n\n${joinSummaryTexts(primaryTexts)}`,
|
|
297
|
+
`Low-priority changes (${LOW_PRIORITY_DESCRIPTION}):\n\n${joinSummaryTexts(lowTexts)}`,
|
|
298
|
+
"The subject line is about the primary changes above.",
|
|
299
|
+
].join("\n\n");
|
|
300
|
+
}
|
|
301
|
+
|
|
175
302
|
/**
|
|
176
303
|
* User prompt for the final stage.
|
|
177
304
|
*
|
|
305
|
+
* Summaries are presented by priority group (see {@link describeSummaries}).
|
|
178
306
|
* In `structured` mode the candidates are returned via {@link MESSAGES_SCHEMA}'s
|
|
179
307
|
* `messages` array. Otherwise, when `count` > 1, they are separated by
|
|
180
308
|
* {@link OPTION_DELIMITER} for text parsing.
|
|
181
309
|
*/
|
|
182
310
|
export function buildFinalUser(
|
|
183
|
-
summaries:
|
|
311
|
+
summaries: DiffSummary[],
|
|
184
312
|
count = 1,
|
|
185
313
|
structured = false,
|
|
186
314
|
): string {
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
? summaries[0]!
|
|
190
|
-
: summaries.map((s, i) => `Part ${i + 1}:\n${s}`).join("\n\n");
|
|
191
|
-
|
|
192
|
-
const header =
|
|
193
|
-
summaries.length === 1
|
|
194
|
-
? "Here is the summary of the staged changes:"
|
|
195
|
-
: "Here are summaries of the parts of the staged changes:";
|
|
315
|
+
const described = describeSummaries(summaries);
|
|
316
|
+
const hasLowPriority = hasLowPrioritySummaries(summaries);
|
|
196
317
|
|
|
197
318
|
if (structured) {
|
|
198
319
|
const ask =
|
|
199
320
|
count <= 1
|
|
200
321
|
? `Produce a single commit message for this change and return it as the only element of the "messages" array.`
|
|
201
|
-
: `${multiOptionInstruction(count)} Return them in the "messages" array.`;
|
|
202
|
-
return `${
|
|
322
|
+
: `${multiOptionInstruction(count, hasLowPriority)} Return them in the "messages" array.`;
|
|
323
|
+
return `${described}\n\n${ask}`;
|
|
203
324
|
}
|
|
204
325
|
|
|
205
326
|
if (count <= 1) {
|
|
206
|
-
return
|
|
327
|
+
return described;
|
|
207
328
|
}
|
|
208
329
|
|
|
209
330
|
return (
|
|
210
|
-
`${
|
|
331
|
+
`${described}\n\n${multiOptionInstruction(count, hasLowPriority)} ` +
|
|
211
332
|
`Output each option on its own, preceded by a line containing exactly "${OPTION_DELIMITER}" and nothing else. ` +
|
|
212
333
|
`Do not number the options or add any other text.`
|
|
213
334
|
);
|
package/src/tokens.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* latency for no real benefit. We slightly over-estimate tokens so that chunks
|
|
8
8
|
* stay safely under the model's context window.
|
|
9
9
|
*/
|
|
10
|
+
import { DEFAULT_OLLAMA_CONTEXT_TOKENS, isOllamaModel } from "./models";
|
|
10
11
|
|
|
11
12
|
/** Estimate the number of tokens in `text` given a chars-per-token ratio. */
|
|
12
13
|
export function estimateTokens(text: string, charsPerToken: number): number {
|
|
@@ -33,31 +34,68 @@ export function tokensToChars(tokens: number, charsPerToken: number): number {
|
|
|
33
34
|
const MILLION_TOKEN_CONTEXT_MODELS =
|
|
34
35
|
/\[1m\]|^(claude-)?(sonnet|opus)$|sonnet-5|sonnet-4-6|opus-4-[678]|fable|mythos/i;
|
|
35
36
|
|
|
36
|
-
/**
|
|
37
|
-
|
|
37
|
+
/**
|
|
38
|
+
* The context window (input token capacity) for a model name or alias.
|
|
39
|
+
*
|
|
40
|
+
* For an `ollama:` model the window is not a property of the name at all -
|
|
41
|
+
* it is whatever `options.num_ctx` the request asks for, which cco pins to
|
|
42
|
+
* `ollama.contextTokens` so that chunk sizing and the request agree. Pass
|
|
43
|
+
* that value as `ollamaContextTokens`; the default matches
|
|
44
|
+
* {@link DEFAULT_OLLAMA_CONTEXT_TOKENS}.
|
|
45
|
+
*/
|
|
46
|
+
export function contextWindowTokens(
|
|
47
|
+
model: string,
|
|
48
|
+
ollamaContextTokens: number = DEFAULT_OLLAMA_CONTEXT_TOKENS,
|
|
49
|
+
): number {
|
|
50
|
+
if (isOllamaModel(model)) return Math.max(1, Math.floor(ollamaContextTokens));
|
|
38
51
|
return MILLION_TOKEN_CONTEXT_MODELS.test(model) ? 1_000_000 : 200_000;
|
|
39
52
|
}
|
|
40
53
|
|
|
41
54
|
/**
|
|
42
|
-
*
|
|
43
|
-
* system prompt, the
|
|
44
|
-
* Generous on purpose - `charsPerToken` is an estimate, and a
|
|
45
|
-
* overflows the window fails the whole run.
|
|
55
|
+
* Ceiling on the tokens reserved out of the context window before sizing
|
|
56
|
+
* diff chunks: the system prompt, the backend's scaffolding, and room for
|
|
57
|
+
* the response. Generous on purpose - `charsPerToken` is an estimate, and a
|
|
58
|
+
* chunk that overflows the window fails the whole run.
|
|
46
59
|
*/
|
|
47
60
|
export const CONTEXT_RESERVE_TOKENS = 32_000;
|
|
48
61
|
|
|
62
|
+
/**
|
|
63
|
+
* The fraction of a context window the reserve may take when the flat
|
|
64
|
+
* {@link CONTEXT_RESERVE_TOKENS} would swallow it. A local model running at
|
|
65
|
+
* 32k has a window smaller than the flat reserve, which would leave a
|
|
66
|
+
* budget of zero and shatter the diff into one chunk per line.
|
|
67
|
+
*/
|
|
68
|
+
const MAX_RESERVE_FRACTION = 4;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Tokens to hold back from `contextWindow` when sizing chunks: the flat
|
|
72
|
+
* reserve, or a quarter of the window when that is smaller. Both Claude
|
|
73
|
+
* tiers (200k and 1M) are far above the crossover, so they reserve the full
|
|
74
|
+
* 32k exactly as before; only windows under 128k - which in practice means
|
|
75
|
+
* Ollama - scale down.
|
|
76
|
+
*/
|
|
77
|
+
export function contextReserveTokens(contextWindow: number): number {
|
|
78
|
+
return Math.min(
|
|
79
|
+
CONTEXT_RESERVE_TOKENS,
|
|
80
|
+
Math.floor(contextWindow / MAX_RESERVE_FRACTION),
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
49
84
|
/**
|
|
50
85
|
* Clamp a configured per-chunk token budget so that one chunk plus overhead
|
|
51
86
|
* always fits the given model's context window. The configured
|
|
52
87
|
* `maxChunkTokens` remains the user-facing cap; this only ever lowers it.
|
|
88
|
+
* `ollamaContextTokens` supplies the window for an `ollama:` model.
|
|
53
89
|
*/
|
|
54
90
|
export function clampChunkTokens(
|
|
55
91
|
model: string,
|
|
56
92
|
maxChunkTokens: number,
|
|
93
|
+
ollamaContextTokens?: number,
|
|
57
94
|
): number {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
95
|
+
const window = contextWindowTokens(model, ollamaContextTokens);
|
|
96
|
+
return Math.max(
|
|
97
|
+
1,
|
|
98
|
+
Math.min(maxChunkTokens, window - contextReserveTokens(window)),
|
|
61
99
|
);
|
|
62
100
|
}
|
|
63
101
|
|
package/src/types.ts
CHANGED
|
@@ -2,7 +2,16 @@
|
|
|
2
2
|
* Shared types for claude-commit.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Which models to use for each stage of the pipeline.
|
|
7
|
+
*
|
|
8
|
+
* A bare name (`sonnet`, `haiku`, a full `claude-*` id) runs through the
|
|
9
|
+
* Claude Agent SDK. An `ollama:`-prefixed name runs against a local or
|
|
10
|
+
* self-hosted Ollama server instead, with everything after the prefix taken
|
|
11
|
+
* as the Ollama model name verbatim - tag included, so
|
|
12
|
+
* `ollama:ornith-1.5:35b` means the model `ornith-1.5:35b`. The two stages
|
|
13
|
+
* are resolved independently, so mixing providers is normal.
|
|
14
|
+
*/
|
|
6
15
|
export interface ModelConfig {
|
|
7
16
|
/** Model used to read diffs and write summaries. Defaults to `sonnet`. */
|
|
8
17
|
summary: string;
|
|
@@ -10,6 +19,44 @@ export interface ModelConfig {
|
|
|
10
19
|
final: string;
|
|
11
20
|
}
|
|
12
21
|
|
|
22
|
+
/** Settings for the Ollama backend, used only by `ollama:`-prefixed models. */
|
|
23
|
+
export interface OllamaConfig {
|
|
24
|
+
/**
|
|
25
|
+
* Base URL of the Ollama server. Defaults to `$OLLAMA_HOST`, falling back
|
|
26
|
+
* to `http://localhost:11434`. A bare `host:port` (Ollama's own
|
|
27
|
+
* convention for that variable) is given an `http://` scheme.
|
|
28
|
+
*/
|
|
29
|
+
host: string;
|
|
30
|
+
/**
|
|
31
|
+
* Context window requested for every Ollama call (`options.num_ctx`) and
|
|
32
|
+
* used to size diff chunks: a token count, or `"auto"` (the default) to
|
|
33
|
+
* take the window Ollama itself chooses for the model on this machine.
|
|
34
|
+
*
|
|
35
|
+
* The window is always sent explicitly, never left to the server: a
|
|
36
|
+
* prompt over it is truncated *silently* - HTTP 200, oldest content
|
|
37
|
+
* dropped, no flag on the response - and a summary written from half a
|
|
38
|
+
* diff is worse than an error, so cco pins the number it sized its chunks
|
|
39
|
+
* against and cross-checks the response's token counts.
|
|
40
|
+
*
|
|
41
|
+
* `"auto"` asks Ollama rather than guessing: the model is preloaded with
|
|
42
|
+
* no `num_ctx`, which makes the server pick from its VRAM tiers (4k / 32k
|
|
43
|
+
* / 256k, capped at the model's trained maximum), and the choice is read
|
|
44
|
+
* back from `/api/ps`. That is the largest window the server believes
|
|
45
|
+
* this machine can run, resolved once per model per run. A number pins
|
|
46
|
+
* the window instead - lower it when memory is tight (memory scales with
|
|
47
|
+
* it, multiplied by `OLLAMA_NUM_PARALLEL`), or raise it past the tier if
|
|
48
|
+
* you know better than the server does.
|
|
49
|
+
*/
|
|
50
|
+
context: number | "auto";
|
|
51
|
+
/**
|
|
52
|
+
* How long the server keeps the model loaded after a request: a duration
|
|
53
|
+
* string (`"10m"`), seconds as a number, `0` to unload immediately, or a
|
|
54
|
+
* negative value to pin it. `null` leaves the server's own default (which
|
|
55
|
+
* is itself 5 minutes unless `OLLAMA_KEEP_ALIVE` says otherwise).
|
|
56
|
+
*/
|
|
57
|
+
keepAlive: string | number | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
13
60
|
/** Fully-resolved configuration after merging defaults, file config and CLI flags. */
|
|
14
61
|
export interface Config {
|
|
15
62
|
/** Format the subject line as a Conventional Commit (`type(scope): description`). */
|
|
@@ -42,7 +89,7 @@ export interface Config {
|
|
|
42
89
|
interactiveTemperature: number | null;
|
|
43
90
|
/**
|
|
44
91
|
* Name of the progress spinner animation: any spinner from the cli-spinners
|
|
45
|
-
* set bundled with ora (e.g. `"dots"`, `"moon"`, `"
|
|
92
|
+
* set bundled with ora (e.g. `"dots"`, `"moon"`, `"material"`). Unknown
|
|
46
93
|
* names are ignored and the default is used instead.
|
|
47
94
|
*/
|
|
48
95
|
spinner: string;
|
|
@@ -64,6 +111,35 @@ export interface Config {
|
|
|
64
111
|
* losing anything a summary could actually use.
|
|
65
112
|
*/
|
|
66
113
|
skipArmored: boolean;
|
|
114
|
+
/**
|
|
115
|
+
* Gitignore-style patterns for paths whose changes matter less than the
|
|
116
|
+
* rest of the commit: generated docs, lockfiles, vendored snapshots, build
|
|
117
|
+
* output. Diff sections under these paths are summarised separately and
|
|
118
|
+
* briefly, and the final model is told to describe the other changes in
|
|
119
|
+
* the subject line and to mention these only after them. When every
|
|
120
|
+
* changed file matches, the changes are described normally - there is
|
|
121
|
+
* nothing else for them to yield to. A pattern containing `/` matches a
|
|
122
|
+
* path or any ancestor directory; a bare pattern matches any path segment
|
|
123
|
+
* (see `src/paths.ts`).
|
|
124
|
+
*/
|
|
125
|
+
lowPriorityPaths: string[];
|
|
126
|
+
/**
|
|
127
|
+
* Gitignore-style patterns - the same language as `lowPriorityPaths` - for
|
|
128
|
+
* paths whose changes should not be read at all: vendored dependency
|
|
129
|
+
* trees, generated clients, bulk data fixtures. Matching diff sections are
|
|
130
|
+
* dropped before anything else looks at the diff, so they cost no tokens
|
|
131
|
+
* and cannot influence the message.
|
|
132
|
+
*
|
|
133
|
+
* The files are still committed; this governs only what the model reads.
|
|
134
|
+
* When every changed file matches, there is nothing left to describe and
|
|
135
|
+
* the run stops with an error naming the directive - unlike
|
|
136
|
+
* `lowPriorityPaths`, which promotes its partition in that case, because
|
|
137
|
+
* "this matters less" can degrade gracefully and "do not look at this"
|
|
138
|
+
* cannot.
|
|
139
|
+
*/
|
|
140
|
+
ignore: string[];
|
|
141
|
+
/** Settings for the Ollama backend (`ollama:`-prefixed models). */
|
|
142
|
+
ollama: OllamaConfig;
|
|
67
143
|
/**
|
|
68
144
|
* Allow API credentials from the environment (`ANTHROPIC_API_KEY` /
|
|
69
145
|
* `ANTHROPIC_AUTH_TOKEN`) to be used, billing pay-as-you-go instead of the
|
|
@@ -76,9 +152,27 @@ export interface Config {
|
|
|
76
152
|
|
|
77
153
|
/** Partial config as it may appear in a config file or be produced by flags. */
|
|
78
154
|
export type PartialConfig = {
|
|
79
|
-
[K in keyof Config]?: K extends "models"
|
|
155
|
+
[K in keyof Config]?: K extends "models"
|
|
156
|
+
? Partial<ModelConfig>
|
|
157
|
+
: K extends "ollama"
|
|
158
|
+
? Partial<OllamaConfig>
|
|
159
|
+
: Config[K];
|
|
80
160
|
};
|
|
81
161
|
|
|
162
|
+
/**
|
|
163
|
+
* How much weight a slice of the diff carries in the commit message.
|
|
164
|
+
* `primary` changes define the commit; `low` changes - those under the
|
|
165
|
+
* configured `lowPriorityPaths` - are summarised briefly and mentioned only
|
|
166
|
+
* after the primary ones.
|
|
167
|
+
*/
|
|
168
|
+
export type ChangePriority = "primary" | "low";
|
|
169
|
+
|
|
170
|
+
/** The summary of one diff chunk, tagged with the priority of the partition it came from. */
|
|
171
|
+
export interface DiffSummary {
|
|
172
|
+
priority: ChangePriority;
|
|
173
|
+
text: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
82
176
|
/** Result of a single model invocation. */
|
|
83
177
|
export interface ModelResult {
|
|
84
178
|
/** The text the model produced. */
|
|
@@ -98,3 +192,47 @@ export interface FileChange {
|
|
|
98
192
|
/** Path of the file (the destination path for renames). */
|
|
99
193
|
path: string;
|
|
100
194
|
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* One prompt to one model, whichever provider serves it.
|
|
198
|
+
*
|
|
199
|
+
* `model` carries the provider: bare names go to Claude, `ollama:`-prefixed
|
|
200
|
+
* ones to Ollama (see {@link ModelConfig}). Some options only apply to one
|
|
201
|
+
* provider - `allowApiKey` gates Claude credentials, `ollama` supplies the
|
|
202
|
+
* host and context window - and each is simply ignored by the other.
|
|
203
|
+
*/
|
|
204
|
+
export interface RunPromptOptions {
|
|
205
|
+
/** Model string: an alias (`sonnet`), a full id, or `ollama:<name>[:<tag>]`. */
|
|
206
|
+
model: string;
|
|
207
|
+
/** Full custom system prompt. */
|
|
208
|
+
system: string;
|
|
209
|
+
/** Receives assistant text as it streams in (enables partial messages). */
|
|
210
|
+
onText?: (delta: string) => void;
|
|
211
|
+
/** Abort the in-flight request. */
|
|
212
|
+
abortController?: AbortController;
|
|
213
|
+
/** Receives the underlying CLI's stderr (for `--verbose`). Claude only. */
|
|
214
|
+
onStderr?: (data: string) => void;
|
|
215
|
+
/**
|
|
216
|
+
* Sampling temperature. Used to add variety when generating several
|
|
217
|
+
* interactive options. Models that don't accept a temperature override
|
|
218
|
+
* will reject the request, so the caller should be prepared to retry
|
|
219
|
+
* without it.
|
|
220
|
+
*/
|
|
221
|
+
temperature?: number;
|
|
222
|
+
/**
|
|
223
|
+
* Request a structured JSON response matching this schema. The parsed object
|
|
224
|
+
* is returned on {@link ModelResult.structured}. Models that don't support
|
|
225
|
+
* structured outputs will reject the request or return unparseable content,
|
|
226
|
+
* so the caller should be prepared to retry without it.
|
|
227
|
+
*/
|
|
228
|
+
outputFormat?: { type: "json_schema"; schema: Record<string, unknown> };
|
|
229
|
+
/**
|
|
230
|
+
* Allow API credentials from the environment to reach the Claude Agent SDK
|
|
231
|
+
* subprocess. Defaults to false: `ANTHROPIC_API_KEY` /
|
|
232
|
+
* `ANTHROPIC_AUTH_TOKEN` are stripped so the run is billed to the Claude
|
|
233
|
+
* subscription. Has no meaning for Ollama, which takes no credential.
|
|
234
|
+
*/
|
|
235
|
+
allowApiKey?: boolean;
|
|
236
|
+
/** Ollama host and context settings; required for an `ollama:` model. */
|
|
237
|
+
ollama?: OllamaConfig;
|
|
238
|
+
}
|
package/src/ui/spinner.ts
CHANGED
|
@@ -16,7 +16,7 @@ import spinners, { type Spinner as SpinnerAnimation } from "cli-spinners";
|
|
|
16
16
|
import { color } from "./colors";
|
|
17
17
|
|
|
18
18
|
/** The spinner used when none (or an unknown one) is configured. */
|
|
19
|
-
export const DEFAULT_SPINNER = "
|
|
19
|
+
export const DEFAULT_SPINNER = "material";
|
|
20
20
|
|
|
21
21
|
/** Whether `name` is one of the cli-spinners animations bundled with ora. */
|
|
22
22
|
export function isSpinnerName(name: string): boolean {
|