aicp-cli 0.7.0__tar.gz → 0.9.0__tar.gz
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.
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/CHANGELOG.md +28 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/PKG-INFO +115 -54
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/README.md +114 -53
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/pyproject.toml +1 -1
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/cli.py +15 -8
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/config.py +12 -11
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/menu.py +12 -6
- aicp_cli-0.9.0/src/aicp/notify.py +49 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/present.py +4 -3
- aicp_cli-0.9.0/src/aicp/telegram_notify.py +57 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/tests/conftest.py +7 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/tests/test_cli.py +6 -11
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/tests/test_config.py +7 -7
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/tests/test_menu.py +17 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/tests/test_menu_skills.py +4 -2
- aicp_cli-0.9.0/tests/test_notify.py +115 -0
- aicp_cli-0.9.0/tests/test_telegram_notify.py +61 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/uv.lock +2 -2
- aicp_cli-0.7.0/src/aicp/notify.py +0 -87
- aicp_cli-0.7.0/tests/test_notify.py +0 -165
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/.github/workflows/ci.yml +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/.github/workflows/release.yml +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/.gitignore +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/CLAUDE.md +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/LICENSE +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/TODO.md +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/config.example.json +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/docs/images/aicp-run.png +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/skills/commit.md +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/skills/safe-git-push/SKILL.md +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/skills/safe-git-push/scripts/safe_push.py +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/__init__.py +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/_keyreader.py +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/_utils.py +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/agentcfg.py +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/agents.json +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/agents.py +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/budget.py +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/contracts.py +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/gitflow.py +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/i18n.py +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/quota.py +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/runner.py +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/secrets.py +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/skills.py +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/src/aicp/timing.py +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/tests/test_agentcfg.py +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/tests/test_agents.py +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/tests/test_budget.py +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/tests/test_cross_platform.py +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/tests/test_gitflow.py +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/tests/test_i18n.py +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/tests/test_present.py +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/tests/test_quota.py +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/tests/test_runner.py +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/tests/test_secrets.py +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/tests/test_skills.py +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/tests/test_timing.py +0 -0
- {aicp_cli-0.7.0 → aicp_cli-0.9.0}/tests/test_undo.py +0 -0
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
## [0.9.0] - 2026-09-19
|
|
2
|
+
|
|
3
|
+
### 🚀 Features
|
|
4
|
+
|
|
5
|
+
- **notify:** Use direct Telegram delivery and row highlight
|
|
6
|
+
|
|
7
|
+
### 🐛 Bug Fixes
|
|
8
|
+
|
|
9
|
+
- Switch notifications to bot api
|
|
10
|
+
|
|
11
|
+
### 🧪 Testing
|
|
12
|
+
|
|
13
|
+
- Isolate Telegram credentials
|
|
14
|
+
## [0.8.0] - 2026-09-18
|
|
15
|
+
|
|
16
|
+
### 🐛 Bug Fixes
|
|
17
|
+
|
|
18
|
+
- **agents:** Keep agent listing widths stable across toggles
|
|
19
|
+
- **cli:** Parenthesize concatenated f-string for ruff ISC004
|
|
20
|
+
|
|
21
|
+
### 📚 Documentation
|
|
22
|
+
|
|
23
|
+
- **readme:** Document configurable agent chain and skills field
|
|
24
|
+
- **changelog:** Release v0.8.0
|
|
1
25
|
## [0.7.0] - 2026-09-17
|
|
2
26
|
|
|
3
27
|
### 🚀 Features
|
|
@@ -7,6 +31,10 @@
|
|
|
7
31
|
### 🐛 Bug Fixes
|
|
8
32
|
|
|
9
33
|
- **menu:** Size panels to their own content
|
|
34
|
+
|
|
35
|
+
### 📚 Documentation
|
|
36
|
+
|
|
37
|
+
- **changelog:** Release v0.7.0
|
|
10
38
|
## [0.6.0] - 2026-09-17
|
|
11
39
|
|
|
12
40
|
### 📚 Documentation
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: aicp-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: AI commit + push, with a git-verified result summary
|
|
5
5
|
Project-URL: Repository, https://github.com/weskao/aicp
|
|
6
6
|
Project-URL: Issues, https://github.com/weskao/aicp/issues
|
|
@@ -14,11 +14,19 @@ Description-Content-Type: text/markdown
|
|
|
14
14
|
|
|
15
15
|
AI commit + push, with a git-verified result summary.
|
|
16
16
|
|
|
17
|
-
`aicp` runs an AI coding CLI to write your commit message(s) and push,
|
|
18
|
-
a fallback chain
|
|
19
|
-
`vibe` → `grok` — until one exits 0; anything not installed is skipped. It
|
|
17
|
+
`aicp` runs an AI coding CLI to write your commit message(s) and push, walking
|
|
18
|
+
a fallback chain until one exits 0; anything not installed is skipped. It
|
|
20
19
|
sends that CLI two literal prompts, `/commit` then `/safe-git-push`.
|
|
21
20
|
|
|
21
|
+
**The chain is yours to arrange, and it isn't a fixed set.** The built-in
|
|
22
|
+
registry ships six CLIs — `copilot`, `agy`, `codex`, `claude`, `vibe`,
|
|
23
|
+
`grok` — but that's only the default *order*, not a hardcoded pipeline. Move
|
|
24
|
+
any one of them to the front with `aicp --swap-ai`, set the whole order from
|
|
25
|
+
`aicp --config` or `AICP_CLI_ORDER`, and add your own CLIs (or drop built-in
|
|
26
|
+
ones) with `aicp --agents`. Every agent in the resolved registry is a
|
|
27
|
+
first-class link in the chain — there is no privileged first CLI and no cap
|
|
28
|
+
on how many you run with.
|
|
29
|
+
|
|
22
30
|
The part that matters: **aicp never trusts the CLI's own account of what
|
|
23
31
|
happened.** An AI CLI can print "pushed!" and exit 0 while `/safe-git-push`
|
|
24
32
|
quietly aborted inside it. So every number in the result summary — new
|
|
@@ -28,17 +36,36 @@ failed `git fetch` is never read as "already in sync" either: a stale
|
|
|
28
36
|
remote-tracking ref resolves just fine and would otherwise report a clean
|
|
29
37
|
push that never reached the remote.
|
|
30
38
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
## Contents
|
|
40
|
+
|
|
41
|
+
**Getting started**
|
|
42
|
+
|
|
43
|
+
- [Requirements](#requirements)
|
|
44
|
+
- [Install](#install)
|
|
45
|
+
- [Set up skills](#set-up-skills)
|
|
46
|
+
- [Run it](#run-it)
|
|
47
|
+
|
|
48
|
+
**Using it**
|
|
49
|
+
|
|
50
|
+
- [Fallback order](#fallback-order) — reorder the chain, `--swap-ai`
|
|
51
|
+
- [Fallback results and quota limits](#fallback-results-and-quota-limits)
|
|
52
|
+
- [`--undo`](#--undo) — take back the last commit
|
|
53
|
+
- [Automation / CI](#automation--ci) — the non-interactive flags
|
|
54
|
+
|
|
55
|
+
**Reference**
|
|
56
|
+
|
|
57
|
+
- [Configuration](#configuration) — every setting and env var
|
|
58
|
+
- [Custom agents](#custom-agents) — add your own AI CLI
|
|
59
|
+
- [Safety](#safety) — the pre-commit secret scan
|
|
60
|
+
- [Platform support](#platform-support) — macOS, Linux, Windows
|
|
35
61
|
|
|
36
62
|
## Requirements
|
|
37
63
|
|
|
38
64
|
- Python 3.10 or newer
|
|
39
65
|
- [`uv`](https://docs.astral.sh/uv/) — the install path below
|
|
40
66
|
- `git`
|
|
41
|
-
- At least one
|
|
67
|
+
- At least one AI CLI from the chain on `PATH` (a built-in one, or your own
|
|
68
|
+
via [Custom agents](#custom-agents))
|
|
42
69
|
|
|
43
70
|
The Python package itself has no runtime dependencies.
|
|
44
71
|
|
|
@@ -79,44 +106,26 @@ Targets follow each CLI's own config directory, not its binary name — `agy`
|
|
|
79
106
|
| CLI | Config dir | `/commit` installed? | `/safe-git-push` installed? |
|
|
80
107
|
| --- | --- | --- | --- |
|
|
81
108
|
| `claude` | `~/.claude` | No — keeps your existing `commands/commit.md` | Yes |
|
|
82
|
-
| `codex` | `~/.codex` |
|
|
83
|
-
| `copilot` | `~/.copilot` |
|
|
84
|
-
| `agy` (Gemini CLI) | `~/.gemini` |
|
|
85
|
-
| `vibe` | `~/.vibe` |
|
|
86
|
-
| `grok` | `$GROK_HOME` when set, otherwise `~/.grok` |
|
|
87
|
-
|
|
88
|
-
`/commit` is
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
109
|
+
| `codex` | `~/.codex` | Yes | Yes |
|
|
110
|
+
| `copilot` | `~/.copilot` | Yes | Yes |
|
|
111
|
+
| `agy` (Gemini CLI) | `~/.gemini` | Yes | Yes |
|
|
112
|
+
| `vibe` | `~/.vibe` | Yes | Yes |
|
|
113
|
+
| `grok` | `$GROK_HOME` when set, otherwise `~/.grok` | Yes | Yes |
|
|
114
|
+
|
|
115
|
+
Every configured CLI gets `/safe-git-push`. `/commit` is installed everywhere
|
|
116
|
+
*except* `claude`, which already resolves `/commit` from
|
|
117
|
+
`commands/commit.md` — a second definition under `skills/` would compete with
|
|
118
|
+
it. Which skills an agent gets is data, not a branch: it's the `skills` field
|
|
119
|
+
of that agent's registry entry, so a [custom agent](#custom-agents) declares
|
|
120
|
+
its own list the same way.
|
|
121
|
+
|
|
122
|
+
The vendored text is rewritten per CLI on install — the shipped copy talks
|
|
123
|
+
about `.claude/` and `CLAUDE.md`, which mean nothing to `codex` or `agy`, so
|
|
124
|
+
each install substitutes that agent's own `config_dir` name and `memory_file`.
|
|
92
125
|
|
|
93
126
|
A CLI whose config directory doesn't exist at all is skipped, never created —
|
|
94
127
|
aicp only ever installs into a CLI you've actually set up.
|
|
95
128
|
|
|
96
|
-
### Fallback results and quota limits
|
|
97
|
-
|
|
98
|
-
The opening run panel prints the resolved chain, and the commit panel and
|
|
99
|
-
final RESULT table name the CLI that handled each step (`—` when skipped).
|
|
100
|
-
When a CLI emits a supported, exact quota/rate-limit signal, aicp records the
|
|
101
|
-
outcome as `quota`, notifies through the usual notification path, and excludes
|
|
102
|
-
that CLI from the rest of that one commit/push flow. The exclusion also
|
|
103
|
-
persists: the CLI stays skipped for `AICP_QUOTA_COOLDOWN` seconds (default one
|
|
104
|
-
hour, state in `~/.aicp/quota.json`), because a token or rate-limit wall
|
|
105
|
-
normally stands for hours and every run inside that window would otherwise burn
|
|
106
|
-
a full budget per step on a CLI that cannot succeed. A skipped CLI prints how
|
|
107
|
-
long is left; `AICP_QUOTA_COOLDOWN=0` switches the cooldown off entirely, and
|
|
108
|
-
deleting the file clears it.
|
|
109
|
-
|
|
110
|
-
Only the `quota` outcome starts a cooldown. A timeout does not — a CLI that
|
|
111
|
-
hangs on its rate limit instead of exiting is indistinguishable from one merely
|
|
112
|
-
running long, and sidelining it for an hour on that guess costs more than the
|
|
113
|
-
retry does.
|
|
114
|
-
|
|
115
|
-
Exact detection is intentionally narrow. It is supported for Codex, Claude,
|
|
116
|
-
Vibe, and Grok only; Copilot and `agy` have no verified quota signature, so a
|
|
117
|
-
nonzero exit from either remains an ordinary failure and is still eligible for
|
|
118
|
-
the next step.
|
|
119
|
-
|
|
120
129
|
## Run it
|
|
121
130
|
|
|
122
131
|
The whole command surface is two things to remember:
|
|
@@ -141,6 +150,52 @@ same as `aicp --config`.
|
|
|
141
150
|
right now": which steps run, message language, fallback CLI order, the
|
|
142
151
|
skills install/upgrade view, and a health check.
|
|
143
152
|
|
|
153
|
+
### Fallback order
|
|
154
|
+
|
|
155
|
+
The order is a saved preference, not a property of the tool. Three ways to
|
|
156
|
+
set it, all writing the same `aicp_cli_order` key:
|
|
157
|
+
|
|
158
|
+
```sh
|
|
159
|
+
aicp --swap-ai # pick a CLI, it trades places with whoever holds #1
|
|
160
|
+
aicp --config # the AI CLI order row, arrow keys
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
```sh
|
|
164
|
+
# ~/.aicp/config.json — or AICP_CLI_ORDER="claude codex" aicp for one run
|
|
165
|
+
{ "aicp_cli_order": "claude codex copilot agy vibe grok" }
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`--swap-ai` is a rotation, not a destructive set: the CLI you pick moves to
|
|
169
|
+
`#1` and the old `#1` takes its place, so nothing falls out of the chain. It
|
|
170
|
+
lists every agent in the registry, including ones whose binary isn't on
|
|
171
|
+
`PATH` — a missing binary must not hide a choice you're about to install.
|
|
172
|
+
Naming only the first few is enough; everything you leave out is appended
|
|
173
|
+
behind them in registry order.
|
|
174
|
+
|
|
175
|
+
### Fallback results and quota limits
|
|
176
|
+
|
|
177
|
+
The opening run panel prints the resolved chain, and the commit panel and
|
|
178
|
+
final RESULT table name the CLI that handled each step (`—` when skipped).
|
|
179
|
+
When a CLI emits a supported, exact quota/rate-limit signal, aicp records the
|
|
180
|
+
outcome as `quota`, notifies through the usual notification path, and excludes
|
|
181
|
+
that CLI from the rest of that one commit/push flow. The exclusion also
|
|
182
|
+
persists: the CLI stays skipped for `AICP_QUOTA_COOLDOWN` seconds (default one
|
|
183
|
+
hour, state in `~/.aicp/quota.json`), because a token or rate-limit wall
|
|
184
|
+
normally stands for hours and every run inside that window would otherwise burn
|
|
185
|
+
a full budget per step on a CLI that cannot succeed. A skipped CLI prints how
|
|
186
|
+
long is left; `AICP_QUOTA_COOLDOWN=0` switches the cooldown off entirely, and
|
|
187
|
+
deleting the file clears it.
|
|
188
|
+
|
|
189
|
+
Only the `quota` outcome starts a cooldown. A timeout does not — a CLI that
|
|
190
|
+
hangs on its rate limit instead of exiting is indistinguishable from one merely
|
|
191
|
+
running long, and sidelining it for an hour on that guess costs more than the
|
|
192
|
+
retry does.
|
|
193
|
+
|
|
194
|
+
Exact detection is intentionally narrow. It is supported for Codex, Claude,
|
|
195
|
+
Vibe, and Grok only; Copilot, `agy` and any agent you add yourself have no
|
|
196
|
+
verified quota signature, so a nonzero exit from those remains an ordinary
|
|
197
|
+
failure and is still eligible for the next step.
|
|
198
|
+
|
|
144
199
|
### `--undo`
|
|
145
200
|
|
|
146
201
|
```sh
|
|
@@ -223,7 +278,7 @@ integration tests and installed-wheel checks on all three operating systems.
|
|
|
223
278
|
| `config_dir` | Absolute path or `~/`-relative directory; home is resolved when used. |
|
|
224
279
|
| `config_dir_env` | Optional environment variable overriding that directory (currently `GROK_HOME`). |
|
|
225
280
|
| `skills_dir` | Relative directory inside `config_dir` where skills are installed. |
|
|
226
|
-
| `skills` |
|
|
281
|
+
| `skills` | Which vendored skills to install for this agent — any of `safe-git-push`, `commit`. Omitting `commit` is how `claude` keeps its own `commands/commit.md`. |
|
|
227
282
|
| `memory_file` | Instruction filename used when adapting vendored skill text. Project directory references use the basename of `config_dir`. |
|
|
228
283
|
| `args` | Argument array containing exactly one standalone `{prompt}`, replaced with the complete prompt as one argument. No shell evaluation. |
|
|
229
284
|
|
|
@@ -351,7 +406,7 @@ See [`config.example.json`](config.example.json) for a ready-to-copy template.
|
|
|
351
406
|
| `AICP_DO_COMMIT` | `1` | Run the `/commit` step. `0` = only push what's already committed. |
|
|
352
407
|
| `AICP_DO_PUSH` | `1` | Run the `/safe-git-push` step. `0` = commit and stop. |
|
|
353
408
|
| `AICP_LANG` | `en` | Message language: `en` or `zh-TW`, everywhere including notifications. |
|
|
354
|
-
| `AICP_CLI_ORDER` | `copilot agy codex claude vibe grok` | Fallback order. A prefix is enough — any
|
|
409
|
+
| `AICP_CLI_ORDER` | registry order (`copilot agy codex claude vibe grok` out of the box) | Fallback order, space-separated. A prefix is enough — any agent left out is appended after it, in registry order, so adding an agent never invalidates an order you already saved. An unknown or repeated name is refused outright and the default order is used. |
|
|
355
410
|
| `AICP_TZ` | `Asia/Taipei` | IANA zone name used to render commit timestamps. Anything else falls back to the default. |
|
|
356
411
|
| `AICP_TZ_LABEL` | `UTC+8` | Cosmetic label shown beside those timestamps; not validated. |
|
|
357
412
|
| `AICP_STEP_TIMEOUT` | *(unset)* | Pins every CLI's per-step budget in seconds, skipping the formula and history below entirely. |
|
|
@@ -364,16 +419,22 @@ See [`config.example.json`](config.example.json) for a ready-to-copy template.
|
|
|
364
419
|
| `AICP_QUOTA_COOLDOWN` | `3600` | Seconds a CLI that reported a quota/rate-limit signal stays skipped, across runs (state in `~/.aicp/quota.json`). `0` switches the feature off; anything not a plain integer in 1..604800 falls back to the default. |
|
|
365
420
|
| `AICP_SKIP_SECRET_SCAN` | *(unset)* | `1` bypasses the pre-commit secret scan for one run — the documented escape for a false positive. |
|
|
366
421
|
| `AICP_CONFIG` | `~/.aicp/config.json` | Which file this loader reads. Environment-variable only — a file can't rename itself. |
|
|
367
|
-
| `AICP_TG_SEND` | `~/.claude/scripts/tg-send.sh` | The Telegram send script run at the end of a notification. **Environment-variable only** — refused if set in `config.json`. |
|
|
368
422
|
| `AICP_TIMING_LOG` | `~/.aicp/timing.log` | Where per-CLI timing rows are appended (rotated at 5 MB, 5 kept). **Environment-variable only** — refused if set in `config.json`. |
|
|
369
|
-
|
|
370
|
-
`
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
423
|
+
| `TG_BOT_TOKEN` | *(unset)* | Telegram bot token used to send the end-of-run notification — sent directly to the Bot API via stdlib HTTP (`aicp.telegram_notify`), no external script or project required. Same name `~/.claude/scripts/tg-send.sh` already uses, so an existing setup carries over. Not part of the `AICP_*` config system at all (see below), so it can only ever be a real environment variable. |
|
|
424
|
+
| `TG_CHAT_ID` | *(unset)* | Telegram chat to notify. Same rules as `TG_BOT_TOKEN`. |
|
|
425
|
+
|
|
426
|
+
Either `TG_BOT_TOKEN` or `TG_CHAT_ID` missing (or the request failing) just
|
|
427
|
+
means the notification prints to the terminal instead — nothing about the
|
|
428
|
+
commit/push run itself depends on it.
|
|
429
|
+
|
|
430
|
+
`AICP_TIMING_LOG` is refused from `config.json` on purpose: it names a path
|
|
431
|
+
that then gets *written and rotated* (`mkdir -p` / `>>` / a rename). A config
|
|
432
|
+
file is exactly the kind of thing that can arrive synced from someone else's
|
|
433
|
+
dotfiles repo, so anything that becomes a filesystem sink stays a
|
|
434
|
+
real-environment-only decision — set it in your shell, not the file.
|
|
435
|
+
(`TG_BOT_TOKEN`/`TG_CHAT_ID` don't need this treatment: they aren't
|
|
436
|
+
`AICP_`-prefixed, so `config.json` was never able to supply them to begin
|
|
437
|
+
with.)
|
|
377
438
|
|
|
378
439
|
## Safety
|
|
379
440
|
|
|
@@ -2,11 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
AI commit + push, with a git-verified result summary.
|
|
4
4
|
|
|
5
|
-
`aicp` runs an AI coding CLI to write your commit message(s) and push,
|
|
6
|
-
a fallback chain
|
|
7
|
-
`vibe` → `grok` — until one exits 0; anything not installed is skipped. It
|
|
5
|
+
`aicp` runs an AI coding CLI to write your commit message(s) and push, walking
|
|
6
|
+
a fallback chain until one exits 0; anything not installed is skipped. It
|
|
8
7
|
sends that CLI two literal prompts, `/commit` then `/safe-git-push`.
|
|
9
8
|
|
|
9
|
+
**The chain is yours to arrange, and it isn't a fixed set.** The built-in
|
|
10
|
+
registry ships six CLIs — `copilot`, `agy`, `codex`, `claude`, `vibe`,
|
|
11
|
+
`grok` — but that's only the default *order*, not a hardcoded pipeline. Move
|
|
12
|
+
any one of them to the front with `aicp --swap-ai`, set the whole order from
|
|
13
|
+
`aicp --config` or `AICP_CLI_ORDER`, and add your own CLIs (or drop built-in
|
|
14
|
+
ones) with `aicp --agents`. Every agent in the resolved registry is a
|
|
15
|
+
first-class link in the chain — there is no privileged first CLI and no cap
|
|
16
|
+
on how many you run with.
|
|
17
|
+
|
|
10
18
|
The part that matters: **aicp never trusts the CLI's own account of what
|
|
11
19
|
happened.** An AI CLI can print "pushed!" and exit 0 while `/safe-git-push`
|
|
12
20
|
quietly aborted inside it. So every number in the result summary — new
|
|
@@ -16,17 +24,36 @@ failed `git fetch` is never read as "already in sync" either: a stale
|
|
|
16
24
|
remote-tracking ref resolves just fine and would otherwise report a clean
|
|
17
25
|
push that never reached the remote.
|
|
18
26
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
27
|
+
## Contents
|
|
28
|
+
|
|
29
|
+
**Getting started**
|
|
30
|
+
|
|
31
|
+
- [Requirements](#requirements)
|
|
32
|
+
- [Install](#install)
|
|
33
|
+
- [Set up skills](#set-up-skills)
|
|
34
|
+
- [Run it](#run-it)
|
|
35
|
+
|
|
36
|
+
**Using it**
|
|
37
|
+
|
|
38
|
+
- [Fallback order](#fallback-order) — reorder the chain, `--swap-ai`
|
|
39
|
+
- [Fallback results and quota limits](#fallback-results-and-quota-limits)
|
|
40
|
+
- [`--undo`](#--undo) — take back the last commit
|
|
41
|
+
- [Automation / CI](#automation--ci) — the non-interactive flags
|
|
42
|
+
|
|
43
|
+
**Reference**
|
|
44
|
+
|
|
45
|
+
- [Configuration](#configuration) — every setting and env var
|
|
46
|
+
- [Custom agents](#custom-agents) — add your own AI CLI
|
|
47
|
+
- [Safety](#safety) — the pre-commit secret scan
|
|
48
|
+
- [Platform support](#platform-support) — macOS, Linux, Windows
|
|
23
49
|
|
|
24
50
|
## Requirements
|
|
25
51
|
|
|
26
52
|
- Python 3.10 or newer
|
|
27
53
|
- [`uv`](https://docs.astral.sh/uv/) — the install path below
|
|
28
54
|
- `git`
|
|
29
|
-
- At least one
|
|
55
|
+
- At least one AI CLI from the chain on `PATH` (a built-in one, or your own
|
|
56
|
+
via [Custom agents](#custom-agents))
|
|
30
57
|
|
|
31
58
|
The Python package itself has no runtime dependencies.
|
|
32
59
|
|
|
@@ -67,44 +94,26 @@ Targets follow each CLI's own config directory, not its binary name — `agy`
|
|
|
67
94
|
| CLI | Config dir | `/commit` installed? | `/safe-git-push` installed? |
|
|
68
95
|
| --- | --- | --- | --- |
|
|
69
96
|
| `claude` | `~/.claude` | No — keeps your existing `commands/commit.md` | Yes |
|
|
70
|
-
| `codex` | `~/.codex` |
|
|
71
|
-
| `copilot` | `~/.copilot` |
|
|
72
|
-
| `agy` (Gemini CLI) | `~/.gemini` |
|
|
73
|
-
| `vibe` | `~/.vibe` |
|
|
74
|
-
| `grok` | `$GROK_HOME` when set, otherwise `~/.grok` |
|
|
75
|
-
|
|
76
|
-
`/commit` is
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
97
|
+
| `codex` | `~/.codex` | Yes | Yes |
|
|
98
|
+
| `copilot` | `~/.copilot` | Yes | Yes |
|
|
99
|
+
| `agy` (Gemini CLI) | `~/.gemini` | Yes | Yes |
|
|
100
|
+
| `vibe` | `~/.vibe` | Yes | Yes |
|
|
101
|
+
| `grok` | `$GROK_HOME` when set, otherwise `~/.grok` | Yes | Yes |
|
|
102
|
+
|
|
103
|
+
Every configured CLI gets `/safe-git-push`. `/commit` is installed everywhere
|
|
104
|
+
*except* `claude`, which already resolves `/commit` from
|
|
105
|
+
`commands/commit.md` — a second definition under `skills/` would compete with
|
|
106
|
+
it. Which skills an agent gets is data, not a branch: it's the `skills` field
|
|
107
|
+
of that agent's registry entry, so a [custom agent](#custom-agents) declares
|
|
108
|
+
its own list the same way.
|
|
109
|
+
|
|
110
|
+
The vendored text is rewritten per CLI on install — the shipped copy talks
|
|
111
|
+
about `.claude/` and `CLAUDE.md`, which mean nothing to `codex` or `agy`, so
|
|
112
|
+
each install substitutes that agent's own `config_dir` name and `memory_file`.
|
|
80
113
|
|
|
81
114
|
A CLI whose config directory doesn't exist at all is skipped, never created —
|
|
82
115
|
aicp only ever installs into a CLI you've actually set up.
|
|
83
116
|
|
|
84
|
-
### Fallback results and quota limits
|
|
85
|
-
|
|
86
|
-
The opening run panel prints the resolved chain, and the commit panel and
|
|
87
|
-
final RESULT table name the CLI that handled each step (`—` when skipped).
|
|
88
|
-
When a CLI emits a supported, exact quota/rate-limit signal, aicp records the
|
|
89
|
-
outcome as `quota`, notifies through the usual notification path, and excludes
|
|
90
|
-
that CLI from the rest of that one commit/push flow. The exclusion also
|
|
91
|
-
persists: the CLI stays skipped for `AICP_QUOTA_COOLDOWN` seconds (default one
|
|
92
|
-
hour, state in `~/.aicp/quota.json`), because a token or rate-limit wall
|
|
93
|
-
normally stands for hours and every run inside that window would otherwise burn
|
|
94
|
-
a full budget per step on a CLI that cannot succeed. A skipped CLI prints how
|
|
95
|
-
long is left; `AICP_QUOTA_COOLDOWN=0` switches the cooldown off entirely, and
|
|
96
|
-
deleting the file clears it.
|
|
97
|
-
|
|
98
|
-
Only the `quota` outcome starts a cooldown. A timeout does not — a CLI that
|
|
99
|
-
hangs on its rate limit instead of exiting is indistinguishable from one merely
|
|
100
|
-
running long, and sidelining it for an hour on that guess costs more than the
|
|
101
|
-
retry does.
|
|
102
|
-
|
|
103
|
-
Exact detection is intentionally narrow. It is supported for Codex, Claude,
|
|
104
|
-
Vibe, and Grok only; Copilot and `agy` have no verified quota signature, so a
|
|
105
|
-
nonzero exit from either remains an ordinary failure and is still eligible for
|
|
106
|
-
the next step.
|
|
107
|
-
|
|
108
117
|
## Run it
|
|
109
118
|
|
|
110
119
|
The whole command surface is two things to remember:
|
|
@@ -129,6 +138,52 @@ same as `aicp --config`.
|
|
|
129
138
|
right now": which steps run, message language, fallback CLI order, the
|
|
130
139
|
skills install/upgrade view, and a health check.
|
|
131
140
|
|
|
141
|
+
### Fallback order
|
|
142
|
+
|
|
143
|
+
The order is a saved preference, not a property of the tool. Three ways to
|
|
144
|
+
set it, all writing the same `aicp_cli_order` key:
|
|
145
|
+
|
|
146
|
+
```sh
|
|
147
|
+
aicp --swap-ai # pick a CLI, it trades places with whoever holds #1
|
|
148
|
+
aicp --config # the AI CLI order row, arrow keys
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
```sh
|
|
152
|
+
# ~/.aicp/config.json — or AICP_CLI_ORDER="claude codex" aicp for one run
|
|
153
|
+
{ "aicp_cli_order": "claude codex copilot agy vibe grok" }
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`--swap-ai` is a rotation, not a destructive set: the CLI you pick moves to
|
|
157
|
+
`#1` and the old `#1` takes its place, so nothing falls out of the chain. It
|
|
158
|
+
lists every agent in the registry, including ones whose binary isn't on
|
|
159
|
+
`PATH` — a missing binary must not hide a choice you're about to install.
|
|
160
|
+
Naming only the first few is enough; everything you leave out is appended
|
|
161
|
+
behind them in registry order.
|
|
162
|
+
|
|
163
|
+
### Fallback results and quota limits
|
|
164
|
+
|
|
165
|
+
The opening run panel prints the resolved chain, and the commit panel and
|
|
166
|
+
final RESULT table name the CLI that handled each step (`—` when skipped).
|
|
167
|
+
When a CLI emits a supported, exact quota/rate-limit signal, aicp records the
|
|
168
|
+
outcome as `quota`, notifies through the usual notification path, and excludes
|
|
169
|
+
that CLI from the rest of that one commit/push flow. The exclusion also
|
|
170
|
+
persists: the CLI stays skipped for `AICP_QUOTA_COOLDOWN` seconds (default one
|
|
171
|
+
hour, state in `~/.aicp/quota.json`), because a token or rate-limit wall
|
|
172
|
+
normally stands for hours and every run inside that window would otherwise burn
|
|
173
|
+
a full budget per step on a CLI that cannot succeed. A skipped CLI prints how
|
|
174
|
+
long is left; `AICP_QUOTA_COOLDOWN=0` switches the cooldown off entirely, and
|
|
175
|
+
deleting the file clears it.
|
|
176
|
+
|
|
177
|
+
Only the `quota` outcome starts a cooldown. A timeout does not — a CLI that
|
|
178
|
+
hangs on its rate limit instead of exiting is indistinguishable from one merely
|
|
179
|
+
running long, and sidelining it for an hour on that guess costs more than the
|
|
180
|
+
retry does.
|
|
181
|
+
|
|
182
|
+
Exact detection is intentionally narrow. It is supported for Codex, Claude,
|
|
183
|
+
Vibe, and Grok only; Copilot, `agy` and any agent you add yourself have no
|
|
184
|
+
verified quota signature, so a nonzero exit from those remains an ordinary
|
|
185
|
+
failure and is still eligible for the next step.
|
|
186
|
+
|
|
132
187
|
### `--undo`
|
|
133
188
|
|
|
134
189
|
```sh
|
|
@@ -211,7 +266,7 @@ integration tests and installed-wheel checks on all three operating systems.
|
|
|
211
266
|
| `config_dir` | Absolute path or `~/`-relative directory; home is resolved when used. |
|
|
212
267
|
| `config_dir_env` | Optional environment variable overriding that directory (currently `GROK_HOME`). |
|
|
213
268
|
| `skills_dir` | Relative directory inside `config_dir` where skills are installed. |
|
|
214
|
-
| `skills` |
|
|
269
|
+
| `skills` | Which vendored skills to install for this agent — any of `safe-git-push`, `commit`. Omitting `commit` is how `claude` keeps its own `commands/commit.md`. |
|
|
215
270
|
| `memory_file` | Instruction filename used when adapting vendored skill text. Project directory references use the basename of `config_dir`. |
|
|
216
271
|
| `args` | Argument array containing exactly one standalone `{prompt}`, replaced with the complete prompt as one argument. No shell evaluation. |
|
|
217
272
|
|
|
@@ -339,7 +394,7 @@ See [`config.example.json`](config.example.json) for a ready-to-copy template.
|
|
|
339
394
|
| `AICP_DO_COMMIT` | `1` | Run the `/commit` step. `0` = only push what's already committed. |
|
|
340
395
|
| `AICP_DO_PUSH` | `1` | Run the `/safe-git-push` step. `0` = commit and stop. |
|
|
341
396
|
| `AICP_LANG` | `en` | Message language: `en` or `zh-TW`, everywhere including notifications. |
|
|
342
|
-
| `AICP_CLI_ORDER` | `copilot agy codex claude vibe grok` | Fallback order. A prefix is enough — any
|
|
397
|
+
| `AICP_CLI_ORDER` | registry order (`copilot agy codex claude vibe grok` out of the box) | Fallback order, space-separated. A prefix is enough — any agent left out is appended after it, in registry order, so adding an agent never invalidates an order you already saved. An unknown or repeated name is refused outright and the default order is used. |
|
|
343
398
|
| `AICP_TZ` | `Asia/Taipei` | IANA zone name used to render commit timestamps. Anything else falls back to the default. |
|
|
344
399
|
| `AICP_TZ_LABEL` | `UTC+8` | Cosmetic label shown beside those timestamps; not validated. |
|
|
345
400
|
| `AICP_STEP_TIMEOUT` | *(unset)* | Pins every CLI's per-step budget in seconds, skipping the formula and history below entirely. |
|
|
@@ -352,16 +407,22 @@ See [`config.example.json`](config.example.json) for a ready-to-copy template.
|
|
|
352
407
|
| `AICP_QUOTA_COOLDOWN` | `3600` | Seconds a CLI that reported a quota/rate-limit signal stays skipped, across runs (state in `~/.aicp/quota.json`). `0` switches the feature off; anything not a plain integer in 1..604800 falls back to the default. |
|
|
353
408
|
| `AICP_SKIP_SECRET_SCAN` | *(unset)* | `1` bypasses the pre-commit secret scan for one run — the documented escape for a false positive. |
|
|
354
409
|
| `AICP_CONFIG` | `~/.aicp/config.json` | Which file this loader reads. Environment-variable only — a file can't rename itself. |
|
|
355
|
-
| `AICP_TG_SEND` | `~/.claude/scripts/tg-send.sh` | The Telegram send script run at the end of a notification. **Environment-variable only** — refused if set in `config.json`. |
|
|
356
410
|
| `AICP_TIMING_LOG` | `~/.aicp/timing.log` | Where per-CLI timing rows are appended (rotated at 5 MB, 5 kept). **Environment-variable only** — refused if set in `config.json`. |
|
|
357
|
-
|
|
358
|
-
`
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
411
|
+
| `TG_BOT_TOKEN` | *(unset)* | Telegram bot token used to send the end-of-run notification — sent directly to the Bot API via stdlib HTTP (`aicp.telegram_notify`), no external script or project required. Same name `~/.claude/scripts/tg-send.sh` already uses, so an existing setup carries over. Not part of the `AICP_*` config system at all (see below), so it can only ever be a real environment variable. |
|
|
412
|
+
| `TG_CHAT_ID` | *(unset)* | Telegram chat to notify. Same rules as `TG_BOT_TOKEN`. |
|
|
413
|
+
|
|
414
|
+
Either `TG_BOT_TOKEN` or `TG_CHAT_ID` missing (or the request failing) just
|
|
415
|
+
means the notification prints to the terminal instead — nothing about the
|
|
416
|
+
commit/push run itself depends on it.
|
|
417
|
+
|
|
418
|
+
`AICP_TIMING_LOG` is refused from `config.json` on purpose: it names a path
|
|
419
|
+
that then gets *written and rotated* (`mkdir -p` / `>>` / a rename). A config
|
|
420
|
+
file is exactly the kind of thing that can arrive synced from someone else's
|
|
421
|
+
dotfiles repo, so anything that becomes a filesystem sink stays a
|
|
422
|
+
real-environment-only decision — set it in your shell, not the file.
|
|
423
|
+
(`TG_BOT_TOKEN`/`TG_CHAT_ID` don't need this treatment: they aren't
|
|
424
|
+
`AICP_`-prefixed, so `config.json` was never able to supply them to begin
|
|
425
|
+
with.)
|
|
365
426
|
|
|
366
427
|
## Safety
|
|
367
428
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# PyPI distribution name only — the `aicp` name was taken in 2021 by an
|
|
3
3
|
# unrelated project. The command, import path and repo stay `aicp`.
|
|
4
4
|
name = "aicp-cli"
|
|
5
|
-
version = "0.
|
|
5
|
+
version = "0.9.0"
|
|
6
6
|
description = "AI commit + push, with a git-verified result summary"
|
|
7
7
|
readme = "README.md"
|
|
8
8
|
requires-python = ">=3.10"
|
|
@@ -91,12 +91,11 @@ def export_settings(settings: config.Settings) -> None:
|
|
|
91
91
|
|
|
92
92
|
Safe by construction with respect to :data:`aicp.config.DENYLIST`:
|
|
93
93
|
``settings.values`` is what :func:`aicp.config.load_config` accepted, and
|
|
94
|
-
that loader drops every denied key — ``
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
the
|
|
99
|
-
to ``DENYLIST``, not to a filter here.
|
|
94
|
+
that loader drops every denied key — ``AICP_TIMING_LOG`` (path-mutating
|
|
95
|
+
sinks) and ``AICP_CONFIG`` (would re-point the next ``persist_key``
|
|
96
|
+
write) — before they ever reach here. So a file can never smuggle a
|
|
97
|
+
path-mutation knob into the environment through this door. Whoever adds
|
|
98
|
+
the next such knob adds it to ``DENYLIST``, not to a filter here.
|
|
100
99
|
|
|
101
100
|
Two things this deliberately does NOT close, both stated rather than left
|
|
102
101
|
to be discovered:
|
|
@@ -305,9 +304,17 @@ def _doctor(*, as_json: bool) -> int:
|
|
|
305
304
|
|
|
306
305
|
|
|
307
306
|
def _agents_list() -> int:
|
|
307
|
+
inventory = agents.inventory()
|
|
308
|
+
exec_w = max((present.width(row.executable) for row in inventory), default=0)
|
|
308
309
|
rows = [
|
|
309
|
-
(
|
|
310
|
-
|
|
310
|
+
(
|
|
311
|
+
row.name,
|
|
312
|
+
(
|
|
313
|
+
f"{row.executable}{' ' * (exec_w - present.width(row.executable))}"
|
|
314
|
+
f" {DIM}· {t(*agentcfg.STATE_LABELS[row.state])}{RESET}"
|
|
315
|
+
),
|
|
316
|
+
)
|
|
317
|
+
for row in inventory
|
|
311
318
|
]
|
|
312
319
|
_echo(present.render(rows, t("agents_title", "AGENTS")))
|
|
313
320
|
_dim(t("agents_hint", " aicp --agents set <name> executable=… · disable <name> · reset <name>"))
|
|
@@ -29,22 +29,23 @@ later used AS a command or a path. Hence three more layers:
|
|
|
29
29
|
a key is written ``aicp_do_commit``, matching the environment's
|
|
30
30
|
``AICP_DO_COMMIT`` only once case-folded on read — see
|
|
31
31
|
:func:`_read_json_object`). A line naming ``PATH`` is inert text to this
|
|
32
|
-
loader, not an assignment.
|
|
32
|
+
loader, not an assignment. (Telegram credentials, ``TG_BOT_TOKEN`` and
|
|
33
|
+
``TG_CHAT_ID``, aren't ``AICP_``-prefixed at all, so they never reach this
|
|
34
|
+
loader in the first place — see :mod:`aicp.notify`.)
|
|
33
35
|
2. **Value charset allowlist** — letters, digits and
|
|
34
36
|
``/ . _ : @ + -`` plus whitespace. ``=`` and ``,`` are excluded because in
|
|
35
37
|
the zsh original a value like ``PATH=0`` reaching an arithmetic context
|
|
36
38
|
would assign into the real ``$PATH``; ``$``/backtick/``;``/``|``/parens
|
|
37
39
|
are excluded as defence in depth for whatever the next consumer of these
|
|
38
40
|
values does with them.
|
|
39
|
-
3. **Denylist** (:data:`DENYLIST`) — ``
|
|
40
|
-
``
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
this class at all.
|
|
41
|
+
3. **Denylist** (:data:`DENYLIST`) — ``AICP_TIMING_LOG`` (reaches
|
|
42
|
+
``mkdir -p``, ``>>``, ``mv -f``, ``rm -f``) and ``AICP_CONFIG`` (names the
|
|
43
|
+
file this loader reads, and the file :func:`persist_key` then
|
|
44
|
+
``mkdir -p``s and atomically replaces) are ENVIRONMENT-VARIABLE ONLY. Both
|
|
45
|
+
are plain literal paths that sail through the charset allowlist, and both
|
|
46
|
+
were real holes. Whoever adds the next knob that flows into an exec path
|
|
47
|
+
or a path-mutating sink adds its name here — the charset allowlist does
|
|
48
|
+
not protect against this class at all.
|
|
48
49
|
|
|
49
50
|
``AICP_CONFIG`` is the subtlest of the three, because a file naming
|
|
50
51
|
*itself* looks inert: nothing in this module acts on the value. The zsh
|
|
@@ -107,7 +108,7 @@ __all__ = [
|
|
|
107
108
|
#: synced from someone else's dotfiles repo. ``AICP_CONFIG`` belongs here for
|
|
108
109
|
#: the reason spelled out in this module's docstring — a file must not be able
|
|
109
110
|
#: to rename the file the next write lands on.
|
|
110
|
-
DENYLIST = frozenset({"
|
|
111
|
+
DENYLIST = frozenset({"AICP_TIMING_LOG", "AICP_CONFIG"})
|
|
111
112
|
|
|
112
113
|
#: Knobs resolved from the system, never from configuration — see
|
|
113
114
|
#: :func:`timeout_bin`. Kept as a set so the reason is greppable from both
|