@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.
- package/CHANGELOG.md +34 -0
- package/README.md +58 -39
- 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 +42 -31
- package/index.ts +0 -71
- package/src/agent.ts +0 -280
- package/src/cli.ts +0 -448
- package/src/config.ts +0 -339
- package/src/diff.ts +0 -580
- package/src/generate.ts +0 -501
- 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 -407
- package/src/tokens.ts +0 -147
- package/src/types.ts +0 -244
- 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
package/src/cli.ts
DELETED
|
@@ -1,448 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Command-line interface: argument parsing (Commander) and orchestration of the
|
|
3
|
-
* non-interactive and interactive flows.
|
|
4
|
-
*/
|
|
5
|
-
import { Command } from "commander";
|
|
6
|
-
import { getVersion } from "./utils";
|
|
7
|
-
import {
|
|
8
|
-
commit,
|
|
9
|
-
getStagedDiff,
|
|
10
|
-
getStagedStat,
|
|
11
|
-
isGitRepo,
|
|
12
|
-
getRepoRoot,
|
|
13
|
-
stageAll,
|
|
14
|
-
} from "./git";
|
|
15
|
-
import { presentCredentialVars } from "./agent";
|
|
16
|
-
import { loadFileConfig, resolveConfig } from "./config";
|
|
17
|
-
import {
|
|
18
|
-
generateCommit,
|
|
19
|
-
type IgnoreStats,
|
|
20
|
-
type LowPriorityStats,
|
|
21
|
-
type OllamaContextWindow,
|
|
22
|
-
} from "./generate";
|
|
23
|
-
import { Spinner } from "./ui/spinner";
|
|
24
|
-
import { confirmCommit, editInEditor } from "./ui/editor";
|
|
25
|
-
import { color } from "./ui/colors";
|
|
26
|
-
import { ClaudeCommitError } from "./errors";
|
|
27
|
-
import type { ModelConfig, OllamaConfig, PartialConfig } from "./types";
|
|
28
|
-
|
|
29
|
-
export const VERSION = getVersion();
|
|
30
|
-
|
|
31
|
-
interface CliOptions {
|
|
32
|
-
interactive?: boolean;
|
|
33
|
-
count?: number;
|
|
34
|
-
multiline?: boolean;
|
|
35
|
-
conventional?: boolean;
|
|
36
|
-
gitmoji?: boolean;
|
|
37
|
-
template?: string;
|
|
38
|
-
prompt?: string;
|
|
39
|
-
all?: boolean;
|
|
40
|
-
modelSummary?: string;
|
|
41
|
-
modelFinal?: string;
|
|
42
|
-
dryRun?: boolean;
|
|
43
|
-
yes?: boolean;
|
|
44
|
-
spinner?: boolean;
|
|
45
|
-
config?: string;
|
|
46
|
-
verbose?: boolean;
|
|
47
|
-
skipArmored?: boolean;
|
|
48
|
-
filenamesOnly?: boolean;
|
|
49
|
-
/** `false` when `--no-low-priority-paths` was passed (Commander's negated-flag shape). */
|
|
50
|
-
lowPriorityPaths?: boolean;
|
|
51
|
-
/** `false` when `--no-ignore` was passed (Commander's negated-flag shape). */
|
|
52
|
-
ignore?: boolean;
|
|
53
|
-
ollamaHost?: string;
|
|
54
|
-
ollamaContext?: number | "auto";
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Build the Commander program. Exported for tests. */
|
|
58
|
-
export function buildProgram(): Command {
|
|
59
|
-
const program = new Command();
|
|
60
|
-
program
|
|
61
|
-
.name("cco")
|
|
62
|
-
.description("Generate a git commit message with Claude.")
|
|
63
|
-
.version(VERSION, "-V, --version", "output the version number")
|
|
64
|
-
.option(
|
|
65
|
-
"-i, --interactive",
|
|
66
|
-
"choose between several options in an interactive TUI",
|
|
67
|
-
)
|
|
68
|
-
.option(
|
|
69
|
-
"--no-interactive",
|
|
70
|
-
'skip the interactive TUI even when "interactive" is set in config',
|
|
71
|
-
)
|
|
72
|
-
.option(
|
|
73
|
-
"-n, --count <n>",
|
|
74
|
-
"number of options to generate in interactive mode",
|
|
75
|
-
(v) => parseInt(v, 10),
|
|
76
|
-
)
|
|
77
|
-
.option("-a, --all", "stage all changes (git add -A) before committing")
|
|
78
|
-
.option("-c, --conventional", "format as a Conventional Commit")
|
|
79
|
-
.option("-g, --gitmoji", "prefix the subject with a gitmoji")
|
|
80
|
-
.option("-m, --multiline", "write a multi-line commit (subject + body)")
|
|
81
|
-
.option("--no-multiline", "write only a single-line subject")
|
|
82
|
-
.option(
|
|
83
|
-
"-t, --template <tpl>",
|
|
84
|
-
'template for the first line, e.g. "[PROJ-1] {message}"',
|
|
85
|
-
)
|
|
86
|
-
.option("-p, --prompt <text>", "extra instructions appended to the prompt")
|
|
87
|
-
.option(
|
|
88
|
-
"-f, --filenames-only",
|
|
89
|
-
"skip summarisation and use only filenames (faster, less useful messages)",
|
|
90
|
-
)
|
|
91
|
-
.option("--model-summary <model>", "model used to summarize the diff")
|
|
92
|
-
.option("--model-final <model>", "model used to write the final message")
|
|
93
|
-
.option(
|
|
94
|
-
"--skip-armored",
|
|
95
|
-
"omit armored/encoded lines (age/gpg armor, base64 blobs) from the " +
|
|
96
|
-
"summarized diff; recommended for chezmoi-style encrypted repos",
|
|
97
|
-
)
|
|
98
|
-
.option(
|
|
99
|
-
"--no-low-priority-paths",
|
|
100
|
-
'ignore the "lowPriorityPaths" config for this run, so every change ' +
|
|
101
|
-
"weighs the same",
|
|
102
|
-
)
|
|
103
|
-
.option(
|
|
104
|
-
"--no-ignore",
|
|
105
|
-
'disregard the "ignore" config for this run, so every staged change is ' +
|
|
106
|
-
"read",
|
|
107
|
-
)
|
|
108
|
-
.option(
|
|
109
|
-
"--ollama-host <url>",
|
|
110
|
-
"base URL of the Ollama server for ollama: models",
|
|
111
|
-
)
|
|
112
|
-
.option(
|
|
113
|
-
"--ollama-context <tokens|auto>",
|
|
114
|
-
"context window for Ollama models: a token count, or auto to use " +
|
|
115
|
-
"the server's own choice for this machine",
|
|
116
|
-
parseContextFlag,
|
|
117
|
-
)
|
|
118
|
-
.option("-d, --dry-run", "print the message to stdout without committing")
|
|
119
|
-
.option("-y, --yes", "commit without asking for confirmation")
|
|
120
|
-
.option("--no-spinner", "disable the progress spinner")
|
|
121
|
-
.option("--config <path>", "path to a config file")
|
|
122
|
-
.option("-v, --verbose", "print summaries, cost and debug output")
|
|
123
|
-
.addHelpText(
|
|
124
|
-
"after",
|
|
125
|
-
[
|
|
126
|
-
"",
|
|
127
|
-
"Authentication:",
|
|
128
|
-
" Claude models use the Claude Agent SDK with your Claude Code",
|
|
129
|
-
" subscription (run `claude login`). ANTHROPIC_API_KEY /",
|
|
130
|
-
" ANTHROPIC_AUTH_TOKEN are ignored unless the config sets",
|
|
131
|
-
' "allowApiKey": true (pay-as-you-go billing).',
|
|
132
|
-
"",
|
|
133
|
-
"Ollama models:",
|
|
134
|
-
" Prefix a model with `ollama:` to run it on a local Ollama server,",
|
|
135
|
-
" e.g. --model-summary ollama:ornith-1.5:35b. Everything after the",
|
|
136
|
-
" prefix is the Ollama model name, tag included. The server needs no",
|
|
137
|
-
" credential; point cco at it with --ollama-host or $OLLAMA_HOST.",
|
|
138
|
-
"",
|
|
139
|
-
"Examples:",
|
|
140
|
-
" cco generate and commit a message for staged changes",
|
|
141
|
-
" cco -a -c stage everything and write a Conventional Commit",
|
|
142
|
-
" cco -i pick from several options interactively",
|
|
143
|
-
" cco --dry-run | cat print a message without committing",
|
|
144
|
-
" cco --model-summary ollama:ornith-1.5:35b",
|
|
145
|
-
" read the diff locally, write the message with Claude",
|
|
146
|
-
].join("\n"),
|
|
147
|
-
);
|
|
148
|
-
return program;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/** `--ollama-context` accepts a token count or the literal `auto`. */
|
|
152
|
-
function parseContextFlag(value: string): number | "auto" {
|
|
153
|
-
return value.trim().toLowerCase() === "auto" ? "auto" : parseInt(value, 10);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Map parsed CLI flags onto a partial config (only set keys the user
|
|
158
|
-
* provided). Exported for tests.
|
|
159
|
-
*/
|
|
160
|
-
export function flagsToConfig(opts: CliOptions): PartialConfig {
|
|
161
|
-
const cfg: PartialConfig = {};
|
|
162
|
-
if (opts.conventional !== undefined)
|
|
163
|
-
cfg.conventionalCommits = opts.conventional;
|
|
164
|
-
if (opts.gitmoji !== undefined) cfg.gitmoji = opts.gitmoji;
|
|
165
|
-
if (opts.multiline !== undefined) cfg.multiline = opts.multiline;
|
|
166
|
-
if (opts.interactive !== undefined) cfg.interactive = opts.interactive;
|
|
167
|
-
if (opts.template !== undefined) cfg.template = opts.template;
|
|
168
|
-
if (opts.prompt !== undefined) cfg.customPrompt = opts.prompt;
|
|
169
|
-
if (opts.skipArmored !== undefined) cfg.skipArmored = opts.skipArmored;
|
|
170
|
-
if (opts.filenamesOnly !== undefined) cfg.filenamesOnly = opts.filenamesOnly;
|
|
171
|
-
// A negated flag arrives as `false`; an empty list overrides any
|
|
172
|
-
// configured patterns because lists replace rather than merge.
|
|
173
|
-
if (opts.lowPriorityPaths === false) cfg.lowPriorityPaths = [];
|
|
174
|
-
if (opts.ignore === false) cfg.ignore = [];
|
|
175
|
-
const ollama: Partial<OllamaConfig> = {};
|
|
176
|
-
if (opts.ollamaHost) ollama.host = opts.ollamaHost;
|
|
177
|
-
if (opts.ollamaContext === "auto") {
|
|
178
|
-
ollama.context = "auto";
|
|
179
|
-
} else if (
|
|
180
|
-
opts.ollamaContext !== undefined &&
|
|
181
|
-
Number.isFinite(opts.ollamaContext)
|
|
182
|
-
) {
|
|
183
|
-
ollama.context = Math.max(1, opts.ollamaContext);
|
|
184
|
-
}
|
|
185
|
-
if (Object.keys(ollama).length) cfg.ollama = ollama;
|
|
186
|
-
if (opts.count !== undefined && Number.isFinite(opts.count)) {
|
|
187
|
-
cfg.interactiveCount = Math.max(1, opts.count);
|
|
188
|
-
}
|
|
189
|
-
const models: Partial<ModelConfig> = {};
|
|
190
|
-
if (opts.modelSummary) models.summary = opts.modelSummary;
|
|
191
|
-
if (opts.modelFinal) models.final = opts.modelFinal;
|
|
192
|
-
if (Object.keys(models).length) cfg.models = models;
|
|
193
|
-
return cfg;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Decide which flow to run from the resolved config, the raw `--interactive`
|
|
198
|
-
* flag state, and whether we have an interactive terminal.
|
|
199
|
-
*
|
|
200
|
-
* - `--dry-run` always wins: it prints and never commits, so the TUI is skipped.
|
|
201
|
-
* - Interactive requires a TTY. An explicit `-i` without one is a hard error
|
|
202
|
-
* (the user asked for something we cannot provide), whereas interactive coming
|
|
203
|
-
* only from config silently falls back to the non-interactive flow so pipes
|
|
204
|
-
* and CI keep working.
|
|
205
|
-
*/
|
|
206
|
-
export function resolveInteractiveMode(args: {
|
|
207
|
-
configInteractive: boolean;
|
|
208
|
-
interactiveFlag: boolean | undefined;
|
|
209
|
-
dryRun: boolean;
|
|
210
|
-
hasTty: boolean;
|
|
211
|
-
}): "interactive" | "non-interactive" | "no-tty-error" {
|
|
212
|
-
if (args.dryRun) return "non-interactive";
|
|
213
|
-
if (!args.configInteractive) return "non-interactive";
|
|
214
|
-
if (args.hasTty) return "interactive";
|
|
215
|
-
return args.interactiveFlag === true ? "no-tty-error" : "non-interactive";
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function printMessage(message: string): void {
|
|
219
|
-
const bar = color("90", "─".repeat(48));
|
|
220
|
-
process.stderr.write(`\n${bar}\n${message}\n${bar}\n`);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/** Entry point. Returns a process exit code. */
|
|
224
|
-
export async function run(argv: string[]): Promise<number> {
|
|
225
|
-
const program = buildProgram();
|
|
226
|
-
program.parse(argv, { from: "user" });
|
|
227
|
-
const opts = program.opts<CliOptions>();
|
|
228
|
-
const verbose = Boolean(opts.verbose);
|
|
229
|
-
|
|
230
|
-
const abortController = new AbortController();
|
|
231
|
-
// Two-stage Ctrl-C: the first interrupt asks the in-flight generation to
|
|
232
|
-
// cancel gracefully (aborting the SDK subprocess); a second, impatient
|
|
233
|
-
// interrupt force-quits in case that abort is slow to take effect.
|
|
234
|
-
let interrupting = false;
|
|
235
|
-
const onSigint = () => {
|
|
236
|
-
if (interrupting) {
|
|
237
|
-
// Second Ctrl-C: force-quit. Restore the cursor in case a spinner hid it,
|
|
238
|
-
// since this path bypasses the spinner's own cleanup.
|
|
239
|
-
if (process.stderr.isTTY) process.stderr.write("\x1b[?25h");
|
|
240
|
-
process.exit(130);
|
|
241
|
-
}
|
|
242
|
-
interrupting = true;
|
|
243
|
-
abortController.abort();
|
|
244
|
-
};
|
|
245
|
-
process.on("SIGINT", onSigint);
|
|
246
|
-
|
|
247
|
-
try {
|
|
248
|
-
if (!(await isGitRepo())) {
|
|
249
|
-
throw new ClaudeCommitError(
|
|
250
|
-
"Not a git repository (or any parent). Run `cco` inside a repo.",
|
|
251
|
-
);
|
|
252
|
-
}
|
|
253
|
-
const repoRoot = await getRepoRoot();
|
|
254
|
-
const fileConfig = await loadFileConfig(
|
|
255
|
-
process.cwd(),
|
|
256
|
-
repoRoot,
|
|
257
|
-
opts.config,
|
|
258
|
-
);
|
|
259
|
-
const config = resolveConfig(fileConfig, flagsToConfig(opts));
|
|
260
|
-
|
|
261
|
-
// Surface the credential gate: the actual stripping happens in the agent
|
|
262
|
-
// layer, but silently ignoring an exported key would be confusing.
|
|
263
|
-
if (!config.allowApiKey) {
|
|
264
|
-
const ignored = presentCredentialVars(process.env);
|
|
265
|
-
if (ignored.length > 0) {
|
|
266
|
-
process.stderr.write(
|
|
267
|
-
color(
|
|
268
|
-
"90",
|
|
269
|
-
`Ignoring ${ignored.join(" and ")}: using subscription auth. Set ` +
|
|
270
|
-
`"allowApiKey": true in your claude-commit config to use API credentials.`,
|
|
271
|
-
) + "\n",
|
|
272
|
-
);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
if (opts.all) await stageAll();
|
|
277
|
-
|
|
278
|
-
const diff = await getStagedDiff();
|
|
279
|
-
if (diff.trim() === "") {
|
|
280
|
-
throw new ClaudeCommitError(
|
|
281
|
-
opts.all
|
|
282
|
-
? "No changes to commit: the working tree is clean."
|
|
283
|
-
: "No staged changes. Stage files with `git add`, or pass -a/--all to stage everything.",
|
|
284
|
-
);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const interactiveMode = resolveInteractiveMode({
|
|
288
|
-
configInteractive: config.interactive,
|
|
289
|
-
interactiveFlag: opts.interactive,
|
|
290
|
-
dryRun: Boolean(opts.dryRun),
|
|
291
|
-
hasTty: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
292
|
-
});
|
|
293
|
-
if (interactiveMode === "no-tty-error") {
|
|
294
|
-
throw new ClaudeCommitError(
|
|
295
|
-
"Interactive mode (-i) requires an interactive terminal.",
|
|
296
|
-
);
|
|
297
|
-
}
|
|
298
|
-
if (interactiveMode === "interactive") {
|
|
299
|
-
const { runInteractive } = await import("./ui/interactive");
|
|
300
|
-
return await runInteractive(diff, config, { verbose, abortController });
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
return await runNonInteractive(
|
|
304
|
-
diff,
|
|
305
|
-
config,
|
|
306
|
-
opts,
|
|
307
|
-
verbose,
|
|
308
|
-
abortController,
|
|
309
|
-
);
|
|
310
|
-
} catch (err) {
|
|
311
|
-
if (err instanceof ClaudeCommitError) {
|
|
312
|
-
process.stderr.write(`${color("31", "error:")} ${err.message}\n`);
|
|
313
|
-
return 1;
|
|
314
|
-
}
|
|
315
|
-
throw err;
|
|
316
|
-
} finally {
|
|
317
|
-
process.off("SIGINT", onSigint);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
async function runNonInteractive(
|
|
322
|
-
diff: string,
|
|
323
|
-
config: ReturnType<typeof resolveConfig>,
|
|
324
|
-
opts: CliOptions,
|
|
325
|
-
verbose: boolean,
|
|
326
|
-
abortController: AbortController,
|
|
327
|
-
): Promise<number> {
|
|
328
|
-
const useSpinner = opts.spinner !== false && process.stderr.isTTY;
|
|
329
|
-
const spinner = new Spinner(useSpinner, config.spinner);
|
|
330
|
-
|
|
331
|
-
spinner.start(config.filenamesOnly ? "Reading filenames" : "Reading diff");
|
|
332
|
-
let result;
|
|
333
|
-
try {
|
|
334
|
-
result = await generateCommit(diff, config, {
|
|
335
|
-
progress: { onPhase: (label) => spinner.update(label) },
|
|
336
|
-
abortController,
|
|
337
|
-
});
|
|
338
|
-
} catch (err) {
|
|
339
|
-
spinner.stop(); // always restore the cursor / clear the line on failure
|
|
340
|
-
throw err;
|
|
341
|
-
}
|
|
342
|
-
spinner.stop();
|
|
343
|
-
|
|
344
|
-
if (verbose) {
|
|
345
|
-
process.stderr.write(
|
|
346
|
-
color(
|
|
347
|
-
"90",
|
|
348
|
-
`${config.filenamesOnly ? "filenames only (summariser skipped)" : `${result.chunkCount} chunk(s)`}, cost $${result.costUsd.toFixed(4)}`,
|
|
349
|
-
) + "\n",
|
|
350
|
-
);
|
|
351
|
-
for (const window of result.ollamaContexts) {
|
|
352
|
-
process.stderr.write(color("90", describeOllamaContext(window)) + "\n");
|
|
353
|
-
}
|
|
354
|
-
if (config.ignore.length > 0) {
|
|
355
|
-
process.stderr.write(
|
|
356
|
-
color("90", describeIgnoreStats(result.ignored)) + "\n",
|
|
357
|
-
);
|
|
358
|
-
}
|
|
359
|
-
if (config.lowPriorityPaths.length > 0) {
|
|
360
|
-
process.stderr.write(
|
|
361
|
-
color("90", describeLowPriorityStats(result.lowPriority)) + "\n",
|
|
362
|
-
);
|
|
363
|
-
}
|
|
364
|
-
for (const [index, summary] of result.summaries.entries()) {
|
|
365
|
-
const label =
|
|
366
|
-
summary.priority === "low"
|
|
367
|
-
? `--- summary ${index + 1} (low priority) ---`
|
|
368
|
-
: `--- summary ${index + 1} ---`;
|
|
369
|
-
process.stderr.write(color("90", `${label}\n${summary.text}`) + "\n");
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
let message = result.messages[0]!;
|
|
374
|
-
|
|
375
|
-
// Dry run: emit to stdout so it can be piped, and never commit.
|
|
376
|
-
if (opts.dryRun) {
|
|
377
|
-
process.stdout.write(message + "\n");
|
|
378
|
-
return 0;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
const canPrompt = process.stdin.isTTY && process.stdout.isTTY;
|
|
382
|
-
if (!opts.yes && canPrompt) {
|
|
383
|
-
printMessage(message);
|
|
384
|
-
const choice = await confirmCommit();
|
|
385
|
-
if (choice === "no") {
|
|
386
|
-
process.stderr.write("Aborted. Nothing was committed.\n");
|
|
387
|
-
return 1;
|
|
388
|
-
}
|
|
389
|
-
if (choice === "edit") {
|
|
390
|
-
message = await editInEditor(message);
|
|
391
|
-
if (message.trim() === "") {
|
|
392
|
-
process.stderr.write("Aborted: empty commit message.\n");
|
|
393
|
-
return 1;
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
const stat = verbose ? await getStagedStat().catch(() => "") : "";
|
|
399
|
-
await commit(message);
|
|
400
|
-
spinner.succeed("Committed");
|
|
401
|
-
process.stderr.write(color("90", firstLine(message)) + "\n");
|
|
402
|
-
if (stat) process.stderr.write(color("90", stat) + "\n");
|
|
403
|
-
return 0;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
/**
|
|
407
|
-
* One verbose line saying how the low-priority patterns applied. Without it
|
|
408
|
-
* a pattern that matched nothing and one that matched everything (and was
|
|
409
|
-
* promoted) are indistinguishable from the message alone.
|
|
410
|
-
*/
|
|
411
|
-
export function describeLowPriorityStats(stats: LowPriorityStats): string {
|
|
412
|
-
const files = `${stats.totalFiles} file${stats.totalFiles === 1 ? "" : "s"}`;
|
|
413
|
-
if (stats.matchedFiles === 0) {
|
|
414
|
-
return `low-priority paths: matched none of ${files}`;
|
|
415
|
-
}
|
|
416
|
-
if (stats.promoted) {
|
|
417
|
-
return `low-priority paths: matched all ${files} - nothing else changed, so treated as primary`;
|
|
418
|
-
}
|
|
419
|
-
return `low-priority paths: matched ${stats.matchedFiles} of ${files}`;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
/**
|
|
423
|
-
* One verbose line per Ollama model naming the context window it ran with
|
|
424
|
-
* and where the number came from. With `"auto"` this is the only place the
|
|
425
|
-
* server's choice is visible, and it is the first thing to check when a
|
|
426
|
-
* summary reads as if it saw half the diff.
|
|
427
|
-
*/
|
|
428
|
-
export function describeOllamaContext(window: OllamaContextWindow): string {
|
|
429
|
-
const source =
|
|
430
|
-
window.source === "auto" ? "chosen by the server" : "from config";
|
|
431
|
-
return `ollama: ${window.model} context ${window.tokens} tokens (${source})`;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
/**
|
|
435
|
-
* One verbose line saying how the ignore patterns applied. The counts are
|
|
436
|
-
* the only way to tell a pattern that quietly matched nothing from one that
|
|
437
|
-
* quietly removed half the commit.
|
|
438
|
-
*/
|
|
439
|
-
export function describeIgnoreStats(stats: IgnoreStats): string {
|
|
440
|
-
const files = `${stats.totalFiles} file${stats.totalFiles === 1 ? "" : "s"}`;
|
|
441
|
-
return stats.ignoredFiles === 0
|
|
442
|
-
? `ignore: matched none of ${files}`
|
|
443
|
-
: `ignore: dropped ${stats.ignoredFiles} of ${files} before reading`;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
function firstLine(text: string): string {
|
|
447
|
-
return text.split("\n", 1)[0] ?? text;
|
|
448
|
-
}
|