codex-autorunner 0.1.2__py3-none-any.whl → 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codex_autorunner/__init__.py +12 -1
- codex_autorunner/__main__.py +4 -0
- codex_autorunner/agents/codex/harness.py +1 -1
- codex_autorunner/agents/opencode/client.py +68 -35
- codex_autorunner/agents/opencode/constants.py +3 -0
- codex_autorunner/agents/opencode/harness.py +6 -1
- codex_autorunner/agents/opencode/logging.py +21 -5
- codex_autorunner/agents/opencode/run_prompt.py +1 -0
- codex_autorunner/agents/opencode/runtime.py +176 -47
- codex_autorunner/agents/opencode/supervisor.py +36 -48
- codex_autorunner/agents/registry.py +155 -8
- codex_autorunner/api.py +25 -0
- codex_autorunner/bootstrap.py +22 -37
- codex_autorunner/cli.py +5 -1156
- codex_autorunner/codex_cli.py +20 -84
- codex_autorunner/core/__init__.py +4 -0
- codex_autorunner/core/about_car.py +49 -32
- codex_autorunner/core/adapter_utils.py +21 -0
- codex_autorunner/core/app_server_ids.py +59 -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 +26 -28
- codex_autorunner/core/app_server_utils.py +165 -0
- codex_autorunner/core/archive.py +349 -0
- codex_autorunner/core/codex_runner.py +12 -2
- codex_autorunner/core/config.py +587 -103
- codex_autorunner/core/docs.py +10 -2
- codex_autorunner/core/drafts.py +136 -0
- codex_autorunner/core/engine.py +1531 -866
- codex_autorunner/core/exceptions.py +4 -0
- codex_autorunner/core/flows/__init__.py +25 -0
- codex_autorunner/core/flows/controller.py +202 -0
- codex_autorunner/core/flows/definition.py +82 -0
- codex_autorunner/core/flows/models.py +88 -0
- codex_autorunner/core/flows/reasons.py +52 -0
- codex_autorunner/core/flows/reconciler.py +131 -0
- codex_autorunner/core/flows/runtime.py +382 -0
- codex_autorunner/core/flows/store.py +568 -0
- codex_autorunner/core/flows/transition.py +138 -0
- codex_autorunner/core/flows/ux_helpers.py +257 -0
- codex_autorunner/core/flows/worker_process.py +242 -0
- codex_autorunner/core/git_utils.py +62 -0
- codex_autorunner/core/hub.py +136 -16
- codex_autorunner/core/locks.py +4 -0
- codex_autorunner/core/notifications.py +14 -2
- codex_autorunner/core/ports/__init__.py +28 -0
- codex_autorunner/core/ports/agent_backend.py +150 -0
- codex_autorunner/core/ports/backend_orchestrator.py +41 -0
- codex_autorunner/core/ports/run_event.py +91 -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/state_roots.py +57 -0
- codex_autorunner/core/supervisor_protocol.py +15 -0
- codex_autorunner/core/supervisor_utils.py +67 -0
- codex_autorunner/core/text_delta_coalescer.py +54 -0
- codex_autorunner/core/ticket_linter_cli.py +201 -0
- codex_autorunner/core/ticket_manager_cli.py +432 -0
- codex_autorunner/core/update.py +24 -16
- codex_autorunner/core/update_paths.py +28 -0
- codex_autorunner/core/update_runner.py +2 -0
- codex_autorunner/core/usage.py +164 -12
- codex_autorunner/core/utils.py +120 -11
- codex_autorunner/discovery.py +2 -4
- codex_autorunner/flows/review/__init__.py +17 -0
- codex_autorunner/{core/review.py → flows/review/service.py} +15 -10
- codex_autorunner/flows/ticket_flow/__init__.py +3 -0
- codex_autorunner/flows/ticket_flow/definition.py +98 -0
- codex_autorunner/integrations/agents/__init__.py +17 -0
- codex_autorunner/integrations/agents/backend_orchestrator.py +284 -0
- codex_autorunner/integrations/agents/codex_adapter.py +90 -0
- codex_autorunner/integrations/agents/codex_backend.py +448 -0
- codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
- codex_autorunner/integrations/agents/opencode_backend.py +598 -0
- codex_autorunner/integrations/agents/runner.py +91 -0
- codex_autorunner/integrations/agents/wiring.py +271 -0
- codex_autorunner/integrations/app_server/client.py +583 -152
- 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/app_server/supervisor.py +59 -33
- codex_autorunner/integrations/telegram/adapter.py +204 -165
- codex_autorunner/integrations/telegram/api_schemas.py +120 -0
- codex_autorunner/integrations/telegram/config.py +221 -0
- codex_autorunner/integrations/telegram/constants.py +17 -2
- codex_autorunner/integrations/telegram/dispatch.py +17 -0
- codex_autorunner/integrations/telegram/doctor.py +47 -0
- codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -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 +1364 -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 +137 -478
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +17 -4
- codex_autorunner/integrations/telegram/handlers/messages.py +121 -9
- codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
- codex_autorunner/integrations/telegram/helpers.py +111 -16
- codex_autorunner/integrations/telegram/outbox.py +208 -37
- codex_autorunner/integrations/telegram/progress_stream.py +3 -10
- codex_autorunner/integrations/telegram/service.py +221 -42
- codex_autorunner/integrations/telegram/state.py +100 -2
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +611 -0
- codex_autorunner/integrations/telegram/transport.py +39 -4
- 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 +37 -67
- codex_autorunner/routes/agents.py +2 -137
- codex_autorunner/routes/analytics.py +3 -0
- codex_autorunner/routes/app_server.py +2 -131
- codex_autorunner/routes/base.py +2 -624
- codex_autorunner/routes/file_chat.py +7 -0
- codex_autorunner/routes/flows.py +7 -0
- codex_autorunner/routes/messages.py +7 -0
- 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 -188
- codex_autorunner/routes/usage.py +3 -0
- codex_autorunner/routes/voice.py +2 -119
- codex_autorunner/routes/workspace.py +3 -0
- codex_autorunner/server.py +3 -2
- codex_autorunner/static/agentControls.js +41 -11
- codex_autorunner/static/agentEvents.js +248 -0
- codex_autorunner/static/app.js +35 -24
- codex_autorunner/static/archive.js +826 -0
- codex_autorunner/static/archiveApi.js +37 -0
- codex_autorunner/static/autoRefresh.js +36 -8
- 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 +344 -325
- 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 +126 -185
- codex_autorunner/static/index.html +839 -863
- codex_autorunner/static/liveUpdates.js +1 -0
- codex_autorunner/static/loader.js +1 -0
- codex_autorunner/static/messages.js +873 -0
- codex_autorunner/static/mobileCompact.js +2 -1
- codex_autorunner/static/preserve.js +17 -0
- codex_autorunner/static/settings.js +149 -217
- codex_autorunner/static/smartRefresh.js +52 -0
- codex_autorunner/static/styles.css +8850 -3876
- codex_autorunner/static/tabs.js +175 -11
- codex_autorunner/static/terminal.js +32 -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 +844 -0
- codex_autorunner/static/ticketVoice.js +9 -0
- codex_autorunner/static/tickets.js +1988 -0
- codex_autorunner/static/utils.js +43 -3
- codex_autorunner/static/voice.js +1 -0
- codex_autorunner/static/workspace.js +765 -0
- codex_autorunner/static/workspaceApi.js +53 -0
- codex_autorunner/static/workspaceFileBrowser.js +504 -0
- codex_autorunner/surfaces/__init__.py +5 -0
- codex_autorunner/surfaces/cli/__init__.py +6 -0
- codex_autorunner/surfaces/cli/cli.py +1224 -0
- codex_autorunner/surfaces/cli/codex_cli.py +20 -0
- codex_autorunner/surfaces/telegram/__init__.py +3 -0
- codex_autorunner/surfaces/web/__init__.py +1 -0
- codex_autorunner/surfaces/web/app.py +2019 -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 +78 -0
- codex_autorunner/surfaces/web/routes/agents.py +138 -0
- codex_autorunner/surfaces/web/routes/analytics.py +277 -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 +836 -0
- codex_autorunner/surfaces/web/routes/flows.py +1164 -0
- codex_autorunner/surfaces/web/routes/messages.py +459 -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 +280 -0
- codex_autorunner/surfaces/web/routes/system.py +196 -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 +417 -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 +27 -0
- codex_autorunner/tickets/agent_pool.py +399 -0
- codex_autorunner/tickets/files.py +89 -0
- codex_autorunner/tickets/frontmatter.py +55 -0
- codex_autorunner/tickets/lint.py +102 -0
- codex_autorunner/tickets/models.py +97 -0
- codex_autorunner/tickets/outbox.py +244 -0
- codex_autorunner/tickets/replies.py +179 -0
- codex_autorunner/tickets/runner.py +881 -0
- codex_autorunner/tickets/spec_ingest.py +77 -0
- codex_autorunner/web/__init__.py +5 -1
- codex_autorunner/web/app.py +2 -1771
- codex_autorunner/web/hub_jobs.py +2 -191
- codex_autorunner/web/middleware.py +2 -587
- codex_autorunner/web/pty_session.py +2 -369
- codex_autorunner/web/runner_manager.py +2 -24
- codex_autorunner/web/schemas.py +2 -396
- codex_autorunner/web/static_assets.py +4 -484
- codex_autorunner/web/static_refresh.py +2 -85
- codex_autorunner/web/terminal_sessions.py +2 -77
- codex_autorunner/workspace/__init__.py +40 -0
- codex_autorunner/workspace/paths.py +335 -0
- codex_autorunner-1.1.0.dist-info/METADATA +154 -0
- codex_autorunner-1.1.0.dist-info/RECORD +308 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.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/METADATA +0 -249
- codex_autorunner-0.1.2.dist-info/RECORD +0 -222
- /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/top_level.txt +0 -0
codex_autorunner/cli.py
CHANGED
|
@@ -1,1159 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
import ipaddress
|
|
3
|
-
import json
|
|
4
|
-
import logging
|
|
5
|
-
import os
|
|
6
|
-
import shlex
|
|
7
|
-
import subprocess
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
from typing import NoReturn, Optional
|
|
1
|
+
"""Backward-compatible CLI entrypoint.
|
|
10
2
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
import uvicorn
|
|
14
|
-
import yaml
|
|
3
|
+
Re-export the Typer app from the CLI surface.
|
|
4
|
+
"""
|
|
15
5
|
|
|
16
|
-
from .
|
|
17
|
-
from .core.config import (
|
|
18
|
-
CONFIG_FILENAME,
|
|
19
|
-
ConfigError,
|
|
20
|
-
HubConfig,
|
|
21
|
-
_normalize_base_path,
|
|
22
|
-
find_nearest_hub_config_path,
|
|
23
|
-
load_hub_config,
|
|
24
|
-
)
|
|
25
|
-
from .core.engine import Engine, LockError, clear_stale_lock, doctor
|
|
26
|
-
from .core.git_utils import GitError, run_git
|
|
27
|
-
from .core.hub import HubSupervisor
|
|
28
|
-
from .core.logging_utils import log_event, setup_rotating_logger
|
|
29
|
-
from .core.optional_dependencies import require_optional_dependencies
|
|
30
|
-
from .core.snapshot import SnapshotError
|
|
31
|
-
from .core.state import RunnerState, load_state, now_iso, save_state, state_lock
|
|
32
|
-
from .core.usage import (
|
|
33
|
-
UsageError,
|
|
34
|
-
default_codex_home,
|
|
35
|
-
parse_iso_datetime,
|
|
36
|
-
summarize_hub_usage,
|
|
37
|
-
summarize_repo_usage,
|
|
38
|
-
)
|
|
39
|
-
from .core.utils import RepoNotFoundError, default_editor, find_repo_root
|
|
40
|
-
from .integrations.app_server.env import build_app_server_env
|
|
41
|
-
from .integrations.app_server.supervisor import WorkspaceAppServerSupervisor
|
|
42
|
-
from .integrations.telegram.adapter import TelegramAPIError, TelegramBotClient
|
|
43
|
-
from .integrations.telegram.service import (
|
|
44
|
-
TelegramBotConfig,
|
|
45
|
-
TelegramBotConfigError,
|
|
46
|
-
TelegramBotLockError,
|
|
47
|
-
TelegramBotService,
|
|
48
|
-
)
|
|
49
|
-
from .manifest import load_manifest
|
|
50
|
-
from .server import create_hub_app
|
|
51
|
-
from .spec_ingest import SpecIngestError, SpecIngestService, clear_work_docs
|
|
52
|
-
from .voice import VoiceConfig
|
|
6
|
+
from .surfaces.cli.cli import _resolve_repo_api_path, app, main # noqa: F401
|
|
53
7
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
app = typer.Typer(add_completion=False)
|
|
57
|
-
hub_app = typer.Typer(add_completion=False)
|
|
58
|
-
telegram_app = typer.Typer(add_completion=False)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def _raise_exit(message: str, *, cause: Optional[BaseException] = None) -> NoReturn:
|
|
62
|
-
typer.echo(message, err=True)
|
|
63
|
-
if cause is not None:
|
|
64
|
-
raise typer.Exit(code=1) from cause
|
|
65
|
-
raise typer.Exit(code=1)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def _require_repo_config(repo: Optional[Path], hub: Optional[Path]) -> Engine:
|
|
69
|
-
try:
|
|
70
|
-
repo_root = find_repo_root(repo or Path.cwd())
|
|
71
|
-
except RepoNotFoundError as exc:
|
|
72
|
-
_raise_exit("No .git directory found for repo commands.", cause=exc)
|
|
73
|
-
try:
|
|
74
|
-
return Engine(repo_root, hub_path=hub)
|
|
75
|
-
except ConfigError as exc:
|
|
76
|
-
_raise_exit(str(exc), cause=exc)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def _require_hub_config(path: Optional[Path]) -> HubConfig:
|
|
80
|
-
try:
|
|
81
|
-
return load_hub_config(path or Path.cwd())
|
|
82
|
-
except ConfigError as exc:
|
|
83
|
-
_raise_exit(str(exc), cause=exc)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def _build_server_url(config, path: str) -> str:
|
|
87
|
-
base_path = config.server_base_path or ""
|
|
88
|
-
if base_path.endswith("/") and path.startswith("/"):
|
|
89
|
-
base_path = base_path[:-1]
|
|
90
|
-
return f"http://{config.server_host}:{config.server_port}{base_path}{path}"
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def _resolve_hub_config_path_for_cli(
|
|
94
|
-
repo_root: Path, hub: Optional[Path]
|
|
95
|
-
) -> Optional[Path]:
|
|
96
|
-
if hub:
|
|
97
|
-
candidate = hub
|
|
98
|
-
if candidate.is_dir():
|
|
99
|
-
candidate = candidate / CONFIG_FILENAME
|
|
100
|
-
return candidate if candidate.exists() else None
|
|
101
|
-
return find_nearest_hub_config_path(repo_root)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def _resolve_repo_api_path(repo_root: Path, hub: Optional[Path], path: str) -> str:
|
|
105
|
-
if not path.startswith("/"):
|
|
106
|
-
path = f"/{path}"
|
|
107
|
-
hub_config_path = _resolve_hub_config_path_for_cli(repo_root, hub)
|
|
108
|
-
if hub_config_path is None:
|
|
109
|
-
return path
|
|
110
|
-
hub_root = hub_config_path.parent.parent.resolve()
|
|
111
|
-
manifest_rel: Optional[str] = None
|
|
112
|
-
try:
|
|
113
|
-
raw = yaml.safe_load(hub_config_path.read_text(encoding="utf-8")) or {}
|
|
114
|
-
if isinstance(raw, dict):
|
|
115
|
-
hub_cfg = raw.get("hub")
|
|
116
|
-
if isinstance(hub_cfg, dict):
|
|
117
|
-
manifest_value = hub_cfg.get("manifest")
|
|
118
|
-
if isinstance(manifest_value, str) and manifest_value.strip():
|
|
119
|
-
manifest_rel = manifest_value.strip()
|
|
120
|
-
except (OSError, yaml.YAMLError, KeyError, ValueError) as exc:
|
|
121
|
-
logger.debug("Failed to read hub config for manifest: %s", exc)
|
|
122
|
-
manifest_rel = None
|
|
123
|
-
manifest_path = hub_root / (manifest_rel or ".codex-autorunner/manifest.yml")
|
|
124
|
-
if not manifest_path.exists():
|
|
125
|
-
return path
|
|
126
|
-
try:
|
|
127
|
-
manifest = load_manifest(manifest_path, hub_root)
|
|
128
|
-
except (OSError, ValueError, KeyError) as exc:
|
|
129
|
-
logger.debug("Failed to load manifest: %s", exc)
|
|
130
|
-
return path
|
|
131
|
-
repo_root = repo_root.resolve()
|
|
132
|
-
for entry in manifest.repos:
|
|
133
|
-
candidate = (hub_root / entry.path).resolve()
|
|
134
|
-
if candidate == repo_root:
|
|
135
|
-
return f"/repos/{entry.id}{path}"
|
|
136
|
-
return path
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def _resolve_auth_token(env_name: str) -> Optional[str]:
|
|
140
|
-
if not env_name:
|
|
141
|
-
return None
|
|
142
|
-
value = os.environ.get(env_name)
|
|
143
|
-
if value is None:
|
|
144
|
-
return None
|
|
145
|
-
value = value.strip()
|
|
146
|
-
return value or None
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
def _require_auth_token(env_name: Optional[str]) -> Optional[str]:
|
|
150
|
-
if not env_name:
|
|
151
|
-
return None
|
|
152
|
-
token = _resolve_auth_token(env_name)
|
|
153
|
-
if not token:
|
|
154
|
-
_raise_exit(
|
|
155
|
-
f"server.auth_token_env is set to {env_name}, but the environment variable is missing."
|
|
156
|
-
)
|
|
157
|
-
return token
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def _is_loopback_host(host: str) -> bool:
|
|
161
|
-
if host == "localhost":
|
|
162
|
-
return True
|
|
163
|
-
try:
|
|
164
|
-
return ipaddress.ip_address(host).is_loopback
|
|
165
|
-
except ValueError:
|
|
166
|
-
return False
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
def _enforce_bind_auth(host: str, token_env: str) -> None:
|
|
170
|
-
if _is_loopback_host(host):
|
|
171
|
-
return
|
|
172
|
-
if _resolve_auth_token(token_env):
|
|
173
|
-
return
|
|
174
|
-
_raise_exit(
|
|
175
|
-
"Refusing to bind to a non-loopback host without server.auth_token_env set."
|
|
176
|
-
)
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
def _request_json(
|
|
180
|
-
method: str,
|
|
181
|
-
url: str,
|
|
182
|
-
payload: Optional[dict] = None,
|
|
183
|
-
token_env: Optional[str] = None,
|
|
184
|
-
) -> dict:
|
|
185
|
-
headers = None
|
|
186
|
-
if token_env:
|
|
187
|
-
token = _require_auth_token(token_env)
|
|
188
|
-
headers = {"Authorization": f"Bearer {token}"}
|
|
189
|
-
response = httpx.request(method, url, json=payload, timeout=2.0, headers=headers)
|
|
190
|
-
response.raise_for_status()
|
|
191
|
-
data = response.json()
|
|
192
|
-
return data if isinstance(data, dict) else {}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
def _require_optional_feature(
|
|
196
|
-
*, feature: str, deps: list[tuple[str, str]], extra: Optional[str] = None
|
|
197
|
-
) -> None:
|
|
198
|
-
try:
|
|
199
|
-
require_optional_dependencies(feature=feature, deps=deps, extra=extra)
|
|
200
|
-
except ConfigError as exc:
|
|
201
|
-
_raise_exit(str(exc), cause=exc)
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
app.add_typer(hub_app, name="hub")
|
|
205
|
-
app.add_typer(telegram_app, name="telegram")
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
def _has_nested_git(path: Path) -> bool:
|
|
209
|
-
try:
|
|
210
|
-
for child in path.iterdir():
|
|
211
|
-
if not child.is_dir() or child.is_symlink():
|
|
212
|
-
continue
|
|
213
|
-
if (child / ".git").exists():
|
|
214
|
-
return True
|
|
215
|
-
if _has_nested_git(child):
|
|
216
|
-
return True
|
|
217
|
-
except OSError:
|
|
218
|
-
return False
|
|
219
|
-
return False
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
@app.command()
|
|
223
|
-
def init(
|
|
224
|
-
path: Optional[Path] = typer.Argument(None, help="Repo path; defaults to CWD"),
|
|
225
|
-
force: bool = typer.Option(False, "--force", help="Overwrite existing files"),
|
|
226
|
-
git_init: bool = typer.Option(False, "--git-init", help="Run git init if missing"),
|
|
227
|
-
mode: str = typer.Option(
|
|
228
|
-
"auto",
|
|
229
|
-
"--mode",
|
|
230
|
-
help="Initialization mode: repo, hub, or auto (default)",
|
|
231
|
-
),
|
|
232
|
-
):
|
|
233
|
-
"""Initialize a repo for Codex autorunner."""
|
|
234
|
-
start_path = (path or Path.cwd()).resolve()
|
|
235
|
-
mode = (mode or "auto").lower()
|
|
236
|
-
if mode not in ("auto", "repo", "hub"):
|
|
237
|
-
_raise_exit("Invalid mode; expected repo, hub, or auto")
|
|
238
|
-
|
|
239
|
-
git_required = True
|
|
240
|
-
target_root: Optional[Path] = None
|
|
241
|
-
selected_mode = mode
|
|
242
|
-
|
|
243
|
-
# First try to treat this as a repo init if requested or auto-detected via .git.
|
|
244
|
-
if mode in ("auto", "repo"):
|
|
245
|
-
try:
|
|
246
|
-
target_root = find_repo_root(start_path)
|
|
247
|
-
selected_mode = "repo"
|
|
248
|
-
except RepoNotFoundError:
|
|
249
|
-
target_root = None
|
|
250
|
-
|
|
251
|
-
# If no git root was found, decide between hub or repo-with-git-init.
|
|
252
|
-
if target_root is None:
|
|
253
|
-
target_root = start_path
|
|
254
|
-
if mode in ("hub",) or (mode == "auto" and _has_nested_git(target_root)):
|
|
255
|
-
selected_mode = "hub"
|
|
256
|
-
git_required = False
|
|
257
|
-
elif git_init:
|
|
258
|
-
selected_mode = "repo"
|
|
259
|
-
try:
|
|
260
|
-
proc = run_git(["init"], target_root, check=False)
|
|
261
|
-
except GitError as exc:
|
|
262
|
-
_raise_exit(f"git init failed: {exc}")
|
|
263
|
-
if proc.returncode != 0:
|
|
264
|
-
detail = (
|
|
265
|
-
proc.stderr or proc.stdout or ""
|
|
266
|
-
).strip() or f"exit {proc.returncode}"
|
|
267
|
-
_raise_exit(f"git init failed: {detail}")
|
|
268
|
-
else:
|
|
269
|
-
_raise_exit("No .git directory found; rerun with --git-init to create one")
|
|
270
|
-
|
|
271
|
-
ca_dir = target_root / ".codex-autorunner"
|
|
272
|
-
ca_dir.mkdir(parents=True, exist_ok=True)
|
|
273
|
-
|
|
274
|
-
hub_config_path = find_nearest_hub_config_path(target_root)
|
|
275
|
-
try:
|
|
276
|
-
if selected_mode == "hub":
|
|
277
|
-
seed_hub_files(target_root, force=force)
|
|
278
|
-
typer.echo(f"Initialized hub at {ca_dir}")
|
|
279
|
-
else:
|
|
280
|
-
seed_repo_files(target_root, force=force, git_required=git_required)
|
|
281
|
-
typer.echo(f"Initialized repo at {ca_dir}")
|
|
282
|
-
if hub_config_path is None:
|
|
283
|
-
seed_hub_files(target_root, force=force)
|
|
284
|
-
typer.echo(f"Initialized hub at {ca_dir}")
|
|
285
|
-
except ConfigError as exc:
|
|
286
|
-
_raise_exit(str(exc), cause=exc)
|
|
287
|
-
typer.echo("Init complete")
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
@app.command()
|
|
291
|
-
def status(
|
|
292
|
-
repo: Optional[Path] = typer.Option(None, "--repo", help="Repo path"),
|
|
293
|
-
hub: Optional[Path] = typer.Option(None, "--hub", help="Hub root path"),
|
|
294
|
-
output_json: bool = typer.Option(False, "--json", help="Emit JSON output"),
|
|
295
|
-
):
|
|
296
|
-
"""Show autorunner status."""
|
|
297
|
-
engine = _require_repo_config(repo, hub)
|
|
298
|
-
state = load_state(engine.state_path)
|
|
299
|
-
outstanding, _ = engine.docs.todos()
|
|
300
|
-
repo_key = str(engine.repo_root)
|
|
301
|
-
session_id = state.repo_to_session.get(repo_key) or state.repo_to_session.get(
|
|
302
|
-
f"{repo_key}:codex"
|
|
303
|
-
)
|
|
304
|
-
opencode_session_id = state.repo_to_session.get(f"{repo_key}:opencode")
|
|
305
|
-
session_record = state.sessions.get(session_id) if session_id else None
|
|
306
|
-
opencode_record = (
|
|
307
|
-
state.sessions.get(opencode_session_id) if opencode_session_id else None
|
|
308
|
-
)
|
|
309
|
-
|
|
310
|
-
if output_json:
|
|
311
|
-
hub_config_path = _resolve_hub_config_path_for_cli(engine.repo_root, hub)
|
|
312
|
-
payload = {
|
|
313
|
-
"repo": str(engine.repo_root),
|
|
314
|
-
"hub": (
|
|
315
|
-
str(hub_config_path.parent.parent.resolve())
|
|
316
|
-
if hub_config_path
|
|
317
|
-
else None
|
|
318
|
-
),
|
|
319
|
-
"status": state.status,
|
|
320
|
-
"last_run_id": state.last_run_id,
|
|
321
|
-
"last_exit_code": state.last_exit_code,
|
|
322
|
-
"last_run_started_at": state.last_run_started_at,
|
|
323
|
-
"last_run_finished_at": state.last_run_finished_at,
|
|
324
|
-
"runner_pid": state.runner_pid,
|
|
325
|
-
"session_id": session_id,
|
|
326
|
-
"session_record": (
|
|
327
|
-
{
|
|
328
|
-
"repo_path": session_record.repo_path,
|
|
329
|
-
"created_at": session_record.created_at,
|
|
330
|
-
"last_seen_at": session_record.last_seen_at,
|
|
331
|
-
"status": session_record.status,
|
|
332
|
-
"agent": session_record.agent,
|
|
333
|
-
}
|
|
334
|
-
if session_record
|
|
335
|
-
else None
|
|
336
|
-
),
|
|
337
|
-
"opencode_session_id": opencode_session_id,
|
|
338
|
-
"opencode_record": (
|
|
339
|
-
{
|
|
340
|
-
"repo_path": opencode_record.repo_path,
|
|
341
|
-
"created_at": opencode_record.created_at,
|
|
342
|
-
"last_seen_at": opencode_record.last_seen_at,
|
|
343
|
-
"status": opencode_record.status,
|
|
344
|
-
"agent": opencode_record.agent,
|
|
345
|
-
}
|
|
346
|
-
if opencode_record
|
|
347
|
-
else None
|
|
348
|
-
),
|
|
349
|
-
"outstanding_todos": len(outstanding),
|
|
350
|
-
}
|
|
351
|
-
typer.echo(json.dumps(payload, indent=2))
|
|
352
|
-
return
|
|
353
|
-
|
|
354
|
-
typer.echo(f"Repo: {engine.repo_root}")
|
|
355
|
-
typer.echo(f"Status: {state.status}")
|
|
356
|
-
typer.echo(f"Last run id: {state.last_run_id}")
|
|
357
|
-
typer.echo(f"Last exit code: {state.last_exit_code}")
|
|
358
|
-
typer.echo(f"Last start: {state.last_run_started_at}")
|
|
359
|
-
typer.echo(f"Last finish: {state.last_run_finished_at}")
|
|
360
|
-
typer.echo(f"Runner pid: {state.runner_pid}")
|
|
361
|
-
if not session_id and not opencode_session_id:
|
|
362
|
-
typer.echo("Terminal session: none")
|
|
363
|
-
if session_id:
|
|
364
|
-
detail = ""
|
|
365
|
-
if session_record:
|
|
366
|
-
detail = f" (status={session_record.status}, last_seen={session_record.last_seen_at})"
|
|
367
|
-
typer.echo(f"Terminal session (codex): {session_id}{detail}")
|
|
368
|
-
if opencode_session_id and opencode_session_id != session_id:
|
|
369
|
-
detail = ""
|
|
370
|
-
if opencode_record:
|
|
371
|
-
detail = f" (status={opencode_record.status}, last_seen={opencode_record.last_seen_at})"
|
|
372
|
-
typer.echo(f"Terminal session (opencode): {opencode_session_id}{detail}")
|
|
373
|
-
typer.echo(f"Outstanding TODO items: {len(outstanding)}")
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
@app.command()
|
|
377
|
-
def sessions(
|
|
378
|
-
repo: Optional[Path] = typer.Option(None, "--repo", help="Repo path"),
|
|
379
|
-
hub: Optional[Path] = typer.Option(None, "--hub", help="Hub root path"),
|
|
380
|
-
output_json: bool = typer.Option(False, "--json", help="Emit JSON output"),
|
|
381
|
-
):
|
|
382
|
-
"""List active terminal sessions."""
|
|
383
|
-
engine = _require_repo_config(repo, hub)
|
|
384
|
-
config = engine.config
|
|
385
|
-
path = _resolve_repo_api_path(engine.repo_root, hub, "/api/sessions")
|
|
386
|
-
url = _build_server_url(config, path)
|
|
387
|
-
auth_token = _resolve_auth_token(config.server_auth_token_env)
|
|
388
|
-
if auth_token:
|
|
389
|
-
url = f"{url}?include_abs_paths=1"
|
|
390
|
-
payload = None
|
|
391
|
-
source = "server"
|
|
392
|
-
try:
|
|
393
|
-
payload = _request_json("GET", url, token_env=config.server_auth_token_env)
|
|
394
|
-
except (
|
|
395
|
-
httpx.HTTPError,
|
|
396
|
-
httpx.ConnectError,
|
|
397
|
-
httpx.TimeoutException,
|
|
398
|
-
OSError,
|
|
399
|
-
) as exc:
|
|
400
|
-
logger.debug(
|
|
401
|
-
"Failed to fetch sessions from server, falling back to state: %s", exc
|
|
402
|
-
)
|
|
403
|
-
state = load_state(engine.state_path)
|
|
404
|
-
payload = {
|
|
405
|
-
"sessions": [
|
|
406
|
-
{
|
|
407
|
-
"session_id": session_id,
|
|
408
|
-
"repo_path": record.repo_path,
|
|
409
|
-
"created_at": record.created_at,
|
|
410
|
-
"last_seen_at": record.last_seen_at,
|
|
411
|
-
"status": record.status,
|
|
412
|
-
"alive": None,
|
|
413
|
-
}
|
|
414
|
-
for session_id, record in state.sessions.items()
|
|
415
|
-
],
|
|
416
|
-
"repo_to_session": dict(state.repo_to_session),
|
|
417
|
-
}
|
|
418
|
-
source = "state"
|
|
419
|
-
|
|
420
|
-
if output_json:
|
|
421
|
-
if source != "server":
|
|
422
|
-
payload["source"] = source
|
|
423
|
-
typer.echo(json.dumps(payload, indent=2))
|
|
424
|
-
return
|
|
425
|
-
|
|
426
|
-
sessions_payload = payload.get("sessions", []) if isinstance(payload, dict) else []
|
|
427
|
-
typer.echo(f"Sessions ({source}): {len(sessions_payload)}")
|
|
428
|
-
for entry in sessions_payload:
|
|
429
|
-
if not isinstance(entry, dict):
|
|
430
|
-
continue
|
|
431
|
-
session_id = entry.get("session_id") or "unknown"
|
|
432
|
-
repo_path = entry.get("abs_repo_path") or entry.get("repo_path") or "unknown"
|
|
433
|
-
status = entry.get("status") or "unknown"
|
|
434
|
-
last_seen = entry.get("last_seen_at") or "unknown"
|
|
435
|
-
alive = entry.get("alive")
|
|
436
|
-
alive_text = "unknown" if alive is None else str(bool(alive))
|
|
437
|
-
typer.echo(
|
|
438
|
-
f"- {session_id}: repo={repo_path} status={status} last_seen={last_seen} alive={alive_text}"
|
|
439
|
-
)
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
@app.command("stop-session")
|
|
443
|
-
def stop_session(
|
|
444
|
-
repo: Optional[Path] = typer.Option(None, "--repo", help="Repo path"),
|
|
445
|
-
hub: Optional[Path] = typer.Option(None, "--hub", help="Hub root path"),
|
|
446
|
-
session_id: Optional[str] = typer.Option(
|
|
447
|
-
None, "--session", help="Session id to stop"
|
|
448
|
-
),
|
|
449
|
-
):
|
|
450
|
-
"""Stop a terminal session by id or repo path."""
|
|
451
|
-
engine = _require_repo_config(repo, hub)
|
|
452
|
-
config = engine.config
|
|
453
|
-
payload: dict[str, str] = {}
|
|
454
|
-
if session_id:
|
|
455
|
-
payload["session_id"] = session_id
|
|
456
|
-
else:
|
|
457
|
-
payload["repo_path"] = str(engine.repo_root)
|
|
458
|
-
|
|
459
|
-
path = _resolve_repo_api_path(engine.repo_root, hub, "/api/sessions/stop")
|
|
460
|
-
url = _build_server_url(config, path)
|
|
461
|
-
try:
|
|
462
|
-
response = _request_json(
|
|
463
|
-
"POST", url, payload, token_env=config.server_auth_token_env
|
|
464
|
-
)
|
|
465
|
-
stopped_id = response.get("session_id", payload.get("session_id", ""))
|
|
466
|
-
typer.echo(f"Stopped session {stopped_id}")
|
|
467
|
-
return
|
|
468
|
-
except (
|
|
469
|
-
httpx.HTTPError,
|
|
470
|
-
httpx.ConnectError,
|
|
471
|
-
httpx.TimeoutException,
|
|
472
|
-
OSError,
|
|
473
|
-
) as exc:
|
|
474
|
-
logger.debug(
|
|
475
|
-
"Failed to stop session via server, falling back to state: %s", exc
|
|
476
|
-
)
|
|
477
|
-
|
|
478
|
-
with state_lock(engine.state_path):
|
|
479
|
-
state = load_state(engine.state_path)
|
|
480
|
-
target_id = payload.get("session_id")
|
|
481
|
-
if not target_id:
|
|
482
|
-
repo_lookup = payload.get("repo_path")
|
|
483
|
-
if repo_lookup:
|
|
484
|
-
target_id = (
|
|
485
|
-
state.repo_to_session.get(repo_lookup)
|
|
486
|
-
or state.repo_to_session.get(f"{repo_lookup}:codex")
|
|
487
|
-
or state.repo_to_session.get(f"{repo_lookup}:opencode")
|
|
488
|
-
)
|
|
489
|
-
if not target_id:
|
|
490
|
-
_raise_exit("Session not found (server unavailable)")
|
|
491
|
-
state.sessions.pop(target_id, None)
|
|
492
|
-
state.repo_to_session = {
|
|
493
|
-
repo_key: sid
|
|
494
|
-
for repo_key, sid in state.repo_to_session.items()
|
|
495
|
-
if sid != target_id
|
|
496
|
-
}
|
|
497
|
-
save_state(engine.state_path, state)
|
|
498
|
-
typer.echo(f"Stopped session {target_id} (state only)")
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
@app.command()
|
|
502
|
-
def usage(
|
|
503
|
-
repo: Optional[Path] = typer.Option(
|
|
504
|
-
None, "--repo", help="Repo or hub path; defaults to CWD"
|
|
505
|
-
),
|
|
506
|
-
hub: Optional[Path] = typer.Option(None, "--hub", help="Hub root path"),
|
|
507
|
-
codex_home: Optional[Path] = typer.Option(
|
|
508
|
-
None, "--codex-home", help="Override CODEX_HOME (defaults to env or ~/.codex)"
|
|
509
|
-
),
|
|
510
|
-
since: Optional[str] = typer.Option(
|
|
511
|
-
None,
|
|
512
|
-
"--since",
|
|
513
|
-
help="ISO timestamp filter, e.g. 2025-12-01 or 2025-12-01T12:00Z",
|
|
514
|
-
),
|
|
515
|
-
until: Optional[str] = typer.Option(
|
|
516
|
-
None, "--until", help="Upper bound ISO timestamp filter"
|
|
517
|
-
),
|
|
518
|
-
output_json: bool = typer.Option(False, "--json", help="Emit JSON output"),
|
|
519
|
-
):
|
|
520
|
-
"""Show Codex/OpenCode token usage for a repo or hub by reading local session logs."""
|
|
521
|
-
try:
|
|
522
|
-
since_dt = parse_iso_datetime(since)
|
|
523
|
-
until_dt = parse_iso_datetime(until)
|
|
524
|
-
except UsageError as exc:
|
|
525
|
-
_raise_exit(str(exc), cause=exc)
|
|
526
|
-
|
|
527
|
-
codex_root = (codex_home or default_codex_home()).expanduser()
|
|
528
|
-
|
|
529
|
-
repo_root: Optional[Path] = None
|
|
530
|
-
try:
|
|
531
|
-
repo_root = find_repo_root(repo or Path.cwd())
|
|
532
|
-
except RepoNotFoundError:
|
|
533
|
-
repo_root = None
|
|
534
|
-
|
|
535
|
-
if repo_root and (repo_root / ".codex-autorunner" / "state.sqlite3").exists():
|
|
536
|
-
engine = _require_repo_config(repo, hub)
|
|
537
|
-
else:
|
|
538
|
-
try:
|
|
539
|
-
config = load_hub_config(hub or repo or Path.cwd())
|
|
540
|
-
except ConfigError as exc:
|
|
541
|
-
_raise_exit(str(exc), cause=exc)
|
|
542
|
-
manifest = load_manifest(config.manifest_path, config.root)
|
|
543
|
-
repo_map = [(entry.id, (config.root / entry.path)) for entry in manifest.repos]
|
|
544
|
-
per_repo, unmatched = summarize_hub_usage(
|
|
545
|
-
repo_map,
|
|
546
|
-
codex_root,
|
|
547
|
-
since=since_dt,
|
|
548
|
-
until=until_dt,
|
|
549
|
-
)
|
|
550
|
-
if output_json:
|
|
551
|
-
payload = {
|
|
552
|
-
"mode": "hub",
|
|
553
|
-
"hub_root": str(config.root),
|
|
554
|
-
"codex_home": str(codex_root),
|
|
555
|
-
"since": since,
|
|
556
|
-
"until": until,
|
|
557
|
-
"repos": {
|
|
558
|
-
repo_id: summary.to_dict() for repo_id, summary in per_repo.items()
|
|
559
|
-
},
|
|
560
|
-
"unmatched": unmatched.to_dict(),
|
|
561
|
-
}
|
|
562
|
-
typer.echo(json.dumps(payload, indent=2))
|
|
563
|
-
return
|
|
564
|
-
|
|
565
|
-
typer.echo(f"Hub: {config.root}")
|
|
566
|
-
typer.echo(f"CODEX_HOME: {codex_root}")
|
|
567
|
-
typer.echo(f"Repos: {len(per_repo)}")
|
|
568
|
-
for repo_id, summary in per_repo.items():
|
|
569
|
-
typer.echo(
|
|
570
|
-
f"- {repo_id}: total={summary.totals.total_tokens} "
|
|
571
|
-
f"(input={summary.totals.input_tokens}, cached={summary.totals.cached_input_tokens}, "
|
|
572
|
-
f"output={summary.totals.output_tokens}, reasoning={summary.totals.reasoning_output_tokens}) "
|
|
573
|
-
f"events={summary.events}"
|
|
574
|
-
)
|
|
575
|
-
if unmatched.events or unmatched.totals.total_tokens:
|
|
576
|
-
typer.echo(
|
|
577
|
-
f"- unmatched: total={unmatched.totals.total_tokens} events={unmatched.events}"
|
|
578
|
-
)
|
|
579
|
-
return
|
|
580
|
-
|
|
581
|
-
summary = summarize_repo_usage(
|
|
582
|
-
engine.repo_root,
|
|
583
|
-
codex_root,
|
|
584
|
-
since=since_dt,
|
|
585
|
-
until=until_dt,
|
|
586
|
-
)
|
|
587
|
-
|
|
588
|
-
if output_json:
|
|
589
|
-
payload = {
|
|
590
|
-
"mode": "repo",
|
|
591
|
-
"repo": str(engine.repo_root),
|
|
592
|
-
"codex_home": str(codex_root),
|
|
593
|
-
"since": since,
|
|
594
|
-
"until": until,
|
|
595
|
-
"usage": summary.to_dict(),
|
|
596
|
-
}
|
|
597
|
-
typer.echo(json.dumps(payload, indent=2))
|
|
598
|
-
return
|
|
599
|
-
|
|
600
|
-
typer.echo(f"Repo: {engine.repo_root}")
|
|
601
|
-
typer.echo(f"CODEX_HOME: {codex_root}")
|
|
602
|
-
typer.echo(
|
|
603
|
-
f"Totals: total={summary.totals.total_tokens} "
|
|
604
|
-
f"(input={summary.totals.input_tokens}, cached={summary.totals.cached_input_tokens}, "
|
|
605
|
-
f"output={summary.totals.output_tokens}, reasoning={summary.totals.reasoning_output_tokens})"
|
|
606
|
-
)
|
|
607
|
-
typer.echo(f"Events counted: {summary.events}")
|
|
608
|
-
if summary.latest_rate_limits:
|
|
609
|
-
primary = summary.latest_rate_limits.get("primary", {}) or {}
|
|
610
|
-
secondary = summary.latest_rate_limits.get("secondary", {}) or {}
|
|
611
|
-
typer.echo(
|
|
612
|
-
f"Latest rate limits: primary_used={primary.get('used_percent')}%/{primary.get('window_minutes')}m, "
|
|
613
|
-
f"secondary_used={secondary.get('used_percent')}%/{secondary.get('window_minutes')}m"
|
|
614
|
-
)
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
@app.command()
|
|
618
|
-
def run(
|
|
619
|
-
repo: Optional[Path] = typer.Option(None, "--repo", help="Repo path"),
|
|
620
|
-
hub: Optional[Path] = typer.Option(None, "--hub", help="Hub root path"),
|
|
621
|
-
force: bool = typer.Option(False, "--force", help="Ignore existing lock"),
|
|
622
|
-
):
|
|
623
|
-
"""Run the autorunner loop."""
|
|
624
|
-
engine: Optional[Engine] = None
|
|
625
|
-
try:
|
|
626
|
-
engine = _require_repo_config(repo, hub)
|
|
627
|
-
engine.clear_stop_request()
|
|
628
|
-
engine.acquire_lock(force=force)
|
|
629
|
-
engine.run_loop()
|
|
630
|
-
except (ConfigError, LockError) as exc:
|
|
631
|
-
_raise_exit(str(exc), cause=exc)
|
|
632
|
-
finally:
|
|
633
|
-
if engine:
|
|
634
|
-
try:
|
|
635
|
-
engine.release_lock()
|
|
636
|
-
except OSError as exc:
|
|
637
|
-
logger.debug("Failed to release lock in run command: %s", exc)
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
@app.command()
|
|
641
|
-
def once(
|
|
642
|
-
repo: Optional[Path] = typer.Option(None, "--repo", help="Repo path"),
|
|
643
|
-
hub: Optional[Path] = typer.Option(None, "--hub", help="Hub root path"),
|
|
644
|
-
force: bool = typer.Option(False, "--force", help="Ignore existing lock"),
|
|
645
|
-
):
|
|
646
|
-
"""Execute a single Codex run."""
|
|
647
|
-
engine: Optional[Engine] = None
|
|
648
|
-
try:
|
|
649
|
-
engine = _require_repo_config(repo, hub)
|
|
650
|
-
engine.clear_stop_request()
|
|
651
|
-
engine.acquire_lock(force=force)
|
|
652
|
-
engine.run_once()
|
|
653
|
-
except (ConfigError, LockError) as exc:
|
|
654
|
-
_raise_exit(str(exc), cause=exc)
|
|
655
|
-
finally:
|
|
656
|
-
if engine:
|
|
657
|
-
try:
|
|
658
|
-
engine.release_lock()
|
|
659
|
-
except OSError as exc:
|
|
660
|
-
logger.debug("Failed to release lock in once command: %s", exc)
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
@app.command()
|
|
664
|
-
def kill(
|
|
665
|
-
repo: Optional[Path] = typer.Option(None, "--repo", help="Repo path"),
|
|
666
|
-
hub: Optional[Path] = typer.Option(None, "--hub", help="Hub root path"),
|
|
667
|
-
):
|
|
668
|
-
"""Force-kill a running autorunner and clear stale lock/state."""
|
|
669
|
-
engine = _require_repo_config(repo, hub)
|
|
670
|
-
pid = engine.kill_running_process()
|
|
671
|
-
with state_lock(engine.state_path):
|
|
672
|
-
state = load_state(engine.state_path)
|
|
673
|
-
new_state = RunnerState(
|
|
674
|
-
last_run_id=state.last_run_id,
|
|
675
|
-
status="error",
|
|
676
|
-
last_exit_code=137,
|
|
677
|
-
last_run_started_at=state.last_run_started_at,
|
|
678
|
-
last_run_finished_at=now_iso(),
|
|
679
|
-
autorunner_agent_override=state.autorunner_agent_override,
|
|
680
|
-
autorunner_model_override=state.autorunner_model_override,
|
|
681
|
-
autorunner_effort_override=state.autorunner_effort_override,
|
|
682
|
-
autorunner_approval_policy=state.autorunner_approval_policy,
|
|
683
|
-
autorunner_sandbox_mode=state.autorunner_sandbox_mode,
|
|
684
|
-
autorunner_workspace_write_network=state.autorunner_workspace_write_network,
|
|
685
|
-
runner_pid=None,
|
|
686
|
-
sessions=state.sessions,
|
|
687
|
-
repo_to_session=state.repo_to_session,
|
|
688
|
-
)
|
|
689
|
-
save_state(engine.state_path, new_state)
|
|
690
|
-
clear_stale_lock(engine.lock_path)
|
|
691
|
-
if pid:
|
|
692
|
-
typer.echo(f"Sent SIGTERM to pid {pid}")
|
|
693
|
-
else:
|
|
694
|
-
typer.echo("No active autorunner process found; cleared stale lock if any.")
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
@app.command()
|
|
698
|
-
def resume(
|
|
699
|
-
repo: Optional[Path] = typer.Option(None, "--repo", help="Repo path"),
|
|
700
|
-
hub: Optional[Path] = typer.Option(None, "--hub", help="Hub root path"),
|
|
701
|
-
once: bool = typer.Option(False, "--once", help="Resume with a single run"),
|
|
702
|
-
force: bool = typer.Option(False, "--force", help="Override active lock"),
|
|
703
|
-
):
|
|
704
|
-
"""Resume a stopped/errored autorunner, clearing stale locks if needed."""
|
|
705
|
-
engine: Optional[Engine] = None
|
|
706
|
-
try:
|
|
707
|
-
engine = _require_repo_config(repo, hub)
|
|
708
|
-
engine.clear_stop_request()
|
|
709
|
-
clear_stale_lock(engine.lock_path)
|
|
710
|
-
engine.acquire_lock(force=force)
|
|
711
|
-
engine.run_loop(stop_after_runs=1 if once else None)
|
|
712
|
-
except (ConfigError, LockError) as exc:
|
|
713
|
-
_raise_exit(str(exc), cause=exc)
|
|
714
|
-
finally:
|
|
715
|
-
if engine:
|
|
716
|
-
try:
|
|
717
|
-
engine.release_lock()
|
|
718
|
-
except OSError as exc:
|
|
719
|
-
logger.debug("Failed to release lock in resume command: %s", exc)
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
@app.command()
|
|
723
|
-
def log(
|
|
724
|
-
repo: Optional[Path] = typer.Option(None, "--repo", help="Repo path"),
|
|
725
|
-
hub: Optional[Path] = typer.Option(None, "--hub", help="Hub root path"),
|
|
726
|
-
run_id: Optional[int] = typer.Option(None, "--run", help="Show a specific run"),
|
|
727
|
-
tail: Optional[int] = typer.Option(None, "--tail", help="Tail last N lines"),
|
|
728
|
-
):
|
|
729
|
-
"""Show autorunner log output."""
|
|
730
|
-
engine = _require_repo_config(repo, hub)
|
|
731
|
-
if not engine.log_path.exists():
|
|
732
|
-
_raise_exit("Log file not found; run init")
|
|
733
|
-
|
|
734
|
-
if run_id is not None:
|
|
735
|
-
block = engine.read_run_block(run_id)
|
|
736
|
-
if not block:
|
|
737
|
-
_raise_exit("run not found")
|
|
738
|
-
typer.echo(block)
|
|
739
|
-
return
|
|
740
|
-
|
|
741
|
-
if tail is not None:
|
|
742
|
-
typer.echo(engine.tail_log(tail))
|
|
743
|
-
else:
|
|
744
|
-
state = load_state(engine.state_path)
|
|
745
|
-
last_id = state.last_run_id
|
|
746
|
-
if last_id is None:
|
|
747
|
-
typer.echo("No runs recorded yet")
|
|
748
|
-
return
|
|
749
|
-
block = engine.read_run_block(last_id)
|
|
750
|
-
if not block:
|
|
751
|
-
typer.echo("No run block found (log may have rotated)")
|
|
752
|
-
return
|
|
753
|
-
typer.echo(block)
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
@app.command()
|
|
757
|
-
def edit(
|
|
758
|
-
target: str = typer.Argument(..., help="todo|progress|opinions|spec"),
|
|
759
|
-
repo: Optional[Path] = typer.Option(None, "--repo", help="Repo path"),
|
|
760
|
-
hub: Optional[Path] = typer.Option(None, "--hub", help="Hub root path"),
|
|
761
|
-
):
|
|
762
|
-
"""Open one of the docs in $EDITOR."""
|
|
763
|
-
engine = _require_repo_config(repo, hub)
|
|
764
|
-
config = engine.config
|
|
765
|
-
key = target.lower()
|
|
766
|
-
if key not in ("todo", "progress", "opinions", "spec"):
|
|
767
|
-
_raise_exit("Invalid target; choose todo, progress, opinions, or spec")
|
|
768
|
-
path = config.doc_path(key)
|
|
769
|
-
editor = os.environ.get("VISUAL") or os.environ.get("EDITOR") or default_editor()
|
|
770
|
-
editor_parts = shlex.split(editor)
|
|
771
|
-
if not editor_parts:
|
|
772
|
-
editor_parts = [editor]
|
|
773
|
-
typer.echo(f"Opening {path} with {' '.join(editor_parts)}")
|
|
774
|
-
subprocess.run([*editor_parts, str(path)])
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
@app.command("ingest-spec")
|
|
778
|
-
def ingest_spec_cmd(
|
|
779
|
-
repo: Optional[Path] = typer.Option(None, "--repo", help="Repo path"),
|
|
780
|
-
hub: Optional[Path] = typer.Option(None, "--hub", help="Hub root path"),
|
|
781
|
-
spec: Optional[Path] = typer.Option(
|
|
782
|
-
None, "--spec", help="Path to SPEC (defaults to configured docs.spec)"
|
|
783
|
-
),
|
|
784
|
-
force: bool = typer.Option(
|
|
785
|
-
False, "--force", help="Overwrite TODO/PROGRESS/OPINIONS"
|
|
786
|
-
),
|
|
787
|
-
):
|
|
788
|
-
"""Generate TODO/PROGRESS/OPINIONS from SPEC using Codex."""
|
|
789
|
-
try:
|
|
790
|
-
engine = _require_repo_config(repo, hub)
|
|
791
|
-
config = engine.config
|
|
792
|
-
if not config.app_server.command:
|
|
793
|
-
raise SpecIngestError("app_server.command must be configured")
|
|
794
|
-
|
|
795
|
-
async def _run_ingest() -> dict:
|
|
796
|
-
logger = logging.getLogger("codex_autorunner.cli.app_server")
|
|
797
|
-
|
|
798
|
-
def _env_builder(
|
|
799
|
-
workspace_root: Path, _workspace_id: str, state_dir: Path
|
|
800
|
-
) -> dict[str, str]:
|
|
801
|
-
state_dir.mkdir(parents=True, exist_ok=True)
|
|
802
|
-
return build_app_server_env(
|
|
803
|
-
config.app_server.command,
|
|
804
|
-
workspace_root,
|
|
805
|
-
state_dir,
|
|
806
|
-
logger=logger,
|
|
807
|
-
event_prefix="cli",
|
|
808
|
-
)
|
|
809
|
-
|
|
810
|
-
supervisor = WorkspaceAppServerSupervisor(
|
|
811
|
-
config.app_server.command,
|
|
812
|
-
state_root=config.app_server.state_root,
|
|
813
|
-
env_builder=_env_builder,
|
|
814
|
-
logger=logger,
|
|
815
|
-
max_handles=config.app_server.max_handles,
|
|
816
|
-
idle_ttl_seconds=config.app_server.idle_ttl_seconds,
|
|
817
|
-
request_timeout=config.app_server.request_timeout,
|
|
818
|
-
)
|
|
819
|
-
service = SpecIngestService(engine, app_server_supervisor=supervisor)
|
|
820
|
-
try:
|
|
821
|
-
await service.execute(force=force, spec_path=spec, message=None)
|
|
822
|
-
return service.apply_patch()
|
|
823
|
-
finally:
|
|
824
|
-
await supervisor.close_all()
|
|
825
|
-
|
|
826
|
-
docs = asyncio.run(_run_ingest())
|
|
827
|
-
except (ConfigError, SpecIngestError) as exc:
|
|
828
|
-
_raise_exit(str(exc), cause=exc)
|
|
829
|
-
|
|
830
|
-
typer.echo("Ingested SPEC into TODO/PROGRESS/OPINIONS.")
|
|
831
|
-
for key, content in docs.items():
|
|
832
|
-
lines = len(content.splitlines())
|
|
833
|
-
typer.echo(f"- {key.upper()}: {lines} lines")
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
@app.command("clear-docs")
|
|
837
|
-
def clear_docs_cmd(
|
|
838
|
-
repo: Optional[Path] = typer.Option(None, "--repo", help="Repo path"),
|
|
839
|
-
hub: Optional[Path] = typer.Option(None, "--hub", help="Hub root path"),
|
|
840
|
-
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
|
|
841
|
-
):
|
|
842
|
-
"""Clear TODO/PROGRESS/OPINIONS to empty templates."""
|
|
843
|
-
if not yes:
|
|
844
|
-
confirm = input("Clear TODO/PROGRESS/OPINIONS? Type CLEAR to confirm: ").strip()
|
|
845
|
-
if confirm.upper() != "CLEAR":
|
|
846
|
-
_raise_exit("Aborted.")
|
|
847
|
-
engine = _require_repo_config(repo, hub)
|
|
848
|
-
try:
|
|
849
|
-
clear_work_docs(engine)
|
|
850
|
-
except ConfigError as exc:
|
|
851
|
-
_raise_exit(str(exc), cause=exc)
|
|
852
|
-
typer.echo("Cleared TODO/PROGRESS/OPINIONS.")
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
@app.command("doctor")
|
|
856
|
-
def doctor_cmd(
|
|
857
|
-
repo: Optional[Path] = typer.Option(None, "--repo", help="Repo or hub path"),
|
|
858
|
-
json_output: bool = typer.Option(False, "--json", help="Output JSON for scripting"),
|
|
859
|
-
):
|
|
860
|
-
"""Validate repo or hub setup."""
|
|
861
|
-
try:
|
|
862
|
-
report = doctor(repo or Path.cwd())
|
|
863
|
-
except ConfigError as exc:
|
|
864
|
-
_raise_exit(str(exc), cause=exc)
|
|
865
|
-
if json_output:
|
|
866
|
-
typer.echo(json.dumps(report.to_dict(), indent=2))
|
|
867
|
-
if report.has_errors():
|
|
868
|
-
raise typer.Exit(code=1)
|
|
869
|
-
return
|
|
870
|
-
for check in report.checks:
|
|
871
|
-
line = f"- {check.status.upper()}: {check.message}"
|
|
872
|
-
if check.fix:
|
|
873
|
-
line = f"{line} Fix: {check.fix}"
|
|
874
|
-
typer.echo(line)
|
|
875
|
-
if report.has_errors():
|
|
876
|
-
_raise_exit("Doctor check failed")
|
|
877
|
-
typer.echo("Doctor check passed")
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
@app.command()
|
|
881
|
-
def snapshot(
|
|
882
|
-
repo: Optional[Path] = typer.Option(None, "--repo", help="Repo path"),
|
|
883
|
-
hub: Optional[Path] = typer.Option(None, "--hub", help="Hub root path"),
|
|
884
|
-
):
|
|
885
|
-
"""Generate or update `.codex-autorunner/SNAPSHOT.md`."""
|
|
886
|
-
try:
|
|
887
|
-
engine = _require_repo_config(repo, hub)
|
|
888
|
-
config = engine.config
|
|
889
|
-
if not config.app_server.command:
|
|
890
|
-
raise SnapshotError("app_server.command must be configured")
|
|
891
|
-
|
|
892
|
-
async def _run_snapshot() -> None:
|
|
893
|
-
logger = logging.getLogger("codex_autorunner.cli.app_server")
|
|
894
|
-
|
|
895
|
-
def _env_builder(
|
|
896
|
-
workspace_root: Path, _workspace_id: str, state_dir: Path
|
|
897
|
-
) -> dict[str, str]:
|
|
898
|
-
state_dir.mkdir(parents=True, exist_ok=True)
|
|
899
|
-
return build_app_server_env(
|
|
900
|
-
config.app_server.command,
|
|
901
|
-
workspace_root,
|
|
902
|
-
state_dir,
|
|
903
|
-
logger=logger,
|
|
904
|
-
event_prefix="cli",
|
|
905
|
-
)
|
|
906
|
-
|
|
907
|
-
supervisor = WorkspaceAppServerSupervisor(
|
|
908
|
-
config.app_server.command,
|
|
909
|
-
state_root=config.app_server.state_root,
|
|
910
|
-
env_builder=_env_builder,
|
|
911
|
-
logger=logger,
|
|
912
|
-
max_handles=config.app_server.max_handles,
|
|
913
|
-
idle_ttl_seconds=config.app_server.idle_ttl_seconds,
|
|
914
|
-
request_timeout=config.app_server.request_timeout,
|
|
915
|
-
)
|
|
916
|
-
from .core.snapshot import SnapshotService
|
|
917
|
-
|
|
918
|
-
service = SnapshotService(engine, app_server_supervisor=supervisor)
|
|
919
|
-
try:
|
|
920
|
-
await service.generate_snapshot()
|
|
921
|
-
finally:
|
|
922
|
-
await supervisor.close_all()
|
|
923
|
-
|
|
924
|
-
asyncio.run(_run_snapshot())
|
|
925
|
-
except (ConfigError, SnapshotError) as exc:
|
|
926
|
-
_raise_exit(str(exc), cause=exc)
|
|
927
|
-
typer.echo("Snapshot written to .codex-autorunner/SNAPSHOT.md")
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
@app.command()
|
|
931
|
-
def serve(
|
|
932
|
-
path: Optional[Path] = typer.Option(None, "--path", "--hub", help="Hub root path"),
|
|
933
|
-
host: Optional[str] = typer.Option(None, "--host", help="Host to bind"),
|
|
934
|
-
port: Optional[int] = typer.Option(None, "--port", help="Port to bind"),
|
|
935
|
-
base_path: Optional[str] = typer.Option(
|
|
936
|
-
None, "--base-path", help="Base path for the server"
|
|
937
|
-
),
|
|
938
|
-
):
|
|
939
|
-
"""Start the hub web server and UI API."""
|
|
940
|
-
try:
|
|
941
|
-
config = load_hub_config(path or Path.cwd())
|
|
942
|
-
except ConfigError as exc:
|
|
943
|
-
_raise_exit(str(exc), cause=exc)
|
|
944
|
-
bind_host = host or config.server_host
|
|
945
|
-
bind_port = port or config.server_port
|
|
946
|
-
normalized_base = (
|
|
947
|
-
_normalize_base_path(base_path)
|
|
948
|
-
if base_path is not None
|
|
949
|
-
else config.server_base_path
|
|
950
|
-
)
|
|
951
|
-
_enforce_bind_auth(bind_host, config.server_auth_token_env)
|
|
952
|
-
typer.echo(f"Serving hub on http://{bind_host}:{bind_port}{normalized_base or ''}")
|
|
953
|
-
uvicorn.run(
|
|
954
|
-
create_hub_app(config.root, base_path=normalized_base),
|
|
955
|
-
host=bind_host,
|
|
956
|
-
port=bind_port,
|
|
957
|
-
root_path="",
|
|
958
|
-
access_log=config.server_access_log,
|
|
959
|
-
)
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
@hub_app.command("create")
|
|
963
|
-
def hub_create(
|
|
964
|
-
repo_id: str = typer.Argument(..., help="Repo id to create and initialize"),
|
|
965
|
-
repo_path: Optional[Path] = typer.Option(
|
|
966
|
-
None,
|
|
967
|
-
"--repo-path",
|
|
968
|
-
help="Custom repo path relative to hub repos_root",
|
|
969
|
-
),
|
|
970
|
-
path: Optional[Path] = typer.Option(None, "--path", help="Hub root path"),
|
|
971
|
-
force: bool = typer.Option(False, "--force", help="Allow existing directory"),
|
|
972
|
-
git_init: bool = typer.Option(
|
|
973
|
-
True, "--git-init/--no-git-init", help="Run git init in the new repo"
|
|
974
|
-
),
|
|
975
|
-
):
|
|
976
|
-
"""Create a new git repo under the hub and initialize codex-autorunner files."""
|
|
977
|
-
config = _require_hub_config(path)
|
|
978
|
-
supervisor = HubSupervisor(config)
|
|
979
|
-
try:
|
|
980
|
-
snapshot = supervisor.create_repo(
|
|
981
|
-
repo_id, repo_path, git_init=git_init, force=force
|
|
982
|
-
)
|
|
983
|
-
except Exception as exc:
|
|
984
|
-
_raise_exit(str(exc), cause=exc)
|
|
985
|
-
typer.echo(f"Created repo {snapshot.id} at {snapshot.path}")
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
@hub_app.command("clone")
|
|
989
|
-
def hub_clone(
|
|
990
|
-
git_url: str = typer.Option(
|
|
991
|
-
..., "--git-url", help="Git URL or local path to clone"
|
|
992
|
-
),
|
|
993
|
-
repo_id: Optional[str] = typer.Option(
|
|
994
|
-
None, "--id", help="Repo id to register (defaults from git URL)"
|
|
995
|
-
),
|
|
996
|
-
repo_path: Optional[Path] = typer.Option(
|
|
997
|
-
None,
|
|
998
|
-
"--repo-path",
|
|
999
|
-
help="Custom repo path relative to hub repos_root",
|
|
1000
|
-
),
|
|
1001
|
-
path: Optional[Path] = typer.Option(None, "--path", help="Hub root path"),
|
|
1002
|
-
force: bool = typer.Option(False, "--force", help="Allow existing directory"),
|
|
1003
|
-
):
|
|
1004
|
-
"""Clone a git repo under the hub and initialize codex-autorunner files."""
|
|
1005
|
-
config = _require_hub_config(path)
|
|
1006
|
-
supervisor = HubSupervisor(config)
|
|
1007
|
-
try:
|
|
1008
|
-
snapshot = supervisor.clone_repo(
|
|
1009
|
-
git_url=git_url, repo_id=repo_id, repo_path=repo_path, force=force
|
|
1010
|
-
)
|
|
1011
|
-
except Exception as exc:
|
|
1012
|
-
_raise_exit(str(exc), cause=exc)
|
|
1013
|
-
typer.echo(
|
|
1014
|
-
f"Cloned repo {snapshot.id} at {snapshot.path} (status={snapshot.status.value})"
|
|
1015
|
-
)
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
@hub_app.command("serve")
|
|
1019
|
-
def hub_serve(
|
|
1020
|
-
path: Optional[Path] = typer.Option(None, "--path", help="Hub root path"),
|
|
1021
|
-
host: Optional[str] = typer.Option(None, "--host", help="Host to bind"),
|
|
1022
|
-
port: Optional[int] = typer.Option(None, "--port", help="Port to bind"),
|
|
1023
|
-
base_path: Optional[str] = typer.Option(
|
|
1024
|
-
None, "--base-path", help="Base path for the server"
|
|
1025
|
-
),
|
|
1026
|
-
):
|
|
1027
|
-
"""Start the hub supervisor server."""
|
|
1028
|
-
config = _require_hub_config(path)
|
|
1029
|
-
normalized_base = (
|
|
1030
|
-
_normalize_base_path(base_path)
|
|
1031
|
-
if base_path is not None
|
|
1032
|
-
else config.server_base_path
|
|
1033
|
-
)
|
|
1034
|
-
bind_host = host or config.server_host
|
|
1035
|
-
bind_port = port or config.server_port
|
|
1036
|
-
_enforce_bind_auth(bind_host, config.server_auth_token_env)
|
|
1037
|
-
typer.echo(f"Serving hub on http://{bind_host}:{bind_port}{normalized_base or ''}")
|
|
1038
|
-
uvicorn.run(
|
|
1039
|
-
create_hub_app(config.root, base_path=normalized_base),
|
|
1040
|
-
host=bind_host,
|
|
1041
|
-
port=bind_port,
|
|
1042
|
-
root_path="",
|
|
1043
|
-
access_log=config.server_access_log,
|
|
1044
|
-
)
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
@hub_app.command("scan")
|
|
1048
|
-
def hub_scan(path: Optional[Path] = typer.Option(None, "--path", help="Hub root path")):
|
|
1049
|
-
"""Trigger discovery/init and print repo statuses."""
|
|
1050
|
-
config = _require_hub_config(path)
|
|
1051
|
-
supervisor = HubSupervisor(config)
|
|
1052
|
-
snapshots = supervisor.scan()
|
|
1053
|
-
typer.echo(f"Scanned hub at {config.root} (repos_root={config.repos_root})")
|
|
1054
|
-
for snap in snapshots:
|
|
1055
|
-
typer.echo(
|
|
1056
|
-
f"- {snap.id}: {snap.status.value}, initialized={snap.initialized}, exists={snap.exists_on_disk}"
|
|
1057
|
-
)
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
@telegram_app.command("start")
|
|
1061
|
-
def telegram_start(
|
|
1062
|
-
path: Optional[Path] = typer.Option(None, "--path", help="Repo or hub root path"),
|
|
1063
|
-
):
|
|
1064
|
-
"""Start the Telegram bot (polling)."""
|
|
1065
|
-
_require_optional_feature(
|
|
1066
|
-
feature="telegram",
|
|
1067
|
-
deps=[("httpx", "httpx")],
|
|
1068
|
-
extra="telegram",
|
|
1069
|
-
)
|
|
1070
|
-
try:
|
|
1071
|
-
config = load_hub_config(path or Path.cwd())
|
|
1072
|
-
except ConfigError as exc:
|
|
1073
|
-
_raise_exit(str(exc), cause=exc)
|
|
1074
|
-
telegram_cfg = TelegramBotConfig.from_raw(
|
|
1075
|
-
config.raw.get("telegram_bot") if isinstance(config.raw, dict) else None,
|
|
1076
|
-
root=config.root,
|
|
1077
|
-
agent_binaries=getattr(config, "agents", None)
|
|
1078
|
-
and {name: agent.binary for name, agent in config.agents.items()},
|
|
1079
|
-
)
|
|
1080
|
-
if not telegram_cfg.enabled:
|
|
1081
|
-
_raise_exit("telegram_bot is disabled; set telegram_bot.enabled: true")
|
|
1082
|
-
try:
|
|
1083
|
-
telegram_cfg.validate()
|
|
1084
|
-
except TelegramBotConfigError as exc:
|
|
1085
|
-
_raise_exit(str(exc), cause=exc)
|
|
1086
|
-
logger = setup_rotating_logger("codex-autorunner-telegram", config.log)
|
|
1087
|
-
log_event(
|
|
1088
|
-
logger,
|
|
1089
|
-
logging.INFO,
|
|
1090
|
-
"telegram.bot.starting",
|
|
1091
|
-
root=str(config.root),
|
|
1092
|
-
mode="hub",
|
|
1093
|
-
)
|
|
1094
|
-
voice_raw = config.repo_defaults.get("voice") if config.repo_defaults else None
|
|
1095
|
-
voice_config = VoiceConfig.from_raw(voice_raw, env=os.environ)
|
|
1096
|
-
update_repo_url = config.update_repo_url
|
|
1097
|
-
update_repo_ref = config.update_repo_ref
|
|
1098
|
-
|
|
1099
|
-
async def _run() -> None:
|
|
1100
|
-
service = TelegramBotService(
|
|
1101
|
-
telegram_cfg,
|
|
1102
|
-
logger=logger,
|
|
1103
|
-
hub_root=config.root,
|
|
1104
|
-
manifest_path=config.manifest_path,
|
|
1105
|
-
voice_config=voice_config,
|
|
1106
|
-
housekeeping_config=config.housekeeping,
|
|
1107
|
-
update_repo_url=update_repo_url,
|
|
1108
|
-
update_repo_ref=update_repo_ref,
|
|
1109
|
-
)
|
|
1110
|
-
await service.run_polling()
|
|
1111
|
-
|
|
1112
|
-
try:
|
|
1113
|
-
asyncio.run(_run())
|
|
1114
|
-
except TelegramBotLockError as exc:
|
|
1115
|
-
_raise_exit(str(exc), cause=exc)
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
@telegram_app.command("health")
|
|
1119
|
-
def telegram_health(
|
|
1120
|
-
path: Optional[Path] = typer.Option(None, "--path", help="Repo or hub root path"),
|
|
1121
|
-
timeout: float = typer.Option(5.0, "--timeout", help="Timeout (seconds)"),
|
|
1122
|
-
):
|
|
1123
|
-
"""Check Telegram API connectivity for the configured bot."""
|
|
1124
|
-
_require_optional_feature(
|
|
1125
|
-
feature="telegram",
|
|
1126
|
-
deps=[("httpx", "httpx")],
|
|
1127
|
-
extra="telegram",
|
|
1128
|
-
)
|
|
1129
|
-
try:
|
|
1130
|
-
config = load_hub_config(path or Path.cwd())
|
|
1131
|
-
except ConfigError as exc:
|
|
1132
|
-
_raise_exit(str(exc), cause=exc)
|
|
1133
|
-
telegram_cfg = TelegramBotConfig.from_raw(
|
|
1134
|
-
config.raw.get("telegram_bot") if isinstance(config.raw, dict) else None,
|
|
1135
|
-
root=config.root,
|
|
1136
|
-
agent_binaries=getattr(config, "agents", None)
|
|
1137
|
-
and {name: agent.binary for name, agent in config.agents.items()},
|
|
1138
|
-
)
|
|
1139
|
-
if not telegram_cfg.enabled:
|
|
1140
|
-
_raise_exit("telegram_bot is disabled; set telegram_bot.enabled: true")
|
|
1141
|
-
bot_token = telegram_cfg.bot_token
|
|
1142
|
-
if not bot_token:
|
|
1143
|
-
_raise_exit(f"missing bot token env '{telegram_cfg.bot_token_env}'")
|
|
1144
|
-
timeout_seconds = max(float(timeout), 0.1)
|
|
1145
|
-
|
|
1146
|
-
async def _run() -> None:
|
|
1147
|
-
async with TelegramBotClient(bot_token) as client:
|
|
1148
|
-
await asyncio.wait_for(client.get_me(), timeout=timeout_seconds)
|
|
1149
|
-
|
|
1150
|
-
try:
|
|
1151
|
-
asyncio.run(_run())
|
|
1152
|
-
except TelegramAPIError as exc:
|
|
1153
|
-
_raise_exit(f"Telegram health check failed: {exc}", cause=exc)
|
|
1154
|
-
except Exception as exc:
|
|
1155
|
-
_raise_exit(f"Telegram health check failed: {exc}", cause=exc)
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
if __name__ == "__main__":
|
|
1159
|
-
app()
|
|
8
|
+
__all__ = ["app", "main", "_resolve_repo_api_path"]
|