@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
package/src/agent.ts
DELETED
|
@@ -1,280 +0,0 @@
|
|
|
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 {
|
|
24
|
-
query,
|
|
25
|
-
type Options,
|
|
26
|
-
type SDKMessage,
|
|
27
|
-
} from "@anthropic-ai/claude-agent-sdk";
|
|
28
|
-
import { ClaudeCommitError } from "./errors";
|
|
29
|
-
import { parseModelRef } from "./models";
|
|
30
|
-
import { runOllamaPrompt } from "./ollama";
|
|
31
|
-
import type { ModelResult, RunPromptOptions } from "./types";
|
|
32
|
-
|
|
33
|
-
export type { RunPromptOptions } from "./types";
|
|
34
|
-
|
|
35
|
-
export const GATED_CREDENTIAL_VARS = [
|
|
36
|
-
"ANTHROPIC_API_KEY",
|
|
37
|
-
"ANTHROPIC_AUTH_TOKEN",
|
|
38
|
-
] as const;
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Names of the gated credential variables present in `env`. An empty string
|
|
42
|
-
* counts as present, since presence alone perturbs credential resolution.
|
|
43
|
-
*/
|
|
44
|
-
export function presentCredentialVars(
|
|
45
|
-
env: Record<string, string | undefined>,
|
|
46
|
-
): string[] {
|
|
47
|
-
return GATED_CREDENTIAL_VARS.filter((name) => env[name] !== undefined);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface SubprocessEnvOptions {
|
|
51
|
-
/** Environment to derive the subprocess environment from (usually `process.env`). */
|
|
52
|
-
baseEnv: Record<string, string | undefined>;
|
|
53
|
-
/**
|
|
54
|
-
* Pass API credential variables through instead of stripping them.
|
|
55
|
-
* Defaults to false so the gate fails safe when a caller omits it.
|
|
56
|
-
*/
|
|
57
|
-
allowApiKey?: boolean;
|
|
58
|
-
/** Sampling temperature to inject via `CLAUDE_CODE_EXTRA_BODY`, preserving any existing extra body. */
|
|
59
|
-
temperature?: number;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Build the environment for the Claude Agent SDK subprocess, or return
|
|
64
|
-
* `undefined` when the parent environment can be inherited unchanged.
|
|
65
|
-
*
|
|
66
|
-
* Unless `allowApiKey` is set, API credential variables are removed so the
|
|
67
|
-
* spawned `claude` binary always authenticates with the user's subscription
|
|
68
|
-
* session - an exported `ANTHROPIC_API_KEY` must never silently switch
|
|
69
|
-
* billing to pay-as-you-go.
|
|
70
|
-
*/
|
|
71
|
-
export function buildSubprocessEnv(
|
|
72
|
-
opts: SubprocessEnvOptions,
|
|
73
|
-
): Record<string, string | undefined> | undefined {
|
|
74
|
-
const { baseEnv, allowApiKey = false, temperature } = opts;
|
|
75
|
-
const stripped = allowApiKey ? [] : presentCredentialVars(baseEnv);
|
|
76
|
-
if (stripped.length === 0 && temperature == null) return undefined;
|
|
77
|
-
|
|
78
|
-
const env = { ...baseEnv };
|
|
79
|
-
for (const name of stripped) delete env[name];
|
|
80
|
-
|
|
81
|
-
if (temperature != null) {
|
|
82
|
-
let extra: Record<string, unknown> = {};
|
|
83
|
-
const existing = baseEnv.CLAUDE_CODE_EXTRA_BODY;
|
|
84
|
-
if (existing) {
|
|
85
|
-
try {
|
|
86
|
-
const parsed: unknown = JSON.parse(existing);
|
|
87
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
88
|
-
extra = parsed as Record<string, unknown>;
|
|
89
|
-
}
|
|
90
|
-
} catch {
|
|
91
|
-
/* ignore a malformed existing value */
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
env.CLAUDE_CODE_EXTRA_BODY = JSON.stringify({ ...extra, temperature });
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return env;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Map a known SDK assistant error code to a friendlier, actionable message. */
|
|
101
|
-
function describeAssistantError(code: string): string {
|
|
102
|
-
switch (code) {
|
|
103
|
-
case "authentication_failed":
|
|
104
|
-
case "oauth_org_not_allowed":
|
|
105
|
-
return (
|
|
106
|
-
"Authentication failed. Run `claude login` to sign in with your Claude " +
|
|
107
|
-
"subscription, or set ANTHROPIC_API_KEY and enable `allowApiKey` in " +
|
|
108
|
-
"your claude-commit config."
|
|
109
|
-
);
|
|
110
|
-
case "billing_error":
|
|
111
|
-
return "Billing error from the Claude API. Check your plan or API credits.";
|
|
112
|
-
case "rate_limit":
|
|
113
|
-
return "Rate limited by the Claude API. Try again shortly.";
|
|
114
|
-
case "overloaded":
|
|
115
|
-
return "The Claude API is overloaded. Try again shortly.";
|
|
116
|
-
case "model_not_found":
|
|
117
|
-
return "The requested model was not found. Check the configured model name.";
|
|
118
|
-
case "max_output_tokens":
|
|
119
|
-
return "The model hit its output limit before finishing.";
|
|
120
|
-
default:
|
|
121
|
-
return `Model request failed (${code}).`;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Build the Agent SDK options for one isolated, single-turn text completion.
|
|
127
|
-
*
|
|
128
|
-
* Isolation is layered because the SDK gates each context source separately:
|
|
129
|
-
*
|
|
130
|
-
* - `settingSources: []` disables settings files and `CLAUDE.md` - and only
|
|
131
|
-
* those. It does NOT stop MCP servers or skills from loading.
|
|
132
|
-
* - `mcpServers: {}` + `strictMcpConfig: true` ignore every MCP server
|
|
133
|
-
* configured in `~/.claude.json`, project `.mcp.json`, and plugins.
|
|
134
|
-
* - `skills: []` disables skill discovery, which the CLI otherwise performs
|
|
135
|
-
* even when the `skills` option is omitted entirely.
|
|
136
|
-
* - `tools: []` and `plugins: []` drop all built-in tools and plugins.
|
|
137
|
-
*
|
|
138
|
-
* Omitting any of these lets the user's global Claude Code configuration
|
|
139
|
-
* (MCP tool schemas, skill listings - easily hundreds of thousands of tokens
|
|
140
|
-
* on a busy setup) into every request; with all of them set, live probes
|
|
141
|
-
* measure ~170 input tokens per request. Historical note: the 2026-07
|
|
142
|
-
* "Prompt is too long" failures were ultimately caused by underestimating
|
|
143
|
-
* the token density of armored diff content (see `estimateDiffTokens`), not
|
|
144
|
-
* by this leak - the isolation is hygiene and cost control, not the fix for
|
|
145
|
-
* that bug.
|
|
146
|
-
*/
|
|
147
|
-
export function buildQueryOptions(
|
|
148
|
-
opts: RunPromptOptions,
|
|
149
|
-
subprocessEnv?: Record<string, string | undefined>,
|
|
150
|
-
): Options {
|
|
151
|
-
return {
|
|
152
|
-
model: opts.model,
|
|
153
|
-
systemPrompt: opts.system,
|
|
154
|
-
tools: [], // pure text completion: no Bash/Read/Edit/etc.
|
|
155
|
-
skills: [],
|
|
156
|
-
mcpServers: {},
|
|
157
|
-
strictMcpConfig: true,
|
|
158
|
-
plugins: [],
|
|
159
|
-
settingSources: [],
|
|
160
|
-
maxTurns: 1,
|
|
161
|
-
includePartialMessages: Boolean(opts.onText),
|
|
162
|
-
...(opts.abortController ? { abortController: opts.abortController } : {}),
|
|
163
|
-
...(opts.onStderr ? { stderr: opts.onStderr } : {}),
|
|
164
|
-
...(subprocessEnv ? { env: subprocessEnv } : {}),
|
|
165
|
-
...(opts.outputFormat ? { outputFormat: opts.outputFormat } : {}),
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Run a single prompt against a Claude model via the Agent SDK.
|
|
171
|
-
*
|
|
172
|
-
* Throws {@link ClaudeCommitError} on any model/authentication/quota failure.
|
|
173
|
-
*/
|
|
174
|
-
export async function runClaudePrompt(
|
|
175
|
-
prompt: string,
|
|
176
|
-
opts: RunPromptOptions,
|
|
177
|
-
): Promise<ModelResult> {
|
|
178
|
-
const subprocessEnv = buildSubprocessEnv({
|
|
179
|
-
baseEnv: process.env,
|
|
180
|
-
allowApiKey: opts.allowApiKey ?? false,
|
|
181
|
-
...(opts.temperature != null ? { temperature: opts.temperature } : {}),
|
|
182
|
-
});
|
|
183
|
-
const options = buildQueryOptions(opts, subprocessEnv);
|
|
184
|
-
|
|
185
|
-
let resultText: string | null = null;
|
|
186
|
-
let costUsd = 0;
|
|
187
|
-
let model: string | undefined;
|
|
188
|
-
let structured: unknown;
|
|
189
|
-
let assistantError: string | undefined;
|
|
190
|
-
|
|
191
|
-
let response;
|
|
192
|
-
try {
|
|
193
|
-
response = query({ prompt, options });
|
|
194
|
-
for await (const message of response as AsyncIterable<SDKMessage>) {
|
|
195
|
-
switch (message.type) {
|
|
196
|
-
case "stream_event": {
|
|
197
|
-
if (opts.onText) {
|
|
198
|
-
const event = message.event as {
|
|
199
|
-
type?: string;
|
|
200
|
-
delta?: { type?: string; text?: string };
|
|
201
|
-
};
|
|
202
|
-
if (
|
|
203
|
-
event.type === "content_block_delta" &&
|
|
204
|
-
event.delta?.type === "text_delta"
|
|
205
|
-
) {
|
|
206
|
-
opts.onText(event.delta.text ?? "");
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
break;
|
|
210
|
-
}
|
|
211
|
-
case "assistant": {
|
|
212
|
-
if (message.error) assistantError = message.error;
|
|
213
|
-
break;
|
|
214
|
-
}
|
|
215
|
-
case "result": {
|
|
216
|
-
costUsd = message.total_cost_usd ?? 0;
|
|
217
|
-
// The served model is the (only) key of modelUsage, when present.
|
|
218
|
-
const usedModels = Object.keys(message.modelUsage ?? {});
|
|
219
|
-
if (usedModels.length > 0) model = usedModels[0];
|
|
220
|
-
if (message.subtype === "success") {
|
|
221
|
-
resultText = message.result;
|
|
222
|
-
structured = message.structured_output;
|
|
223
|
-
} else {
|
|
224
|
-
const detail =
|
|
225
|
-
"errors" in message && message.errors.length
|
|
226
|
-
? message.errors.join("; ")
|
|
227
|
-
: message.subtype;
|
|
228
|
-
throw new ClaudeCommitError(`Model run failed: ${detail}`);
|
|
229
|
-
}
|
|
230
|
-
break;
|
|
231
|
-
}
|
|
232
|
-
default:
|
|
233
|
-
break;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
} catch (err) {
|
|
237
|
-
if (err instanceof ClaudeCommitError) throw err;
|
|
238
|
-
if (opts.abortController?.signal.aborted) {
|
|
239
|
-
throw new ClaudeCommitError("Generation was cancelled.");
|
|
240
|
-
}
|
|
241
|
-
throw new ClaudeCommitError(
|
|
242
|
-
`Failed to call the Claude Agent SDK: ${(err as Error).message}`,
|
|
243
|
-
);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (assistantError) {
|
|
247
|
-
throw new ClaudeCommitError(describeAssistantError(assistantError));
|
|
248
|
-
}
|
|
249
|
-
if (resultText === null) {
|
|
250
|
-
throw new ClaudeCommitError("The model returned no result.");
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return {
|
|
254
|
-
text: resultText.trim(),
|
|
255
|
-
costUsd,
|
|
256
|
-
...(model ? { model } : {}),
|
|
257
|
-
...(structured !== undefined ? { structured } : {}),
|
|
258
|
-
};
|
|
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
|
-
}
|