code-agent-auto-commit 1.3.0 → 1.3.2
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 +15 -6
- package/dist/cli.js +202 -20
- package/dist/core/ai.js +128 -53
- package/dist/core/config.js +27 -1
- package/dist/core/fs.d.ts +5 -0
- package/dist/core/fs.js +34 -0
- package/dist/test/config.test.js +16 -1
- package/docs/CONFIG.md +5 -2
- package/docs/zh-CN.md +16 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,9 +45,9 @@ cac --help
|
|
|
45
45
|
cac init
|
|
46
46
|
|
|
47
47
|
# 2. Configure AI API key for commit messages
|
|
48
|
-
# Edit .code-agent-auto-commit.json — set your
|
|
49
|
-
#
|
|
50
|
-
|
|
48
|
+
# Edit .cac/.code-agent-auto-commit.json — set your model and defaultProvider.
|
|
49
|
+
# Fill keys in .cac/.env and load them:
|
|
50
|
+
source .cac/.env
|
|
51
51
|
|
|
52
52
|
# 3. Install hooks
|
|
53
53
|
cac install --tool all --scope project
|
|
@@ -79,22 +79,31 @@ cac uninstall [--tool all|opencode|codex|claude] [--scope project|global] [--wor
|
|
|
79
79
|
cac status [--scope project|global] [--worktree <path>] [--config <path>]
|
|
80
80
|
cac run [--tool opencode|codex|claude|manual] [--worktree <path>] [--config <path>] [--event-json <json>] [--event-stdin]
|
|
81
81
|
cac set-worktree <path> [--config <path>]
|
|
82
|
+
cac ai <message> [--config <path>]
|
|
83
|
+
cac ai set-key <provider|ENV_VAR> <api-key> [--config <path>]
|
|
84
|
+
cac ai get-key <provider|ENV_VAR> [--config <path>]
|
|
82
85
|
```
|
|
83
86
|
|
|
84
87
|
### Command Details
|
|
85
88
|
|
|
86
|
-
- `cac init`: creates
|
|
89
|
+
- `cac init`: creates `.cac/.code-agent-auto-commit.json` under the worktree (unless `--config` is provided), and also writes `.cac/.env.example` and `.cac/.env` with default provider API key env vars.
|
|
87
90
|
- `cac install`: installs adapters/hooks for selected tools (`opencode`, `codex`, `claude`) in `project` or `global` scope. If no config exists at the resolved path, it creates one first.
|
|
88
91
|
- `cac uninstall`: removes previously installed adapters/hooks for selected tools and scope.
|
|
89
92
|
- `cac status`: prints resolved config path, worktree, commit mode, AI/push toggles, and install status of each adapter.
|
|
90
|
-
- `cac run`: executes one auto-commit pass (manual or hook-triggered). It reads config, filters changed files, stages/commits by configured mode, and optionally pushes.
|
|
93
|
+
- `cac run`: executes one auto-commit pass (manual or hook-triggered). It reads config, filters changed files, stages/commits by configured mode, and optionally pushes. Hook-triggered runs also write logs to `.cac/run-<timestamp>.log`.
|
|
91
94
|
- `cac set-worktree`: updates only the `worktree` field in the resolved config file.
|
|
95
|
+
- `cac ai`: tests AI request (`cac ai "hi"`) or manages global keys (`set-key` / `get-key`).
|
|
92
96
|
|
|
93
97
|
## Config File
|
|
94
98
|
|
|
95
99
|
Default project config file:
|
|
96
100
|
|
|
97
|
-
`.code-agent-auto-commit.json`
|
|
101
|
+
`.cac/.code-agent-auto-commit.json`
|
|
102
|
+
|
|
103
|
+
Generated env templates:
|
|
104
|
+
|
|
105
|
+
- `.cac/.env.example`
|
|
106
|
+
- `.cac/.env`
|
|
98
107
|
|
|
99
108
|
You can copy from:
|
|
100
109
|
|
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
};
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
8
9
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
10
|
const claude_1 = require("./adapters/claude");
|
|
10
11
|
const codex_1 = require("./adapters/codex");
|
|
@@ -73,6 +74,87 @@ function parseTools(value) {
|
|
|
73
74
|
}
|
|
74
75
|
return tools;
|
|
75
76
|
}
|
|
77
|
+
const ENV_NAME_REGEX = /^[A-Z_][A-Z0-9_]*$/;
|
|
78
|
+
function resolveAiEnvName(target, aiConfig) {
|
|
79
|
+
if (ENV_NAME_REGEX.test(target)) {
|
|
80
|
+
return target;
|
|
81
|
+
}
|
|
82
|
+
const providerConfig = aiConfig.providers[target];
|
|
83
|
+
const envName = providerConfig?.apiKeyEnv?.trim();
|
|
84
|
+
if (envName && ENV_NAME_REGEX.test(envName)) {
|
|
85
|
+
return envName;
|
|
86
|
+
}
|
|
87
|
+
throw new Error(`Unknown provider or env var: ${target}`);
|
|
88
|
+
}
|
|
89
|
+
function shellQuoteSingle(value) {
|
|
90
|
+
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
91
|
+
}
|
|
92
|
+
function parseExportValue(raw) {
|
|
93
|
+
const trimmed = raw.trim();
|
|
94
|
+
if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
95
|
+
return trimmed.slice(1, -1).replace(/'"'"'/g, "'");
|
|
96
|
+
}
|
|
97
|
+
if (trimmed.startsWith("\"") && trimmed.endsWith("\"")) {
|
|
98
|
+
return trimmed.slice(1, -1);
|
|
99
|
+
}
|
|
100
|
+
return trimmed;
|
|
101
|
+
}
|
|
102
|
+
function readKeyFromEnvFile(filePath, envName) {
|
|
103
|
+
if (!node_fs_1.default.existsSync(filePath)) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
const lines = node_fs_1.default.readFileSync(filePath, "utf8").split(/\r?\n/);
|
|
107
|
+
for (const line of lines) {
|
|
108
|
+
const match = line.match(/^\s*export\s+([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
109
|
+
if (!match) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (match[1] !== envName) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
return parseExportValue(match[2]);
|
|
116
|
+
}
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
function detectShellRcPath() {
|
|
120
|
+
const shell = node_path_1.default.basename(process.env.SHELL ?? "");
|
|
121
|
+
const home = node_os_1.default.homedir();
|
|
122
|
+
if (shell === "zsh") {
|
|
123
|
+
return node_path_1.default.join(home, ".zshrc");
|
|
124
|
+
}
|
|
125
|
+
if (shell === "bash") {
|
|
126
|
+
return node_path_1.default.join(home, ".bashrc");
|
|
127
|
+
}
|
|
128
|
+
if (shell === "fish") {
|
|
129
|
+
return node_path_1.default.join(home, ".config", "fish", "config.fish");
|
|
130
|
+
}
|
|
131
|
+
return node_path_1.default.join(home, ".profile");
|
|
132
|
+
}
|
|
133
|
+
function ensureGlobalKeysSource(shellRcPath, keysPath) {
|
|
134
|
+
const isFish = node_path_1.default.basename(shellRcPath) === "config.fish";
|
|
135
|
+
const sourceLine = isFish
|
|
136
|
+
? `if test -f ${JSON.stringify(keysPath)}; source ${JSON.stringify(keysPath)}; end`
|
|
137
|
+
: `[ -f ${JSON.stringify(keysPath)} ] && source ${JSON.stringify(keysPath)}`;
|
|
138
|
+
const existing = node_fs_1.default.existsSync(shellRcPath) ? node_fs_1.default.readFileSync(shellRcPath, "utf8") : "";
|
|
139
|
+
if (existing.includes(keysPath)) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
const next = existing.trimEnd().length > 0
|
|
143
|
+
? `${existing.trimEnd()}\n\n# code-agent-auto-commit global AI keys\n${sourceLine}\n`
|
|
144
|
+
: `# code-agent-auto-commit global AI keys\n${sourceLine}\n`;
|
|
145
|
+
(0, fs_1.writeTextFile)(shellRcPath, next);
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
function writeRunLog(worktree, lines) {
|
|
149
|
+
const logPath = (0, fs_1.getProjectRunLogPath)(worktree);
|
|
150
|
+
const content = [
|
|
151
|
+
`time=${new Date().toISOString()}`,
|
|
152
|
+
...lines,
|
|
153
|
+
"",
|
|
154
|
+
].join("\n");
|
|
155
|
+
(0, fs_1.writeTextFile)(logPath, content);
|
|
156
|
+
return logPath;
|
|
157
|
+
}
|
|
76
158
|
async function readStdinText() {
|
|
77
159
|
const chunks = [];
|
|
78
160
|
for await (const chunk of process.stdin) {
|
|
@@ -91,6 +173,8 @@ Usage:
|
|
|
91
173
|
cac run [--tool opencode|codex|claude|manual] [--worktree <path>] [--config <path>] [--event-json <json>] [--event-stdin]
|
|
92
174
|
cac set-worktree <path> [--config <path>]
|
|
93
175
|
cac ai <message> [--config <path>]
|
|
176
|
+
cac ai set-key <provider|ENV_VAR> <api-key> [--config <path>]
|
|
177
|
+
cac ai get-key <provider|ENV_VAR> [--config <path>]
|
|
94
178
|
cac version
|
|
95
179
|
`);
|
|
96
180
|
}
|
|
@@ -100,6 +184,8 @@ async function commandInit(flags) {
|
|
|
100
184
|
const configPath = explicit ? node_path_1.default.resolve(explicit) : (0, fs_1.getProjectConfigPath)(worktree);
|
|
101
185
|
(0, config_1.initConfigFile)(configPath, worktree);
|
|
102
186
|
console.log(`Initialized config: ${configPath}`);
|
|
187
|
+
console.log(`Generated env template: ${(0, fs_1.getProjectEnvExamplePath)(worktree)}`);
|
|
188
|
+
console.log(`Generated local env: ${(0, fs_1.getProjectEnvPath)(worktree)}`);
|
|
103
189
|
}
|
|
104
190
|
async function commandInstall(flags) {
|
|
105
191
|
const worktree = node_path_1.default.resolve(getStringFlag(flags, "worktree") ?? process.cwd());
|
|
@@ -198,6 +284,23 @@ async function commandRun(flags, positionals) {
|
|
|
198
284
|
const tool = (getStringFlag(flags, "tool") ?? "manual");
|
|
199
285
|
const worktree = getStringFlag(flags, "worktree");
|
|
200
286
|
const configPath = getStringFlag(flags, "config");
|
|
287
|
+
const shouldLogRun = tool !== "manual";
|
|
288
|
+
const runLogLines = [];
|
|
289
|
+
const logInfo = (message) => {
|
|
290
|
+
console.log(message);
|
|
291
|
+
runLogLines.push(message);
|
|
292
|
+
};
|
|
293
|
+
const logWarn = (message) => {
|
|
294
|
+
console.warn(message);
|
|
295
|
+
runLogLines.push(message);
|
|
296
|
+
};
|
|
297
|
+
const flushRunLog = (resolvedWorktree) => {
|
|
298
|
+
if (!shouldLogRun) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const logPath = writeRunLog(resolvedWorktree, runLogLines);
|
|
302
|
+
logInfo(`Run log: ${logPath}`);
|
|
303
|
+
};
|
|
201
304
|
let event;
|
|
202
305
|
const eventJson = getStringFlag(flags, "event-json");
|
|
203
306
|
if (eventJson) {
|
|
@@ -220,37 +323,119 @@ async function commandRun(flags, positionals) {
|
|
|
220
323
|
if (tool === "codex" && event && typeof event === "object") {
|
|
221
324
|
const eventType = event.type;
|
|
222
325
|
if (eventType && eventType !== "agent-turn-complete") {
|
|
223
|
-
|
|
326
|
+
logInfo(`Skipped: codex event ${eventType}`);
|
|
224
327
|
return;
|
|
225
328
|
}
|
|
226
329
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
330
|
+
let result;
|
|
331
|
+
try {
|
|
332
|
+
result = await (0, run_1.runAutoCommit)({
|
|
333
|
+
tool,
|
|
334
|
+
worktree,
|
|
335
|
+
event,
|
|
336
|
+
sessionID: getStringFlag(flags, "session-id"),
|
|
337
|
+
}, {
|
|
338
|
+
explicitPath: configPath,
|
|
339
|
+
worktree,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
344
|
+
logWarn(`Error: ${message}`);
|
|
345
|
+
flushRunLog(node_path_1.default.resolve(worktree ?? process.cwd()));
|
|
346
|
+
throw error;
|
|
347
|
+
}
|
|
236
348
|
if (result.skipped) {
|
|
237
|
-
|
|
349
|
+
logInfo(`Skipped: ${result.reason ?? "unknown"}`);
|
|
350
|
+
flushRunLog(result.worktree);
|
|
238
351
|
return;
|
|
239
352
|
}
|
|
240
|
-
|
|
353
|
+
logInfo(`Committed: ${result.committed.length}`);
|
|
241
354
|
for (const item of result.committed) {
|
|
242
|
-
|
|
355
|
+
logInfo(`- ${item.hash.slice(0, 12)} ${item.message}`);
|
|
243
356
|
}
|
|
244
|
-
|
|
357
|
+
logInfo(`Pushed: ${result.pushed ? "yes" : "no"}`);
|
|
245
358
|
if (result.tokenUsage) {
|
|
246
|
-
|
|
359
|
+
logInfo(`AI tokens: ${result.tokenUsage.totalTokens} (prompt: ${result.tokenUsage.promptTokens}, completion: ${result.tokenUsage.completionTokens})`);
|
|
247
360
|
}
|
|
248
361
|
if (result.aiWarning) {
|
|
249
|
-
|
|
250
|
-
|
|
362
|
+
logWarn("");
|
|
363
|
+
logWarn(`Warning: AI commit message failed — ${result.aiWarning}`);
|
|
364
|
+
logWarn(`Using fallback prefix instead. Run "cac ai hello" to test your AI config.`);
|
|
251
365
|
}
|
|
366
|
+
flushRunLog(result.worktree);
|
|
252
367
|
}
|
|
253
368
|
async function commandAI(flags, positionals) {
|
|
369
|
+
const worktree = node_path_1.default.resolve(getStringFlag(flags, "worktree") ?? process.cwd());
|
|
370
|
+
const explicitConfig = getStringFlag(flags, "config");
|
|
371
|
+
const loaded = (0, config_1.loadConfig)({ explicitPath: explicitConfig, worktree });
|
|
372
|
+
const subcommand = positionals[0];
|
|
373
|
+
if (subcommand === "set-key") {
|
|
374
|
+
const target = positionals[1];
|
|
375
|
+
const key = positionals.slice(2).join(" ").trim();
|
|
376
|
+
if (!target || !key) {
|
|
377
|
+
console.error("Usage: cac ai set-key <provider|ENV_VAR> <api-key>");
|
|
378
|
+
process.exitCode = 1;
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const envName = resolveAiEnvName(target, loaded.config.ai);
|
|
382
|
+
const keysPath = (0, fs_1.getGlobalKeysEnvPath)();
|
|
383
|
+
const line = `export ${envName}=${shellQuoteSingle(key)}`;
|
|
384
|
+
const existing = node_fs_1.default.existsSync(keysPath) ? node_fs_1.default.readFileSync(keysPath, "utf8") : "";
|
|
385
|
+
const lines = existing.split(/\r?\n/);
|
|
386
|
+
let found = false;
|
|
387
|
+
const updated = lines.map((entry) => {
|
|
388
|
+
const match = entry.match(/^\s*export\s+([A-Za-z_][A-Za-z0-9_]*)=/);
|
|
389
|
+
if (match && match[1] === envName) {
|
|
390
|
+
found = true;
|
|
391
|
+
return line;
|
|
392
|
+
}
|
|
393
|
+
return entry;
|
|
394
|
+
});
|
|
395
|
+
if (!found) {
|
|
396
|
+
if (updated.length === 1 && updated[0] === "") {
|
|
397
|
+
updated.splice(0, 1);
|
|
398
|
+
}
|
|
399
|
+
if (updated.length === 0) {
|
|
400
|
+
updated.push("# code-agent-auto-commit global AI keys");
|
|
401
|
+
}
|
|
402
|
+
updated.push(line);
|
|
403
|
+
}
|
|
404
|
+
(0, fs_1.writeTextFile)(keysPath, `${updated.filter((entry, idx, arr) => !(idx === arr.length - 1 && entry === "")).join("\n")}\n`);
|
|
405
|
+
process.env[envName] = key;
|
|
406
|
+
const shellRcPath = detectShellRcPath();
|
|
407
|
+
const inserted = ensureGlobalKeysSource(shellRcPath, keysPath);
|
|
408
|
+
console.log(`Set ${envName} in ${keysPath}`);
|
|
409
|
+
if (inserted) {
|
|
410
|
+
console.log(`Added source line to ${shellRcPath}`);
|
|
411
|
+
}
|
|
412
|
+
console.log(`Global key configured. Open a new shell or run: source ${shellRcPath}`);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (subcommand === "get-key") {
|
|
416
|
+
const target = positionals[1];
|
|
417
|
+
if (!target) {
|
|
418
|
+
console.error("Usage: cac ai get-key <provider|ENV_VAR>");
|
|
419
|
+
process.exitCode = 1;
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
const envName = resolveAiEnvName(target, loaded.config.ai);
|
|
423
|
+
const fromProcess = process.env[envName]?.trim();
|
|
424
|
+
const fromFile = readKeyFromEnvFile((0, fs_1.getGlobalKeysEnvPath)(), envName);
|
|
425
|
+
const value = fromProcess || fromFile;
|
|
426
|
+
if (!value) {
|
|
427
|
+
console.log(`${envName} is not set`);
|
|
428
|
+
process.exitCode = 1;
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const masked = value.length <= 8
|
|
432
|
+
? `${value.slice(0, 2)}***`
|
|
433
|
+
: `${value.slice(0, 4)}***${value.slice(-4)}`;
|
|
434
|
+
console.log(`Env: ${envName}`);
|
|
435
|
+
console.log(`Value: ${masked}`);
|
|
436
|
+
console.log(`Source: ${fromProcess ? "process env" : (0, fs_1.getGlobalKeysEnvPath)()}`);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
254
439
|
const message = positionals.join(" ").trim();
|
|
255
440
|
if (!message) {
|
|
256
441
|
console.error(`Usage: cac ai <message>`);
|
|
@@ -258,9 +443,6 @@ async function commandAI(flags, positionals) {
|
|
|
258
443
|
process.exitCode = 1;
|
|
259
444
|
return;
|
|
260
445
|
}
|
|
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
446
|
console.log(`Provider: ${loaded.config.ai.defaultProvider}`);
|
|
265
447
|
console.log(`Model: ${loaded.config.ai.model}`);
|
|
266
448
|
console.log(`Sending: "${message}"`);
|
package/dist/core/ai.js
CHANGED
|
@@ -13,6 +13,32 @@ const TYPE_ALIASES = {
|
|
|
13
13
|
refactoring: "refactor",
|
|
14
14
|
refector: "refactor",
|
|
15
15
|
};
|
|
16
|
+
const MINIMAX_MODEL_ALIASES = {
|
|
17
|
+
"minimax-m2.5": "MiniMax-M2.5",
|
|
18
|
+
"minimax-m2.5-highspeed": "MiniMax-M2.5-highspeed",
|
|
19
|
+
"minimax-m2.1": "MiniMax-M2.1",
|
|
20
|
+
"minimax-m2.1-highspeed": "MiniMax-M2.1-highspeed",
|
|
21
|
+
"minimax-m2": "MiniMax-M2",
|
|
22
|
+
"minimax-text-01": "MiniMax-Text-01",
|
|
23
|
+
"text-01": "MiniMax-Text-01",
|
|
24
|
+
};
|
|
25
|
+
function normalizeProviderModel(provider, model) {
|
|
26
|
+
const trimmed = model.trim();
|
|
27
|
+
const raw = trimmed.includes("/") ? trimmed.slice(trimmed.lastIndexOf("/") + 1) : trimmed;
|
|
28
|
+
if (provider !== "minimax") {
|
|
29
|
+
return raw;
|
|
30
|
+
}
|
|
31
|
+
return MINIMAX_MODEL_ALIASES[raw.toLowerCase()] ?? raw;
|
|
32
|
+
}
|
|
33
|
+
function minimaxFallbackModel(model) {
|
|
34
|
+
return model === "MiniMax-Text-01" ? undefined : "MiniMax-Text-01";
|
|
35
|
+
}
|
|
36
|
+
function isUnknownModelError(status, body) {
|
|
37
|
+
if (status < 400 || status >= 500) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return /unknown\s+model|invalid\s+model|model.*not\s+found|does\s+not\s+exist|not\s+supported/i.test(body);
|
|
41
|
+
}
|
|
16
42
|
function normalizeCommitType(raw) {
|
|
17
43
|
const value = raw.trim().toLowerCase();
|
|
18
44
|
if (VALID_TYPES.has(value)) {
|
|
@@ -134,7 +160,7 @@ function validateAIConfig(ai) {
|
|
|
134
160
|
}
|
|
135
161
|
return undefined;
|
|
136
162
|
}
|
|
137
|
-
async function generateOpenAiStyleMessage(provider, model, summary, maxLength, signal) {
|
|
163
|
+
async function generateOpenAiStyleMessage(providerName, provider, model, summary, maxLength, signal) {
|
|
138
164
|
const apiKey = getApiKey(provider);
|
|
139
165
|
const headers = {
|
|
140
166
|
"Content-Type": "application/json",
|
|
@@ -143,38 +169,59 @@ async function generateOpenAiStyleMessage(provider, model, summary, maxLength, s
|
|
|
143
169
|
if (apiKey) {
|
|
144
170
|
headers.Authorization = `Bearer ${apiKey}`;
|
|
145
171
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
172
|
+
async function requestModel(modelName) {
|
|
173
|
+
const response = await fetch(`${provider.baseUrl.replace(/\/$/, "")}/chat/completions`, {
|
|
174
|
+
method: "POST",
|
|
175
|
+
headers,
|
|
176
|
+
body: JSON.stringify({
|
|
177
|
+
model: modelName,
|
|
178
|
+
temperature: 0.2,
|
|
179
|
+
messages: [
|
|
180
|
+
{
|
|
181
|
+
role: "system",
|
|
182
|
+
content: "You generate exactly one conventional commit message. Format: '<type>(<scope>): <description>'. Scope is optional. Allowed types: feat, fix, refactor, docs, style, test, chore, perf, ci, build. Description must be imperative, lowercase, no period. Describe the actual change, not just 'update <file>'. No quotes. No code block.",
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
role: "user",
|
|
186
|
+
content: buildUserPrompt(summary, maxLength),
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
}),
|
|
190
|
+
signal,
|
|
191
|
+
});
|
|
192
|
+
if (!response.ok) {
|
|
193
|
+
const body = await response.text().catch(() => "");
|
|
194
|
+
return { ok: false, status: response.status, body };
|
|
195
|
+
}
|
|
196
|
+
const payload = (await response.json());
|
|
197
|
+
const usage = payload.usage
|
|
198
|
+
? {
|
|
199
|
+
promptTokens: payload.usage.prompt_tokens ?? 0,
|
|
200
|
+
completionTokens: payload.usage.completion_tokens ?? 0,
|
|
201
|
+
totalTokens: payload.usage.total_tokens ?? 0,
|
|
202
|
+
}
|
|
203
|
+
: undefined;
|
|
204
|
+
return { ok: true, content: payload.choices?.[0]?.message?.content, usage };
|
|
168
205
|
}
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
206
|
+
const first = await requestModel(model);
|
|
207
|
+
if (first.ok) {
|
|
208
|
+
return { content: first.content, usage: first.usage };
|
|
209
|
+
}
|
|
210
|
+
if (providerName === "minimax" && isUnknownModelError(first.status, first.body)) {
|
|
211
|
+
const fallback = minimaxFallbackModel(model);
|
|
212
|
+
if (fallback) {
|
|
213
|
+
const retry = await requestModel(fallback);
|
|
214
|
+
if (retry.ok) {
|
|
215
|
+
return { content: retry.content, usage: retry.usage };
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
content: undefined,
|
|
219
|
+
usage: undefined,
|
|
220
|
+
error: `HTTP ${first.status}: ${first.body.slice(0, 200)} | retry(${fallback}) HTTP ${retry.status}: ${retry.body.slice(0, 120)}`,
|
|
221
|
+
};
|
|
175
222
|
}
|
|
176
|
-
|
|
177
|
-
return { content:
|
|
223
|
+
}
|
|
224
|
+
return { content: undefined, usage: undefined, error: `HTTP ${first.status}: ${first.body.slice(0, 200)}` };
|
|
178
225
|
}
|
|
179
226
|
async function generateAnthropicStyleMessage(provider, model, summary, maxLength, signal) {
|
|
180
227
|
const apiKey = getApiKey(provider);
|
|
@@ -220,21 +267,25 @@ async function generateAnthropicStyleMessage(provider, model, summary, maxLength
|
|
|
220
267
|
return { content: firstText, usage };
|
|
221
268
|
}
|
|
222
269
|
async function generateCommitMessage(ai, summary, maxLength) {
|
|
270
|
+
if (!ai.enabled) {
|
|
271
|
+
return { message: undefined, usage: undefined };
|
|
272
|
+
}
|
|
223
273
|
const configError = validateAIConfig(ai);
|
|
224
274
|
if (configError) {
|
|
225
275
|
return { message: undefined, usage: undefined, warning: configError };
|
|
226
276
|
}
|
|
227
277
|
const { provider, model } = splitModelRef(ai.model, ai.defaultProvider);
|
|
278
|
+
const resolvedModel = normalizeProviderModel(provider, model);
|
|
228
279
|
const providerConfig = ai.providers[provider];
|
|
229
280
|
const controller = new AbortController();
|
|
230
281
|
const timeout = setTimeout(() => controller.abort(), ai.timeoutMs);
|
|
231
282
|
try {
|
|
232
283
|
let result;
|
|
233
284
|
if (providerConfig.api === "openai-completions") {
|
|
234
|
-
result = await generateOpenAiStyleMessage(providerConfig,
|
|
285
|
+
result = await generateOpenAiStyleMessage(provider, providerConfig, resolvedModel, summary, maxLength, controller.signal);
|
|
235
286
|
}
|
|
236
287
|
else {
|
|
237
|
-
result = await generateAnthropicStyleMessage(providerConfig,
|
|
288
|
+
result = await generateAnthropicStyleMessage(providerConfig, resolvedModel, summary, maxLength, controller.signal);
|
|
238
289
|
}
|
|
239
290
|
if (result.error) {
|
|
240
291
|
return { message: undefined, usage: result.usage, warning: result.error };
|
|
@@ -258,6 +309,7 @@ async function testAI(ai, userMessage) {
|
|
|
258
309
|
return { ok: false, error: configError };
|
|
259
310
|
}
|
|
260
311
|
const { provider, model } = splitModelRef(ai.model, ai.defaultProvider);
|
|
312
|
+
const resolvedModel = normalizeProviderModel(provider, model);
|
|
261
313
|
const providerConfig = ai.providers[provider];
|
|
262
314
|
const apiKey = getApiKey(providerConfig);
|
|
263
315
|
const controller = new AbortController();
|
|
@@ -269,26 +321,49 @@ async function testAI(ai, userMessage) {
|
|
|
269
321
|
Authorization: `Bearer ${apiKey}`,
|
|
270
322
|
...(providerConfig.headers ?? {}),
|
|
271
323
|
};
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
324
|
+
async function requestModel(modelName) {
|
|
325
|
+
const response = await fetch(`${providerConfig.baseUrl.replace(/\/$/, "")}/chat/completions`, {
|
|
326
|
+
method: "POST",
|
|
327
|
+
headers,
|
|
328
|
+
body: JSON.stringify({
|
|
329
|
+
model: modelName,
|
|
330
|
+
temperature: 0.2,
|
|
331
|
+
messages: [{ role: "user", content: userMessage }],
|
|
332
|
+
}),
|
|
333
|
+
signal: controller.signal,
|
|
334
|
+
});
|
|
335
|
+
if (!response.ok) {
|
|
336
|
+
const body = await response.text().catch(() => "");
|
|
337
|
+
return { ok: false, status: response.status, body };
|
|
338
|
+
}
|
|
339
|
+
const payload = (await response.json());
|
|
340
|
+
const usage = payload.usage
|
|
341
|
+
? {
|
|
342
|
+
promptTokens: payload.usage.prompt_tokens ?? 0,
|
|
343
|
+
completionTokens: payload.usage.completion_tokens ?? 0,
|
|
344
|
+
totalTokens: payload.usage.total_tokens ?? 0,
|
|
345
|
+
}
|
|
346
|
+
: undefined;
|
|
347
|
+
return { ok: true, reply: payload.choices?.[0]?.message?.content ?? "", usage };
|
|
285
348
|
}
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
349
|
+
const first = await requestModel(resolvedModel);
|
|
350
|
+
if (first.ok) {
|
|
351
|
+
return { ok: true, reply: first.reply, usage: first.usage };
|
|
352
|
+
}
|
|
353
|
+
if (provider === "minimax" && isUnknownModelError(first.status, first.body)) {
|
|
354
|
+
const fallback = minimaxFallbackModel(resolvedModel);
|
|
355
|
+
if (fallback) {
|
|
356
|
+
const retry = await requestModel(fallback);
|
|
357
|
+
if (retry.ok) {
|
|
358
|
+
return { ok: true, reply: retry.reply, usage: retry.usage };
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
ok: false,
|
|
362
|
+
error: `HTTP ${first.status}: ${first.body.slice(0, 300)} | retry(${fallback}) HTTP ${retry.status}: ${retry.body.slice(0, 200)}`,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return { ok: false, error: `HTTP ${first.status}: ${first.body.slice(0, 300)}` };
|
|
292
367
|
}
|
|
293
368
|
else {
|
|
294
369
|
const headers = {
|
|
@@ -301,7 +376,7 @@ async function testAI(ai, userMessage) {
|
|
|
301
376
|
method: "POST",
|
|
302
377
|
headers,
|
|
303
378
|
body: JSON.stringify({
|
|
304
|
-
model,
|
|
379
|
+
model: resolvedModel,
|
|
305
380
|
max_tokens: 256,
|
|
306
381
|
messages: [{ role: "user", content: userMessage }],
|
|
307
382
|
}),
|
package/dist/core/config.js
CHANGED
|
@@ -141,6 +141,21 @@ function normalizeConfig(config) {
|
|
|
141
141
|
}
|
|
142
142
|
return config;
|
|
143
143
|
}
|
|
144
|
+
function buildProjectEnvContent(config) {
|
|
145
|
+
const envNames = Array.from(new Set(Object.values(config.ai.providers)
|
|
146
|
+
.map((provider) => provider.apiKeyEnv?.trim())
|
|
147
|
+
.filter((name) => Boolean(name)))).sort();
|
|
148
|
+
const lines = [
|
|
149
|
+
"# code-agent-auto-commit local AI keys",
|
|
150
|
+
"# Fill values and run: source .cac/.env",
|
|
151
|
+
"",
|
|
152
|
+
];
|
|
153
|
+
for (const envName of envNames) {
|
|
154
|
+
lines.push(`${envName}=`);
|
|
155
|
+
}
|
|
156
|
+
lines.push("");
|
|
157
|
+
return lines.join("\n");
|
|
158
|
+
}
|
|
144
159
|
function resolveConfigPath(options) {
|
|
145
160
|
if (options.explicitPath) {
|
|
146
161
|
return node_path_1.default.resolve(options.explicitPath);
|
|
@@ -150,6 +165,10 @@ function resolveConfigPath(options) {
|
|
|
150
165
|
if (node_fs_1.default.existsSync(projectPath)) {
|
|
151
166
|
return projectPath;
|
|
152
167
|
}
|
|
168
|
+
const legacyProjectPath = (0, fs_1.getLegacyProjectConfigPath)(cwd);
|
|
169
|
+
if (node_fs_1.default.existsSync(legacyProjectPath)) {
|
|
170
|
+
return legacyProjectPath;
|
|
171
|
+
}
|
|
153
172
|
return (0, fs_1.getGlobalConfigPath)();
|
|
154
173
|
}
|
|
155
174
|
function loadConfig(options) {
|
|
@@ -166,8 +185,15 @@ function loadConfig(options) {
|
|
|
166
185
|
};
|
|
167
186
|
}
|
|
168
187
|
function initConfigFile(targetPath, worktree) {
|
|
169
|
-
const
|
|
188
|
+
const resolvedWorktree = node_path_1.default.resolve(worktree);
|
|
189
|
+
const config = DEFAULT_CONFIG(resolvedWorktree);
|
|
170
190
|
(0, fs_1.writeJsonFile)(targetPath, config);
|
|
191
|
+
const envExamplePath = (0, fs_1.getProjectEnvExamplePath)(resolvedWorktree);
|
|
192
|
+
(0, fs_1.writeTextFile)(envExamplePath, buildProjectEnvContent(config));
|
|
193
|
+
const envPath = (0, fs_1.getProjectEnvPath)(resolvedWorktree);
|
|
194
|
+
if (!node_fs_1.default.existsSync(envPath)) {
|
|
195
|
+
(0, fs_1.writeTextFile)(envPath, buildProjectEnvContent(config));
|
|
196
|
+
}
|
|
171
197
|
return config;
|
|
172
198
|
}
|
|
173
199
|
function updateConfigWorktree(configPath, worktree) {
|
package/dist/core/fs.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
export declare function getUserConfigHome(): string;
|
|
2
2
|
export declare function getProjectConfigPath(worktree: string): string;
|
|
3
|
+
export declare function getLegacyProjectConfigPath(worktree: string): string;
|
|
4
|
+
export declare function getProjectEnvExamplePath(worktree: string): string;
|
|
5
|
+
export declare function getProjectEnvPath(worktree: string): string;
|
|
6
|
+
export declare function getProjectRunLogPath(worktree: string, date?: Date): string;
|
|
3
7
|
export declare function getGlobalConfigPath(): string;
|
|
8
|
+
export declare function getGlobalKeysEnvPath(): string;
|
|
4
9
|
export declare function ensureDirForFile(filePath: string): void;
|
|
5
10
|
export declare function readJsonFile<T>(filePath: string): T | undefined;
|
|
6
11
|
export declare function writeJsonFile(filePath: string, value: unknown): void;
|
package/dist/core/fs.js
CHANGED
|
@@ -5,7 +5,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.getUserConfigHome = getUserConfigHome;
|
|
7
7
|
exports.getProjectConfigPath = getProjectConfigPath;
|
|
8
|
+
exports.getLegacyProjectConfigPath = getLegacyProjectConfigPath;
|
|
9
|
+
exports.getProjectEnvExamplePath = getProjectEnvExamplePath;
|
|
10
|
+
exports.getProjectEnvPath = getProjectEnvPath;
|
|
11
|
+
exports.getProjectRunLogPath = getProjectRunLogPath;
|
|
8
12
|
exports.getGlobalConfigPath = getGlobalConfigPath;
|
|
13
|
+
exports.getGlobalKeysEnvPath = getGlobalKeysEnvPath;
|
|
9
14
|
exports.ensureDirForFile = ensureDirForFile;
|
|
10
15
|
exports.readJsonFile = readJsonFile;
|
|
11
16
|
exports.writeJsonFile = writeJsonFile;
|
|
@@ -13,6 +18,20 @@ exports.writeTextFile = writeTextFile;
|
|
|
13
18
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
14
19
|
const node_os_1 = __importDefault(require("node:os"));
|
|
15
20
|
const node_path_1 = __importDefault(require("node:path"));
|
|
21
|
+
function formatTimestamp(date) {
|
|
22
|
+
const pad = (value, size = 2) => String(value).padStart(size, "0");
|
|
23
|
+
return [
|
|
24
|
+
date.getFullYear(),
|
|
25
|
+
pad(date.getMonth() + 1),
|
|
26
|
+
pad(date.getDate()),
|
|
27
|
+
"-",
|
|
28
|
+
pad(date.getHours()),
|
|
29
|
+
pad(date.getMinutes()),
|
|
30
|
+
pad(date.getSeconds()),
|
|
31
|
+
"-",
|
|
32
|
+
pad(date.getMilliseconds(), 3),
|
|
33
|
+
].join("");
|
|
34
|
+
}
|
|
16
35
|
function getUserConfigHome() {
|
|
17
36
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
18
37
|
if (xdg && xdg.trim().length > 0) {
|
|
@@ -21,11 +40,26 @@ function getUserConfigHome() {
|
|
|
21
40
|
return node_path_1.default.join(node_os_1.default.homedir(), ".config");
|
|
22
41
|
}
|
|
23
42
|
function getProjectConfigPath(worktree) {
|
|
43
|
+
return node_path_1.default.join(worktree, ".cac", ".code-agent-auto-commit.json");
|
|
44
|
+
}
|
|
45
|
+
function getLegacyProjectConfigPath(worktree) {
|
|
24
46
|
return node_path_1.default.join(worktree, ".code-agent-auto-commit.json");
|
|
25
47
|
}
|
|
48
|
+
function getProjectEnvExamplePath(worktree) {
|
|
49
|
+
return node_path_1.default.join(worktree, ".cac", ".env.example");
|
|
50
|
+
}
|
|
51
|
+
function getProjectEnvPath(worktree) {
|
|
52
|
+
return node_path_1.default.join(worktree, ".cac", ".env");
|
|
53
|
+
}
|
|
54
|
+
function getProjectRunLogPath(worktree, date = new Date()) {
|
|
55
|
+
return node_path_1.default.join(worktree, ".cac", `run-${formatTimestamp(date)}.log`);
|
|
56
|
+
}
|
|
26
57
|
function getGlobalConfigPath() {
|
|
27
58
|
return node_path_1.default.join(getUserConfigHome(), "code-agent-auto-commit", "config.json");
|
|
28
59
|
}
|
|
60
|
+
function getGlobalKeysEnvPath() {
|
|
61
|
+
return node_path_1.default.join(getUserConfigHome(), "code-agent-auto-commit", "keys.env");
|
|
62
|
+
}
|
|
29
63
|
function ensureDirForFile(filePath) {
|
|
30
64
|
node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { recursive: true });
|
|
31
65
|
}
|
package/dist/test/config.test.js
CHANGED
|
@@ -9,15 +9,30 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
9
9
|
const node_test_1 = __importDefault(require("node:test"));
|
|
10
10
|
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
11
11
|
const config_1 = require("../core/config");
|
|
12
|
+
const fs_1 = require("../core/fs");
|
|
12
13
|
(0, node_test_1.default)("init and update config file", () => {
|
|
13
14
|
const tempDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "code-agent-auto-commit-"));
|
|
14
|
-
const configPath =
|
|
15
|
+
const configPath = (0, fs_1.getProjectConfigPath)(tempDir);
|
|
15
16
|
const created = (0, config_1.initConfigFile)(configPath, tempDir);
|
|
16
17
|
strict_1.default.equal(created.version, 1);
|
|
17
18
|
strict_1.default.equal(created.worktree, tempDir);
|
|
19
|
+
strict_1.default.equal(node_fs_1.default.existsSync(node_path_1.default.join(tempDir, ".cac", ".env.example")), true);
|
|
20
|
+
strict_1.default.equal(node_fs_1.default.existsSync(node_path_1.default.join(tempDir, ".cac", ".env")), true);
|
|
18
21
|
const loaded = (0, config_1.loadConfig)({ explicitPath: configPath, worktree: tempDir });
|
|
19
22
|
strict_1.default.equal(loaded.config.worktree, tempDir);
|
|
20
23
|
const nextDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "code-agent-auto-commit-next-"));
|
|
21
24
|
const updated = (0, config_1.updateConfigWorktree)(configPath, nextDir);
|
|
22
25
|
strict_1.default.equal(updated.worktree, nextDir);
|
|
23
26
|
});
|
|
27
|
+
(0, node_test_1.default)("loadConfig resolves new and legacy project paths", () => {
|
|
28
|
+
const tempDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "code-agent-auto-commit-resolve-"));
|
|
29
|
+
const newConfigPath = (0, fs_1.getProjectConfigPath)(tempDir);
|
|
30
|
+
(0, config_1.initConfigFile)(newConfigPath, tempDir);
|
|
31
|
+
const loadedNew = (0, config_1.loadConfig)({ worktree: tempDir });
|
|
32
|
+
strict_1.default.equal(loadedNew.path, newConfigPath);
|
|
33
|
+
node_fs_1.default.rmSync(node_path_1.default.join(tempDir, ".cac"), { recursive: true, force: true });
|
|
34
|
+
const legacyPath = node_path_1.default.join(tempDir, ".code-agent-auto-commit.json");
|
|
35
|
+
node_fs_1.default.writeFileSync(legacyPath, JSON.stringify({ version: 1, enabled: true }, null, 2), "utf8");
|
|
36
|
+
const loadedLegacy = (0, config_1.loadConfig)({ worktree: tempDir });
|
|
37
|
+
strict_1.default.equal(loadedLegacy.path, legacyPath);
|
|
38
|
+
});
|
package/docs/CONFIG.md
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
`cac` reads JSON config from:
|
|
4
4
|
|
|
5
5
|
1. `--config <path>` if provided
|
|
6
|
-
2. `<worktree>/.code-agent-auto-commit.json` if it exists
|
|
7
|
-
3.
|
|
6
|
+
2. `<worktree>/.cac/.code-agent-auto-commit.json` if it exists
|
|
7
|
+
3. `<worktree>/.code-agent-auto-commit.json` if it exists (legacy path)
|
|
8
|
+
4. `~/.config/code-agent-auto-commit/config.json`
|
|
8
9
|
|
|
9
10
|
## Schema
|
|
10
11
|
|
|
@@ -70,3 +71,5 @@
|
|
|
70
71
|
- `gitlab`: remote URL must contain `gitlab`
|
|
71
72
|
- `generic`: no provider URL validation
|
|
72
73
|
- Keep API keys in environment variables when possible.
|
|
74
|
+
- `cac init` also creates `.cac/.env.example` and `.cac/.env` with provider key variables.
|
|
75
|
+
- Hook-triggered `cac run` writes output logs to `.cac/run-<timestamp>.log`.
|
package/docs/zh-CN.md
CHANGED
|
@@ -15,9 +15,9 @@ It runs commits automatically when a chat/agent turn ends.
|
|
|
15
15
|
cac init
|
|
16
16
|
|
|
17
17
|
# 2. 配置 AI API Key(必须,否则无法生成 AI commit message)
|
|
18
|
-
# 编辑 .code-agent-auto-commit.json,设置
|
|
19
|
-
#
|
|
20
|
-
|
|
18
|
+
# 编辑 .cac/.code-agent-auto-commit.json,设置 model 和 defaultProvider。
|
|
19
|
+
# 在 .cac/.env 中填入 API Key 后加载:
|
|
20
|
+
source .cac/.env
|
|
21
21
|
|
|
22
22
|
# 3. 安装钩子
|
|
23
23
|
cac install --tool all --scope project
|
|
@@ -37,7 +37,8 @@ cac status --scope project
|
|
|
37
37
|
|
|
38
38
|
- `cac init [--worktree <path>] [--config <path>]`
|
|
39
39
|
- Initializes a config file.
|
|
40
|
-
-
|
|
40
|
+
- 默认写入 `<worktree>/.cac/.code-agent-auto-commit.json`;也会生成 `.cac/.env.example` 和 `.cac/.env`。
|
|
41
|
+
- 可通过 `--config` 指定自定义路径。
|
|
41
42
|
|
|
42
43
|
- `cac install [--tool all|opencode|codex|claude] [--scope project|global] [--worktree <path>] [--config <path>]`
|
|
43
44
|
- Installs auto-commit adapters for selected tools (OpenCode/Codex/Claude).
|
|
@@ -54,6 +55,16 @@ cac status --scope project
|
|
|
54
55
|
- `cac run [--tool opencode|codex|claude|manual] [--worktree <path>] [--config <path>] [--event-json <json>] [--event-stdin]`
|
|
55
56
|
- Executes one auto-commit pass (manual trigger or hook trigger).
|
|
56
57
|
- Runs the configured pipeline: filter files -> stage -> commit -> optional push.
|
|
58
|
+
- 由聊天结束自动触发时,会把本次输出写到 `.cac/run-<timestamp>.log`。
|
|
59
|
+
|
|
60
|
+
- `cac ai <message> [--config <path>]`
|
|
61
|
+
- 发送一条测试消息到当前 AI 配置并打印回复。
|
|
62
|
+
|
|
63
|
+
- `cac ai set-key <provider|ENV_VAR> <api-key> [--config <path>]`
|
|
64
|
+
- 全局设置 API Key(写入 `~/.config/code-agent-auto-commit/keys.env`),并自动尝试把 source 语句加入 shell rc。
|
|
65
|
+
|
|
66
|
+
- `cac ai get-key <provider|ENV_VAR> [--config <path>]`
|
|
67
|
+
- 查看当前 key 是否已配置(以脱敏形式显示)。
|
|
57
68
|
|
|
58
69
|
- `cac set-worktree <path> [--config <path>]`
|
|
59
70
|
- Updates only the `worktree` field in config and leaves other settings unchanged.
|
|
@@ -68,7 +79,7 @@ cac status --scope project
|
|
|
68
79
|
|
|
69
80
|
## Config File
|
|
70
81
|
|
|
71
|
-
Default location in repository root: `.code-agent-auto-commit.json`
|
|
82
|
+
Default location in repository root: `.cac/.code-agent-auto-commit.json`
|
|
72
83
|
|
|
73
84
|
For full field details, see `docs/CONFIG.md`.
|
|
74
85
|
|
package/package.json
CHANGED