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 CHANGED
@@ -21,7 +21,7 @@
21
21
  ## Installation
22
22
 
23
23
  ```bash
24
- pnpm add -g code-agent-auto-commit
24
+ pnpm add -g code-agent-auto-commit@latest
25
25
  ```
26
26
 
27
27
  To update to the latest version:
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
- return { content: undefined, usage: undefined };
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
- return { content: undefined, usage: undefined };
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 empty = { message: undefined, usage: undefined };
199
- if (!ai.enabled) {
200
- return empty;
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
- return empty;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-agent-auto-commit",
3
- "version": "1.2.0",
3
+ "version": "1.3.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",