code-agent-auto-commit 1.0.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/.code-agent-auto-commit.example.json +53 -0
- package/CODE_OF_CONDUCT.md +15 -0
- package/CONTRIBUTING.md +23 -0
- package/LICENSE +21 -0
- package/README.md +162 -0
- package/SECURITY.md +17 -0
- package/dist/adapters/claude.d.ts +20 -0
- package/dist/adapters/claude.js +134 -0
- package/dist/adapters/codex.d.ts +13 -0
- package/dist/adapters/codex.js +71 -0
- package/dist/adapters/opencode.d.ts +13 -0
- package/dist/adapters/opencode.js +98 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +281 -0
- package/dist/core/ai.d.ts +2 -0
- package/dist/core/ai.js +205 -0
- package/dist/core/config.d.ts +8 -0
- package/dist/core/config.js +181 -0
- package/dist/core/exec.d.ts +7 -0
- package/dist/core/exec.js +24 -0
- package/dist/core/filter.d.ts +2 -0
- package/dist/core/filter.js +45 -0
- package/dist/core/fs.d.ts +7 -0
- package/dist/core/fs.js +46 -0
- package/dist/core/git.d.ts +10 -0
- package/dist/core/git.js +108 -0
- package/dist/core/run.d.ts +2 -0
- package/dist/core/run.js +148 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +22 -0
- package/dist/test/config.test.d.ts +1 -0
- package/dist/test/config.test.js +23 -0
- package/dist/test/filter.test.d.ts +1 -0
- package/dist/test/filter.test.js +14 -0
- package/dist/types.d.ts +76 -0
- package/dist/types.js +2 -0
- package/docs/CONFIG.md +72 -0
- package/docs/zh-CN.md +81 -0
- package/package.json +57 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"enabled": true,
|
|
4
|
+
"worktree": "/path/to/your/repo",
|
|
5
|
+
"commit": {
|
|
6
|
+
"mode": "single",
|
|
7
|
+
"fallbackPrefix": "chore(auto)",
|
|
8
|
+
"maxMessageLength": 72
|
|
9
|
+
},
|
|
10
|
+
"ai": {
|
|
11
|
+
"enabled": true,
|
|
12
|
+
"timeoutMs": 15000,
|
|
13
|
+
"model": "openai/gpt-4.1-mini",
|
|
14
|
+
"defaultProvider": "openai",
|
|
15
|
+
"providers": {
|
|
16
|
+
"openai": {
|
|
17
|
+
"api": "openai-completions",
|
|
18
|
+
"baseUrl": "https://api.openai.com/v1",
|
|
19
|
+
"apiKeyEnv": "OPENAI_API_KEY"
|
|
20
|
+
},
|
|
21
|
+
"anthropic": {
|
|
22
|
+
"api": "anthropic-messages",
|
|
23
|
+
"baseUrl": "https://api.anthropic.com/v1",
|
|
24
|
+
"apiKeyEnv": "ANTHROPIC_API_KEY"
|
|
25
|
+
},
|
|
26
|
+
"moonshot": {
|
|
27
|
+
"api": "openai-completions",
|
|
28
|
+
"baseUrl": "https://api.moonshot.ai/v1",
|
|
29
|
+
"apiKeyEnv": "MOONSHOT_API_KEY"
|
|
30
|
+
},
|
|
31
|
+
"minimax": {
|
|
32
|
+
"api": "openai-completions",
|
|
33
|
+
"baseUrl": "https://api.minimax.chat/v1",
|
|
34
|
+
"apiKeyEnv": "MINIMAX_API_KEY"
|
|
35
|
+
},
|
|
36
|
+
"ollama": {
|
|
37
|
+
"api": "openai-completions",
|
|
38
|
+
"baseUrl": "http://127.0.0.1:11434/v1",
|
|
39
|
+
"apiKeyEnv": "OLLAMA_API_KEY"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"push": {
|
|
44
|
+
"enabled": false,
|
|
45
|
+
"provider": "github",
|
|
46
|
+
"remote": "origin",
|
|
47
|
+
"branch": "main"
|
|
48
|
+
},
|
|
49
|
+
"filters": {
|
|
50
|
+
"include": [],
|
|
51
|
+
"exclude": [".env", ".env.*", "*.pem", "*.key", "*.p12"]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our Pledge
|
|
4
|
+
|
|
5
|
+
We are committed to a harassment-free experience for everyone.
|
|
6
|
+
|
|
7
|
+
## Standards
|
|
8
|
+
|
|
9
|
+
- Be respectful and constructive.
|
|
10
|
+
- Assume good intent.
|
|
11
|
+
- Focus feedback on code and ideas, not people.
|
|
12
|
+
|
|
13
|
+
## Enforcement
|
|
14
|
+
|
|
15
|
+
Project maintainers may remove comments, commits, issues, or contributors that violate this policy.
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for contributing.
|
|
4
|
+
|
|
5
|
+
## Development setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm install
|
|
9
|
+
pnpm run typecheck
|
|
10
|
+
pnpm run build
|
|
11
|
+
pnpm test
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Pull requests
|
|
15
|
+
|
|
16
|
+
- Keep changes focused and small.
|
|
17
|
+
- Update docs when behavior changes.
|
|
18
|
+
- Add or update tests for logic changes.
|
|
19
|
+
- Do not commit secrets or personal API keys.
|
|
20
|
+
|
|
21
|
+
## Commit style
|
|
22
|
+
|
|
23
|
+
Use concise, imperative commit messages.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 chat-auto-commit contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# code-agent-auto-commit
|
|
2
|
+
|
|
3
|
+
`code-agent-auto-commit` (`cac`) provides configurable code-agent-end auto-commit(using your git account) for:
|
|
4
|
+
|
|
5
|
+
- OpenCode
|
|
6
|
+
- Claude Code
|
|
7
|
+
- OpenAI Codex CLI
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Auto-commit when a chat/agent turn ends
|
|
12
|
+
- Commit strategies:
|
|
13
|
+
- `single`: all changed files in one commit
|
|
14
|
+
- `per-file`: one file per commit
|
|
15
|
+
- AI-generated commit messages with multi-provider model configuration
|
|
16
|
+
- OpenAI-compatible mode (`openai-completions`)
|
|
17
|
+
- Anthropic-compatible mode (`anthropic-messages`)
|
|
18
|
+
- Optional auto-push to GitHub, GitLab, or generic remotes
|
|
19
|
+
- Tool installers for OpenCode, Codex, and Claude Code
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pnpm add -g code-agent-auto-commit
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Then use the short command:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
cac --help
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# 1. Initialize config
|
|
37
|
+
cac init
|
|
38
|
+
|
|
39
|
+
# 2. Configure AI API key for commit messages
|
|
40
|
+
# Edit .code-agent-auto-commit.json — set your provider, model, and API key env var.
|
|
41
|
+
# Or export the key in your shell:
|
|
42
|
+
export MINIMAX_API_KEY='your-api-key' # or OPENAI_API_KEY, etc.
|
|
43
|
+
|
|
44
|
+
# 3. Install hooks
|
|
45
|
+
cac install --tool all --scope project
|
|
46
|
+
|
|
47
|
+
# 4. Verify
|
|
48
|
+
cac status --scope project
|
|
49
|
+
|
|
50
|
+
# 5. Git config
|
|
51
|
+
git init
|
|
52
|
+
|
|
53
|
+
# 6. Agentic coding
|
|
54
|
+
opencode / claude / codex
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
> **Important:** After `cac init`, you **must** configure an AI provider API key.
|
|
58
|
+
> Without a valid key, AI commit messages will not work and `cac` falls back to
|
|
59
|
+
> generic `chore(auto): ...` prefixed messages.
|
|
60
|
+
>
|
|
61
|
+
> **Model tip:** Choose a fast, lightweight model (e.g. `gpt-4.1-mini`,
|
|
62
|
+
> `MiniMax-M2.1-highspeed`). Commit messages are short — speed matters more
|
|
63
|
+
> than intelligence here.
|
|
64
|
+
|
|
65
|
+
## Commands
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
cac init [--worktree <path>] [--config <path>]
|
|
69
|
+
cac install [--tool all|opencode|codex|claude] [--scope project|global] [--worktree <path>] [--config <path>]
|
|
70
|
+
cac uninstall [--tool all|opencode|codex|claude] [--scope project|global] [--worktree <path>]
|
|
71
|
+
cac status [--scope project|global] [--worktree <path>] [--config <path>]
|
|
72
|
+
cac run [--tool opencode|codex|claude|manual] [--worktree <path>] [--config <path>] [--event-json <json>] [--event-stdin]
|
|
73
|
+
cac set-worktree <path> [--config <path>]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Command Details
|
|
77
|
+
|
|
78
|
+
- `cac init`: creates a config file for a worktree. It resolves the target path from `--config` or defaults to `<worktree>/.code-agent-auto-commit.json`.
|
|
79
|
+
- `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.
|
|
80
|
+
- `cac uninstall`: removes previously installed adapters/hooks for selected tools and scope.
|
|
81
|
+
- `cac status`: prints resolved config path, worktree, commit mode, AI/push toggles, and install status of each adapter.
|
|
82
|
+
- `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.
|
|
83
|
+
- `cac set-worktree`: updates only the `worktree` field in the resolved config file.
|
|
84
|
+
|
|
85
|
+
## Config File
|
|
86
|
+
|
|
87
|
+
Default project config file:
|
|
88
|
+
|
|
89
|
+
`.code-agent-auto-commit.json`
|
|
90
|
+
|
|
91
|
+
You can copy from:
|
|
92
|
+
|
|
93
|
+
`.code-agent-auto-commit.example.json`
|
|
94
|
+
|
|
95
|
+
Full schema and options:
|
|
96
|
+
|
|
97
|
+
- `docs/CONFIG.md`
|
|
98
|
+
- `docs/zh-CN.md`
|
|
99
|
+
|
|
100
|
+
### AI Key Fields
|
|
101
|
+
|
|
102
|
+
- `ai.providers.<name>.apiKeyEnv` expects an environment variable name (for example, `MINIMAX_API_KEY`), not the raw key value.
|
|
103
|
+
- If you prefer storing a key directly in config, use `ai.providers.<name>.apiKey`.
|
|
104
|
+
- If AI request fails (missing key, invalid provider/model, or non-2xx response), `cac` falls back to `commit.fallbackPrefix`-style messages.
|
|
105
|
+
|
|
106
|
+
## AI Models (Multi-Provider)
|
|
107
|
+
|
|
108
|
+
Model format follows `provider/model` (OpenClaw-style). Example:
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"ai": {
|
|
113
|
+
"enabled": true,
|
|
114
|
+
"model": "openai/gpt-4.1-mini",
|
|
115
|
+
"defaultProvider": "openai",
|
|
116
|
+
"providers": {
|
|
117
|
+
"openai": {
|
|
118
|
+
"api": "openai-completions",
|
|
119
|
+
"baseUrl": "https://api.openai.com/v1",
|
|
120
|
+
"apiKeyEnv": "OPENAI_API_KEY"
|
|
121
|
+
},
|
|
122
|
+
"anthropic": {
|
|
123
|
+
"api": "anthropic-messages",
|
|
124
|
+
"baseUrl": "https://api.anthropic.com/v1",
|
|
125
|
+
"apiKeyEnv": "ANTHROPIC_API_KEY"
|
|
126
|
+
},
|
|
127
|
+
"moonshot": {
|
|
128
|
+
"api": "openai-completions",
|
|
129
|
+
"baseUrl": "https://api.moonshot.ai/v1",
|
|
130
|
+
"apiKeyEnv": "MOONSHOT_API_KEY"
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Integration Notes
|
|
138
|
+
|
|
139
|
+
- OpenCode: installs plugin under `.opencode/plugins/` or `~/.config/opencode/plugins/`
|
|
140
|
+
- Codex CLI: writes managed `notify` block in `.codex/config.toml` or `~/.codex/config.toml`
|
|
141
|
+
- Claude Code: installs `Stop` hook in `.claude/settings.json` or `~/.claude/settings.json`
|
|
142
|
+
|
|
143
|
+
If Codex config already has a custom `notify = ...`, installer stops and asks for manual merge.
|
|
144
|
+
|
|
145
|
+
## Development
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
pnpm install
|
|
149
|
+
pnpm run typecheck
|
|
150
|
+
pnpm run build
|
|
151
|
+
pnpm test
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Open Source
|
|
155
|
+
|
|
156
|
+
- Contribution guide: `CONTRIBUTING.md`
|
|
157
|
+
- Security policy: `SECURITY.md`
|
|
158
|
+
- Code of conduct: `CODE_OF_CONDUCT.md`
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
MIT
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Reporting a vulnerability
|
|
4
|
+
|
|
5
|
+
Please report security issues privately to project maintainers instead of opening a public issue.
|
|
6
|
+
|
|
7
|
+
Include:
|
|
8
|
+
|
|
9
|
+
- affected version
|
|
10
|
+
- reproduction steps
|
|
11
|
+
- impact assessment
|
|
12
|
+
|
|
13
|
+
## Secret handling
|
|
14
|
+
|
|
15
|
+
- Never commit API keys.
|
|
16
|
+
- Prefer environment variables (`OPENAI_API_KEY` by default).
|
|
17
|
+
- Keep `.code-agent-auto-commit.json` free of plaintext secrets when possible.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { InstallScope } from "../types";
|
|
2
|
+
export interface ClaudeInstallInput {
|
|
3
|
+
scope: InstallScope;
|
|
4
|
+
worktree: string;
|
|
5
|
+
configPath: string;
|
|
6
|
+
runnerCommand: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function installClaudeAdapter(input: ClaudeInstallInput): {
|
|
9
|
+
settingsPath: string;
|
|
10
|
+
scriptPath: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function uninstallClaudeAdapter(scope: InstallScope, worktree: string): {
|
|
13
|
+
settingsPath: string;
|
|
14
|
+
scriptPath: string;
|
|
15
|
+
};
|
|
16
|
+
export declare function claudeAdapterStatus(scope: InstallScope, worktree: string): {
|
|
17
|
+
settingsPath: string;
|
|
18
|
+
scriptPath: string;
|
|
19
|
+
installed: boolean;
|
|
20
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.installClaudeAdapter = installClaudeAdapter;
|
|
7
|
+
exports.uninstallClaudeAdapter = uninstallClaudeAdapter;
|
|
8
|
+
exports.claudeAdapterStatus = claudeAdapterStatus;
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
11
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
+
const fs_1 = require("../core/fs");
|
|
13
|
+
function settingsPath(scope, worktree) {
|
|
14
|
+
if (scope === "global") {
|
|
15
|
+
return node_path_1.default.join(node_os_1.default.homedir(), ".claude", "settings.json");
|
|
16
|
+
}
|
|
17
|
+
return node_path_1.default.join(worktree, ".claude", "settings.json");
|
|
18
|
+
}
|
|
19
|
+
function hookScriptPath(scope, worktree) {
|
|
20
|
+
if (scope === "global") {
|
|
21
|
+
return node_path_1.default.join(node_os_1.default.homedir(), ".claude", "hooks", "code-agent-auto-commit.sh");
|
|
22
|
+
}
|
|
23
|
+
return node_path_1.default.join(worktree, ".claude", "hooks", "code-agent-auto-commit.sh");
|
|
24
|
+
}
|
|
25
|
+
function readSettings(filePath) {
|
|
26
|
+
if (!node_fs_1.default.existsSync(filePath)) {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
const raw = node_fs_1.default.readFileSync(filePath, "utf8");
|
|
30
|
+
if (!raw.trim()) {
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
return JSON.parse(raw);
|
|
34
|
+
}
|
|
35
|
+
function writeSettings(filePath, settings) {
|
|
36
|
+
(0, fs_1.ensureDirForFile)(filePath);
|
|
37
|
+
node_fs_1.default.writeFileSync(filePath, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
|
|
38
|
+
}
|
|
39
|
+
function buildScript(configPath, runnerCommand) {
|
|
40
|
+
return `#!/usr/bin/env bash
|
|
41
|
+
set -euo pipefail
|
|
42
|
+
|
|
43
|
+
WORKTREE="\${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
44
|
+
${runnerCommand} run --tool claude --worktree "$WORKTREE" --config ${JSON.stringify(configPath)} --event-stdin || true
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
47
|
+
function installClaudeAdapter(input) {
|
|
48
|
+
const resolvedWorktree = node_path_1.default.resolve(input.worktree);
|
|
49
|
+
const resolvedConfig = node_path_1.default.resolve(input.configPath);
|
|
50
|
+
const targetSettingsPath = settingsPath(input.scope, resolvedWorktree);
|
|
51
|
+
const targetScriptPath = hookScriptPath(input.scope, resolvedWorktree);
|
|
52
|
+
const command = `bash ${JSON.stringify(targetScriptPath)}`;
|
|
53
|
+
(0, fs_1.writeTextFile)(targetScriptPath, buildScript(resolvedConfig, input.runnerCommand));
|
|
54
|
+
node_fs_1.default.chmodSync(targetScriptPath, 0o755);
|
|
55
|
+
const settings = readSettings(targetSettingsPath);
|
|
56
|
+
if (!settings.hooks) {
|
|
57
|
+
settings.hooks = {};
|
|
58
|
+
}
|
|
59
|
+
const stopHooks = settings.hooks.Stop ?? [];
|
|
60
|
+
const alreadyExists = stopHooks.some((entry) => entry.hooks.some((hook) => hook.command === command));
|
|
61
|
+
if (!alreadyExists) {
|
|
62
|
+
stopHooks.push({
|
|
63
|
+
matcher: "",
|
|
64
|
+
hooks: [
|
|
65
|
+
{
|
|
66
|
+
type: "command",
|
|
67
|
+
command,
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
settings.hooks.Stop = stopHooks;
|
|
73
|
+
writeSettings(targetSettingsPath, settings);
|
|
74
|
+
return {
|
|
75
|
+
settingsPath: targetSettingsPath,
|
|
76
|
+
scriptPath: targetScriptPath,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function uninstallClaudeAdapter(scope, worktree) {
|
|
80
|
+
const resolvedWorktree = node_path_1.default.resolve(worktree);
|
|
81
|
+
const targetSettingsPath = settingsPath(scope, resolvedWorktree);
|
|
82
|
+
const targetScriptPath = hookScriptPath(scope, resolvedWorktree);
|
|
83
|
+
const command = `bash ${JSON.stringify(targetScriptPath)}`;
|
|
84
|
+
if (node_fs_1.default.existsSync(targetSettingsPath)) {
|
|
85
|
+
const settings = readSettings(targetSettingsPath);
|
|
86
|
+
const stopHooks = settings.hooks?.Stop ?? [];
|
|
87
|
+
const cleaned = stopHooks
|
|
88
|
+
.map((entry) => ({
|
|
89
|
+
...entry,
|
|
90
|
+
hooks: entry.hooks.filter((hook) => hook.command !== command),
|
|
91
|
+
}))
|
|
92
|
+
.filter((entry) => entry.hooks.length > 0);
|
|
93
|
+
if (!settings.hooks) {
|
|
94
|
+
settings.hooks = {};
|
|
95
|
+
}
|
|
96
|
+
if (cleaned.length > 0) {
|
|
97
|
+
settings.hooks.Stop = cleaned;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
delete settings.hooks.Stop;
|
|
101
|
+
}
|
|
102
|
+
if (Object.keys(settings.hooks).length === 0) {
|
|
103
|
+
delete settings.hooks;
|
|
104
|
+
}
|
|
105
|
+
writeSettings(targetSettingsPath, settings);
|
|
106
|
+
}
|
|
107
|
+
if (node_fs_1.default.existsSync(targetScriptPath)) {
|
|
108
|
+
node_fs_1.default.unlinkSync(targetScriptPath);
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
settingsPath: targetSettingsPath,
|
|
112
|
+
scriptPath: targetScriptPath,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function claudeAdapterStatus(scope, worktree) {
|
|
116
|
+
const resolvedWorktree = node_path_1.default.resolve(worktree);
|
|
117
|
+
const targetSettingsPath = settingsPath(scope, resolvedWorktree);
|
|
118
|
+
const targetScriptPath = hookScriptPath(scope, resolvedWorktree);
|
|
119
|
+
const command = `bash ${JSON.stringify(targetScriptPath)}`;
|
|
120
|
+
if (!node_fs_1.default.existsSync(targetSettingsPath)) {
|
|
121
|
+
return {
|
|
122
|
+
settingsPath: targetSettingsPath,
|
|
123
|
+
scriptPath: targetScriptPath,
|
|
124
|
+
installed: false,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const settings = readSettings(targetSettingsPath);
|
|
128
|
+
const hasHook = (settings.hooks?.Stop ?? []).some((entry) => entry.hooks.some((hook) => hook.command === command));
|
|
129
|
+
return {
|
|
130
|
+
settingsPath: targetSettingsPath,
|
|
131
|
+
scriptPath: targetScriptPath,
|
|
132
|
+
installed: hasHook && node_fs_1.default.existsSync(targetScriptPath),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { InstallScope } from "../types";
|
|
2
|
+
export interface CodexInstallInput {
|
|
3
|
+
scope: InstallScope;
|
|
4
|
+
worktree: string;
|
|
5
|
+
configPath: string;
|
|
6
|
+
runnerCommand: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function installCodexAdapter(input: CodexInstallInput): string;
|
|
9
|
+
export declare function uninstallCodexAdapter(scope: InstallScope, worktree: string): string;
|
|
10
|
+
export declare function codexAdapterStatus(scope: InstallScope, worktree: string): {
|
|
11
|
+
path: string;
|
|
12
|
+
installed: boolean;
|
|
13
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.installCodexAdapter = installCodexAdapter;
|
|
7
|
+
exports.uninstallCodexAdapter = uninstallCodexAdapter;
|
|
8
|
+
exports.codexAdapterStatus = codexAdapterStatus;
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
11
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
+
const fs_1 = require("../core/fs");
|
|
13
|
+
const START_MARKER = "# BEGIN code-agent-auto-commit";
|
|
14
|
+
const END_MARKER = "# END code-agent-auto-commit";
|
|
15
|
+
function codexConfigPath(scope, worktree) {
|
|
16
|
+
if (scope === "global") {
|
|
17
|
+
return node_path_1.default.join(node_os_1.default.homedir(), ".codex", "config.toml");
|
|
18
|
+
}
|
|
19
|
+
return node_path_1.default.join(worktree, ".codex", "config.toml");
|
|
20
|
+
}
|
|
21
|
+
function tomlString(value) {
|
|
22
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
23
|
+
return `"${escaped}"`;
|
|
24
|
+
}
|
|
25
|
+
function buildNotifyBlock(worktree, configPath, runnerCommand) {
|
|
26
|
+
const command = [runnerCommand, "run", "--tool", "codex", "--worktree", worktree, "--config", configPath];
|
|
27
|
+
const rendered = command.map((item) => tomlString(item)).join(", ");
|
|
28
|
+
return [
|
|
29
|
+
START_MARKER,
|
|
30
|
+
`notify = [${rendered}]`,
|
|
31
|
+
END_MARKER,
|
|
32
|
+
].join("\n");
|
|
33
|
+
}
|
|
34
|
+
function stripManagedBlock(content) {
|
|
35
|
+
const blockRegex = new RegExp(`${START_MARKER}[\\s\\S]*?${END_MARKER}\\n?`, "g");
|
|
36
|
+
return content.replace(blockRegex, "");
|
|
37
|
+
}
|
|
38
|
+
function installCodexAdapter(input) {
|
|
39
|
+
const filePath = codexConfigPath(input.scope, node_path_1.default.resolve(input.worktree));
|
|
40
|
+
(0, fs_1.ensureDirForFile)(filePath);
|
|
41
|
+
const existing = node_fs_1.default.existsSync(filePath) ? node_fs_1.default.readFileSync(filePath, "utf8") : "";
|
|
42
|
+
const withoutBlock = stripManagedBlock(existing);
|
|
43
|
+
if (/^\s*notify\s*=\s*/m.test(withoutBlock)) {
|
|
44
|
+
throw new Error(`Codex config already contains notify=. Merge manually in ${filePath}`);
|
|
45
|
+
}
|
|
46
|
+
const block = buildNotifyBlock(node_path_1.default.resolve(input.worktree), node_path_1.default.resolve(input.configPath), input.runnerCommand);
|
|
47
|
+
const output = withoutBlock.trimEnd().length > 0 ? `${withoutBlock.trimEnd()}\n\n${block}\n` : `${block}\n`;
|
|
48
|
+
node_fs_1.default.writeFileSync(filePath, output, "utf8");
|
|
49
|
+
return filePath;
|
|
50
|
+
}
|
|
51
|
+
function uninstallCodexAdapter(scope, worktree) {
|
|
52
|
+
const filePath = codexConfigPath(scope, node_path_1.default.resolve(worktree));
|
|
53
|
+
if (!node_fs_1.default.existsSync(filePath)) {
|
|
54
|
+
return filePath;
|
|
55
|
+
}
|
|
56
|
+
const existing = node_fs_1.default.readFileSync(filePath, "utf8");
|
|
57
|
+
const updated = stripManagedBlock(existing).trimEnd();
|
|
58
|
+
node_fs_1.default.writeFileSync(filePath, updated.length > 0 ? `${updated}\n` : "", "utf8");
|
|
59
|
+
return filePath;
|
|
60
|
+
}
|
|
61
|
+
function codexAdapterStatus(scope, worktree) {
|
|
62
|
+
const filePath = codexConfigPath(scope, node_path_1.default.resolve(worktree));
|
|
63
|
+
if (!node_fs_1.default.existsSync(filePath)) {
|
|
64
|
+
return { path: filePath, installed: false };
|
|
65
|
+
}
|
|
66
|
+
const content = node_fs_1.default.readFileSync(filePath, "utf8");
|
|
67
|
+
return {
|
|
68
|
+
path: filePath,
|
|
69
|
+
installed: content.includes(START_MARKER),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { InstallScope } from "../types";
|
|
2
|
+
export interface AdapterInstallInput {
|
|
3
|
+
scope: InstallScope;
|
|
4
|
+
worktree: string;
|
|
5
|
+
configPath: string;
|
|
6
|
+
runnerCommand: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function installOpenCodeAdapter(input: AdapterInstallInput): string;
|
|
9
|
+
export declare function uninstallOpenCodeAdapter(scope: InstallScope, worktree: string): string;
|
|
10
|
+
export declare function opencodeAdapterStatus(scope: InstallScope, worktree: string): {
|
|
11
|
+
path: string;
|
|
12
|
+
installed: boolean;
|
|
13
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.installOpenCodeAdapter = installOpenCodeAdapter;
|
|
7
|
+
exports.uninstallOpenCodeAdapter = uninstallOpenCodeAdapter;
|
|
8
|
+
exports.opencodeAdapterStatus = opencodeAdapterStatus;
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const fs_1 = require("../core/fs");
|
|
12
|
+
const PLUGIN_FILENAME = "code-agent-auto-commit.ts";
|
|
13
|
+
function resolvePluginPath(scope, worktree) {
|
|
14
|
+
if (scope === "global") {
|
|
15
|
+
return node_path_1.default.join((0, fs_1.getUserConfigHome)(), "opencode", "plugins", PLUGIN_FILENAME);
|
|
16
|
+
}
|
|
17
|
+
return node_path_1.default.join(worktree, ".opencode", "plugins", PLUGIN_FILENAME);
|
|
18
|
+
}
|
|
19
|
+
function pluginContent(worktree, configPath, runnerCommand) {
|
|
20
|
+
const target = JSON.stringify(worktree);
|
|
21
|
+
const cfg = JSON.stringify(configPath);
|
|
22
|
+
const runner = JSON.stringify(runnerCommand);
|
|
23
|
+
return `import type { Plugin } from "@opencode-ai/plugin"
|
|
24
|
+
|
|
25
|
+
const TARGET_WORKTREE = ${target}
|
|
26
|
+
const CONFIG_PATH = ${cfg}
|
|
27
|
+
const RUNNER_COMMAND = ${runner}
|
|
28
|
+
|
|
29
|
+
export const ChatAutoCommitPlugin: Plugin = async ({ $, client, worktree }) => {
|
|
30
|
+
const done = new Set<string>()
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
event: async ({ event }) => {
|
|
34
|
+
if (event.type !== "session.status") return
|
|
35
|
+
if (worktree !== TARGET_WORKTREE) return
|
|
36
|
+
|
|
37
|
+
if (event.properties.status.type === "busy") {
|
|
38
|
+
done.delete(event.properties.sessionID)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (event.properties.status.type !== "idle") return
|
|
43
|
+
if (done.has(event.properties.sessionID)) return
|
|
44
|
+
|
|
45
|
+
const result = await $\`${"${RUNNER_COMMAND}"} run --tool opencode --worktree ${"${worktree}"} --config ${"${CONFIG_PATH}"} --session-id ${"${event.properties.sessionID}"}\`.quiet().nothrow()
|
|
46
|
+
if (result.exitCode !== 0) {
|
|
47
|
+
await client.app.log({
|
|
48
|
+
body: {
|
|
49
|
+
service: "code-agent-auto-commit",
|
|
50
|
+
level: "warn",
|
|
51
|
+
message: "auto-commit runner failed",
|
|
52
|
+
extra: {
|
|
53
|
+
sessionID: event.properties.sessionID,
|
|
54
|
+
stderr: result.stderr.toString(),
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
done.add(event.properties.sessionID)
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
done.add(event.properties.sessionID)
|
|
63
|
+
await client.app.log({
|
|
64
|
+
body: {
|
|
65
|
+
service: "code-agent-auto-commit",
|
|
66
|
+
level: "info",
|
|
67
|
+
message: "auto-commit runner finished",
|
|
68
|
+
extra: {
|
|
69
|
+
sessionID: event.properties.sessionID,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
`;
|
|
78
|
+
}
|
|
79
|
+
function installOpenCodeAdapter(input) {
|
|
80
|
+
const targetPath = resolvePluginPath(input.scope, node_path_1.default.resolve(input.worktree));
|
|
81
|
+
(0, fs_1.ensureDirForFile)(targetPath);
|
|
82
|
+
(0, fs_1.writeTextFile)(targetPath, pluginContent(node_path_1.default.resolve(input.worktree), node_path_1.default.resolve(input.configPath), input.runnerCommand));
|
|
83
|
+
return targetPath;
|
|
84
|
+
}
|
|
85
|
+
function uninstallOpenCodeAdapter(scope, worktree) {
|
|
86
|
+
const targetPath = resolvePluginPath(scope, node_path_1.default.resolve(worktree));
|
|
87
|
+
if (node_fs_1.default.existsSync(targetPath)) {
|
|
88
|
+
node_fs_1.default.unlinkSync(targetPath);
|
|
89
|
+
}
|
|
90
|
+
return targetPath;
|
|
91
|
+
}
|
|
92
|
+
function opencodeAdapterStatus(scope, worktree) {
|
|
93
|
+
const targetPath = resolvePluginPath(scope, node_path_1.default.resolve(worktree));
|
|
94
|
+
return {
|
|
95
|
+
path: targetPath,
|
|
96
|
+
installed: node_fs_1.default.existsSync(targetPath),
|
|
97
|
+
};
|
|
98
|
+
}
|
package/dist/cli.d.ts
ADDED