code-agent-auto-commit 1.1.0 → 1.2.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 +8 -0
- package/dist/cli.js +9 -0
- package/dist/core/ai.d.ts +2 -2
- package/dist/core/ai.js +70 -52
- package/dist/core/run.js +29 -16
- package/dist/types.d.ts +10 -0
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/cli.js
CHANGED
|
@@ -89,6 +89,7 @@ Usage:
|
|
|
89
89
|
cac status [--scope project|global] [--worktree <path>] [--config <path>]
|
|
90
90
|
cac run [--tool opencode|codex|claude|manual] [--worktree <path>] [--config <path>] [--event-json <json>] [--event-stdin]
|
|
91
91
|
cac set-worktree <path> [--config <path>]
|
|
92
|
+
cac version
|
|
92
93
|
`);
|
|
93
94
|
}
|
|
94
95
|
async function commandInit(flags) {
|
|
@@ -239,6 +240,9 @@ async function commandRun(flags, positionals) {
|
|
|
239
240
|
console.log(`- ${item.hash.slice(0, 12)} ${item.message}`);
|
|
240
241
|
}
|
|
241
242
|
console.log(`Pushed: ${result.pushed ? "yes" : "no"}`);
|
|
243
|
+
if (result.tokenUsage) {
|
|
244
|
+
console.log(`AI tokens: ${result.tokenUsage.totalTokens} (prompt: ${result.tokenUsage.promptTokens}, completion: ${result.tokenUsage.completionTokens})`);
|
|
245
|
+
}
|
|
242
246
|
}
|
|
243
247
|
async function main() {
|
|
244
248
|
const argv = process.argv.slice(2);
|
|
@@ -248,6 +252,11 @@ async function main() {
|
|
|
248
252
|
printHelp();
|
|
249
253
|
return;
|
|
250
254
|
}
|
|
255
|
+
if (command === "version" || command === "--version" || command === "-v") {
|
|
256
|
+
const pkg = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(__dirname, "..", "package.json"), "utf-8"));
|
|
257
|
+
console.log(pkg.version);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
251
260
|
if (command === "init") {
|
|
252
261
|
await commandInit(parsed.flags);
|
|
253
262
|
return;
|
package/dist/core/ai.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { AIConfig, CommitSummary } from "../types";
|
|
2
|
-
export declare function generateCommitMessage(ai: AIConfig, summary: CommitSummary, maxLength: number): Promise<
|
|
1
|
+
import type { AIConfig, AIGenerateResult, CommitSummary } from "../types";
|
|
2
|
+
export declare function generateCommitMessage(ai: AIConfig, summary: CommitSummary, maxLength: number): Promise<AIGenerateResult>;
|
package/dist/core/ai.js
CHANGED
|
@@ -1,52 +1,55 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.generateCommitMessage = generateCommitMessage;
|
|
4
|
-
const
|
|
4
|
+
const VALID_TYPES = new Set([
|
|
5
|
+
"feat", "fix", "refactor", "docs", "style", "test",
|
|
6
|
+
"chore", "perf", "ci", "build", "revert",
|
|
7
|
+
]);
|
|
8
|
+
const TYPE_ALIASES = {
|
|
9
|
+
feature: "feat",
|
|
10
|
+
bugfix: "fix",
|
|
11
|
+
hotfix: "fix",
|
|
12
|
+
refactoring: "refactor",
|
|
13
|
+
refector: "refactor",
|
|
14
|
+
};
|
|
5
15
|
function normalizeCommitType(raw) {
|
|
6
16
|
const value = raw.trim().toLowerCase();
|
|
7
|
-
if (value
|
|
8
|
-
return
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
return "fix";
|
|
12
|
-
}
|
|
13
|
-
if (value === "refector"
|
|
14
|
-
|| value === "refactor"
|
|
15
|
-
|| value === "refactoring"
|
|
16
|
-
|| value === "chore"
|
|
17
|
-
|| value === "docs"
|
|
18
|
-
|| value === "style"
|
|
19
|
-
|| value === "test"
|
|
20
|
-
|| value === "perf"
|
|
21
|
-
|| value === "build"
|
|
22
|
-
|| value === "ci"
|
|
23
|
-
|| value === "revert") {
|
|
24
|
-
return "refector";
|
|
25
|
-
}
|
|
26
|
-
return undefined;
|
|
17
|
+
if (VALID_TYPES.has(value)) {
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
return TYPE_ALIASES[value] ?? undefined;
|
|
27
21
|
}
|
|
28
22
|
function formatTypedMessage(raw, maxLength) {
|
|
29
|
-
const conventional = raw.match(/^([a-zA-Z-]+)(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
23
|
+
const conventional = raw.match(/^([a-zA-Z-]+)(\([^)]*\))?\s*:\s*(.+)$/);
|
|
24
|
+
if (conventional) {
|
|
25
|
+
const type = normalizeCommitType(conventional[1]) ?? "chore";
|
|
26
|
+
const scope = conventional[2] ?? "";
|
|
27
|
+
const subject = conventional[3]
|
|
28
|
+
.replace(/^['"`]+|['"`]+$/g, "")
|
|
29
|
+
.replace(/^[-:]+/, "")
|
|
30
|
+
.trim();
|
|
31
|
+
if (subject.length === 0)
|
|
32
|
+
return "";
|
|
33
|
+
const full = `${type}${scope}: ${subject}`;
|
|
34
|
+
if (full.length <= maxLength)
|
|
35
|
+
return full;
|
|
36
|
+
const prefix = `${type}${scope}: `;
|
|
37
|
+
const available = maxLength - prefix.length;
|
|
38
|
+
if (available <= 1)
|
|
39
|
+
return prefix.trimEnd().slice(0, maxLength);
|
|
40
|
+
return `${prefix}${subject.slice(0, available - 1).trimEnd()}…`;
|
|
41
|
+
}
|
|
42
|
+
const subject = raw.replace(/^['"`]+|['"`]+$/g, "").trim();
|
|
43
|
+
if (subject.length === 0)
|
|
38
44
|
return "";
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
if (full.length <= maxLength) {
|
|
45
|
+
const prefix = "chore: ";
|
|
46
|
+
const full = `${prefix}${subject}`;
|
|
47
|
+
if (full.length <= maxLength)
|
|
43
48
|
return full;
|
|
44
|
-
}
|
|
45
49
|
const available = maxLength - prefix.length;
|
|
46
|
-
if (available <= 1)
|
|
50
|
+
if (available <= 1)
|
|
47
51
|
return prefix.trimEnd().slice(0, maxLength);
|
|
48
|
-
}
|
|
49
|
-
return `${prefix}${subjectCandidate.slice(0, available - 1).trimEnd()}…`;
|
|
52
|
+
return `${prefix}${subject.slice(0, available - 1).trimEnd()}…`;
|
|
50
53
|
}
|
|
51
54
|
function normalizeMessage(raw, maxLength) {
|
|
52
55
|
const withoutThinking = raw
|
|
@@ -137,15 +140,22 @@ async function generateOpenAiStyleMessage(provider, model, summary, maxLength, s
|
|
|
137
140
|
signal,
|
|
138
141
|
});
|
|
139
142
|
if (!response.ok) {
|
|
140
|
-
return undefined;
|
|
143
|
+
return { content: undefined, usage: undefined };
|
|
141
144
|
}
|
|
142
145
|
const payload = (await response.json());
|
|
143
|
-
|
|
146
|
+
const usage = payload.usage
|
|
147
|
+
? {
|
|
148
|
+
promptTokens: payload.usage.prompt_tokens ?? 0,
|
|
149
|
+
completionTokens: payload.usage.completion_tokens ?? 0,
|
|
150
|
+
totalTokens: payload.usage.total_tokens ?? 0,
|
|
151
|
+
}
|
|
152
|
+
: undefined;
|
|
153
|
+
return { content: payload.choices?.[0]?.message?.content, usage };
|
|
144
154
|
}
|
|
145
155
|
async function generateAnthropicStyleMessage(provider, model, summary, maxLength, signal) {
|
|
146
156
|
const apiKey = getApiKey(provider);
|
|
147
157
|
if (!apiKey) {
|
|
148
|
-
return undefined;
|
|
158
|
+
return { content: undefined, usage: undefined };
|
|
149
159
|
}
|
|
150
160
|
const headers = {
|
|
151
161
|
"Content-Type": "application/json",
|
|
@@ -171,39 +181,47 @@ async function generateAnthropicStyleMessage(provider, model, summary, maxLength
|
|
|
171
181
|
signal,
|
|
172
182
|
});
|
|
173
183
|
if (!response.ok) {
|
|
174
|
-
return undefined;
|
|
184
|
+
return { content: undefined, usage: undefined };
|
|
175
185
|
}
|
|
176
186
|
const payload = (await response.json());
|
|
177
187
|
const firstText = payload.content?.find((item) => item.type === "text")?.text;
|
|
178
|
-
|
|
188
|
+
const usage = payload.usage
|
|
189
|
+
? {
|
|
190
|
+
promptTokens: payload.usage.input_tokens ?? 0,
|
|
191
|
+
completionTokens: payload.usage.output_tokens ?? 0,
|
|
192
|
+
totalTokens: (payload.usage.input_tokens ?? 0) + (payload.usage.output_tokens ?? 0),
|
|
193
|
+
}
|
|
194
|
+
: undefined;
|
|
195
|
+
return { content: firstText, usage };
|
|
179
196
|
}
|
|
180
197
|
async function generateCommitMessage(ai, summary, maxLength) {
|
|
198
|
+
const empty = { message: undefined, usage: undefined };
|
|
181
199
|
if (!ai.enabled) {
|
|
182
|
-
return
|
|
200
|
+
return empty;
|
|
183
201
|
}
|
|
184
202
|
const { provider, model } = splitModelRef(ai.model, ai.defaultProvider);
|
|
185
203
|
if (!provider || !model) {
|
|
186
|
-
return
|
|
204
|
+
return empty;
|
|
187
205
|
}
|
|
188
206
|
const providerConfig = ai.providers[provider];
|
|
189
207
|
if (!providerConfig) {
|
|
190
|
-
return
|
|
208
|
+
return empty;
|
|
191
209
|
}
|
|
192
210
|
const controller = new AbortController();
|
|
193
211
|
const timeout = setTimeout(() => controller.abort(), ai.timeoutMs);
|
|
194
212
|
try {
|
|
195
|
-
let
|
|
213
|
+
let result;
|
|
196
214
|
if (providerConfig.api === "openai-completions") {
|
|
197
|
-
|
|
215
|
+
result = await generateOpenAiStyleMessage(providerConfig, model, summary, maxLength, controller.signal);
|
|
198
216
|
}
|
|
199
217
|
else {
|
|
200
|
-
|
|
218
|
+
result = await generateAnthropicStyleMessage(providerConfig, model, summary, maxLength, controller.signal);
|
|
201
219
|
}
|
|
202
|
-
const normalized = normalizeMessage(content ?? "", maxLength);
|
|
203
|
-
return normalized || undefined;
|
|
220
|
+
const normalized = normalizeMessage(result.content ?? "", maxLength);
|
|
221
|
+
return { message: normalized || undefined, usage: result.usage };
|
|
204
222
|
}
|
|
205
223
|
catch {
|
|
206
|
-
return
|
|
224
|
+
return empty;
|
|
207
225
|
}
|
|
208
226
|
finally {
|
|
209
227
|
clearTimeout(timeout);
|
package/dist/core/run.js
CHANGED
|
@@ -17,10 +17,11 @@ function normalizeFallbackType(prefix) {
|
|
|
17
17
|
if (/(^|[^a-z])(fix|bugfix|hotfix)([^a-z]|$)/.test(value)) {
|
|
18
18
|
return "fix";
|
|
19
19
|
}
|
|
20
|
-
if (/(^|[^a-z])(
|
|
21
|
-
|
|
20
|
+
if (/(^|[^a-z])(refactor|chore|docs|style|test|perf|build|ci|revert)([^a-z]|$)/.test(value)) {
|
|
21
|
+
const match = value.match(/(refactor|chore|docs|style|test|perf|build|ci|revert)/);
|
|
22
|
+
return match ? match[1] : "chore";
|
|
22
23
|
}
|
|
23
|
-
return "
|
|
24
|
+
return "chore";
|
|
24
25
|
}
|
|
25
26
|
function fallbackSingleMessage(prefix, count) {
|
|
26
27
|
const suffix = count === 1 ? "file" : "files";
|
|
@@ -59,14 +60,14 @@ function filterFiles(files, include, exclude) {
|
|
|
59
60
|
}
|
|
60
61
|
async function buildMessage(prefix, maxLength, aiConfig, stagedPath, fallback, worktree) {
|
|
61
62
|
const summary = (0, git_1.getStagedSummary)(worktree, stagedPath);
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
64
|
-
return
|
|
63
|
+
const result = await (0, ai_1.generateCommitMessage)(aiConfig, summary, maxLength);
|
|
64
|
+
if (result.message) {
|
|
65
|
+
return { message: result.message, usage: result.usage };
|
|
65
66
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return
|
|
67
|
+
const msg = fallback.length <= maxLength
|
|
68
|
+
? fallback
|
|
69
|
+
: `${normalizeFallbackType(prefix)}: update changes`;
|
|
70
|
+
return { message: msg, usage: result.usage };
|
|
70
71
|
}
|
|
71
72
|
async function runAutoCommit(context, configOptions) {
|
|
72
73
|
const { config } = (0, config_1.loadConfig)(configOptions);
|
|
@@ -92,6 +93,14 @@ async function runAutoCommit(context, configOptions) {
|
|
|
92
93
|
};
|
|
93
94
|
}
|
|
94
95
|
const commits = [];
|
|
96
|
+
const totalUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
97
|
+
function addUsage(usage) {
|
|
98
|
+
if (!usage)
|
|
99
|
+
return;
|
|
100
|
+
totalUsage.promptTokens += usage.promptTokens;
|
|
101
|
+
totalUsage.completionTokens += usage.completionTokens;
|
|
102
|
+
totalUsage.totalTokens += usage.totalTokens;
|
|
103
|
+
}
|
|
95
104
|
if (config.commit.mode === "single") {
|
|
96
105
|
for (const file of changed) {
|
|
97
106
|
(0, git_1.stagePath)(worktree, file.path);
|
|
@@ -106,11 +115,12 @@ async function runAutoCommit(context, configOptions) {
|
|
|
106
115
|
};
|
|
107
116
|
}
|
|
108
117
|
const fallback = fallbackSingleMessage(config.commit.fallbackPrefix, changed.length);
|
|
109
|
-
const
|
|
110
|
-
|
|
118
|
+
const result = await buildMessage(config.commit.fallbackPrefix, config.commit.maxMessageLength, config.ai, undefined, fallback, worktree);
|
|
119
|
+
addUsage(result.usage);
|
|
120
|
+
const hash = (0, git_1.commit)(worktree, result.message);
|
|
111
121
|
commits.push({
|
|
112
122
|
hash,
|
|
113
|
-
message,
|
|
123
|
+
message: result.message,
|
|
114
124
|
files: changed.map((item) => item.path),
|
|
115
125
|
});
|
|
116
126
|
}
|
|
@@ -124,11 +134,12 @@ async function runAutoCommit(context, configOptions) {
|
|
|
124
134
|
continue;
|
|
125
135
|
}
|
|
126
136
|
const fallback = fallbackPerFileMessage(config.commit.fallbackPrefix, file);
|
|
127
|
-
const
|
|
128
|
-
|
|
137
|
+
const result = await buildMessage(config.commit.fallbackPrefix, config.commit.maxMessageLength, config.ai, file.path, fallback, worktree);
|
|
138
|
+
addUsage(result.usage);
|
|
139
|
+
const hash = (0, git_1.commit)(worktree, result.message);
|
|
129
140
|
commits.push({
|
|
130
141
|
hash,
|
|
131
|
-
message,
|
|
142
|
+
message: result.message,
|
|
132
143
|
files: [file.path],
|
|
133
144
|
});
|
|
134
145
|
}
|
|
@@ -139,10 +150,12 @@ async function runAutoCommit(context, configOptions) {
|
|
|
139
150
|
(0, git_1.push)(worktree, config.push.remote, branch, config.push.provider);
|
|
140
151
|
pushed = true;
|
|
141
152
|
}
|
|
153
|
+
const hasUsage = totalUsage.totalTokens > 0;
|
|
142
154
|
return {
|
|
143
155
|
skipped: false,
|
|
144
156
|
worktree,
|
|
145
157
|
committed: commits,
|
|
146
158
|
pushed,
|
|
159
|
+
tokenUsage: hasUsage ? totalUsage : undefined,
|
|
147
160
|
};
|
|
148
161
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -57,12 +57,22 @@ export interface CommitRecord {
|
|
|
57
57
|
message: string;
|
|
58
58
|
files: string[];
|
|
59
59
|
}
|
|
60
|
+
export interface TokenUsage {
|
|
61
|
+
promptTokens: number;
|
|
62
|
+
completionTokens: number;
|
|
63
|
+
totalTokens: number;
|
|
64
|
+
}
|
|
65
|
+
export interface AIGenerateResult {
|
|
66
|
+
message: string | undefined;
|
|
67
|
+
usage: TokenUsage | undefined;
|
|
68
|
+
}
|
|
60
69
|
export interface RunResult {
|
|
61
70
|
skipped: boolean;
|
|
62
71
|
reason?: string;
|
|
63
72
|
worktree: string;
|
|
64
73
|
committed: CommitRecord[];
|
|
65
74
|
pushed: boolean;
|
|
75
|
+
tokenUsage?: TokenUsage;
|
|
66
76
|
}
|
|
67
77
|
export interface RunContext {
|
|
68
78
|
tool: ToolName;
|
package/package.json
CHANGED