python-codex 0.0.1__py3-none-any.whl → 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 (62) hide show
  1. pycodex/__init__.py +139 -2
  2. pycodex/agent.py +290 -0
  3. pycodex/cli.py +641 -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/prompts/collaboration_default.md +11 -0
  9. pycodex/prompts/collaboration_plan.md +128 -0
  10. pycodex/prompts/default_base_instructions.md +275 -0
  11. pycodex/prompts/exec_tools.json +411 -0
  12. pycodex/prompts/models.json +847 -0
  13. pycodex/prompts/permissions/approval_policy/never.md +1 -0
  14. pycodex/prompts/permissions/approval_policy/on_failure.md +1 -0
  15. pycodex/prompts/permissions/approval_policy/on_request.md +57 -0
  16. pycodex/prompts/permissions/approval_policy/on_request_rule_request_permission.md +33 -0
  17. pycodex/prompts/permissions/approval_policy/unless_trusted.md +1 -0
  18. pycodex/prompts/permissions/sandbox_mode/danger_full_access.md +1 -0
  19. pycodex/prompts/permissions/sandbox_mode/read_only.md +1 -0
  20. pycodex/prompts/permissions/sandbox_mode/workspace_write.md +1 -0
  21. pycodex/prompts/subagent_tools.json +163 -0
  22. pycodex/protocol.py +347 -0
  23. pycodex/runtime.py +200 -0
  24. pycodex/runtime_services.py +408 -0
  25. pycodex/tools/__init__.py +58 -0
  26. pycodex/tools/agent_tool_schemas.py +70 -0
  27. pycodex/tools/apply_patch_tool.py +363 -0
  28. pycodex/tools/base_tool.py +168 -0
  29. pycodex/tools/close_agent_tool.py +55 -0
  30. pycodex/tools/code_mode_manager.py +519 -0
  31. pycodex/tools/exec_command_tool.py +96 -0
  32. pycodex/tools/exec_runtime.js +161 -0
  33. pycodex/tools/exec_tool.py +48 -0
  34. pycodex/tools/grep_files_tool.py +150 -0
  35. pycodex/tools/list_dir_tool.py +135 -0
  36. pycodex/tools/read_file_tool.py +217 -0
  37. pycodex/tools/request_permissions_tool.py +95 -0
  38. pycodex/tools/request_user_input_tool.py +167 -0
  39. pycodex/tools/resume_agent_tool.py +56 -0
  40. pycodex/tools/send_input_tool.py +106 -0
  41. pycodex/tools/shell_command_tool.py +107 -0
  42. pycodex/tools/shell_tool.py +112 -0
  43. pycodex/tools/spawn_agent_tool.py +97 -0
  44. pycodex/tools/unified_exec_manager.py +380 -0
  45. pycodex/tools/update_plan_tool.py +79 -0
  46. pycodex/tools/view_image_tool.py +111 -0
  47. pycodex/tools/wait_agent_tool.py +75 -0
  48. pycodex/tools/wait_tool.py +68 -0
  49. pycodex/tools/web_search_tool.py +30 -0
  50. pycodex/tools/write_stdin_tool.py +75 -0
  51. pycodex/utils/__init__.py +40 -0
  52. pycodex/utils/dotenv.py +64 -0
  53. pycodex/utils/get_env.py +218 -0
  54. pycodex/utils/random_ids.py +19 -0
  55. pycodex/utils/visualize.py +978 -0
  56. python_codex-0.1.0.dist-info/METADATA +267 -0
  57. python_codex-0.1.0.dist-info/RECORD +60 -0
  58. python_codex-0.1.0.dist-info/entry_points.txt +2 -0
  59. python_codex-0.1.0.dist-info/licenses/LICENSE +201 -0
  60. python_codex-0.0.1.dist-info/METADATA +0 -30
  61. python_codex-0.0.1.dist-info/RECORD +0 -4
  62. {python_codex-0.0.1.dist-info → python_codex-0.1.0.dist-info}/WHEEL +0 -0
pycodex/doctor.py ADDED
@@ -0,0 +1,360 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import socket
6
+ import ssl
7
+ import time
8
+ import urllib.parse
9
+ from dataclasses import asdict, dataclass, field
10
+ from pathlib import Path
11
+
12
+ import requests
13
+
14
+ from .model import ResponsesModelClient, ResponsesProviderConfig
15
+ from .protocol import AssistantMessage, Prompt, UserMessage
16
+ from .utils.dotenv import DOTENV_FILENAME, load_codex_dotenv
17
+
18
+
19
+ @dataclass(slots=True)
20
+ class DoctorCheck:
21
+ name: str
22
+ ok: bool
23
+ detail: str
24
+
25
+
26
+ @dataclass(slots=True)
27
+ class DoctorReport:
28
+ ok: bool
29
+ config_path: str
30
+ dotenv_path: str
31
+ profile: str | None
32
+ provider_name: str | None = None
33
+ model: str | None = None
34
+ base_url: str | None = None
35
+ responses_url: str | None = None
36
+ api_key_env: str | None = None
37
+ api_key_loaded: bool = False
38
+ checks: list[DoctorCheck] = field(default_factory=list)
39
+ live_output_text: str | None = None
40
+
41
+ def to_dict(self) -> dict[str, object]:
42
+ return asdict(self)
43
+
44
+
45
+ def build_doctor_parser():
46
+ import argparse
47
+
48
+ parser = argparse.ArgumentParser(
49
+ prog="pycodex doctor",
50
+ description="Diagnose pycodex config, auth, and provider connectivity.",
51
+ )
52
+ parser.add_argument(
53
+ "--config",
54
+ default=str(Path.home() / ".codex" / "config.toml"),
55
+ help="Path to Codex config.toml.",
56
+ )
57
+ parser.add_argument(
58
+ "--profile",
59
+ default=None,
60
+ help="Optional profile name from config.toml.",
61
+ )
62
+ parser.add_argument(
63
+ "--timeout-seconds",
64
+ type=float,
65
+ default=120.0,
66
+ help="Timeout used for network and live model checks.",
67
+ )
68
+ parser.add_argument(
69
+ "--skip-live",
70
+ action="store_true",
71
+ help="Skip the live Responses API request and only run static/network checks.",
72
+ )
73
+ parser.add_argument(
74
+ "--json",
75
+ action="store_true",
76
+ help="Print the full doctor report as JSON.",
77
+ )
78
+ return parser
79
+
80
+
81
+ async def collect_doctor_report(
82
+ config_path: str | Path,
83
+ profile: str | None = None,
84
+ timeout_seconds: float = 120.0,
85
+ skip_live: bool = False,
86
+ ) -> DoctorReport:
87
+ config_file = Path(config_path).expanduser().resolve()
88
+ dotenv_file = config_file.parent / DOTENV_FILENAME
89
+ report = DoctorReport(
90
+ ok=False,
91
+ config_path=str(config_file),
92
+ dotenv_path=str(dotenv_file),
93
+ profile=profile,
94
+ )
95
+
96
+ checks = report.checks
97
+ checks.append(
98
+ DoctorCheck(
99
+ "config",
100
+ config_file.is_file(),
101
+ str(config_file) if config_file.is_file() else f"missing: {config_file}",
102
+ )
103
+ )
104
+ checks.append(
105
+ DoctorCheck(
106
+ "dotenv",
107
+ True,
108
+ (
109
+ str(dotenv_file)
110
+ if dotenv_file.is_file()
111
+ else f"missing (optional): {dotenv_file}"
112
+ ),
113
+ )
114
+ )
115
+ if not config_file.is_file():
116
+ return report
117
+
118
+ load_codex_dotenv(config_file)
119
+
120
+ try:
121
+ provider_config = ResponsesProviderConfig.from_codex_config(config_file, profile)
122
+ except Exception as exc:
123
+ checks.append(DoctorCheck("config_parse", False, f"{type(exc).__name__}: {exc}"))
124
+ return report
125
+
126
+ report.provider_name = provider_config.provider_name
127
+ report.model = provider_config.model
128
+ report.base_url = provider_config.base_url
129
+ client = ResponsesModelClient(provider_config)
130
+ report.responses_url = client.responses_url()
131
+ report.api_key_env = provider_config.api_key_env
132
+ report.api_key_loaded = bool(provider_config.api_key_env and _loaded_api_key(provider_config))
133
+
134
+ checks.append(
135
+ DoctorCheck(
136
+ "provider",
137
+ True,
138
+ (
139
+ f"provider={provider_config.provider_name} "
140
+ f"model={provider_config.model} "
141
+ f"wire_api={provider_config.wire_api}"
142
+ ),
143
+ )
144
+ )
145
+ checks.append(
146
+ DoctorCheck(
147
+ "api_key",
148
+ report.api_key_loaded,
149
+ (
150
+ f"{provider_config.api_key_env} loaded"
151
+ if report.api_key_loaded
152
+ else f"{provider_config.api_key_env} is missing"
153
+ ),
154
+ )
155
+ )
156
+
157
+ parsed_url = urllib.parse.urlsplit(client.responses_url())
158
+ host = parsed_url.hostname or ""
159
+ port = parsed_url.port or (443 if parsed_url.scheme == "https" else 80)
160
+ proxies = requests.utils.get_environ_proxies(client.responses_url())
161
+
162
+ checks.append(
163
+ DoctorCheck(
164
+ "proxy",
165
+ True,
166
+ _proxy_detail(proxies),
167
+ )
168
+ )
169
+
170
+ try:
171
+ addresses = await asyncio.to_thread(
172
+ socket.getaddrinfo,
173
+ host,
174
+ port,
175
+ type=socket.SOCK_STREAM,
176
+ )
177
+ except OSError as exc:
178
+ checks.append(DoctorCheck("dns", False, f"{host}:{port} -> {exc}"))
179
+ return _finalize_report(report)
180
+
181
+ resolved_addresses = sorted(
182
+ {
183
+ result[4][0]
184
+ for result in addresses
185
+ if len(result) >= 5 and isinstance(result[4], tuple) and result[4]
186
+ }
187
+ )
188
+ checks.append(
189
+ DoctorCheck(
190
+ "dns",
191
+ True,
192
+ f"{host}:{port} -> {', '.join(resolved_addresses) or 'resolved'}",
193
+ )
194
+ )
195
+
196
+ if proxies:
197
+ checks.append(
198
+ DoctorCheck(
199
+ "transport",
200
+ True,
201
+ "skipped direct probe because requests will use environment proxy settings",
202
+ )
203
+ )
204
+ else:
205
+ tcp_ok, tcp_detail = await asyncio.to_thread(
206
+ _probe_transport,
207
+ parsed_url.scheme,
208
+ host,
209
+ port,
210
+ timeout_seconds,
211
+ )
212
+ checks.append(DoctorCheck("transport", tcp_ok, tcp_detail))
213
+
214
+ if skip_live:
215
+ return _finalize_report(report)
216
+ if not report.api_key_loaded:
217
+ checks.append(DoctorCheck("live", False, "skipped: missing API key"))
218
+ return _finalize_report(report)
219
+
220
+ live_ok, live_detail, live_output_text = await _run_live_check(
221
+ config_file,
222
+ profile,
223
+ timeout_seconds,
224
+ )
225
+ report.live_output_text = live_output_text
226
+ checks.append(DoctorCheck("live", live_ok, live_detail))
227
+ return _finalize_report(report)
228
+
229
+
230
+ async def run_doctor_cli(args) -> int:
231
+ report = await collect_doctor_report(
232
+ args.config,
233
+ args.profile,
234
+ args.timeout_seconds,
235
+ args.skip_live,
236
+ )
237
+
238
+ if args.json:
239
+ print(json.dumps(report.to_dict(), ensure_ascii=False, indent=2))
240
+ else:
241
+ print(format_doctor_report(report))
242
+
243
+ return 0 if report.ok else 1
244
+
245
+
246
+ def format_doctor_report(report: DoctorReport) -> str:
247
+ lines = [
248
+ f"config: {report.config_path}",
249
+ f"dotenv: {report.dotenv_path}",
250
+ ]
251
+ if report.profile is not None:
252
+ lines.append(f"profile: {report.profile}")
253
+ if report.provider_name is not None:
254
+ lines.append(f"provider: {report.provider_name}")
255
+ if report.model is not None:
256
+ lines.append(f"model: {report.model}")
257
+ if report.responses_url is not None:
258
+ lines.append(f"responses_url: {report.responses_url}")
259
+ if report.api_key_env is not None:
260
+ lines.append(f"api_key_env: {report.api_key_env}")
261
+ if report.live_output_text is not None:
262
+ lines.append(f"live_output: {report.live_output_text}")
263
+ for check in report.checks:
264
+ status = "ok" if check.ok else "fail"
265
+ lines.append(f"{check.name}: {status} - {check.detail}")
266
+ lines.append(f"overall: {'ok' if report.ok else 'fail'}")
267
+ return "\n".join(lines)
268
+
269
+
270
+ def _loaded_api_key(provider_config: ResponsesProviderConfig) -> str:
271
+ try:
272
+ return provider_config.api_key()
273
+ except Exception:
274
+ return ""
275
+
276
+
277
+ def _probe_transport(
278
+ scheme: str,
279
+ host: str,
280
+ port: int,
281
+ timeout_seconds: float,
282
+ ) -> tuple[bool, str]:
283
+ started = time.perf_counter()
284
+ try:
285
+ with socket.create_connection((host, port), timeout=timeout_seconds) as sock:
286
+ if scheme == "https":
287
+ with ssl.create_default_context().wrap_socket(
288
+ sock,
289
+ server_hostname=host,
290
+ ):
291
+ pass
292
+ except OSError as exc:
293
+ elapsed = time.perf_counter() - started
294
+ return False, f"{scheme.upper()} {host}:{port} failed after {elapsed:.2f}s: {exc}"
295
+
296
+ elapsed = time.perf_counter() - started
297
+ label = "tls" if scheme == "https" else "tcp"
298
+ return True, f"{label} {host}:{port} connected in {elapsed:.2f}s"
299
+
300
+
301
+ def _proxy_detail(proxies: dict[str, str]) -> str:
302
+ if not proxies:
303
+ return "not configured"
304
+ return ", ".join(
305
+ f"{key}={_redact_proxy_url(value)}" for key, value in sorted(proxies.items())
306
+ )
307
+
308
+
309
+ def _redact_proxy_url(value: str) -> str:
310
+ parsed = urllib.parse.urlsplit(value)
311
+ if not parsed.scheme or not parsed.netloc:
312
+ return value
313
+ host = parsed.hostname or ""
314
+ port = f":{parsed.port}" if parsed.port is not None else ""
315
+ netloc = f"{host}{port}"
316
+ return urllib.parse.urlunsplit(
317
+ (parsed.scheme, netloc, parsed.path, parsed.query, parsed.fragment)
318
+ )
319
+
320
+
321
+ async def _run_live_check(
322
+ config_path: Path,
323
+ profile: str | None,
324
+ timeout_seconds: float,
325
+ ) -> tuple[bool, str, str | None]:
326
+ client = ResponsesModelClient.from_codex_config(
327
+ config_path,
328
+ profile,
329
+ timeout_seconds,
330
+ originator="pycodex_doctor",
331
+ )
332
+ prompt = Prompt(
333
+ input=[UserMessage(text="Reply with exactly OK.")],
334
+ tools=[],
335
+ parallel_tool_calls=True,
336
+ base_instructions="Reply with exactly OK.",
337
+ )
338
+
339
+ started = time.perf_counter()
340
+ try:
341
+ response = await client.complete(prompt)
342
+ except Exception as exc:
343
+ elapsed = time.perf_counter() - started
344
+ return False, f"failed after {elapsed:.2f}s: {type(exc).__name__}: {exc}", None
345
+
346
+ elapsed = time.perf_counter() - started
347
+ output_text = next(
348
+ (
349
+ item.text
350
+ for item in response.items
351
+ if isinstance(item, AssistantMessage)
352
+ ),
353
+ None,
354
+ )
355
+ return True, f"completed in {elapsed:.2f}s", output_text
356
+
357
+
358
+ def _finalize_report(report: DoctorReport) -> DoctorReport:
359
+ report.ok = all(check.ok for check in report.checks)
360
+ return report