code-agent-auto-commit 1.2.0 → 1.3.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/README.md +1 -1
- package/dist/cli.js +37 -0
- package/dist/core/ai.d.ts +2 -1
- package/dist/core/ai.js +116 -14
- package/dist/core/run.js +7 -1
- package/dist/types.d.ts +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
9
9
|
const claude_1 = require("./adapters/claude");
|
|
10
10
|
const codex_1 = require("./adapters/codex");
|
|
11
11
|
const opencode_1 = require("./adapters/opencode");
|
|
12
|
+
const ai_1 = require("./core/ai");
|
|
12
13
|
const config_1 = require("./core/config");
|
|
13
14
|
const fs_1 = require("./core/fs");
|
|
14
15
|
const run_1 = require("./core/run");
|
|
@@ -89,6 +90,7 @@ Usage:
|
|
|
89
90
|
cac status [--scope project|global] [--worktree <path>] [--config <path>]
|
|
90
91
|
cac run [--tool opencode|codex|claude|manual] [--worktree <path>] [--config <path>] [--event-json <json>] [--event-stdin]
|
|
91
92
|
cac set-worktree <path> [--config <path>]
|
|
93
|
+
cac ai <message> [--config <path>]
|
|
92
94
|
cac version
|
|
93
95
|
`);
|
|
94
96
|
}
|
|
@@ -243,6 +245,37 @@ async function commandRun(flags, positionals) {
|
|
|
243
245
|
if (result.tokenUsage) {
|
|
244
246
|
console.log(`AI tokens: ${result.tokenUsage.totalTokens} (prompt: ${result.tokenUsage.promptTokens}, completion: ${result.tokenUsage.completionTokens})`);
|
|
245
247
|
}
|
|
248
|
+
if (result.aiWarning) {
|
|
249
|
+
console.warn(`\nWarning: AI commit message failed — ${result.aiWarning}`);
|
|
250
|
+
console.warn(`Using fallback prefix instead. Run "cac ai hello" to test your AI config.`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async function commandAI(flags, positionals) {
|
|
254
|
+
const message = positionals.join(" ").trim();
|
|
255
|
+
if (!message) {
|
|
256
|
+
console.error(`Usage: cac ai <message>`);
|
|
257
|
+
console.error(`Example: cac ai "hello, are you there?"`);
|
|
258
|
+
process.exitCode = 1;
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const worktree = node_path_1.default.resolve(getStringFlag(flags, "worktree") ?? process.cwd());
|
|
262
|
+
const explicitConfig = getStringFlag(flags, "config");
|
|
263
|
+
const loaded = (0, config_1.loadConfig)({ explicitPath: explicitConfig, worktree });
|
|
264
|
+
console.log(`Provider: ${loaded.config.ai.defaultProvider}`);
|
|
265
|
+
console.log(`Model: ${loaded.config.ai.model}`);
|
|
266
|
+
console.log(`Sending: "${message}"`);
|
|
267
|
+
console.log();
|
|
268
|
+
const result = await (0, ai_1.testAI)(loaded.config.ai, message);
|
|
269
|
+
if (!result.ok) {
|
|
270
|
+
console.error(`AI test failed: ${result.error}`);
|
|
271
|
+
process.exitCode = 1;
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
console.log(`Reply: ${result.reply}`);
|
|
275
|
+
if (result.usage) {
|
|
276
|
+
console.log(`Tokens: ${result.usage.totalTokens} (prompt: ${result.usage.promptTokens}, completion: ${result.usage.completionTokens})`);
|
|
277
|
+
}
|
|
278
|
+
console.log(`\nAI is configured correctly.`);
|
|
246
279
|
}
|
|
247
280
|
async function main() {
|
|
248
281
|
const argv = process.argv.slice(2);
|
|
@@ -281,6 +314,10 @@ async function main() {
|
|
|
281
314
|
await commandRun(parsed.flags, parsed.positionals);
|
|
282
315
|
return;
|
|
283
316
|
}
|
|
317
|
+
if (command === "ai") {
|
|
318
|
+
await commandAI(parsed.flags, parsed.positionals);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
284
321
|
throw new Error(`Unknown command: ${command}`);
|
|
285
322
|
}
|
|
286
323
|
main().catch((error) => {
|
package/dist/core/ai.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import type { AIConfig, AIGenerateResult, CommitSummary } from "../types";
|
|
1
|
+
import type { AIConfig, AIGenerateResult, AITestResult, CommitSummary } from "../types";
|
|
2
2
|
export declare function generateCommitMessage(ai: AIConfig, summary: CommitSummary, maxLength: number): Promise<AIGenerateResult>;
|
|
3
|
+
export declare function testAI(ai: AIConfig, userMessage: string): Promise<AITestResult>;
|
package/dist/core/ai.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.generateCommitMessage = generateCommitMessage;
|
|
4
|
+
exports.testAI = testAI;
|
|
4
5
|
const VALID_TYPES = new Set([
|
|
5
6
|
"feat", "fix", "refactor", "docs", "style", "test",
|
|
6
7
|
"chore", "perf", "ci", "build", "revert",
|
|
@@ -111,6 +112,28 @@ function buildUserPrompt(summary, maxLength) {
|
|
|
111
112
|
summary.patch || "(none)",
|
|
112
113
|
].join("\n");
|
|
113
114
|
}
|
|
115
|
+
function validateAIConfig(ai) {
|
|
116
|
+
if (!ai.enabled) {
|
|
117
|
+
return "ai.enabled is false";
|
|
118
|
+
}
|
|
119
|
+
const { provider, model } = splitModelRef(ai.model, ai.defaultProvider);
|
|
120
|
+
if (!provider || !model) {
|
|
121
|
+
return `invalid ai.model "${ai.model}" — expected "provider/model" format`;
|
|
122
|
+
}
|
|
123
|
+
const providerConfig = ai.providers[provider];
|
|
124
|
+
if (!providerConfig) {
|
|
125
|
+
return `provider "${provider}" not found in ai.providers (available: ${Object.keys(ai.providers).join(", ") || "none"})`;
|
|
126
|
+
}
|
|
127
|
+
const apiKey = getApiKey(providerConfig);
|
|
128
|
+
if (!apiKey) {
|
|
129
|
+
const envName = providerConfig.apiKeyEnv;
|
|
130
|
+
if (envName) {
|
|
131
|
+
return `API key not found — env var "${envName}" is not set. Run: export ${envName}='your-key'`;
|
|
132
|
+
}
|
|
133
|
+
return `no API key configured for provider "${provider}" — set apiKeyEnv or apiKey in config`;
|
|
134
|
+
}
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
114
137
|
async function generateOpenAiStyleMessage(provider, model, summary, maxLength, signal) {
|
|
115
138
|
const apiKey = getApiKey(provider);
|
|
116
139
|
const headers = {
|
|
@@ -140,7 +163,8 @@ async function generateOpenAiStyleMessage(provider, model, summary, maxLength, s
|
|
|
140
163
|
signal,
|
|
141
164
|
});
|
|
142
165
|
if (!response.ok) {
|
|
143
|
-
|
|
166
|
+
const body = await response.text().catch(() => "");
|
|
167
|
+
return { content: undefined, usage: undefined, error: `HTTP ${response.status}: ${body.slice(0, 200)}` };
|
|
144
168
|
}
|
|
145
169
|
const payload = (await response.json());
|
|
146
170
|
const usage = payload.usage
|
|
@@ -155,7 +179,7 @@ async function generateOpenAiStyleMessage(provider, model, summary, maxLength, s
|
|
|
155
179
|
async function generateAnthropicStyleMessage(provider, model, summary, maxLength, signal) {
|
|
156
180
|
const apiKey = getApiKey(provider);
|
|
157
181
|
if (!apiKey) {
|
|
158
|
-
return { content: undefined, usage: undefined };
|
|
182
|
+
return { content: undefined, usage: undefined, error: "no API key" };
|
|
159
183
|
}
|
|
160
184
|
const headers = {
|
|
161
185
|
"Content-Type": "application/json",
|
|
@@ -181,7 +205,8 @@ async function generateAnthropicStyleMessage(provider, model, summary, maxLength
|
|
|
181
205
|
signal,
|
|
182
206
|
});
|
|
183
207
|
if (!response.ok) {
|
|
184
|
-
|
|
208
|
+
const body = await response.text().catch(() => "");
|
|
209
|
+
return { content: undefined, usage: undefined, error: `HTTP ${response.status}: ${body.slice(0, 200)}` };
|
|
185
210
|
}
|
|
186
211
|
const payload = (await response.json());
|
|
187
212
|
const firstText = payload.content?.find((item) => item.type === "text")?.text;
|
|
@@ -195,18 +220,12 @@ async function generateAnthropicStyleMessage(provider, model, summary, maxLength
|
|
|
195
220
|
return { content: firstText, usage };
|
|
196
221
|
}
|
|
197
222
|
async function generateCommitMessage(ai, summary, maxLength) {
|
|
198
|
-
const
|
|
199
|
-
if (
|
|
200
|
-
return
|
|
223
|
+
const configError = validateAIConfig(ai);
|
|
224
|
+
if (configError) {
|
|
225
|
+
return { message: undefined, usage: undefined, warning: configError };
|
|
201
226
|
}
|
|
202
227
|
const { provider, model } = splitModelRef(ai.model, ai.defaultProvider);
|
|
203
|
-
if (!provider || !model) {
|
|
204
|
-
return empty;
|
|
205
|
-
}
|
|
206
228
|
const providerConfig = ai.providers[provider];
|
|
207
|
-
if (!providerConfig) {
|
|
208
|
-
return empty;
|
|
209
|
-
}
|
|
210
229
|
const controller = new AbortController();
|
|
211
230
|
const timeout = setTimeout(() => controller.abort(), ai.timeoutMs);
|
|
212
231
|
try {
|
|
@@ -217,11 +236,94 @@ async function generateCommitMessage(ai, summary, maxLength) {
|
|
|
217
236
|
else {
|
|
218
237
|
result = await generateAnthropicStyleMessage(providerConfig, model, summary, maxLength, controller.signal);
|
|
219
238
|
}
|
|
239
|
+
if (result.error) {
|
|
240
|
+
return { message: undefined, usage: result.usage, warning: result.error };
|
|
241
|
+
}
|
|
220
242
|
const normalized = normalizeMessage(result.content ?? "", maxLength);
|
|
221
243
|
return { message: normalized || undefined, usage: result.usage };
|
|
222
244
|
}
|
|
223
|
-
catch {
|
|
224
|
-
|
|
245
|
+
catch (err) {
|
|
246
|
+
const msg = err instanceof Error && err.name === "AbortError"
|
|
247
|
+
? `AI request timed out after ${ai.timeoutMs}ms`
|
|
248
|
+
: `AI request failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
249
|
+
return { message: undefined, usage: undefined, warning: msg };
|
|
250
|
+
}
|
|
251
|
+
finally {
|
|
252
|
+
clearTimeout(timeout);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
async function testAI(ai, userMessage) {
|
|
256
|
+
const configError = validateAIConfig(ai);
|
|
257
|
+
if (configError) {
|
|
258
|
+
return { ok: false, error: configError };
|
|
259
|
+
}
|
|
260
|
+
const { provider, model } = splitModelRef(ai.model, ai.defaultProvider);
|
|
261
|
+
const providerConfig = ai.providers[provider];
|
|
262
|
+
const apiKey = getApiKey(providerConfig);
|
|
263
|
+
const controller = new AbortController();
|
|
264
|
+
const timeout = setTimeout(() => controller.abort(), ai.timeoutMs);
|
|
265
|
+
try {
|
|
266
|
+
if (providerConfig.api === "openai-completions") {
|
|
267
|
+
const headers = {
|
|
268
|
+
"Content-Type": "application/json",
|
|
269
|
+
Authorization: `Bearer ${apiKey}`,
|
|
270
|
+
...(providerConfig.headers ?? {}),
|
|
271
|
+
};
|
|
272
|
+
const response = await fetch(`${providerConfig.baseUrl.replace(/\/$/, "")}/chat/completions`, {
|
|
273
|
+
method: "POST",
|
|
274
|
+
headers,
|
|
275
|
+
body: JSON.stringify({
|
|
276
|
+
model,
|
|
277
|
+
temperature: 0.2,
|
|
278
|
+
messages: [{ role: "user", content: userMessage }],
|
|
279
|
+
}),
|
|
280
|
+
signal: controller.signal,
|
|
281
|
+
});
|
|
282
|
+
if (!response.ok) {
|
|
283
|
+
const body = await response.text().catch(() => "");
|
|
284
|
+
return { ok: false, error: `HTTP ${response.status}: ${body.slice(0, 300)}` };
|
|
285
|
+
}
|
|
286
|
+
const payload = (await response.json());
|
|
287
|
+
const reply = payload.choices?.[0]?.message?.content ?? "";
|
|
288
|
+
const usage = payload.usage
|
|
289
|
+
? { promptTokens: payload.usage.prompt_tokens ?? 0, completionTokens: payload.usage.completion_tokens ?? 0, totalTokens: payload.usage.total_tokens ?? 0 }
|
|
290
|
+
: undefined;
|
|
291
|
+
return { ok: true, reply, usage };
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
const headers = {
|
|
295
|
+
"Content-Type": "application/json",
|
|
296
|
+
"x-api-key": apiKey,
|
|
297
|
+
"anthropic-version": "2023-06-01",
|
|
298
|
+
...(providerConfig.headers ?? {}),
|
|
299
|
+
};
|
|
300
|
+
const response = await fetch(`${providerConfig.baseUrl.replace(/\/$/, "")}/messages`, {
|
|
301
|
+
method: "POST",
|
|
302
|
+
headers,
|
|
303
|
+
body: JSON.stringify({
|
|
304
|
+
model,
|
|
305
|
+
max_tokens: 256,
|
|
306
|
+
messages: [{ role: "user", content: userMessage }],
|
|
307
|
+
}),
|
|
308
|
+
signal: controller.signal,
|
|
309
|
+
});
|
|
310
|
+
if (!response.ok) {
|
|
311
|
+
const body = await response.text().catch(() => "");
|
|
312
|
+
return { ok: false, error: `HTTP ${response.status}: ${body.slice(0, 300)}` };
|
|
313
|
+
}
|
|
314
|
+
const payload = (await response.json());
|
|
315
|
+
const reply = payload.content?.find((item) => item.type === "text")?.text ?? "";
|
|
316
|
+
const usage = payload.usage
|
|
317
|
+
? { promptTokens: payload.usage.input_tokens ?? 0, completionTokens: payload.usage.output_tokens ?? 0, totalTokens: (payload.usage.input_tokens ?? 0) + (payload.usage.output_tokens ?? 0) }
|
|
318
|
+
: undefined;
|
|
319
|
+
return { ok: true, reply, usage };
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
catch (err) {
|
|
323
|
+
const msg = err instanceof Error && err.name === "AbortError"
|
|
324
|
+
? `request timed out after ${ai.timeoutMs}ms`
|
|
325
|
+
: `request failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
326
|
+
return { ok: false, error: msg };
|
|
225
327
|
}
|
|
226
328
|
finally {
|
|
227
329
|
clearTimeout(timeout);
|
package/dist/core/run.js
CHANGED
|
@@ -67,7 +67,7 @@ async function buildMessage(prefix, maxLength, aiConfig, stagedPath, fallback, w
|
|
|
67
67
|
const msg = fallback.length <= maxLength
|
|
68
68
|
? fallback
|
|
69
69
|
: `${normalizeFallbackType(prefix)}: update changes`;
|
|
70
|
-
return { message: msg, usage: result.usage };
|
|
70
|
+
return { message: msg, usage: result.usage, warning: result.warning };
|
|
71
71
|
}
|
|
72
72
|
async function runAutoCommit(context, configOptions) {
|
|
73
73
|
const { config } = (0, config_1.loadConfig)(configOptions);
|
|
@@ -94,6 +94,7 @@ async function runAutoCommit(context, configOptions) {
|
|
|
94
94
|
}
|
|
95
95
|
const commits = [];
|
|
96
96
|
const totalUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
97
|
+
let firstWarning;
|
|
97
98
|
function addUsage(usage) {
|
|
98
99
|
if (!usage)
|
|
99
100
|
return;
|
|
@@ -117,6 +118,8 @@ async function runAutoCommit(context, configOptions) {
|
|
|
117
118
|
const fallback = fallbackSingleMessage(config.commit.fallbackPrefix, changed.length);
|
|
118
119
|
const result = await buildMessage(config.commit.fallbackPrefix, config.commit.maxMessageLength, config.ai, undefined, fallback, worktree);
|
|
119
120
|
addUsage(result.usage);
|
|
121
|
+
if (result.warning && !firstWarning)
|
|
122
|
+
firstWarning = result.warning;
|
|
120
123
|
const hash = (0, git_1.commit)(worktree, result.message);
|
|
121
124
|
commits.push({
|
|
122
125
|
hash,
|
|
@@ -136,6 +139,8 @@ async function runAutoCommit(context, configOptions) {
|
|
|
136
139
|
const fallback = fallbackPerFileMessage(config.commit.fallbackPrefix, file);
|
|
137
140
|
const result = await buildMessage(config.commit.fallbackPrefix, config.commit.maxMessageLength, config.ai, file.path, fallback, worktree);
|
|
138
141
|
addUsage(result.usage);
|
|
142
|
+
if (result.warning && !firstWarning)
|
|
143
|
+
firstWarning = result.warning;
|
|
139
144
|
const hash = (0, git_1.commit)(worktree, result.message);
|
|
140
145
|
commits.push({
|
|
141
146
|
hash,
|
|
@@ -157,5 +162,6 @@ async function runAutoCommit(context, configOptions) {
|
|
|
157
162
|
committed: commits,
|
|
158
163
|
pushed,
|
|
159
164
|
tokenUsage: hasUsage ? totalUsage : undefined,
|
|
165
|
+
aiWarning: firstWarning,
|
|
160
166
|
};
|
|
161
167
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -65,6 +65,13 @@ export interface TokenUsage {
|
|
|
65
65
|
export interface AIGenerateResult {
|
|
66
66
|
message: string | undefined;
|
|
67
67
|
usage: TokenUsage | undefined;
|
|
68
|
+
warning?: string;
|
|
69
|
+
}
|
|
70
|
+
export interface AITestResult {
|
|
71
|
+
ok: boolean;
|
|
72
|
+
reply?: string;
|
|
73
|
+
usage?: TokenUsage;
|
|
74
|
+
error?: string;
|
|
68
75
|
}
|
|
69
76
|
export interface RunResult {
|
|
70
77
|
skipped: boolean;
|
|
@@ -73,6 +80,7 @@ export interface RunResult {
|
|
|
73
80
|
committed: CommitRecord[];
|
|
74
81
|
pushed: boolean;
|
|
75
82
|
tokenUsage?: TokenUsage;
|
|
83
|
+
aiWarning?: string;
|
|
76
84
|
}
|
|
77
85
|
export interface RunContext {
|
|
78
86
|
tool: ToolName;
|
package/package.json
CHANGED