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 CHANGED
@@ -24,6 +24,14 @@
24
24
  pnpm add -g code-agent-auto-commit
25
25
  ```
26
26
 
27
+ To update to the latest version:
28
+
29
+ ```bash
30
+ pnpm update -g code-agent-auto-commit
31
+ OR:
32
+ pnpm add -g code-agent-auto-commit@latest
33
+ ```
34
+
27
35
  Then use the short command:
28
36
 
29
37
  ```bash
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<string | undefined>;
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 DEFAULT_COMMIT_TYPE = "refector";
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 === "feat" || value === "feature") {
8
- return "feat";
9
- }
10
- if (value === "fix" || value === "bugfix" || value === "hotfix") {
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-]+)(?:\([^)]*\))?\s*:\s*(.+)$/);
30
- const shorthand = raw.match(/^(feat|feature|fix|bugfix|hotfix|refactor|refector)\b[\s:-]+(.+)$/i);
31
- const detectedType = normalizeCommitType(conventional?.[1] ?? shorthand?.[1] ?? "");
32
- const type = detectedType ?? DEFAULT_COMMIT_TYPE;
33
- const subjectCandidate = (conventional?.[2] ?? shorthand?.[2] ?? raw)
34
- .replace(/^['"`]+|['"`]+$/g, "")
35
- .replace(/^[-:]+/, "")
36
- .trim();
37
- if (subjectCandidate.length === 0) {
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 prefix = `${type}: `;
41
- const full = `${prefix}${subjectCandidate}`;
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
- return payload.choices?.[0]?.message?.content;
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
- return firstText;
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 undefined;
200
+ return empty;
183
201
  }
184
202
  const { provider, model } = splitModelRef(ai.model, ai.defaultProvider);
185
203
  if (!provider || !model) {
186
- return undefined;
204
+ return empty;
187
205
  }
188
206
  const providerConfig = ai.providers[provider];
189
207
  if (!providerConfig) {
190
- return undefined;
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 content;
213
+ let result;
196
214
  if (providerConfig.api === "openai-completions") {
197
- content = await generateOpenAiStyleMessage(providerConfig, model, summary, maxLength, controller.signal);
215
+ result = await generateOpenAiStyleMessage(providerConfig, model, summary, maxLength, controller.signal);
198
216
  }
199
217
  else {
200
- content = await generateAnthropicStyleMessage(providerConfig, model, summary, maxLength, controller.signal);
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 undefined;
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])(refector|refactor|chore|docs|style|test|perf|build|ci|revert)([^a-z]|$)/.test(value)) {
21
- return "refector";
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 "refector";
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 generated = await (0, ai_1.generateCommitMessage)(aiConfig, summary, maxLength);
63
- if (generated) {
64
- return generated;
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
- if (fallback.length <= maxLength) {
67
- return fallback;
68
- }
69
- return `${normalizeFallbackType(prefix)}: update changes`;
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 message = await buildMessage(config.commit.fallbackPrefix, config.commit.maxMessageLength, config.ai, undefined, fallback, worktree);
110
- const hash = (0, git_1.commit)(worktree, message);
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 message = await buildMessage(config.commit.fallbackPrefix, config.commit.maxMessageLength, config.ai, file.path, fallback, worktree);
128
- const hash = (0, git_1.commit)(worktree, message);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-agent-auto-commit",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "CAC provides configurable AI auto-commit(using your git account) for OpenCode, Claude Code, Codex, and other AI code agents",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",