aicp-cli 0.3.0__tar.gz → 0.4.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 → aicp_cli-0.4.0}/CHANGELOG.md +22 -0
- aicp_cli-0.4.0/CLAUDE.md +7 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/PKG-INFO +54 -20
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/README.md +53 -19
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/pyproject.toml +1 -1
- aicp_cli-0.4.0/src/aicp/agents.json +54 -0
- aicp_cli-0.4.0/src/aicp/agents.py +83 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/src/aicp/cli.py +2 -1
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/src/aicp/contracts.py +7 -8
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/src/aicp/gitflow.py +2 -1
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/src/aicp/menu.py +10 -1
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/src/aicp/runner.py +7 -27
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/src/aicp/skills.py +19 -25
- aicp_cli-0.4.0/tests/test_agents.py +112 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/tests/test_menu.py +10 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/uv.lock +1 -1
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/.github/workflows/ci.yml +0 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/.github/workflows/release.yml +0 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/.gitignore +0 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/LICENSE +0 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/TODO.md +0 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/config.example.json +0 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/docs/images/aicp-run.png +0 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/skills/commit.md +0 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/skills/safe-git-push/SKILL.md +0 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/skills/safe-git-push/scripts/safe_push.py +0 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/src/aicp/__init__.py +0 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/src/aicp/_keyreader.py +0 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/src/aicp/_utils.py +0 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/src/aicp/budget.py +0 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/src/aicp/config.py +0 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/src/aicp/i18n.py +0 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/src/aicp/notify.py +0 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/src/aicp/present.py +0 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/src/aicp/quota.py +0 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/src/aicp/secrets.py +0 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/src/aicp/timing.py +0 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/tests/conftest.py +0 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/tests/test_budget.py +0 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/tests/test_cli.py +0 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/tests/test_config.py +0 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/tests/test_cross_platform.py +0 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/tests/test_gitflow.py +0 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/tests/test_i18n.py +0 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/tests/test_menu_skills.py +0 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/tests/test_notify.py +0 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/tests/test_present.py +0 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/tests/test_quota.py +0 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/tests/test_runner.py +0 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/tests/test_secrets.py +0 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/tests/test_skills.py +0 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/tests/test_timing.py +0 -0
- {aicp_cli-0.3.0 → aicp_cli-0.4.0}/tests/test_undo.py +0 -0
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
## [0.4.0] - 2026-09-14
|
|
2
|
+
|
|
3
|
+
### 🚜 Refactor
|
|
4
|
+
|
|
5
|
+
- **agents:** Centralize agent definitions
|
|
6
|
+
## [0.3.1] - 2026-09-14
|
|
7
|
+
|
|
8
|
+
### 🐛 Bug Fixes
|
|
9
|
+
|
|
10
|
+
- **menu:** Stop config panel resizing on row selection
|
|
11
|
+
|
|
12
|
+
### 📚 Documentation
|
|
13
|
+
|
|
14
|
+
- **readme:** Document releasing and the wheel version check
|
|
15
|
+
- Remove CI section from README.md
|
|
16
|
+
- Add uv requirement note to README.md
|
|
17
|
+
- **changelog:** Release v0.3.1
|
|
1
18
|
## [0.3.0] - 2026-09-14
|
|
2
19
|
|
|
3
20
|
### 🐛 Bug Fixes
|
|
@@ -16,8 +33,13 @@
|
|
|
16
33
|
|
|
17
34
|
- **release:** Publish wheels to PyPI via trusted publishing
|
|
18
35
|
- Assert the built wheel reports the right version
|
|
36
|
+
- **release:** V0.3.0
|
|
19
37
|
## [0.2.1] - 2026-09-13
|
|
20
38
|
|
|
39
|
+
### 📚 Documentation
|
|
40
|
+
|
|
41
|
+
- **changelog:** Release v0.2.1
|
|
42
|
+
|
|
21
43
|
### ⚙️ Miscellaneous Tasks
|
|
22
44
|
|
|
23
45
|
- Refresh uv lockfile for v0.2.0
|
aicp_cli-0.4.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
## Cross-platform compatibility
|
|
4
|
+
|
|
5
|
+
Keep tools platform-agnostic. OS-specific behavior (clipboard, package installation, ANSI) belongs in a shared helper, funneled through one function, so individual tool modules stay clean.
|
|
6
|
+
|
|
7
|
+
Support macOS, Windows, and Linux for user-facing platform behavior.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: aicp-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -36,6 +36,7 @@ push that never reached the remote.
|
|
|
36
36
|
## Requirements
|
|
37
37
|
|
|
38
38
|
- Python 3.10 or newer
|
|
39
|
+
- [`uv`](https://docs.astral.sh/uv/) — the install path below
|
|
39
40
|
- `git`
|
|
40
41
|
- At least one of the six AI CLIs above on `PATH`
|
|
41
42
|
|
|
@@ -48,7 +49,8 @@ uv tool install aicp-cli # latest release
|
|
|
48
49
|
uv tool install aicp-cli==X.Y.Z # pin to a specific version
|
|
49
50
|
```
|
|
50
51
|
|
|
51
|
-
The distribution is `aicp-cli
|
|
52
|
+
The distribution is [`aicp-cli`](https://pypi.org/project/aicp-cli/); the command
|
|
53
|
+
it installs is `aicp`. (The plain
|
|
52
54
|
`aicp` name on PyPI belongs to an unrelated 2021 project — don't install it.)
|
|
53
55
|
Replace `X.Y.Z` with the release version you want; re-running either command
|
|
54
56
|
switches an existing install to that version.
|
|
@@ -178,30 +180,62 @@ that, and even then the original is moved aside to `<name>.bak` (numbered
|
|
|
178
180
|
`.bak.1`, `.bak.2`, … so a second forced install never clobbers the first
|
|
179
181
|
backup) before aicp writes its own copy.
|
|
180
182
|
|
|
181
|
-
###
|
|
183
|
+
### Releasing
|
|
182
184
|
|
|
183
|
-
`.github/workflows/
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
185
|
+
`.github/workflows/release.yml` runs on a `v*` tag. It repeats the full
|
|
186
|
+
lint/test/build pass, installs the wheel into a throwaway venv, and refuses to
|
|
187
|
+
upload unless that wheel reports the version being tagged — so a tag that
|
|
188
|
+
disagrees with `pyproject.toml` fails before anything reaches PyPI. It then
|
|
189
|
+
publishes with Trusted Publishing (OIDC — there is no API token stored in this
|
|
190
|
+
repo) and attaches the wheel, sdist and `SHA256SUMS` to the GitHub release.
|
|
187
191
|
|
|
188
|
-
|
|
189
|
-
|
|
192
|
+
Checksums are generated *after* publishing on purpose: `uv publish` uploads
|
|
193
|
+
everything in `dist/`, and `SHA256SUMS` is not a distribution.
|
|
190
194
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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.
|
|
195
|
+
A tag runs the workflow file **as it existed at that tag**, so a release that
|
|
196
|
+
failed to publish can't be repaired by re-running the old tag — the fix isn't
|
|
197
|
+
in that tree. Bump the version and tag again.
|
|
202
198
|
|
|
203
199
|
## Configuration
|
|
204
200
|
|
|
201
|
+
Agent definitions live in [`src/aicp/agents.json`](src/aicp/agents.json),
|
|
202
|
+
bundled with the installed package. Both `aicp` and `aicp --config` use this
|
|
203
|
+
registry for executable lookup, invocation arguments, config directories,
|
|
204
|
+
and skill installation/status. To maintain an agent, edit its entry here.
|
|
205
|
+
Registry order supplies the default fallback order; `aicp_cli_order` still
|
|
206
|
+
controls the user's preferred order.
|
|
207
|
+
|
|
208
|
+
Use `~/` paths and bare executable names for entries shared across macOS,
|
|
209
|
+
Linux, and Windows. Python resolves `~` to the native home directory
|
|
210
|
+
(`USERPROFILE` on Windows); forward slashes work on all three systems.
|
|
211
|
+
Absolute paths are specific to the host OS; in JSON, a Windows path can use
|
|
212
|
+
`C:/Users/name/.codex` or escaped backslashes (`C:\\Users\\name\\.codex`).
|
|
213
|
+
Executable lookup uses `PATH` and, on Windows, `PATHEXT`; the runner handles
|
|
214
|
+
both native executables and `.cmd`/`.bat` launchers. CI runs the registry
|
|
215
|
+
integration tests and installed-wheel checks on all three operating systems.
|
|
216
|
+
|
|
217
|
+
| Agent field | Meaning |
|
|
218
|
+
| --- | --- |
|
|
219
|
+
| `executable` | Binary name resolved on `PATH`, or an absolute executable path. Separate from the stable agent ID used in fallback order and history. |
|
|
220
|
+
| `config_dir` | Absolute path or `~/`-relative directory; home is resolved when used. |
|
|
221
|
+
| `config_dir_env` | Optional environment variable overriding that directory (currently `GROK_HOME`). |
|
|
222
|
+
| `skills_dir` | Relative directory inside `config_dir` where skills are installed. |
|
|
223
|
+
| `skills` | Vendored skills to install: `commit` and/or `safe-git-push`. Claude keeps its existing `/commit`. |
|
|
224
|
+
| `memory_file` | Instruction filename used when adapting vendored skill text. Project directory references use the basename of `config_dir`. |
|
|
225
|
+
| `args` | Argument array containing exactly one standalone `{prompt}`, replaced with the complete prompt as one argument. No shell evaluation. |
|
|
226
|
+
|
|
227
|
+
The JSON envelope has `version: 1` and an `agents` object keyed by stable
|
|
228
|
+
agent IDs. Future supported settings can be added to each entry and the
|
|
229
|
+
loader; adding a JSON field alone does not implement new behavior.
|
|
230
|
+
Invocation defaults preserve agy's `--new-project` (use the current working
|
|
231
|
+
directory), vibe's `--trust` (avoid an interactive trust prompt), and the
|
|
232
|
+
existing MCP startup suppression flags.
|
|
233
|
+
|
|
234
|
+
This is packaged application data, not a per-user override file. Package
|
|
235
|
+
upgrades replace it; neither repository-local files nor
|
|
236
|
+
`~/.aicp/config.json` can override agent execution settings. Ordinary user
|
|
237
|
+
preferences continue to use the config file below.
|
|
238
|
+
|
|
205
239
|
The config file lives at `~/.aicp/config.json` (override the path itself
|
|
206
240
|
with `AICP_CONFIG`, which — being the thing that names the file — can only be
|
|
207
241
|
set as a real environment variable). Precedence everywhere is
|
|
@@ -24,6 +24,7 @@ push that never reached the remote.
|
|
|
24
24
|
## Requirements
|
|
25
25
|
|
|
26
26
|
- Python 3.10 or newer
|
|
27
|
+
- [`uv`](https://docs.astral.sh/uv/) — the install path below
|
|
27
28
|
- `git`
|
|
28
29
|
- At least one of the six AI CLIs above on `PATH`
|
|
29
30
|
|
|
@@ -36,7 +37,8 @@ uv tool install aicp-cli # latest release
|
|
|
36
37
|
uv tool install aicp-cli==X.Y.Z # pin to a specific version
|
|
37
38
|
```
|
|
38
39
|
|
|
39
|
-
The distribution is `aicp-cli
|
|
40
|
+
The distribution is [`aicp-cli`](https://pypi.org/project/aicp-cli/); the command
|
|
41
|
+
it installs is `aicp`. (The plain
|
|
40
42
|
`aicp` name on PyPI belongs to an unrelated 2021 project — don't install it.)
|
|
41
43
|
Replace `X.Y.Z` with the release version you want; re-running either command
|
|
42
44
|
switches an existing install to that version.
|
|
@@ -166,30 +168,62 @@ that, and even then the original is moved aside to `<name>.bak` (numbered
|
|
|
166
168
|
`.bak.1`, `.bak.2`, … so a second forced install never clobbers the first
|
|
167
169
|
backup) before aicp writes its own copy.
|
|
168
170
|
|
|
169
|
-
###
|
|
171
|
+
### Releasing
|
|
170
172
|
|
|
171
|
-
`.github/workflows/
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
173
|
+
`.github/workflows/release.yml` runs on a `v*` tag. It repeats the full
|
|
174
|
+
lint/test/build pass, installs the wheel into a throwaway venv, and refuses to
|
|
175
|
+
upload unless that wheel reports the version being tagged — so a tag that
|
|
176
|
+
disagrees with `pyproject.toml` fails before anything reaches PyPI. It then
|
|
177
|
+
publishes with Trusted Publishing (OIDC — there is no API token stored in this
|
|
178
|
+
repo) and attaches the wheel, sdist and `SHA256SUMS` to the GitHub release.
|
|
175
179
|
|
|
176
|
-
|
|
177
|
-
|
|
180
|
+
Checksums are generated *after* publishing on purpose: `uv publish` uploads
|
|
181
|
+
everything in `dist/`, and `SHA256SUMS` is not a distribution.
|
|
178
182
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
```sh
|
|
184
|
-
gh secret set TELEGRAM_BOT_TOKEN -R <owner>/<repo> # paste the bot token
|
|
185
|
-
gh secret set TELEGRAM_CHAT_ID -R <owner>/<repo> # paste the chat id
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
Verify with `gh secret list -R <owner>/<repo>` — GitHub shows the names and
|
|
189
|
-
timestamps only; secret values can never be read back, by you or by CI logs.
|
|
183
|
+
A tag runs the workflow file **as it existed at that tag**, so a release that
|
|
184
|
+
failed to publish can't be repaired by re-running the old tag — the fix isn't
|
|
185
|
+
in that tree. Bump the version and tag again.
|
|
190
186
|
|
|
191
187
|
## Configuration
|
|
192
188
|
|
|
189
|
+
Agent definitions live in [`src/aicp/agents.json`](src/aicp/agents.json),
|
|
190
|
+
bundled with the installed package. Both `aicp` and `aicp --config` use this
|
|
191
|
+
registry for executable lookup, invocation arguments, config directories,
|
|
192
|
+
and skill installation/status. To maintain an agent, edit its entry here.
|
|
193
|
+
Registry order supplies the default fallback order; `aicp_cli_order` still
|
|
194
|
+
controls the user's preferred order.
|
|
195
|
+
|
|
196
|
+
Use `~/` paths and bare executable names for entries shared across macOS,
|
|
197
|
+
Linux, and Windows. Python resolves `~` to the native home directory
|
|
198
|
+
(`USERPROFILE` on Windows); forward slashes work on all three systems.
|
|
199
|
+
Absolute paths are specific to the host OS; in JSON, a Windows path can use
|
|
200
|
+
`C:/Users/name/.codex` or escaped backslashes (`C:\\Users\\name\\.codex`).
|
|
201
|
+
Executable lookup uses `PATH` and, on Windows, `PATHEXT`; the runner handles
|
|
202
|
+
both native executables and `.cmd`/`.bat` launchers. CI runs the registry
|
|
203
|
+
integration tests and installed-wheel checks on all three operating systems.
|
|
204
|
+
|
|
205
|
+
| Agent field | Meaning |
|
|
206
|
+
| --- | --- |
|
|
207
|
+
| `executable` | Binary name resolved on `PATH`, or an absolute executable path. Separate from the stable agent ID used in fallback order and history. |
|
|
208
|
+
| `config_dir` | Absolute path or `~/`-relative directory; home is resolved when used. |
|
|
209
|
+
| `config_dir_env` | Optional environment variable overriding that directory (currently `GROK_HOME`). |
|
|
210
|
+
| `skills_dir` | Relative directory inside `config_dir` where skills are installed. |
|
|
211
|
+
| `skills` | Vendored skills to install: `commit` and/or `safe-git-push`. Claude keeps its existing `/commit`. |
|
|
212
|
+
| `memory_file` | Instruction filename used when adapting vendored skill text. Project directory references use the basename of `config_dir`. |
|
|
213
|
+
| `args` | Argument array containing exactly one standalone `{prompt}`, replaced with the complete prompt as one argument. No shell evaluation. |
|
|
214
|
+
|
|
215
|
+
The JSON envelope has `version: 1` and an `agents` object keyed by stable
|
|
216
|
+
agent IDs. Future supported settings can be added to each entry and the
|
|
217
|
+
loader; adding a JSON field alone does not implement new behavior.
|
|
218
|
+
Invocation defaults preserve agy's `--new-project` (use the current working
|
|
219
|
+
directory), vibe's `--trust` (avoid an interactive trust prompt), and the
|
|
220
|
+
existing MCP startup suppression flags.
|
|
221
|
+
|
|
222
|
+
This is packaged application data, not a per-user override file. Package
|
|
223
|
+
upgrades replace it; neither repository-local files nor
|
|
224
|
+
`~/.aicp/config.json` can override agent execution settings. Ordinary user
|
|
225
|
+
preferences continue to use the config file below.
|
|
226
|
+
|
|
193
227
|
The config file lives at `~/.aicp/config.json` (override the path itself
|
|
194
228
|
with `AICP_CONFIG`, which — being the thing that names the file — can only be
|
|
195
229
|
set as a real environment variable). Precedence everywhere is
|
|
@@ -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.4.0"
|
|
6
6
|
description = "AI commit + push, with a git-verified result summary"
|
|
7
7
|
readme = "README.md"
|
|
8
8
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"agents": {
|
|
4
|
+
"copilot": {
|
|
5
|
+
"executable": "copilot",
|
|
6
|
+
"config_dir": "~/.copilot",
|
|
7
|
+
"memory_file": "AGENTS.md",
|
|
8
|
+
"skills_dir": "skills",
|
|
9
|
+
"skills": ["commit", "safe-git-push"],
|
|
10
|
+
"args": ["-p", "{prompt}", "--allow-all", "--disable-builtin-mcps", "--no-auto-update"]
|
|
11
|
+
},
|
|
12
|
+
"agy": {
|
|
13
|
+
"executable": "agy",
|
|
14
|
+
"config_dir": "~/.gemini",
|
|
15
|
+
"memory_file": "GEMINI.md",
|
|
16
|
+
"skills_dir": "skills",
|
|
17
|
+
"skills": ["commit", "safe-git-push"],
|
|
18
|
+
"args": ["-p", "{prompt}", "--dangerously-skip-permissions", "--new-project"]
|
|
19
|
+
},
|
|
20
|
+
"codex": {
|
|
21
|
+
"executable": "codex",
|
|
22
|
+
"config_dir": "~/.codex",
|
|
23
|
+
"memory_file": "AGENTS.md",
|
|
24
|
+
"skills_dir": "skills",
|
|
25
|
+
"skills": ["commit", "safe-git-push"],
|
|
26
|
+
"args": ["exec", "--dangerously-bypass-approvals-and-sandbox", "{prompt}"]
|
|
27
|
+
},
|
|
28
|
+
"claude": {
|
|
29
|
+
"executable": "claude",
|
|
30
|
+
"config_dir": "~/.claude",
|
|
31
|
+
"memory_file": "CLAUDE.md",
|
|
32
|
+
"skills_dir": "skills",
|
|
33
|
+
"skills": ["safe-git-push"],
|
|
34
|
+
"args": ["-p", "{prompt}", "--dangerously-skip-permissions", "--strict-mcp-config"]
|
|
35
|
+
},
|
|
36
|
+
"vibe": {
|
|
37
|
+
"executable": "vibe",
|
|
38
|
+
"config_dir": "~/.vibe",
|
|
39
|
+
"memory_file": "AGENTS.md",
|
|
40
|
+
"skills_dir": "skills",
|
|
41
|
+
"skills": ["commit", "safe-git-push"],
|
|
42
|
+
"args": ["-p", "{prompt}", "--auto-approve", "--trust"]
|
|
43
|
+
},
|
|
44
|
+
"grok": {
|
|
45
|
+
"executable": "grok",
|
|
46
|
+
"config_dir": "~/.grok",
|
|
47
|
+
"config_dir_env": "GROK_HOME",
|
|
48
|
+
"memory_file": "AGENTS.md",
|
|
49
|
+
"skills_dir": "skills",
|
|
50
|
+
"skills": ["commit", "safe-git-push"],
|
|
51
|
+
"args": ["-p", "{prompt}", "--always-approve", "--no-subagents"]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Bundled agent definitions shared by execution and skill management.
|
|
2
|
+
|
|
3
|
+
Only the package's agents.json is read; working-directory and user config
|
|
4
|
+
files cannot supply executable paths or arguments through this registry.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class Agent:
|
|
17
|
+
executable: str
|
|
18
|
+
config_dir: str
|
|
19
|
+
memory_file: str
|
|
20
|
+
skills_dir: str
|
|
21
|
+
skills: tuple[str, ...]
|
|
22
|
+
args: tuple[str, ...]
|
|
23
|
+
config_dir_env: str = ""
|
|
24
|
+
|
|
25
|
+
def config_root(self, home: Path | None = None) -> Path:
|
|
26
|
+
if home is None and (override := os.environ.get(self.config_dir_env)):
|
|
27
|
+
return Path(override).expanduser()
|
|
28
|
+
if self.config_dir.startswith("~/"):
|
|
29
|
+
return (Path.home() if home is None else home) / self.config_dir[2:]
|
|
30
|
+
return Path(self.config_dir)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def load_agents(path: Path) -> dict[str, Agent]:
|
|
34
|
+
"""Validate packaged data and preserve its default fallback order."""
|
|
35
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
36
|
+
if not isinstance(data, dict) or data.get("version") != 1:
|
|
37
|
+
raise ValueError(f"{path}: unsupported agent registry version")
|
|
38
|
+
entries = data.get("agents")
|
|
39
|
+
if not isinstance(entries, dict) or not entries:
|
|
40
|
+
raise ValueError(f"{path}: expected a nonempty agents object")
|
|
41
|
+
agents = {}
|
|
42
|
+
for name, entry in entries.items():
|
|
43
|
+
if not isinstance(entry, dict):
|
|
44
|
+
raise TypeError(f"{path}: {name}: expected an agent object")
|
|
45
|
+
for key in ["executable", "config_dir", "memory_file", "skills_dir"]:
|
|
46
|
+
value = entry.get(key)
|
|
47
|
+
if not isinstance(value, str) or not value.strip() or "\0" in value:
|
|
48
|
+
raise ValueError(f"{path}: {name}: invalid {key}")
|
|
49
|
+
for key in ("args", "skills"):
|
|
50
|
+
value = entry.get(key)
|
|
51
|
+
if not isinstance(value, list) or not all(
|
|
52
|
+
isinstance(item, str) and "\0" not in item for item in value
|
|
53
|
+
):
|
|
54
|
+
raise ValueError(f"{path}: {name}: invalid {key}")
|
|
55
|
+
if entry["args"].count("{prompt}") != 1:
|
|
56
|
+
raise ValueError(f"{path}: {name}: args must contain one {{prompt}} argument")
|
|
57
|
+
if not entry["config_dir"].startswith("~/") and not Path(entry["config_dir"]).is_absolute():
|
|
58
|
+
raise ValueError(f"{path}: {name}: config_dir must be absolute or start with ~/")
|
|
59
|
+
skills_dir = Path(entry["skills_dir"])
|
|
60
|
+
if skills_dir.anchor or ".." in skills_dir.parts or skills_dir == Path("."):
|
|
61
|
+
raise ValueError(f"{path}: {name}: skills_dir must stay inside config_dir")
|
|
62
|
+
env = entry.get("config_dir_env", "")
|
|
63
|
+
if not isinstance(env, str) or (env and not env.isidentifier()):
|
|
64
|
+
raise ValueError(f"{path}: {name}: invalid config_dir_env")
|
|
65
|
+
agents[name] = Agent(
|
|
66
|
+
executable=entry["executable"],
|
|
67
|
+
config_dir=entry["config_dir"],
|
|
68
|
+
memory_file=entry["memory_file"],
|
|
69
|
+
skills_dir=entry["skills_dir"],
|
|
70
|
+
skills=tuple(entry["skills"]),
|
|
71
|
+
args=tuple(entry["args"]),
|
|
72
|
+
config_dir_env=env,
|
|
73
|
+
)
|
|
74
|
+
return agents
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
AGENTS = load_agents(Path(__file__).with_name("agents.json"))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def executable(name: str) -> str:
|
|
81
|
+
"""Resolve a roster ID to its binary; leave non-agent commands alone."""
|
|
82
|
+
agent = AGENTS.get(name)
|
|
83
|
+
return agent.executable if agent else name
|
|
@@ -38,6 +38,7 @@ from typing import NoReturn
|
|
|
38
38
|
|
|
39
39
|
from . import __version__, config, gitflow, i18n, menu, notify, present, runner, skills
|
|
40
40
|
from ._utils import BLUE, BOLD, CYAN, DIM, GREEN, MAGENTA, RED, RESET, YELLOW, have
|
|
41
|
+
from .agents import executable
|
|
41
42
|
from .contracts import ROSTER
|
|
42
43
|
from .i18n import t
|
|
43
44
|
|
|
@@ -152,7 +153,7 @@ def _nudge(chain: Sequence[str]) -> None:
|
|
|
152
153
|
target, no reads, no sidecar parsing).
|
|
153
154
|
"""
|
|
154
155
|
for name in chain:
|
|
155
|
-
if not have(name):
|
|
156
|
+
if not have(executable(name)):
|
|
156
157
|
continue
|
|
157
158
|
entry = _BY_NAME.get(name)
|
|
158
159
|
if entry is None:
|
|
@@ -17,6 +17,8 @@ from collections.abc import Callable
|
|
|
17
17
|
from dataclasses import dataclass
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
|
|
20
|
+
from .agents import AGENTS
|
|
21
|
+
|
|
20
22
|
__all__ = [
|
|
21
23
|
"CLI",
|
|
22
24
|
"ROSTER",
|
|
@@ -35,7 +37,7 @@ class CLI:
|
|
|
35
37
|
equivalents) both key off ``name``; T2 additionally needs ``config_dir``
|
|
36
38
|
to detect whether a CLI is configured at all before trying to run it.
|
|
37
39
|
|
|
38
|
-
``name`` is the
|
|
40
|
+
``name`` is the stable agent ID; agents.json supplies its executable.
|
|
39
41
|
``config_dir`` is where that CLI keeps its own config/session state — and
|
|
40
42
|
is deliberately a SEPARATE field, not derived from ``name``, because it
|
|
41
43
|
does not always match: the ``agy`` binary (this project's wrapper name
|
|
@@ -53,13 +55,10 @@ class CLI:
|
|
|
53
55
|
# saved-chain compatibility. T3 is the consumer that reorders/persists a
|
|
54
56
|
# chain derived from this; T2 and T4 must never hardcode this tuple themselves
|
|
55
57
|
# — import it.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
CLI(name=
|
|
59
|
-
|
|
60
|
-
CLI(name="claude", config_dir=Path.home() / ".claude"),
|
|
61
|
-
CLI(name="vibe", config_dir=Path.home() / ".vibe"),
|
|
62
|
-
CLI(name="grok", config_dir=Path.home() / ".grok"),
|
|
58
|
+
# Resolve defaults here; environment overrides stay live in config_root.
|
|
59
|
+
ROSTER: tuple[CLI, ...] = tuple(
|
|
60
|
+
CLI(name=name, config_dir=agent.config_root(Path.home()))
|
|
61
|
+
for name, agent in AGENTS.items()
|
|
63
62
|
)
|
|
64
63
|
|
|
65
64
|
# The resolved fallback chain — whatever order a run will actually try its
|
|
@@ -30,6 +30,7 @@ from dataclasses import dataclass
|
|
|
30
30
|
from pathlib import Path
|
|
31
31
|
|
|
32
32
|
from ._utils import BOLD, DIM, GREEN, RED, RESET, YELLOW, have, run_interruptible
|
|
33
|
+
from .agents import executable
|
|
33
34
|
from .i18n import t
|
|
34
35
|
from .secrets import report_lines, scan
|
|
35
36
|
|
|
@@ -188,7 +189,7 @@ def preflight(chain: Sequence[str]) -> tuple[bool, list[str]]:
|
|
|
188
189
|
alone and succeeds with no AI CLI installed. A blanket pre-flight would
|
|
189
190
|
turn that into a failure — a regression wearing a check's clothing.
|
|
190
191
|
"""
|
|
191
|
-
if any(have(cli) for cli in chain):
|
|
192
|
+
if any(have(executable(cli)) for cli in chain):
|
|
192
193
|
return True, []
|
|
193
194
|
return False, [
|
|
194
195
|
f"{RED}" + t("no_cli_found", "✗ no AI CLI found on PATH") + RESET,
|
|
@@ -653,8 +653,17 @@ def _panel(
|
|
|
653
653
|
)
|
|
654
654
|
title = f"{_t(state.lang, 'config_title', 'aicp config')} (v{__version__})"
|
|
655
655
|
if selected is not None:
|
|
656
|
+
# Reserve the widest help line ANY row can show, not just the selected
|
|
657
|
+
# one's — otherwise the frame narrows and widens as the cursor moves
|
|
658
|
+
# across rows with shorter and longer help text (render_panel sizes
|
|
659
|
+
# its own width off the notes it is handed).
|
|
660
|
+
help_budget = frame_columns - 4
|
|
661
|
+
fitted_help = [_fit(_t(state.lang, *row.help), help_budget) for row in ROWS]
|
|
662
|
+
reserve = max(width(h) for h in fitted_help)
|
|
663
|
+
help_line = fitted_help[selected - 1]
|
|
664
|
+
help_line += " " * (reserve - width(help_line))
|
|
656
665
|
texts = [
|
|
657
|
-
|
|
666
|
+
help_line,
|
|
658
667
|
"",
|
|
659
668
|
_t(state.lang, "config_keys_tui", "↑↓ select · ←→ change · ⏎ change/run · q/Ctrl-C quit · saves as you go"),
|
|
660
669
|
]
|
|
@@ -60,6 +60,7 @@ from aicp._utils import (
|
|
|
60
60
|
have,
|
|
61
61
|
run_interruptible,
|
|
62
62
|
)
|
|
63
|
+
from aicp.agents import AGENTS
|
|
63
64
|
from aicp.contracts import NotifyFn
|
|
64
65
|
from aicp.i18n import t
|
|
65
66
|
from aicp.present import Spinner
|
|
@@ -103,27 +104,6 @@ class StepResult:
|
|
|
103
104
|
# budget.py's stated invariant true: a bad config value never bricks aicp.
|
|
104
105
|
_MAX_DEADLINE = 365 * 24 * 3600
|
|
105
106
|
|
|
106
|
-
# Per-CLI flags, ported flag-for-flag from _aicp_invoke. Each branch skips MCP
|
|
107
|
-
# server startup where the CLI exposes a way to — /commit and /safe-git-push
|
|
108
|
-
# are plain git prompts, no MCP tool use — which measurably shortens cold start;
|
|
109
|
-
# codex and agy showed no consistent gain, so they keep their existing flags.
|
|
110
|
-
_FLAGS: dict[str, tuple[str, ...]] = {
|
|
111
|
-
# agy resumes its last session/workspace by default, ignoring $PWD entirely
|
|
112
|
-
# unless --new-project pins it to the caller's cwd.
|
|
113
|
-
"agy": ("--dangerously-skip-permissions", "--new-project"),
|
|
114
|
-
"claude": ("--dangerously-skip-permissions", "--strict-mcp-config"),
|
|
115
|
-
"copilot": ("--allow-all", "--disable-builtin-mcps", "--no-auto-update"),
|
|
116
|
-
"grok": ("--always-approve", "--no-subagents"),
|
|
117
|
-
# --trust is not the approval flag (--auto-approve is) and is not optional:
|
|
118
|
-
# vibe asks once, interactively, before it will touch an untrusted
|
|
119
|
-
# directory, and its own --help names --trust as the answer for
|
|
120
|
-
# non-interactive automation. Without it the very first aicp run in a new
|
|
121
|
-
# repo blocks on a prompt nobody can see behind the spinner until the budget
|
|
122
|
-
# kills it — a hang presented as a timeout. Per-invocation, never written to
|
|
123
|
-
# vibe's trusted_folders.toml, so aicp leaves no persistent trust behind.
|
|
124
|
-
"vibe": ("--auto-approve", "--trust"),
|
|
125
|
-
}
|
|
126
|
-
|
|
127
107
|
# Literal signals confirmed by T1. Copilot and agy are intentionally absent:
|
|
128
108
|
# neither had a supported quota signature, and broad guesses would turn an
|
|
129
109
|
# ordinary failure into a run-scoped exclusion.
|
|
@@ -153,10 +133,10 @@ def invoke_argv(cli: str, prompt: str) -> list[str] | None:
|
|
|
153
133
|
silently returned 0 for a name it did not know, which would be read here as
|
|
154
134
|
"that CLI succeeded" without anything having run.
|
|
155
135
|
"""
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
return
|
|
136
|
+
agent = AGENTS.get(cli)
|
|
137
|
+
if agent is None:
|
|
138
|
+
return None
|
|
139
|
+
return [agent.executable, *(prompt if arg == "{prompt}" else arg for arg in agent.args)]
|
|
160
140
|
|
|
161
141
|
|
|
162
142
|
# ── Windows: launching an npm shim ───────────────────────────────────────────
|
|
@@ -187,7 +167,7 @@ _BATCH_SUFFIXES = frozenset({".cmd", ".bat"})
|
|
|
187
167
|
# is expanded inside quotes too. There is no sound, portable escape for either
|
|
188
168
|
# one on a cmd command line, so a launch carrying them is refused rather than
|
|
189
169
|
# half-escaped. aicp never builds one — prompts are slash-command names and the
|
|
190
|
-
# flags are
|
|
170
|
+
# flags are literals in the bundled agents.json — so this is a tripwire, not a limit
|
|
191
171
|
# anyone meets.
|
|
192
172
|
# ponytail: fail closed; write a real cmd quoter only if aicp ever has to pass
|
|
193
173
|
# arbitrary free text as a prompt on Windows.
|
|
@@ -447,7 +427,7 @@ def run_step(
|
|
|
447
427
|
with _capture_file() as (capture, capture_path):
|
|
448
428
|
for cli in chain:
|
|
449
429
|
argv = invoke_argv(cli, prompt)
|
|
450
|
-
if argv is None or not have(
|
|
430
|
+
if argv is None or not have(argv[0]):
|
|
451
431
|
continue
|
|
452
432
|
|
|
453
433
|
# An earlier run already found this one out of quota. Skipping is
|
|
@@ -8,9 +8,8 @@ vendored copies under ``skills/`` into each configured CLI.
|
|
|
8
8
|
|
|
9
9
|
Three rules make it safe:
|
|
10
10
|
|
|
11
|
-
* **Targets
|
|
12
|
-
|
|
13
|
-
:data:`PLATFORMS` is that table, keyed by the config dir's own name.
|
|
11
|
+
* **Targets come from agents.json**, never from the binary name — ``agy``
|
|
12
|
+
reads ``~/.gemini``, and ``~/.agy`` does not exist.
|
|
14
13
|
* **Anything aicp did not write, byte for byte, is the user's**
|
|
15
14
|
(:data:`FOREIGN`). It is never overwritten without an explicit
|
|
16
15
|
``force=True``; a ``--yes``-style non-interactive run installs only what's
|
|
@@ -24,11 +23,9 @@ Three rules make it safe:
|
|
|
24
23
|
config dir is the skill itself. Pre-0.2 in-place ``.aicp-version`` sidecars
|
|
25
24
|
are folded into that file and deleted on the next install.
|
|
26
25
|
|
|
27
|
-
All public functions take ``home=``
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
still comes from ``config_dir.name`` — from the contract, not from the binary
|
|
31
|
-
name.
|
|
26
|
+
All public functions take ``home=`` to rebase home-relative registry paths
|
|
27
|
+
and otherwise resolve :func:`Path.home` at call time. Absolute registry paths
|
|
28
|
+
remain absolute; environment overrides apply only when home is omitted.
|
|
32
29
|
"""
|
|
33
30
|
|
|
34
31
|
from __future__ import annotations
|
|
@@ -42,6 +39,7 @@ from dataclasses import dataclass
|
|
|
42
39
|
from pathlib import Path
|
|
43
40
|
|
|
44
41
|
from . import __version__
|
|
42
|
+
from .agents import AGENTS
|
|
45
43
|
from .contracts import (
|
|
46
44
|
CLI,
|
|
47
45
|
ROSTER,
|
|
@@ -166,15 +164,10 @@ class Platform:
|
|
|
166
164
|
skills: tuple[str, ...]
|
|
167
165
|
|
|
168
166
|
|
|
169
|
-
_BOTH = ("commit", "safe-git-push")
|
|
170
|
-
|
|
171
167
|
PLATFORMS: dict[str, Platform] = {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
".gemini": Platform(".gemini", "GEMINI.md", _BOTH), # the agy binary
|
|
176
|
-
".vibe": Platform(".vibe", "AGENTS.md", _BOTH),
|
|
177
|
-
".grok": Platform(".grok", "AGENTS.md", _BOTH),
|
|
168
|
+
name: Platform(name, agent.memory_file, agent.skills)
|
|
169
|
+
for agent in AGENTS.values()
|
|
170
|
+
for name in (Path(agent.config_dir).name,)
|
|
178
171
|
}
|
|
179
172
|
|
|
180
173
|
|
|
@@ -204,20 +197,21 @@ class InstallResult:
|
|
|
204
197
|
|
|
205
198
|
|
|
206
199
|
def _platform(cli: CLI) -> Platform:
|
|
207
|
-
|
|
200
|
+
agent = AGENTS[cli.name]
|
|
201
|
+
return Platform(Path(agent.config_dir).name, agent.memory_file, agent.skills)
|
|
208
202
|
|
|
209
203
|
|
|
210
204
|
def config_root(cli: CLI, home: Path | None = None) -> Path:
|
|
211
205
|
"""This CLI's config dir, re-based on *home* or the live environment."""
|
|
212
|
-
|
|
213
|
-
return Path(home) / cli.config_dir.name
|
|
214
|
-
if cli.name == "grok" and (grok_home := os.environ.get("GROK_HOME")):
|
|
215
|
-
return Path(grok_home)
|
|
216
|
-
return Path.home() / cli.config_dir.name
|
|
206
|
+
return AGENTS[cli.name].config_root(home)
|
|
217
207
|
|
|
218
208
|
|
|
219
209
|
def target_path(cli: CLI, skill: str, home: Path | None = None) -> Path:
|
|
220
|
-
return
|
|
210
|
+
return (
|
|
211
|
+
config_root(cli, home)
|
|
212
|
+
/ AGENTS[cli.name].skills_dir
|
|
213
|
+
/ SKILLS[skill].rel_target.relative_to("skills")
|
|
214
|
+
)
|
|
221
215
|
|
|
222
216
|
|
|
223
217
|
# ── the state file ───────────────────────────────────────────────────────────
|
|
@@ -455,7 +449,7 @@ def _inspect(cli: CLI, skill: str, home: Path | None) -> tuple[str, str | None]:
|
|
|
455
449
|
if not root.is_dir():
|
|
456
450
|
return NOT_INSTALLED, None
|
|
457
451
|
spec = SKILLS[skill]
|
|
458
|
-
target =
|
|
452
|
+
target = target_path(cli, skill, home)
|
|
459
453
|
if not target.exists():
|
|
460
454
|
return MISSING, None
|
|
461
455
|
|
|
@@ -500,7 +494,7 @@ def missing_skills(cli: CLI, home: Path | None = None) -> tuple[str, ...]:
|
|
|
500
494
|
return tuple(
|
|
501
495
|
name
|
|
502
496
|
for name in _platform(cli).skills
|
|
503
|
-
if not (
|
|
497
|
+
if not target_path(cli, name, home).exists()
|
|
504
498
|
)
|
|
505
499
|
|
|
506
500
|
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Exercise registry edits through the real command and skill consumers."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
import aicp
|
|
13
|
+
from aicp.agents import load_agents
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.mark.parametrize("location", ["home", "absolute", "environment"])
|
|
17
|
+
def test_registry_edit_reaches_commands_and_skills(tmp_path, location):
|
|
18
|
+
package = Path(aicp.__file__).parent
|
|
19
|
+
registry = package / "agents.json"
|
|
20
|
+
assert registry.is_file(), "agent definitions must ship with the package"
|
|
21
|
+
shutil.copytree(package, tmp_path / "aicp")
|
|
22
|
+
from aicp.skills import VENDOR_DIR
|
|
23
|
+
|
|
24
|
+
shutil.copytree(VENDOR_DIR, tmp_path / "aicp/_skills_data", dirs_exist_ok=True)
|
|
25
|
+
data = json.loads(registry.read_text())
|
|
26
|
+
root = Path.home() / "custom agent 測試" / "config"
|
|
27
|
+
data["agents"]["codex"].update(
|
|
28
|
+
executable=sys.executable,
|
|
29
|
+
config_dir="~/custom agent 測試/config" if location == "home" else str(root),
|
|
30
|
+
skills_dir="custom-skills",
|
|
31
|
+
args=["-c", "import sys; print(sys.argv[1])", "{prompt}"],
|
|
32
|
+
)
|
|
33
|
+
if location == "environment":
|
|
34
|
+
data["agents"]["codex"].update(
|
|
35
|
+
config_dir="~/unused", config_dir_env="AICP_TEST_AGENT_HOME"
|
|
36
|
+
)
|
|
37
|
+
(tmp_path / "aicp/agents.json").write_text(json.dumps(data))
|
|
38
|
+
result = subprocess.run(
|
|
39
|
+
[sys.executable, "-c", """
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
import os
|
|
42
|
+
from aicp import cli, gitflow, runner, skills
|
|
43
|
+
from aicp.contracts import ROSTER
|
|
44
|
+
agent = next(c for c in ROSTER if c.name == 'codex')
|
|
45
|
+
root = Path(os.environ['AICP_TEST_AGENT_HOME'])
|
|
46
|
+
root.mkdir(parents=True)
|
|
47
|
+
assert skills.config_root(agent) == root
|
|
48
|
+
assert gitflow.preflight(['codex'])[0]
|
|
49
|
+
cli._nudge(['codex'])
|
|
50
|
+
assert skills.missing_skills(agent) == ('commit', 'safe-git-push')
|
|
51
|
+
skills.install([agent])
|
|
52
|
+
target = root / 'custom-skills/commit/SKILL.md'
|
|
53
|
+
assert target.is_file()
|
|
54
|
+
assert skills.detect(agent, 'commit') == skills.CURRENT
|
|
55
|
+
assert skills.missing_skills(agent) == ()
|
|
56
|
+
assert skills.full_status([agent])[0].target == target
|
|
57
|
+
prompt = 'literal $HOME; {prompt} with spaces'
|
|
58
|
+
assert runner.invoke_argv('codex', prompt)[-1] == prompt
|
|
59
|
+
assert runner.run_step(prompt, ['codex']).rc == 0
|
|
60
|
+
"""],
|
|
61
|
+
cwd=tmp_path,
|
|
62
|
+
env={
|
|
63
|
+
**os.environ,
|
|
64
|
+
"PYTHONPATH": str(tmp_path),
|
|
65
|
+
"PATH": "",
|
|
66
|
+
"PYTHONUTF8": "1",
|
|
67
|
+
"AICP_TEST_AGENT_HOME": str(root),
|
|
68
|
+
},
|
|
69
|
+
capture_output=True,
|
|
70
|
+
text=True,
|
|
71
|
+
encoding="utf-8",
|
|
72
|
+
check=False,
|
|
73
|
+
)
|
|
74
|
+
assert result.returncode == 0, result.stdout + result.stderr
|
|
75
|
+
assert "missing the commit, safe-git-push skill" in result.stdout
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@pytest.mark.parametrize(
|
|
79
|
+
("field", "value"),
|
|
80
|
+
[
|
|
81
|
+
("args", ["-p"]),
|
|
82
|
+
("args", ["{prompt}", "{prompt}"]),
|
|
83
|
+
("args", "-p {prompt}"),
|
|
84
|
+
("args", ["{prompt}", 1]),
|
|
85
|
+
("executable", ""),
|
|
86
|
+
("config_dir", "relative/to/cwd"),
|
|
87
|
+
("skills_dir", "../outside"),
|
|
88
|
+
("skills_dir", "/outside"),
|
|
89
|
+
("skills_dir", "."),
|
|
90
|
+
("config_dir_env", ["INVALID"]),
|
|
91
|
+
],
|
|
92
|
+
)
|
|
93
|
+
def test_rejects_invalid_registry_fields(tmp_path, field, value):
|
|
94
|
+
data = json.loads(Path(aicp.__file__).with_name("agents.json").read_text())
|
|
95
|
+
data["agents"]["codex"][field] = value
|
|
96
|
+
path = tmp_path / "agents.json"
|
|
97
|
+
path.write_text(json.dumps(data))
|
|
98
|
+
with pytest.raises(ValueError, match=field):
|
|
99
|
+
load_agents(path)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_config_root_preserves_absolute_paths_and_live_env(tmp_path, monkeypatch):
|
|
103
|
+
from dataclasses import replace
|
|
104
|
+
|
|
105
|
+
from aicp.agents import AGENTS
|
|
106
|
+
|
|
107
|
+
agent = replace(AGENTS["grok"], config_dir=str(tmp_path / "absolute"))
|
|
108
|
+
monkeypatch.setenv("GROK_HOME", str(tmp_path / "override"))
|
|
109
|
+
assert agent.config_root() == tmp_path / "override"
|
|
110
|
+
assert agent.config_root(tmp_path / "home") == tmp_path / "absolute"
|
|
111
|
+
monkeypatch.delenv("GROK_HOME")
|
|
112
|
+
assert agent.config_root() == tmp_path / "absolute"
|
|
@@ -146,6 +146,16 @@ def test_cli_order_motion_runs_the_other_way_too():
|
|
|
146
146
|
assert _plain(frames[-1]) == " → ".join(after)
|
|
147
147
|
|
|
148
148
|
|
|
149
|
+
def test_selecting_a_different_row_does_not_resize_the_panel():
|
|
150
|
+
"""Rows carry help text of very different lengths — the box must be sized
|
|
151
|
+
for the longest one up front, not reflow as the cursor passes each row."""
|
|
152
|
+
state = MenuState(Path("menu.aicprc"), True, True, "en", list(ROSTER_NAMES))
|
|
153
|
+
|
|
154
|
+
widths = {width(_panel(state, selected=i)[0]) for i in range(1, len(ROWS) + 1)}
|
|
155
|
+
|
|
156
|
+
assert len(widths) == 1, "the frame width changed when a different row was selected"
|
|
157
|
+
|
|
158
|
+
|
|
149
159
|
@pytest.mark.parametrize("columns, lines", [(80, 24), (100, 24), (60, 24), (100, 14)])
|
|
150
160
|
def test_the_panel_fits_the_terminal_it_draws_on(columns, lines, monkeypatch):
|
|
151
161
|
"""Every line inside the terminal, in both languages.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|