agentpool-cli 0.1.0__py3-none-any.whl
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.
- agentpool/__init__.py +3 -0
- agentpool/agent_io.py +134 -0
- agentpool/artifacts.py +151 -0
- agentpool/cli.py +1199 -0
- agentpool/config.py +373 -0
- agentpool/docs/agentpool-skill.md +85 -0
- agentpool/docs/onboarding.md +169 -0
- agentpool/event_detection.py +150 -0
- agentpool/fixtures/__init__.py +1 -0
- agentpool/fixtures/fake_agents/__init__.py +1 -0
- agentpool/fixtures/fake_agents/fake_approval_agent.py +16 -0
- agentpool/fixtures/fake_agents/fake_common.py +44 -0
- agentpool/fixtures/fake_agents/fake_completed_agent.py +13 -0
- agentpool/fixtures/fake_agents/fake_idle_agent.py +16 -0
- agentpool/fixtures/fake_agents/fake_limit_agent.py +14 -0
- agentpool/fixtures/fake_agents/fake_patch_agent.py +17 -0
- agentpool/fixtures/fake_agents/fake_question_agent.py +16 -0
- agentpool/git_worktree.py +144 -0
- agentpool/mcp/__init__.py +1 -0
- agentpool/mcp/resources.py +64 -0
- agentpool/mcp/tools.py +259 -0
- agentpool/mcp_server.py +487 -0
- agentpool/models.py +310 -0
- agentpool/onboarding.py +1279 -0
- agentpool/policy.py +63 -0
- agentpool/provider_model_catalog.json +997 -0
- agentpool/providers/__init__.py +3 -0
- agentpool/providers/base.py +411 -0
- agentpool/providers/registry.py +139 -0
- agentpool/redaction.py +30 -0
- agentpool/runtimes/__init__.py +3 -0
- agentpool/runtimes/base.py +36 -0
- agentpool/runtimes/tmux.py +133 -0
- agentpool/session_manager.py +1061 -0
- agentpool/stats/__init__.py +6 -0
- agentpool/stats/card.py +74 -0
- agentpool/stats/compute.py +496 -0
- agentpool/stats/queries.py +138 -0
- agentpool/stats/render.py +103 -0
- agentpool/stats/window.py +85 -0
- agentpool/store.py +478 -0
- agentpool/usage/__init__.py +1 -0
- agentpool/usage/_common.py +223 -0
- agentpool/usage/ccusage.py +130 -0
- agentpool/usage/claude.py +23 -0
- agentpool/usage/codex.py +210 -0
- agentpool/usage/codexbar.py +186 -0
- agentpool/usage/combine.py +71 -0
- agentpool/usage/copilot.py +146 -0
- agentpool/usage/devin.py +265 -0
- agentpool/usage/parsers.py +41 -0
- agentpool/usage/probes.py +52 -0
- agentpool/usage/provider_parsers.py +276 -0
- agentpool/usage/summary.py +166 -0
- agentpool/utils.py +59 -0
- agentpool_cli-0.1.0.dist-info/METADATA +292 -0
- agentpool_cli-0.1.0.dist-info/RECORD +60 -0
- agentpool_cli-0.1.0.dist-info/WHEEL +4 -0
- agentpool_cli-0.1.0.dist-info/entry_points.txt +2 -0
- agentpool_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
agentpool/cli.py
ADDED
|
@@ -0,0 +1,1199 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Annotated
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
import yaml
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
from agentpool import __version__
|
|
15
|
+
from agentpool.agent_io import collect_payload, observe_payload, parse_detail, read_stdin_text
|
|
16
|
+
from agentpool.config import (
|
|
17
|
+
DEFAULT_CONFIG_PATH,
|
|
18
|
+
DEFAULT_MODEL_CATALOG_PATH,
|
|
19
|
+
load_config,
|
|
20
|
+
validate_config,
|
|
21
|
+
validate_model_catalog_path,
|
|
22
|
+
)
|
|
23
|
+
from agentpool.mcp_server import run_mcp_server
|
|
24
|
+
from agentpool.models import SpawnWorkerRequest, ToolError
|
|
25
|
+
from agentpool.onboarding import (
|
|
26
|
+
command_path,
|
|
27
|
+
deep_doctor,
|
|
28
|
+
default_onboarding_nudges,
|
|
29
|
+
format_mcp_install,
|
|
30
|
+
init_config,
|
|
31
|
+
mcp_client_config,
|
|
32
|
+
mcp_host_config,
|
|
33
|
+
privacy_doctor,
|
|
34
|
+
run_fake_smoke,
|
|
35
|
+
run_real_read_only_smoke,
|
|
36
|
+
setup_all_providers,
|
|
37
|
+
setup_provider,
|
|
38
|
+
)
|
|
39
|
+
from agentpool.mcp import tools as mcp_tools
|
|
40
|
+
from agentpool.session_manager import SessionManager
|
|
41
|
+
from agentpool.stats.card import render_stats_card
|
|
42
|
+
from agentpool.stats.render import render_stats_panel, render_stats_plain
|
|
43
|
+
from agentpool.usage.probes import detect_codexbar
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
app = typer.Typer(
|
|
47
|
+
help="AgentPool local coding-agent control plane.",
|
|
48
|
+
invoke_without_command=True,
|
|
49
|
+
no_args_is_help=True,
|
|
50
|
+
)
|
|
51
|
+
config_app = typer.Typer(help="Inspect AgentPool config.")
|
|
52
|
+
leases_app = typer.Typer(help="Manage advisory file leases.")
|
|
53
|
+
worktrees_app = typer.Typer(help="Inspect and clean AgentPool-created worktrees.")
|
|
54
|
+
app.add_typer(config_app, name="config")
|
|
55
|
+
app.add_typer(leases_app, name="leases")
|
|
56
|
+
app.add_typer(worktrees_app, name="worktrees")
|
|
57
|
+
console = Console()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@app.callback()
|
|
61
|
+
def root(
|
|
62
|
+
version: Annotated[bool, typer.Option("--version", help="Show AgentPool version.")] = False,
|
|
63
|
+
) -> None:
|
|
64
|
+
if version:
|
|
65
|
+
console.print(f"agentpool {__version__}")
|
|
66
|
+
raise typer.Exit()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def print_data(data: object, json_output: bool) -> None:
|
|
70
|
+
if json_output:
|
|
71
|
+
console.print_json(json.dumps(data, default=str))
|
|
72
|
+
else:
|
|
73
|
+
console.print(data)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def manager() -> SessionManager:
|
|
77
|
+
return SessionManager(load_config())
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def handle_tool_error(exc: ToolError, json_output: bool = False) -> None:
|
|
81
|
+
data = {"error": exc.error.model_dump(mode="json")}
|
|
82
|
+
next_command = _next_command_for_error(exc)
|
|
83
|
+
if next_command:
|
|
84
|
+
data["error"]["details"] = {**(data["error"].get("details") or {}), "example": next_command}
|
|
85
|
+
if json_output:
|
|
86
|
+
console.print_json(json.dumps(data))
|
|
87
|
+
else:
|
|
88
|
+
console.print(f"[red]{exc.error.code}[/red]: {exc.error.message}")
|
|
89
|
+
if next_command:
|
|
90
|
+
console.print(f"try: {next_command}")
|
|
91
|
+
raise typer.Exit(1)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _next_command_for_error(exc: ToolError) -> str | None:
|
|
95
|
+
code = exc.error.code
|
|
96
|
+
details = exc.error.details or {}
|
|
97
|
+
if code == "PROVIDER_NOT_FOUND":
|
|
98
|
+
return "agentpool inventory --json"
|
|
99
|
+
if code == "PROVIDER_NOT_INSTALLED":
|
|
100
|
+
provider_id = details.get("provider_id") or "<provider-id>"
|
|
101
|
+
return f"agentpool setup {provider_id}"
|
|
102
|
+
if code == "POLICY_BLOCKED" and details.get("policy") in {
|
|
103
|
+
"require_explicit_provider",
|
|
104
|
+
"denied_providers",
|
|
105
|
+
"allowed_providers",
|
|
106
|
+
}:
|
|
107
|
+
return "agentpool inventory --json"
|
|
108
|
+
if code == "POLICY_BLOCKED" and "max_parallel_sessions" in details:
|
|
109
|
+
return "agentpool sessions --json"
|
|
110
|
+
if code == "USAGE_POLICY_BLOCKED":
|
|
111
|
+
provider_id = details.get("provider_id") or "<provider-id>"
|
|
112
|
+
return f"agentpool usage-summary --provider {provider_id} --refresh --json"
|
|
113
|
+
if code in {"INVALID_REQUEST", "INVALID_STDIN"}:
|
|
114
|
+
return str(details.get("example") or "agentpool spawn --provider <provider-id> --repo . --task \"Inspect this repo.\"")
|
|
115
|
+
if code == "INVALID_DETAIL":
|
|
116
|
+
return "agentpool observe <session-id> --detail excerpt"
|
|
117
|
+
if code == "INVALID_SESSION_PAGE":
|
|
118
|
+
return "agentpool sessions --limit 50 --offset 0 --json"
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@app.command()
|
|
123
|
+
def doctor(
|
|
124
|
+
json_output: Annotated[bool, typer.Option("--json", help="Emit JSON.")] = False,
|
|
125
|
+
deep: Annotated[bool, typer.Option("--deep", help="Run tmux/sqlite/artifact/cache checks.")] = False,
|
|
126
|
+
privacy: Annotated[
|
|
127
|
+
bool,
|
|
128
|
+
typer.Option("--privacy", help="Show local storage and usage-probe privacy posture."),
|
|
129
|
+
] = False,
|
|
130
|
+
) -> None:
|
|
131
|
+
mgr = manager()
|
|
132
|
+
tmux_path = shutil.which("tmux")
|
|
133
|
+
inventory = mgr.inventory(include_usage=True)
|
|
134
|
+
data = {
|
|
135
|
+
"tmux": {"installed": bool(tmux_path), "path": tmux_path},
|
|
136
|
+
"config_path": str(DEFAULT_CONFIG_PATH),
|
|
137
|
+
"db_path": str(mgr.config.storage.db),
|
|
138
|
+
"artifact_root": str(mgr.config.storage.artifacts),
|
|
139
|
+
"inventory": inventory,
|
|
140
|
+
}
|
|
141
|
+
if deep:
|
|
142
|
+
data["deep"] = deep_doctor(mgr)
|
|
143
|
+
if privacy:
|
|
144
|
+
data["privacy"] = privacy_doctor(mgr)
|
|
145
|
+
if json_output:
|
|
146
|
+
console.print_json(json.dumps(data, default=str))
|
|
147
|
+
return
|
|
148
|
+
table = Table("Provider", "Installed", "Auth", "Usage")
|
|
149
|
+
for provider in inventory["providers"]:
|
|
150
|
+
table.add_row(
|
|
151
|
+
provider["id"],
|
|
152
|
+
"yes" if provider["installed"] else "no",
|
|
153
|
+
provider["auth"]["status"],
|
|
154
|
+
provider["usage"]["status"] if provider.get("usage") else "unknown",
|
|
155
|
+
)
|
|
156
|
+
console.print(f"tmux: {tmux_path or 'missing'}")
|
|
157
|
+
if deep:
|
|
158
|
+
deep_data = data["deep"]
|
|
159
|
+
console.print(f"deep checks: {'ok' if deep_data['ok'] else 'failed'}")
|
|
160
|
+
for check in deep_data["checks"]:
|
|
161
|
+
console.print(f" {check['name']}: {'ok' if check['ok'] else 'failed'}")
|
|
162
|
+
if privacy:
|
|
163
|
+
privacy_data = data["privacy"]
|
|
164
|
+
console.print("privacy:")
|
|
165
|
+
console.print(
|
|
166
|
+
" credential storage: "
|
|
167
|
+
f"{'yes' if privacy_data['credential_storage']['agentpool_stores_provider_credentials'] else 'no'}"
|
|
168
|
+
)
|
|
169
|
+
console.print(
|
|
170
|
+
" browser scraping by default: "
|
|
171
|
+
f"{'yes' if privacy_data['credential_storage']['browser_scraping_enabled_by_default'] else 'no'}"
|
|
172
|
+
)
|
|
173
|
+
console.print(f" sqlite db: {privacy_data['local_storage']['sqlite_db']}")
|
|
174
|
+
console.print(f" artifacts: {privacy_data['local_storage']['artifact_root']}")
|
|
175
|
+
console.print(" live usage probes run only on explicit refresh: yes")
|
|
176
|
+
console.print(table)
|
|
177
|
+
console.print("\nWire AgentPool into Cursor:")
|
|
178
|
+
for command in default_onboarding_nudges():
|
|
179
|
+
console.print(f" {command}")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@app.command("init")
|
|
183
|
+
def init_command(
|
|
184
|
+
path: Annotated[Path, typer.Option("--path", help="Config path to initialize.")] = DEFAULT_CONFIG_PATH,
|
|
185
|
+
force: Annotated[bool, typer.Option("--force", help="Back up and overwrite existing config.")] = False,
|
|
186
|
+
json_output: Annotated[bool, typer.Option("--json", help="Emit JSON.")] = False,
|
|
187
|
+
) -> None:
|
|
188
|
+
data = init_config(path, force=force)
|
|
189
|
+
if json_output:
|
|
190
|
+
console.print_json(json.dumps(data, default=str))
|
|
191
|
+
return
|
|
192
|
+
status = "wrote" if data["changed"] else "exists"
|
|
193
|
+
console.print(f"config {status}: {data['config_path']}")
|
|
194
|
+
if data.get("backup_path"):
|
|
195
|
+
console.print(f"backup: {data['backup_path']}")
|
|
196
|
+
console.print("next:")
|
|
197
|
+
for command in data.get("next_commands") or default_onboarding_nudges():
|
|
198
|
+
console.print(f" {command}")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@app.command("mcp-config")
|
|
202
|
+
def mcp_config(
|
|
203
|
+
client: Annotated[
|
|
204
|
+
str,
|
|
205
|
+
typer.Option(
|
|
206
|
+
"--client",
|
|
207
|
+
help=(
|
|
208
|
+
"MCP host: generic, claude-code, claude-desktop, codex, cursor, or copilot-cli."
|
|
209
|
+
),
|
|
210
|
+
),
|
|
211
|
+
] = "generic",
|
|
212
|
+
absolute_command: Annotated[
|
|
213
|
+
bool, typer.Option("--absolute-command", help="Use the current resolved agentpool command.")
|
|
214
|
+
] = False,
|
|
215
|
+
install: Annotated[
|
|
216
|
+
bool,
|
|
217
|
+
typer.Option(
|
|
218
|
+
"--install",
|
|
219
|
+
help="Print one-click install helpers (deeplink, shell command, or Copilot CLI steps).",
|
|
220
|
+
),
|
|
221
|
+
] = False,
|
|
222
|
+
json_output: Annotated[bool, typer.Option("--json", help="Emit JSON.")] = False,
|
|
223
|
+
) -> None:
|
|
224
|
+
"""Print MCP host configuration.
|
|
225
|
+
|
|
226
|
+
Examples:
|
|
227
|
+
agentpool mcp-config --client codex --absolute-command --install
|
|
228
|
+
agentpool mcp-config --client claude-code --json
|
|
229
|
+
agentpool mcp-config --client generic
|
|
230
|
+
"""
|
|
231
|
+
command = command_path(absolute=absolute_command) if absolute_command else "agentpool"
|
|
232
|
+
data = mcp_client_config(client, command)
|
|
233
|
+
if json_output:
|
|
234
|
+
console.print_json(json.dumps(data, default=str))
|
|
235
|
+
if not data.get("ok", True):
|
|
236
|
+
raise typer.Exit(1)
|
|
237
|
+
return
|
|
238
|
+
if not data.get("ok", True):
|
|
239
|
+
console.print(f"[red]{data['error']}[/red]")
|
|
240
|
+
console.print(f"supported: {', '.join(data['supported_clients'])}")
|
|
241
|
+
raise typer.Exit(1)
|
|
242
|
+
if install:
|
|
243
|
+
console.print(format_mcp_install(data), markup=False)
|
|
244
|
+
return
|
|
245
|
+
if data.get("format") == "toml":
|
|
246
|
+
console.print(data["config"], end="", markup=False)
|
|
247
|
+
else:
|
|
248
|
+
console.print_json(json.dumps(data["config"]))
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@app.command()
|
|
252
|
+
def inventory(json_output: Annotated[bool, typer.Option("--json", help="Emit JSON.")] = False) -> None:
|
|
253
|
+
data = manager().inventory(include_usage=True)
|
|
254
|
+
if json_output:
|
|
255
|
+
console.print_json(json.dumps(data, default=str))
|
|
256
|
+
else:
|
|
257
|
+
table = Table("Provider", "Installed", "Binary", "Auth", "Usage")
|
|
258
|
+
for provider in data["providers"]:
|
|
259
|
+
table.add_row(
|
|
260
|
+
provider["id"],
|
|
261
|
+
"yes" if provider["installed"] else "no",
|
|
262
|
+
provider.get("binary_path") or "",
|
|
263
|
+
provider["auth"]["status"],
|
|
264
|
+
provider["usage"]["status"] if provider.get("usage") else "unknown",
|
|
265
|
+
)
|
|
266
|
+
console.print(table)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@app.command()
|
|
270
|
+
def usage(
|
|
271
|
+
provider: Annotated[str | None, typer.Option("--provider", help="Provider id.")] = None,
|
|
272
|
+
backend: Annotated[
|
|
273
|
+
str,
|
|
274
|
+
typer.Option("--backend", help="Usage backend: native, codexbar, ccusage, or combined."),
|
|
275
|
+
] = "combined",
|
|
276
|
+
cached: Annotated[bool, typer.Option("--cached", help="Read latest persisted snapshot without probing.")] = False,
|
|
277
|
+
json_output: Annotated[bool, typer.Option("--json", help="Emit JSON.")] = False,
|
|
278
|
+
) -> None:
|
|
279
|
+
try:
|
|
280
|
+
data = manager().cached_usage_snapshot(provider) if cached else manager().usage_snapshot(provider, backend=backend)
|
|
281
|
+
print_data(data, json_output)
|
|
282
|
+
except ToolError as exc:
|
|
283
|
+
handle_tool_error(exc, json_output)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@app.command("usage-summary")
|
|
287
|
+
def usage_summary(
|
|
288
|
+
provider: Annotated[str | None, typer.Option("--provider", help="Provider id.")] = None,
|
|
289
|
+
refresh: Annotated[bool, typer.Option("--refresh", help="Run live probes before summarizing.")] = False,
|
|
290
|
+
backend: Annotated[
|
|
291
|
+
str,
|
|
292
|
+
typer.Option("--backend", help="Live usage backend: native, codexbar, ccusage, or combined."),
|
|
293
|
+
] = "combined",
|
|
294
|
+
json_output: Annotated[bool, typer.Option("--json", help="Emit JSON.")] = False,
|
|
295
|
+
) -> None:
|
|
296
|
+
"""Summarize provider usage.
|
|
297
|
+
|
|
298
|
+
Examples:
|
|
299
|
+
agentpool usage-summary --json
|
|
300
|
+
agentpool usage-summary --provider codex-cli --refresh --json
|
|
301
|
+
agentpool usage-summary --backend codexbar --json
|
|
302
|
+
"""
|
|
303
|
+
try:
|
|
304
|
+
data = manager().usage_summary(provider_id=provider, refresh=refresh, backend=backend)
|
|
305
|
+
if json_output:
|
|
306
|
+
console.print_json(json.dumps(data, default=str))
|
|
307
|
+
return
|
|
308
|
+
table = Table("Provider", "Status", "Confidence", "Summary", "Checked")
|
|
309
|
+
for row in data["providers"].values():
|
|
310
|
+
table.add_row(
|
|
311
|
+
row["provider_id"],
|
|
312
|
+
row["status"],
|
|
313
|
+
row["confidence"],
|
|
314
|
+
row["summary"],
|
|
315
|
+
row["checked_at"],
|
|
316
|
+
)
|
|
317
|
+
console.print(f"source: {data['source']}")
|
|
318
|
+
console.print(table)
|
|
319
|
+
except ToolError as exc:
|
|
320
|
+
handle_tool_error(exc, json_output)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
@app.command("capacity-summary")
|
|
324
|
+
def capacity_summary(
|
|
325
|
+
provider: Annotated[str | None, typer.Option("--provider", help="Provider id.")] = None,
|
|
326
|
+
refresh: Annotated[bool, typer.Option("--refresh", help="Run live probes before summarizing.")] = False,
|
|
327
|
+
backend: Annotated[
|
|
328
|
+
str,
|
|
329
|
+
typer.Option("--backend", help="Live usage backend: native, codexbar, ccusage, or combined."),
|
|
330
|
+
] = "combined",
|
|
331
|
+
json_output: Annotated[bool, typer.Option("--json", help="Emit JSON.")] = False,
|
|
332
|
+
) -> None:
|
|
333
|
+
usage_summary(provider=provider, refresh=refresh, backend=backend, json_output=json_output)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@app.command("setup")
|
|
337
|
+
def setup_command(
|
|
338
|
+
target: Annotated[
|
|
339
|
+
str,
|
|
340
|
+
typer.Argument(help="Setup target, for example: cursor, codex, claude-code, droid-cli."),
|
|
341
|
+
],
|
|
342
|
+
backend: Annotated[
|
|
343
|
+
str | None,
|
|
344
|
+
typer.Option("--backend", help="Usage backend override: native, codexbar, ccusage, or combined."),
|
|
345
|
+
] = None,
|
|
346
|
+
skip_usage: Annotated[
|
|
347
|
+
bool,
|
|
348
|
+
typer.Option("--skip-usage", help="Do not run live usage probes during setup."),
|
|
349
|
+
] = False,
|
|
350
|
+
relative_command: Annotated[
|
|
351
|
+
bool,
|
|
352
|
+
typer.Option("--relative-command", help="Use 'agentpool' instead of an absolute path in MCP config."),
|
|
353
|
+
] = False,
|
|
354
|
+
json_output: Annotated[bool, typer.Option("--json", help="Emit JSON.")] = False,
|
|
355
|
+
) -> None:
|
|
356
|
+
if target.strip().lower() == "all":
|
|
357
|
+
data = setup_all_providers(
|
|
358
|
+
manager(),
|
|
359
|
+
backend=backend,
|
|
360
|
+
run_usage=not skip_usage,
|
|
361
|
+
absolute_command=not relative_command,
|
|
362
|
+
)
|
|
363
|
+
if json_output:
|
|
364
|
+
console.print_json(json.dumps(data, default=str))
|
|
365
|
+
return
|
|
366
|
+
console.print("[bold]AgentPool setup: all providers[/bold]")
|
|
367
|
+
table = Table("Provider", "Installed", "Usage", "MCP", "Details", "Next action")
|
|
368
|
+
for row in data["rows"]:
|
|
369
|
+
installed = "yes" if row["installed"] else "no"
|
|
370
|
+
if row["installed"] is None:
|
|
371
|
+
installed = "n/a"
|
|
372
|
+
usage = "skip" if row["usage_ok"] is None else ("ok" if row["usage_ok"] else "needs action")
|
|
373
|
+
table.add_row(
|
|
374
|
+
row["provider_id"],
|
|
375
|
+
installed,
|
|
376
|
+
usage,
|
|
377
|
+
row["mcp_config"],
|
|
378
|
+
str(row.get("usage_message") or ""),
|
|
379
|
+
str(row.get("action") or ""),
|
|
380
|
+
)
|
|
381
|
+
console.print(table)
|
|
382
|
+
console.print("\nRun a focused setup for details:")
|
|
383
|
+
for target_name in data["targets"]:
|
|
384
|
+
console.print(f" agentpool setup {target_name}")
|
|
385
|
+
return
|
|
386
|
+
|
|
387
|
+
data = setup_provider(
|
|
388
|
+
manager(),
|
|
389
|
+
target,
|
|
390
|
+
backend=backend,
|
|
391
|
+
run_usage=not skip_usage,
|
|
392
|
+
absolute_command=not relative_command,
|
|
393
|
+
)
|
|
394
|
+
if json_output:
|
|
395
|
+
console.print_json(json.dumps(data, default=str))
|
|
396
|
+
if not data["ok"]:
|
|
397
|
+
raise typer.Exit(1)
|
|
398
|
+
return
|
|
399
|
+
if data.get("error"):
|
|
400
|
+
console.print(f"[red]{data['error']}[/red]")
|
|
401
|
+
console.print(f"supported: {', '.join(data.get('supported_targets', []))}")
|
|
402
|
+
if data.get("action"):
|
|
403
|
+
console.print(f"next: {data['action']}")
|
|
404
|
+
raise typer.Exit(1)
|
|
405
|
+
console.print(f"[bold]AgentPool setup: {data['display_name']}[/bold]")
|
|
406
|
+
table = Table("Check", "Status", "Details", "Next action")
|
|
407
|
+
for check in data["checks"]:
|
|
408
|
+
status = "skip" if check["ok"] is None else ("ok" if check["ok"] else "needs action")
|
|
409
|
+
table.add_row(check["name"], status, str(check.get("message") or ""), str(check.get("action") or ""))
|
|
410
|
+
console.print(table)
|
|
411
|
+
if data.get("mcp_config"):
|
|
412
|
+
config = data["mcp_config"]
|
|
413
|
+
install_text = format_mcp_install(config)
|
|
414
|
+
if install_text:
|
|
415
|
+
console.print(f"\n{install_text}")
|
|
416
|
+
console.print(f"\nMCP config for {config['path']}:")
|
|
417
|
+
if config.get("format") == "toml":
|
|
418
|
+
console.print(config["config"], end="", markup=False)
|
|
419
|
+
else:
|
|
420
|
+
console.print_json(json.dumps(config["config"], default=str))
|
|
421
|
+
console.print("\nManual steps:")
|
|
422
|
+
for step in data["manual_steps"]:
|
|
423
|
+
console.print(f" - {step}")
|
|
424
|
+
if data.get("actions"):
|
|
425
|
+
console.print("\nActions to resolve setup issues:")
|
|
426
|
+
for action in data["actions"]:
|
|
427
|
+
console.print(f" - {action}")
|
|
428
|
+
if data.get("setup_doc"):
|
|
429
|
+
console.print(f"\nGuide: {data['setup_doc']}")
|
|
430
|
+
console.print("\nUseful next commands:")
|
|
431
|
+
for command in data["next_commands"]:
|
|
432
|
+
console.print(f" {command}")
|
|
433
|
+
if not data["ok"]:
|
|
434
|
+
raise typer.Exit(1)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
@app.command()
|
|
438
|
+
def onboard(json_output: Annotated[bool, typer.Option("--json", help="Emit JSON.")] = False) -> None:
|
|
439
|
+
mgr = manager()
|
|
440
|
+
data = {
|
|
441
|
+
"config_path": str(DEFAULT_CONFIG_PATH),
|
|
442
|
+
"db_path": str(mgr.config.storage.db),
|
|
443
|
+
"artifact_root": str(mgr.config.storage.artifacts),
|
|
444
|
+
"usage_backends": {
|
|
445
|
+
"default": "combined",
|
|
446
|
+
"available": ["native", "codexbar", "ccusage", "combined"],
|
|
447
|
+
"codexbar": detect_codexbar(),
|
|
448
|
+
"web_sources_enabled_by_default": False,
|
|
449
|
+
},
|
|
450
|
+
"first_commands": [
|
|
451
|
+
"agentpool init",
|
|
452
|
+
"agentpool doctor --deep",
|
|
453
|
+
"agentpool usage-summary --refresh",
|
|
454
|
+
"agentpool usage-summary --refresh --backend codexbar",
|
|
455
|
+
"agentpool providers",
|
|
456
|
+
"agentpool models",
|
|
457
|
+
"agentpool smoke --provider fake-question --repo .",
|
|
458
|
+
],
|
|
459
|
+
"mcp_resources": [
|
|
460
|
+
"agentpool://onboarding",
|
|
461
|
+
"agentpool://skill.md",
|
|
462
|
+
"agentpool://sessions/{session_id}/transcript",
|
|
463
|
+
"agentpool://sessions/{session_id}/events",
|
|
464
|
+
"agentpool://artifacts/{session_id}",
|
|
465
|
+
],
|
|
466
|
+
"mcp_host_config": {
|
|
467
|
+
"mcpServers": {"agentpool": {"command": "agentpool", "args": ["mcp"]}},
|
|
468
|
+
},
|
|
469
|
+
"rules": [
|
|
470
|
+
"Select providers explicitly; provider=auto is rejected.",
|
|
471
|
+
"Use usage-summary before delegating when possible.",
|
|
472
|
+
"Use read_only for exploration; choose worktree isolation explicitly when AgentPool should create one.",
|
|
473
|
+
"Observe and collect workers deliberately; terminate when done.",
|
|
474
|
+
],
|
|
475
|
+
}
|
|
476
|
+
if json_output:
|
|
477
|
+
console.print_json(json.dumps(data, default=str))
|
|
478
|
+
return
|
|
479
|
+
console.print("[bold]AgentPool Onboarding[/bold]")
|
|
480
|
+
console.print(f"config: {data['config_path']}")
|
|
481
|
+
console.print(f"db: {data['db_path']}")
|
|
482
|
+
console.print(f"artifacts: {data['artifact_root']}")
|
|
483
|
+
codexbar = data["usage_backends"]["codexbar"]
|
|
484
|
+
console.print(f"codexbar: {'installed' if codexbar['installed'] else 'not installed'}")
|
|
485
|
+
console.print("\nFirst commands:")
|
|
486
|
+
for command in data["first_commands"]:
|
|
487
|
+
console.print(f" {command}")
|
|
488
|
+
console.print("\nMCP resources agents may read on demand:")
|
|
489
|
+
for resource in data["mcp_resources"]:
|
|
490
|
+
console.print(f" {resource}")
|
|
491
|
+
console.print("\nHost config:")
|
|
492
|
+
console.print_json(json.dumps(data["mcp_host_config"]))
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
@app.command()
|
|
496
|
+
def smoke(
|
|
497
|
+
provider: Annotated[str, typer.Option("--provider", help="Provider id to smoke test.")] = "fake-question",
|
|
498
|
+
repo: Annotated[Path, typer.Option("--repo", help="Repository path.")] = Path("."),
|
|
499
|
+
model: Annotated[
|
|
500
|
+
str | None,
|
|
501
|
+
typer.Option("--model", help="Explicit model id for real-provider smoke. Defaults to provider smoke_model."),
|
|
502
|
+
] = None,
|
|
503
|
+
real_read_only: Annotated[
|
|
504
|
+
bool,
|
|
505
|
+
typer.Option("--real-read-only", help="Allow a guarded read-only smoke for a real provider."),
|
|
506
|
+
] = False,
|
|
507
|
+
timeout: Annotated[int, typer.Option("--timeout", help="Real-provider observe timeout in seconds.")] = 60,
|
|
508
|
+
no_accept_startup_trust: Annotated[
|
|
509
|
+
bool,
|
|
510
|
+
typer.Option("--no-accept-startup-trust", help="Do not answer known startup trust prompts."),
|
|
511
|
+
] = False,
|
|
512
|
+
json_output: Annotated[bool, typer.Option("--json", help="Emit JSON.")] = False,
|
|
513
|
+
) -> None:
|
|
514
|
+
try:
|
|
515
|
+
if provider.startswith("fake-"):
|
|
516
|
+
data = run_fake_smoke(manager(), repo=repo.expanduser().resolve(), provider_id=provider)
|
|
517
|
+
elif real_read_only:
|
|
518
|
+
data = run_real_read_only_smoke(
|
|
519
|
+
manager(),
|
|
520
|
+
repo=repo.expanduser().resolve(),
|
|
521
|
+
provider_id=provider,
|
|
522
|
+
model=model,
|
|
523
|
+
timeout_seconds=timeout,
|
|
524
|
+
accept_startup_trust=not no_accept_startup_trust,
|
|
525
|
+
)
|
|
526
|
+
else:
|
|
527
|
+
raise ToolError(
|
|
528
|
+
"POLICY_BLOCKED",
|
|
529
|
+
"Real-provider smoke requires --real-read-only.",
|
|
530
|
+
{"provider_id": provider, "isolation": "read_only"},
|
|
531
|
+
)
|
|
532
|
+
if json_output:
|
|
533
|
+
console.print_json(json.dumps(data, default=str))
|
|
534
|
+
return
|
|
535
|
+
console.print(f"smoke {'ok' if data['ok'] else 'failed'}: {data['session_id']}")
|
|
536
|
+
console.print(f"artifacts: {data.get('artifact_dir')}")
|
|
537
|
+
except ToolError as exc:
|
|
538
|
+
handle_tool_error(exc, json_output)
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
@app.command()
|
|
542
|
+
def providers(json_output: Annotated[bool, typer.Option("--json", help="Emit JSON.")] = False) -> None:
|
|
543
|
+
data = manager().inventory(include_usage=False)
|
|
544
|
+
if json_output:
|
|
545
|
+
console.print_json(json.dumps({"providers": data["providers"]}, default=str))
|
|
546
|
+
else:
|
|
547
|
+
for provider in data["providers"]:
|
|
548
|
+
console.print(provider["id"])
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
@app.command("models")
|
|
552
|
+
def models_command(
|
|
553
|
+
action: Annotated[
|
|
554
|
+
str | None,
|
|
555
|
+
typer.Argument(help="Use 'validate' to validate a JSON model catalog."),
|
|
556
|
+
] = None,
|
|
557
|
+
provider: Annotated[str | None, typer.Option("--provider", help="Provider id.")] = None,
|
|
558
|
+
path: Annotated[
|
|
559
|
+
Path | None,
|
|
560
|
+
typer.Option("--path", help="JSON model catalog path. Defaults to the embedded catalog."),
|
|
561
|
+
] = None,
|
|
562
|
+
json_output: Annotated[bool, typer.Option("--json", help="Emit JSON.")] = False,
|
|
563
|
+
) -> None:
|
|
564
|
+
mgr = manager()
|
|
565
|
+
if action:
|
|
566
|
+
if action != "validate":
|
|
567
|
+
raise typer.BadParameter("Only supported models action is 'validate'.")
|
|
568
|
+
data = validate_model_catalog_path(
|
|
569
|
+
path or DEFAULT_MODEL_CATALOG_PATH,
|
|
570
|
+
known_provider_ids=set(mgr.config.providers),
|
|
571
|
+
)
|
|
572
|
+
if json_output:
|
|
573
|
+
console.print_json(json.dumps(data, default=str))
|
|
574
|
+
else:
|
|
575
|
+
console.print(f"catalog {'ok' if data['ok'] else 'failed'}: {data['path']}")
|
|
576
|
+
for warning in data["warnings"]:
|
|
577
|
+
console.print(f"[yellow]warning[/yellow]: {warning}")
|
|
578
|
+
for error in data["errors"]:
|
|
579
|
+
console.print(f"[red]error[/red]: {error}")
|
|
580
|
+
if not data["ok"]:
|
|
581
|
+
raise typer.Exit(1)
|
|
582
|
+
return
|
|
583
|
+
rows = mgr.provider_models(provider)["providers"]
|
|
584
|
+
if json_output:
|
|
585
|
+
console.print_json(json.dumps({"providers": rows}, default=str))
|
|
586
|
+
return
|
|
587
|
+
if provider:
|
|
588
|
+
row = rows[0]
|
|
589
|
+
console.print(f"[bold]{row['provider_id']}[/bold]")
|
|
590
|
+
console.print(f"default: {row['default_model'] or ''}")
|
|
591
|
+
console.print(f"smoke: {row['smoke_model'] or ''}")
|
|
592
|
+
console.print(f"selection: {row['model_selection'] or ''}")
|
|
593
|
+
console.print(f"catalog: {row['catalog_completeness'] or ''}")
|
|
594
|
+
if row["quirks"]:
|
|
595
|
+
console.print("quirks:")
|
|
596
|
+
for quirk in row["quirks"]:
|
|
597
|
+
console.print(f" {quirk}")
|
|
598
|
+
table = Table("Model", "Display", "Confidence", "Reasoning")
|
|
599
|
+
for model in row["models"]:
|
|
600
|
+
metadata = model.get("metadata") or {}
|
|
601
|
+
reasoning = metadata.get("reasoning") or {}
|
|
602
|
+
supported = ", ".join(reasoning.get("supported") or [])
|
|
603
|
+
default = reasoning.get("default")
|
|
604
|
+
reasoning_text = f"{supported}; default {default}" if supported and default else supported
|
|
605
|
+
table.add_row(
|
|
606
|
+
model["id"],
|
|
607
|
+
model.get("display_name") or "",
|
|
608
|
+
model.get("confidence") or "",
|
|
609
|
+
reasoning_text,
|
|
610
|
+
)
|
|
611
|
+
console.print(table)
|
|
612
|
+
return
|
|
613
|
+
table = Table("Provider", "Default", "Smoke", "Selection", "Models", "Catalog")
|
|
614
|
+
for row in rows:
|
|
615
|
+
table.add_row(
|
|
616
|
+
row["provider_id"],
|
|
617
|
+
str(row["default_model"] or ""),
|
|
618
|
+
str(row["smoke_model"] or ""),
|
|
619
|
+
str(row["model_selection"] or ""),
|
|
620
|
+
str(len(row["models"])),
|
|
621
|
+
str(row["catalog_completeness"] or ""),
|
|
622
|
+
)
|
|
623
|
+
console.print(table)
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
@app.command()
|
|
627
|
+
def stats(
|
|
628
|
+
since: Annotated[
|
|
629
|
+
str | None,
|
|
630
|
+
typer.Option("--since", help="Window spec: 7d, 30d, 12h, 1w, ISO date, or all."),
|
|
631
|
+
] = None,
|
|
632
|
+
window_from: Annotated[
|
|
633
|
+
str | None,
|
|
634
|
+
typer.Option("--from", help="Window start ISO timestamp. Mutually exclusive with --since."),
|
|
635
|
+
] = None,
|
|
636
|
+
window_to: Annotated[
|
|
637
|
+
str | None,
|
|
638
|
+
typer.Option("--to", help="Window end ISO timestamp. Requires --from."),
|
|
639
|
+
] = None,
|
|
640
|
+
provider: Annotated[str | None, typer.Option("--provider", help="Filter by provider id.")] = None,
|
|
641
|
+
scope: Annotated[
|
|
642
|
+
str,
|
|
643
|
+
typer.Option("--scope", help="Session scope: mine or all."),
|
|
644
|
+
] = "all",
|
|
645
|
+
sections: Annotated[
|
|
646
|
+
list[str] | None,
|
|
647
|
+
typer.Option("--sections", help="Limit output sections. Repeat for multiple."),
|
|
648
|
+
] = None,
|
|
649
|
+
share: Annotated[
|
|
650
|
+
Path | None,
|
|
651
|
+
typer.Option(
|
|
652
|
+
"--share",
|
|
653
|
+
help="Render a PNG share card. Optional output path.",
|
|
654
|
+
file_okay=True,
|
|
655
|
+
dir_okay=False,
|
|
656
|
+
),
|
|
657
|
+
] = None,
|
|
658
|
+
json_output: Annotated[bool, typer.Option("--json", help="Emit JSON.")] = False,
|
|
659
|
+
plain: Annotated[bool, typer.Option("--plain", help="Emit grep-friendly key=value lines.")] = False,
|
|
660
|
+
) -> None:
|
|
661
|
+
"""Report pool stats for a time window. Defaults to the last 7 days."""
|
|
662
|
+
if json_output and plain:
|
|
663
|
+
handle_tool_error(
|
|
664
|
+
ToolError("INVALID_OUTPUT", "Choose either --json or --plain, not both."),
|
|
665
|
+
json_output,
|
|
666
|
+
)
|
|
667
|
+
if window_from or window_to:
|
|
668
|
+
if since is not None:
|
|
669
|
+
handle_tool_error(
|
|
670
|
+
ToolError("INVALID_WINDOW", "Use either --since or --from/--to, not both."),
|
|
671
|
+
json_output,
|
|
672
|
+
)
|
|
673
|
+
if not window_from or not window_to:
|
|
674
|
+
handle_tool_error(
|
|
675
|
+
ToolError("INVALID_WINDOW", "--from and --to must be provided together."),
|
|
676
|
+
json_output,
|
|
677
|
+
)
|
|
678
|
+
window_spec = f"{window_from}/{window_to}"
|
|
679
|
+
else:
|
|
680
|
+
window_spec = since or "7d"
|
|
681
|
+
|
|
682
|
+
try:
|
|
683
|
+
data = mcp_tools.get_stats(
|
|
684
|
+
manager(),
|
|
685
|
+
window=window_spec,
|
|
686
|
+
provider_id=provider,
|
|
687
|
+
sections=sections,
|
|
688
|
+
scope=scope,
|
|
689
|
+
)
|
|
690
|
+
if share is not None:
|
|
691
|
+
card = render_stats_card(data, str(share))
|
|
692
|
+
if json_output:
|
|
693
|
+
data = {**data, "share_card": card}
|
|
694
|
+
elif not plain:
|
|
695
|
+
console.print(f"share card: {card['path']} ({card['bytes']} bytes)")
|
|
696
|
+
if json_output:
|
|
697
|
+
console.print_json(json.dumps(data, default=str))
|
|
698
|
+
return
|
|
699
|
+
if plain:
|
|
700
|
+
console.print(render_stats_plain(data))
|
|
701
|
+
return
|
|
702
|
+
console.print(render_stats_panel(data))
|
|
703
|
+
except ToolError as exc:
|
|
704
|
+
handle_tool_error(exc, json_output)
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
@app.command()
|
|
708
|
+
def sessions(
|
|
709
|
+
state: Annotated[
|
|
710
|
+
str | None,
|
|
711
|
+
typer.Option("--state", help="Comma-separated states such as running,completed."),
|
|
712
|
+
] = None,
|
|
713
|
+
provider: Annotated[str | None, typer.Option("--provider", help="Filter by provider id.")] = None,
|
|
714
|
+
limit: Annotated[int, typer.Option("--limit", help="Maximum sessions to return.")] = 50,
|
|
715
|
+
offset: Annotated[int, typer.Option("--offset", help="Zero-based session page offset.")] = 0,
|
|
716
|
+
recent: Annotated[int | None, typer.Option("--recent", help="Return the N most recent sessions.")] = None,
|
|
717
|
+
all_rows: Annotated[bool, typer.Option("--all", help="Return all matching sessions.")] = False,
|
|
718
|
+
json_output: Annotated[bool, typer.Option("--json", help="Emit JSON.")] = False,
|
|
719
|
+
) -> None:
|
|
720
|
+
"""List sessions with bounded output by default.
|
|
721
|
+
|
|
722
|
+
Examples:
|
|
723
|
+
agentpool sessions --json
|
|
724
|
+
agentpool sessions --limit 25 --offset 25 --json
|
|
725
|
+
agentpool sessions --state running,awaiting_user_input --json
|
|
726
|
+
agentpool sessions --recent 10 --json
|
|
727
|
+
"""
|
|
728
|
+
try:
|
|
729
|
+
if recent is not None and all_rows:
|
|
730
|
+
raise ToolError(
|
|
731
|
+
"INVALID_SESSION_PAGE",
|
|
732
|
+
"Use either --recent or --all, not both.",
|
|
733
|
+
{"example": "agentpool sessions --recent 10 --json"},
|
|
734
|
+
)
|
|
735
|
+
page_limit: int | None = None if all_rows else (recent if recent is not None else limit)
|
|
736
|
+
page_offset = 0 if recent is not None else offset
|
|
737
|
+
states = [part.strip() for part in state.split(",") if part.strip()] if state else None
|
|
738
|
+
data = manager().list_sessions(
|
|
739
|
+
states=states,
|
|
740
|
+
provider_id=provider,
|
|
741
|
+
limit=page_limit,
|
|
742
|
+
offset=page_offset,
|
|
743
|
+
)
|
|
744
|
+
except ToolError as exc:
|
|
745
|
+
handle_tool_error(exc, json_output)
|
|
746
|
+
return
|
|
747
|
+
print_data(data, json_output)
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
@app.command()
|
|
751
|
+
def spawn(
|
|
752
|
+
provider: Annotated[str, typer.Option("--provider", help="Explicit provider id.")],
|
|
753
|
+
task: Annotated[str | None, typer.Option("--task", help="Worker task.")] = None,
|
|
754
|
+
task_stdin: Annotated[bool, typer.Option("--task-stdin", help="Read worker task from stdin.")] = False,
|
|
755
|
+
repo: Annotated[Path, typer.Option("--repo", help="Repository path.")] = Path("."),
|
|
756
|
+
role: Annotated[
|
|
757
|
+
str,
|
|
758
|
+
typer.Option("--role", help="Worker role: explorer, reviewer, implementer, tester, or custom."),
|
|
759
|
+
] = "explorer",
|
|
760
|
+
runtime: Annotated[str, typer.Option("--runtime", help="Runtime. v0.1 supports tmux only.")] = "tmux",
|
|
761
|
+
isolation: Annotated[
|
|
762
|
+
str,
|
|
763
|
+
typer.Option(
|
|
764
|
+
"--isolation",
|
|
765
|
+
help="Isolation: read_only, shared, or worktree. Worktree is explicit, not the default.",
|
|
766
|
+
),
|
|
767
|
+
] = "read_only",
|
|
768
|
+
model: Annotated[
|
|
769
|
+
str | None,
|
|
770
|
+
typer.Option("--model", help="Explicit model id. Defaults to the selected provider's configured default_model."),
|
|
771
|
+
] = None,
|
|
772
|
+
account: Annotated[
|
|
773
|
+
str | None,
|
|
774
|
+
typer.Option("--account", help="Optional account label/id to persist with the session."),
|
|
775
|
+
] = None,
|
|
776
|
+
allowed_file: Annotated[
|
|
777
|
+
list[str] | None,
|
|
778
|
+
typer.Option("--allowed-file", help="Advisory allowed file path. Repeat for multiple paths."),
|
|
779
|
+
] = None,
|
|
780
|
+
max_runtime_seconds: Annotated[
|
|
781
|
+
int | None,
|
|
782
|
+
typer.Option("--max-runtime-seconds", help="Terminate on the next control operation after this runtime."),
|
|
783
|
+
] = None,
|
|
784
|
+
max_turns: Annotated[
|
|
785
|
+
int | None,
|
|
786
|
+
typer.Option("--max-turns", help="Maximum number of send-message turns AgentPool will allow."),
|
|
787
|
+
] = None,
|
|
788
|
+
supervision: Annotated[
|
|
789
|
+
str,
|
|
790
|
+
typer.Option("--supervision", help="Supervision: interactive, autonomous, or human_visible."),
|
|
791
|
+
] = "interactive",
|
|
792
|
+
initial_prompt_mode: Annotated[
|
|
793
|
+
str,
|
|
794
|
+
typer.Option("--initial-prompt-mode", help="Initial prompt mode: provider_default, send_after_launch, arg, or stdin."),
|
|
795
|
+
] = "provider_default",
|
|
796
|
+
reasoning_effort: Annotated[
|
|
797
|
+
str | None,
|
|
798
|
+
typer.Option("--reasoning-effort", help="Provider reasoning effort override when supported, for example high."),
|
|
799
|
+
] = None,
|
|
800
|
+
service_tier: Annotated[
|
|
801
|
+
str | None,
|
|
802
|
+
typer.Option("--service-tier", help="Provider service tier override when supported, for example fast."),
|
|
803
|
+
] = None,
|
|
804
|
+
json_output: Annotated[bool, typer.Option("--json", help="Emit JSON.")] = False,
|
|
805
|
+
) -> None:
|
|
806
|
+
"""Spawn one explicitly selected worker.
|
|
807
|
+
|
|
808
|
+
Examples:
|
|
809
|
+
agentpool spawn --provider codex-cli --repo . --task "Review the auth module read-only." --isolation read_only
|
|
810
|
+
cat task.md | agentpool spawn --provider fake-question --repo . --task-stdin --json
|
|
811
|
+
agentpool spawn --provider codex-cli --repo . --task "Make the narrow patch." --isolation worktree
|
|
812
|
+
"""
|
|
813
|
+
try:
|
|
814
|
+
if task_stdin and task:
|
|
815
|
+
raise ToolError(
|
|
816
|
+
"INVALID_REQUEST",
|
|
817
|
+
"Use either --task or --task-stdin, not both.",
|
|
818
|
+
{"example": "cat task.md | agentpool spawn --provider <provider-id> --repo . --task-stdin"},
|
|
819
|
+
)
|
|
820
|
+
if task_stdin:
|
|
821
|
+
task = read_stdin_text(
|
|
822
|
+
sys.stdin.read(),
|
|
823
|
+
"task",
|
|
824
|
+
"cat task.md | agentpool spawn --provider <provider-id> --repo . --task-stdin",
|
|
825
|
+
)
|
|
826
|
+
if not task:
|
|
827
|
+
raise ToolError(
|
|
828
|
+
"INVALID_REQUEST",
|
|
829
|
+
"Missing worker task.",
|
|
830
|
+
{"example": "agentpool spawn --provider <provider-id> --repo . --task \"Inspect this repo read-only.\""},
|
|
831
|
+
)
|
|
832
|
+
data = manager().spawn_worker(
|
|
833
|
+
SpawnWorkerRequest(
|
|
834
|
+
provider_id=provider,
|
|
835
|
+
task=task,
|
|
836
|
+
repo_path=str(repo),
|
|
837
|
+
role=role, # type: ignore[arg-type]
|
|
838
|
+
runtime=runtime, # type: ignore[arg-type]
|
|
839
|
+
isolation=isolation, # type: ignore[arg-type]
|
|
840
|
+
model=model,
|
|
841
|
+
account=account,
|
|
842
|
+
allowed_files=allowed_file or [],
|
|
843
|
+
max_runtime_seconds=max_runtime_seconds,
|
|
844
|
+
max_turns=max_turns,
|
|
845
|
+
supervision=supervision, # type: ignore[arg-type]
|
|
846
|
+
initial_prompt_mode=initial_prompt_mode, # type: ignore[arg-type]
|
|
847
|
+
reasoning_effort=reasoning_effort,
|
|
848
|
+
service_tier=service_tier,
|
|
849
|
+
)
|
|
850
|
+
)
|
|
851
|
+
if json_output:
|
|
852
|
+
console.print_json(json.dumps(data, default=str))
|
|
853
|
+
else:
|
|
854
|
+
console.print(data["session"]["id"])
|
|
855
|
+
console.print(data["attach_command"])
|
|
856
|
+
except ToolError as exc:
|
|
857
|
+
handle_tool_error(exc, json_output)
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
@app.command()
|
|
861
|
+
def observe(
|
|
862
|
+
session_id: str,
|
|
863
|
+
wait_for: Annotated[str | None, typer.Option("--wait-for", help="Comma-separated events.")] = None,
|
|
864
|
+
timeout: Annotated[int, typer.Option("--timeout")] = 0,
|
|
865
|
+
detail: Annotated[str, typer.Option("--detail", help="Output detail: summary, excerpt, or full.")] = "summary",
|
|
866
|
+
max_lines: Annotated[int | None, typer.Option("--max-lines", help="tmux capture line limit.")] = None,
|
|
867
|
+
output: Annotated[Path | None, typer.Option("--output", help="Write JSON observe payload to this path.")] = None,
|
|
868
|
+
json_output: Annotated[bool, typer.Option("--json", help="Emit JSON.")] = False,
|
|
869
|
+
) -> None:
|
|
870
|
+
"""Observe worker state without dumping large transcripts by default.
|
|
871
|
+
|
|
872
|
+
Examples:
|
|
873
|
+
agentpool observe <session-id> --wait-for completed,error,question,approval_prompt --timeout 60 --json
|
|
874
|
+
agentpool observe <session-id> --detail excerpt --json
|
|
875
|
+
agentpool observe <session-id> --detail full --output /tmp/observe.json
|
|
876
|
+
"""
|
|
877
|
+
try:
|
|
878
|
+
parsed_detail = parse_detail(detail)
|
|
879
|
+
mgr = manager()
|
|
880
|
+
observed = mgr.observe_worker(
|
|
881
|
+
session_id,
|
|
882
|
+
wait_for=wait_for.split(",") if wait_for else None,
|
|
883
|
+
timeout_seconds=timeout,
|
|
884
|
+
include_screen=parsed_detail != "summary",
|
|
885
|
+
include_recent_log=False,
|
|
886
|
+
max_lines=max_lines,
|
|
887
|
+
).model_dump(mode="json")
|
|
888
|
+
data = observe_payload(observed, mgr.artifact_manifest(session_id), parsed_detail)
|
|
889
|
+
if output:
|
|
890
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
891
|
+
output.write_text(json.dumps(data, indent=2, default=str), encoding="utf-8")
|
|
892
|
+
if json_output:
|
|
893
|
+
console.print_json(json.dumps({"output_path": str(output), **data}, default=str))
|
|
894
|
+
else:
|
|
895
|
+
console.print(str(output))
|
|
896
|
+
return
|
|
897
|
+
print_data(data, json_output)
|
|
898
|
+
except ToolError as exc:
|
|
899
|
+
handle_tool_error(exc, json_output)
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
@app.command()
|
|
903
|
+
def send(
|
|
904
|
+
session_id: str,
|
|
905
|
+
message: Annotated[str | None, typer.Argument(help="Message to send. Omit with --stdin.")] = None,
|
|
906
|
+
stdin: Annotated[bool, typer.Option("--stdin", help="Read message from stdin.")] = False,
|
|
907
|
+
json_output: Annotated[bool, typer.Option("--json", help="Emit JSON.")] = False,
|
|
908
|
+
) -> None:
|
|
909
|
+
"""Send one message to a worker.
|
|
910
|
+
|
|
911
|
+
Examples:
|
|
912
|
+
agentpool send <session-id> "Continue with the review." --json
|
|
913
|
+
cat reply.md | agentpool send <session-id> --stdin --json
|
|
914
|
+
agentpool send <session-id> "" --json
|
|
915
|
+
"""
|
|
916
|
+
try:
|
|
917
|
+
if stdin and message:
|
|
918
|
+
raise ToolError(
|
|
919
|
+
"INVALID_REQUEST",
|
|
920
|
+
"Use either a message argument or --stdin, not both.",
|
|
921
|
+
{"example": "cat reply.md | agentpool send <session-id> --stdin"},
|
|
922
|
+
)
|
|
923
|
+
if stdin:
|
|
924
|
+
message = read_stdin_text(
|
|
925
|
+
sys.stdin.read(),
|
|
926
|
+
"message",
|
|
927
|
+
"cat reply.md | agentpool send <session-id> --stdin",
|
|
928
|
+
)
|
|
929
|
+
if message is None:
|
|
930
|
+
raise ToolError(
|
|
931
|
+
"INVALID_REQUEST",
|
|
932
|
+
"Missing message.",
|
|
933
|
+
{"example": "agentpool send <session-id> \"Continue.\""},
|
|
934
|
+
)
|
|
935
|
+
print_data(manager().send_worker_message(session_id, message), json_output)
|
|
936
|
+
except ToolError as exc:
|
|
937
|
+
handle_tool_error(exc, json_output)
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
@app.command()
|
|
941
|
+
def keys(
|
|
942
|
+
session_id: str,
|
|
943
|
+
key: list[str],
|
|
944
|
+
json_output: Annotated[bool, typer.Option("--json", help="Emit JSON.")] = False,
|
|
945
|
+
) -> None:
|
|
946
|
+
"""Send raw keys when policy allows it.
|
|
947
|
+
|
|
948
|
+
Examples:
|
|
949
|
+
agentpool keys <session-id> C-c --json
|
|
950
|
+
agentpool keys <session-id> Enter --json
|
|
951
|
+
"""
|
|
952
|
+
try:
|
|
953
|
+
print_data(manager().send_worker_keys(session_id, key), json_output)
|
|
954
|
+
except ToolError as exc:
|
|
955
|
+
handle_tool_error(exc, json_output)
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
@app.command()
|
|
959
|
+
def interrupt(
|
|
960
|
+
session_id: str,
|
|
961
|
+
json_output: Annotated[bool, typer.Option("--json", help="Emit JSON.")] = False,
|
|
962
|
+
) -> None:
|
|
963
|
+
"""Interrupt a worker.
|
|
964
|
+
|
|
965
|
+
Examples:
|
|
966
|
+
agentpool interrupt <session-id> --json
|
|
967
|
+
"""
|
|
968
|
+
try:
|
|
969
|
+
print_data(manager().interrupt_worker(session_id), json_output)
|
|
970
|
+
except ToolError as exc:
|
|
971
|
+
handle_tool_error(exc, json_output)
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
@app.command()
|
|
975
|
+
def attach(
|
|
976
|
+
session_id: str,
|
|
977
|
+
json_output: Annotated[bool, typer.Option("--json", help="Emit JSON.")] = False,
|
|
978
|
+
) -> None:
|
|
979
|
+
"""Print attach information for a worker.
|
|
980
|
+
|
|
981
|
+
Examples:
|
|
982
|
+
agentpool attach <session-id>
|
|
983
|
+
agentpool attach <session-id> --json
|
|
984
|
+
"""
|
|
985
|
+
try:
|
|
986
|
+
data = manager().attach_info(session_id)
|
|
987
|
+
if json_output:
|
|
988
|
+
console.print_json(json.dumps(data, default=str))
|
|
989
|
+
else:
|
|
990
|
+
console.print(data["attach_command"])
|
|
991
|
+
except ToolError as exc:
|
|
992
|
+
handle_tool_error(exc, json_output)
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
@app.command()
|
|
996
|
+
def collect(
|
|
997
|
+
session_id: str,
|
|
998
|
+
detail: Annotated[str, typer.Option("--detail", help="Output detail: summary, excerpt, or full.")] = "summary",
|
|
999
|
+
json_output: Annotated[bool, typer.Option("--json", help="Emit JSON.")] = False,
|
|
1000
|
+
) -> None:
|
|
1001
|
+
"""Collect worker artifacts and return paths by default.
|
|
1002
|
+
|
|
1003
|
+
Examples:
|
|
1004
|
+
agentpool collect <session-id> --json
|
|
1005
|
+
agentpool collect <session-id> --detail excerpt --json
|
|
1006
|
+
agentpool collect <session-id> --detail full
|
|
1007
|
+
"""
|
|
1008
|
+
try:
|
|
1009
|
+
parsed_detail = parse_detail(detail)
|
|
1010
|
+
print_data(collect_payload(manager().collect_worker_artifacts(session_id), parsed_detail), json_output)
|
|
1011
|
+
except ToolError as exc:
|
|
1012
|
+
handle_tool_error(exc, json_output)
|
|
1013
|
+
|
|
1014
|
+
|
|
1015
|
+
@app.command("artifacts")
|
|
1016
|
+
def artifacts_command(
|
|
1017
|
+
session_id: str,
|
|
1018
|
+
json_output: Annotated[bool, typer.Option("--json", help="Emit JSON.")] = False,
|
|
1019
|
+
) -> None:
|
|
1020
|
+
"""List artifact paths for a worker.
|
|
1021
|
+
|
|
1022
|
+
Examples:
|
|
1023
|
+
agentpool artifacts <session-id> --json
|
|
1024
|
+
agentpool artifacts <session-id>
|
|
1025
|
+
"""
|
|
1026
|
+
try:
|
|
1027
|
+
data = manager().artifact_manifest(session_id)
|
|
1028
|
+
print_data(data, json_output)
|
|
1029
|
+
except ToolError as exc:
|
|
1030
|
+
handle_tool_error(exc, json_output)
|
|
1031
|
+
|
|
1032
|
+
|
|
1033
|
+
@app.command()
|
|
1034
|
+
def transcript(
|
|
1035
|
+
session_id: str,
|
|
1036
|
+
offset: Annotated[int, typer.Option("--offset", help="Zero-based byte offset.")] = 0,
|
|
1037
|
+
limit: Annotated[int, typer.Option("--limit", help="Maximum bytes to read.")] = 4000,
|
|
1038
|
+
tail_lines: Annotated[int | None, typer.Option("--tail-lines", help="Read the last N transcript lines.")] = None,
|
|
1039
|
+
json_output: Annotated[bool, typer.Option("--json", help="Emit JSON.")] = False,
|
|
1040
|
+
) -> None:
|
|
1041
|
+
"""Read a bounded transcript page or tail.
|
|
1042
|
+
|
|
1043
|
+
Examples:
|
|
1044
|
+
agentpool transcript <session-id> --offset 0 --limit 4000 --json
|
|
1045
|
+
agentpool transcript <session-id> --offset 4000 --limit 4000 --json
|
|
1046
|
+
agentpool transcript <session-id> --tail-lines 80
|
|
1047
|
+
"""
|
|
1048
|
+
try:
|
|
1049
|
+
data = manager().read_transcript(session_id, offset=offset, limit=limit, tail_lines=tail_lines)
|
|
1050
|
+
if json_output:
|
|
1051
|
+
console.print_json(json.dumps(data, default=str))
|
|
1052
|
+
else:
|
|
1053
|
+
console.print(data["text"], end="")
|
|
1054
|
+
except ToolError as exc:
|
|
1055
|
+
handle_tool_error(exc, json_output)
|
|
1056
|
+
|
|
1057
|
+
|
|
1058
|
+
@app.command()
|
|
1059
|
+
def terminate(
|
|
1060
|
+
session_id: str,
|
|
1061
|
+
json_output: Annotated[bool, typer.Option("--json", help="Emit JSON.")] = False,
|
|
1062
|
+
) -> None:
|
|
1063
|
+
"""Terminate a worker.
|
|
1064
|
+
|
|
1065
|
+
Examples:
|
|
1066
|
+
agentpool terminate <session-id> --json
|
|
1067
|
+
"""
|
|
1068
|
+
try:
|
|
1069
|
+
print_data(manager().terminate_worker(session_id), json_output)
|
|
1070
|
+
except ToolError as exc:
|
|
1071
|
+
handle_tool_error(exc, json_output)
|
|
1072
|
+
|
|
1073
|
+
|
|
1074
|
+
@app.command()
|
|
1075
|
+
def mcp(
|
|
1076
|
+
toolsets: Annotated[
|
|
1077
|
+
str | None,
|
|
1078
|
+
typer.Option("--toolsets", help="Comma-separated MCP toolsets. Defaults to env or default."),
|
|
1079
|
+
] = None,
|
|
1080
|
+
tools: Annotated[
|
|
1081
|
+
str | None,
|
|
1082
|
+
typer.Option("--tools", help="Comma-separated extra MCP tool names to expose."),
|
|
1083
|
+
] = None,
|
|
1084
|
+
lockdown: Annotated[bool, typer.Option("--lockdown", help="Suppress inline untrusted worker output.")] = False,
|
|
1085
|
+
) -> None:
|
|
1086
|
+
"""Start the AgentPool MCP server.
|
|
1087
|
+
|
|
1088
|
+
Examples:
|
|
1089
|
+
agentpool mcp
|
|
1090
|
+
agentpool mcp --toolsets default,stats
|
|
1091
|
+
AGENTPOOL_MCP_LOCKDOWN=1 agentpool mcp --toolsets default
|
|
1092
|
+
"""
|
|
1093
|
+
run_mcp_server(toolsets=toolsets, tools=tools, lockdown=lockdown)
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
@config_app.command("path")
|
|
1097
|
+
def config_path() -> None:
|
|
1098
|
+
console.print(str(DEFAULT_CONFIG_PATH))
|
|
1099
|
+
|
|
1100
|
+
|
|
1101
|
+
@config_app.command("print")
|
|
1102
|
+
def config_print() -> None:
|
|
1103
|
+
console.print(yaml.safe_dump(load_config().model_dump(mode="json"), sort_keys=False))
|
|
1104
|
+
|
|
1105
|
+
|
|
1106
|
+
@config_app.command("validate")
|
|
1107
|
+
def config_validate(
|
|
1108
|
+
path: Annotated[Path | None, typer.Option("--path", help="Config path. Defaults to ~/.agentpool/config.yaml.")] = None,
|
|
1109
|
+
json_output: Annotated[bool, typer.Option("--json", help="Emit JSON.")] = False,
|
|
1110
|
+
) -> None:
|
|
1111
|
+
try:
|
|
1112
|
+
config = load_config(path)
|
|
1113
|
+
data = validate_config(config)
|
|
1114
|
+
if json_output:
|
|
1115
|
+
console.print_json(json.dumps(data, default=str))
|
|
1116
|
+
return
|
|
1117
|
+
console.print(f"config {'ok' if data['ok'] else 'failed'}")
|
|
1118
|
+
for warning in data["warnings"]:
|
|
1119
|
+
console.print(f"[yellow]warning[/yellow]: {warning}")
|
|
1120
|
+
for error in data["errors"]:
|
|
1121
|
+
console.print(f"[red]error[/red]: {error}")
|
|
1122
|
+
if not data["ok"]:
|
|
1123
|
+
raise typer.Exit(1)
|
|
1124
|
+
except Exception as exc:
|
|
1125
|
+
if json_output:
|
|
1126
|
+
console.print_json(json.dumps({"ok": False, "errors": [str(exc)], "warnings": []}, default=str))
|
|
1127
|
+
else:
|
|
1128
|
+
console.print(f"[red]error[/red]: {exc}")
|
|
1129
|
+
raise typer.Exit(1)
|
|
1130
|
+
|
|
1131
|
+
|
|
1132
|
+
@leases_app.command("list")
|
|
1133
|
+
def leases_list(
|
|
1134
|
+
session_id: Annotated[str | None, typer.Option("--session-id", help="Filter by session id.")] = None,
|
|
1135
|
+
repo: Annotated[Path | None, typer.Option("--repo", help="Filter by repository path.")] = None,
|
|
1136
|
+
all_leases: Annotated[bool, typer.Option("--all", help="Include released and expired leases.")] = False,
|
|
1137
|
+
json_output: Annotated[bool, typer.Option("--json", help="Emit JSON.")] = False,
|
|
1138
|
+
) -> None:
|
|
1139
|
+
try:
|
|
1140
|
+
data = manager().list_file_leases(
|
|
1141
|
+
session_id=session_id,
|
|
1142
|
+
repo_path=str(repo) if repo else None,
|
|
1143
|
+
active_only=not all_leases,
|
|
1144
|
+
)
|
|
1145
|
+
print_data(data, json_output)
|
|
1146
|
+
except ToolError as exc:
|
|
1147
|
+
handle_tool_error(exc, json_output)
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
@leases_app.command("acquire")
|
|
1151
|
+
def leases_acquire(
|
|
1152
|
+
session_id: Annotated[str, typer.Option("--session-id", help="Owning session id.")],
|
|
1153
|
+
file_path: Annotated[str, typer.Option("--file", help="File path to lease.")],
|
|
1154
|
+
mode: Annotated[str, typer.Option("--mode", help="Lease mode: read or write.")] = "write",
|
|
1155
|
+
ttl_seconds: Annotated[int | None, typer.Option("--ttl-seconds", help="Optional lease TTL.")] = None,
|
|
1156
|
+
json_output: Annotated[bool, typer.Option("--json", help="Emit JSON.")] = False,
|
|
1157
|
+
) -> None:
|
|
1158
|
+
try:
|
|
1159
|
+
print_data(manager().acquire_file_lease(session_id, file_path, mode=mode, ttl_seconds=ttl_seconds), json_output)
|
|
1160
|
+
except ToolError as exc:
|
|
1161
|
+
handle_tool_error(exc, json_output)
|
|
1162
|
+
|
|
1163
|
+
|
|
1164
|
+
@leases_app.command("release")
|
|
1165
|
+
def leases_release(
|
|
1166
|
+
lease_id: Annotated[int | None, typer.Option("--lease-id", help="Lease id to release.")] = None,
|
|
1167
|
+
session_id: Annotated[str | None, typer.Option("--session-id", help="Release leases for this session.")] = None,
|
|
1168
|
+
file_path: Annotated[str | None, typer.Option("--file", help="Optional file path filter with --session-id.")] = None,
|
|
1169
|
+
json_output: Annotated[bool, typer.Option("--json", help="Emit JSON.")] = False,
|
|
1170
|
+
) -> None:
|
|
1171
|
+
try:
|
|
1172
|
+
print_data(manager().release_file_lease(lease_id=lease_id, session_id=session_id, file_path=file_path), json_output)
|
|
1173
|
+
except ToolError as exc:
|
|
1174
|
+
handle_tool_error(exc, json_output)
|
|
1175
|
+
except ValueError as exc:
|
|
1176
|
+
handle_tool_error(ToolError("INVALID_LEASE_RELEASE", str(exc)), json_output)
|
|
1177
|
+
|
|
1178
|
+
|
|
1179
|
+
@worktrees_app.command("list")
|
|
1180
|
+
def worktrees_list(
|
|
1181
|
+
repo: Annotated[Path, typer.Option("--repo", help="Repository path.")] = Path("."),
|
|
1182
|
+
json_output: Annotated[bool, typer.Option("--json", help="Emit JSON.")] = False,
|
|
1183
|
+
) -> None:
|
|
1184
|
+
try:
|
|
1185
|
+
print_data(manager().list_worktrees(str(repo)), json_output)
|
|
1186
|
+
except ToolError as exc:
|
|
1187
|
+
handle_tool_error(exc, json_output)
|
|
1188
|
+
|
|
1189
|
+
|
|
1190
|
+
@worktrees_app.command("cleanup")
|
|
1191
|
+
def worktrees_cleanup(
|
|
1192
|
+
session_id: Annotated[str, typer.Option("--session-id", help="Session whose AgentPool worktree should be removed.")],
|
|
1193
|
+
force: Annotated[bool, typer.Option("--force", help="Remove even if active or dirty.")] = False,
|
|
1194
|
+
json_output: Annotated[bool, typer.Option("--json", help="Emit JSON.")] = False,
|
|
1195
|
+
) -> None:
|
|
1196
|
+
try:
|
|
1197
|
+
print_data(manager().cleanup_worktree(session_id, force=force), json_output)
|
|
1198
|
+
except ToolError as exc:
|
|
1199
|
+
handle_tool_error(exc, json_output)
|