codex-autorunner 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. codex_autorunner/__init__.py +3 -0
  2. codex_autorunner/bootstrap.py +151 -0
  3. codex_autorunner/cli.py +886 -0
  4. codex_autorunner/codex_cli.py +79 -0
  5. codex_autorunner/codex_runner.py +17 -0
  6. codex_autorunner/core/__init__.py +1 -0
  7. codex_autorunner/core/about_car.py +125 -0
  8. codex_autorunner/core/codex_runner.py +100 -0
  9. codex_autorunner/core/config.py +1465 -0
  10. codex_autorunner/core/doc_chat.py +547 -0
  11. codex_autorunner/core/docs.py +37 -0
  12. codex_autorunner/core/engine.py +720 -0
  13. codex_autorunner/core/git_utils.py +206 -0
  14. codex_autorunner/core/hub.py +756 -0
  15. codex_autorunner/core/injected_context.py +9 -0
  16. codex_autorunner/core/locks.py +57 -0
  17. codex_autorunner/core/logging_utils.py +158 -0
  18. codex_autorunner/core/notifications.py +465 -0
  19. codex_autorunner/core/optional_dependencies.py +41 -0
  20. codex_autorunner/core/prompt.py +107 -0
  21. codex_autorunner/core/prompts.py +275 -0
  22. codex_autorunner/core/request_context.py +21 -0
  23. codex_autorunner/core/runner_controller.py +116 -0
  24. codex_autorunner/core/runner_process.py +29 -0
  25. codex_autorunner/core/snapshot.py +576 -0
  26. codex_autorunner/core/state.py +156 -0
  27. codex_autorunner/core/update.py +567 -0
  28. codex_autorunner/core/update_runner.py +44 -0
  29. codex_autorunner/core/usage.py +1221 -0
  30. codex_autorunner/core/utils.py +108 -0
  31. codex_autorunner/discovery.py +102 -0
  32. codex_autorunner/housekeeping.py +423 -0
  33. codex_autorunner/integrations/__init__.py +1 -0
  34. codex_autorunner/integrations/app_server/__init__.py +6 -0
  35. codex_autorunner/integrations/app_server/client.py +1386 -0
  36. codex_autorunner/integrations/app_server/supervisor.py +206 -0
  37. codex_autorunner/integrations/github/__init__.py +10 -0
  38. codex_autorunner/integrations/github/service.py +889 -0
  39. codex_autorunner/integrations/telegram/__init__.py +1 -0
  40. codex_autorunner/integrations/telegram/adapter.py +1401 -0
  41. codex_autorunner/integrations/telegram/commands_registry.py +104 -0
  42. codex_autorunner/integrations/telegram/config.py +450 -0
  43. codex_autorunner/integrations/telegram/constants.py +154 -0
  44. codex_autorunner/integrations/telegram/dispatch.py +162 -0
  45. codex_autorunner/integrations/telegram/handlers/__init__.py +0 -0
  46. codex_autorunner/integrations/telegram/handlers/approvals.py +241 -0
  47. codex_autorunner/integrations/telegram/handlers/callbacks.py +72 -0
  48. codex_autorunner/integrations/telegram/handlers/commands.py +160 -0
  49. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +5262 -0
  50. codex_autorunner/integrations/telegram/handlers/messages.py +477 -0
  51. codex_autorunner/integrations/telegram/handlers/selections.py +545 -0
  52. codex_autorunner/integrations/telegram/helpers.py +2084 -0
  53. codex_autorunner/integrations/telegram/notifications.py +164 -0
  54. codex_autorunner/integrations/telegram/outbox.py +174 -0
  55. codex_autorunner/integrations/telegram/rendering.py +102 -0
  56. codex_autorunner/integrations/telegram/retry.py +37 -0
  57. codex_autorunner/integrations/telegram/runtime.py +270 -0
  58. codex_autorunner/integrations/telegram/service.py +921 -0
  59. codex_autorunner/integrations/telegram/state.py +1223 -0
  60. codex_autorunner/integrations/telegram/transport.py +318 -0
  61. codex_autorunner/integrations/telegram/types.py +57 -0
  62. codex_autorunner/integrations/telegram/voice.py +413 -0
  63. codex_autorunner/manifest.py +150 -0
  64. codex_autorunner/routes/__init__.py +53 -0
  65. codex_autorunner/routes/base.py +470 -0
  66. codex_autorunner/routes/docs.py +275 -0
  67. codex_autorunner/routes/github.py +197 -0
  68. codex_autorunner/routes/repos.py +121 -0
  69. codex_autorunner/routes/sessions.py +137 -0
  70. codex_autorunner/routes/shared.py +137 -0
  71. codex_autorunner/routes/system.py +175 -0
  72. codex_autorunner/routes/terminal_images.py +107 -0
  73. codex_autorunner/routes/voice.py +128 -0
  74. codex_autorunner/server.py +23 -0
  75. codex_autorunner/spec_ingest.py +113 -0
  76. codex_autorunner/static/app.js +95 -0
  77. codex_autorunner/static/autoRefresh.js +209 -0
  78. codex_autorunner/static/bootstrap.js +105 -0
  79. codex_autorunner/static/bus.js +23 -0
  80. codex_autorunner/static/cache.js +52 -0
  81. codex_autorunner/static/constants.js +48 -0
  82. codex_autorunner/static/dashboard.js +795 -0
  83. codex_autorunner/static/docs.js +1514 -0
  84. codex_autorunner/static/env.js +99 -0
  85. codex_autorunner/static/github.js +168 -0
  86. codex_autorunner/static/hub.js +1511 -0
  87. codex_autorunner/static/index.html +622 -0
  88. codex_autorunner/static/loader.js +28 -0
  89. codex_autorunner/static/logs.js +690 -0
  90. codex_autorunner/static/mobileCompact.js +300 -0
  91. codex_autorunner/static/snapshot.js +116 -0
  92. codex_autorunner/static/state.js +87 -0
  93. codex_autorunner/static/styles.css +4966 -0
  94. codex_autorunner/static/tabs.js +50 -0
  95. codex_autorunner/static/terminal.js +21 -0
  96. codex_autorunner/static/terminalManager.js +3535 -0
  97. codex_autorunner/static/todoPreview.js +25 -0
  98. codex_autorunner/static/types.d.ts +8 -0
  99. codex_autorunner/static/utils.js +597 -0
  100. codex_autorunner/static/vendor/LICENSE.xterm +24 -0
  101. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-cyrillic-ext.woff2 +0 -0
  102. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-cyrillic.woff2 +0 -0
  103. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-greek.woff2 +0 -0
  104. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-latin-ext.woff2 +0 -0
  105. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-latin.woff2 +0 -0
  106. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-vietnamese.woff2 +0 -0
  107. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-cyrillic-ext.woff2 +0 -0
  108. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-cyrillic.woff2 +0 -0
  109. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-greek.woff2 +0 -0
  110. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-latin-ext.woff2 +0 -0
  111. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-latin.woff2 +0 -0
  112. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-vietnamese.woff2 +0 -0
  113. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-cyrillic-ext.woff2 +0 -0
  114. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-cyrillic.woff2 +0 -0
  115. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-greek.woff2 +0 -0
  116. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-latin-ext.woff2 +0 -0
  117. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-latin.woff2 +0 -0
  118. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-vietnamese.woff2 +0 -0
  119. codex_autorunner/static/vendor/fonts/jetbrains-mono/OFL.txt +93 -0
  120. codex_autorunner/static/vendor/xterm-addon-fit.js +2 -0
  121. codex_autorunner/static/vendor/xterm.css +209 -0
  122. codex_autorunner/static/vendor/xterm.js +2 -0
  123. codex_autorunner/static/voice.js +591 -0
  124. codex_autorunner/voice/__init__.py +39 -0
  125. codex_autorunner/voice/capture.py +349 -0
  126. codex_autorunner/voice/config.py +167 -0
  127. codex_autorunner/voice/provider.py +66 -0
  128. codex_autorunner/voice/providers/__init__.py +7 -0
  129. codex_autorunner/voice/providers/openai_whisper.py +345 -0
  130. codex_autorunner/voice/resolver.py +36 -0
  131. codex_autorunner/voice/service.py +210 -0
  132. codex_autorunner/web/__init__.py +1 -0
  133. codex_autorunner/web/app.py +1037 -0
  134. codex_autorunner/web/hub_jobs.py +181 -0
  135. codex_autorunner/web/middleware.py +552 -0
  136. codex_autorunner/web/pty_session.py +357 -0
  137. codex_autorunner/web/runner_manager.py +25 -0
  138. codex_autorunner/web/schemas.py +253 -0
  139. codex_autorunner/web/static_assets.py +430 -0
  140. codex_autorunner/web/terminal_sessions.py +78 -0
  141. codex_autorunner/workspace.py +16 -0
  142. codex_autorunner-0.1.0.dist-info/METADATA +240 -0
  143. codex_autorunner-0.1.0.dist-info/RECORD +147 -0
  144. codex_autorunner-0.1.0.dist-info/WHEEL +5 -0
  145. codex_autorunner-0.1.0.dist-info/entry_points.txt +3 -0
  146. codex_autorunner-0.1.0.dist-info/licenses/LICENSE +21 -0
  147. codex_autorunner-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,79 @@
1
+ import subprocess
2
+ from functools import lru_cache
3
+ from typing import Iterable, Optional
4
+
5
+ SUBCOMMAND_HINTS = ("exec", "resume")
6
+
7
+
8
+ def extract_flag_value(args: Iterable[str], flag: str) -> Optional[str]:
9
+ if not args:
10
+ return None
11
+ for arg in args:
12
+ if not isinstance(arg, str):
13
+ continue
14
+ if arg.startswith(f"{flag}="):
15
+ return arg.split("=", 1)[1] or None
16
+ args_list = [str(a) for a in args]
17
+ for idx, arg in enumerate(args_list):
18
+ if arg == flag and idx + 1 < len(args_list):
19
+ return args_list[idx + 1]
20
+ return None
21
+
22
+
23
+ def inject_flag(
24
+ args: Iterable[str],
25
+ flag: str,
26
+ value: Optional[str],
27
+ *,
28
+ subcommands: Iterable[str] = SUBCOMMAND_HINTS,
29
+ ) -> list[str]:
30
+ if not value:
31
+ return [str(a) for a in args]
32
+ args_list = [str(a) for a in args]
33
+ if extract_flag_value(args_list, flag):
34
+ return args_list
35
+ insert_at = None
36
+ for cmd in subcommands:
37
+ try:
38
+ insert_at = args_list.index(cmd)
39
+ break
40
+ except ValueError:
41
+ continue
42
+ if insert_at is None:
43
+ return [flag, value] + args_list
44
+ return args_list[:insert_at] + [flag, value] + args_list[insert_at:]
45
+
46
+
47
+ def apply_codex_options(
48
+ args: Iterable[str],
49
+ *,
50
+ model: Optional[str] = None,
51
+ reasoning: Optional[str] = None,
52
+ supports_reasoning: Optional[bool] = None,
53
+ ) -> list[str]:
54
+ with_model = inject_flag(args, "--model", model)
55
+ if reasoning and supports_reasoning is False:
56
+ return with_model
57
+ return inject_flag(with_model, "--reasoning", reasoning)
58
+
59
+
60
+ def _read_help_text(binary: str) -> str:
61
+ try:
62
+ result = subprocess.run(
63
+ [binary, "--help"],
64
+ capture_output=True,
65
+ text=True,
66
+ check=False,
67
+ )
68
+ except FileNotFoundError:
69
+ return ""
70
+ return "\n".join(filter(None, [result.stdout, result.stderr]))
71
+
72
+
73
+ @lru_cache(maxsize=8)
74
+ def supports_flag(binary: str, flag: str) -> bool:
75
+ return flag in _read_help_text(binary)
76
+
77
+
78
+ def supports_reasoning(binary: str) -> bool:
79
+ return supports_flag(binary, "--reasoning")
@@ -0,0 +1,17 @@
1
+ from .core.codex_runner import (
2
+ CodexRunnerError,
3
+ CodexTimeoutError,
4
+ build_codex_command,
5
+ resolve_codex_binary,
6
+ run_codex_capture_async,
7
+ run_codex_streaming,
8
+ )
9
+
10
+ __all__ = [
11
+ "CodexRunnerError",
12
+ "CodexTimeoutError",
13
+ "build_codex_command",
14
+ "resolve_codex_binary",
15
+ "run_codex_capture_async",
16
+ "run_codex_streaming",
17
+ ]
@@ -0,0 +1 @@
1
+ """Core runtime primitives."""
@@ -0,0 +1,125 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Mapping, Optional
5
+
6
+ from .config import ROOT_CONFIG_FILENAME, ROOT_OVERRIDE_FILENAME, Config
7
+
8
+ ABOUT_CAR_BASENAME = "ABOUT_CAR.md"
9
+ ABOUT_CAR_REL_PATH = Path(".codex-autorunner") / ABOUT_CAR_BASENAME
10
+
11
+ # If this marker is present, codex-autorunner may safely refresh the file content.
12
+ ABOUT_CAR_GENERATED_MARKER = "<!-- CAR:AUTOGENERATED -->"
13
+
14
+
15
+ def _display_path(repo_root: Path, path: Path) -> str:
16
+ try:
17
+ return str(path.relative_to(repo_root))
18
+ except ValueError:
19
+ return str(path)
20
+
21
+
22
+ def build_about_car_markdown(
23
+ *,
24
+ repo_root: Path,
25
+ todo_path: Path,
26
+ progress_path: Path,
27
+ opinions_path: Path,
28
+ spec_path: Path,
29
+ summary_path: Path,
30
+ config_path: Optional[Path] = None,
31
+ ) -> str:
32
+ config_path = config_path or (repo_root / ".codex-autorunner" / "config.yml")
33
+ root_config_path = repo_root / ROOT_CONFIG_FILENAME
34
+ root_override_path = repo_root / ROOT_OVERRIDE_FILENAME
35
+ todo_disp = _display_path(repo_root, todo_path)
36
+ progress_disp = _display_path(repo_root, progress_path)
37
+ opinions_disp = _display_path(repo_root, opinions_path)
38
+ spec_disp = _display_path(repo_root, spec_path)
39
+ summary_disp = _display_path(repo_root, summary_path)
40
+ config_disp = _display_path(repo_root, config_path)
41
+ root_config_disp = _display_path(repo_root, root_config_path)
42
+ root_override_disp = _display_path(repo_root, root_override_path)
43
+
44
+ return (
45
+ f"{ABOUT_CAR_GENERATED_MARKER}\n"
46
+ "# ABOUT_CAR — Codex Autorunner (CAR)\n\n"
47
+ "You are running inside **Codex Autorunner (CAR)**.\n\n"
48
+ "CAR uses a small set of markdown **work docs** as the control surface for long-horizon work. "
49
+ "These docs live under the repo-local, gitignored `.codex-autorunner/` directory.\n\n"
50
+ "## Work docs (canonical)\n"
51
+ "- **TODO** — ordered checklist of high-level tasks: "
52
+ f"`{todo_disp}`\n"
53
+ "- **PROGRESS** — running notes / validation / context: "
54
+ f"`{progress_disp}`\n"
55
+ "- **OPINIONS** — constraints + style guidelines: "
56
+ f"`{opinions_disp}`\n"
57
+ "- **SPEC** — source-of-truth requirements and scope: "
58
+ f"`{spec_disp}`\n"
59
+ "- **SUMMARY** — user-facing report + external/user action items: "
60
+ f"`{summary_disp}`\n\n"
61
+ "## Critical rules\n"
62
+ f'- When the user says **"add this to the TODOs"**, edit `{todo_disp}`.\n'
63
+ "- Do **not** create new copies of TODO/PROGRESS/OPINIONS/SPEC/SUMMARY elsewhere in the repo.\n"
64
+ "- Treat `.codex-autorunner/` as intentional project structure even though it is hidden/gitignored.\n\n"
65
+ "## How CAR works (short)\n"
66
+ "- `car run/once` repeatedly runs Codex non-interactively, feeding it the work docs (and the prior run tail).\n"
67
+ "- `car serve` starts a local web UI. The **Terminal** tab launches the configured `codex` binary in a PTY.\n"
68
+ f"- Repo config lives at `{config_disp}` (doc paths, codex args, server host/port).\n"
69
+ f"- Root defaults live at `{root_config_disp}` with optional `{root_override_disp}` overrides.\n"
70
+ )
71
+
72
+
73
+ def ensure_about_car_file_for_repo(
74
+ repo_root: Path,
75
+ *,
76
+ doc_paths: Mapping[str, Path],
77
+ force: bool = False,
78
+ ) -> Path:
79
+ """
80
+ Ensure `.codex-autorunner/ABOUT_CAR.md` exists for this repo.
81
+
82
+ If the file already exists and contains the generated marker, it may be refreshed
83
+ (unless force=False and content already matches).
84
+ """
85
+ path = repo_root / ABOUT_CAR_REL_PATH
86
+ path.parent.mkdir(parents=True, exist_ok=True)
87
+
88
+ content = build_about_car_markdown(
89
+ repo_root=repo_root,
90
+ todo_path=doc_paths["todo"],
91
+ progress_path=doc_paths["progress"],
92
+ opinions_path=doc_paths["opinions"],
93
+ spec_path=doc_paths["spec"],
94
+ summary_path=doc_paths["summary"],
95
+ config_path=repo_root / ".codex-autorunner" / "config.yml",
96
+ )
97
+ if content and not content.endswith("\n"):
98
+ content += "\n"
99
+
100
+ if path.exists() and not force:
101
+ try:
102
+ existing = path.read_text(encoding="utf-8")
103
+ except OSError:
104
+ existing = ""
105
+ # Only overwrite if it's our generated file (marker) and content changed.
106
+ if ABOUT_CAR_GENERATED_MARKER not in existing:
107
+ return path
108
+ if existing == content:
109
+ return path
110
+
111
+ path.write_text(content, encoding="utf-8")
112
+ return path
113
+
114
+
115
+ def ensure_about_car_file(config: Config, *, force: bool = False) -> Path:
116
+ """Config-aware wrapper that uses configured doc paths."""
117
+ repo_root = config.root
118
+ docs = {
119
+ "todo": config.doc_path("todo"),
120
+ "progress": config.doc_path("progress"),
121
+ "opinions": config.doc_path("opinions"),
122
+ "spec": config.doc_path("spec"),
123
+ "summary": config.doc_path("summary"),
124
+ }
125
+ return ensure_about_car_file_for_repo(repo_root, doc_paths=docs, force=force)
@@ -0,0 +1,100 @@
1
+ import asyncio
2
+ import subprocess
3
+ from pathlib import Path
4
+ from typing import Callable, Optional
5
+
6
+ from ..codex_cli import apply_codex_options, supports_reasoning
7
+ from .config import Config, ConfigError
8
+ from .utils import resolve_executable, subprocess_env
9
+
10
+
11
+ class CodexRunnerError(Exception):
12
+ """Raised when a Codex subprocess fails at the runner boundary."""
13
+
14
+
15
+ class CodexTimeoutError(CodexRunnerError):
16
+ """Raised when a Codex subprocess exceeds the timeout."""
17
+
18
+
19
+ def resolve_codex_binary(config: Config) -> str:
20
+ resolved = resolve_executable(config.codex_binary)
21
+ if not resolved:
22
+ raise ConfigError(f"Codex binary not found: {config.codex_binary}")
23
+ return resolved
24
+
25
+
26
+ def build_codex_command(
27
+ config: Config, prompt: str, *, resolved_binary: Optional[str] = None
28
+ ) -> list[str]:
29
+ binary = resolved_binary or resolve_codex_binary(config)
30
+ reasoning_supported = supports_reasoning(binary)
31
+ args = apply_codex_options(
32
+ config.codex_args,
33
+ model=config.codex_model,
34
+ reasoning=config.codex_reasoning,
35
+ supports_reasoning=reasoning_supported,
36
+ )
37
+ return [binary] + args + [prompt]
38
+
39
+
40
+ def run_codex_streaming(
41
+ config: Config,
42
+ repo_root: Path,
43
+ prompt: str,
44
+ *,
45
+ on_stdout_line: Optional[Callable[[str], None]] = None,
46
+ cmd: Optional[list[str]] = None,
47
+ ) -> int:
48
+ cmd = cmd or build_codex_command(config, prompt)
49
+ try:
50
+ proc = subprocess.Popen(
51
+ cmd,
52
+ cwd=str(repo_root),
53
+ env=subprocess_env(),
54
+ stdout=subprocess.PIPE,
55
+ stderr=subprocess.STDOUT,
56
+ text=True,
57
+ bufsize=1,
58
+ )
59
+ except FileNotFoundError as exc:
60
+ raise ConfigError(f"Codex binary not found: {config.codex_binary}") from exc
61
+
62
+ if proc.stdout:
63
+ for line in proc.stdout:
64
+ if on_stdout_line:
65
+ on_stdout_line(line.rstrip("\n"))
66
+
67
+ return proc.wait()
68
+
69
+
70
+ async def run_codex_capture_async(
71
+ config: Config,
72
+ repo_root: Path,
73
+ prompt: str,
74
+ *,
75
+ timeout_seconds: Optional[int] = None,
76
+ cmd: Optional[list[str]] = None,
77
+ ) -> tuple[int, str]:
78
+ cmd = cmd or build_codex_command(config, prompt)
79
+ try:
80
+ proc = await asyncio.create_subprocess_exec(
81
+ *cmd,
82
+ cwd=str(repo_root),
83
+ stdout=asyncio.subprocess.PIPE,
84
+ stderr=asyncio.subprocess.STDOUT,
85
+ )
86
+ except FileNotFoundError as exc:
87
+ raise ConfigError(f"Codex binary not found: {config.codex_binary}") from exc
88
+
89
+ try:
90
+ stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=timeout_seconds)
91
+ except asyncio.TimeoutError as exc:
92
+ proc.kill()
93
+ await proc.wait()
94
+ raise CodexTimeoutError("Codex process timed out") from exc
95
+
96
+ output = stdout.decode("utf-8", errors="ignore") if stdout else ""
97
+ returncode = proc.returncode
98
+ if returncode is None:
99
+ returncode = await proc.wait()
100
+ return returncode, output