codex-autorunner 0.1.2__py3-none-any.whl → 1.0.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/__main__.py +4 -0
- codex_autorunner/agents/opencode/client.py +68 -35
- codex_autorunner/agents/opencode/logging.py +21 -5
- codex_autorunner/agents/opencode/run_prompt.py +1 -0
- codex_autorunner/agents/opencode/runtime.py +118 -30
- codex_autorunner/agents/opencode/supervisor.py +36 -48
- codex_autorunner/agents/registry.py +136 -8
- codex_autorunner/api.py +25 -0
- codex_autorunner/bootstrap.py +16 -35
- codex_autorunner/cli.py +157 -139
- codex_autorunner/core/about_car.py +44 -32
- codex_autorunner/core/adapter_utils.py +21 -0
- codex_autorunner/core/app_server_logging.py +7 -3
- codex_autorunner/core/app_server_prompts.py +27 -260
- codex_autorunner/core/app_server_threads.py +15 -26
- codex_autorunner/core/codex_runner.py +6 -0
- codex_autorunner/core/config.py +390 -100
- codex_autorunner/core/docs.py +10 -2
- codex_autorunner/core/drafts.py +82 -0
- codex_autorunner/core/engine.py +278 -262
- codex_autorunner/core/flows/__init__.py +25 -0
- codex_autorunner/core/flows/controller.py +178 -0
- codex_autorunner/core/flows/definition.py +82 -0
- codex_autorunner/core/flows/models.py +75 -0
- codex_autorunner/core/flows/runtime.py +351 -0
- codex_autorunner/core/flows/store.py +485 -0
- codex_autorunner/core/flows/transition.py +133 -0
- codex_autorunner/core/flows/worker_process.py +242 -0
- codex_autorunner/core/hub.py +15 -9
- codex_autorunner/core/locks.py +4 -0
- codex_autorunner/core/prompt.py +15 -7
- codex_autorunner/core/redaction.py +29 -0
- codex_autorunner/core/review_context.py +5 -8
- codex_autorunner/core/run_index.py +6 -0
- codex_autorunner/core/runner_process.py +5 -2
- codex_autorunner/core/state.py +0 -88
- codex_autorunner/core/static_assets.py +55 -0
- codex_autorunner/core/supervisor_utils.py +67 -0
- codex_autorunner/core/update.py +20 -11
- codex_autorunner/core/update_runner.py +2 -0
- codex_autorunner/core/utils.py +29 -2
- codex_autorunner/discovery.py +2 -4
- codex_autorunner/flows/ticket_flow/__init__.py +3 -0
- codex_autorunner/flows/ticket_flow/definition.py +91 -0
- codex_autorunner/integrations/agents/__init__.py +27 -0
- codex_autorunner/integrations/agents/agent_backend.py +142 -0
- codex_autorunner/integrations/agents/codex_backend.py +307 -0
- codex_autorunner/integrations/agents/opencode_backend.py +325 -0
- codex_autorunner/integrations/agents/run_event.py +71 -0
- codex_autorunner/integrations/app_server/client.py +576 -92
- codex_autorunner/integrations/app_server/supervisor.py +59 -33
- codex_autorunner/integrations/telegram/adapter.py +141 -167
- codex_autorunner/integrations/telegram/api_schemas.py +120 -0
- codex_autorunner/integrations/telegram/config.py +175 -0
- codex_autorunner/integrations/telegram/constants.py +16 -1
- codex_autorunner/integrations/telegram/dispatch.py +17 -0
- codex_autorunner/integrations/telegram/doctor.py +47 -0
- codex_autorunner/integrations/telegram/handlers/callbacks.py +0 -4
- codex_autorunner/integrations/telegram/handlers/commands/__init__.py +2 -0
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +53 -57
- codex_autorunner/integrations/telegram/handlers/commands/files.py +2 -6
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +227 -0
- codex_autorunner/integrations/telegram/handlers/commands/formatting.py +1 -1
- codex_autorunner/integrations/telegram/handlers/commands/github.py +41 -582
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +8 -8
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +133 -475
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +11 -4
- codex_autorunner/integrations/telegram/handlers/messages.py +120 -9
- codex_autorunner/integrations/telegram/helpers.py +88 -16
- codex_autorunner/integrations/telegram/outbox.py +208 -37
- codex_autorunner/integrations/telegram/progress_stream.py +3 -10
- codex_autorunner/integrations/telegram/service.py +214 -40
- codex_autorunner/integrations/telegram/state.py +100 -2
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +322 -0
- codex_autorunner/integrations/telegram/transport.py +36 -3
- codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
- codex_autorunner/manifest.py +2 -0
- codex_autorunner/plugin_api.py +22 -0
- codex_autorunner/routes/__init__.py +23 -14
- codex_autorunner/routes/analytics.py +239 -0
- codex_autorunner/routes/base.py +81 -109
- codex_autorunner/routes/file_chat.py +836 -0
- codex_autorunner/routes/flows.py +980 -0
- codex_autorunner/routes/messages.py +459 -0
- codex_autorunner/routes/system.py +6 -1
- codex_autorunner/routes/usage.py +87 -0
- codex_autorunner/routes/workspace.py +271 -0
- codex_autorunner/server.py +2 -1
- codex_autorunner/static/agentControls.js +1 -0
- codex_autorunner/static/agentEvents.js +248 -0
- codex_autorunner/static/app.js +25 -22
- codex_autorunner/static/autoRefresh.js +29 -1
- codex_autorunner/static/bootstrap.js +1 -0
- codex_autorunner/static/bus.js +1 -0
- codex_autorunner/static/cache.js +1 -0
- codex_autorunner/static/constants.js +20 -4
- codex_autorunner/static/dashboard.js +162 -196
- codex_autorunner/static/diffRenderer.js +37 -0
- codex_autorunner/static/docChatCore.js +324 -0
- codex_autorunner/static/docChatStorage.js +65 -0
- codex_autorunner/static/docChatVoice.js +65 -0
- codex_autorunner/static/docEditor.js +133 -0
- codex_autorunner/static/env.js +1 -0
- codex_autorunner/static/eventSummarizer.js +166 -0
- codex_autorunner/static/fileChat.js +182 -0
- codex_autorunner/static/health.js +155 -0
- codex_autorunner/static/hub.js +41 -118
- codex_autorunner/static/index.html +787 -858
- codex_autorunner/static/liveUpdates.js +1 -0
- codex_autorunner/static/loader.js +1 -0
- codex_autorunner/static/messages.js +470 -0
- codex_autorunner/static/mobileCompact.js +2 -1
- codex_autorunner/static/settings.js +24 -211
- codex_autorunner/static/styles.css +7567 -3865
- codex_autorunner/static/tabs.js +28 -5
- codex_autorunner/static/terminal.js +14 -0
- codex_autorunner/static/terminalManager.js +34 -59
- codex_autorunner/static/ticketChatActions.js +333 -0
- codex_autorunner/static/ticketChatEvents.js +16 -0
- codex_autorunner/static/ticketChatStorage.js +16 -0
- codex_autorunner/static/ticketChatStream.js +264 -0
- codex_autorunner/static/ticketEditor.js +750 -0
- codex_autorunner/static/ticketVoice.js +9 -0
- codex_autorunner/static/tickets.js +1315 -0
- codex_autorunner/static/utils.js +32 -3
- codex_autorunner/static/voice.js +1 -0
- codex_autorunner/static/workspace.js +672 -0
- codex_autorunner/static/workspaceApi.js +53 -0
- codex_autorunner/static/workspaceFileBrowser.js +504 -0
- codex_autorunner/tickets/__init__.py +20 -0
- codex_autorunner/tickets/agent_pool.py +377 -0
- codex_autorunner/tickets/files.py +85 -0
- codex_autorunner/tickets/frontmatter.py +55 -0
- codex_autorunner/tickets/lint.py +102 -0
- codex_autorunner/tickets/models.py +95 -0
- codex_autorunner/tickets/outbox.py +232 -0
- codex_autorunner/tickets/replies.py +179 -0
- codex_autorunner/tickets/runner.py +823 -0
- codex_autorunner/tickets/spec_ingest.py +77 -0
- codex_autorunner/web/app.py +269 -91
- codex_autorunner/web/middleware.py +3 -4
- codex_autorunner/web/schemas.py +89 -109
- codex_autorunner/web/static_assets.py +1 -44
- codex_autorunner/workspace/__init__.py +40 -0
- codex_autorunner/workspace/paths.py +319 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/METADATA +18 -21
- codex_autorunner-1.0.0.dist-info/RECORD +251 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/WHEEL +1 -1
- codex_autorunner/agents/execution/policy.py +0 -292
- codex_autorunner/agents/factory.py +0 -52
- codex_autorunner/agents/orchestrator.py +0 -358
- codex_autorunner/core/doc_chat.py +0 -1446
- codex_autorunner/core/snapshot.py +0 -580
- codex_autorunner/integrations/github/chatops.py +0 -268
- codex_autorunner/integrations/github/pr_flow.py +0 -1314
- codex_autorunner/routes/docs.py +0 -381
- codex_autorunner/routes/github.py +0 -327
- codex_autorunner/routes/runs.py +0 -250
- codex_autorunner/spec_ingest.py +0 -812
- codex_autorunner/static/docChatActions.js +0 -287
- codex_autorunner/static/docChatEvents.js +0 -300
- codex_autorunner/static/docChatRender.js +0 -205
- codex_autorunner/static/docChatStream.js +0 -361
- codex_autorunner/static/docs.js +0 -20
- codex_autorunner/static/docsClipboard.js +0 -69
- codex_autorunner/static/docsCrud.js +0 -257
- codex_autorunner/static/docsDocUpdates.js +0 -62
- codex_autorunner/static/docsDrafts.js +0 -16
- codex_autorunner/static/docsElements.js +0 -69
- codex_autorunner/static/docsInit.js +0 -285
- codex_autorunner/static/docsParse.js +0 -160
- codex_autorunner/static/docsSnapshot.js +0 -87
- codex_autorunner/static/docsSpecIngest.js +0 -263
- codex_autorunner/static/docsState.js +0 -127
- codex_autorunner/static/docsThreadRegistry.js +0 -44
- codex_autorunner/static/docsUi.js +0 -153
- codex_autorunner/static/docsVoice.js +0 -56
- codex_autorunner/static/github.js +0 -504
- codex_autorunner/static/logs.js +0 -678
- codex_autorunner/static/review.js +0 -157
- codex_autorunner/static/runs.js +0 -418
- codex_autorunner/static/snapshot.js +0 -124
- codex_autorunner/static/state.js +0 -94
- codex_autorunner/static/todoPreview.js +0 -27
- codex_autorunner/workspace.py +0 -16
- codex_autorunner-0.1.2.dist-info/RECORD +0 -222
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Analytics summary routes.
|
|
2
|
+
|
|
3
|
+
This module aggregates run/ticket/message metadata for the analytics dashboard
|
|
4
|
+
without relying on legacy autorunner endpoints. It intentionally reads from the
|
|
5
|
+
filesystem-backed ticket_flow store and ticket files to keep the UI consistent
|
|
6
|
+
with the rest of the app.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Dict, Optional
|
|
14
|
+
|
|
15
|
+
from fastapi import APIRouter
|
|
16
|
+
|
|
17
|
+
from ..core.flows.models import FlowRunRecord, FlowRunStatus
|
|
18
|
+
from ..core.flows.store import FlowStore
|
|
19
|
+
from ..core.utils import find_repo_root
|
|
20
|
+
from ..tickets.files import list_ticket_paths, read_ticket, ticket_is_done
|
|
21
|
+
from ..tickets.outbox import resolve_outbox_paths
|
|
22
|
+
from ..tickets.replies import resolve_reply_paths
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _flows_db_path(repo_root: Path) -> Path:
|
|
26
|
+
return repo_root / ".codex-autorunner" / "flows.db"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _load_flow_store(repo_root: Path) -> Optional[FlowStore]:
|
|
30
|
+
db_path = _flows_db_path(repo_root)
|
|
31
|
+
if not db_path.exists():
|
|
32
|
+
return None
|
|
33
|
+
store = FlowStore(db_path)
|
|
34
|
+
try:
|
|
35
|
+
store.initialize()
|
|
36
|
+
except Exception:
|
|
37
|
+
return None
|
|
38
|
+
return store
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _select_primary_run(records: list[FlowRunRecord]) -> Optional[FlowRunRecord]:
|
|
42
|
+
"""Select the primary run for analytics display.
|
|
43
|
+
|
|
44
|
+
Only considers the newest run (records[0]). If it's active or paused, return it.
|
|
45
|
+
If the newest run is terminal (completed/stopped/failed), return None to show idle.
|
|
46
|
+
This matches the backend's _active_or_paused_run() logic and prevents showing
|
|
47
|
+
stale data from old paused runs when newer runs have completed.
|
|
48
|
+
"""
|
|
49
|
+
if not records:
|
|
50
|
+
return None
|
|
51
|
+
newest = records[0]
|
|
52
|
+
if (
|
|
53
|
+
FlowRunStatus(newest.status).is_active()
|
|
54
|
+
or FlowRunStatus(newest.status).is_paused()
|
|
55
|
+
):
|
|
56
|
+
return newest
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _parse_timestamp(value: Optional[str]) -> Optional[datetime]:
|
|
61
|
+
if not value:
|
|
62
|
+
return None
|
|
63
|
+
try:
|
|
64
|
+
if value.endswith("Z"):
|
|
65
|
+
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
66
|
+
return datetime.fromisoformat(value)
|
|
67
|
+
except ValueError:
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _duration_seconds(
|
|
72
|
+
started_at: Optional[str], finished_at: Optional[str], status: str
|
|
73
|
+
) -> Optional[float]:
|
|
74
|
+
start_dt = _parse_timestamp(started_at)
|
|
75
|
+
if not start_dt:
|
|
76
|
+
return None
|
|
77
|
+
end_dt = _parse_timestamp(finished_at)
|
|
78
|
+
if not end_dt and status in {
|
|
79
|
+
FlowRunStatus.RUNNING.value,
|
|
80
|
+
FlowRunStatus.PAUSED.value,
|
|
81
|
+
FlowRunStatus.PENDING.value,
|
|
82
|
+
}:
|
|
83
|
+
end_dt = datetime.now(timezone.utc)
|
|
84
|
+
if not end_dt:
|
|
85
|
+
return None
|
|
86
|
+
return (end_dt - start_dt).total_seconds()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _ticket_counts(ticket_dir: Path) -> dict[str, int]:
|
|
90
|
+
total = 0
|
|
91
|
+
done = 0
|
|
92
|
+
for path in list_ticket_paths(ticket_dir):
|
|
93
|
+
total += 1
|
|
94
|
+
try:
|
|
95
|
+
if ticket_is_done(path):
|
|
96
|
+
done += 1
|
|
97
|
+
except Exception:
|
|
98
|
+
# Treat unreadable/invalid tickets as not-done but still count them.
|
|
99
|
+
continue
|
|
100
|
+
todo = max(total - done, 0)
|
|
101
|
+
return {"todo": todo, "done": done, "total": total}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _count_history_dirs(history_dir: Path) -> int:
|
|
105
|
+
if not history_dir.exists() or not history_dir.is_dir():
|
|
106
|
+
return 0
|
|
107
|
+
count = 0
|
|
108
|
+
try:
|
|
109
|
+
for child in history_dir.iterdir():
|
|
110
|
+
try:
|
|
111
|
+
if child.is_dir() and len(child.name) == 4 and child.name.isdigit():
|
|
112
|
+
count += 1
|
|
113
|
+
except OSError:
|
|
114
|
+
continue
|
|
115
|
+
except OSError:
|
|
116
|
+
return count
|
|
117
|
+
return count
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _build_summary(repo_root: Path) -> Dict[str, Any]:
|
|
121
|
+
ticket_dir = repo_root / ".codex-autorunner" / "tickets"
|
|
122
|
+
store = _load_flow_store(repo_root)
|
|
123
|
+
records: list[FlowRunRecord] = []
|
|
124
|
+
if store:
|
|
125
|
+
try:
|
|
126
|
+
records = store.list_flow_runs(flow_type="ticket_flow")
|
|
127
|
+
except Exception:
|
|
128
|
+
records = []
|
|
129
|
+
finally:
|
|
130
|
+
try:
|
|
131
|
+
store.close()
|
|
132
|
+
except Exception:
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
run_record = _select_primary_run(records)
|
|
136
|
+
|
|
137
|
+
default_run = {
|
|
138
|
+
"id": None,
|
|
139
|
+
"short_id": None,
|
|
140
|
+
"status": "idle",
|
|
141
|
+
"started_at": None,
|
|
142
|
+
"finished_at": None,
|
|
143
|
+
"duration_seconds": None,
|
|
144
|
+
"current_step": None,
|
|
145
|
+
"created_at": None,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
run_data: Dict[str, Any] = default_run
|
|
149
|
+
turns: Dict[str, Optional[int]] = {
|
|
150
|
+
"total": None,
|
|
151
|
+
"current_ticket": None,
|
|
152
|
+
"dispatches": 0,
|
|
153
|
+
"replies": 0,
|
|
154
|
+
}
|
|
155
|
+
current_ticket: Optional[str] = None
|
|
156
|
+
agent_id: Optional[str] = None
|
|
157
|
+
|
|
158
|
+
if run_record:
|
|
159
|
+
run_data = {
|
|
160
|
+
"id": run_record.id,
|
|
161
|
+
"short_id": run_record.id.split("-")[0] if run_record.id else None,
|
|
162
|
+
"status": run_record.status.value,
|
|
163
|
+
"started_at": run_record.started_at,
|
|
164
|
+
"finished_at": run_record.finished_at,
|
|
165
|
+
"duration_seconds": _duration_seconds(
|
|
166
|
+
run_record.started_at, run_record.finished_at, run_record.status.value
|
|
167
|
+
),
|
|
168
|
+
"current_step": run_record.current_step,
|
|
169
|
+
"created_at": run_record.created_at,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
state = run_record.state if isinstance(run_record.state, dict) else {}
|
|
173
|
+
ticket_state = state.get("ticket_engine") if isinstance(state, dict) else {}
|
|
174
|
+
if isinstance(ticket_state, dict):
|
|
175
|
+
turns["total"] = ticket_state.get("total_turns") # type: ignore[index]
|
|
176
|
+
turns["current_ticket"] = ticket_state.get("ticket_turns") # type: ignore[index]
|
|
177
|
+
current_ticket = ticket_state.get("current_ticket") # type: ignore[assignment]
|
|
178
|
+
agent_id = ticket_state.get("last_agent_id") # type: ignore[assignment]
|
|
179
|
+
|
|
180
|
+
workspace_value = run_record.input_data.get("workspace_root")
|
|
181
|
+
workspace_root = Path(workspace_value) if workspace_value else repo_root
|
|
182
|
+
runs_dir = Path(
|
|
183
|
+
run_record.input_data.get("runs_dir") or ".codex-autorunner/runs"
|
|
184
|
+
)
|
|
185
|
+
outbox_paths = resolve_outbox_paths(
|
|
186
|
+
workspace_root=workspace_root, runs_dir=runs_dir, run_id=run_record.id
|
|
187
|
+
)
|
|
188
|
+
reply_paths = resolve_reply_paths(
|
|
189
|
+
workspace_root=workspace_root, runs_dir=runs_dir, run_id=run_record.id
|
|
190
|
+
)
|
|
191
|
+
turns["dispatches"] = _count_history_dirs(outbox_paths.dispatch_history_dir)
|
|
192
|
+
turns["replies"] = _count_history_dirs(reply_paths.reply_history_dir)
|
|
193
|
+
|
|
194
|
+
# If current ticket is known, read its frontmatter to pick agent id when available.
|
|
195
|
+
if current_ticket:
|
|
196
|
+
current_path = (workspace_root / current_ticket).resolve()
|
|
197
|
+
try:
|
|
198
|
+
doc, _errors = read_ticket(current_path)
|
|
199
|
+
if doc and doc.frontmatter and getattr(doc.frontmatter, "agent", None):
|
|
200
|
+
agent_id = doc.frontmatter.agent
|
|
201
|
+
except Exception:
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
ticket_counts = _ticket_counts(ticket_dir)
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
"run": run_data,
|
|
208
|
+
"tickets": {
|
|
209
|
+
"todo_count": ticket_counts["todo"],
|
|
210
|
+
"done_count": ticket_counts["done"],
|
|
211
|
+
"total_count": ticket_counts["total"],
|
|
212
|
+
"current_ticket": current_ticket,
|
|
213
|
+
},
|
|
214
|
+
"turns": {
|
|
215
|
+
"total": turns.get("total"),
|
|
216
|
+
"current_ticket": turns.get("current_ticket"),
|
|
217
|
+
"dispatches": turns.get("dispatches"),
|
|
218
|
+
"replies": turns.get("replies"),
|
|
219
|
+
},
|
|
220
|
+
"agent": {
|
|
221
|
+
"id": agent_id,
|
|
222
|
+
"model": None,
|
|
223
|
+
},
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def build_analytics_routes() -> APIRouter:
|
|
228
|
+
router = APIRouter(prefix="/api/analytics", tags=["analytics"])
|
|
229
|
+
|
|
230
|
+
@router.get("/summary")
|
|
231
|
+
def get_analytics_summary():
|
|
232
|
+
repo_root = find_repo_root()
|
|
233
|
+
data = _build_summary(repo_root)
|
|
234
|
+
return data
|
|
235
|
+
|
|
236
|
+
return router
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
__all__ = ["build_analytics_routes"]
|
codex_autorunner/routes/base.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Base routes: Index,
|
|
2
|
+
Base routes: Index, WebSocket terminal.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
@@ -14,142 +14,94 @@ from fastapi import APIRouter, HTTPException, Request, WebSocket, WebSocketDisco
|
|
|
14
14
|
from fastapi.responses import (
|
|
15
15
|
HTMLResponse,
|
|
16
16
|
JSONResponse,
|
|
17
|
-
PlainTextResponse,
|
|
18
|
-
StreamingResponse,
|
|
19
17
|
)
|
|
20
18
|
|
|
21
|
-
from ..
|
|
19
|
+
from ..core.config import HubConfig
|
|
22
20
|
from ..core.logging_utils import safe_log
|
|
23
|
-
from ..core.state import SessionRecord,
|
|
21
|
+
from ..core.state import SessionRecord, now_iso, persist_session_registry
|
|
24
22
|
from ..web.pty_session import REPLAY_END, ActiveSession, PTYSession
|
|
25
|
-
from ..web.schemas import
|
|
23
|
+
from ..web.schemas import VersionResponse
|
|
26
24
|
from ..web.static_assets import index_response_headers, render_index_html
|
|
27
25
|
from ..web.static_refresh import refresh_static_assets
|
|
28
26
|
from .shared import (
|
|
29
|
-
SSE_HEADERS,
|
|
30
27
|
build_codex_terminal_cmd,
|
|
31
28
|
build_opencode_terminal_cmd,
|
|
32
|
-
log_stream,
|
|
33
|
-
resolve_lock_payload,
|
|
34
|
-
resolve_runner_status,
|
|
35
|
-
state_stream,
|
|
36
29
|
)
|
|
37
30
|
|
|
38
31
|
ALT_SCREEN_ENTER = b"\x1b[?1049h"
|
|
39
32
|
|
|
40
33
|
|
|
34
|
+
def _serve_index(request: Request, static_dir: Path):
|
|
35
|
+
active_static = getattr(request.app.state, "static_dir", static_dir)
|
|
36
|
+
index_path = active_static / "index.html"
|
|
37
|
+
if not index_path.exists():
|
|
38
|
+
if refresh_static_assets(request.app):
|
|
39
|
+
active_static = request.app.state.static_dir
|
|
40
|
+
index_path = active_static / "index.html"
|
|
41
|
+
if not index_path.exists():
|
|
42
|
+
raise HTTPException(
|
|
43
|
+
status_code=500, detail="Static UI assets missing; reinstall package"
|
|
44
|
+
)
|
|
45
|
+
html = render_index_html(active_static, request.app.state.asset_version)
|
|
46
|
+
return HTMLResponse(html, headers=index_response_headers())
|
|
47
|
+
|
|
48
|
+
|
|
41
49
|
def build_base_routes(static_dir: Path) -> APIRouter:
|
|
42
50
|
"""Build routes for index, state, logs, and terminal WebSocket."""
|
|
43
51
|
router = APIRouter()
|
|
44
52
|
|
|
45
53
|
@router.get("/", include_in_schema=False)
|
|
46
54
|
def index(request: Request):
|
|
47
|
-
|
|
48
|
-
index_path = active_static / "index.html"
|
|
49
|
-
if not index_path.exists():
|
|
50
|
-
if refresh_static_assets(request.app):
|
|
51
|
-
active_static = request.app.state.static_dir
|
|
52
|
-
index_path = active_static / "index.html"
|
|
53
|
-
if not index_path.exists():
|
|
54
|
-
raise HTTPException(
|
|
55
|
-
status_code=500, detail="Static UI assets missing; reinstall package"
|
|
56
|
-
)
|
|
57
|
-
html = render_index_html(active_static, request.app.state.asset_version)
|
|
58
|
-
return HTMLResponse(html, headers=index_response_headers())
|
|
59
|
-
|
|
60
|
-
@router.get("/api/state", response_model=StateResponse)
|
|
61
|
-
def get_state(request: Request):
|
|
62
|
-
engine = request.app.state.engine
|
|
63
|
-
config = request.app.state.config
|
|
64
|
-
state = load_state(engine.state_path)
|
|
65
|
-
outstanding, done = engine.docs.todos()
|
|
66
|
-
status, runner_pid, running = resolve_runner_status(engine, state)
|
|
67
|
-
lock_payload = resolve_lock_payload(engine)
|
|
68
|
-
codex_model = config.codex_model or extract_flag_value(
|
|
69
|
-
config.codex_args, "--model"
|
|
70
|
-
)
|
|
71
|
-
return {
|
|
72
|
-
"last_run_id": state.last_run_id,
|
|
73
|
-
"status": status,
|
|
74
|
-
"last_exit_code": state.last_exit_code,
|
|
75
|
-
"last_run_started_at": state.last_run_started_at,
|
|
76
|
-
"last_run_finished_at": state.last_run_finished_at,
|
|
77
|
-
"outstanding_count": len(outstanding),
|
|
78
|
-
"done_count": len(done),
|
|
79
|
-
"running": running,
|
|
80
|
-
"runner_pid": runner_pid,
|
|
81
|
-
**lock_payload,
|
|
82
|
-
"terminal_idle_timeout_seconds": config.terminal_idle_timeout_seconds,
|
|
83
|
-
"codex_model": codex_model or "auto",
|
|
84
|
-
}
|
|
55
|
+
return _serve_index(request, static_dir)
|
|
85
56
|
|
|
86
57
|
@router.get("/api/version", response_model=VersionResponse)
|
|
87
58
|
def get_version(request: Request):
|
|
88
59
|
return {"asset_version": request.app.state.asset_version}
|
|
89
60
|
|
|
90
|
-
@router.get("/api/
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
)
|
|
61
|
+
@router.get("/api/repo/health")
|
|
62
|
+
def repo_health(request: Request):
|
|
63
|
+
config = getattr(request.app.state, "config", None)
|
|
64
|
+
if isinstance(config, HubConfig):
|
|
65
|
+
raise HTTPException(
|
|
66
|
+
status_code=404, detail="Repo health not available in hub mode"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
engine = getattr(request.app.state, "engine", None)
|
|
70
|
+
repo_root = getattr(engine, "repo_root", None)
|
|
71
|
+
if repo_root is None:
|
|
72
|
+
return JSONResponse(
|
|
73
|
+
{"status": "error", "detail": "Repo context unavailable"},
|
|
74
|
+
status_code=503,
|
|
75
|
+
)
|
|
106
76
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if
|
|
120
|
-
return PlainTextResponse(text or "")
|
|
121
|
-
payload = {"log": text or ""}
|
|
122
|
-
if run_id is not None:
|
|
123
|
-
payload["run_id"] = run_id
|
|
124
|
-
if tail is not None:
|
|
125
|
-
payload["tail"] = tail
|
|
126
|
-
return JSONResponse(payload)
|
|
127
|
-
|
|
128
|
-
if run_id is not None:
|
|
129
|
-
block = engine.read_run_block(run_id)
|
|
130
|
-
if not block:
|
|
131
|
-
raise HTTPException(status_code=404, detail="run not found")
|
|
132
|
-
return _build_response(block, run_id=run_id)
|
|
133
|
-
if tail is not None:
|
|
134
|
-
return _build_response(engine.tail_log(tail), tail=tail)
|
|
135
|
-
state = load_state(engine.state_path)
|
|
136
|
-
if state.last_run_id is None:
|
|
137
|
-
return _build_response("")
|
|
138
|
-
block = engine.read_run_block(state.last_run_id) or ""
|
|
139
|
-
return _build_response(block, run_id=state.last_run_id)
|
|
140
|
-
|
|
141
|
-
@router.get("/api/logs/stream")
|
|
142
|
-
async def stream_logs_endpoint(request: Request):
|
|
143
|
-
engine = request.app.state.engine
|
|
144
|
-
shutdown_event = getattr(request.app.state, "shutdown_event", None)
|
|
145
|
-
return StreamingResponse(
|
|
146
|
-
log_stream(
|
|
147
|
-
engine.log_path, shutdown_event=shutdown_event, max_seconds=60.0
|
|
148
|
-
),
|
|
149
|
-
media_type="text/event-stream",
|
|
150
|
-
headers=SSE_HEADERS,
|
|
77
|
+
flows_db = repo_root / ".codex-autorunner" / "flows.db"
|
|
78
|
+
|
|
79
|
+
docs_dir = repo_root / ".codex-autorunner"
|
|
80
|
+
docs_status = "ok" if docs_dir.exists() else "missing"
|
|
81
|
+
|
|
82
|
+
tickets_dir = repo_root / ".codex-autorunner" / "tickets"
|
|
83
|
+
tickets_status = "ok" if tickets_dir.exists() else "missing"
|
|
84
|
+
|
|
85
|
+
flows_status = "ok" if tickets_dir.exists() else "missing"
|
|
86
|
+
flows_detail = None
|
|
87
|
+
|
|
88
|
+
overall_status = (
|
|
89
|
+
"ok" if docs_status == "ok" and tickets_status == "ok" else "degraded"
|
|
151
90
|
)
|
|
152
91
|
|
|
92
|
+
return {
|
|
93
|
+
"status": overall_status,
|
|
94
|
+
"mode": "repo",
|
|
95
|
+
"repo_root": str(repo_root),
|
|
96
|
+
"flows": {
|
|
97
|
+
"status": flows_status,
|
|
98
|
+
"path": str(flows_db),
|
|
99
|
+
"detail": flows_detail,
|
|
100
|
+
},
|
|
101
|
+
"docs": {"status": docs_status, "path": str(docs_dir)},
|
|
102
|
+
"tickets": {"status": tickets_status, "path": str(tickets_dir)},
|
|
103
|
+
}
|
|
104
|
+
|
|
153
105
|
@router.websocket("/api/terminal")
|
|
154
106
|
async def terminal(ws: WebSocket):
|
|
155
107
|
selected_protocol = None
|
|
@@ -623,3 +575,23 @@ def build_base_routes(static_dir: Path) -> APIRouter:
|
|
|
623
575
|
active_websockets.discard(ws)
|
|
624
576
|
|
|
625
577
|
return router
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def build_frontend_routes(static_dir: Path) -> APIRouter:
|
|
581
|
+
"""Build catch-all routes for frontend tabs."""
|
|
582
|
+
router = APIRouter()
|
|
583
|
+
|
|
584
|
+
@router.get("/{tab}", include_in_schema=False)
|
|
585
|
+
def tab_route(tab: str, request: Request):
|
|
586
|
+
if tab in {
|
|
587
|
+
"workspace",
|
|
588
|
+
"tickets",
|
|
589
|
+
"messages",
|
|
590
|
+
"analytics",
|
|
591
|
+
"terminal",
|
|
592
|
+
"settings",
|
|
593
|
+
}:
|
|
594
|
+
return _serve_index(request, static_dir)
|
|
595
|
+
raise HTTPException(status_code=404, detail="Not Found")
|
|
596
|
+
|
|
597
|
+
return router
|