python-codex 0.0.1__py3-none-any.whl → 0.1.1__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 (64) hide show
  1. pycodex/__init__.py +141 -2
  2. pycodex/agent.py +290 -0
  3. pycodex/cli.py +705 -0
  4. pycodex/collaboration.py +21 -0
  5. pycodex/context.py +580 -0
  6. pycodex/doctor.py +360 -0
  7. pycodex/model.py +533 -0
  8. pycodex/portable.py +390 -0
  9. pycodex/portable_server.py +205 -0
  10. pycodex/prompts/collaboration_default.md +11 -0
  11. pycodex/prompts/collaboration_plan.md +128 -0
  12. pycodex/prompts/default_base_instructions.md +275 -0
  13. pycodex/prompts/exec_tools.json +411 -0
  14. pycodex/prompts/models.json +847 -0
  15. pycodex/prompts/permissions/approval_policy/never.md +1 -0
  16. pycodex/prompts/permissions/approval_policy/on_failure.md +1 -0
  17. pycodex/prompts/permissions/approval_policy/on_request.md +57 -0
  18. pycodex/prompts/permissions/approval_policy/on_request_rule_request_permission.md +33 -0
  19. pycodex/prompts/permissions/approval_policy/unless_trusted.md +1 -0
  20. pycodex/prompts/permissions/sandbox_mode/danger_full_access.md +1 -0
  21. pycodex/prompts/permissions/sandbox_mode/read_only.md +1 -0
  22. pycodex/prompts/permissions/sandbox_mode/workspace_write.md +1 -0
  23. pycodex/prompts/subagent_tools.json +163 -0
  24. pycodex/protocol.py +347 -0
  25. pycodex/runtime.py +204 -0
  26. pycodex/runtime_services.py +409 -0
  27. pycodex/tools/__init__.py +58 -0
  28. pycodex/tools/agent_tool_schemas.py +70 -0
  29. pycodex/tools/apply_patch_tool.py +363 -0
  30. pycodex/tools/base_tool.py +168 -0
  31. pycodex/tools/close_agent_tool.py +55 -0
  32. pycodex/tools/code_mode_manager.py +519 -0
  33. pycodex/tools/exec_command_tool.py +96 -0
  34. pycodex/tools/exec_runtime.js +161 -0
  35. pycodex/tools/exec_tool.py +48 -0
  36. pycodex/tools/grep_files_tool.py +150 -0
  37. pycodex/tools/list_dir_tool.py +135 -0
  38. pycodex/tools/read_file_tool.py +217 -0
  39. pycodex/tools/request_permissions_tool.py +95 -0
  40. pycodex/tools/request_user_input_tool.py +167 -0
  41. pycodex/tools/resume_agent_tool.py +56 -0
  42. pycodex/tools/send_input_tool.py +106 -0
  43. pycodex/tools/shell_command_tool.py +107 -0
  44. pycodex/tools/shell_tool.py +112 -0
  45. pycodex/tools/spawn_agent_tool.py +97 -0
  46. pycodex/tools/unified_exec_manager.py +380 -0
  47. pycodex/tools/update_plan_tool.py +79 -0
  48. pycodex/tools/view_image_tool.py +111 -0
  49. pycodex/tools/wait_agent_tool.py +75 -0
  50. pycodex/tools/wait_tool.py +68 -0
  51. pycodex/tools/web_search_tool.py +30 -0
  52. pycodex/tools/write_stdin_tool.py +75 -0
  53. pycodex/utils/__init__.py +40 -0
  54. pycodex/utils/dotenv.py +64 -0
  55. pycodex/utils/get_env.py +218 -0
  56. pycodex/utils/random_ids.py +19 -0
  57. pycodex/utils/visualize.py +978 -0
  58. python_codex-0.1.1.dist-info/METADATA +355 -0
  59. python_codex-0.1.1.dist-info/RECORD +62 -0
  60. python_codex-0.1.1.dist-info/entry_points.txt +2 -0
  61. python_codex-0.1.1.dist-info/licenses/LICENSE +201 -0
  62. python_codex-0.0.1.dist-info/METADATA +0 -30
  63. python_codex-0.0.1.dist-info/RECORD +0 -4
  64. {python_codex-0.0.1.dist-info → python_codex-0.1.1.dist-info}/WHEEL +0 -0
pycodex/cli.py ADDED
@@ -0,0 +1,705 @@
1
+ from __future__ import annotations
2
+
3
+ import atexit
4
+ import argparse
5
+ import asyncio
6
+ import json
7
+ import os
8
+ import shlex
9
+ import sys
10
+ import tempfile
11
+ from dataclasses import asdict, replace
12
+ from pathlib import Path
13
+ from typing import Literal, Sequence
14
+
15
+ from .agent import AgentLoop
16
+ from .collaboration import DEFAULT_COLLABORATION_MODE, CollaborationMode
17
+ from .context import ContextManager
18
+ from .model import ResponsesModelClient, ResponsesProviderConfig
19
+ from .portable import bootstrap_called_home, upload_codex_home
20
+ from .protocol import AgentEvent
21
+ from .runtime import AgentRuntime
22
+ from .runtime_services import RuntimeEnvironment, create_runtime_environment
23
+ from .utils import CliSessionView, load_codex_dotenv
24
+ from responses_server import launch_chat_completion_compat_server
25
+
26
+ EXIT_COMMANDS = {"/exit", "/quit"}
27
+ HISTORY_COMMAND = "/history"
28
+ TITLE_COMMAND = "/title"
29
+ MODEL_COMMAND = "/model"
30
+ QUEUE_COMMAND = "/queue"
31
+ CliSessionMode = Literal["exec", "tui"]
32
+ LOCAL_RESPONSES_SERVER_API_KEY_ENV = "PYCODEX_LOCAL_RESPONSES_SERVER_KEY"
33
+ CLI_ORIGINATOR = "codex-tui"
34
+
35
+
36
+ def configure_loguru() -> None:
37
+ try:
38
+ from loguru import logger
39
+ except ImportError: # pragma: no cover - dependency may be absent in minimal envs
40
+ return
41
+
42
+ logger.remove()
43
+ log_path = os.environ.get("PYCODEX_DEBUG_LOG", "").strip()
44
+ if log_path:
45
+ logger.add(log_path, level="DEBUG")
46
+ return
47
+
48
+ if os.environ.get("PYCODEX_DEBUG_STDERR", "").strip().lower() in {
49
+ "1",
50
+ "true",
51
+ "yes",
52
+ "on",
53
+ }:
54
+ logger.add(sys.stderr, level="DEBUG")
55
+
56
+
57
+ def build_parser() -> argparse.ArgumentParser:
58
+ parser = argparse.ArgumentParser(
59
+ prog="pycodex",
60
+ description="Minimal Codex-style local CLI backed by ~/.codex/config.toml.",
61
+ )
62
+ parser.add_argument(
63
+ "prompt", nargs="*", help="Prompt text. If omitted, read from stdin."
64
+ )
65
+ parser.add_argument(
66
+ "--put",
67
+ default=None,
68
+ metavar="PATH@SERVER",
69
+ help=(
70
+ "Upload a Codex home using `--put @host:port` or "
71
+ "`--put /path/.codex@host:port`."
72
+ ),
73
+ )
74
+ parser.add_argument(
75
+ "--call",
76
+ default=None,
77
+ help=(
78
+ "Download and use a stored Codex home via <secret>-<call_id>@<host:port>."
79
+ ),
80
+ )
81
+ parser.add_argument(
82
+ "--config",
83
+ default=str(Path.home() / ".codex" / "config.toml"),
84
+ help="Path to Codex config.toml.",
85
+ )
86
+ parser.add_argument(
87
+ "--profile",
88
+ default=None,
89
+ help="Optional profile name from config.toml.",
90
+ )
91
+ parser.add_argument(
92
+ "--vllm-endpoint",
93
+ default=None,
94
+ help=(
95
+ "Optional base URL for a chat-completions-backed vLLM server. "
96
+ "When set, pycodex starts a local responses compat server for this "
97
+ "session and appends /v1 if the path is empty."
98
+ ),
99
+ )
100
+ parser.add_argument(
101
+ "--use-chat-completion",
102
+ default=False,
103
+ action="store_true",
104
+ help=(
105
+ "When set, pycodex starts a local responses compat server for this session."
106
+ ),
107
+ )
108
+ parser.add_argument(
109
+ "--system-prompt",
110
+ default=None,
111
+ help="Optional base instructions override passed to the model.",
112
+ )
113
+ parser.add_argument(
114
+ "--timeout-seconds",
115
+ type=float,
116
+ default=120.0,
117
+ help="HTTP timeout for one model call.",
118
+ )
119
+ parser.add_argument(
120
+ "--json",
121
+ action="store_true",
122
+ help="Print the full TurnResult as JSON.",
123
+ )
124
+ return parser
125
+
126
+
127
+ def should_run_interactive(prompt_parts: Sequence[str], stdin_is_tty: bool) -> bool:
128
+ return not prompt_parts and stdin_is_tty
129
+
130
+
131
+ def resolve_prompt_text(prompt_parts: Sequence[str]) -> str:
132
+ if prompt_parts:
133
+ return " ".join(prompt_parts).strip()
134
+
135
+ if not sys.stdin.isatty():
136
+ prompt_text = sys.stdin.read().strip()
137
+ if prompt_text:
138
+ return prompt_text
139
+
140
+ raise ValueError("prompt is required either as argv text or stdin")
141
+
142
+
143
+ def get_tools(
144
+ runtime_environment: RuntimeEnvironment | None = None,
145
+ exec_mode: bool = False,
146
+ ):
147
+ from .tools import (
148
+ ApplyPatchTool,
149
+ CloseAgentTool,
150
+ CodeModeManager,
151
+ ExecTool,
152
+ ExecCommandTool,
153
+ GrepFilesTool,
154
+ ListDirTool,
155
+ ReadFileTool,
156
+ RequestPermissionsTool,
157
+ RequestUserInputTool,
158
+ ResumeAgentTool,
159
+ Registry,
160
+ SendInputTool,
161
+ ShellCommandTool,
162
+ ShellTool,
163
+ SpawnAgentTool,
164
+ UnifiedExecManager,
165
+ UpdatePlanTool,
166
+ ViewImageTool,
167
+ WaitAgentTool,
168
+ WaitTool,
169
+ WebSearchTool,
170
+ WriteStdinTool,
171
+ )
172
+
173
+ runtime_environment = runtime_environment or create_runtime_environment()
174
+ registry = Registry()
175
+ code_mode_manager = CodeModeManager(registry)
176
+ unified_exec_manager = UnifiedExecManager()
177
+ exec_tool = ExecTool(code_mode_manager)
178
+ wait_tool = WaitTool(code_mode_manager)
179
+ web_search_tool = WebSearchTool()
180
+ update_plan_tool = UpdatePlanTool(runtime_environment.plan_store)
181
+ request_user_input_tool = RequestUserInputTool(
182
+ runtime_environment.request_user_input_manager
183
+ )
184
+ request_permissions_tool = RequestPermissionsTool(
185
+ runtime_environment.request_permissions_manager
186
+ )
187
+ spawn_agent_tool = SpawnAgentTool(runtime_environment.subagent_manager)
188
+ send_input_tool = SendInputTool(runtime_environment.subagent_manager)
189
+ resume_agent_tool = ResumeAgentTool(runtime_environment.subagent_manager)
190
+ wait_agent_tool = WaitAgentTool(runtime_environment.subagent_manager)
191
+ close_agent_tool = CloseAgentTool(runtime_environment.subagent_manager)
192
+ apply_patch_tool = ApplyPatchTool()
193
+ shell_tool = ShellTool()
194
+ shell_command_tool = ShellCommandTool()
195
+ exec_command_tool = ExecCommandTool(unified_exec_manager)
196
+ write_stdin_tool = WriteStdinTool(unified_exec_manager)
197
+ grep_files_tool = GrepFilesTool()
198
+ read_file_tool = ReadFileTool()
199
+ list_dir_tool = ListDirTool()
200
+ view_image_tool = ViewImageTool()
201
+ if exec_mode:
202
+ registry.register(exec_command_tool)
203
+ registry.register(write_stdin_tool)
204
+ registry.register(update_plan_tool)
205
+ registry.register(request_user_input_tool)
206
+ registry.register(apply_patch_tool)
207
+ registry.register(web_search_tool)
208
+ registry.register(view_image_tool)
209
+ registry.register(spawn_agent_tool)
210
+ registry.register(send_input_tool)
211
+ registry.register(resume_agent_tool)
212
+ registry.register(wait_agent_tool)
213
+ registry.register(close_agent_tool)
214
+ return registry
215
+
216
+ registry.register(shell_tool)
217
+ registry.register(shell_command_tool)
218
+ registry.register(exec_command_tool)
219
+ registry.register(write_stdin_tool)
220
+ registry.register(exec_tool)
221
+ registry.register(wait_tool)
222
+ registry.register(web_search_tool)
223
+ registry.register(update_plan_tool)
224
+ registry.register(request_user_input_tool)
225
+ registry.register(request_permissions_tool)
226
+ registry.register(spawn_agent_tool)
227
+ registry.register(send_input_tool)
228
+ registry.register(resume_agent_tool)
229
+ registry.register(wait_agent_tool)
230
+ registry.register(close_agent_tool)
231
+ registry.register(apply_patch_tool)
232
+ registry.register(grep_files_tool)
233
+ registry.register(read_file_tool)
234
+ registry.register(list_dir_tool)
235
+ registry.register(view_image_tool)
236
+ return registry
237
+
238
+
239
+ def get_subagent_tools(runtime_environment: RuntimeEnvironment | None = None):
240
+ from .tools import (
241
+ ApplyPatchTool,
242
+ ExecCommandTool,
243
+ Registry,
244
+ UnifiedExecManager,
245
+ UpdatePlanTool,
246
+ ViewImageTool,
247
+ WebSearchTool,
248
+ WriteStdinTool,
249
+ )
250
+
251
+ runtime_environment = runtime_environment or create_runtime_environment()
252
+ registry = Registry()
253
+ unified_exec_manager = UnifiedExecManager()
254
+ registry.register(ExecCommandTool(unified_exec_manager))
255
+ registry.register(WriteStdinTool(unified_exec_manager))
256
+ registry.register(UpdatePlanTool(runtime_environment.plan_store))
257
+ registry.register(ApplyPatchTool())
258
+ registry.register(WebSearchTool())
259
+ registry.register(ViewImageTool())
260
+ return registry
261
+
262
+
263
+ def build_runtime(
264
+ config_path: str,
265
+ profile: str | None,
266
+ system_prompt: str | None,
267
+ client,
268
+ session_mode: CliSessionMode = "exec",
269
+ collaboration_mode: CollaborationMode = DEFAULT_COLLABORATION_MODE,
270
+ ) -> AgentRuntime:
271
+ use_tui_context = session_mode == "tui"
272
+ context_manager = ContextManager.from_codex_config(
273
+ config_path,
274
+ profile,
275
+ base_instructions_override=system_prompt,
276
+ collaboration_mode=collaboration_mode,
277
+ include_collaboration_instructions=use_tui_context,
278
+ )
279
+ subagent_context_manager = ContextManager.from_codex_config(
280
+ config_path,
281
+ profile,
282
+ base_instructions_override=system_prompt,
283
+ include_collaboration_instructions=False,
284
+ )
285
+ runtime_environment = create_runtime_environment()
286
+ runtime_environment.request_user_input_manager.set_handler(None)
287
+ runtime_environment.request_permissions_manager.set_handler(None)
288
+
289
+ def make_subagent_runtime_builder(base_client):
290
+ def build_subagent_runtime(
291
+ model_override: str | None,
292
+ reasoning_effort_override: str | None,
293
+ initial_history=(),
294
+ session_id: str | None = None,
295
+ ) -> AgentRuntime:
296
+ nested_client = base_client.with_overrides(
297
+ model_override,
298
+ reasoning_effort_override,
299
+ session_id=session_id,
300
+ openai_subagent="collab_spawn",
301
+ )
302
+ subagent_runtime_environment = create_runtime_environment()
303
+ subagent_runtime_environment.request_user_input_manager.set_handler(None)
304
+ subagent_runtime_environment.request_permissions_manager.set_handler(None)
305
+ subagent_runtime_environment.subagent_manager.set_runtime_builder(
306
+ make_subagent_runtime_builder(nested_client)
307
+ )
308
+ sub_agent = AgentLoop(
309
+ nested_client,
310
+ get_subagent_tools(subagent_runtime_environment),
311
+ subagent_context_manager,
312
+ initial_history=tuple(initial_history),
313
+ )
314
+ return AgentRuntime(
315
+ sub_agent, runtime_environment=subagent_runtime_environment
316
+ )
317
+
318
+ return build_subagent_runtime
319
+
320
+ runtime_environment.subagent_manager.set_runtime_builder(
321
+ make_subagent_runtime_builder(client)
322
+ )
323
+ return AgentRuntime(
324
+ AgentLoop(
325
+ client, get_tools(runtime_environment, exec_mode=True), context_manager
326
+ ),
327
+ runtime_environment=runtime_environment,
328
+ )
329
+
330
+
331
+ def format_turn_output(result, json_mode: bool) -> str:
332
+ if json_mode:
333
+ return json.dumps(asdict(result), ensure_ascii=False, indent=2)
334
+ return result.output_text or ""
335
+
336
+
337
+ def _build_model_client(
338
+ config_path: str,
339
+ profile: str | None,
340
+ timeout_seconds: float,
341
+ managed_responses_base_url: str | None = None,
342
+ vllm_endpoint: str | None = None,
343
+ use_chat_completion: bool = False,
344
+ ):
345
+ load_codex_dotenv(config_path)
346
+ provider_config = ResponsesProviderConfig.from_codex_config(
347
+ config_path,
348
+ profile,
349
+ )
350
+ url, key_env = provider_config.base_url, provider_config.api_key_env
351
+ if managed_responses_base_url is not None:
352
+ url, key_env = (
353
+ managed_responses_base_url,
354
+ LOCAL_RESPONSES_SERVER_API_KEY_ENV,
355
+ )
356
+ os.environ.setdefault(LOCAL_RESPONSES_SERVER_API_KEY_ENV, "dummy")
357
+ elif vllm_endpoint or use_chat_completion:
358
+ if vllm_endpoint:
359
+ managed_server = launch_chat_completion_compat_server(
360
+ vllm_endpoint,
361
+ model_provider="vllm",
362
+ )
363
+ else:
364
+ managed_server = launch_chat_completion_compat_server(
365
+ provider_config.base_url,
366
+ provider_config.api_key_env,
367
+ model_provider=provider_config.provider_name,
368
+ )
369
+ atexit.register(managed_server.stop)
370
+ url, key_env = (
371
+ managed_server.base_url,
372
+ LOCAL_RESPONSES_SERVER_API_KEY_ENV,
373
+ )
374
+ os.environ.setdefault(LOCAL_RESPONSES_SERVER_API_KEY_ENV, "dummy")
375
+
376
+ provider_config = replace(
377
+ provider_config,
378
+ base_url=url,
379
+ api_key_env=key_env,
380
+ )
381
+ return ResponsesModelClient(
382
+ provider_config,
383
+ timeout_seconds,
384
+ originator=CLI_ORIGINATOR,
385
+ )
386
+
387
+
388
+ async def prompt_request_user_input(
389
+ view: CliSessionView,
390
+ payload: dict[str, object],
391
+ ) -> dict[str, object] | None:
392
+ view.finish_stream()
393
+ view.pause_spinner()
394
+ view.write_line("[request_user_input] waiting for user response")
395
+ answers: dict[str, dict[str, list[str]]] = {}
396
+ try:
397
+ for question in payload.get("questions", []):
398
+ if not isinstance(question, dict):
399
+ continue
400
+ header = str(question.get("header", "")).strip()
401
+ question_text = str(question.get("question", "")).strip()
402
+ question_id = str(question.get("id", "")).strip()
403
+ if header:
404
+ view.write_line(f"[{header}] {question_text}")
405
+ else:
406
+ view.write_line(question_text)
407
+
408
+ options = question.get("options") or []
409
+ if isinstance(options, list):
410
+ for index, option in enumerate(options, start=1):
411
+ if not isinstance(option, dict):
412
+ continue
413
+ label = str(option.get("label", "")).strip()
414
+ description = str(option.get("description", "")).strip()
415
+ view.write_line(f" {index}. {label} - {description}")
416
+ view.write_line(" 0. Other")
417
+
418
+ try:
419
+ raw_answer = await view.prompt_async("answer> ")
420
+ except EOFError:
421
+ return None
422
+ answer_text = raw_answer.strip()
423
+ if not answer_text:
424
+ return None
425
+
426
+ selected_answer = answer_text
427
+ if answer_text.isdigit() and isinstance(options, list):
428
+ choice = int(answer_text)
429
+ if 1 <= choice <= len(options):
430
+ option = options[choice - 1]
431
+ if isinstance(option, dict):
432
+ selected_answer = (
433
+ str(option.get("label", "")).strip() or answer_text
434
+ )
435
+ elif choice == 0:
436
+ try:
437
+ raw_answer = await view.prompt_async("other> ")
438
+ except EOFError:
439
+ return None
440
+ selected_answer = raw_answer.strip()
441
+ if not selected_answer:
442
+ return None
443
+
444
+ answers[question_id] = {"answers": [selected_answer]}
445
+
446
+ return {"answers": answers}
447
+ finally:
448
+ view.resume_spinner()
449
+
450
+
451
+ async def prompt_request_permissions(
452
+ view: CliSessionView,
453
+ payload: dict[str, object],
454
+ ) -> dict[str, object] | None:
455
+ view.finish_stream()
456
+ view.pause_spinner()
457
+ view.write_line("[request_permissions] user approval required")
458
+ reason = payload.get("reason")
459
+ if reason:
460
+ view.write_line(f"Reason: {reason}")
461
+ view.write_line("Requested permissions:")
462
+ view.write_line(
463
+ json.dumps(payload.get("permissions", {}), ensure_ascii=False, indent=2)
464
+ )
465
+ view.write_line("Choose: [n] deny / [t] grant for turn / [s] grant for session")
466
+ try:
467
+ raw_answer = await view.prompt_async("permissions> ")
468
+ except EOFError:
469
+ return None
470
+ finally:
471
+ view.resume_spinner()
472
+
473
+ answer = raw_answer.strip().lower()
474
+ if answer in {"t", "turn", "y", "yes"}:
475
+ return {
476
+ "permissions": payload.get("permissions", {}),
477
+ "scope": "turn",
478
+ }
479
+ if answer in {"s", "session"}:
480
+ return {
481
+ "permissions": payload.get("permissions", {}),
482
+ "scope": "session",
483
+ }
484
+ return {
485
+ "permissions": {},
486
+ "scope": "turn",
487
+ }
488
+
489
+
490
+ async def run_interactive_session(
491
+ runtime: AgentRuntime,
492
+ json_mode: bool,
493
+ ) -> int:
494
+ worker = asyncio.create_task(runtime.run_forever())
495
+ view = CliSessionView()
496
+ model_client = runtime._agent_loop._model_client
497
+ runtime.set_event_handler(view.handle_event)
498
+ pending_turn_tasks: set[asyncio.Task[None]] = set()
499
+ runtime_environment = runtime.runtime_environment
500
+ if runtime_environment is None:
501
+ runtime_environment = create_runtime_environment()
502
+ runtime.runtime_environment = runtime_environment
503
+ runtime_environment.request_user_input_manager.set_handler(
504
+ lambda payload: prompt_request_user_input(view, payload)
505
+ )
506
+ runtime_environment.request_permissions_manager.set_handler(
507
+ lambda payload: prompt_request_permissions(view, payload)
508
+ )
509
+ view.write_line("pycodex interactive mode. Type /exit to quit.")
510
+ view.write_line("Extra commands: /history, /title, /model")
511
+ try:
512
+
513
+ def has_pending_turn_tasks() -> bool:
514
+ pending_turn_tasks.difference_update(
515
+ task for task in tuple(pending_turn_tasks) if task.done()
516
+ )
517
+ return bool(pending_turn_tasks)
518
+
519
+ async def wait_for_turn_result(future) -> None:
520
+ try:
521
+ result = await future
522
+ except Exception as exc: # pragma: no cover - defensive surface
523
+ if str(exc) == "submission interrupted":
524
+ return
525
+ view.show_error(str(exc))
526
+ return
527
+
528
+ if json_mode:
529
+ view.write_line(format_turn_output(result, True))
530
+
531
+ while True:
532
+ try:
533
+ raw_line = await view.poll_prompt("pycodex> ")
534
+ except EOFError:
535
+ break
536
+ if raw_line is None:
537
+ await asyncio.sleep(0.05)
538
+ continue
539
+
540
+ prompt_text = raw_line.strip()
541
+ if not prompt_text:
542
+ continue
543
+ if prompt_text in EXIT_COMMANDS:
544
+ break
545
+ if prompt_text == HISTORY_COMMAND:
546
+ view.show_history()
547
+ continue
548
+ if prompt_text == TITLE_COMMAND:
549
+ view.show_title()
550
+ continue
551
+ if prompt_text.startswith(f"{QUEUE_COMMAND} "):
552
+ queued_text = prompt_text[len(QUEUE_COMMAND) :].strip()
553
+ if not queued_text:
554
+ view.write_line("Usage: /queue <message>")
555
+ continue
556
+ try:
557
+ submission_id, future = await runtime.enqueue_user_turn(
558
+ queued_text, queue="enqueue"
559
+ )
560
+ view.show_steer_queued(submission_id, queued_text)
561
+ turn_task = asyncio.create_task(wait_for_turn_result(future))
562
+ pending_turn_tasks.add(turn_task)
563
+ except Exception as exc: # pragma: no cover - defensive surface
564
+ view.show_error(str(exc))
565
+ continue
566
+ if prompt_text == MODEL_COMMAND:
567
+ view.write_line(
568
+ f"Current model: {getattr(model_client, 'model', None) or 'unavailable'}"
569
+ )
570
+ models = await model_client.list_models()
571
+ view.write_line(f"Available models: {', '.join(models)}")
572
+ continue
573
+ if prompt_text.startswith(f"{MODEL_COMMAND} "):
574
+ if has_pending_turn_tasks():
575
+ view.write_line(
576
+ "Cannot change model while work is running or queued in steer mode."
577
+ )
578
+ continue
579
+ model_name = prompt_text[len(MODEL_COMMAND) :].strip()
580
+ if not model_name:
581
+ view.write_line("Usage: /model <model>")
582
+ continue
583
+
584
+ model_client.model = model_name
585
+ view.write_line(f"Switched model to {model_name}.")
586
+ continue
587
+
588
+ try:
589
+ steered = has_pending_turn_tasks()
590
+ submission_id, future = await runtime.enqueue_user_turn(
591
+ prompt_text,
592
+ queue="steer",
593
+ )
594
+ if steered:
595
+ view.schedule_steer_inserted(submission_id, prompt_text)
596
+ turn_task = asyncio.create_task(wait_for_turn_result(future))
597
+ pending_turn_tasks.add(turn_task)
598
+ continue
599
+ except Exception as exc: # pragma: no cover - defensive surface
600
+ view.show_error(str(exc))
601
+ continue
602
+ finally:
603
+ runtime_environment.request_user_input_manager.set_handler(None)
604
+ runtime_environment.request_permissions_manager.set_handler(None)
605
+ await runtime.shutdown()
606
+ await worker
607
+ if pending_turn_tasks:
608
+ await asyncio.gather(*pending_turn_tasks, return_exceptions=True)
609
+ view.close()
610
+
611
+ return 0
612
+
613
+
614
+ async def run_cli(args: argparse.Namespace) -> int:
615
+ runtime = None
616
+ worker = None
617
+ try:
618
+ if args.put is not None and args.call:
619
+ raise ValueError("--put and --call cannot be combined")
620
+ if args.put is not None and args.prompt:
621
+ raise ValueError("--put does not accept prompt text")
622
+ configure_loguru()
623
+ if args.put is not None:
624
+ def emit_put_log(message: str) -> None:
625
+ print(message, flush=True)
626
+
627
+ call_spec = upload_codex_home(args.put, event_handler=emit_put_log)
628
+ emit_put_log(f"[put] testing call: {call_spec}")
629
+ with tempfile.TemporaryDirectory(prefix="pycodex-put-call-test-") as tmpdir:
630
+ config_path = bootstrap_called_home(call_spec, storage_root=tmpdir)
631
+ emit_put_log(f"[put] call test ok: {config_path.name}")
632
+ print("[put] one-click start:", flush=True)
633
+ print(f"pycodex --call {shlex.quote(call_spec)}", flush=True)
634
+ return 0
635
+ if args.call:
636
+ config_path = bootstrap_called_home(args.call)
637
+ args.config = str(config_path)
638
+ os.environ["CODEX_HOME"] = str(config_path.parent)
639
+ client = _build_model_client(
640
+ args.config,
641
+ args.profile,
642
+ args.timeout_seconds,
643
+ vllm_endpoint=args.vllm_endpoint,
644
+ use_chat_completion=args.use_chat_completion,
645
+ )
646
+
647
+ runtime = build_runtime(
648
+ args.config,
649
+ args.profile,
650
+ args.system_prompt,
651
+ client,
652
+ session_mode="tui",
653
+ )
654
+ if should_run_interactive(args.prompt, sys.stdin.isatty()):
655
+ return await run_interactive_session(
656
+ runtime,
657
+ args.json,
658
+ )
659
+ else:
660
+ prompt_text = resolve_prompt_text(args.prompt)
661
+ worker = asyncio.create_task(runtime.run_forever())
662
+ result = await runtime.submit_user_turn(prompt_text)
663
+ print(format_turn_output(result, args.json))
664
+ return 0
665
+ except Exception as exc:
666
+ print(f"Error: {exc}", file=sys.stderr)
667
+ return 1
668
+ finally:
669
+ if runtime is not None and worker is not None:
670
+ await runtime.shutdown()
671
+ await worker
672
+
673
+
674
+ def main(argv: Sequence[str] | None = None) -> int:
675
+ raw_args = list(argv) if argv is not None else None
676
+ if raw_args is None:
677
+ raw_args = sys.argv[1:]
678
+
679
+ if raw_args and raw_args[0] == "doctor":
680
+ from .doctor import build_doctor_parser, run_doctor_cli
681
+
682
+ parser = build_doctor_parser()
683
+ args = parser.parse_args(raw_args[1:])
684
+ try:
685
+ return asyncio.run(run_doctor_cli(args))
686
+ except ValueError as exc:
687
+ parser.error(str(exc))
688
+ except KeyboardInterrupt:
689
+ return 130
690
+ return 0
691
+
692
+ parser = build_parser()
693
+ args = parser.parse_args(raw_args)
694
+
695
+ try:
696
+ return asyncio.run(run_cli(args))
697
+ except ValueError as exc:
698
+ parser.error(str(exc))
699
+ except KeyboardInterrupt:
700
+ return 130
701
+ return 0
702
+
703
+
704
+ if __name__ == "__main__":
705
+ raise SystemExit(main())