codepp 0.0.437__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 (288) hide show
  1. code_puppy/__init__.py +10 -0
  2. code_puppy/__main__.py +10 -0
  3. code_puppy/agents/__init__.py +31 -0
  4. code_puppy/agents/agent_c_reviewer.py +155 -0
  5. code_puppy/agents/agent_code_puppy.py +117 -0
  6. code_puppy/agents/agent_code_reviewer.py +90 -0
  7. code_puppy/agents/agent_cpp_reviewer.py +132 -0
  8. code_puppy/agents/agent_creator_agent.py +638 -0
  9. code_puppy/agents/agent_golang_reviewer.py +151 -0
  10. code_puppy/agents/agent_helios.py +124 -0
  11. code_puppy/agents/agent_javascript_reviewer.py +160 -0
  12. code_puppy/agents/agent_manager.py +742 -0
  13. code_puppy/agents/agent_pack_leader.py +385 -0
  14. code_puppy/agents/agent_planning.py +165 -0
  15. code_puppy/agents/agent_python_programmer.py +169 -0
  16. code_puppy/agents/agent_python_reviewer.py +90 -0
  17. code_puppy/agents/agent_qa_expert.py +163 -0
  18. code_puppy/agents/agent_qa_kitten.py +208 -0
  19. code_puppy/agents/agent_scheduler.py +121 -0
  20. code_puppy/agents/agent_security_auditor.py +181 -0
  21. code_puppy/agents/agent_terminal_qa.py +323 -0
  22. code_puppy/agents/agent_typescript_reviewer.py +166 -0
  23. code_puppy/agents/base_agent.py +2156 -0
  24. code_puppy/agents/event_stream_handler.py +348 -0
  25. code_puppy/agents/json_agent.py +202 -0
  26. code_puppy/agents/pack/__init__.py +34 -0
  27. code_puppy/agents/pack/bloodhound.py +304 -0
  28. code_puppy/agents/pack/husky.py +327 -0
  29. code_puppy/agents/pack/retriever.py +393 -0
  30. code_puppy/agents/pack/shepherd.py +348 -0
  31. code_puppy/agents/pack/terrier.py +287 -0
  32. code_puppy/agents/pack/watchdog.py +367 -0
  33. code_puppy/agents/prompt_reviewer.py +145 -0
  34. code_puppy/agents/subagent_stream_handler.py +276 -0
  35. code_puppy/api/__init__.py +13 -0
  36. code_puppy/api/app.py +169 -0
  37. code_puppy/api/main.py +21 -0
  38. code_puppy/api/pty_manager.py +453 -0
  39. code_puppy/api/routers/__init__.py +12 -0
  40. code_puppy/api/routers/agents.py +36 -0
  41. code_puppy/api/routers/commands.py +217 -0
  42. code_puppy/api/routers/config.py +75 -0
  43. code_puppy/api/routers/sessions.py +234 -0
  44. code_puppy/api/templates/terminal.html +361 -0
  45. code_puppy/api/websocket.py +154 -0
  46. code_puppy/callbacks.py +692 -0
  47. code_puppy/chatgpt_codex_client.py +338 -0
  48. code_puppy/claude_cache_client.py +672 -0
  49. code_puppy/cli_runner.py +1073 -0
  50. code_puppy/command_line/__init__.py +1 -0
  51. code_puppy/command_line/add_model_menu.py +1092 -0
  52. code_puppy/command_line/agent_menu.py +662 -0
  53. code_puppy/command_line/attachments.py +395 -0
  54. code_puppy/command_line/autosave_menu.py +704 -0
  55. code_puppy/command_line/clipboard.py +527 -0
  56. code_puppy/command_line/colors_menu.py +532 -0
  57. code_puppy/command_line/command_handler.py +293 -0
  58. code_puppy/command_line/command_registry.py +150 -0
  59. code_puppy/command_line/config_commands.py +719 -0
  60. code_puppy/command_line/core_commands.py +867 -0
  61. code_puppy/command_line/diff_menu.py +865 -0
  62. code_puppy/command_line/file_path_completion.py +73 -0
  63. code_puppy/command_line/load_context_completion.py +52 -0
  64. code_puppy/command_line/mcp/__init__.py +10 -0
  65. code_puppy/command_line/mcp/base.py +32 -0
  66. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  67. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  68. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  69. code_puppy/command_line/mcp/edit_command.py +148 -0
  70. code_puppy/command_line/mcp/handler.py +138 -0
  71. code_puppy/command_line/mcp/help_command.py +147 -0
  72. code_puppy/command_line/mcp/install_command.py +214 -0
  73. code_puppy/command_line/mcp/install_menu.py +705 -0
  74. code_puppy/command_line/mcp/list_command.py +94 -0
  75. code_puppy/command_line/mcp/logs_command.py +235 -0
  76. code_puppy/command_line/mcp/remove_command.py +82 -0
  77. code_puppy/command_line/mcp/restart_command.py +100 -0
  78. code_puppy/command_line/mcp/search_command.py +123 -0
  79. code_puppy/command_line/mcp/start_all_command.py +135 -0
  80. code_puppy/command_line/mcp/start_command.py +117 -0
  81. code_puppy/command_line/mcp/status_command.py +184 -0
  82. code_puppy/command_line/mcp/stop_all_command.py +112 -0
  83. code_puppy/command_line/mcp/stop_command.py +80 -0
  84. code_puppy/command_line/mcp/test_command.py +107 -0
  85. code_puppy/command_line/mcp/utils.py +129 -0
  86. code_puppy/command_line/mcp/wizard_utils.py +334 -0
  87. code_puppy/command_line/mcp_completion.py +174 -0
  88. code_puppy/command_line/model_picker_completion.py +197 -0
  89. code_puppy/command_line/model_settings_menu.py +932 -0
  90. code_puppy/command_line/motd.py +96 -0
  91. code_puppy/command_line/onboarding_slides.py +179 -0
  92. code_puppy/command_line/onboarding_wizard.py +342 -0
  93. code_puppy/command_line/pin_command_completion.py +329 -0
  94. code_puppy/command_line/prompt_toolkit_completion.py +846 -0
  95. code_puppy/command_line/session_commands.py +302 -0
  96. code_puppy/command_line/shell_passthrough.py +145 -0
  97. code_puppy/command_line/skills_completion.py +160 -0
  98. code_puppy/command_line/uc_menu.py +893 -0
  99. code_puppy/command_line/utils.py +93 -0
  100. code_puppy/command_line/wiggum_state.py +78 -0
  101. code_puppy/config.py +1770 -0
  102. code_puppy/error_logging.py +134 -0
  103. code_puppy/gemini_code_assist.py +385 -0
  104. code_puppy/gemini_model.py +754 -0
  105. code_puppy/hook_engine/README.md +105 -0
  106. code_puppy/hook_engine/__init__.py +21 -0
  107. code_puppy/hook_engine/aliases.py +155 -0
  108. code_puppy/hook_engine/engine.py +221 -0
  109. code_puppy/hook_engine/executor.py +296 -0
  110. code_puppy/hook_engine/matcher.py +156 -0
  111. code_puppy/hook_engine/models.py +240 -0
  112. code_puppy/hook_engine/registry.py +106 -0
  113. code_puppy/hook_engine/validator.py +144 -0
  114. code_puppy/http_utils.py +361 -0
  115. code_puppy/keymap.py +128 -0
  116. code_puppy/main.py +10 -0
  117. code_puppy/mcp_/__init__.py +66 -0
  118. code_puppy/mcp_/async_lifecycle.py +286 -0
  119. code_puppy/mcp_/blocking_startup.py +469 -0
  120. code_puppy/mcp_/captured_stdio_server.py +275 -0
  121. code_puppy/mcp_/circuit_breaker.py +290 -0
  122. code_puppy/mcp_/config_wizard.py +507 -0
  123. code_puppy/mcp_/dashboard.py +308 -0
  124. code_puppy/mcp_/error_isolation.py +407 -0
  125. code_puppy/mcp_/examples/retry_example.py +226 -0
  126. code_puppy/mcp_/health_monitor.py +589 -0
  127. code_puppy/mcp_/managed_server.py +428 -0
  128. code_puppy/mcp_/manager.py +807 -0
  129. code_puppy/mcp_/mcp_logs.py +224 -0
  130. code_puppy/mcp_/registry.py +451 -0
  131. code_puppy/mcp_/retry_manager.py +337 -0
  132. code_puppy/mcp_/server_registry_catalog.py +1126 -0
  133. code_puppy/mcp_/status_tracker.py +355 -0
  134. code_puppy/mcp_/system_tools.py +209 -0
  135. code_puppy/mcp_prompts/__init__.py +1 -0
  136. code_puppy/mcp_prompts/hook_creator.py +103 -0
  137. code_puppy/messaging/__init__.py +255 -0
  138. code_puppy/messaging/bus.py +613 -0
  139. code_puppy/messaging/commands.py +167 -0
  140. code_puppy/messaging/markdown_patches.py +57 -0
  141. code_puppy/messaging/message_queue.py +361 -0
  142. code_puppy/messaging/messages.py +569 -0
  143. code_puppy/messaging/queue_console.py +271 -0
  144. code_puppy/messaging/renderers.py +311 -0
  145. code_puppy/messaging/rich_renderer.py +1158 -0
  146. code_puppy/messaging/spinner/__init__.py +83 -0
  147. code_puppy/messaging/spinner/console_spinner.py +240 -0
  148. code_puppy/messaging/spinner/spinner_base.py +95 -0
  149. code_puppy/messaging/subagent_console.py +460 -0
  150. code_puppy/model_factory.py +848 -0
  151. code_puppy/model_switching.py +63 -0
  152. code_puppy/model_utils.py +168 -0
  153. code_puppy/models.json +174 -0
  154. code_puppy/models_dev_api.json +1 -0
  155. code_puppy/models_dev_parser.py +592 -0
  156. code_puppy/plugins/__init__.py +186 -0
  157. code_puppy/plugins/agent_skills/__init__.py +22 -0
  158. code_puppy/plugins/agent_skills/config.py +175 -0
  159. code_puppy/plugins/agent_skills/discovery.py +136 -0
  160. code_puppy/plugins/agent_skills/downloader.py +392 -0
  161. code_puppy/plugins/agent_skills/installer.py +22 -0
  162. code_puppy/plugins/agent_skills/metadata.py +219 -0
  163. code_puppy/plugins/agent_skills/prompt_builder.py +60 -0
  164. code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
  165. code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
  166. code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
  167. code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
  168. code_puppy/plugins/agent_skills/skills_menu.py +781 -0
  169. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  170. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  171. code_puppy/plugins/antigravity_oauth/antigravity_model.py +706 -0
  172. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  173. code_puppy/plugins/antigravity_oauth/constants.py +133 -0
  174. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  175. code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
  176. code_puppy/plugins/antigravity_oauth/storage.py +288 -0
  177. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  178. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  179. code_puppy/plugins/antigravity_oauth/transport.py +863 -0
  180. code_puppy/plugins/antigravity_oauth/utils.py +168 -0
  181. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  182. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  183. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +329 -0
  184. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
  185. code_puppy/plugins/chatgpt_oauth/test_plugin.py +301 -0
  186. code_puppy/plugins/chatgpt_oauth/utils.py +523 -0
  187. code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
  188. code_puppy/plugins/claude_code_hooks/config.py +137 -0
  189. code_puppy/plugins/claude_code_hooks/register_callbacks.py +175 -0
  190. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  191. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  192. code_puppy/plugins/claude_code_oauth/__init__.py +25 -0
  193. code_puppy/plugins/claude_code_oauth/config.py +52 -0
  194. code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
  195. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  196. code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
  197. code_puppy/plugins/claude_code_oauth/utils.py +640 -0
  198. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  199. code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
  200. code_puppy/plugins/example_custom_command/README.md +280 -0
  201. code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
  202. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  203. code_puppy/plugins/file_permission_handler/register_callbacks.py +470 -0
  204. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  205. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  206. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  207. code_puppy/plugins/hook_creator/__init__.py +1 -0
  208. code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
  209. code_puppy/plugins/hook_manager/__init__.py +1 -0
  210. code_puppy/plugins/hook_manager/config.py +290 -0
  211. code_puppy/plugins/hook_manager/hooks_menu.py +564 -0
  212. code_puppy/plugins/hook_manager/register_callbacks.py +227 -0
  213. code_puppy/plugins/oauth_puppy_html.py +228 -0
  214. code_puppy/plugins/scheduler/__init__.py +1 -0
  215. code_puppy/plugins/scheduler/register_callbacks.py +88 -0
  216. code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
  217. code_puppy/plugins/scheduler/scheduler_wizard.py +341 -0
  218. code_puppy/plugins/shell_safety/__init__.py +6 -0
  219. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  220. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  221. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  222. code_puppy/plugins/synthetic_status/__init__.py +1 -0
  223. code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
  224. code_puppy/plugins/synthetic_status/status_api.py +147 -0
  225. code_puppy/plugins/universal_constructor/__init__.py +13 -0
  226. code_puppy/plugins/universal_constructor/models.py +138 -0
  227. code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
  228. code_puppy/plugins/universal_constructor/registry.py +302 -0
  229. code_puppy/plugins/universal_constructor/sandbox.py +584 -0
  230. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  231. code_puppy/pydantic_patches.py +356 -0
  232. code_puppy/reopenable_async_client.py +232 -0
  233. code_puppy/round_robin_model.py +150 -0
  234. code_puppy/scheduler/__init__.py +41 -0
  235. code_puppy/scheduler/__main__.py +9 -0
  236. code_puppy/scheduler/cli.py +118 -0
  237. code_puppy/scheduler/config.py +126 -0
  238. code_puppy/scheduler/daemon.py +280 -0
  239. code_puppy/scheduler/executor.py +155 -0
  240. code_puppy/scheduler/platform.py +19 -0
  241. code_puppy/scheduler/platform_unix.py +22 -0
  242. code_puppy/scheduler/platform_win.py +32 -0
  243. code_puppy/session_storage.py +338 -0
  244. code_puppy/status_display.py +257 -0
  245. code_puppy/summarization_agent.py +176 -0
  246. code_puppy/terminal_utils.py +418 -0
  247. code_puppy/tools/__init__.py +501 -0
  248. code_puppy/tools/agent_tools.py +603 -0
  249. code_puppy/tools/ask_user_question/__init__.py +26 -0
  250. code_puppy/tools/ask_user_question/constants.py +73 -0
  251. code_puppy/tools/ask_user_question/demo_tui.py +55 -0
  252. code_puppy/tools/ask_user_question/handler.py +232 -0
  253. code_puppy/tools/ask_user_question/models.py +304 -0
  254. code_puppy/tools/ask_user_question/registration.py +26 -0
  255. code_puppy/tools/ask_user_question/renderers.py +309 -0
  256. code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
  257. code_puppy/tools/ask_user_question/theme.py +155 -0
  258. code_puppy/tools/ask_user_question/tui_loop.py +423 -0
  259. code_puppy/tools/browser/__init__.py +37 -0
  260. code_puppy/tools/browser/browser_control.py +289 -0
  261. code_puppy/tools/browser/browser_interactions.py +545 -0
  262. code_puppy/tools/browser/browser_locators.py +640 -0
  263. code_puppy/tools/browser/browser_manager.py +378 -0
  264. code_puppy/tools/browser/browser_navigation.py +251 -0
  265. code_puppy/tools/browser/browser_screenshot.py +179 -0
  266. code_puppy/tools/browser/browser_scripts.py +462 -0
  267. code_puppy/tools/browser/browser_workflows.py +221 -0
  268. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  269. code_puppy/tools/browser/terminal_command_tools.py +534 -0
  270. code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
  271. code_puppy/tools/browser/terminal_tools.py +525 -0
  272. code_puppy/tools/command_runner.py +1346 -0
  273. code_puppy/tools/common.py +1409 -0
  274. code_puppy/tools/display.py +84 -0
  275. code_puppy/tools/file_modifications.py +886 -0
  276. code_puppy/tools/file_operations.py +802 -0
  277. code_puppy/tools/scheduler_tools.py +412 -0
  278. code_puppy/tools/skills_tools.py +244 -0
  279. code_puppy/tools/subagent_context.py +158 -0
  280. code_puppy/tools/tools_content.py +51 -0
  281. code_puppy/tools/universal_constructor.py +889 -0
  282. code_puppy/uvx_detection.py +242 -0
  283. code_puppy/version_checker.py +82 -0
  284. codepp-0.0.437.dist-info/METADATA +766 -0
  285. codepp-0.0.437.dist-info/RECORD +288 -0
  286. codepp-0.0.437.dist-info/WHEEL +4 -0
  287. codepp-0.0.437.dist-info/entry_points.txt +3 -0
  288. codepp-0.0.437.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,106 @@
1
+ """
2
+ Registry management for hooks.
3
+
4
+ Builds and manages the HookRegistry from configuration dictionaries.
5
+ """
6
+
7
+ import logging
8
+ import re
9
+ from typing import Any, Dict
10
+
11
+ from .models import HookConfig, HookRegistry
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # Supported event types
16
+ SUPPORTED_EVENT_TYPES = [
17
+ "PreToolUse",
18
+ "PostToolUse",
19
+ "SessionStart",
20
+ "SessionEnd",
21
+ "PreCompact",
22
+ "UserPromptSubmit",
23
+ "Notification",
24
+ "Stop",
25
+ "SubagentStop",
26
+ ]
27
+
28
+
29
+ def build_registry_from_config(config: Dict[str, Any]) -> HookRegistry:
30
+ """
31
+ Build a HookRegistry from a configuration dictionary.
32
+
33
+ Args:
34
+ config: Hook configuration dictionary
35
+
36
+ Returns:
37
+ Populated HookRegistry
38
+ """
39
+ registry = HookRegistry()
40
+
41
+ for event_type, hook_groups in config.items():
42
+ if event_type.startswith("_"):
43
+ continue # skip comment keys
44
+
45
+ if not isinstance(hook_groups, list):
46
+ logger.warning(f"Hook groups for '{event_type}' must be a list, skipping")
47
+ continue
48
+
49
+ for group in hook_groups:
50
+ if not isinstance(group, dict):
51
+ continue
52
+
53
+ matcher = group.get("matcher", "*")
54
+ hooks_data = group.get("hooks", [])
55
+
56
+ for hook_data in hooks_data:
57
+ if not isinstance(hook_data, dict):
58
+ continue
59
+ if hook_data.get("type") == "command" and not hook_data.get("command"):
60
+ continue
61
+
62
+ try:
63
+ hook = HookConfig(
64
+ matcher=matcher,
65
+ type=hook_data.get("type", "command"),
66
+ command=hook_data.get("command", hook_data.get("prompt", "")),
67
+ timeout=hook_data.get("timeout", 5000),
68
+ once=hook_data.get("once", False),
69
+ enabled=hook_data.get("enabled", True),
70
+ id=hook_data.get("id"),
71
+ )
72
+ registry.add_hook(event_type, hook)
73
+ except (ValueError, KeyError) as e:
74
+ logger.warning(f"Skipping invalid hook in {event_type}: {e}")
75
+
76
+ return registry
77
+
78
+
79
+ def get_registry_stats(registry: HookRegistry) -> Dict[str, Any]:
80
+ """Get statistics about a registry."""
81
+ stats: Dict[str, Any] = {
82
+ "total_hooks": registry.count_hooks(),
83
+ "enabled_hooks": 0,
84
+ "disabled_hooks": 0,
85
+ "by_event": {},
86
+ }
87
+
88
+ def _to_attr(event_type: str) -> str:
89
+ s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", event_type)
90
+ return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
91
+
92
+ for event_type in SUPPORTED_EVENT_TYPES:
93
+ attr = _to_attr(event_type)
94
+ hooks = getattr(registry, attr, [])
95
+ enabled = sum(1 for h in hooks if h.enabled)
96
+ disabled = len(hooks) - enabled
97
+ stats["enabled_hooks"] += enabled
98
+ stats["disabled_hooks"] += disabled
99
+ if hooks:
100
+ stats["by_event"][event_type] = {
101
+ "total": len(hooks),
102
+ "enabled": enabled,
103
+ "disabled": disabled,
104
+ }
105
+
106
+ return stats
@@ -0,0 +1,144 @@
1
+ """
2
+ Configuration validation for hooks.
3
+
4
+ Validates hook configuration dictionaries and provides actionable error messages.
5
+ """
6
+
7
+ import logging
8
+ from typing import Any, Dict, List, Optional, Tuple
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ VALID_EVENT_TYPES = [
13
+ "PreToolUse",
14
+ "PostToolUse",
15
+ "SessionStart",
16
+ "SessionEnd",
17
+ "PreCompact",
18
+ "UserPromptSubmit",
19
+ "Notification",
20
+ "Stop",
21
+ "SubagentStop",
22
+ ]
23
+
24
+ VALID_HOOK_TYPES = ["command", "prompt"]
25
+
26
+
27
+ def validate_hooks_config(config: Dict[str, Any]) -> Tuple[bool, List[str]]:
28
+ """
29
+ Validate a hooks configuration dictionary.
30
+
31
+ Returns:
32
+ Tuple of (is_valid, list_of_error_messages)
33
+ """
34
+ errors: List[str] = []
35
+
36
+ if not isinstance(config, dict):
37
+ return False, ["Configuration must be a dictionary"]
38
+
39
+ for event_type, hook_groups in config.items():
40
+ if event_type.startswith("_"):
41
+ continue # skip comment keys
42
+
43
+ if event_type not in VALID_EVENT_TYPES:
44
+ errors.append(
45
+ f"Unknown event type '{event_type}'. "
46
+ f"Valid types: {', '.join(VALID_EVENT_TYPES)}"
47
+ )
48
+ continue
49
+
50
+ if not isinstance(hook_groups, list):
51
+ errors.append(f"'{event_type}' must be a list of hook groups")
52
+ continue
53
+
54
+ for i, group in enumerate(hook_groups):
55
+ if not isinstance(group, dict):
56
+ errors.append(
57
+ f"'{event_type}[{i}]' must be a dict with 'matcher' and 'hooks'"
58
+ )
59
+ continue
60
+
61
+ if "matcher" not in group:
62
+ errors.append(f"'{event_type}[{i}]' missing required field 'matcher'")
63
+
64
+ if "hooks" not in group:
65
+ errors.append(f"'{event_type}[{i}]' missing required field 'hooks'")
66
+ continue
67
+
68
+ if not isinstance(group["hooks"], list):
69
+ errors.append(f"'{event_type}[{i}].hooks' must be a list")
70
+ continue
71
+
72
+ for j, hook in enumerate(group["hooks"]):
73
+ hook_errors = _validate_hook(event_type, i, j, hook)
74
+ errors.extend(hook_errors)
75
+
76
+ return len(errors) == 0, errors
77
+
78
+
79
+ def _validate_hook(
80
+ event_type: str, group_idx: int, hook_idx: int, hook: Any
81
+ ) -> List[str]:
82
+ errors: List[str] = []
83
+ prefix = f"'{event_type}[{group_idx}].hooks[{hook_idx}]'"
84
+
85
+ if not isinstance(hook, dict):
86
+ return [f"{prefix} must be a dict"]
87
+
88
+ hook_type = hook.get("type")
89
+ if not hook_type:
90
+ errors.append(f"{prefix} missing required field 'type'")
91
+ elif hook_type not in VALID_HOOK_TYPES:
92
+ errors.append(
93
+ f"{prefix} invalid type '{hook_type}'. Must be one of: {', '.join(VALID_HOOK_TYPES)}"
94
+ )
95
+
96
+ if hook_type == "command" and not hook.get("command"):
97
+ errors.append(f"{prefix} missing required field 'command' for type 'command'")
98
+ elif hook_type == "prompt" and not hook.get("prompt") and not hook.get("command"):
99
+ errors.append(
100
+ f"{prefix} missing required field 'prompt' (or 'command') for type 'prompt'"
101
+ )
102
+
103
+ timeout = hook.get("timeout")
104
+ if timeout is not None:
105
+ if not isinstance(timeout, (int, float)) or timeout < 100:
106
+ errors.append(f"{prefix} 'timeout' must be >= 100ms, got: {timeout}")
107
+
108
+ return errors
109
+
110
+
111
+ def format_validation_report(
112
+ is_valid: bool, errors: List[str], suggestions: Optional[List[str]] = None
113
+ ) -> str:
114
+ lines = []
115
+ if is_valid:
116
+ lines.append("✓ Configuration is valid")
117
+ else:
118
+ lines.append(f"✗ Configuration has {len(errors)} error(s):")
119
+ for error in errors:
120
+ lines.append(f" • {error}")
121
+
122
+ if suggestions:
123
+ lines.append("\nSuggestions:")
124
+ for suggestion in suggestions:
125
+ lines.append(f" → {suggestion}")
126
+
127
+ return "\n".join(lines)
128
+
129
+
130
+ def get_config_suggestions(config: Dict[str, Any], errors: List[str]) -> List[str]:
131
+ suggestions: List[str] = []
132
+
133
+ for error in errors:
134
+ if "Unknown event type" in error:
135
+ suggestions.append("Valid event types are: " + ", ".join(VALID_EVENT_TYPES))
136
+ break
137
+
138
+ if any("missing required field 'command'" in e for e in errors):
139
+ suggestions.append(
140
+ "Hook commands should be shell commands like: "
141
+ "'bash .claude/hooks/my-hook.sh'"
142
+ )
143
+
144
+ return suggestions
@@ -0,0 +1,361 @@
1
+ """
2
+ HTTP utilities module for code-puppy.
3
+
4
+ This module provides functions for creating properly configured HTTP clients.
5
+ """
6
+
7
+ import asyncio
8
+ import os
9
+ import socket
10
+ import time
11
+ from dataclasses import dataclass
12
+ from typing import TYPE_CHECKING, Any, Dict, Optional, Union
13
+
14
+ import httpx
15
+
16
+ if TYPE_CHECKING:
17
+ import requests
18
+ from code_puppy.config import get_http2
19
+
20
+
21
+ @dataclass
22
+ class ProxyConfig:
23
+ """Configuration for proxy and SSL settings."""
24
+
25
+ verify: Union[bool, str, None]
26
+ trust_env: bool
27
+ proxy_url: str | None
28
+ disable_retry: bool
29
+ http2_enabled: bool
30
+
31
+
32
+ def _resolve_proxy_config(verify: Union[bool, str, None] = None) -> ProxyConfig:
33
+ """Resolve proxy, SSL, and retry settings from environment.
34
+
35
+ This centralizes the logic for detecting proxies, determining SSL verification,
36
+ and checking if retry transport should be disabled.
37
+ """
38
+ if verify is None:
39
+ verify = get_cert_bundle_path()
40
+
41
+ http2_enabled = get_http2()
42
+
43
+ disable_retry = os.environ.get(
44
+ "CODE_PUPPY_DISABLE_RETRY_TRANSPORT", ""
45
+ ).lower() in ("1", "true", "yes")
46
+
47
+ has_proxy = bool(
48
+ os.environ.get("HTTP_PROXY")
49
+ or os.environ.get("HTTPS_PROXY")
50
+ or os.environ.get("http_proxy")
51
+ or os.environ.get("https_proxy")
52
+ )
53
+
54
+ # Determine trust_env and verify based on proxy/retry settings
55
+ if disable_retry:
56
+ # Test mode: disable SSL verification for proxy testing
57
+ verify = False
58
+ trust_env = True
59
+ elif has_proxy:
60
+ # Production proxy: keep SSL verification enabled
61
+ trust_env = True
62
+ else:
63
+ trust_env = False
64
+
65
+ # Extract proxy URL
66
+ proxy_url = None
67
+ if has_proxy:
68
+ proxy_url = (
69
+ os.environ.get("HTTPS_PROXY")
70
+ or os.environ.get("https_proxy")
71
+ or os.environ.get("HTTP_PROXY")
72
+ or os.environ.get("http_proxy")
73
+ )
74
+
75
+ return ProxyConfig(
76
+ verify=verify,
77
+ trust_env=trust_env,
78
+ proxy_url=proxy_url,
79
+ disable_retry=disable_retry,
80
+ http2_enabled=http2_enabled,
81
+ )
82
+
83
+
84
+ try:
85
+ from .reopenable_async_client import ReopenableAsyncClient
86
+ except ImportError:
87
+ ReopenableAsyncClient = None
88
+
89
+ try:
90
+ from .messaging import emit_info, emit_warning
91
+ except ImportError:
92
+ # Fallback if messaging system is not available
93
+ def emit_info(content: str, **metadata):
94
+ pass # No-op if messaging system is not available
95
+
96
+ def emit_warning(content: str, **metadata):
97
+ pass
98
+
99
+
100
+ class RetryingAsyncClient(httpx.AsyncClient):
101
+ """AsyncClient with built-in rate limit handling (429) and retries.
102
+
103
+ This replaces the Tenacity transport with a more direct subclass implementation,
104
+ which plays nicer with proxies and custom transports (like Antigravity).
105
+
106
+ Special handling for Cerebras: Their Retry-After headers are absurdly aggressive
107
+ (often 60s), so we ignore them and use a 3s base backoff instead.
108
+ """
109
+
110
+ def __init__(
111
+ self,
112
+ retry_status_codes: tuple = (429, 502, 503, 504),
113
+ max_retries: int = 5,
114
+ model_name: str = "",
115
+ **kwargs,
116
+ ):
117
+ super().__init__(**kwargs)
118
+ self.retry_status_codes = retry_status_codes
119
+ self.max_retries = max_retries
120
+ self.model_name = model_name.lower() if model_name else ""
121
+ # Cerebras sends crazy aggressive Retry-After headers (60s), ignore them
122
+ self._ignore_retry_headers = "cerebras" in self.model_name
123
+
124
+ async def send(self, request: httpx.Request, **kwargs: Any) -> httpx.Response:
125
+ """Send request with automatic retries for rate limits and server errors."""
126
+ last_response = None
127
+ last_exception = None
128
+
129
+ for attempt in range(self.max_retries + 1):
130
+ try:
131
+ response = await super().send(request, **kwargs)
132
+ last_response = response
133
+
134
+ # Check for retryable status
135
+ if response.status_code not in self.retry_status_codes:
136
+ return response
137
+
138
+ # Close response if we're going to retry
139
+ await response.aclose()
140
+
141
+ # Determine wait time - Cerebras gets special treatment
142
+ if self._ignore_retry_headers:
143
+ # Cerebras: 3s base with exponential backoff (3s, 6s, 12s...)
144
+ wait_time = 3.0 * (2**attempt)
145
+ else:
146
+ # Default exponential backoff: 1s, 2s, 4s...
147
+ wait_time = 1.0 * (2**attempt)
148
+
149
+ # Check Retry-After header (only for non-Cerebras)
150
+ retry_after = response.headers.get("Retry-After")
151
+ if retry_after:
152
+ try:
153
+ wait_time = float(retry_after)
154
+ except ValueError:
155
+ # Try parsing http-date
156
+ from email.utils import parsedate_to_datetime
157
+
158
+ try:
159
+ date = parsedate_to_datetime(retry_after)
160
+ wait_time = date.timestamp() - time.time()
161
+ except Exception:
162
+ pass
163
+
164
+ # Cap wait time
165
+ wait_time = max(0.5, min(wait_time, 60.0))
166
+
167
+ if attempt < self.max_retries:
168
+ provider_note = (
169
+ " (ignoring header)" if self._ignore_retry_headers else ""
170
+ )
171
+ emit_info(
172
+ f"HTTP retry: {response.status_code} received{provider_note}. "
173
+ f"Waiting {wait_time:.1f}s (attempt {attempt + 1}/{self.max_retries})"
174
+ )
175
+ await asyncio.sleep(wait_time)
176
+
177
+ except (httpx.ConnectError, httpx.ReadTimeout, httpx.PoolTimeout) as e:
178
+ last_exception = e
179
+ wait_time = 1.0 * (2**attempt)
180
+ if attempt < self.max_retries:
181
+ emit_warning(
182
+ f"HTTP connection error: {e}. Retrying in {wait_time}s..."
183
+ )
184
+ await asyncio.sleep(wait_time)
185
+ else:
186
+ raise
187
+ except Exception:
188
+ raise
189
+
190
+ # Return last response (even if it's an error status)
191
+ if last_response:
192
+ return last_response
193
+
194
+ # Should catch this in loop, but just in case
195
+ if last_exception:
196
+ raise last_exception
197
+
198
+ return last_response
199
+
200
+
201
+ def get_cert_bundle_path() -> str | None:
202
+ # First check if SSL_CERT_FILE environment variable is set
203
+ ssl_cert_file = os.environ.get("SSL_CERT_FILE")
204
+ if ssl_cert_file and os.path.exists(ssl_cert_file):
205
+ return ssl_cert_file
206
+
207
+
208
+ def create_client(
209
+ timeout: int = 180,
210
+ verify: Union[bool, str] = None,
211
+ headers: Optional[Dict[str, str]] = None,
212
+ retry_status_codes: tuple = (429, 502, 503, 504),
213
+ ) -> httpx.Client:
214
+ if verify is None:
215
+ verify = get_cert_bundle_path()
216
+
217
+ # Check if HTTP/2 is enabled in config
218
+ http2_enabled = get_http2()
219
+
220
+ # If retry components are available, create a client with retry transport
221
+ # Note: TenacityTransport was removed. For now we just return a standard client.
222
+ # Future TODO: Implement RetryingClient(httpx.Client) if needed.
223
+ return httpx.Client(
224
+ verify=verify,
225
+ headers=headers or {},
226
+ timeout=timeout,
227
+ http2=http2_enabled,
228
+ )
229
+
230
+
231
+ def create_async_client(
232
+ timeout: int = 180,
233
+ verify: Union[bool, str] = None,
234
+ headers: Optional[Dict[str, str]] = None,
235
+ retry_status_codes: tuple = (429, 502, 503, 504),
236
+ model_name: str = "",
237
+ ) -> httpx.AsyncClient:
238
+ config = _resolve_proxy_config(verify)
239
+
240
+ if not config.disable_retry:
241
+ return RetryingAsyncClient(
242
+ retry_status_codes=retry_status_codes,
243
+ model_name=model_name,
244
+ proxy=config.proxy_url,
245
+ verify=config.verify,
246
+ headers=headers or {},
247
+ timeout=timeout,
248
+ http2=config.http2_enabled,
249
+ trust_env=config.trust_env,
250
+ )
251
+ else:
252
+ return httpx.AsyncClient(
253
+ proxy=config.proxy_url,
254
+ verify=config.verify,
255
+ headers=headers or {},
256
+ timeout=timeout,
257
+ http2=config.http2_enabled,
258
+ trust_env=config.trust_env,
259
+ )
260
+
261
+
262
+ def create_requests_session(
263
+ timeout: float = 5.0,
264
+ verify: Union[bool, str] = None,
265
+ headers: Optional[Dict[str, str]] = None,
266
+ ) -> "requests.Session":
267
+ import requests
268
+
269
+ session = requests.Session()
270
+
271
+ if verify is None:
272
+ verify = get_cert_bundle_path()
273
+
274
+ session.verify = verify
275
+
276
+ if headers:
277
+ session.headers.update(headers or {})
278
+
279
+ return session
280
+
281
+
282
+ def create_auth_headers(
283
+ api_key: str, header_name: str = "Authorization"
284
+ ) -> Dict[str, str]:
285
+ return {header_name: f"Bearer {api_key}"}
286
+
287
+
288
+ def resolve_env_var_in_header(headers: Dict[str, str]) -> Dict[str, str]:
289
+ resolved_headers = {}
290
+
291
+ for key, value in headers.items():
292
+ if isinstance(value, str):
293
+ try:
294
+ expanded = os.path.expandvars(value)
295
+ resolved_headers[key] = expanded
296
+ except Exception:
297
+ resolved_headers[key] = value
298
+ else:
299
+ resolved_headers[key] = value
300
+
301
+ return resolved_headers
302
+
303
+
304
+ def create_reopenable_async_client(
305
+ timeout: int = 180,
306
+ verify: Union[bool, str] = None,
307
+ headers: Optional[Dict[str, str]] = None,
308
+ retry_status_codes: tuple = (429, 502, 503, 504),
309
+ model_name: str = "",
310
+ ) -> Union[ReopenableAsyncClient, httpx.AsyncClient]:
311
+ config = _resolve_proxy_config(verify)
312
+
313
+ base_kwargs = {
314
+ "proxy": config.proxy_url,
315
+ "verify": config.verify,
316
+ "headers": headers or {},
317
+ "timeout": timeout,
318
+ "http2": config.http2_enabled,
319
+ "trust_env": config.trust_env,
320
+ }
321
+
322
+ if ReopenableAsyncClient is not None:
323
+ client_class = (
324
+ RetryingAsyncClient if not config.disable_retry else httpx.AsyncClient
325
+ )
326
+ kwargs = {**base_kwargs, "client_class": client_class}
327
+ if not config.disable_retry:
328
+ kwargs["retry_status_codes"] = retry_status_codes
329
+ kwargs["model_name"] = model_name
330
+ return ReopenableAsyncClient(**kwargs)
331
+ else:
332
+ # Fallback to RetryingAsyncClient or plain AsyncClient
333
+ if not config.disable_retry:
334
+ return RetryingAsyncClient(
335
+ retry_status_codes=retry_status_codes,
336
+ model_name=model_name,
337
+ **base_kwargs,
338
+ )
339
+ else:
340
+ return httpx.AsyncClient(**base_kwargs)
341
+
342
+
343
+ def is_cert_bundle_available() -> bool:
344
+ cert_path = get_cert_bundle_path()
345
+ if cert_path is None:
346
+ return False
347
+ return os.path.exists(cert_path) and os.path.isfile(cert_path)
348
+
349
+
350
+ def find_available_port(start_port=8090, end_port=9010, host="127.0.0.1"):
351
+ for port in range(start_port, end_port + 1):
352
+ try:
353
+ # Try to bind to the port to check if it's available
354
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
355
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
356
+ sock.bind((host, port))
357
+ return port
358
+ except OSError:
359
+ # Port is in use, try the next one
360
+ continue
361
+ return None