codex-autorunner 1.0.0__py3-none-any.whl → 1.2.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.
- codex_autorunner/__init__.py +12 -1
- codex_autorunner/agents/codex/harness.py +1 -1
- codex_autorunner/agents/opencode/client.py +113 -4
- codex_autorunner/agents/opencode/constants.py +3 -0
- codex_autorunner/agents/opencode/harness.py +6 -1
- codex_autorunner/agents/opencode/runtime.py +59 -18
- codex_autorunner/agents/opencode/supervisor.py +4 -0
- codex_autorunner/agents/registry.py +36 -7
- codex_autorunner/bootstrap.py +226 -4
- codex_autorunner/cli.py +5 -1174
- codex_autorunner/codex_cli.py +20 -84
- codex_autorunner/core/__init__.py +20 -0
- codex_autorunner/core/about_car.py +119 -1
- codex_autorunner/core/app_server_ids.py +59 -0
- codex_autorunner/core/app_server_threads.py +17 -2
- codex_autorunner/core/app_server_utils.py +165 -0
- codex_autorunner/core/archive.py +349 -0
- codex_autorunner/core/codex_runner.py +6 -2
- codex_autorunner/core/config.py +433 -4
- codex_autorunner/core/context_awareness.py +38 -0
- codex_autorunner/core/docs.py +0 -122
- codex_autorunner/core/drafts.py +58 -4
- codex_autorunner/core/exceptions.py +4 -0
- codex_autorunner/core/filebox.py +265 -0
- codex_autorunner/core/flows/controller.py +96 -2
- codex_autorunner/core/flows/models.py +13 -0
- codex_autorunner/core/flows/reasons.py +52 -0
- codex_autorunner/core/flows/reconciler.py +134 -0
- codex_autorunner/core/flows/runtime.py +57 -4
- codex_autorunner/core/flows/store.py +142 -7
- codex_autorunner/core/flows/transition.py +27 -15
- codex_autorunner/core/flows/ux_helpers.py +272 -0
- codex_autorunner/core/flows/worker_process.py +32 -6
- codex_autorunner/core/git_utils.py +62 -0
- codex_autorunner/core/hub.py +291 -20
- codex_autorunner/core/lifecycle_events.py +253 -0
- codex_autorunner/core/notifications.py +14 -2
- codex_autorunner/core/path_utils.py +2 -1
- codex_autorunner/core/pma_audit.py +224 -0
- codex_autorunner/core/pma_context.py +496 -0
- codex_autorunner/core/pma_dispatch_interceptor.py +284 -0
- codex_autorunner/core/pma_lifecycle.py +527 -0
- codex_autorunner/core/pma_queue.py +367 -0
- codex_autorunner/core/pma_safety.py +221 -0
- codex_autorunner/core/pma_state.py +115 -0
- codex_autorunner/core/ports/__init__.py +28 -0
- codex_autorunner/{integrations/agents → core/ports}/agent_backend.py +13 -8
- codex_autorunner/core/ports/backend_orchestrator.py +41 -0
- codex_autorunner/{integrations/agents → core/ports}/run_event.py +23 -6
- codex_autorunner/core/prompt.py +0 -80
- codex_autorunner/core/prompts.py +56 -172
- codex_autorunner/core/redaction.py +0 -4
- codex_autorunner/core/review_context.py +11 -9
- codex_autorunner/core/runner_controller.py +35 -33
- codex_autorunner/core/runner_state.py +147 -0
- codex_autorunner/core/runtime.py +829 -0
- codex_autorunner/core/sqlite_utils.py +13 -4
- codex_autorunner/core/state.py +7 -10
- codex_autorunner/core/state_roots.py +62 -0
- codex_autorunner/core/supervisor_protocol.py +15 -0
- codex_autorunner/core/templates/__init__.py +39 -0
- codex_autorunner/core/templates/git_mirror.py +234 -0
- codex_autorunner/core/templates/provenance.py +56 -0
- codex_autorunner/core/templates/scan_cache.py +120 -0
- codex_autorunner/core/text_delta_coalescer.py +54 -0
- codex_autorunner/core/ticket_linter_cli.py +218 -0
- codex_autorunner/core/ticket_manager_cli.py +494 -0
- codex_autorunner/core/time_utils.py +11 -0
- codex_autorunner/core/types.py +18 -0
- codex_autorunner/core/update.py +4 -5
- codex_autorunner/core/update_paths.py +28 -0
- codex_autorunner/core/usage.py +164 -12
- codex_autorunner/core/utils.py +125 -15
- codex_autorunner/flows/review/__init__.py +17 -0
- codex_autorunner/{core/review.py → flows/review/service.py} +37 -34
- codex_autorunner/flows/ticket_flow/definition.py +52 -3
- codex_autorunner/integrations/agents/__init__.py +11 -19
- codex_autorunner/integrations/agents/backend_orchestrator.py +302 -0
- codex_autorunner/integrations/agents/codex_adapter.py +90 -0
- codex_autorunner/integrations/agents/codex_backend.py +177 -25
- codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
- codex_autorunner/integrations/agents/opencode_backend.py +305 -32
- codex_autorunner/integrations/agents/runner.py +86 -0
- codex_autorunner/integrations/agents/wiring.py +279 -0
- codex_autorunner/integrations/app_server/client.py +7 -60
- codex_autorunner/integrations/app_server/env.py +2 -107
- codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
- codex_autorunner/integrations/telegram/adapter.py +65 -0
- codex_autorunner/integrations/telegram/config.py +46 -0
- codex_autorunner/integrations/telegram/constants.py +1 -1
- codex_autorunner/integrations/telegram/doctor.py +228 -6
- codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -0
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +236 -74
- codex_autorunner/integrations/telegram/handlers/commands/files.py +314 -75
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +1496 -71
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +206 -48
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +20 -3
- codex_autorunner/integrations/telegram/handlers/messages.py +27 -1
- codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
- codex_autorunner/integrations/telegram/helpers.py +22 -1
- codex_autorunner/integrations/telegram/runtime.py +9 -4
- codex_autorunner/integrations/telegram/service.py +45 -10
- codex_autorunner/integrations/telegram/state.py +38 -0
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +338 -43
- codex_autorunner/integrations/telegram/transport.py +13 -4
- codex_autorunner/integrations/templates/__init__.py +27 -0
- codex_autorunner/integrations/templates/scan_agent.py +312 -0
- codex_autorunner/routes/__init__.py +37 -76
- codex_autorunner/routes/agents.py +2 -137
- codex_autorunner/routes/analytics.py +2 -238
- codex_autorunner/routes/app_server.py +2 -131
- codex_autorunner/routes/base.py +2 -596
- codex_autorunner/routes/file_chat.py +4 -833
- codex_autorunner/routes/flows.py +4 -977
- codex_autorunner/routes/messages.py +4 -456
- codex_autorunner/routes/repos.py +2 -196
- codex_autorunner/routes/review.py +2 -147
- codex_autorunner/routes/sessions.py +2 -175
- codex_autorunner/routes/settings.py +2 -168
- codex_autorunner/routes/shared.py +2 -275
- codex_autorunner/routes/system.py +4 -193
- codex_autorunner/routes/usage.py +2 -86
- codex_autorunner/routes/voice.py +2 -119
- codex_autorunner/routes/workspace.py +2 -270
- codex_autorunner/server.py +4 -4
- codex_autorunner/static/agentControls.js +61 -16
- codex_autorunner/static/app.js +126 -14
- codex_autorunner/static/archive.js +826 -0
- codex_autorunner/static/archiveApi.js +37 -0
- codex_autorunner/static/autoRefresh.js +7 -7
- codex_autorunner/static/chatUploads.js +137 -0
- codex_autorunner/static/dashboard.js +224 -171
- codex_autorunner/static/docChatCore.js +185 -13
- codex_autorunner/static/fileChat.js +68 -40
- codex_autorunner/static/fileboxUi.js +159 -0
- codex_autorunner/static/hub.js +114 -131
- codex_autorunner/static/index.html +375 -49
- codex_autorunner/static/messages.js +568 -87
- codex_autorunner/static/notifications.js +255 -0
- codex_autorunner/static/pma.js +1167 -0
- codex_autorunner/static/preserve.js +17 -0
- codex_autorunner/static/settings.js +128 -6
- codex_autorunner/static/smartRefresh.js +52 -0
- codex_autorunner/static/streamUtils.js +57 -0
- codex_autorunner/static/styles.css +9798 -6143
- codex_autorunner/static/tabs.js +152 -11
- codex_autorunner/static/templateReposSettings.js +225 -0
- codex_autorunner/static/terminal.js +18 -0
- codex_autorunner/static/ticketChatActions.js +165 -3
- codex_autorunner/static/ticketChatStream.js +17 -119
- codex_autorunner/static/ticketEditor.js +137 -15
- codex_autorunner/static/ticketTemplates.js +798 -0
- codex_autorunner/static/tickets.js +821 -98
- codex_autorunner/static/turnEvents.js +27 -0
- codex_autorunner/static/turnResume.js +33 -0
- codex_autorunner/static/utils.js +39 -0
- codex_autorunner/static/workspace.js +389 -82
- codex_autorunner/static/workspaceFileBrowser.js +15 -13
- codex_autorunner/surfaces/__init__.py +5 -0
- codex_autorunner/surfaces/cli/__init__.py +6 -0
- codex_autorunner/surfaces/cli/cli.py +2534 -0
- codex_autorunner/surfaces/cli/codex_cli.py +20 -0
- codex_autorunner/surfaces/cli/pma_cli.py +817 -0
- codex_autorunner/surfaces/telegram/__init__.py +3 -0
- codex_autorunner/surfaces/web/__init__.py +1 -0
- codex_autorunner/surfaces/web/app.py +2223 -0
- codex_autorunner/surfaces/web/hub_jobs.py +192 -0
- codex_autorunner/surfaces/web/middleware.py +587 -0
- codex_autorunner/surfaces/web/pty_session.py +370 -0
- codex_autorunner/surfaces/web/review.py +6 -0
- codex_autorunner/surfaces/web/routes/__init__.py +82 -0
- codex_autorunner/surfaces/web/routes/agents.py +138 -0
- codex_autorunner/surfaces/web/routes/analytics.py +284 -0
- codex_autorunner/surfaces/web/routes/app_server.py +132 -0
- codex_autorunner/surfaces/web/routes/archive.py +357 -0
- codex_autorunner/surfaces/web/routes/base.py +615 -0
- codex_autorunner/surfaces/web/routes/file_chat.py +1117 -0
- codex_autorunner/surfaces/web/routes/filebox.py +227 -0
- codex_autorunner/surfaces/web/routes/flows.py +1354 -0
- codex_autorunner/surfaces/web/routes/messages.py +490 -0
- codex_autorunner/surfaces/web/routes/pma.py +1652 -0
- codex_autorunner/surfaces/web/routes/repos.py +197 -0
- codex_autorunner/surfaces/web/routes/review.py +148 -0
- codex_autorunner/surfaces/web/routes/sessions.py +176 -0
- codex_autorunner/surfaces/web/routes/settings.py +169 -0
- codex_autorunner/surfaces/web/routes/shared.py +277 -0
- codex_autorunner/surfaces/web/routes/system.py +196 -0
- codex_autorunner/surfaces/web/routes/templates.py +634 -0
- codex_autorunner/surfaces/web/routes/usage.py +89 -0
- codex_autorunner/surfaces/web/routes/voice.py +120 -0
- codex_autorunner/surfaces/web/routes/workspace.py +271 -0
- codex_autorunner/surfaces/web/runner_manager.py +25 -0
- codex_autorunner/surfaces/web/schemas.py +469 -0
- codex_autorunner/surfaces/web/static_assets.py +490 -0
- codex_autorunner/surfaces/web/static_refresh.py +86 -0
- codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
- codex_autorunner/tickets/__init__.py +8 -1
- codex_autorunner/tickets/agent_pool.py +53 -4
- codex_autorunner/tickets/files.py +37 -16
- codex_autorunner/tickets/lint.py +50 -0
- codex_autorunner/tickets/models.py +6 -1
- codex_autorunner/tickets/outbox.py +50 -2
- codex_autorunner/tickets/runner.py +396 -57
- codex_autorunner/web/__init__.py +5 -1
- codex_autorunner/web/app.py +2 -1949
- codex_autorunner/web/hub_jobs.py +2 -191
- codex_autorunner/web/middleware.py +2 -586
- codex_autorunner/web/pty_session.py +2 -369
- codex_autorunner/web/runner_manager.py +2 -24
- codex_autorunner/web/schemas.py +2 -376
- codex_autorunner/web/static_assets.py +4 -441
- codex_autorunner/web/static_refresh.py +2 -85
- codex_autorunner/web/terminal_sessions.py +2 -77
- codex_autorunner/workspace/paths.py +49 -33
- codex_autorunner-1.2.0.dist-info/METADATA +150 -0
- codex_autorunner-1.2.0.dist-info/RECORD +339 -0
- codex_autorunner/core/adapter_utils.py +0 -21
- codex_autorunner/core/engine.py +0 -2653
- codex_autorunner/core/static_assets.py +0 -55
- codex_autorunner-1.0.0.dist-info/METADATA +0 -246
- codex_autorunner-1.0.0.dist-info/RECORD +0 -251
- /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/WHEEL +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,817 @@
|
|
|
1
|
+
"""PMA CLI commands for Project Management Assistant."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from ...bootstrap import ensure_pma_docs
|
|
13
|
+
from ...core.config import load_hub_config
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
pma_app = typer.Typer(add_completion=False, rich_markup_mode=None)
|
|
18
|
+
docs_app = typer.Typer(add_completion=False, rich_markup_mode=None, name="docs")
|
|
19
|
+
context_app = typer.Typer(add_completion=False, rich_markup_mode=None, name="context")
|
|
20
|
+
pma_app.add_typer(docs_app)
|
|
21
|
+
pma_app.add_typer(context_app)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _build_pma_url(config, path: str) -> str:
|
|
25
|
+
base_path = config.server_base_path or ""
|
|
26
|
+
if base_path.endswith("/") and path.startswith("/"):
|
|
27
|
+
base_path = base_path[:-1]
|
|
28
|
+
return f"http://{config.server_host}:{config.server_port}{base_path}/hub/pma{path}"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _resolve_hub_path(path: Optional[Path]) -> Path:
|
|
32
|
+
if path:
|
|
33
|
+
candidate = path
|
|
34
|
+
if candidate.is_dir():
|
|
35
|
+
candidate = candidate / "codex-autorunner.yml"
|
|
36
|
+
if not candidate.exists():
|
|
37
|
+
candidate = path / ".codex-autorunner" / "config.yml"
|
|
38
|
+
if candidate.exists():
|
|
39
|
+
return candidate.parent.parent.resolve()
|
|
40
|
+
return Path.cwd()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _request_json(
|
|
44
|
+
method: str,
|
|
45
|
+
url: str,
|
|
46
|
+
payload: Optional[dict] = None,
|
|
47
|
+
token_env: Optional[str] = None,
|
|
48
|
+
) -> dict:
|
|
49
|
+
import os
|
|
50
|
+
|
|
51
|
+
headers = None
|
|
52
|
+
if token_env:
|
|
53
|
+
token = os.environ.get(token_env)
|
|
54
|
+
if token and token.strip():
|
|
55
|
+
headers = {"Authorization": f"Bearer {token.strip()}"}
|
|
56
|
+
response = httpx.request(method, url, json=payload, timeout=30.0, headers=headers)
|
|
57
|
+
response.raise_for_status()
|
|
58
|
+
data = response.json()
|
|
59
|
+
return data if isinstance(data, dict) else {}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _is_json_response_error(data: dict) -> Optional[str]:
|
|
63
|
+
if not isinstance(data, dict):
|
|
64
|
+
return "Unexpected response format"
|
|
65
|
+
if data.get("detail"):
|
|
66
|
+
return str(data["detail"])
|
|
67
|
+
if data.get("error"):
|
|
68
|
+
return str(data["error"])
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@pma_app.command("chat")
|
|
73
|
+
def pma_chat(
|
|
74
|
+
message: str = typer.Argument(..., help="Message to send to PMA"),
|
|
75
|
+
agent: Optional[str] = typer.Option(
|
|
76
|
+
None, "--agent", help="Agent to use (codex|opencode)"
|
|
77
|
+
),
|
|
78
|
+
model: Optional[str] = typer.Option(None, "--model", help="Model override"),
|
|
79
|
+
reasoning: Optional[str] = typer.Option(
|
|
80
|
+
None, "--reasoning", help="Reasoning effort override"
|
|
81
|
+
),
|
|
82
|
+
stream: bool = typer.Option(False, "--stream", help="Stream response tokens"),
|
|
83
|
+
output_json: bool = typer.Option(False, "--json", help="Emit JSON output"),
|
|
84
|
+
path: Optional[Path] = typer.Option(None, "--path", "--hub", help="Hub root path"),
|
|
85
|
+
):
|
|
86
|
+
"""Send a message to the Project Management Assistant."""
|
|
87
|
+
hub_root = _resolve_hub_path(path)
|
|
88
|
+
try:
|
|
89
|
+
config = load_hub_config(hub_root)
|
|
90
|
+
except Exception as exc:
|
|
91
|
+
typer.echo(f"Failed to load hub config: {exc}", err=True)
|
|
92
|
+
raise typer.Exit(code=1) from None
|
|
93
|
+
|
|
94
|
+
url = _build_pma_url(config, "/chat")
|
|
95
|
+
payload: dict[str, Any] = {"message": message, "stream": stream}
|
|
96
|
+
if agent:
|
|
97
|
+
payload["agent"] = agent
|
|
98
|
+
if model:
|
|
99
|
+
payload["model"] = model
|
|
100
|
+
if reasoning:
|
|
101
|
+
payload["reasoning"] = reasoning
|
|
102
|
+
|
|
103
|
+
if stream:
|
|
104
|
+
import os
|
|
105
|
+
|
|
106
|
+
from ...integrations.app_server.event_buffer import parse_sse_line
|
|
107
|
+
|
|
108
|
+
token_env = config.server_auth_token_env
|
|
109
|
+
headers = None
|
|
110
|
+
if token_env:
|
|
111
|
+
token = os.environ.get(token_env)
|
|
112
|
+
if token and token.strip():
|
|
113
|
+
headers = {"Authorization": f"Bearer {token.strip()}"}
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
with httpx.stream(
|
|
117
|
+
"POST", url, json=payload, timeout=240.0, headers=headers
|
|
118
|
+
) as response:
|
|
119
|
+
response.raise_for_status()
|
|
120
|
+
for line in response.iter_lines():
|
|
121
|
+
if not line:
|
|
122
|
+
continue
|
|
123
|
+
event_type, data = parse_sse_line(line)
|
|
124
|
+
if event_type is None or data is None:
|
|
125
|
+
continue
|
|
126
|
+
if event_type == "status":
|
|
127
|
+
if output_json:
|
|
128
|
+
typer.echo(
|
|
129
|
+
json.dumps({"event": "status", **data}, indent=2)
|
|
130
|
+
)
|
|
131
|
+
continue
|
|
132
|
+
if event_type == "token":
|
|
133
|
+
token = data.get("token", "") if isinstance(data, dict) else ""
|
|
134
|
+
if output_json:
|
|
135
|
+
typer.echo(
|
|
136
|
+
json.dumps({"event": "token", "token": token}, indent=2)
|
|
137
|
+
)
|
|
138
|
+
else:
|
|
139
|
+
typer.echo(token, nl=False)
|
|
140
|
+
elif event_type == "update":
|
|
141
|
+
status = data.get("status") if isinstance(data, dict) else ""
|
|
142
|
+
msg = data.get("message") if isinstance(data, dict) else ""
|
|
143
|
+
if output_json:
|
|
144
|
+
typer.echo(
|
|
145
|
+
json.dumps(
|
|
146
|
+
{
|
|
147
|
+
"event": "update",
|
|
148
|
+
"status": status,
|
|
149
|
+
"message": msg,
|
|
150
|
+
},
|
|
151
|
+
indent=2,
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
else:
|
|
155
|
+
typer.echo(f"\nStatus: {status}")
|
|
156
|
+
elif event_type == "error":
|
|
157
|
+
detail = (
|
|
158
|
+
data.get("detail")
|
|
159
|
+
if isinstance(data, dict)
|
|
160
|
+
else "Unknown error"
|
|
161
|
+
)
|
|
162
|
+
if output_json:
|
|
163
|
+
typer.echo(
|
|
164
|
+
json.dumps(
|
|
165
|
+
{"event": "error", "detail": detail}, indent=2
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
else:
|
|
169
|
+
typer.echo(f"\nError: {detail}", err=True)
|
|
170
|
+
elif event_type == "done":
|
|
171
|
+
if not output_json:
|
|
172
|
+
typer.echo()
|
|
173
|
+
return
|
|
174
|
+
elif event_type == "interrupted":
|
|
175
|
+
detail = (
|
|
176
|
+
data.get("detail")
|
|
177
|
+
if isinstance(data, dict)
|
|
178
|
+
else "Interrupted"
|
|
179
|
+
)
|
|
180
|
+
if output_json:
|
|
181
|
+
typer.echo(
|
|
182
|
+
json.dumps(
|
|
183
|
+
{"event": "interrupted", "detail": detail}, indent=2
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
else:
|
|
187
|
+
typer.echo(f"\nInterrupted: {detail}")
|
|
188
|
+
return
|
|
189
|
+
except httpx.HTTPError as exc:
|
|
190
|
+
typer.echo(f"HTTP error: {exc}", err=True)
|
|
191
|
+
raise typer.Exit(code=1) from None
|
|
192
|
+
except Exception as exc:
|
|
193
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
194
|
+
raise typer.Exit(code=1) from None
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
data = _request_json(
|
|
199
|
+
"POST", url, payload, token_env=config.server_auth_token_env
|
|
200
|
+
)
|
|
201
|
+
except httpx.HTTPError as exc:
|
|
202
|
+
typer.echo(f"HTTP error: {exc}", err=True)
|
|
203
|
+
raise typer.Exit(code=1) from None
|
|
204
|
+
except Exception as exc:
|
|
205
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
206
|
+
raise typer.Exit(code=1) from None
|
|
207
|
+
|
|
208
|
+
error = _is_json_response_error(data)
|
|
209
|
+
if error:
|
|
210
|
+
if output_json:
|
|
211
|
+
typer.echo(json.dumps({"error": error, "detail": data}, indent=2))
|
|
212
|
+
else:
|
|
213
|
+
typer.echo(f"Chat failed: {error}", err=True)
|
|
214
|
+
raise typer.Exit(code=1) from None
|
|
215
|
+
|
|
216
|
+
if output_json:
|
|
217
|
+
typer.echo(json.dumps(data, indent=2))
|
|
218
|
+
else:
|
|
219
|
+
msg = data.get("message") if isinstance(data, dict) else ""
|
|
220
|
+
typer.echo(msg or "No message returned")
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@pma_app.command("interrupt")
|
|
224
|
+
def pma_interrupt(
|
|
225
|
+
output_json: bool = typer.Option(False, "--json", help="Emit JSON output"),
|
|
226
|
+
path: Optional[Path] = typer.Option(None, "--path", "--hub", help="Hub root path"),
|
|
227
|
+
):
|
|
228
|
+
"""Interrupt a running PMA chat."""
|
|
229
|
+
hub_root = _resolve_hub_path(path)
|
|
230
|
+
try:
|
|
231
|
+
config = load_hub_config(hub_root)
|
|
232
|
+
except Exception as exc:
|
|
233
|
+
typer.echo(f"Failed to load hub config: {exc}", err=True)
|
|
234
|
+
raise typer.Exit(code=1) from None
|
|
235
|
+
|
|
236
|
+
url = _build_pma_url(config, "/interrupt")
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
data = _request_json("POST", url, token_env=config.server_auth_token_env)
|
|
240
|
+
except httpx.HTTPError as exc:
|
|
241
|
+
typer.echo(f"HTTP error: {exc}", err=True)
|
|
242
|
+
raise typer.Exit(code=1) from None
|
|
243
|
+
except Exception as exc:
|
|
244
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
245
|
+
raise typer.Exit(code=1) from None
|
|
246
|
+
|
|
247
|
+
if output_json:
|
|
248
|
+
typer.echo(json.dumps(data, indent=2))
|
|
249
|
+
else:
|
|
250
|
+
interrupted = data.get("interrupted") if isinstance(data, dict) else False
|
|
251
|
+
detail = data.get("detail") if isinstance(data, dict) else ""
|
|
252
|
+
agent = data.get("agent") if isinstance(data, dict) else ""
|
|
253
|
+
if interrupted:
|
|
254
|
+
typer.echo(f"PMA chat interrupted (agent={agent})")
|
|
255
|
+
else:
|
|
256
|
+
typer.echo("No active PMA chat to interrupt")
|
|
257
|
+
if detail:
|
|
258
|
+
typer.echo(f"Detail: {detail}")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@pma_app.command("reset")
|
|
262
|
+
def pma_reset(
|
|
263
|
+
agent: Optional[str] = typer.Option(
|
|
264
|
+
None, "--agent", help="Agent thread to reset (opencode|codex|all)"
|
|
265
|
+
),
|
|
266
|
+
output_json: bool = typer.Option(False, "--json", help="Emit JSON output"),
|
|
267
|
+
path: Optional[Path] = typer.Option(None, "--path", "--hub", help="Hub root path"),
|
|
268
|
+
):
|
|
269
|
+
"""Reset PMA thread state."""
|
|
270
|
+
hub_root = _resolve_hub_path(path)
|
|
271
|
+
try:
|
|
272
|
+
config = load_hub_config(hub_root)
|
|
273
|
+
except Exception as exc:
|
|
274
|
+
typer.echo(f"Failed to load hub config: {exc}", err=True)
|
|
275
|
+
raise typer.Exit(code=1) from None
|
|
276
|
+
|
|
277
|
+
url = _build_pma_url(config, "/thread/reset")
|
|
278
|
+
payload: dict[str, Any] = {}
|
|
279
|
+
if agent:
|
|
280
|
+
payload["agent"] = agent
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
data = _request_json(
|
|
284
|
+
"POST", url, payload, token_env=config.server_auth_token_env
|
|
285
|
+
)
|
|
286
|
+
except httpx.HTTPError as exc:
|
|
287
|
+
typer.echo(f"HTTP error: {exc}", err=True)
|
|
288
|
+
raise typer.Exit(code=1) from None
|
|
289
|
+
except Exception as exc:
|
|
290
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
291
|
+
raise typer.Exit(code=1) from None
|
|
292
|
+
|
|
293
|
+
if output_json:
|
|
294
|
+
typer.echo(json.dumps(data, indent=2))
|
|
295
|
+
else:
|
|
296
|
+
cleared = data.get("cleared") if isinstance(data, dict) else []
|
|
297
|
+
if cleared:
|
|
298
|
+
typer.echo(f"Cleared threads: {', '.join(cleared)}")
|
|
299
|
+
else:
|
|
300
|
+
typer.echo("No threads to clear")
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@pma_app.command("active")
|
|
304
|
+
def pma_active(
|
|
305
|
+
client_turn_id: Optional[str] = typer.Option(
|
|
306
|
+
None, "--turn-id", help="Filter by client turn ID"
|
|
307
|
+
),
|
|
308
|
+
output_json: bool = typer.Option(False, "--json", help="Emit JSON output"),
|
|
309
|
+
path: Optional[Path] = typer.Option(None, "--path", "--hub", help="Hub root path"),
|
|
310
|
+
):
|
|
311
|
+
"""Show active PMA chat status."""
|
|
312
|
+
hub_root = _resolve_hub_path(path)
|
|
313
|
+
try:
|
|
314
|
+
config = load_hub_config(hub_root)
|
|
315
|
+
except Exception as exc:
|
|
316
|
+
typer.echo(f"Failed to load hub config: {exc}", err=True)
|
|
317
|
+
raise typer.Exit(code=1) from None
|
|
318
|
+
|
|
319
|
+
url = _build_pma_url(config, "/active")
|
|
320
|
+
params = {}
|
|
321
|
+
if client_turn_id:
|
|
322
|
+
params["client_turn_id"] = client_turn_id
|
|
323
|
+
|
|
324
|
+
try:
|
|
325
|
+
response = httpx.get(url, params=params, timeout=5.0)
|
|
326
|
+
response.raise_for_status()
|
|
327
|
+
data = response.json()
|
|
328
|
+
except httpx.HTTPError as exc:
|
|
329
|
+
typer.echo(f"HTTP error: {exc}", err=True)
|
|
330
|
+
raise typer.Exit(code=1) from None
|
|
331
|
+
except Exception as exc:
|
|
332
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
333
|
+
raise typer.Exit(code=1) from None
|
|
334
|
+
|
|
335
|
+
if output_json:
|
|
336
|
+
typer.echo(json.dumps(data, indent=2))
|
|
337
|
+
else:
|
|
338
|
+
active = data.get("active") if isinstance(data, dict) else False
|
|
339
|
+
current = data.get("current") if isinstance(data, dict) else {}
|
|
340
|
+
last_result = data.get("last_result") if isinstance(data, dict) else {}
|
|
341
|
+
|
|
342
|
+
typer.echo(f"Active: {active}")
|
|
343
|
+
if current:
|
|
344
|
+
status = current.get("status", "unknown")
|
|
345
|
+
agent = current.get("agent", "unknown")
|
|
346
|
+
started = current.get("started_at", "")
|
|
347
|
+
typer.echo(
|
|
348
|
+
f"Current turn: status={status}, agent={agent}, started={started}"
|
|
349
|
+
)
|
|
350
|
+
if last_result:
|
|
351
|
+
status = last_result.get("status", "unknown")
|
|
352
|
+
agent = last_result.get("agent", "unknown")
|
|
353
|
+
finished = last_result.get("finished_at", "")
|
|
354
|
+
typer.echo(
|
|
355
|
+
f"Last result: status={status}, agent={agent}, finished={finished}"
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
@pma_app.command("agents")
|
|
360
|
+
def pma_agents(
|
|
361
|
+
output_json: bool = typer.Option(False, "--json", help="Emit JSON output"),
|
|
362
|
+
path: Optional[Path] = typer.Option(None, "--path", "--hub", help="Hub root path"),
|
|
363
|
+
):
|
|
364
|
+
"""List available PMA agents."""
|
|
365
|
+
hub_root = _resolve_hub_path(path)
|
|
366
|
+
try:
|
|
367
|
+
config = load_hub_config(hub_root)
|
|
368
|
+
except Exception as exc:
|
|
369
|
+
typer.echo(f"Failed to load hub config: {exc}", err=True)
|
|
370
|
+
raise typer.Exit(code=1) from None
|
|
371
|
+
|
|
372
|
+
url = _build_pma_url(config, "/agents")
|
|
373
|
+
|
|
374
|
+
try:
|
|
375
|
+
data = _request_json("GET", url, token_env=config.server_auth_token_env)
|
|
376
|
+
except httpx.HTTPError as exc:
|
|
377
|
+
typer.echo(f"HTTP error: {exc}", err=True)
|
|
378
|
+
raise typer.Exit(code=1) from None
|
|
379
|
+
except Exception as exc:
|
|
380
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
381
|
+
raise typer.Exit(code=1) from None
|
|
382
|
+
|
|
383
|
+
if output_json:
|
|
384
|
+
typer.echo(json.dumps(data, indent=2))
|
|
385
|
+
else:
|
|
386
|
+
agents = data.get("agents", []) if isinstance(data, dict) else []
|
|
387
|
+
default = data.get("default", "") if isinstance(data, dict) else ""
|
|
388
|
+
defaults = data.get("defaults", {}) if isinstance(data, dict) else {}
|
|
389
|
+
|
|
390
|
+
typer.echo(f"Default agent: {default or 'none'}")
|
|
391
|
+
if defaults:
|
|
392
|
+
typer.echo("Defaults:")
|
|
393
|
+
for key, value in defaults.items():
|
|
394
|
+
typer.echo(f" {key}: {value}")
|
|
395
|
+
typer.echo(f"\nAgents ({len(agents)}):")
|
|
396
|
+
for agent in agents:
|
|
397
|
+
if not isinstance(agent, dict):
|
|
398
|
+
continue
|
|
399
|
+
agent_id = agent.get("id", "")
|
|
400
|
+
agent_name = agent.get("name", agent_id)
|
|
401
|
+
available = agent.get("available", False)
|
|
402
|
+
status = "available" if available else "unavailable"
|
|
403
|
+
typer.echo(f" - {agent_name} ({agent_id}): {status}")
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
@pma_app.command("models")
|
|
407
|
+
def pma_models(
|
|
408
|
+
agent: str = typer.Argument(..., help="Agent ID (codex|opencode)"),
|
|
409
|
+
output_json: bool = typer.Option(False, "--json", help="Emit JSON output"),
|
|
410
|
+
path: Optional[Path] = typer.Option(None, "--path", "--hub", help="Hub root path"),
|
|
411
|
+
):
|
|
412
|
+
"""List available models for an agent."""
|
|
413
|
+
hub_root = _resolve_hub_path(path)
|
|
414
|
+
try:
|
|
415
|
+
config = load_hub_config(hub_root)
|
|
416
|
+
except Exception as exc:
|
|
417
|
+
typer.echo(f"Failed to load hub config: {exc}", err=True)
|
|
418
|
+
raise typer.Exit(code=1) from None
|
|
419
|
+
|
|
420
|
+
url = _build_pma_url(config, f"/agents/{agent}/models")
|
|
421
|
+
|
|
422
|
+
try:
|
|
423
|
+
data = _request_json("GET", url, token_env=config.server_auth_token_env)
|
|
424
|
+
except httpx.HTTPError as exc:
|
|
425
|
+
typer.echo(f"HTTP error: {exc}", err=True)
|
|
426
|
+
raise typer.Exit(code=1) from None
|
|
427
|
+
except Exception as exc:
|
|
428
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
429
|
+
raise typer.Exit(code=1) from None
|
|
430
|
+
|
|
431
|
+
if output_json:
|
|
432
|
+
typer.echo(json.dumps(data, indent=2))
|
|
433
|
+
else:
|
|
434
|
+
models = data.get("models", []) if isinstance(data, dict) else []
|
|
435
|
+
default_model = data.get("default_model", "") if isinstance(data, dict) else ""
|
|
436
|
+
|
|
437
|
+
typer.echo(f"Default model: {default_model or 'none'}")
|
|
438
|
+
typer.echo(f"\nModels ({len(models)}):")
|
|
439
|
+
for model in models:
|
|
440
|
+
if not isinstance(model, dict):
|
|
441
|
+
continue
|
|
442
|
+
model_id = model.get("id", "")
|
|
443
|
+
model_name = model.get("name", model_id)
|
|
444
|
+
typer.echo(f" - {model_name} ({model_id})")
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
@pma_app.command("files")
|
|
448
|
+
def pma_files(
|
|
449
|
+
output_json: bool = typer.Option(False, "--json", help="Emit JSON output"),
|
|
450
|
+
path: Optional[Path] = typer.Option(None, "--path", "--hub", help="Hub root path"),
|
|
451
|
+
):
|
|
452
|
+
"""List files in PMA inbox and outbox."""
|
|
453
|
+
hub_root = _resolve_hub_path(path)
|
|
454
|
+
try:
|
|
455
|
+
config = load_hub_config(hub_root)
|
|
456
|
+
except Exception as exc:
|
|
457
|
+
typer.echo(f"Failed to load hub config: {exc}", err=True)
|
|
458
|
+
raise typer.Exit(code=1) from None
|
|
459
|
+
|
|
460
|
+
url = _build_pma_url(config, "/files")
|
|
461
|
+
|
|
462
|
+
try:
|
|
463
|
+
data = _request_json("GET", url, token_env=config.server_auth_token_env)
|
|
464
|
+
except httpx.HTTPError as exc:
|
|
465
|
+
typer.echo(f"HTTP error: {exc}", err=True)
|
|
466
|
+
raise typer.Exit(code=1) from None
|
|
467
|
+
except Exception as exc:
|
|
468
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
469
|
+
raise typer.Exit(code=1) from None
|
|
470
|
+
|
|
471
|
+
if output_json:
|
|
472
|
+
typer.echo(json.dumps(data, indent=2))
|
|
473
|
+
else:
|
|
474
|
+
inbox = data.get("inbox", []) if isinstance(data, dict) else []
|
|
475
|
+
outbox = data.get("outbox", []) if isinstance(data, dict) else []
|
|
476
|
+
|
|
477
|
+
typer.echo(f"Inbox ({len(inbox)}):")
|
|
478
|
+
for file in inbox:
|
|
479
|
+
if not isinstance(file, dict):
|
|
480
|
+
continue
|
|
481
|
+
name = file.get("name", "")
|
|
482
|
+
size = file.get("size", 0)
|
|
483
|
+
modified = file.get("modified_at", "")
|
|
484
|
+
typer.echo(f" - {name} ({size} bytes, {modified})")
|
|
485
|
+
|
|
486
|
+
typer.echo(f"\nOutbox ({len(outbox)}):")
|
|
487
|
+
for file in outbox:
|
|
488
|
+
if not isinstance(file, dict):
|
|
489
|
+
continue
|
|
490
|
+
name = file.get("name", "")
|
|
491
|
+
size = file.get("size", 0)
|
|
492
|
+
modified = file.get("modified_at", "")
|
|
493
|
+
typer.echo(f" - {name} ({size} bytes, {modified})")
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
@pma_app.command("upload")
|
|
497
|
+
def pma_upload(
|
|
498
|
+
box: str = typer.Argument(..., help="Target box (inbox|outbox)"),
|
|
499
|
+
files: list[Path] = typer.Argument(..., help="Files to upload"),
|
|
500
|
+
output_json: bool = typer.Option(False, "--json", help="Emit JSON output"),
|
|
501
|
+
path: Optional[Path] = typer.Option(None, "--path", "--hub", help="Hub root path"),
|
|
502
|
+
):
|
|
503
|
+
"""Upload files to PMA inbox or outbox."""
|
|
504
|
+
hub_root = _resolve_hub_path(path)
|
|
505
|
+
try:
|
|
506
|
+
config = load_hub_config(hub_root)
|
|
507
|
+
except Exception as exc:
|
|
508
|
+
typer.echo(f"Failed to load hub config: {exc}", err=True)
|
|
509
|
+
raise typer.Exit(code=1) from None
|
|
510
|
+
|
|
511
|
+
if box not in ("inbox", "outbox"):
|
|
512
|
+
typer.echo("Box must be 'inbox' or 'outbox'", err=True)
|
|
513
|
+
raise typer.Exit(code=1) from None
|
|
514
|
+
|
|
515
|
+
url = _build_pma_url(config, f"/files/{box}")
|
|
516
|
+
|
|
517
|
+
for file_path in files:
|
|
518
|
+
if not file_path.exists():
|
|
519
|
+
typer.echo(f"File not found: {file_path}", err=True)
|
|
520
|
+
raise typer.Exit(code=1) from None
|
|
521
|
+
|
|
522
|
+
import os
|
|
523
|
+
|
|
524
|
+
token_env = config.server_auth_token_env
|
|
525
|
+
headers = {}
|
|
526
|
+
if token_env:
|
|
527
|
+
token = os.environ.get(token_env)
|
|
528
|
+
if token and token.strip():
|
|
529
|
+
headers["Authorization"] = f"Bearer {token.strip()}"
|
|
530
|
+
|
|
531
|
+
saved_files = []
|
|
532
|
+
for file_path in files:
|
|
533
|
+
try:
|
|
534
|
+
with open(file_path, "rb") as f:
|
|
535
|
+
files_data = {"file": (file_path.name, f, "application/octet-stream")}
|
|
536
|
+
response = httpx.post(
|
|
537
|
+
url, files=files_data, headers=headers, timeout=30.0
|
|
538
|
+
)
|
|
539
|
+
response.raise_for_status()
|
|
540
|
+
data = response.json()
|
|
541
|
+
saved = data.get("saved", []) if isinstance(data, dict) else []
|
|
542
|
+
saved_files.extend(saved)
|
|
543
|
+
except httpx.HTTPError as exc:
|
|
544
|
+
typer.echo(f"HTTP error uploading {file_path}: {exc}", err=True)
|
|
545
|
+
raise typer.Exit(code=1) from None
|
|
546
|
+
except OSError as exc:
|
|
547
|
+
typer.echo(f"Error reading file {file_path}: {exc}", err=True)
|
|
548
|
+
raise typer.Exit(code=1) from None
|
|
549
|
+
|
|
550
|
+
if output_json:
|
|
551
|
+
typer.echo(json.dumps({"saved": saved_files}, indent=2))
|
|
552
|
+
else:
|
|
553
|
+
typer.echo(f"Uploaded {len(saved_files)} file(s): {', '.join(saved_files)}")
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
@pma_app.command("download")
|
|
557
|
+
def pma_download(
|
|
558
|
+
box: str = typer.Argument(..., help="Source box (inbox|outbox)"),
|
|
559
|
+
filename: str = typer.Argument(..., help="File to download"),
|
|
560
|
+
output: Optional[Path] = typer.Option(
|
|
561
|
+
None, "--output", "-o", help="Output path (default: current directory)"
|
|
562
|
+
),
|
|
563
|
+
path: Optional[Path] = typer.Option(None, "--path", "--hub", help="Hub root path"),
|
|
564
|
+
):
|
|
565
|
+
"""Download a file from PMA inbox or outbox."""
|
|
566
|
+
hub_root = _resolve_hub_path(path)
|
|
567
|
+
try:
|
|
568
|
+
config = load_hub_config(hub_root)
|
|
569
|
+
except Exception as exc:
|
|
570
|
+
typer.echo(f"Failed to load hub config: {exc}", err=True)
|
|
571
|
+
raise typer.Exit(code=1) from None
|
|
572
|
+
|
|
573
|
+
if box not in ("inbox", "outbox"):
|
|
574
|
+
typer.echo("Box must be 'inbox' or 'outbox'", err=True)
|
|
575
|
+
raise typer.Exit(code=1) from None
|
|
576
|
+
|
|
577
|
+
url = _build_pma_url(config, f"/files/{box}/{filename}")
|
|
578
|
+
|
|
579
|
+
try:
|
|
580
|
+
response = httpx.get(url, timeout=30.0)
|
|
581
|
+
response.raise_for_status()
|
|
582
|
+
except httpx.HTTPError as exc:
|
|
583
|
+
typer.echo(f"HTTP error: {exc}", err=True)
|
|
584
|
+
raise typer.Exit(code=1) from None
|
|
585
|
+
|
|
586
|
+
output_path = output if output else Path(filename)
|
|
587
|
+
output_path.write_bytes(response.content)
|
|
588
|
+
typer.echo(f"Downloaded to {output_path}")
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
@pma_app.command("delete")
|
|
592
|
+
def pma_delete(
|
|
593
|
+
box: Optional[str] = typer.Argument(None, help="Target box (inbox|outbox)"),
|
|
594
|
+
filename: Optional[str] = typer.Argument(None, help="File to delete"),
|
|
595
|
+
all_files: bool = typer.Option(False, "--all", help="Delete all files in the box"),
|
|
596
|
+
output_json: bool = typer.Option(False, "--json", help="Emit JSON output"),
|
|
597
|
+
path: Optional[Path] = typer.Option(None, "--path", "--hub", help="Hub root path"),
|
|
598
|
+
):
|
|
599
|
+
"""Delete files from PMA inbox or outbox."""
|
|
600
|
+
hub_root = _resolve_hub_path(path)
|
|
601
|
+
try:
|
|
602
|
+
config = load_hub_config(hub_root)
|
|
603
|
+
except Exception as exc:
|
|
604
|
+
typer.echo(f"Failed to load hub config: {exc}", err=True)
|
|
605
|
+
raise typer.Exit(code=1) from None
|
|
606
|
+
|
|
607
|
+
if all_files:
|
|
608
|
+
if not box or box not in ("inbox", "outbox"):
|
|
609
|
+
typer.echo("Box must be 'inbox' or 'outbox' when using --all", err=True)
|
|
610
|
+
raise typer.Exit(code=1) from None
|
|
611
|
+
url = _build_pma_url(config, f"/files/{box}")
|
|
612
|
+
method = "DELETE"
|
|
613
|
+
payload = None
|
|
614
|
+
else:
|
|
615
|
+
if not box or not filename:
|
|
616
|
+
typer.echo("Box and filename are required (or use --all)", err=True)
|
|
617
|
+
raise typer.Exit(code=1) from None
|
|
618
|
+
if box not in ("inbox", "outbox"):
|
|
619
|
+
typer.echo("Box must be 'inbox' or 'outbox'", err=True)
|
|
620
|
+
raise typer.Exit(code=1) from None
|
|
621
|
+
url = _build_pma_url(config, f"/files/{box}/{filename}")
|
|
622
|
+
method = "DELETE"
|
|
623
|
+
payload = None
|
|
624
|
+
|
|
625
|
+
try:
|
|
626
|
+
response = httpx.request(method, url, json=payload, timeout=30.0)
|
|
627
|
+
response.raise_for_status()
|
|
628
|
+
data = response.json()
|
|
629
|
+
except httpx.HTTPError as exc:
|
|
630
|
+
typer.echo(f"HTTP error: {exc}", err=True)
|
|
631
|
+
raise typer.Exit(code=1) from None
|
|
632
|
+
except Exception as exc:
|
|
633
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
634
|
+
raise typer.Exit(code=1) from None
|
|
635
|
+
|
|
636
|
+
if output_json:
|
|
637
|
+
typer.echo(json.dumps(data, indent=2))
|
|
638
|
+
else:
|
|
639
|
+
if all_files:
|
|
640
|
+
typer.echo(f"Deleted all files in {box}")
|
|
641
|
+
else:
|
|
642
|
+
typer.echo(f"Deleted {filename} from {box}")
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
@docs_app.command("show")
|
|
646
|
+
def pma_docs_show(
|
|
647
|
+
doc_type: str = typer.Argument(..., help="Document type: agents, active, or log"),
|
|
648
|
+
path: Optional[Path] = typer.Option(None, "--path", "--hub", help="Hub root path"),
|
|
649
|
+
):
|
|
650
|
+
"""Show PMA docs content to stdout."""
|
|
651
|
+
hub_root = _resolve_hub_path(path)
|
|
652
|
+
try:
|
|
653
|
+
ensure_pma_docs(hub_root)
|
|
654
|
+
except Exception as exc:
|
|
655
|
+
typer.echo(f"Failed to ensure PMA docs: {exc}", err=True)
|
|
656
|
+
raise typer.Exit(code=1) from None
|
|
657
|
+
|
|
658
|
+
pma_dir = hub_root / ".codex-autorunner" / "pma"
|
|
659
|
+
|
|
660
|
+
if doc_type == "agents":
|
|
661
|
+
doc_path = pma_dir / "AGENTS.md"
|
|
662
|
+
elif doc_type == "active":
|
|
663
|
+
doc_path = pma_dir / "active_context.md"
|
|
664
|
+
elif doc_type == "log":
|
|
665
|
+
doc_path = pma_dir / "context_log.md"
|
|
666
|
+
else:
|
|
667
|
+
typer.echo("Invalid doc_type. Must be one of: agents, active, log", err=True)
|
|
668
|
+
raise typer.Exit(code=1) from None
|
|
669
|
+
|
|
670
|
+
try:
|
|
671
|
+
content = doc_path.read_text(encoding="utf-8")
|
|
672
|
+
typer.echo(content, nl=False)
|
|
673
|
+
except OSError as exc:
|
|
674
|
+
typer.echo(f"Failed to read {doc_path}: {exc}", err=True)
|
|
675
|
+
raise typer.Exit(code=1) from None
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
@context_app.command("reset")
|
|
679
|
+
def pma_context_reset(
|
|
680
|
+
path: Optional[Path] = typer.Option(None, "--path", "--hub", help="Hub root path"),
|
|
681
|
+
):
|
|
682
|
+
"""Reset active_context.md to a minimal header."""
|
|
683
|
+
hub_root = _resolve_hub_path(path)
|
|
684
|
+
try:
|
|
685
|
+
ensure_pma_docs(hub_root)
|
|
686
|
+
except Exception as exc:
|
|
687
|
+
typer.echo(f"Failed to ensure PMA docs: {exc}", err=True)
|
|
688
|
+
raise typer.Exit(code=1) from None
|
|
689
|
+
|
|
690
|
+
pma_dir = hub_root / ".codex-autorunner" / "pma"
|
|
691
|
+
active_context_path = pma_dir / "active_context.md"
|
|
692
|
+
|
|
693
|
+
minimal_content = """# PMA active context (short-lived)
|
|
694
|
+
|
|
695
|
+
Use this file for the current working set: active projects, open questions, links, and immediate next steps.
|
|
696
|
+
|
|
697
|
+
Pruning guidance:
|
|
698
|
+
- Keep this file compact (prefer bullet points).
|
|
699
|
+
- When it grows too large, summarize older items and move durable guidance to `AGENTS.md`.
|
|
700
|
+
- Before a major prune, append a timestamped snapshot to `context_log.md`.
|
|
701
|
+
"""
|
|
702
|
+
|
|
703
|
+
try:
|
|
704
|
+
active_context_path.write_text(minimal_content, encoding="utf-8")
|
|
705
|
+
typer.echo(f"Reset active_context.md at {active_context_path}")
|
|
706
|
+
except OSError as exc:
|
|
707
|
+
typer.echo(f"Failed to write {active_context_path}: {exc}", err=True)
|
|
708
|
+
raise typer.Exit(code=1) from None
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
@context_app.command("snapshot")
|
|
712
|
+
def pma_context_snapshot(
|
|
713
|
+
path: Optional[Path] = typer.Option(None, "--path", "--hub", help="Hub root path"),
|
|
714
|
+
):
|
|
715
|
+
"""Snapshot active_context.md into context_log.md with ISO timestamp."""
|
|
716
|
+
hub_root = _resolve_hub_path(path)
|
|
717
|
+
try:
|
|
718
|
+
ensure_pma_docs(hub_root)
|
|
719
|
+
except Exception as exc:
|
|
720
|
+
typer.echo(f"Failed to ensure PMA docs: {exc}", err=True)
|
|
721
|
+
raise typer.Exit(code=1) from None
|
|
722
|
+
|
|
723
|
+
pma_dir = hub_root / ".codex-autorunner" / "pma"
|
|
724
|
+
active_context_path = pma_dir / "active_context.md"
|
|
725
|
+
context_log_path = pma_dir / "context_log.md"
|
|
726
|
+
|
|
727
|
+
try:
|
|
728
|
+
active_content = active_context_path.read_text(encoding="utf-8")
|
|
729
|
+
except OSError as exc:
|
|
730
|
+
typer.echo(f"Failed to read {active_context_path}: {exc}", err=True)
|
|
731
|
+
raise typer.Exit(code=1) from None
|
|
732
|
+
|
|
733
|
+
timestamp = datetime.now(timezone.utc).isoformat()
|
|
734
|
+
snapshot_header = f"\n\n## Snapshot: {timestamp}\n\n"
|
|
735
|
+
snapshot_content = snapshot_header + active_content
|
|
736
|
+
|
|
737
|
+
try:
|
|
738
|
+
with context_log_path.open("a", encoding="utf-8") as f:
|
|
739
|
+
f.write(snapshot_content)
|
|
740
|
+
typer.echo(f"Appended snapshot to {context_log_path}")
|
|
741
|
+
except OSError as exc:
|
|
742
|
+
typer.echo(f"Failed to write {context_log_path}: {exc}", err=True)
|
|
743
|
+
raise typer.Exit(code=1) from None
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
@context_app.command("prune")
|
|
747
|
+
def pma_context_prune(
|
|
748
|
+
path: Optional[Path] = typer.Option(None, "--path", "--hub", help="Hub root path"),
|
|
749
|
+
):
|
|
750
|
+
"""Prune active_context.md if over budget (snapshot first)."""
|
|
751
|
+
hub_root = _resolve_hub_path(path)
|
|
752
|
+
|
|
753
|
+
max_lines = 200
|
|
754
|
+
try:
|
|
755
|
+
config = load_hub_config(hub_root)
|
|
756
|
+
pma_cfg = getattr(config, "pma", None)
|
|
757
|
+
if pma_cfg is not None:
|
|
758
|
+
max_lines = int(getattr(pma_cfg, "active_context_max_lines", max_lines))
|
|
759
|
+
except Exception:
|
|
760
|
+
pass
|
|
761
|
+
|
|
762
|
+
try:
|
|
763
|
+
ensure_pma_docs(hub_root)
|
|
764
|
+
except Exception as exc:
|
|
765
|
+
typer.echo(f"Failed to ensure PMA docs: {exc}", err=True)
|
|
766
|
+
raise typer.Exit(code=1) from None
|
|
767
|
+
|
|
768
|
+
pma_dir = hub_root / ".codex-autorunner" / "pma"
|
|
769
|
+
active_context_path = pma_dir / "active_context.md"
|
|
770
|
+
|
|
771
|
+
try:
|
|
772
|
+
active_content = active_context_path.read_text(encoding="utf-8")
|
|
773
|
+
line_count = len(active_content.splitlines())
|
|
774
|
+
except OSError as exc:
|
|
775
|
+
typer.echo(f"Failed to read {active_context_path}: {exc}", err=True)
|
|
776
|
+
raise typer.Exit(code=1) from None
|
|
777
|
+
|
|
778
|
+
if line_count <= max_lines:
|
|
779
|
+
typer.echo(
|
|
780
|
+
f"active_context.md has {line_count} lines (budget: {max_lines}), no prune needed"
|
|
781
|
+
)
|
|
782
|
+
return
|
|
783
|
+
|
|
784
|
+
typer.echo(
|
|
785
|
+
f"active_context.md has {line_count} lines (budget: {max_lines}), snapshotting and pruning"
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
timestamp = datetime.now(timezone.utc).isoformat()
|
|
789
|
+
snapshot_header = f"\n\n## Snapshot: {timestamp}\n\n"
|
|
790
|
+
snapshot_content = snapshot_header + active_content
|
|
791
|
+
|
|
792
|
+
context_log_path = pma_dir / "context_log.md"
|
|
793
|
+
try:
|
|
794
|
+
with context_log_path.open("a", encoding="utf-8") as f:
|
|
795
|
+
f.write(snapshot_content)
|
|
796
|
+
except OSError as exc:
|
|
797
|
+
typer.echo(f"Failed to write {context_log_path}: {exc}", err=True)
|
|
798
|
+
raise typer.Exit(code=1) from None
|
|
799
|
+
|
|
800
|
+
minimal_content = f"""# PMA active context (short-lived)
|
|
801
|
+
|
|
802
|
+
Use this file for the current working set: active projects, open questions, links, and immediate next steps.
|
|
803
|
+
|
|
804
|
+
Pruning guidance:
|
|
805
|
+
- Keep this file compact (prefer bullet points).
|
|
806
|
+
- When it grows too large, summarize older items and move durable guidance to `AGENTS.md`.
|
|
807
|
+
- Before a major prune, append a timestamped snapshot to `context_log.md`.
|
|
808
|
+
|
|
809
|
+
> Note: This file was pruned on {timestamp} (had {line_count} lines, budget: {max_lines})
|
|
810
|
+
"""
|
|
811
|
+
|
|
812
|
+
try:
|
|
813
|
+
active_context_path.write_text(minimal_content, encoding="utf-8")
|
|
814
|
+
typer.echo(f"Pruned active_context.md at {active_context_path}")
|
|
815
|
+
except OSError as exc:
|
|
816
|
+
typer.echo(f"Failed to write {active_context_path}: {exc}", err=True)
|
|
817
|
+
raise typer.Exit(code=1) from None
|