@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/README.md
CHANGED
|
@@ -20,18 +20,24 @@ feat(auth): add error handling and refresh token rotation to login
|
|
|
20
20
|
Want more details? See [WALKTHROUGH.md](WALKTHROUGH.md).
|
|
21
21
|
|
|
22
22
|
```plaintext
|
|
23
|
-
staged diff ──split──▶ [chunk, …] ──sonnet──▶ summaries ──sonnet──▶ commit message
|
|
23
|
+
staged diff ──ignore──▶ ──split──▶ [chunk, …] ──sonnet──▶ summaries ──sonnet──▶ commit message
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
1. **Summarize** - the diff is split into chunks that fit the context window and
|
|
27
27
|
each chunk is summarized by a strong model (`sonnet`, which carries a native
|
|
28
28
|
1M-token context). Diffs larger than 1M tokens simply produce more chunks.
|
|
29
|
+
Paths listed in [`ignore`](#ignoring-paths-entirely) are dropped first and
|
|
30
|
+
never read at all; changes under configured
|
|
31
|
+
[low-priority paths](#low-priority-paths) are summarized separately, so churn
|
|
32
|
+
cannot crowd out the code.
|
|
29
33
|
2. **Write** - the summaries are handed to the same model (`sonnet`) to write the
|
|
30
34
|
final commit message according to your formatting rules. The message is the
|
|
31
35
|
whole point of the tool, and its input is tiny, so a strong model here costs
|
|
32
36
|
almost nothing extra.
|
|
33
37
|
|
|
34
|
-
|
|
38
|
+
Either stage can run on a local [Ollama](#ollama-models) model instead; by
|
|
39
|
+
default both go through the
|
|
40
|
+
[Claude Agent SDK](https://code.claude.com/docs/en/agent-sdk/overview).
|
|
35
41
|
|
|
36
42
|
## Install
|
|
37
43
|
|
|
@@ -64,6 +70,9 @@ instead (pay-as-you-go), opt in explicitly in your configuration:
|
|
|
64
70
|
{ "allowApiKey": true }
|
|
65
71
|
```
|
|
66
72
|
|
|
73
|
+
Ollama models sit outside all of this: they run on a server you control, over
|
|
74
|
+
plain HTTP, with no credential at all. See [Ollama models](#ollama-models).
|
|
75
|
+
|
|
67
76
|
## Usage
|
|
68
77
|
|
|
69
78
|
```sh
|
|
@@ -88,6 +97,11 @@ and asks for confirmation before committing. Pass `-y` to skip the prompt, or
|
|
|
88
97
|
| `-p, --prompt <text>` | Extra instructions appended to the prompt |
|
|
89
98
|
| `--model-summary <model>` | Model used to summarize the diff (default `sonnet`) |
|
|
90
99
|
| `--model-final <model>` | Model used to write the message (default `sonnet`) |
|
|
100
|
+
| `--skip-armored` | Omit armored/encoded lines (age/gpg armor, base64 blobs) from the summarized diff |
|
|
101
|
+
| `--no-low-priority-paths` | Ignore `lowPriorityPaths` for this run, so every change weighs the same |
|
|
102
|
+
| `--no-ignore` | Disregard `ignore` for this run, so every staged change is read |
|
|
103
|
+
| `--ollama-host <url>` | Base URL of the Ollama server for `ollama:` models |
|
|
104
|
+
| `--ollama-context <tokens>` | Context window requested from Ollama models |
|
|
91
105
|
| `-d, --dry-run` | Print the message to stdout without committing |
|
|
92
106
|
| `-y, --yes` | Commit without asking for confirmation |
|
|
93
107
|
| `--no-spinner` | Disable the progress spinner |
|
|
@@ -151,7 +165,7 @@ keys are valid at every level:
|
|
|
151
165
|
"interactive": true,
|
|
152
166
|
"interactiveCount": 3,
|
|
153
167
|
"interactiveTemperature": 1,
|
|
154
|
-
"spinner": "
|
|
168
|
+
"spinner": "material",
|
|
155
169
|
"models": {
|
|
156
170
|
"summary": "sonnet",
|
|
157
171
|
"final": "sonnet"
|
|
@@ -159,6 +173,13 @@ keys are valid at every level:
|
|
|
159
173
|
"maxChunkTokens": 600000,
|
|
160
174
|
"charsPerToken": 3.5,
|
|
161
175
|
"skipArmored": false,
|
|
176
|
+
"lowPriorityPaths": [],
|
|
177
|
+
"ignore": [],
|
|
178
|
+
"ollama": {
|
|
179
|
+
"host": "http://localhost:11434",
|
|
180
|
+
"context": "auto",
|
|
181
|
+
"keepAlive": null
|
|
182
|
+
},
|
|
162
183
|
"allowApiKey": false
|
|
163
184
|
}
|
|
164
185
|
```
|
|
@@ -166,10 +187,12 @@ keys are valid at every level:
|
|
|
166
187
|
`spinner` chooses the progress animation: any name from the
|
|
167
188
|
[cli-spinners](https://github.com/sindresorhus/cli-spinners) set bundled with
|
|
168
189
|
[ora](https://github.com/sindresorhus/ora) (`"dots"`, `"moon"`, `"pong"`,
|
|
169
|
-
`"
|
|
170
|
-
`
|
|
190
|
+
`"material"`, ...). Unknown names are ignored and the default
|
|
191
|
+
`material` is used. `--no-spinner` disables the animated spinner, but final
|
|
171
192
|
status lines still print.
|
|
172
193
|
|
|
194
|
+
The `material` spinner is chosen because it's fucking cool. Fight me.
|
|
195
|
+
|
|
173
196
|
`maxChunkTokens` is a cap, not a promise: at run time it is clamped to the
|
|
174
197
|
summary model's context window minus a fixed reserve (1M-window models such as
|
|
175
198
|
current Sonnet/Opus keep the full budget; Haiku, older pinned model ids, and
|
|
@@ -193,6 +216,186 @@ every `chezmoi re-add` re-encrypts nondeterministically and produces megabytes
|
|
|
193
216
|
of churned armor. Drop a `.claude-commit.json` with `{ "skipArmored": true }`
|
|
194
217
|
in the repo root to enable it per-repo.
|
|
195
218
|
|
|
219
|
+
### Low-priority paths
|
|
220
|
+
|
|
221
|
+
Some paths change a lot without meaning much - generated docs, lockfiles,
|
|
222
|
+
vendored snapshots, build output. Left alone, a commit that touches twenty
|
|
223
|
+
lines of code and regenerates two thousand lines of tooling gets a subject
|
|
224
|
+
line about the tooling. `lowPriorityPaths` lists gitignore-style patterns for
|
|
225
|
+
those paths:
|
|
226
|
+
|
|
227
|
+
```json
|
|
228
|
+
{
|
|
229
|
+
"lowPriorityPaths": [".agents/skills/*-skilld", "bun.lock", "!bun.lock.keep"]
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Changes under matching paths are summarised separately and briefly, and the
|
|
234
|
+
model is told that the subject line - and the commit type, scope and gitmoji
|
|
235
|
+
where you use them - comes from the _other_ changes, however small they are.
|
|
236
|
+
The low-priority changes are mentioned in the subject only if they fit, and in
|
|
237
|
+
the body (with `multiline`) only after the primary changes. When _every_
|
|
238
|
+
changed file is low priority there is nothing for it to yield to, so the
|
|
239
|
+
changes are described normally, exactly as if no patterns were configured.
|
|
240
|
+
|
|
241
|
+
Pattern rules follow `.gitignore` conventions, so trunk or gitignore lines can
|
|
242
|
+
usually be copied in:
|
|
243
|
+
|
|
244
|
+
- A pattern with a `/` in it is anchored at the repository root and matches a
|
|
245
|
+
path or any directory above it - `.agents/skills/*-skilld` covers every file
|
|
246
|
+
inside each matching directory.
|
|
247
|
+
- A pattern without a `/` matches any path segment at any depth - `bun.lock`
|
|
248
|
+
matches `packages/app/bun.lock`; `*-skilld` matches everything inside any
|
|
249
|
+
`*-skilld` directory.
|
|
250
|
+
- `*` matches dotfiles and does not cross `/`; `**` does; `{a,b}` expands. A
|
|
251
|
+
leading `/` or `./` anchors, a trailing `/` is ignored. The anchoring
|
|
252
|
+
decision looks at the whole pattern, so a `/` inside a brace group anchors
|
|
253
|
+
all of its alternatives - prefer one pattern per intent.
|
|
254
|
+
- A leading `!` negates, and the last matching pattern wins:
|
|
255
|
+
`["docs/**", "!docs/adr/**"]` deprioritises docs except the ADRs.
|
|
256
|
+
- Patterns are always matched against repository-root-relative paths with
|
|
257
|
+
`/` separators, whichever directory you run `cco` from. A backslash in a
|
|
258
|
+
pattern is an escape (`\[`, `\{`, `\!` for the literal characters), so
|
|
259
|
+
Windows-style `dist\**` matches nothing.
|
|
260
|
+
- Patterns are not validated: a typo such as an unbalanced `{` is parsed
|
|
261
|
+
rather than rejected and may match something unexpected, so check the
|
|
262
|
+
`--verbose` match counts when you add one.
|
|
263
|
+
|
|
264
|
+
A rename into or out of a low-priority path counts as primary (both sides
|
|
265
|
+
must match). The nearest config layer that sets the key wins outright - lists
|
|
266
|
+
are never merged - so `"lowPriorityPaths": []` in a project opts out of a
|
|
267
|
+
global list, and a project that wants the global patterns plus its own must
|
|
268
|
+
repeat them. `--no-low-priority-paths` switches the feature off for one run,
|
|
269
|
+
which is handy when the churn _is_ the story, or for comparing messages while
|
|
270
|
+
tuning patterns.
|
|
271
|
+
|
|
272
|
+
This changes how changes are _weighted_ in the message, not how much of the
|
|
273
|
+
diff is read: low-priority content is still summarised in full, at the same
|
|
274
|
+
cost. To skip content outright, see `skipArmored`. Under `--verbose`, `cco`
|
|
275
|
+
reports how many files matched (`low-priority paths: matched 3 of 41 files`),
|
|
276
|
+
which is the only way to tell a pattern that matched nothing from one that
|
|
277
|
+
matched everything and was promoted.
|
|
278
|
+
|
|
279
|
+
### Ignoring paths entirely
|
|
280
|
+
|
|
281
|
+
`lowPriorityPaths` still reads everything it deprioritises, and pays for it.
|
|
282
|
+
Some content is worth neither the tokens nor the time: a vendored dependency
|
|
283
|
+
tree, a generated API client, a data fixture that changes wholesale.
|
|
284
|
+
|
|
285
|
+
`ignore` takes the same gitignore-style patterns and removes those file
|
|
286
|
+
sections from the diff **before anything else looks at it** - before the
|
|
287
|
+
low-priority partition, before chunking, before any model call:
|
|
288
|
+
|
|
289
|
+
```json
|
|
290
|
+
{ "ignore": ["vendor/**", "**/__snapshots__", "*.generated.ts"] }
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
The stages compose in the order their names suggest:
|
|
294
|
+
|
|
295
|
+
```text
|
|
296
|
+
diff ─ ignore ─▶ ─ skipArmored ─▶ ─ lowPriorityPaths ─▶ chunks ─▶ summaries
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Two things worth being clear about:
|
|
300
|
+
|
|
301
|
+
- **The files are still committed.** `ignore` governs what the model reads,
|
|
302
|
+
never what git stages. `cco` is writing a message, not choosing a changeset.
|
|
303
|
+
- **When it matches _everything_, `cco` stops** with an error naming the
|
|
304
|
+
directive, rather than inventing a message about changes you told it not to
|
|
305
|
+
read. This is deliberately unlike `lowPriorityPaths`, which promotes its
|
|
306
|
+
partition in the same situation - "this matters less" can degrade
|
|
307
|
+
gracefully, "do not look at this" has nothing to degrade to. Pass
|
|
308
|
+
`--no-ignore` for that one commit.
|
|
309
|
+
|
|
310
|
+
As with `lowPriorityPaths`, a section is dropped only when it names at least
|
|
311
|
+
one path and _all_ of them match, so a rename out of an ignored directory
|
|
312
|
+
survives. `--verbose` reports the count
|
|
313
|
+
(`ignore: dropped 3 of 41 files before reading`).
|
|
314
|
+
|
|
315
|
+
## Ollama models
|
|
316
|
+
|
|
317
|
+
Any model can be run on a local (or self-hosted) [Ollama](https://ollama.com)
|
|
318
|
+
server instead of Claude, by prefixing its name with `ollama:`. Everything
|
|
319
|
+
after the prefix is the Ollama model name **verbatim**, tag included:
|
|
320
|
+
|
|
321
|
+
```json
|
|
322
|
+
{
|
|
323
|
+
"models": {
|
|
324
|
+
"summary": "ollama:ornith-1.5:35b",
|
|
325
|
+
"final": "sonnet"
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
The two stages resolve independently, so that mixed setup is the interesting
|
|
331
|
+
one: reading the diff is the bulk of the work and the most sensitive thing
|
|
332
|
+
`cco` touches, so it runs locally and free, while the final message - one
|
|
333
|
+
short, quality-sensitive call on a summary - still goes to Claude. The prefix
|
|
334
|
+
works anywhere a model name does, including the flags:
|
|
335
|
+
|
|
336
|
+
```sh
|
|
337
|
+
cco --model-summary ollama:ornith-1.5:35b --dry-run -v
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
The server needs no credential. `cco` talks to Ollama's native `/api/chat`
|
|
341
|
+
endpoint, not either of its OpenAI/Anthropic compatibility layers, because
|
|
342
|
+
only the native API can set a context length.
|
|
343
|
+
|
|
344
|
+
### Context length is the setting that matters
|
|
345
|
+
|
|
346
|
+
Ollama picks a context window for each model from available VRAM (4k / 32k /
|
|
347
|
+
256k tiers, capped at the model's trained maximum), and a prompt that
|
|
348
|
+
exceeds it is truncated **silently** - HTTP 200, oldest content dropped,
|
|
349
|
+
nothing on the response to say so. A summary written from half a diff is
|
|
350
|
+
worse than no summary, so `cco` never lets that number stay implicit: it
|
|
351
|
+
sends an explicit window on every request, sizes its diff chunks against the
|
|
352
|
+
same number, and checks the token counts afterwards to catch a truncation
|
|
353
|
+
that happened anyway (in which case it re-splits the chunk and retries,
|
|
354
|
+
exactly as it does for a Claude context overflow).
|
|
355
|
+
|
|
356
|
+
Where the number comes from is `ollama.context`:
|
|
357
|
+
|
|
358
|
+
```json
|
|
359
|
+
{
|
|
360
|
+
"ollama": {
|
|
361
|
+
"host": "http://localhost:11434",
|
|
362
|
+
"context": "auto",
|
|
363
|
+
"keepAlive": "10m"
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
- `context` - `"auto"` (the default) **asks the server** rather than
|
|
369
|
+
guessing: the model is preloaded with no window set, so Ollama applies
|
|
370
|
+
its own VRAM-based choice, and `cco` reads that choice back from
|
|
371
|
+
`/api/ps` before sizing anything. That is the largest window Ollama
|
|
372
|
+
believes this machine can actually run - 131072 for a Gemma model on a
|
|
373
|
+
large Mac, 4096 for the same model on a small laptop - resolved once per
|
|
374
|
+
model per run, and shown under `--verbose`. A number pins the window
|
|
375
|
+
instead: lower it when memory is tight (usage scales with it, multiplied
|
|
376
|
+
by `OLLAMA_NUM_PARALLEL`), or raise it past the tier if you know your
|
|
377
|
+
hardware better than the server does. A smaller window is never a
|
|
378
|
+
correctness problem - `cco` just splits the diff into more chunks.
|
|
379
|
+
- `host` - defaults to `$OLLAMA_HOST`, then `http://localhost:11434`. A bare
|
|
380
|
+
`box.local:11434` gains an `http://`, matching Ollama's own convention.
|
|
381
|
+
- `keepAlive` - how long the server keeps the model loaded after a request: a
|
|
382
|
+
duration string (`"10m"`), seconds as a number, `0` to unload immediately,
|
|
383
|
+
or negative to pin it. `null` leaves the server's own default. Pinning is
|
|
384
|
+
worth it if you commit often; a 35b model takes a while to load.
|
|
385
|
+
|
|
386
|
+
### What differs from a Claude model
|
|
387
|
+
|
|
388
|
+
- **Cost is reported as zero**, because local inference is not billed. In a
|
|
389
|
+
mixed run, `--verbose`'s total is exactly the Claude half.
|
|
390
|
+
- **Structured output** is requested through Ollama's `format` field. A model
|
|
391
|
+
or server that cannot honour it (Ollama Cloud does not support it at all)
|
|
392
|
+
falls back to plain-text parsing automatically.
|
|
393
|
+
- **A missing model is an error, not a download.** `cco` tells you to run
|
|
394
|
+
`ollama pull <model>` rather than pulling tens of gigabytes on your behalf.
|
|
395
|
+
- **Reasoning is never requested**, and any the model volunteers is
|
|
396
|
+
discarded - models disagree about whether thinking can even be switched
|
|
397
|
+
off, and asking is a good way to earn a 400.
|
|
398
|
+
|
|
196
399
|
## Development
|
|
197
400
|
|
|
198
401
|
```sh
|
package/index.ts
CHANGED
|
@@ -9,10 +9,36 @@ export type {
|
|
|
9
9
|
GenerateOptions,
|
|
10
10
|
GenerateProgress,
|
|
11
11
|
GenerateResult,
|
|
12
|
+
IgnoreStats,
|
|
13
|
+
LowPriorityStats,
|
|
14
|
+
OllamaContextWindow,
|
|
12
15
|
} from "./src/generate";
|
|
13
|
-
export { runPrompt } from "./src/agent";
|
|
16
|
+
export { runClaudePrompt, runPrompt } from "./src/agent";
|
|
14
17
|
export type { RunPromptOptions } from "./src/agent";
|
|
15
|
-
export {
|
|
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
|
+
partitionDiff,
|
|
36
|
+
sectionPaths,
|
|
37
|
+
splitDiff,
|
|
38
|
+
} from "./src/diff";
|
|
39
|
+
export type { DiffPartition, IgnoreResult } from "./src/diff";
|
|
40
|
+
export { createPathMatcher, matchesPathPatterns } from "./src/paths";
|
|
41
|
+
export type { PathMatcher } from "./src/paths";
|
|
16
42
|
export {
|
|
17
43
|
DEFAULT_CONFIG,
|
|
18
44
|
loadFileConfig,
|
|
@@ -32,8 +58,11 @@ export {
|
|
|
32
58
|
export * as git from "./src/git";
|
|
33
59
|
export { ClaudeCommitError } from "./src/errors";
|
|
34
60
|
export type {
|
|
61
|
+
ChangePriority,
|
|
35
62
|
Config,
|
|
63
|
+
DiffSummary,
|
|
36
64
|
ModelConfig,
|
|
65
|
+
OllamaConfig,
|
|
37
66
|
PartialConfig,
|
|
38
67
|
ModelResult,
|
|
39
68
|
FileChange,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synmux/claude-commit",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Generate git commit messages with Claude, using your Claude Code subscription.",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"module": "index.ts",
|
|
@@ -16,36 +16,48 @@
|
|
|
16
16
|
"index.ts"
|
|
17
17
|
],
|
|
18
18
|
"claude-commit": {
|
|
19
|
+
"allowApiKey": false,
|
|
20
|
+
"charsPerToken": 3.5,
|
|
19
21
|
"conventionalCommits": true,
|
|
20
|
-
"gitmoji": true,
|
|
21
|
-
"multiline": true,
|
|
22
|
-
"template": null,
|
|
23
22
|
"customPrompt": null,
|
|
23
|
+
"gitmoji": true,
|
|
24
24
|
"interactive": false,
|
|
25
|
-
"interactiveCount":
|
|
25
|
+
"interactiveCount": 10,
|
|
26
26
|
"interactiveTemperature": 1,
|
|
27
|
-
"
|
|
27
|
+
"lowPriorityPaths": [
|
|
28
|
+
".agents/**",
|
|
29
|
+
".claude/**",
|
|
30
|
+
"bun.lock",
|
|
31
|
+
".serena"
|
|
32
|
+
],
|
|
33
|
+
"maxChunkTokens": 1000000,
|
|
28
34
|
"models": {
|
|
29
|
-
"summary": "
|
|
30
|
-
"final": "
|
|
35
|
+
"summary": "fable",
|
|
36
|
+
"final": "fable"
|
|
31
37
|
},
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
|
|
38
|
+
"multiline": true,
|
|
39
|
+
"ollama": {
|
|
40
|
+
"host": "http://localhost:11434",
|
|
41
|
+
"context": "auto",
|
|
42
|
+
"keepAlive": "10m"
|
|
43
|
+
},
|
|
44
|
+
"skipArmored": true,
|
|
45
|
+
"spinner": "pong",
|
|
46
|
+
"template": null
|
|
35
47
|
},
|
|
36
48
|
"devDependencies": {
|
|
37
|
-
"@anthropic-ai/claude-code": "^2.1.
|
|
49
|
+
"@anthropic-ai/claude-code": "^2.1.241",
|
|
38
50
|
"@trunkio/launcher": "^1.3.4",
|
|
39
|
-
"@types/bun": "^1.
|
|
51
|
+
"@types/bun": "^1.4.0",
|
|
40
52
|
"prettier": "3.9.4",
|
|
41
|
-
"skilld": "^2.
|
|
53
|
+
"skilld": "^2.3.0"
|
|
42
54
|
},
|
|
43
55
|
"peerDependencies": {
|
|
44
56
|
"typescript": "^6.0.3"
|
|
45
57
|
},
|
|
46
58
|
"dependencies": {
|
|
47
|
-
"@anthropic-ai/claude-agent-sdk": "^0.3.
|
|
48
|
-
"@opentui/core": "^0.
|
|
59
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.241",
|
|
60
|
+
"@opentui/core": "^0.5.7",
|
|
49
61
|
"cli-spinners": "^3.4.0",
|
|
50
62
|
"commander": "^15.0.0",
|
|
51
63
|
"ora": "^9.4.1"
|
package/src/agent.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
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:
|
|
4
11
|
*
|
|
5
12
|
* The Agent SDK spawns a bundled `claude` binary, so authentication follows
|
|
6
13
|
* Claude Code's own resolution order over the environment we hand it. By
|
|
@@ -19,47 +26,12 @@ import {
|
|
|
19
26
|
type SDKMessage,
|
|
20
27
|
} from "@anthropic-ai/claude-agent-sdk";
|
|
21
28
|
import { ClaudeCommitError } from "./errors";
|
|
22
|
-
import
|
|
29
|
+
import { parseModelRef } from "./models";
|
|
30
|
+
import { runOllamaPrompt } from "./ollama";
|
|
31
|
+
import type { ModelResult, RunPromptOptions } from "./types";
|
|
23
32
|
|
|
24
|
-
export
|
|
25
|
-
/** Model string (alias like `sonnet`, `haiku`, or a full model id). */
|
|
26
|
-
model: string;
|
|
27
|
-
/** Full custom system prompt. */
|
|
28
|
-
system: string;
|
|
29
|
-
/** Receives assistant text as it streams in (enables partial messages). */
|
|
30
|
-
onText?: (delta: string) => void;
|
|
31
|
-
/** Abort the in-flight request. */
|
|
32
|
-
abortController?: AbortController;
|
|
33
|
-
/** Receives the underlying CLI's stderr (for `--verbose`). */
|
|
34
|
-
onStderr?: (data: string) => void;
|
|
35
|
-
/**
|
|
36
|
-
* Sampling temperature. Passed to the model via `CLAUDE_CODE_EXTRA_BODY`.
|
|
37
|
-
* Used to add variety when generating several interactive options. Models
|
|
38
|
-
* that don't accept a temperature override will reject the request, so the
|
|
39
|
-
* caller should be prepared to retry without it.
|
|
40
|
-
*/
|
|
41
|
-
temperature?: number;
|
|
42
|
-
/**
|
|
43
|
-
* Request a structured JSON response matching this schema. The parsed object
|
|
44
|
-
* is returned on {@link ModelResult.structured}. Models that don't support
|
|
45
|
-
* structured outputs will reject the request, so the caller should be
|
|
46
|
-
* prepared to retry without it.
|
|
47
|
-
*/
|
|
48
|
-
outputFormat?: { type: "json_schema"; schema: Record<string, unknown> };
|
|
49
|
-
/**
|
|
50
|
-
* Allow API credentials from the environment to reach the SDK subprocess.
|
|
51
|
-
* Defaults to false: `ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN` are
|
|
52
|
-
* stripped so the run is billed to the Claude subscription.
|
|
53
|
-
*/
|
|
54
|
-
allowApiKey?: boolean;
|
|
55
|
-
}
|
|
33
|
+
export type { RunPromptOptions } from "./types";
|
|
56
34
|
|
|
57
|
-
/**
|
|
58
|
-
* Environment variables that carry Claude API credentials. Their presence
|
|
59
|
-
* switches the spawned `claude` binary from subscription auth to
|
|
60
|
-
* pay-as-you-go API billing, so they are stripped from the subprocess
|
|
61
|
-
* environment unless the user opts in via the `allowApiKey` config option.
|
|
62
|
-
*/
|
|
63
35
|
export const GATED_CREDENTIAL_VARS = [
|
|
64
36
|
"ANTHROPIC_API_KEY",
|
|
65
37
|
"ANTHROPIC_AUTH_TOKEN",
|
|
@@ -195,11 +167,11 @@ export function buildQueryOptions(
|
|
|
195
167
|
}
|
|
196
168
|
|
|
197
169
|
/**
|
|
198
|
-
* Run a single prompt
|
|
170
|
+
* Run a single prompt against a Claude model via the Agent SDK.
|
|
199
171
|
*
|
|
200
172
|
* Throws {@link ClaudeCommitError} on any model/authentication/quota failure.
|
|
201
173
|
*/
|
|
202
|
-
export async function
|
|
174
|
+
export async function runClaudePrompt(
|
|
203
175
|
prompt: string,
|
|
204
176
|
opts: RunPromptOptions,
|
|
205
177
|
): Promise<ModelResult> {
|
|
@@ -285,3 +257,24 @@ export async function runPrompt(
|
|
|
285
257
|
...(structured !== undefined ? { structured } : {}),
|
|
286
258
|
};
|
|
287
259
|
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Run a single prompt against whichever provider `opts.model` names, and
|
|
263
|
+
* return its text response.
|
|
264
|
+
*
|
|
265
|
+
* This is the single seam every caller uses; `generate.ts` accepts a
|
|
266
|
+
* replacement of exactly this shape so the pipeline can be tested without a
|
|
267
|
+
* model of either kind.
|
|
268
|
+
*
|
|
269
|
+
* Throws {@link ClaudeCommitError} on any model, authentication, transport
|
|
270
|
+
* or quota failure.
|
|
271
|
+
*/
|
|
272
|
+
export async function runPrompt(
|
|
273
|
+
prompt: string,
|
|
274
|
+
opts: RunPromptOptions,
|
|
275
|
+
): Promise<ModelResult> {
|
|
276
|
+
const { provider } = parseModelRef(opts.model);
|
|
277
|
+
return provider === "ollama"
|
|
278
|
+
? runOllamaPrompt(prompt, opts)
|
|
279
|
+
: runClaudePrompt(prompt, opts);
|
|
280
|
+
}
|