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.
Files changed (118) hide show
  1. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/AGENTS.md +16 -2
  2. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/PKG-INFO +65 -3
  3. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/README.md +64 -2
  4. agent_shell_py-0.2.2/docs/assets/skill_banner.png +0 -0
  5. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/docs/development/agent_parameter_comparison.md +54 -2
  6. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/pyproject.toml +1 -1
  7. {agent_shell_py-0.2.0/agent-skills → agent_shell_py-0.2.2/skills}/invoking-cli-agents/SKILL.md +40 -4
  8. {agent_shell_py-0.2.0/agent-skills → agent_shell_py-0.2.2/skills}/invoking-cli-agents/api-reference.md +19 -1
  9. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/_version.py +2 -2
  10. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/adapters/agent_adapter_protocol.py +7 -0
  11. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/adapters/claude_code_adapter.py +88 -13
  12. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/adapters/codex_adapter.py +55 -12
  13. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/adapters/copilot_cli_adapter.py +159 -13
  14. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/adapters/cursor_adapter.py +46 -13
  15. agent_shell_py-0.2.2/src/agent_shell/adapters/model_discovery.py +81 -0
  16. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/adapters/opencode_adapter.py +40 -15
  17. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/adapters/pi_adapter.py +54 -13
  18. agent_shell_py-0.2.2/src/agent_shell/process_cleanup.py +158 -0
  19. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/shell.py +10 -0
  20. agent_shell_py-0.2.2/tests/conftest.py +20 -0
  21. agent_shell_py-0.2.2/tests/e2e/test_model_discovery_e2e.py +32 -0
  22. agent_shell_py-0.2.2/tests/integration/test_model_discovery_integration.py +519 -0
  23. agent_shell_py-0.2.2/tests/integration/test_process_lifecycle.py +660 -0
  24. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_adapter_transport.py +8 -19
  25. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_cancel.py +1 -1
  26. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_codex_cancel.py +1 -1
  27. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_copilot_cli_cancel.py +1 -1
  28. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_cursor_cancel.py +1 -1
  29. agent_shell_py-0.2.2/tests/unit/test_model_discovery.py +98 -0
  30. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_opencode_cancel.py +1 -1
  31. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_pi_cancel.py +1 -1
  32. agent_shell_py-0.2.2/tests/unit/test_process_cleanup.py +201 -0
  33. agent_shell_py-0.2.2/tests/unit/test_process_group_registration.py +57 -0
  34. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_shell.py +28 -0
  35. agent_shell_py-0.2.0/src/agent_shell/process_cleanup.py +0 -213
  36. agent_shell_py-0.2.0/tests/conftest.py +0 -50
  37. agent_shell_py-0.2.0/tests/integration/test_process_lifecycle.py +0 -283
  38. agent_shell_py-0.2.0/tests/unit/test_process_cleanup.py +0 -596
  39. agent_shell_py-0.2.0/tests/unit/test_process_group_registration.py +0 -208
  40. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/.github/workflows/build.yml +0 -0
  41. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/.github/workflows/ci.yml +0 -0
  42. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/.github/workflows/publish.yml +0 -0
  43. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/.gitignore +0 -0
  44. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/.python-version +0 -0
  45. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/LICENSE +0 -0
  46. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/docs/development/disabled_tools.md +0 -0
  47. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/docs/development/info.md +0 -0
  48. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/docs/development/total_token_count.md +0 -0
  49. {agent_shell_py-0.2.0/agent-skills → agent_shell_py-0.2.2/skills}/delegating-code-review/SKILL.md +0 -0
  50. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/__init__.py +0 -0
  51. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/adapters/__init__.py +0 -0
  52. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/adapters/health.py +0 -0
  53. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/adapters/outcome.py +0 -0
  54. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/adapters/response.py +0 -0
  55. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/adapters/stderr_format.py +0 -0
  56. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/adapters/tool_denial.py +0 -0
  57. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/models/__init__.py +0 -0
  58. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/src/agent_shell/models/agent.py +0 -0
  59. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/__init__.py +0 -0
  60. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/e2e/__init__.py +0 -0
  61. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/e2e/test_claude_code_e2e.py +0 -0
  62. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/e2e/test_codex_e2e.py +0 -0
  63. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/e2e/test_copilot_cli_e2e.py +0 -0
  64. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/e2e/test_cursor_e2e.py +0 -0
  65. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/e2e/test_health_check_e2e.py +0 -0
  66. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/e2e/test_opencode_e2e.py +0 -0
  67. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/e2e/test_pi_e2e.py +0 -0
  68. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/integration/__init__.py +0 -0
  69. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/integration/test_claude_code_integration.py +0 -0
  70. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/integration/test_claude_code_mcp_integration.py +0 -0
  71. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/integration/test_codex_integration.py +0 -0
  72. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/integration/test_codex_mcp_integration.py +0 -0
  73. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/integration/test_copilot_cli_integration.py +0 -0
  74. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/integration/test_copilot_cli_mcp_integration.py +0 -0
  75. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/integration/test_cursor_integration.py +0 -0
  76. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/integration/test_cursor_mcp_integration.py +0 -0
  77. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/integration/test_health_check_integration.py +0 -0
  78. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/integration/test_opencode_integration.py +0 -0
  79. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/integration/test_opencode_mcp_integration.py +0 -0
  80. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/integration/test_pi_integration.py +0 -0
  81. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/integration/test_pi_mcp_integration.py +0 -0
  82. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/__init__.py +0 -0
  83. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/adapter_matrix.py +0 -0
  84. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/codex_fixtures.py +0 -0
  85. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/copilot_fixtures.py +0 -0
  86. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/cursor_fixtures.py +0 -0
  87. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/fixtures.py +0 -0
  88. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/opencode_fixtures.py +0 -0
  89. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/pi_fixtures.py +0 -0
  90. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_codex_execute.py +0 -0
  91. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_codex_parse_event.py +0 -0
  92. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_codex_warnings.py +0 -0
  93. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_copilot_cli_execute.py +0 -0
  94. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_copilot_cli_parse_event.py +0 -0
  95. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_copilot_cli_stream.py +0 -0
  96. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_cursor_execute.py +0 -0
  97. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_cursor_parse_event.py +0 -0
  98. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_cursor_warnings.py +0 -0
  99. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_execute.py +0 -0
  100. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_execute_outcome.py +0 -0
  101. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_health_probe.py +0 -0
  102. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_mcp_server_spec.py +0 -0
  103. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_models.py +0 -0
  104. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_opencode_execute.py +0 -0
  105. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_opencode_parse_event.py +0 -0
  106. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_opencode_spawn.py +0 -0
  107. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_opencode_stream.py +0 -0
  108. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_parse_event.py +0 -0
  109. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_pi_execute.py +0 -0
  110. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_pi_parse_event.py +0 -0
  111. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_pi_warnings.py +0 -0
  112. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_response_aggregation.py +0 -0
  113. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_shell_cancellation.py +0 -0
  114. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_shell_mcp.py +0 -0
  115. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_stderr_format.py +0 -0
  116. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_stream.py +0 -0
  117. {agent_shell_py-0.2.0 → agent_shell_py-0.2.2}/tests/unit/test_tool_denial.py +0 -0
  118. {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 agent calls, real API costs | No (local only) | Yes |
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 exactly but substitute a mocked subprocess emitting captured NDJSON fixtures. This means CI validates the entire class interaction chain without credentials or API spend. E2E tests exist as a local smoke test to confirm the real agents still behave as expected.
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.0
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` / `stream` / `health_check` API across every
17
- agent; swap the backend without changing a line of consuming code.
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
+ ![Skill_Banner](docs/assets/skill_banner.png)
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` / `stream` / `health_check` API across every
8
- agent; swap the backend without changing a line of consuming code.
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
+ ![Skill_Banner](docs/assets/skill_banner.png)
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
@@ -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-07-26
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 for those two.
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: end-to-end tests that call real CLI agents (requires credentials, costs money)",
26
+ "e2e: real CLI tests (may require credentials and incur API costs)",
27
27
  ]
28
28
 
29
29
  [dependency-groups]
@@ -1,6 +1,12 @@
1
1
  ---
2
2
  name: invoking-cli-agents
3
- description: Use when programmatically invoking a CLI coding agent (Claude Code, OpenCode, Copilot CLI, Codex, Pi, Cursor) from Python, delegating a task to a sub-agent, orchestrating several agents, streaming an agent's output, resuming an agent session, restricting which tools an agent may use, or checking whether an agent/model is healthy. Keywords: AgentShell, headless agent, subprocess, allowed_tools, disallowed_tools, read-only agent, session_id, cost, output_tokens.
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 management. All
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
- | Use a specific model | `model="sonnet"` |
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) — `execute`, `stream`, `health_check`, MCP management
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.0'
22
- __version_tuple__ = version_tuple = (0, 2, 0)
21
+ __version__ = version = '0.2.2'
22
+ __version_tuple__ = version_tuple = (0, 2, 2)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -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
 
@@ -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 AgentResponse, StreamEvent, MCPServerSpec, MCPServerType, HealthCheckResult
11
- from agent_shell.process_cleanup import (register_process_group, kill_process_group,
12
- release_process)
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 asyncio.create_subprocess_exec(
108
- *cmd,
109
- stdin=asyncio.subprocess.DEVNULL,
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.pid)
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(