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,271 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import zipfile
|
|
5
|
+
from dataclasses import asdict
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, File, Form, HTTPException, Request, UploadFile
|
|
8
|
+
from fastapi.responses import FileResponse, PlainTextResponse, StreamingResponse
|
|
9
|
+
|
|
10
|
+
from ..core import drafts as draft_utils
|
|
11
|
+
from ..tickets.spec_ingest import (
|
|
12
|
+
SpecIngestTicketsError,
|
|
13
|
+
ingest_workspace_spec_to_tickets,
|
|
14
|
+
)
|
|
15
|
+
from ..web.schemas import (
|
|
16
|
+
SpecIngestTicketsResponse,
|
|
17
|
+
WorkspaceFileListResponse,
|
|
18
|
+
WorkspaceResponse,
|
|
19
|
+
WorkspaceTreeResponse,
|
|
20
|
+
WorkspaceUploadResponse,
|
|
21
|
+
WorkspaceWriteRequest,
|
|
22
|
+
)
|
|
23
|
+
from ..workspace.paths import (
|
|
24
|
+
PINNED_DOC_FILENAMES,
|
|
25
|
+
WORKSPACE_DOC_KINDS,
|
|
26
|
+
list_workspace_files,
|
|
27
|
+
list_workspace_tree,
|
|
28
|
+
normalize_workspace_rel_path,
|
|
29
|
+
read_workspace_doc,
|
|
30
|
+
read_workspace_file,
|
|
31
|
+
sanitize_workspace_filename,
|
|
32
|
+
workspace_dir,
|
|
33
|
+
workspace_doc_path,
|
|
34
|
+
write_workspace_doc,
|
|
35
|
+
write_workspace_file,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def build_workspace_routes() -> APIRouter:
|
|
40
|
+
router = APIRouter(prefix="/api", tags=["workspace"])
|
|
41
|
+
|
|
42
|
+
@router.get("/workspace", response_model=WorkspaceResponse)
|
|
43
|
+
def get_workspace(request: Request):
|
|
44
|
+
repo_root = request.app.state.engine.repo_root
|
|
45
|
+
return {
|
|
46
|
+
"active_context": read_workspace_doc(repo_root, "active_context"),
|
|
47
|
+
"decisions": read_workspace_doc(repo_root, "decisions"),
|
|
48
|
+
"spec": read_workspace_doc(repo_root, "spec"),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@router.get("/workspace/file", response_class=PlainTextResponse)
|
|
52
|
+
def read_workspace(request: Request, path: str):
|
|
53
|
+
repo_root = request.app.state.engine.repo_root
|
|
54
|
+
try:
|
|
55
|
+
content = read_workspace_file(repo_root, path)
|
|
56
|
+
except ValueError as exc: # invalid path
|
|
57
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
58
|
+
return PlainTextResponse(content)
|
|
59
|
+
|
|
60
|
+
@router.put("/workspace/file", response_class=PlainTextResponse)
|
|
61
|
+
def write_workspace(request: Request, payload: WorkspaceWriteRequest, path: str):
|
|
62
|
+
repo_root = request.app.state.engine.repo_root
|
|
63
|
+
try:
|
|
64
|
+
# Normalize path the same way workspace helpers do to avoid traversal
|
|
65
|
+
safe_path, rel_posix = normalize_workspace_rel_path(repo_root, path)
|
|
66
|
+
content = write_workspace_file(repo_root, path, payload.content)
|
|
67
|
+
try:
|
|
68
|
+
rel_repo_path = safe_path.relative_to(repo_root).as_posix()
|
|
69
|
+
draft_utils.invalidate_drafts_for_path(repo_root, rel_repo_path)
|
|
70
|
+
state_key = f"workspace_{rel_posix.replace('/', '_')}"
|
|
71
|
+
draft_utils.remove_draft(repo_root, state_key)
|
|
72
|
+
except Exception:
|
|
73
|
+
pass
|
|
74
|
+
except ValueError as exc:
|
|
75
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
76
|
+
return PlainTextResponse(content)
|
|
77
|
+
|
|
78
|
+
@router.put("/workspace/{kind}", response_model=WorkspaceResponse)
|
|
79
|
+
def put_workspace(kind: str, payload: WorkspaceWriteRequest, request: Request):
|
|
80
|
+
key = (kind or "").strip().lower()
|
|
81
|
+
if key not in WORKSPACE_DOC_KINDS:
|
|
82
|
+
raise HTTPException(status_code=400, detail="invalid workspace doc kind")
|
|
83
|
+
repo_root = request.app.state.engine.repo_root
|
|
84
|
+
write_workspace_doc(repo_root, key, payload.content)
|
|
85
|
+
try:
|
|
86
|
+
rel_path = workspace_doc_path(repo_root, key).relative_to(repo_root)
|
|
87
|
+
draft_utils.invalidate_drafts_for_path(repo_root, rel_path.as_posix())
|
|
88
|
+
state_key = f"workspace_{rel_path.name}"
|
|
89
|
+
draft_utils.remove_draft(repo_root, state_key)
|
|
90
|
+
except Exception:
|
|
91
|
+
# best-effort invalidation; avoid blocking writes
|
|
92
|
+
pass
|
|
93
|
+
return {
|
|
94
|
+
"active_context": read_workspace_doc(repo_root, "active_context"),
|
|
95
|
+
"decisions": read_workspace_doc(repo_root, "decisions"),
|
|
96
|
+
"spec": read_workspace_doc(repo_root, "spec"),
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@router.get("/workspace/files", response_model=WorkspaceFileListResponse)
|
|
100
|
+
def list_files(request: Request):
|
|
101
|
+
repo_root = request.app.state.engine.repo_root
|
|
102
|
+
files = [asdict(item) for item in list_workspace_files(repo_root)]
|
|
103
|
+
return {"files": files}
|
|
104
|
+
|
|
105
|
+
@router.get("/workspace/tree", response_model=WorkspaceTreeResponse)
|
|
106
|
+
def get_workspace_tree(request: Request):
|
|
107
|
+
repo_root = request.app.state.engine.repo_root
|
|
108
|
+
tree = [asdict(item) for item in list_workspace_tree(repo_root)]
|
|
109
|
+
return {"tree": tree}
|
|
110
|
+
|
|
111
|
+
@router.post("/workspace/upload", response_model=WorkspaceUploadResponse)
|
|
112
|
+
async def upload_workspace_files(
|
|
113
|
+
request: Request,
|
|
114
|
+
files: list[UploadFile] = File(...), # noqa: B008
|
|
115
|
+
subdir: str = Form(""),
|
|
116
|
+
):
|
|
117
|
+
if not files:
|
|
118
|
+
raise HTTPException(status_code=400, detail="no files provided")
|
|
119
|
+
|
|
120
|
+
repo_root = request.app.state.engine.repo_root
|
|
121
|
+
base = workspace_dir(repo_root)
|
|
122
|
+
target_dir = base
|
|
123
|
+
if subdir:
|
|
124
|
+
try:
|
|
125
|
+
target_dir, _ = normalize_workspace_rel_path(repo_root, subdir)
|
|
126
|
+
except ValueError as exc:
|
|
127
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
128
|
+
|
|
129
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
130
|
+
|
|
131
|
+
uploaded: list[dict[str, str | int]] = []
|
|
132
|
+
for upload in files:
|
|
133
|
+
filename = sanitize_workspace_filename(upload.filename or "")
|
|
134
|
+
try:
|
|
135
|
+
data = await upload.read()
|
|
136
|
+
except (
|
|
137
|
+
Exception
|
|
138
|
+
) as exc: # pragma: no cover - handled by FastAPI for most cases
|
|
139
|
+
raise HTTPException(
|
|
140
|
+
status_code=400, detail="failed to read upload"
|
|
141
|
+
) from exc
|
|
142
|
+
|
|
143
|
+
dest = target_dir / filename
|
|
144
|
+
dest.write_bytes(
|
|
145
|
+
data
|
|
146
|
+
) # codeql[py/path-injection] dest sits under normalized workspace dir
|
|
147
|
+
rel_path = dest.relative_to(base).as_posix()
|
|
148
|
+
uploaded.append({"filename": filename, "path": rel_path, "size": len(data)})
|
|
149
|
+
|
|
150
|
+
return {"status": "ok", "uploaded": uploaded}
|
|
151
|
+
|
|
152
|
+
@router.get("/workspace/download")
|
|
153
|
+
async def download_workspace_file(request: Request, path: str):
|
|
154
|
+
repo_root = request.app.state.engine.repo_root
|
|
155
|
+
try:
|
|
156
|
+
safe_path, _ = normalize_workspace_rel_path(repo_root, path)
|
|
157
|
+
except ValueError as exc:
|
|
158
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
159
|
+
|
|
160
|
+
if not safe_path.exists() or safe_path.is_dir():
|
|
161
|
+
raise HTTPException(status_code=404, detail="file not found")
|
|
162
|
+
|
|
163
|
+
return FileResponse(
|
|
164
|
+
path=safe_path, filename=safe_path.name
|
|
165
|
+
) # codeql[py/path-injection] safe_path validated by normalize_workspace_rel_path
|
|
166
|
+
|
|
167
|
+
@router.get("/workspace/download-zip")
|
|
168
|
+
async def download_workspace_zip(request: Request, path: str = ""):
|
|
169
|
+
repo_root = request.app.state.engine.repo_root
|
|
170
|
+
base = workspace_dir(repo_root)
|
|
171
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
172
|
+
|
|
173
|
+
target_dir = base
|
|
174
|
+
zip_name = "workspace.zip"
|
|
175
|
+
if path:
|
|
176
|
+
try:
|
|
177
|
+
target_dir, _ = normalize_workspace_rel_path(repo_root, path)
|
|
178
|
+
except ValueError as exc:
|
|
179
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
180
|
+
if not target_dir.exists() or not target_dir.is_dir():
|
|
181
|
+
raise HTTPException(status_code=404, detail="folder not found")
|
|
182
|
+
zip_name = f"{target_dir.name}.zip"
|
|
183
|
+
|
|
184
|
+
buffer = io.BytesIO()
|
|
185
|
+
base_real = base.resolve()
|
|
186
|
+
with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
187
|
+
for file_path in target_dir.rglob("*"):
|
|
188
|
+
if file_path.is_dir():
|
|
189
|
+
continue
|
|
190
|
+
if file_path.is_symlink():
|
|
191
|
+
try:
|
|
192
|
+
file_path.resolve().relative_to(base_real)
|
|
193
|
+
except Exception:
|
|
194
|
+
continue
|
|
195
|
+
arc_name = file_path.relative_to(target_dir).as_posix()
|
|
196
|
+
zf.write(
|
|
197
|
+
file_path, arc_name
|
|
198
|
+
) # codeql[py/path-injection] file_path constrained to workspace dir
|
|
199
|
+
|
|
200
|
+
buffer.seek(0)
|
|
201
|
+
return StreamingResponse(
|
|
202
|
+
buffer,
|
|
203
|
+
media_type="application/zip",
|
|
204
|
+
headers={"Content-Disposition": f'attachment; filename="{zip_name}"'},
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
@router.post("/workspace/folder")
|
|
208
|
+
async def create_workspace_folder(request: Request, path: str):
|
|
209
|
+
repo_root = request.app.state.engine.repo_root
|
|
210
|
+
try:
|
|
211
|
+
safe_path, rel_posix = normalize_workspace_rel_path(repo_root, path)
|
|
212
|
+
except ValueError as exc:
|
|
213
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
214
|
+
|
|
215
|
+
if safe_path.exists():
|
|
216
|
+
raise HTTPException(status_code=400, detail="path already exists")
|
|
217
|
+
|
|
218
|
+
safe_path.mkdir(parents=True, exist_ok=True)
|
|
219
|
+
return {"status": "created", "path": rel_posix}
|
|
220
|
+
|
|
221
|
+
@router.delete("/workspace/folder")
|
|
222
|
+
async def delete_workspace_folder(request: Request, path: str):
|
|
223
|
+
repo_root = request.app.state.engine.repo_root
|
|
224
|
+
try:
|
|
225
|
+
safe_path, rel_posix = normalize_workspace_rel_path(repo_root, path)
|
|
226
|
+
except ValueError as exc:
|
|
227
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
228
|
+
|
|
229
|
+
if not safe_path.exists():
|
|
230
|
+
raise HTTPException(status_code=404, detail="folder not found")
|
|
231
|
+
if not safe_path.is_dir():
|
|
232
|
+
raise HTTPException(status_code=400, detail="not a folder")
|
|
233
|
+
if any(safe_path.iterdir()):
|
|
234
|
+
raise HTTPException(status_code=400, detail="folder not empty")
|
|
235
|
+
|
|
236
|
+
safe_path.rmdir()
|
|
237
|
+
return {"status": "deleted", "path": rel_posix}
|
|
238
|
+
|
|
239
|
+
@router.delete("/workspace/file")
|
|
240
|
+
async def delete_workspace_file(request: Request, path: str):
|
|
241
|
+
repo_root = request.app.state.engine.repo_root
|
|
242
|
+
base = workspace_dir(repo_root)
|
|
243
|
+
try:
|
|
244
|
+
safe_path, rel_posix = normalize_workspace_rel_path(repo_root, path)
|
|
245
|
+
except ValueError as exc:
|
|
246
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
247
|
+
|
|
248
|
+
if safe_path.parent == base and safe_path.name in PINNED_DOC_FILENAMES:
|
|
249
|
+
raise HTTPException(status_code=400, detail="cannot delete pinned docs")
|
|
250
|
+
if not safe_path.exists():
|
|
251
|
+
raise HTTPException(status_code=404, detail="file not found")
|
|
252
|
+
if safe_path.is_dir():
|
|
253
|
+
raise HTTPException(status_code=400, detail="use folder delete endpoint")
|
|
254
|
+
|
|
255
|
+
safe_path.unlink() # codeql[py/path-injection] safe_path validated by normalize_workspace_rel_path
|
|
256
|
+
return {"status": "deleted", "path": rel_posix}
|
|
257
|
+
|
|
258
|
+
@router.post("/workspace/spec/ingest", response_model=SpecIngestTicketsResponse)
|
|
259
|
+
def ingest_workspace_spec(request: Request):
|
|
260
|
+
repo_root = request.app.state.engine.repo_root
|
|
261
|
+
try:
|
|
262
|
+
result = ingest_workspace_spec_to_tickets(repo_root)
|
|
263
|
+
except SpecIngestTicketsError as exc:
|
|
264
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
265
|
+
return {
|
|
266
|
+
"status": "ok",
|
|
267
|
+
"created": result.created,
|
|
268
|
+
"first_ticket_path": result.first_ticket_path,
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return router
|
codex_autorunner/server.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from importlib import resources
|
|
2
2
|
|
|
3
3
|
from .core.engine import Engine, LockError, clear_stale_lock, doctor
|
|
4
|
-
from .web.app import create_app, create_hub_app
|
|
4
|
+
from .web.app import create_app, create_hub_app, create_repo_app
|
|
5
5
|
from .web.middleware import BasePathRouterMiddleware
|
|
6
6
|
|
|
7
7
|
__all__ = [
|
|
@@ -11,6 +11,7 @@ __all__ = [
|
|
|
11
11
|
"clear_stale_lock",
|
|
12
12
|
"create_app",
|
|
13
13
|
"create_hub_app",
|
|
14
|
+
"create_repo_app",
|
|
14
15
|
"doctor",
|
|
15
16
|
"resources",
|
|
16
17
|
]
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// GENERATED FILE - do not edit directly. Source: static_src/
|
|
2
|
+
/**
|
|
3
|
+
* Shared parsing helpers for agent (app-server) events.
|
|
4
|
+
* Used by ticket chat and live agent output to render rich activity.
|
|
5
|
+
*/
|
|
6
|
+
function extractCommand(item, params) {
|
|
7
|
+
const command = item?.command ?? params?.command;
|
|
8
|
+
if (Array.isArray(command)) {
|
|
9
|
+
return command
|
|
10
|
+
.map((part) => String(part))
|
|
11
|
+
.join(" ")
|
|
12
|
+
.trim();
|
|
13
|
+
}
|
|
14
|
+
if (typeof command === "string")
|
|
15
|
+
return command.trim();
|
|
16
|
+
return "";
|
|
17
|
+
}
|
|
18
|
+
function extractFiles(payload) {
|
|
19
|
+
const files = [];
|
|
20
|
+
const addEntry = (entry) => {
|
|
21
|
+
if (typeof entry === "string" && entry.trim()) {
|
|
22
|
+
files.push(entry.trim());
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (entry && typeof entry === "object") {
|
|
26
|
+
const entryObj = entry;
|
|
27
|
+
const path = entryObj.path || entryObj.file || entryObj.name;
|
|
28
|
+
if (typeof path === "string" && path.trim()) {
|
|
29
|
+
files.push(path.trim());
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
if (!payload || typeof payload !== "object")
|
|
34
|
+
return files;
|
|
35
|
+
for (const key of ["files", "fileChanges", "paths"]) {
|
|
36
|
+
const value = payload[key];
|
|
37
|
+
if (Array.isArray(value)) {
|
|
38
|
+
value.forEach(addEntry);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
for (const key of ["path", "file", "name"]) {
|
|
42
|
+
addEntry(payload[key]);
|
|
43
|
+
}
|
|
44
|
+
return files;
|
|
45
|
+
}
|
|
46
|
+
function extractErrorMessage(params) {
|
|
47
|
+
if (!params || typeof params !== "object")
|
|
48
|
+
return "";
|
|
49
|
+
const err = params.error;
|
|
50
|
+
if (err && typeof err === "object") {
|
|
51
|
+
const errObj = err;
|
|
52
|
+
const message = typeof errObj.message === "string" ? errObj.message : "";
|
|
53
|
+
const details = typeof errObj.additionalDetails === "string"
|
|
54
|
+
? errObj.additionalDetails
|
|
55
|
+
: typeof errObj.details === "string"
|
|
56
|
+
? errObj.details
|
|
57
|
+
: "";
|
|
58
|
+
if (message && details && message !== details) {
|
|
59
|
+
return `${message} (${details})`;
|
|
60
|
+
}
|
|
61
|
+
return message || details;
|
|
62
|
+
}
|
|
63
|
+
if (typeof err === "string")
|
|
64
|
+
return err;
|
|
65
|
+
if (typeof params.message === "string")
|
|
66
|
+
return params.message;
|
|
67
|
+
return "";
|
|
68
|
+
}
|
|
69
|
+
function hasMeaningfulText(summary, detail) {
|
|
70
|
+
return Boolean(summary.trim() || detail.trim());
|
|
71
|
+
}
|
|
72
|
+
function inferSignificance(kind, method) {
|
|
73
|
+
if (kind === "thinking")
|
|
74
|
+
return true;
|
|
75
|
+
if (kind === "error")
|
|
76
|
+
return true;
|
|
77
|
+
if (["tool", "command", "file", "output"].includes(kind))
|
|
78
|
+
return true;
|
|
79
|
+
if (method.includes("requestApproval"))
|
|
80
|
+
return true;
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Extract output delta text from an event payload.
|
|
85
|
+
*/
|
|
86
|
+
export function extractOutputDelta(payload) {
|
|
87
|
+
const message = payload && typeof payload === "object" ? payload.message || payload : payload;
|
|
88
|
+
if (!message || typeof message !== "object")
|
|
89
|
+
return "";
|
|
90
|
+
const method = String(message.method || "").toLowerCase();
|
|
91
|
+
if (!method.includes("outputdelta"))
|
|
92
|
+
return "";
|
|
93
|
+
const params = message.params || {};
|
|
94
|
+
if (typeof params.delta === "string")
|
|
95
|
+
return params.delta;
|
|
96
|
+
if (typeof params.text === "string")
|
|
97
|
+
return params.text;
|
|
98
|
+
if (typeof params.output === "string")
|
|
99
|
+
return params.output;
|
|
100
|
+
return "";
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Parse an app-server event payload into a normalized AgentEvent plus merge hints.
|
|
104
|
+
*/
|
|
105
|
+
export function parseAppServerEvent(payload) {
|
|
106
|
+
const message = payload && typeof payload === "object" ? payload.message || payload : payload;
|
|
107
|
+
if (!message || typeof message !== "object")
|
|
108
|
+
return null;
|
|
109
|
+
const messageObj = message;
|
|
110
|
+
const method = messageObj.method || "app-server";
|
|
111
|
+
const params = messageObj.params || {};
|
|
112
|
+
const item = params.item || {};
|
|
113
|
+
const itemId = params.itemId || item.id || item.itemId || null;
|
|
114
|
+
const receivedAt = payload && typeof payload === "object"
|
|
115
|
+
? payload.received_at || payload.receivedAt || Date.now()
|
|
116
|
+
: Date.now();
|
|
117
|
+
// Handle reasoning/thinking deltas - accumulate into existing event
|
|
118
|
+
if (method === "item/reasoning/summaryTextDelta") {
|
|
119
|
+
const delta = params.delta || "";
|
|
120
|
+
if (!delta)
|
|
121
|
+
return null;
|
|
122
|
+
const event = {
|
|
123
|
+
id: payload?.id || `${Date.now()}`,
|
|
124
|
+
title: "Thinking",
|
|
125
|
+
summary: delta,
|
|
126
|
+
detail: "",
|
|
127
|
+
kind: "thinking",
|
|
128
|
+
isSignificant: true,
|
|
129
|
+
time: receivedAt,
|
|
130
|
+
itemId,
|
|
131
|
+
method,
|
|
132
|
+
};
|
|
133
|
+
return { event, mergeStrategy: "append" };
|
|
134
|
+
}
|
|
135
|
+
// Handle reasoning part added (paragraph break)
|
|
136
|
+
if (method === "item/reasoning/summaryPartAdded") {
|
|
137
|
+
const event = {
|
|
138
|
+
id: payload?.id || `${Date.now()}`,
|
|
139
|
+
title: "Thinking",
|
|
140
|
+
summary: "",
|
|
141
|
+
detail: "",
|
|
142
|
+
kind: "thinking",
|
|
143
|
+
isSignificant: true,
|
|
144
|
+
time: receivedAt,
|
|
145
|
+
itemId,
|
|
146
|
+
method,
|
|
147
|
+
};
|
|
148
|
+
return { event, mergeStrategy: "newline" };
|
|
149
|
+
}
|
|
150
|
+
let title = method;
|
|
151
|
+
let summary = "";
|
|
152
|
+
let detail = "";
|
|
153
|
+
let kind = "event";
|
|
154
|
+
// Handle generic status updates
|
|
155
|
+
if (method === "status" || params.status) {
|
|
156
|
+
title = "Status";
|
|
157
|
+
summary = params.status || "Processing";
|
|
158
|
+
kind = "status";
|
|
159
|
+
}
|
|
160
|
+
else if (method === "item/completed") {
|
|
161
|
+
const itemType = item.type;
|
|
162
|
+
if (itemType === "commandExecution") {
|
|
163
|
+
title = "Command";
|
|
164
|
+
summary = extractCommand(item, params);
|
|
165
|
+
kind = "command";
|
|
166
|
+
if (item.exitCode !== undefined && item.exitCode !== null) {
|
|
167
|
+
detail = `exit ${item.exitCode}`;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
else if (itemType === "fileChange") {
|
|
171
|
+
title = "File change";
|
|
172
|
+
const files = extractFiles(item);
|
|
173
|
+
summary = files.join(", ") || "Updated files";
|
|
174
|
+
kind = "file";
|
|
175
|
+
}
|
|
176
|
+
else if (itemType === "tool") {
|
|
177
|
+
title = "Tool";
|
|
178
|
+
summary =
|
|
179
|
+
item.name ||
|
|
180
|
+
item.tool ||
|
|
181
|
+
item.id ||
|
|
182
|
+
"Tool call";
|
|
183
|
+
kind = "tool";
|
|
184
|
+
}
|
|
185
|
+
else if (itemType === "agentMessage") {
|
|
186
|
+
title = "Agent";
|
|
187
|
+
summary = item.text || "Agent message";
|
|
188
|
+
kind = "output";
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
title = itemType ? `Item ${itemType}` : "Item completed";
|
|
192
|
+
summary = item.text || item.message || "";
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
else if (method === "item/commandExecution/requestApproval") {
|
|
196
|
+
title = "Command approval";
|
|
197
|
+
summary = extractCommand(item, params) || "Approval requested";
|
|
198
|
+
kind = "command";
|
|
199
|
+
}
|
|
200
|
+
else if (method === "item/fileChange/requestApproval") {
|
|
201
|
+
title = "File approval";
|
|
202
|
+
const files = extractFiles(params);
|
|
203
|
+
summary = files.join(", ") || "Approval requested";
|
|
204
|
+
kind = "file";
|
|
205
|
+
}
|
|
206
|
+
else if (method === "turn/completed") {
|
|
207
|
+
title = "Turn completed";
|
|
208
|
+
summary = params.status || "completed";
|
|
209
|
+
kind = "status";
|
|
210
|
+
}
|
|
211
|
+
else if (method === "error") {
|
|
212
|
+
title = "Error";
|
|
213
|
+
summary = extractErrorMessage(params) || "App-server error";
|
|
214
|
+
kind = "error";
|
|
215
|
+
}
|
|
216
|
+
else if (method.includes("outputDelta")) {
|
|
217
|
+
title = "Output";
|
|
218
|
+
summary = params.delta || params.text || "";
|
|
219
|
+
kind = "output";
|
|
220
|
+
}
|
|
221
|
+
else if (params.delta) {
|
|
222
|
+
title = "Delta";
|
|
223
|
+
summary = params.delta;
|
|
224
|
+
}
|
|
225
|
+
const summaryText = typeof summary === "string" ? summary : String(summary ?? "");
|
|
226
|
+
const detailText = typeof detail === "string" ? detail : String(detail ?? "");
|
|
227
|
+
const meaningful = hasMeaningfulText(summaryText, detailText);
|
|
228
|
+
const isStarted = method.includes("item/started");
|
|
229
|
+
if (!meaningful && isStarted) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
if (!meaningful) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
const isSignificant = inferSignificance(kind, method);
|
|
236
|
+
const event = {
|
|
237
|
+
id: payload?.id || `${Date.now()}`,
|
|
238
|
+
title,
|
|
239
|
+
summary: summaryText,
|
|
240
|
+
detail: detailText,
|
|
241
|
+
kind,
|
|
242
|
+
isSignificant,
|
|
243
|
+
time: receivedAt,
|
|
244
|
+
itemId,
|
|
245
|
+
method,
|
|
246
|
+
};
|
|
247
|
+
return { event };
|
|
248
|
+
}
|
codex_autorunner/static/app.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
|
+
// GENERATED FILE - do not edit directly. Source: static_src/
|
|
1
2
|
import { REPO_ID, HUB_BASE } from "./env.js";
|
|
2
3
|
import { initHub } from "./hub.js";
|
|
3
4
|
import { initTabs, registerTab } from "./tabs.js";
|
|
4
|
-
import { initDashboard } from "./dashboard.js";
|
|
5
|
-
import { initDocs } from "./docs.js";
|
|
6
|
-
import { initLogs } from "./logs.js";
|
|
7
5
|
import { initTerminal } from "./terminal.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { initGitHub } from "./github.js";
|
|
6
|
+
import { initTicketFlow } from "./tickets.js";
|
|
7
|
+
import { initMessages, initMessageBell } from "./messages.js";
|
|
11
8
|
import { initMobileCompact } from "./mobileCompact.js";
|
|
12
9
|
import { subscribe } from "./bus.js";
|
|
13
10
|
import { initRepoSettingsPanel } from "./settings.js";
|
|
14
11
|
import { flash } from "./utils.js";
|
|
15
12
|
import { initLiveUpdates } from "./liveUpdates.js";
|
|
16
|
-
|
|
13
|
+
import { initHealthGate } from "./health.js";
|
|
14
|
+
import { initWorkspace } from "./workspace.js";
|
|
15
|
+
import { initDashboard } from "./dashboard.js";
|
|
16
|
+
async function initRepoShell() {
|
|
17
|
+
await initHealthGate();
|
|
17
18
|
if (REPO_ID) {
|
|
18
19
|
const navBar = document.querySelector(".nav-bar");
|
|
19
20
|
if (navBar) {
|
|
@@ -32,23 +33,27 @@ function initRepoShell() {
|
|
|
32
33
|
brand.insertAdjacentElement("afterend", repoName);
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
|
-
|
|
36
|
-
registerTab("
|
|
37
|
-
registerTab("
|
|
38
|
-
registerTab("
|
|
36
|
+
const defaultTab = REPO_ID ? "tickets" : "analytics";
|
|
37
|
+
registerTab("tickets", "Tickets");
|
|
38
|
+
registerTab("inbox", "Inbox");
|
|
39
|
+
registerTab("analytics", "Analytics");
|
|
40
|
+
registerTab("workspace", "Workspace");
|
|
39
41
|
registerTab("terminal", "Terminal");
|
|
40
42
|
const initializedTabs = new Set();
|
|
41
43
|
const lazyInit = (tabId) => {
|
|
42
44
|
if (initializedTabs.has(tabId))
|
|
43
45
|
return;
|
|
44
|
-
if (tabId === "
|
|
45
|
-
|
|
46
|
+
if (tabId === "workspace") {
|
|
47
|
+
initWorkspace();
|
|
48
|
+
}
|
|
49
|
+
else if (tabId === "inbox" || tabId === "messages") {
|
|
50
|
+
initMessages();
|
|
46
51
|
}
|
|
47
|
-
else if (tabId === "
|
|
48
|
-
|
|
52
|
+
else if (tabId === "analytics") {
|
|
53
|
+
initDashboard();
|
|
49
54
|
}
|
|
50
|
-
else if (tabId === "
|
|
51
|
-
|
|
55
|
+
else if (tabId === "tickets") {
|
|
56
|
+
initTicketFlow();
|
|
52
57
|
}
|
|
53
58
|
initializedTabs.add(tabId);
|
|
54
59
|
};
|
|
@@ -58,7 +63,7 @@ function initRepoShell() {
|
|
|
58
63
|
}
|
|
59
64
|
lazyInit(tabId);
|
|
60
65
|
});
|
|
61
|
-
initTabs();
|
|
66
|
+
initTabs(defaultTab);
|
|
62
67
|
const activePanel = document.querySelector(".panel.active");
|
|
63
68
|
if (activePanel?.id) {
|
|
64
69
|
lazyInit(activePanel.id);
|
|
@@ -67,12 +72,10 @@ function initRepoShell() {
|
|
|
67
72
|
terminalPanel?.addEventListener("pointerdown", () => {
|
|
68
73
|
lazyInit("terminal");
|
|
69
74
|
}, { once: true });
|
|
70
|
-
|
|
75
|
+
initMessageBell();
|
|
71
76
|
initLiveUpdates();
|
|
72
77
|
initRepoSettingsPanel();
|
|
73
|
-
initGitHub();
|
|
74
78
|
initMobileCompact();
|
|
75
|
-
loadState();
|
|
76
79
|
const repoShell = document.getElementById("repo-shell");
|
|
77
80
|
if (repoShell?.hasAttribute("inert")) {
|
|
78
81
|
const openModals = document.querySelectorAll(".modal-overlay:not([hidden])");
|
|
@@ -97,6 +100,6 @@ function bootstrap() {
|
|
|
97
100
|
repoShell.classList.remove("hidden");
|
|
98
101
|
if (hubShell)
|
|
99
102
|
hubShell.classList.add("hidden");
|
|
100
|
-
initRepoShell();
|
|
103
|
+
void initRepoShell();
|
|
101
104
|
}
|
|
102
105
|
bootstrap();
|