agent-shell-py 0.2.0__tar.gz → 0.2.2__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.
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/AGENTS.md +16 -2
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/PKG-INFO +65 -3
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/README.md +64 -2
- agent_shell_py-0.2.2/docs/assets/skill_banner.png +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/docs/development/agent_parameter_comparison.md +54 -2
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/pyproject.toml +1 -1
- {agent_shell_py-0.2.0/agent-skills → agent_shell_py-0.2.2/skills}/invoking-cli-agents/SKILL.md +40 -4
- {agent_shell_py-0.2.0/agent-skills → agent_shell_py-0.2.2/skills}/invoking-cli-agents/api-reference.md +19 -1
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/_version.py +2 -2
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/adapters/agent_adapter_protocol.py +7 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/adapters/claude_code_adapter.py +88 -13
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/adapters/codex_adapter.py +55 -12
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/adapters/copilot_cli_adapter.py +159 -13
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/adapters/cursor_adapter.py +46 -13
- agent_shell_py-0.2.2/src/agent_shell/adapters/model_discovery.py +81 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/adapters/opencode_adapter.py +40 -15
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/adapters/pi_adapter.py +54 -13
- agent_shell_py-0.2.2/src/agent_shell/process_cleanup.py +158 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/shell.py +10 -0
- agent_shell_py-0.2.2/tests/conftest.py +20 -0
- agent_shell_py-0.2.2/tests/e2e/test_model_discovery_e2e.py +32 -0
- agent_shell_py-0.2.2/tests/integration/test_model_discovery_integration.py +519 -0
- agent_shell_py-0.2.2/tests/integration/test_process_lifecycle.py +660 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_adapter_transport.py +8 -19
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_cancel.py +1 -1
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_codex_cancel.py +1 -1
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_copilot_cli_cancel.py +1 -1
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_cursor_cancel.py +1 -1
- agent_shell_py-0.2.2/tests/unit/test_model_discovery.py +98 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_opencode_cancel.py +1 -1
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_pi_cancel.py +1 -1
- agent_shell_py-0.2.2/tests/unit/test_process_cleanup.py +201 -0
- agent_shell_py-0.2.2/tests/unit/test_process_group_registration.py +57 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_shell.py +28 -0
- agent_shell_py-0.2.0/src/agent_shell/process_cleanup.py +0 -213
- agent_shell_py-0.2.0/tests/conftest.py +0 -50
- agent_shell_py-0.2.0/tests/integration/test_process_lifecycle.py +0 -283
- agent_shell_py-0.2.0/tests/unit/test_process_cleanup.py +0 -596
- agent_shell_py-0.2.0/tests/unit/test_process_group_registration.py +0 -208
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/.github/workflows/build.yml +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/.github/workflows/ci.yml +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/.github/workflows/publish.yml +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/.gitignore +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/.python-version +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/LICENSE +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/docs/development/disabled_tools.md +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/docs/development/info.md +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/docs/development/total_token_count.md +0 -0
- {agent_shell_py-0.2.0/agent-skills → agent_shell_py-0.2.2/skills}/delegating-code-review/SKILL.md +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/__init__.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/adapters/__init__.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/adapters/health.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/adapters/outcome.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/adapters/response.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/adapters/stderr_format.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/adapters/tool_denial.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/models/__init__.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/models/agent.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/__init__.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/e2e/__init__.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/e2e/test_claude_code_e2e.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/e2e/test_codex_e2e.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/e2e/test_copilot_cli_e2e.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/e2e/test_cursor_e2e.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/e2e/test_health_check_e2e.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/e2e/test_opencode_e2e.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/e2e/test_pi_e2e.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/integration/__init__.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/integration/test_claude_code_integration.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/integration/test_claude_code_mcp_integration.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/integration/test_codex_integration.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/integration/test_codex_mcp_integration.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/integration/test_copilot_cli_integration.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/integration/test_copilot_cli_mcp_integration.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/integration/test_cursor_integration.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/integration/test_cursor_mcp_integration.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/integration/test_health_check_integration.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/integration/test_opencode_integration.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/integration/test_opencode_mcp_integration.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/integration/test_pi_integration.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/integration/test_pi_mcp_integration.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/__init__.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/adapter_matrix.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/codex_fixtures.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/copilot_fixtures.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/cursor_fixtures.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/fixtures.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/opencode_fixtures.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/pi_fixtures.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_codex_execute.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_codex_parse_event.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_codex_warnings.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_copilot_cli_execute.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_copilot_cli_parse_event.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_copilot_cli_stream.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_cursor_execute.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_cursor_parse_event.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_cursor_warnings.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_execute.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_execute_outcome.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_health_probe.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_mcp_server_spec.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_models.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_opencode_execute.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_opencode_parse_event.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_opencode_spawn.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_opencode_stream.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_parse_event.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_pi_execute.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_pi_parse_event.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_pi_warnings.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_response_aggregation.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_shell_cancellation.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_shell_mcp.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_stderr_format.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_stream.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_tool_denial.py +0 -0
- {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/uv.lock +0 -0
|
@@ -11,6 +11,7 @@ classDiagram
|
|
|
11
11
|
+execute(cwd, prompt, ...) AgentResponse
|
|
12
12
|
+stream(cwd, prompt, ...) AsyncIterator~StreamEvent~
|
|
13
13
|
+health_check(cwd, model, timeout) HealthCheckResult
|
|
14
|
+
+list_models(cwd, timeout) list~str~
|
|
14
15
|
+add_mcp_server(spec) None
|
|
15
16
|
+remove_mcp_server(name) None
|
|
16
17
|
+list_mcp_servers() list~MCPServerSpec~
|
|
@@ -22,6 +23,7 @@ classDiagram
|
|
|
22
23
|
+stream(cwd, prompt, ...) AsyncIterator~StreamEvent~
|
|
23
24
|
+cancel() None
|
|
24
25
|
+health_check(cwd, model, timeout) HealthCheckResult
|
|
26
|
+
+list_models(cwd, timeout) list~str~
|
|
25
27
|
+add_mcp_server(spec) None
|
|
26
28
|
+remove_mcp_server(name) None
|
|
27
29
|
+list_mcp_servers() list~MCPServerSpec~
|
|
@@ -32,6 +34,7 @@ classDiagram
|
|
|
32
34
|
+execute(cwd, prompt, ...) AgentResponse
|
|
33
35
|
+stream(cwd, prompt, ...) AsyncIterator~StreamEvent~
|
|
34
36
|
+cancel() None
|
|
37
|
+
+list_models(cwd, timeout) list~str~
|
|
35
38
|
+add_mcp_server(spec) None
|
|
36
39
|
+remove_mcp_server(name) None
|
|
37
40
|
+list_mcp_servers() list~MCPServerSpec~
|
|
@@ -125,6 +128,12 @@ stream; `adapters/health.py` wraps it for the health probe, and `adapters/respon
|
|
|
125
128
|
for `execute()`'s stream-to-`AgentResponse` collection (used by every adapter — each `execute()`
|
|
126
129
|
is a single delegating call into it).
|
|
127
130
|
|
|
131
|
+
`list_models(cwd, timeout)` asks the selected CLI for its current account/workspace-aware model
|
|
132
|
+
catalog and returns exact `list[str]` selectors that can be passed unchanged to `execute()` or
|
|
133
|
+
`stream()`. It sends no inference prompt, imports no harness SDK, invokes no separate refresh
|
|
134
|
+
command, and never substitutes a static catalog. "Available" means advertised as selectable; use
|
|
135
|
+
`health_check(model=...)` when actual execution must be proven.
|
|
136
|
+
|
|
128
137
|
## Supported Agents
|
|
129
138
|
|
|
130
139
|
- [x] Claude Code
|
|
@@ -172,9 +181,14 @@ Tests validate real functionality, not code coverage metrics. Three tiers, each
|
|
|
172
181
|
|------|-------|-----------|----------------|
|
|
173
182
|
| **Unit** | Isolated functions (`_parse_event`, adapter resolution, input validation) | Yes | No |
|
|
174
183
|
| **Integration** | Full flow through `AgentShell` -> `Adapter` -> parser with mocked subprocess | Yes | No |
|
|
175
|
-
| **E2E** | Real CLI
|
|
184
|
+
| **E2E** | Real CLI calls; usually real API costs | No (local only) | Yes |
|
|
185
|
+
|
|
186
|
+
The model-discovery E2E test is the exception: it calls all six real CLIs but only reads
|
|
187
|
+
metadata, so it sends no inference request and incurs no model-token cost.
|
|
176
188
|
|
|
177
|
-
Integration tests mirror the E2E tests
|
|
189
|
+
Integration tests mirror the E2E tests but substitute mocked subprocesses emitting captured CLI
|
|
190
|
+
output fixtures. This lets CI validate the full class interaction chain without credentials or API
|
|
191
|
+
spend. E2E tests remain local smoke tests for the real agents.
|
|
178
192
|
|
|
179
193
|
All tests follow the **AAA pattern** (Arrange, Act, Assert).
|
|
180
194
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-shell-py
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: A lightweight abstraction for executing CLI coding agents headlessly
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -13,8 +13,8 @@ and returning the output that can be used programatically as a unified contract
|
|
|
13
13
|
|
|
14
14
|
## Features
|
|
15
15
|
|
|
16
|
-
- **One unified contract** — the same `execute
|
|
17
|
-
agent; swap the backend without changing
|
|
16
|
+
- **One unified contract** — the same `execute`, `stream`, `health_check`, and
|
|
17
|
+
`list_models` API across every agent; swap the backend without changing consuming code.
|
|
18
18
|
- **Six CLI agents** — Claude Code, OpenCode, Copilot CLI, Codex, Pi, and Cursor behind a
|
|
19
19
|
common adapter protocol.
|
|
20
20
|
- **Execute or stream** — get one `AgentResponse` (raises `AgentExecutionError` on a failed run),
|
|
@@ -22,6 +22,8 @@ and returning the output that can be used programatically as a unified contract
|
|
|
22
22
|
- **Session resumption** — continue any conversation by passing back its `session_id`.
|
|
23
23
|
- **Normalized cost & tokens** — consistent `cost` and `output_tokens` (reasoning included)
|
|
24
24
|
regardless of how each CLI reports them.
|
|
25
|
+
- **Model discovery** — retrieve the exact account/workspace-aware model strings accepted by
|
|
26
|
+
each CLI, without inference calls, SDK dependencies, or static catalogs.
|
|
25
27
|
- **Health checks** — confirm an agent + model combination actually works before you rely on
|
|
26
28
|
it, read from the event stream rather than unreliable exit codes.
|
|
27
29
|
- **Portable tool control** — one canonical allow/deny vocabulary
|
|
@@ -42,6 +44,39 @@ or with pip:
|
|
|
42
44
|
pip install agent-shell-py
|
|
43
45
|
```
|
|
44
46
|
|
|
47
|
+
## Agent skills
|
|
48
|
+
|
|
49
|
+

|
|
50
|
+
|
|
51
|
+
The repository includes reusable skills that teach coding agents how to use AgentShell:
|
|
52
|
+
|
|
53
|
+
- `invoking-cli-agents` — invoke, stream, resume, and restrict CLI agents.
|
|
54
|
+
- `delegating-code-review` — delegate an independent code review through AgentShell.
|
|
55
|
+
|
|
56
|
+
Install them interactively with the Vercel Skills CLI:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npx skills add ScottRBK/agent-shell
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Or install both skills globally for every coding agent supported by AgentShell:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npx skills add ScottRBK/agent-shell --global \
|
|
66
|
+
--skill '*' \
|
|
67
|
+
--agent claude-code opencode github-copilot codex pi cursor \
|
|
68
|
+
--yes
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Install only the core AgentShell skill with:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npx skills add ScottRBK/agent-shell --skill invoking-cli-agents
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The skills provide agent instructions. Install `agent-shell-py` and the chosen coding-agent CLIs
|
|
78
|
+
separately.
|
|
79
|
+
|
|
45
80
|
## Examples
|
|
46
81
|
|
|
47
82
|
### Execute
|
|
@@ -115,6 +150,33 @@ async for event in shell.stream(
|
|
|
115
150
|
print(f"[{event.type}] {event.content}")
|
|
116
151
|
```
|
|
117
152
|
|
|
153
|
+
### Model discovery
|
|
154
|
+
|
|
155
|
+
Ask the selected CLI which model strings it currently advertises, then pass one back
|
|
156
|
+
unchanged. Discovery sends no inference prompt and has no model-token cost.
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
shell = AgentShell(agent_type=AgentType.CLAUDE_CODE)
|
|
160
|
+
|
|
161
|
+
models = await shell.list_models(cwd="/path/to/project")
|
|
162
|
+
selected_model = models[0]
|
|
163
|
+
|
|
164
|
+
response = await shell.execute(
|
|
165
|
+
cwd="/path/to/project",
|
|
166
|
+
prompt="Review this project",
|
|
167
|
+
model=selected_model,
|
|
168
|
+
)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
"Available" means advertised as selectable for the current harness, account, and workspace.
|
|
172
|
+
It does not prove quota, entitlement, credentials, or provider health. The harness's order and
|
|
173
|
+
aliases such as `auto` and `default` are preserved. A genuine empty catalog returns `[]`;
|
|
174
|
+
discovery failures are raised instead of being mistaken for an empty catalog.
|
|
175
|
+
|
|
176
|
+
See the
|
|
177
|
+
[agent parameter comparison](docs/development/agent_parameter_comparison.md#model-discovery)
|
|
178
|
+
for each harness's underlying discovery mechanism.
|
|
179
|
+
|
|
118
180
|
### Health check
|
|
119
181
|
|
|
120
182
|
Verify an agent + model combination actually works before relying on it. It sends a
|
|
@@ -4,8 +4,8 @@ and returning the output that can be used programatically as a unified contract
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **One unified contract** — the same `execute
|
|
8
|
-
agent; swap the backend without changing
|
|
7
|
+
- **One unified contract** — the same `execute`, `stream`, `health_check`, and
|
|
8
|
+
`list_models` API across every agent; swap the backend without changing consuming code.
|
|
9
9
|
- **Six CLI agents** — Claude Code, OpenCode, Copilot CLI, Codex, Pi, and Cursor behind a
|
|
10
10
|
common adapter protocol.
|
|
11
11
|
- **Execute or stream** — get one `AgentResponse` (raises `AgentExecutionError` on a failed run),
|
|
@@ -13,6 +13,8 @@ and returning the output that can be used programatically as a unified contract
|
|
|
13
13
|
- **Session resumption** — continue any conversation by passing back its `session_id`.
|
|
14
14
|
- **Normalized cost & tokens** — consistent `cost` and `output_tokens` (reasoning included)
|
|
15
15
|
regardless of how each CLI reports them.
|
|
16
|
+
- **Model discovery** — retrieve the exact account/workspace-aware model strings accepted by
|
|
17
|
+
each CLI, without inference calls, SDK dependencies, or static catalogs.
|
|
16
18
|
- **Health checks** — confirm an agent + model combination actually works before you rely on
|
|
17
19
|
it, read from the event stream rather than unreliable exit codes.
|
|
18
20
|
- **Portable tool control** — one canonical allow/deny vocabulary
|
|
@@ -33,6 +35,39 @@ or with pip:
|
|
|
33
35
|
pip install agent-shell-py
|
|
34
36
|
```
|
|
35
37
|
|
|
38
|
+
## Agent skills
|
|
39
|
+
|
|
40
|
+

|
|
41
|
+
|
|
42
|
+
The repository includes reusable skills that teach coding agents how to use AgentShell:
|
|
43
|
+
|
|
44
|
+
- `invoking-cli-agents` — invoke, stream, resume, and restrict CLI agents.
|
|
45
|
+
- `delegating-code-review` — delegate an independent code review through AgentShell.
|
|
46
|
+
|
|
47
|
+
Install them interactively with the Vercel Skills CLI:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx skills add ScottRBK/agent-shell
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Or install both skills globally for every coding agent supported by AgentShell:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npx skills add ScottRBK/agent-shell --global \
|
|
57
|
+
--skill '*' \
|
|
58
|
+
--agent claude-code opencode github-copilot codex pi cursor \
|
|
59
|
+
--yes
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Install only the core AgentShell skill with:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npx skills add ScottRBK/agent-shell --skill invoking-cli-agents
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The skills provide agent instructions. Install `agent-shell-py` and the chosen coding-agent CLIs
|
|
69
|
+
separately.
|
|
70
|
+
|
|
36
71
|
## Examples
|
|
37
72
|
|
|
38
73
|
### Execute
|
|
@@ -106,6 +141,33 @@ async for event in shell.stream(
|
|
|
106
141
|
print(f"[{event.type}] {event.content}")
|
|
107
142
|
```
|
|
108
143
|
|
|
144
|
+
### Model discovery
|
|
145
|
+
|
|
146
|
+
Ask the selected CLI which model strings it currently advertises, then pass one back
|
|
147
|
+
unchanged. Discovery sends no inference prompt and has no model-token cost.
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
shell = AgentShell(agent_type=AgentType.CLAUDE_CODE)
|
|
151
|
+
|
|
152
|
+
models = await shell.list_models(cwd="/path/to/project")
|
|
153
|
+
selected_model = models[0]
|
|
154
|
+
|
|
155
|
+
response = await shell.execute(
|
|
156
|
+
cwd="/path/to/project",
|
|
157
|
+
prompt="Review this project",
|
|
158
|
+
model=selected_model,
|
|
159
|
+
)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
"Available" means advertised as selectable for the current harness, account, and workspace.
|
|
163
|
+
It does not prove quota, entitlement, credentials, or provider health. The harness's order and
|
|
164
|
+
aliases such as `auto` and `default` are preserved. A genuine empty catalog returns `[]`;
|
|
165
|
+
discovery failures are raised instead of being mistaken for an empty catalog.
|
|
166
|
+
|
|
167
|
+
See the
|
|
168
|
+
[agent parameter comparison](docs/development/agent_parameter_comparison.md#model-discovery)
|
|
169
|
+
for each harness's underlying discovery mechanism.
|
|
170
|
+
|
|
109
171
|
### Health check
|
|
110
172
|
|
|
111
173
|
Verify an agent + model combination actually works before relying on it. It sends a
|
|
Binary file
|
{agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/docs/development/agent_parameter_comparison.md
RENAMED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Agent CLI Parameter Comparison
|
|
2
2
|
|
|
3
3
|
Comparison of headless/non-interactive configuration across supported CLI coding agents.
|
|
4
|
-
Last updated: 2026-
|
|
4
|
+
Last updated: 2026-08-02
|
|
5
5
|
|
|
6
6
|
> The summary matrix below has no Pi or Cursor column (it predates both adapters); see the
|
|
7
|
-
> per-agent detail sections and the `disallowed_tools` table
|
|
7
|
+
> per-agent detail sections, the model-discovery section, and the `disallowed_tools` table.
|
|
8
8
|
|
|
9
9
|
> Every "measured 2026-07-26" claim below comes from a real three-call run per agent (first
|
|
10
10
|
> turn, resume on the returned id, then a session-less turn), recorded by the resume e2e tests
|
|
@@ -26,6 +26,58 @@ Last updated: 2026-07-26
|
|
|
26
26
|
| **Auto-approve** | `--dangerously-skip-permissions` | `--yolo` | `--yolo` / `--allow-all` | Auto in `run` mode |
|
|
27
27
|
| **Session resume** | `--resume` | `exec resume <id>` | `--resume` | `-s <id>` |
|
|
28
28
|
|
|
29
|
+
## Model discovery
|
|
30
|
+
|
|
31
|
+
Every adapter exposes the same account/workspace-aware API:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
shell = AgentShell(agent_type=AgentType.CLAUDE_CODE)
|
|
35
|
+
|
|
36
|
+
models = await shell.list_models(cwd="/path/to/project")
|
|
37
|
+
selected_model = models[0]
|
|
38
|
+
|
|
39
|
+
response = await shell.execute(
|
|
40
|
+
cwd="/path/to/project",
|
|
41
|
+
prompt="Do the work",
|
|
42
|
+
model=selected_model,
|
|
43
|
+
)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`list_models()` returns `list[str]`. Each string is the exact selector accepted by that
|
|
47
|
+
same adapter's `execute(model=...)` and `stream(model=...)`; callers do not translate it.
|
|
48
|
+
Discovery reads CLI metadata and makes no inference call.
|
|
49
|
+
|
|
50
|
+
| Harness | Discovery mechanism | Returned selector |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| Claude Code | stream-JSON `initialize` control request | `models[].value` |
|
|
53
|
+
| OpenCode | `opencode models` | each complete output line |
|
|
54
|
+
| Copilot CLI | headless JSON-RPC `models.list` | `models[].id` |
|
|
55
|
+
| Codex | `codex debug models` | visible model `slug` |
|
|
56
|
+
| Pi | `pi --no-approve --list-models` | `provider/model` |
|
|
57
|
+
| Cursor | `cursor-agent models` | ID before the first ` - ` |
|
|
58
|
+
|
|
59
|
+
Important semantics:
|
|
60
|
+
|
|
61
|
+
- "Available" means advertised as selectable for the current harness, account, and
|
|
62
|
+
workspace. It does not guarantee current quota, entitlement, credentials, or provider
|
|
63
|
+
health. Use `health_check(model=...)` when actual execution must be proven.
|
|
64
|
+
- Claude and Copilot are invoked directly as CLIs. Their SDK documentation was protocol
|
|
65
|
+
evidence only; AgentShell imports no harness SDKs and adds no runtime dependency.
|
|
66
|
+
- Claude runs in print mode with `--no-session-persistence`, so discovery is not saved as a
|
|
67
|
+
resumable session.
|
|
68
|
+
- Codex filters the refreshed JSON catalog to `visibility == "list"`. Its documented
|
|
69
|
+
`debug models` command is experimental, so integration and local E2E tests guard drift.
|
|
70
|
+
- Pi model IDs are provider-qualified because IDs can repeat across providers. Discovery
|
|
71
|
+
does not run the mutating `pi update --models` command.
|
|
72
|
+
- The harness's order and aliases such as `auto` or `default` are preserved.
|
|
73
|
+
- A genuine empty catalog returns `[]`. Authentication, timeout, non-zero exit, and
|
|
74
|
+
malformed protocol/output failures are surfaced with actionable errors.
|
|
75
|
+
- AgentShell does not cache or replace results with a static fallback catalog.
|
|
76
|
+
|
|
77
|
+
The behavior was verified against all six installed CLIs on 2026-08-02 without inference
|
|
78
|
+
spend. CI covers captured real outputs through `AgentShell`; a local E2E test checks all six
|
|
79
|
+
real discovery commands.
|
|
80
|
+
|
|
29
81
|
## Claude Code
|
|
30
82
|
|
|
31
83
|
- **Headless mode**: `-p` / `--print`
|
|
@@ -23,7 +23,7 @@ packages = ["src/agent_shell"]
|
|
|
23
23
|
[tool.pytest.ini_options]
|
|
24
24
|
asyncio_mode = "auto"
|
|
25
25
|
markers = [
|
|
26
|
-
"e2e:
|
|
26
|
+
"e2e: real CLI tests (may require credentials and incur API costs)",
|
|
27
27
|
]
|
|
28
28
|
|
|
29
29
|
[dependency-groups]
|
{agent_shell_py-0.2.0/agent-skills → agent_shell_py-0.2.2/skills}/invoking-cli-agents/SKILL.md
RENAMED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: invoking-cli-agents
|
|
3
|
-
description:
|
|
3
|
+
description: >-
|
|
4
|
+
Use when invoking a CLI coding agent from Python, delegating to a sub-agent, discovering
|
|
5
|
+
available model strings, orchestrating agents, streaming output, resuming sessions,
|
|
6
|
+
restricting tools, or checking agent/model health. Supports Claude Code, OpenCode,
|
|
7
|
+
Copilot CLI, Codex, Pi, and Cursor. Keywords: AgentShell, list_models, headless agent,
|
|
8
|
+
model discovery, subprocess, allowed_tools, disallowed_tools, session_id, cost,
|
|
9
|
+
output_tokens.
|
|
4
10
|
---
|
|
5
11
|
|
|
6
12
|
# Invoking CLI Agents with AgentShell
|
|
@@ -20,6 +26,7 @@ passes `model` strings through verbatim and does not manage credentials.
|
|
|
20
26
|
- You need to orchestrate multi-step workflows across agents
|
|
21
27
|
- You want to stream agent output in real-time
|
|
22
28
|
- You need to restrict what tools a delegated agent can run
|
|
29
|
+
- You need the exact model strings currently advertised by a selected CLI
|
|
23
30
|
- You want to check whether an agent/model combination works before relying on it
|
|
24
31
|
|
|
25
32
|
## When NOT to Use
|
|
@@ -36,8 +43,32 @@ uv add agent-shell-py
|
|
|
36
43
|
## Core Concepts
|
|
37
44
|
|
|
38
45
|
AgentShell has two invocation methods — `execute()` collects a complete response, `stream()`
|
|
39
|
-
yields events in real-time — plus helpers for health checks and MCP server
|
|
40
|
-
are async.
|
|
46
|
+
yields events in real-time — plus helpers for model discovery, health checks, and MCP server
|
|
47
|
+
management. All are async.
|
|
48
|
+
|
|
49
|
+
### Discover Available Model Strings
|
|
50
|
+
|
|
51
|
+
Use `list_models()` before selecting a model dynamically. It returns exact strings that the
|
|
52
|
+
same shell accepts through `execute(model=...)` and `stream(model=...)`.
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
shell = AgentShell(agent_type=AgentType.CLAUDE_CODE)
|
|
56
|
+
|
|
57
|
+
models = await shell.list_models(cwd="/path/to/project")
|
|
58
|
+
selected_model = models[0]
|
|
59
|
+
|
|
60
|
+
response = await shell.execute(
|
|
61
|
+
cwd="/path/to/project",
|
|
62
|
+
prompt="Review this project",
|
|
63
|
+
model=selected_model,
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Discovery reads CLI metadata. It sends no inference prompt and incurs no model-token cost.
|
|
68
|
+
"Available" means the CLI advertises the selector for the current account/workspace; it does
|
|
69
|
+
not prove quota or entitlement. Use `health_check(model=selected_model)` for that stronger
|
|
70
|
+
inference check, which may incur cost. Preserve the returned string unchanged, including
|
|
71
|
+
such as `auto` or `default`.
|
|
41
72
|
|
|
42
73
|
### Execute: Run and Collect
|
|
43
74
|
|
|
@@ -276,6 +307,8 @@ succeeded = saw_ok and error is None # absent result => succeeded stays
|
|
|
276
307
|
|
|
277
308
|
## Other Capabilities
|
|
278
309
|
|
|
310
|
+
- **Model discovery** — `await shell.list_models(cwd)` returns exact selectable strings.
|
|
311
|
+
A genuine empty catalog returns `[]`; discovery failures are raised.
|
|
279
312
|
- **Health check** — `await shell.health_check(cwd, model=...)` returns a `HealthCheckResult`.
|
|
280
313
|
It sends its *own* trivial no-tool prompt to confirm the agent/model completes a turn — it does
|
|
281
314
|
**not** run your prompt, so use the stream-based check above when you care about a specific
|
|
@@ -305,7 +338,8 @@ logging.getLogger("agent_shell").addHandler(logging.StreamHandler())
|
|
|
305
338
|
| Whitelist tools (Claude/Copilot/Pi) | `allowed_tools=["Read", "Glob"]` |
|
|
306
339
|
| Deny tools (Claude/OpenCode/Copilot/Pi) | `disallowed_tools=["edit", "bash"]` |
|
|
307
340
|
| Track usage | Read `response.output_tokens` (portable) or `response.cost` |
|
|
308
|
-
|
|
|
341
|
+
| Discover selectable models | `await shell.list_models(cwd)` |
|
|
342
|
+
| Use a specific model | Pass one returned string as `model=...` |
|
|
309
343
|
| Increase reasoning depth | `effort="high"` (not OpenCode or Cursor) |
|
|
310
344
|
| See agent thinking | `include_thinking=True` in `stream()` |
|
|
311
345
|
| Check an agent/model works | `await shell.health_check(cwd, model=...)` |
|
|
@@ -316,6 +350,8 @@ logging.getLogger("agent_shell").addHandler(logging.StreamHandler())
|
|
|
316
350
|
| Mistake | Fix |
|
|
317
351
|
|---------|-----|
|
|
318
352
|
| Passing a non-existent `cwd` | Validate the path exists first (else `ValueError`) |
|
|
353
|
+
| Hard-coding a public model catalog | Use `await shell.list_models(cwd)` |
|
|
354
|
+
| Altering a discovered model string | Pass it back unchanged as `model=...` |
|
|
319
355
|
| Forgetting `await` | Both `execute()` and the `stream()` iterator are async |
|
|
320
356
|
| `allowed_tools=[]` to disable tools | Empty list is falsy → full access. Use a non-empty list or `disallowed_tools` |
|
|
321
357
|
| `allowed_tools` with the default `auto_approve=True` as a safety boundary | `--dangerously-skip-permissions` bypasses it. Set `auto_approve=False`, or use `disallowed_tools` |
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
- [Models](#models) — `AgentType`, `AgentResponse`, `AgentExecutionError`, `StreamEvent`,
|
|
4
4
|
`MCPServerSpec`, `HealthCheckResult`
|
|
5
5
|
- [StreamEvent types](#event-types)
|
|
6
|
-
- [AgentShell class](#agentshell-class) —
|
|
6
|
+
- [AgentShell class](#agentshell-class) — invocation, model discovery, health, MCP management
|
|
7
7
|
- [AgentAdapter protocol](#agentadapter-protocol)
|
|
8
8
|
- [Agent-specific notes](#agent-specific-notes)
|
|
9
9
|
|
|
@@ -163,11 +163,28 @@ class AgentShell:
|
|
|
163
163
|
) -> HealthCheckResult: ...
|
|
164
164
|
# Sends a trivial no-tool prompt; healthy iff the LAST result event is "ok" and no error.
|
|
165
165
|
|
|
166
|
+
async def list_models(
|
|
167
|
+
self, cwd: str, timeout: float = 30.0,
|
|
168
|
+
) -> list[str]: ...
|
|
169
|
+
# Returns exact selectors accepted by execute(model=...) and stream(model=...).
|
|
170
|
+
|
|
166
171
|
async def add_mcp_server(self, mcp_server: MCPServerSpec) -> None: ...
|
|
167
172
|
async def remove_mcp_server(self, mcp_server_name: str) -> None: ...
|
|
168
173
|
async def list_mcp_servers(self) -> list[MCPServerSpec]: ...
|
|
169
174
|
```
|
|
170
175
|
|
|
176
|
+
### Model discovery semantics
|
|
177
|
+
|
|
178
|
+
`list_models()` reads the selected CLI's account/workspace-aware catalog without sending an
|
|
179
|
+
inference prompt. It preserves harness order and aliases such as `auto` and `default`. Pi
|
|
180
|
+
selectors are provider-qualified (`provider/model`) because model IDs can repeat across
|
|
181
|
+
providers.
|
|
182
|
+
|
|
183
|
+
A returned string is advertised as selectable, not guaranteed runnable at that moment. Use
|
|
184
|
+
`health_check(model=...)` to validate credentials, entitlement, quota, and provider health.
|
|
185
|
+
A genuine empty catalog returns `[]`; timeout, authentication, CLI, and malformed-output
|
|
186
|
+
failures are raised. AgentShell does not cache or substitute a static catalog.
|
|
187
|
+
|
|
171
188
|
### disallowed_tools canonical vocabulary
|
|
172
189
|
|
|
173
190
|
`disallowed_tools` accepts these canonical names; each adapter maps them to native deny
|
|
@@ -198,6 +215,7 @@ class AgentAdapter(Protocol):
|
|
|
198
215
|
async def cancel(self) -> None: ...
|
|
199
216
|
|
|
200
217
|
async def health_check(self, cwd, model=None, timeout=60.0) -> HealthCheckResult: ...
|
|
218
|
+
async def list_models(self, cwd, timeout=30.0) -> list[str]: ...
|
|
201
219
|
async def add_mcp_server(self, mcp_server: MCPServerSpec) -> None: ...
|
|
202
220
|
async def remove_mcp_server(self, mcp_server_name: str) -> None: ...
|
|
203
221
|
async def list_mcp_servers(self) -> list[MCPServerSpec]: ...
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.2.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 2,
|
|
21
|
+
__version__ = version = '0.2.2'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 2)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
{agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/adapters/agent_adapter_protocol.py
RENAMED
|
@@ -48,6 +48,13 @@ class AgentAdapter(Protocol):
|
|
|
48
48
|
) -> HealthCheckResult:
|
|
49
49
|
...
|
|
50
50
|
|
|
51
|
+
async def list_models(
|
|
52
|
+
self,
|
|
53
|
+
cwd: str,
|
|
54
|
+
timeout: float = 30.0,
|
|
55
|
+
) -> list[str]:
|
|
56
|
+
...
|
|
57
|
+
|
|
51
58
|
async def add_mcp_server(self, mcp_server: MCPServerSpec) -> None:
|
|
52
59
|
...
|
|
53
60
|
|
{agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/adapters/claude_code_adapter.py
RENAMED
|
@@ -7,10 +7,20 @@ import warnings
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import AsyncIterator
|
|
9
9
|
|
|
10
|
-
from agent_shell.models.agent import
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
from agent_shell.models.agent import (
|
|
11
|
+
AgentResponse,
|
|
12
|
+
HealthCheckResult,
|
|
13
|
+
MCPServerSpec,
|
|
14
|
+
MCPServerType,
|
|
15
|
+
StreamEvent,
|
|
16
|
+
)
|
|
17
|
+
from agent_shell.process_cleanup import (
|
|
18
|
+
create_grouped_process,
|
|
19
|
+
kill_process_group,
|
|
20
|
+
release_process,
|
|
21
|
+
)
|
|
13
22
|
from agent_shell.adapters.health import run_health_probe
|
|
23
|
+
from agent_shell.adapters.model_discovery import decode_model_output, run_model_command
|
|
14
24
|
from agent_shell.adapters.response import collect_response
|
|
15
25
|
from agent_shell.adapters.stderr_format import format_stderr
|
|
16
26
|
from agent_shell.adapters.tool_denial import resolve_disallowed_tools
|
|
@@ -104,18 +114,12 @@ class ClaudeCodeAdapter():
|
|
|
104
114
|
logger.debug("Command: %s", cmd)
|
|
105
115
|
logger.info("Process started (cwd=%s)", os.path.abspath(cwd))
|
|
106
116
|
|
|
107
|
-
process = await
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
stdout=asyncio.subprocess.PIPE,
|
|
111
|
-
stderr=asyncio.subprocess.PIPE,
|
|
112
|
-
cwd=os.path.abspath(cwd),
|
|
113
|
-
preexec_fn=os.setsid,
|
|
117
|
+
process = await create_grouped_process(
|
|
118
|
+
cmd,
|
|
119
|
+
cwd=os.path.abspath(cwd),
|
|
114
120
|
)
|
|
115
121
|
|
|
116
122
|
self._active_processes.append(process)
|
|
117
|
-
# setsid makes the child a session leader, so pgid == pid
|
|
118
|
-
register_process_group(process.pid)
|
|
119
123
|
|
|
120
124
|
# Drain stderr concurrently with stdout. Reading it only after the stdout loop can
|
|
121
125
|
# deadlock: a child that fills its stderr pipe buffer (~64KB) mid-run blocks on that
|
|
@@ -226,7 +230,7 @@ class ClaudeCodeAdapter():
|
|
|
226
230
|
|
|
227
231
|
async def cancel(self) -> None:
|
|
228
232
|
for process in self._active_processes:
|
|
229
|
-
kill_process_group(process
|
|
233
|
+
kill_process_group(process)
|
|
230
234
|
self._active_processes.clear()
|
|
231
235
|
|
|
232
236
|
async def health_check(
|
|
@@ -237,6 +241,77 @@ class ClaudeCodeAdapter():
|
|
|
237
241
|
) -> HealthCheckResult:
|
|
238
242
|
return await run_health_probe(self, cwd, model=model, timeout=timeout)
|
|
239
243
|
|
|
244
|
+
async def list_models(
|
|
245
|
+
self,
|
|
246
|
+
cwd: str,
|
|
247
|
+
timeout: float = 30.0,
|
|
248
|
+
) -> list[str]:
|
|
249
|
+
cmd = [
|
|
250
|
+
"claude",
|
|
251
|
+
"--print",
|
|
252
|
+
"--no-session-persistence",
|
|
253
|
+
"--output-format", "stream-json",
|
|
254
|
+
"--verbose",
|
|
255
|
+
"--input-format", "stream-json",
|
|
256
|
+
"--tools", "",
|
|
257
|
+
"--permission-mode", "dontAsk",
|
|
258
|
+
]
|
|
259
|
+
request = {
|
|
260
|
+
"request_id": "agent-shell-models",
|
|
261
|
+
"type": "control_request",
|
|
262
|
+
"request": {"subtype": "initialize"},
|
|
263
|
+
}
|
|
264
|
+
input_data = (json.dumps(request) + "\n").encode("utf-8")
|
|
265
|
+
returncode, stdout, stderr = await run_model_command(
|
|
266
|
+
cmd,
|
|
267
|
+
cwd,
|
|
268
|
+
timeout,
|
|
269
|
+
input_data=input_data,
|
|
270
|
+
)
|
|
271
|
+
if returncode != 0:
|
|
272
|
+
message = format_stderr(stderr) or f"exit code {returncode}"
|
|
273
|
+
raise RuntimeError(f"Claude model discovery failed: {message}")
|
|
274
|
+
|
|
275
|
+
output = decode_model_output(stdout, "Claude model discovery")
|
|
276
|
+
for line in output.splitlines():
|
|
277
|
+
if not line:
|
|
278
|
+
continue
|
|
279
|
+
try:
|
|
280
|
+
event = json.loads(line)
|
|
281
|
+
except json.JSONDecodeError as error:
|
|
282
|
+
raise RuntimeError(
|
|
283
|
+
"Claude model discovery returned invalid JSON"
|
|
284
|
+
) from error
|
|
285
|
+
if not isinstance(event, dict):
|
|
286
|
+
raise RuntimeError("Claude model discovery returned invalid JSON")
|
|
287
|
+
if event.get("type") != "control_response":
|
|
288
|
+
continue
|
|
289
|
+
|
|
290
|
+
response = event.get("response")
|
|
291
|
+
if not isinstance(response, dict):
|
|
292
|
+
raise RuntimeError("Claude model discovery returned an invalid response")
|
|
293
|
+
if response.get("request_id") != "agent-shell-models":
|
|
294
|
+
continue
|
|
295
|
+
if response.get("subtype") != "success":
|
|
296
|
+
raise RuntimeError("Claude model discovery request failed")
|
|
297
|
+
|
|
298
|
+
payload = response.get("response")
|
|
299
|
+
models = payload.get("models", []) if isinstance(payload, dict) else None
|
|
300
|
+
if not isinstance(models, list):
|
|
301
|
+
raise RuntimeError("Claude model discovery returned no model list")
|
|
302
|
+
|
|
303
|
+
model_values: list[str] = []
|
|
304
|
+
for model in models:
|
|
305
|
+
value = model.get("value") if isinstance(model, dict) else None
|
|
306
|
+
if not isinstance(value, str) or not value:
|
|
307
|
+
raise RuntimeError(
|
|
308
|
+
"Claude model discovery returned an invalid model entry"
|
|
309
|
+
)
|
|
310
|
+
model_values.append(value)
|
|
311
|
+
return model_values
|
|
312
|
+
|
|
313
|
+
raise RuntimeError("Claude model discovery returned no initialization response")
|
|
314
|
+
|
|
240
315
|
async def add_mcp_server(self, mcp_server: MCPServerSpec) -> None:
|
|
241
316
|
# Pre-remove for overwrite semantics; ignore failure (server may not exist).
|
|
242
317
|
await self._run_mcp_command(
|