aicp-cli 0.3.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.3.0/.github/workflows/ci.yml +141 -0
- aicp_cli-0.3.0/.github/workflows/release.yml +67 -0
- aicp_cli-0.3.0/.gitignore +12 -0
- aicp_cli-0.3.0/CHANGELOG.md +99 -0
- aicp_cli-0.3.0/LICENSE +20 -0
- aicp_cli-0.3.0/PKG-INFO +298 -0
- aicp_cli-0.3.0/README.md +286 -0
- aicp_cli-0.3.0/TODO.md +39 -0
- aicp_cli-0.3.0/config.example.json +14 -0
- aicp_cli-0.3.0/docs/images/aicp-run.png +0 -0
- aicp_cli-0.3.0/pyproject.toml +45 -0
- aicp_cli-0.3.0/skills/commit.md +90 -0
- aicp_cli-0.3.0/skills/safe-git-push/SKILL.md +55 -0
- aicp_cli-0.3.0/skills/safe-git-push/scripts/safe_push.py +248 -0
- aicp_cli-0.3.0/src/aicp/__init__.py +18 -0
- aicp_cli-0.3.0/src/aicp/_keyreader.py +210 -0
- aicp_cli-0.3.0/src/aicp/_utils.py +183 -0
- aicp_cli-0.3.0/src/aicp/budget.py +158 -0
- aicp_cli-0.3.0/src/aicp/cli.py +595 -0
- aicp_cli-0.3.0/src/aicp/config.py +434 -0
- aicp_cli-0.3.0/src/aicp/contracts.py +144 -0
- aicp_cli-0.3.0/src/aicp/gitflow.py +456 -0
- aicp_cli-0.3.0/src/aicp/i18n.py +228 -0
- aicp_cli-0.3.0/src/aicp/menu.py +1013 -0
- aicp_cli-0.3.0/src/aicp/notify.py +87 -0
- aicp_cli-0.3.0/src/aicp/present.py +303 -0
- aicp_cli-0.3.0/src/aicp/quota.py +115 -0
- aicp_cli-0.3.0/src/aicp/runner.py +560 -0
- aicp_cli-0.3.0/src/aicp/secrets.py +291 -0
- aicp_cli-0.3.0/src/aicp/skills.py +679 -0
- aicp_cli-0.3.0/src/aicp/timing.py +116 -0
- aicp_cli-0.3.0/tests/conftest.py +222 -0
- aicp_cli-0.3.0/tests/test_budget.py +269 -0
- aicp_cli-0.3.0/tests/test_cli.py +709 -0
- aicp_cli-0.3.0/tests/test_config.py +581 -0
- aicp_cli-0.3.0/tests/test_cross_platform.py +122 -0
- aicp_cli-0.3.0/tests/test_gitflow.py +320 -0
- aicp_cli-0.3.0/tests/test_i18n.py +253 -0
- aicp_cli-0.3.0/tests/test_menu.py +457 -0
- aicp_cli-0.3.0/tests/test_menu_skills.py +217 -0
- aicp_cli-0.3.0/tests/test_notify.py +165 -0
- aicp_cli-0.3.0/tests/test_present.py +129 -0
- aicp_cli-0.3.0/tests/test_quota.py +149 -0
- aicp_cli-0.3.0/tests/test_runner.py +952 -0
- aicp_cli-0.3.0/tests/test_secrets.py +295 -0
- aicp_cli-0.3.0/tests/test_skills.py +851 -0
- aicp_cli-0.3.0/tests/test_timing.py +159 -0
- aicp_cli-0.3.0/tests/test_undo.py +174 -0
- aicp_cli-0.3.0/uv.lock +215 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
concurrency:
|
|
11
|
+
group: ci-${{ github.workflow }}-${{ github.ref }}
|
|
12
|
+
cancel-in-progress: true
|
|
13
|
+
|
|
14
|
+
env:
|
|
15
|
+
PYTHONUTF8: "1"
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
test:
|
|
19
|
+
name: Python 3.10 / ${{ matrix.os }}
|
|
20
|
+
timeout-minutes: 15
|
|
21
|
+
strategy:
|
|
22
|
+
fail-fast: false
|
|
23
|
+
matrix:
|
|
24
|
+
os: [macos-latest, ubuntu-latest, windows-latest]
|
|
25
|
+
runs-on: ${{ matrix.os }}
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
|
|
28
|
+
with:
|
|
29
|
+
persist-credentials: false
|
|
30
|
+
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
|
31
|
+
with:
|
|
32
|
+
python-version: "3.10"
|
|
33
|
+
enable-cache: true
|
|
34
|
+
cache-dependency-glob: uv.lock
|
|
35
|
+
- name: Install locked dependencies
|
|
36
|
+
run: uv sync --locked --all-groups
|
|
37
|
+
- name: Lint
|
|
38
|
+
run: uv run ruff check .
|
|
39
|
+
- name: Test
|
|
40
|
+
run: uv run pytest
|
|
41
|
+
- name: Build distributions
|
|
42
|
+
run: uv build
|
|
43
|
+
- name: Validate wheel and console script
|
|
44
|
+
shell: python
|
|
45
|
+
run: |
|
|
46
|
+
import os
|
|
47
|
+
import pathlib
|
|
48
|
+
import re
|
|
49
|
+
import subprocess
|
|
50
|
+
import tempfile
|
|
51
|
+
|
|
52
|
+
wheel, = pathlib.Path("dist").glob("*.whl")
|
|
53
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
54
|
+
environment = pathlib.Path(directory)
|
|
55
|
+
subprocess.run(["uv", "venv", environment], check=True)
|
|
56
|
+
scripts = environment / ("Scripts" if os.name == "nt" else "bin")
|
|
57
|
+
python = scripts / ("python.exe" if os.name == "nt" else "python")
|
|
58
|
+
subprocess.run(
|
|
59
|
+
["uv", "pip", "install", "--python", python, "--no-index", wheel],
|
|
60
|
+
check=True,
|
|
61
|
+
)
|
|
62
|
+
suffix = ".exe" if os.name == "nt" else ""
|
|
63
|
+
subprocess.run([scripts / f"aicp{suffix}", "--help"], check=True)
|
|
64
|
+
|
|
65
|
+
# The distribution name (aicp-cli) and the import name (aicp)
|
|
66
|
+
# differ, so __init__'s importlib.metadata lookup can miss and
|
|
67
|
+
# fall back to "0+unknown" without failing anything. Compare the
|
|
68
|
+
# installed wheel's own report against pyproject.toml.
|
|
69
|
+
expected, = re.findall(
|
|
70
|
+
r'^version = "(.+)"$',
|
|
71
|
+
pathlib.Path("pyproject.toml").read_text(encoding="utf-8"),
|
|
72
|
+
re.MULTILINE,
|
|
73
|
+
)
|
|
74
|
+
reported = subprocess.run(
|
|
75
|
+
[scripts / f"aicp{suffix}", "--version"],
|
|
76
|
+
check=True,
|
|
77
|
+
capture_output=True,
|
|
78
|
+
text=True,
|
|
79
|
+
).stdout.strip()
|
|
80
|
+
assert reported == f"aicp {expected}", f"{reported!r} != 'aicp {expected}'"
|
|
81
|
+
print("version OK:", reported)
|
|
82
|
+
|
|
83
|
+
# The vendored skills must survive packaging. Without the
|
|
84
|
+
# force-include key in pyproject.toml the wheel ships no skill
|
|
85
|
+
# data, _vendor_dir() falls back to a checkout path that does not
|
|
86
|
+
# exist once installed, and installing skills silently finds
|
|
87
|
+
# nothing. Assert against the INSTALLED package, never the checkout.
|
|
88
|
+
subprocess.run(
|
|
89
|
+
[
|
|
90
|
+
python,
|
|
91
|
+
"-c",
|
|
92
|
+
"from aicp import skills;"
|
|
93
|
+
"d = skills.VENDOR_DIR;"
|
|
94
|
+
"assert d.is_dir(), f'no vendored skills in wheel: {d}';"
|
|
95
|
+
"assert (d / 'commit.md').is_file(), 'commit.md missing';"
|
|
96
|
+
"assert (d / 'safe-git-push' / 'SKILL.md').is_file(),"
|
|
97
|
+
" 'safe-git-push missing';"
|
|
98
|
+
"print('vendored skills OK:', d)",
|
|
99
|
+
],
|
|
100
|
+
check=True,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Failure-only pager. A green run sends nothing on purpose: a notification
|
|
104
|
+
# that arrives on every push is one nobody reads. Credentials live in repo
|
|
105
|
+
# secrets, never in this file — see README "CI notifications" for the two
|
|
106
|
+
# `gh secret set` commands that configure them.
|
|
107
|
+
notify-telegram:
|
|
108
|
+
needs: test
|
|
109
|
+
if: ${{ failure() && github.event_name == 'push' }}
|
|
110
|
+
runs-on: ubuntu-latest
|
|
111
|
+
timeout-minutes: 5
|
|
112
|
+
steps:
|
|
113
|
+
- name: Notify Telegram
|
|
114
|
+
env:
|
|
115
|
+
TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
|
116
|
+
CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
|
117
|
+
run: |
|
|
118
|
+
# Unconfigured secrets are a valid state (a fork, or a clone whose
|
|
119
|
+
# owner never set them up). Skip quietly instead of failing the job
|
|
120
|
+
# and reporting a second, misleading red X on top of the real one.
|
|
121
|
+
if [ -z "$TOKEN" ] || [ -z "$CHAT_ID" ]; then
|
|
122
|
+
echo "::notice::TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID not set — skipping notification"
|
|
123
|
+
exit 0
|
|
124
|
+
fi
|
|
125
|
+
# parse_mode=HTML: a branch or commit subject containing < & > would
|
|
126
|
+
# otherwise produce unparseable markup and Telegram would 400.
|
|
127
|
+
esc() { printf '%s' "$1" | sed -e 's/&/\&/g' -e 's/</\</g' -e 's/>/\>/g'; }
|
|
128
|
+
ci_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
|
|
129
|
+
message="🚨 <b>CI failed</b>
|
|
130
|
+
|
|
131
|
+
<b>Repository:</b> <code>$(esc "$GITHUB_REPOSITORY")</code>
|
|
132
|
+
<b>Branch:</b> <code>$(esc "$GITHUB_REF_NAME")</code>
|
|
133
|
+
<b>Commit:</b> <code>$(esc "${GITHUB_SHA::7}")</code>
|
|
134
|
+
|
|
135
|
+
<a href=\"$ci_url\">Open failed CI run</a>"
|
|
136
|
+
curl --fail --silent --show-error --max-time 30 \
|
|
137
|
+
--data-urlencode "chat_id=$CHAT_ID" \
|
|
138
|
+
--data-urlencode "parse_mode=HTML" \
|
|
139
|
+
--data-urlencode "disable_web_page_preview=true" \
|
|
140
|
+
--data-urlencode "text=$message" \
|
|
141
|
+
"https://api.telegram.org/bot$TOKEN/sendMessage"
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
id-token: write
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: release-${{ github.ref }}
|
|
14
|
+
|
|
15
|
+
env:
|
|
16
|
+
PYTHONUTF8: "1"
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
release:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
timeout-minutes: 15
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
|
|
24
|
+
with:
|
|
25
|
+
persist-credentials: false
|
|
26
|
+
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
|
27
|
+
with:
|
|
28
|
+
python-version: "3.10"
|
|
29
|
+
enable-cache: true
|
|
30
|
+
cache-dependency-glob: uv.lock
|
|
31
|
+
- name: Install locked dependencies
|
|
32
|
+
run: uv sync --locked --all-groups
|
|
33
|
+
- name: Check, test, and build
|
|
34
|
+
run: |
|
|
35
|
+
uv run ruff check .
|
|
36
|
+
uv run pytest
|
|
37
|
+
uv build
|
|
38
|
+
- name: Validate release wheel
|
|
39
|
+
run: |
|
|
40
|
+
uv venv .release-venv
|
|
41
|
+
uv pip install --python .release-venv/bin/python --no-index dist/*.whl
|
|
42
|
+
.release-venv/bin/aicp --help
|
|
43
|
+
# The wheel must report the version being tagged. This catches both a
|
|
44
|
+
# tag/pyproject mismatch and the silent `0+unknown` that appears when
|
|
45
|
+
# __init__ asks importlib.metadata for the wrong distribution name.
|
|
46
|
+
reported="$(.release-venv/bin/aicp --version)"
|
|
47
|
+
expected="aicp ${GITHUB_REF_NAME#v}"
|
|
48
|
+
if [ "$reported" != "$expected" ]; then
|
|
49
|
+
echo "::error::wheel reports '$reported', expected '$expected'"
|
|
50
|
+
exit 1
|
|
51
|
+
fi
|
|
52
|
+
- name: Publish to PyPI
|
|
53
|
+
run: uv publish --trusted-publishing always
|
|
54
|
+
|
|
55
|
+
# After publish: uv publish uploads dist/*, and SHA256SUMS is not a distribution.
|
|
56
|
+
- name: Create checksums
|
|
57
|
+
run: sha256sum dist/* > dist/SHA256SUMS
|
|
58
|
+
- name: Create GitHub release
|
|
59
|
+
env:
|
|
60
|
+
GH_TOKEN: ${{ github.token }}
|
|
61
|
+
run: |
|
|
62
|
+
gh api "repos/${GITHUB_REPOSITORY}/git/refs/tags/$GITHUB_REF_NAME" >/dev/null
|
|
63
|
+
if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then
|
|
64
|
+
gh release upload "$GITHUB_REF_NAME" dist/* --clobber
|
|
65
|
+
else
|
|
66
|
+
gh release create "$GITHUB_REF_NAME" dist/* --generate-notes --title "$GITHUB_REF_NAME" --verify-tag
|
|
67
|
+
fi
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
## [0.3.0] - 2026-09-14
|
|
2
|
+
|
|
3
|
+
### 🐛 Bug Fixes
|
|
4
|
+
|
|
5
|
+
- **version:** Derive __version__ from package metadata
|
|
6
|
+
|
|
7
|
+
### 💼 Other
|
|
8
|
+
|
|
9
|
+
- **pypi:** [**breaking**] Publish as aicp-cli
|
|
10
|
+
|
|
11
|
+
### 📚 Documentation
|
|
12
|
+
|
|
13
|
+
- **readme:** Show install commands for pinning and upgrading
|
|
14
|
+
|
|
15
|
+
### ⚙️ Miscellaneous Tasks
|
|
16
|
+
|
|
17
|
+
- **release:** Publish wheels to PyPI via trusted publishing
|
|
18
|
+
- Assert the built wheel reports the right version
|
|
19
|
+
## [0.2.1] - 2026-09-13
|
|
20
|
+
|
|
21
|
+
### ⚙️ Miscellaneous Tasks
|
|
22
|
+
|
|
23
|
+
- Refresh uv lockfile for v0.2.0
|
|
24
|
+
## [0.2.0] - 2026-09-13
|
|
25
|
+
|
|
26
|
+
### 🚀 Features
|
|
27
|
+
|
|
28
|
+
- **cli:** Wire the entry point and bridge config to its consumers
|
|
29
|
+
- **menu:** Add Skills and Doctor rows with inline status
|
|
30
|
+
- **i18n:** Fill the zh-TW catalogue and retire menu.py's _ZH bridge
|
|
31
|
+
- [**breaking**] Support grok and quota-aware fallback
|
|
32
|
+
- **menu:** Display full CLI chain in config panel
|
|
33
|
+
- **skills:** Track installed skills via hashed state file
|
|
34
|
+
- **config:** Migrate config store to ~/.aicp/config.json
|
|
35
|
+
- **menu:** Bold the cursor row in the config panel
|
|
36
|
+
- **runner:** Persist quota exclusion across runs
|
|
37
|
+
|
|
38
|
+
### 🐛 Bug Fixes
|
|
39
|
+
|
|
40
|
+
- **aicp:** Double a trailing backslash when quoting a batch-shim argument
|
|
41
|
+
- **config:** [**breaking**] Deny AICP_CONFIG from .aicprc
|
|
42
|
+
- **cli:** Let AICP_LANG reach the translator, and refuse orphan sub-flags
|
|
43
|
+
- **menu:** Stop the config panel smearing during CLI-order changes
|
|
44
|
+
- **skills:** Reject unsafe record keys before hashing
|
|
45
|
+
- **config:** Normalize key casing between JSON and env
|
|
46
|
+
- **present:** Style config menu group headings
|
|
47
|
+
- **present:** Make config group headings legible
|
|
48
|
+
|
|
49
|
+
### 📚 Documentation
|
|
50
|
+
|
|
51
|
+
- **todo:** Record the Windows timeout-orphan defect as a known issue
|
|
52
|
+
- Record grok and quota behavior
|
|
53
|
+
- Add demo screenshot to readme
|
|
54
|
+
- Note follow-up security review for skills state store
|
|
55
|
+
- Add aicp performance improvement todo item
|
|
56
|
+
|
|
57
|
+
### 🧪 Testing
|
|
58
|
+
|
|
59
|
+
- Add intentional import failures for testing
|
|
60
|
+
- **config:** Cover case-folded and denylisted keys
|
|
61
|
+
|
|
62
|
+
### ⚙️ Miscellaneous Tasks
|
|
63
|
+
|
|
64
|
+
- Page Telegram on a failed push
|
|
65
|
+
- Ignore local omc state
|
|
66
|
+
- **release:** Bump version to 0.2.0
|
|
67
|
+
## [0.1.0] - 2026-09-12
|
|
68
|
+
|
|
69
|
+
### 🚀 Features
|
|
70
|
+
|
|
71
|
+
- **aicp:** Scaffold Python port with frozen contracts and test harness
|
|
72
|
+
- **skills:** Add cross-platform skills installer
|
|
73
|
+
- **aicp:** Port the git flow, secret scanner, and notifier
|
|
74
|
+
- **aicp:** Port hardened .aicprc layer and the --config/--swap-ai menu
|
|
75
|
+
- **aicp:** Port the timing log with 5MB x 5-file rotation
|
|
76
|
+
- **aicp:** Port the per-CLI timeout budget with history widening
|
|
77
|
+
- **aicp:** Port the fallback-chain runner with signal-safe timeouts
|
|
78
|
+
|
|
79
|
+
### 🐛 Bug Fixes
|
|
80
|
+
|
|
81
|
+
- **aicp:** Use CTRL_BREAK_EVENT and fix Windows stub argv logging
|
|
82
|
+
- **skills:** Overwrite a stale .bak on re-forced install
|
|
83
|
+
- **skills:** Never let a backup overwrite an earlier backup
|
|
84
|
+
- **aicp:** Harden the budget and notifier against values that crash a run
|
|
85
|
+
- **packaging:** Ship vendored skills in the wheel
|
|
86
|
+
- **aicp:** Stop the runner leaking children and writing through a symlink
|
|
87
|
+
- **aicp:** Bound the notifier so a hung tg-send.sh cannot block a run
|
|
88
|
+
- **aicp:** Launch npm .cmd shims on Windows instead of failing to start
|
|
89
|
+
|
|
90
|
+
### 📚 Documentation
|
|
91
|
+
|
|
92
|
+
- **aicp:** Write README, changelog, and .aicprc.example for 0.1.0
|
|
93
|
+
|
|
94
|
+
### 🧪 Testing
|
|
95
|
+
|
|
96
|
+
- **aicp:** Count Path.stat, and exercise the non-TTY guard on a real fd
|
|
97
|
+
- **aicp:** Bound the unreaped-child tests on elapsed time
|
|
98
|
+
- **aicp:** Delete git's read-only objects when tearing down a bare remote
|
|
99
|
+
- **aicp:** Skip the argv-passthrough test on the platform it does not describe
|
aicp_cli-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Wes Kao
|
|
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 OF THE SOFTWARE.
|
aicp_cli-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: aicp-cli
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: AI commit + push, with a git-verified result summary
|
|
5
|
+
Project-URL: Repository, https://github.com/weskao/aicp
|
|
6
|
+
Project-URL: Issues, https://github.com/weskao/aicp/issues
|
|
7
|
+
Author: Wes Kao
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# aicp
|
|
14
|
+
|
|
15
|
+
AI commit + push, with a git-verified result summary.
|
|
16
|
+
|
|
17
|
+
`aicp` runs an AI coding CLI to write your commit message(s) and push, trying
|
|
18
|
+
a fallback chain of six CLIs — `copilot` → `agy` → `codex` → `claude` →
|
|
19
|
+
`vibe` → `grok` — until one exits 0; anything not installed is skipped. It
|
|
20
|
+
sends that CLI two literal prompts, `/commit` then `/safe-git-push`.
|
|
21
|
+
|
|
22
|
+
The part that matters: **aicp never trusts the CLI's own account of what
|
|
23
|
+
happened.** An AI CLI can print "pushed!" and exit 0 while `/safe-git-push`
|
|
24
|
+
quietly aborted inside it. So every number in the result summary — new
|
|
25
|
+
commits, ahead/behind, whether the branch is actually in sync — is read back
|
|
26
|
+
from `git` itself after the CLI is done, never taken from its output. A
|
|
27
|
+
failed `git fetch` is never read as "already in sync" either: a stale
|
|
28
|
+
remote-tracking ref resolves just fine and would otherwise report a clean
|
|
29
|
+
push that never reached the remote.
|
|
30
|
+
|
|
31
|
+
[Install](#install) · [Set up skills](#set-up-skills) · [Run it](#run-it) ·
|
|
32
|
+
[`--undo`](#--undo) · [Automation / CI](#automation--ci) ·
|
|
33
|
+
[Configuration](#configuration) · [Safety](#safety) ·
|
|
34
|
+
[Platform support](#platform-support)
|
|
35
|
+
|
|
36
|
+
## Requirements
|
|
37
|
+
|
|
38
|
+
- Python 3.10 or newer
|
|
39
|
+
- `git`
|
|
40
|
+
- At least one of the six AI CLIs above on `PATH`
|
|
41
|
+
|
|
42
|
+
The Python package itself has no runtime dependencies.
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
uv tool install aicp-cli # latest release
|
|
48
|
+
uv tool install aicp-cli==X.Y.Z # pin to a specific version
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The distribution is `aicp-cli`; the command it installs is `aicp`. (The plain
|
|
52
|
+
`aicp` name on PyPI belongs to an unrelated 2021 project — don't install it.)
|
|
53
|
+
Replace `X.Y.Z` with the release version you want; re-running either command
|
|
54
|
+
switches an existing install to that version.
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
uv tool upgrade aicp-cli
|
|
58
|
+
uv tool uninstall aicp-cli
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Set up skills
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
aicp --config # -> Skills
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`/commit` and `/safe-git-push` only mean something if a skill by that exact
|
|
68
|
+
name exists in the CLI's own config directory — otherwise the CLI receives a
|
|
69
|
+
slash command it has never heard of and improvises. aicp ships byte-identical
|
|
70
|
+
copies of both skills and installs them for you, but it never overwrites a
|
|
71
|
+
skill you already have: a file with no aicp version marker next to it is
|
|
72
|
+
treated as yours and left alone. If you already have a better `/commit` for
|
|
73
|
+
this repo, it stays.
|
|
74
|
+
|
|
75
|
+
Targets follow each CLI's own config directory, not its binary name — `agy`
|
|
76
|
+
(this project's name for the Gemini CLI) reads `~/.gemini`, not `~/.agy`:
|
|
77
|
+
|
|
78
|
+
| CLI | Config dir | `/commit` installed? | `/safe-git-push` installed? |
|
|
79
|
+
| --- | --- | --- | --- |
|
|
80
|
+
| `claude` | `~/.claude` | No — keeps your existing `commands/commit.md` | Yes |
|
|
81
|
+
| `codex` | `~/.codex` | Yes | Yes |
|
|
82
|
+
| `copilot` | `~/.copilot` | Yes | Yes |
|
|
83
|
+
| `agy` (Gemini CLI) | `~/.gemini` | Yes | Yes |
|
|
84
|
+
| `vibe` | `~/.vibe` | Yes | Yes |
|
|
85
|
+
| `grok` | `$GROK_HOME` when set, otherwise `~/.grok` | Yes | Yes |
|
|
86
|
+
|
|
87
|
+
`claude` is the one exception: it already resolves `/commit` from its own
|
|
88
|
+
`commands/commit.md`, so aicp never installs a competing definition under
|
|
89
|
+
`skills/` there — installing one would just shadow the one Claude already
|
|
90
|
+
uses. Every other CLI gets both skills.
|
|
91
|
+
|
|
92
|
+
A CLI whose config directory doesn't exist at all is skipped, never created —
|
|
93
|
+
aicp only ever installs into a CLI you've actually set up.
|
|
94
|
+
|
|
95
|
+
### Fallback results and quota limits
|
|
96
|
+
|
|
97
|
+
The opening run panel prints the resolved chain, and the commit panel and
|
|
98
|
+
final RESULT table name the CLI that handled each step (`—` when skipped).
|
|
99
|
+
When a CLI emits a supported, exact quota/rate-limit signal, aicp records the
|
|
100
|
+
outcome as `quota`, notifies through the usual notification path, and excludes
|
|
101
|
+
that CLI from the rest of that one commit/push flow. The exclusion also
|
|
102
|
+
persists: the CLI stays skipped for `AICP_QUOTA_COOLDOWN` seconds (default one
|
|
103
|
+
hour, state in `~/.aicp/quota.json`), because a token or rate-limit wall
|
|
104
|
+
normally stands for hours and every run inside that window would otherwise burn
|
|
105
|
+
a full budget per step on a CLI that cannot succeed. A skipped CLI prints how
|
|
106
|
+
long is left; `AICP_QUOTA_COOLDOWN=0` switches the cooldown off entirely, and
|
|
107
|
+
deleting the file clears it.
|
|
108
|
+
|
|
109
|
+
Only the `quota` outcome starts a cooldown. A timeout does not — a CLI that
|
|
110
|
+
hangs on its rate limit instead of exiting is indistinguishable from one merely
|
|
111
|
+
running long, and sidelining it for an hour on that guess costs more than the
|
|
112
|
+
retry does.
|
|
113
|
+
|
|
114
|
+
Exact detection is intentionally narrow. It is supported for Codex, Claude,
|
|
115
|
+
Vibe, and Grok only; Copilot and `agy` have no verified quota signature, so a
|
|
116
|
+
nonzero exit from either remains an ordinary failure and is still eligible for
|
|
117
|
+
the next step.
|
|
118
|
+
|
|
119
|
+
## Run it
|
|
120
|
+
|
|
121
|
+
The whole command surface is two things to remember:
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
aicp # commit, then push
|
|
125
|
+
aicp --config # settings, skills, health check — everything else lives here
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
A plain `aicp` run: checks whether anything is pending (skipping the AI CLI
|
|
129
|
+
entirely on a clean, already-in-sync repo), scans for secrets, runs `/commit`
|
|
130
|
+
through the fallback chain, then `/safe-git-push` the same way, then prints
|
|
131
|
+
the git-verified result table described above. `-v`/`--verbose` streams each
|
|
132
|
+
CLI's raw output live instead of showing a spinner.
|
|
133
|
+
|
|
134
|
+

|
|
135
|
+
|
|
136
|
+
`aicp --config` is one menu for everything that isn't "commit and push
|
|
137
|
+
right now": which steps run, message language, fallback CLI order, the
|
|
138
|
+
skills install/upgrade view, and a health check.
|
|
139
|
+
|
|
140
|
+
### `--undo`
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
aicp --undo
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Runs `git reset --soft HEAD^` on the last commit — the escape hatch for a bad
|
|
147
|
+
commit message or a wrong stage. Changes land back in the index, not lost,
|
|
148
|
+
not pushed. It never calls an AI CLI and refuses outright (HEAD untouched) in
|
|
149
|
+
four cases, because once a commit reaches the remote other clones or CI may
|
|
150
|
+
already be building on it:
|
|
151
|
+
|
|
152
|
+
- there's no branch to compare against (detached `HEAD`),
|
|
153
|
+
- there's no parent commit to reset onto,
|
|
154
|
+
- the last commit is already on the remote,
|
|
155
|
+
- the remote can't be verified at all (deleted remote, failed fetch, never
|
|
156
|
+
pushed) — "can't verify" is treated as the risky case, never as "safe."
|
|
157
|
+
|
|
158
|
+
## Automation / CI
|
|
159
|
+
|
|
160
|
+
Everything below is a non-interactive escape hatch for scripting; day-to-day
|
|
161
|
+
use is the two commands above.
|
|
162
|
+
|
|
163
|
+
```sh
|
|
164
|
+
aicp --doctor --json # skill-install status for every configured CLI, as JSON
|
|
165
|
+
aicp --install-skills --yes # install what's missing, upgrade what's outdated
|
|
166
|
+
aicp --install-skills --force # also replace files aicp doesn't recognize
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`--doctor --json` reports, per CLI and per skill: the target path and whether
|
|
170
|
+
it's missing, current, an outdated aicp copy, someone else's file, or the CLI
|
|
171
|
+
itself isn't configured — the same view `--config`'s Skills screen shows,
|
|
172
|
+
without a terminal.
|
|
173
|
+
|
|
174
|
+
`--install-skills --yes` installs anything `MISSING` and upgrades anything
|
|
175
|
+
older than the version aicp ships, exactly like the interactive Skills view —
|
|
176
|
+
and it **never** touches a file aicp doesn't recognize. Only `--force` does
|
|
177
|
+
that, and even then the original is moved aside to `<name>.bak` (numbered
|
|
178
|
+
`.bak.1`, `.bak.2`, … so a second forced install never clobbers the first
|
|
179
|
+
backup) before aicp writes its own copy.
|
|
180
|
+
|
|
181
|
+
### This repo's own CI
|
|
182
|
+
|
|
183
|
+
`.github/workflows/ci.yml` runs the suite on **macOS, Linux, and Windows** in
|
|
184
|
+
parallel (`fail-fast: false`, so one platform failing still tells you about the
|
|
185
|
+
other two), then builds the wheel, installs it into a throwaway venv, and checks
|
|
186
|
+
that the console script runs and the vendored skills survived packaging.
|
|
187
|
+
|
|
188
|
+
A failed run on a `push` also sends one Telegram message. Failure-only is
|
|
189
|
+
deliberate: a notification on every green push is one nobody reads.
|
|
190
|
+
|
|
191
|
+
The credentials are **repo secrets — never committed**. The workflow reads them
|
|
192
|
+
through `${{ secrets.* }}` and skips quietly when they're unset, so a fork or a
|
|
193
|
+
fresh clone gets no second red X on top of the real failure. To enable it:
|
|
194
|
+
|
|
195
|
+
```sh
|
|
196
|
+
gh secret set TELEGRAM_BOT_TOKEN -R <owner>/<repo> # paste the bot token
|
|
197
|
+
gh secret set TELEGRAM_CHAT_ID -R <owner>/<repo> # paste the chat id
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Verify with `gh secret list -R <owner>/<repo>` — GitHub shows the names and
|
|
201
|
+
timestamps only; secret values can never be read back, by you or by CI logs.
|
|
202
|
+
|
|
203
|
+
## Configuration
|
|
204
|
+
|
|
205
|
+
The config file lives at `~/.aicp/config.json` (override the path itself
|
|
206
|
+
with `AICP_CONFIG`, which — being the thing that names the file — can only be
|
|
207
|
+
set as a real environment variable). Precedence everywhere is
|
|
208
|
+
**environment > `config.json` > hardcoded default**. The file is a JSON
|
|
209
|
+
object, parsed as data and never sourced or eval'd, written atomically and
|
|
210
|
+
owner-only (`0600`) by `--config`/`--swap-ai`; only keys matching
|
|
211
|
+
`AICP_[A-Z0-9_]*` case-insensitively with a **string** value built from
|
|
212
|
+
letters, digits and `` / . _ : @ + - `` survive — everything else (an unknown
|
|
213
|
+
key, a non-string value, a value outside that charset) is skipped
|
|
214
|
+
individually, so one bad key never costs the rest of the file. An invalid
|
|
215
|
+
*value* for a known key never aborts a run — it's reported on stderr and
|
|
216
|
+
falls back to the default.
|
|
217
|
+
|
|
218
|
+
`aicp` always **writes** keys `lower_case` (`aicp_do_commit`, not
|
|
219
|
+
`AICP_DO_COMMIT`) — every key in [`config.example.json`](config.example.json)
|
|
220
|
+
and any new knob added in the future follows the same convention. Reading is
|
|
221
|
+
case-insensitive, so an existing file with `AICP_`-cased keys still works.
|
|
222
|
+
|
|
223
|
+
A legacy `~/.aicprc` (the pre-JSON `KEY=value` format) is migrated into
|
|
224
|
+
`~/.aicp/config.json` automatically, once, the first time `aicp` runs — the
|
|
225
|
+
old file is left in place untouched, never deleted or rewritten.
|
|
226
|
+
|
|
227
|
+
See [`config.example.json`](config.example.json) for a ready-to-copy template.
|
|
228
|
+
|
|
229
|
+
| Variable | Default | What it does |
|
|
230
|
+
| --- | --- | --- |
|
|
231
|
+
| `AICP_DO_COMMIT` | `1` | Run the `/commit` step. `0` = only push what's already committed. |
|
|
232
|
+
| `AICP_DO_PUSH` | `1` | Run the `/safe-git-push` step. `0` = commit and stop. |
|
|
233
|
+
| `AICP_LANG` | `en` | Message language: `en` or `zh-TW`, everywhere including notifications. |
|
|
234
|
+
| `AICP_CLI_ORDER` | `copilot agy codex claude vibe grok` | Fallback order. A prefix is enough — any roster name left out is appended after it, in roster order. An unknown or repeated name is refused outright and the default order is used. |
|
|
235
|
+
| `AICP_TZ` | `Asia/Taipei` | IANA zone name used to render commit timestamps. Anything else falls back to the default. |
|
|
236
|
+
| `AICP_TZ_LABEL` | `UTC+8` | Cosmetic label shown beside those timestamps; not validated. |
|
|
237
|
+
| `AICP_STEP_TIMEOUT` | *(unset)* | Pins every CLI's per-step budget in seconds, skipping the formula and history below entirely. |
|
|
238
|
+
| `AICP_TIMEOUT_BASE` | `180` | Budget floor (seconds) — covers cold start plus a small prompt. |
|
|
239
|
+
| `AICP_TIMEOUT_PER_FILE` | `15` | Seconds added per changed or untracked file. |
|
|
240
|
+
| `AICP_TIMEOUT_PER_100L` | `5` | Seconds added per 100 changed lines in tracked files. |
|
|
241
|
+
| `AICP_TIMEOUT_MAX` | `1800` | Ceiling on the formula above. A CLI's own run history may still widen its budget past this — that's direct evidence it legitimately needs the time, not a guess. |
|
|
242
|
+
| `AICP_TIMEOUT_HISTORY_LINES` | `500` | How many recent timing-log rows are scanned when widening a budget from history. |
|
|
243
|
+
| `AICP_TIMEOUT_HISTORY_MULT` | `1.3` | Multiplier applied to a CLI's largest successful run when that exceeds the formula. |
|
|
244
|
+
| `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. |
|
|
245
|
+
| `AICP_SKIP_SECRET_SCAN` | *(unset)* | `1` bypasses the pre-commit secret scan for one run — the documented escape for a false positive. |
|
|
246
|
+
| `AICP_CONFIG` | `~/.aicp/config.json` | Which file this loader reads. Environment-variable only — a file can't rename itself. |
|
|
247
|
+
| `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`. |
|
|
248
|
+
| `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`. |
|
|
249
|
+
|
|
250
|
+
`AICP_TG_SEND` and `AICP_TIMING_LOG` are refused from `config.json` on
|
|
251
|
+
purpose: both name a path that then gets *executed* (`AICP_TG_SEND`, run as a
|
|
252
|
+
script) or *written and rotated* (`AICP_TIMING_LOG`, `mkdir -p` / `>>` / a
|
|
253
|
+
rename). A config file is exactly the kind of thing that can arrive synced
|
|
254
|
+
from someone else's dotfiles repo, so anything that becomes a command or a
|
|
255
|
+
filesystem sink stays a real-environment-only decision — set it in your
|
|
256
|
+
shell, not the file.
|
|
257
|
+
|
|
258
|
+
## Safety
|
|
259
|
+
|
|
260
|
+
Before any AI CLI runs, aicp scans everything the run *could* commit for
|
|
261
|
+
likely secrets: the added lines of the pending diff, every tracked file git
|
|
262
|
+
reports as binary, and every untracked file — since aicp never runs `git add`
|
|
263
|
+
itself, a brand-new file with a pasted secret is exactly the case that would
|
|
264
|
+
otherwise slip through unscanned. A hit **stops the run before any AI CLI is
|
|
265
|
+
called**, and only ever prints the file, line number and the pattern's name —
|
|
266
|
+
never the matched text itself, so a real secret can't reach your terminal,
|
|
267
|
+
a log, or a Telegram notification through this path.
|
|
268
|
+
|
|
269
|
+
Seven fixed patterns are checked (OpenAI-style `sk-…` keys, GitHub PATs and
|
|
270
|
+
App tokens, AWS access key IDs, bearer tokens, and PEM private-key blocks) —
|
|
271
|
+
deliberately prefix/format checks only, with **no entropy heuristic**: that
|
|
272
|
+
was tried and rejected upstream as the main source of false positives (this
|
|
273
|
+
project's own API-key-shaped test fixtures tripped it). A false positive is
|
|
274
|
+
bypassed once with `AICP_SKIP_SECRET_SCAN=1`.
|
|
275
|
+
|
|
276
|
+
A file the scanner can't read as text — genuinely binary, or a UTF-16 `.env`
|
|
277
|
+
full of NUL bytes — is never silently skipped: it's reported as "not
|
|
278
|
+
scanned, verify manually" so an unreadable file never reads as a clean one.
|
|
279
|
+
|
|
280
|
+
## Platform support
|
|
281
|
+
|
|
282
|
+
Tested in CI on macOS, Linux, and Windows, full test suite on all three — no
|
|
283
|
+
platform is a reduced or best-effort target.
|
|
284
|
+
|
|
285
|
+
| Platform | Notes |
|
|
286
|
+
| --- | --- |
|
|
287
|
+
| macOS | No caveats. |
|
|
288
|
+
| Linux | No caveats. |
|
|
289
|
+
| Windows | Every AI CLI ships as an npm `.cmd`/`.bat` shim, which Windows can't launch directly (`CreateProcess` doesn't honor `PATHEXT` and can't run a batch file itself) — aicp resolves the real executable and, for a shim, launches it through `cmd.exe` itself rather than `shell=True`, so no user-controlled text ever builds a shell command line. Ctrl+C is forwarded as `CTRL_BREAK_EVENT` rather than delivered directly (Windows has no "foreground process group" concept), so a CLI that ignores that signal may not stop as cleanly as it would elsewhere. |
|
|
290
|
+
|
|
291
|
+
On POSIX, a per-CLI timeout signals the CLI process itself, not any
|
|
292
|
+
grandchildren it spawned. On Windows, terminating the intermediate `cmd.exe`
|
|
293
|
+
can orphan the CLI behind the shim; that high-priority defect remains open in
|
|
294
|
+
[TODO.md](TODO.md). Ctrl+C still targets the Windows process group.
|
|
295
|
+
|
|
296
|
+
## License
|
|
297
|
+
|
|
298
|
+
MIT — see [`LICENSE`](LICENSE).
|