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.
- code_puppy/__init__.py +10 -0
- code_puppy/__main__.py +10 -0
- code_puppy/agents/__init__.py +31 -0
- code_puppy/agents/agent_c_reviewer.py +155 -0
- code_puppy/agents/agent_code_puppy.py +117 -0
- code_puppy/agents/agent_code_reviewer.py +90 -0
- code_puppy/agents/agent_cpp_reviewer.py +132 -0
- code_puppy/agents/agent_creator_agent.py +638 -0
- code_puppy/agents/agent_golang_reviewer.py +151 -0
- code_puppy/agents/agent_helios.py +124 -0
- code_puppy/agents/agent_javascript_reviewer.py +160 -0
- code_puppy/agents/agent_manager.py +742 -0
- code_puppy/agents/agent_pack_leader.py +385 -0
- code_puppy/agents/agent_planning.py +165 -0
- code_puppy/agents/agent_python_programmer.py +169 -0
- code_puppy/agents/agent_python_reviewer.py +90 -0
- code_puppy/agents/agent_qa_expert.py +163 -0
- code_puppy/agents/agent_qa_kitten.py +208 -0
- code_puppy/agents/agent_scheduler.py +121 -0
- code_puppy/agents/agent_security_auditor.py +181 -0
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +166 -0
- code_puppy/agents/base_agent.py +2156 -0
- code_puppy/agents/event_stream_handler.py +348 -0
- code_puppy/agents/json_agent.py +202 -0
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +327 -0
- code_puppy/agents/pack/retriever.py +393 -0
- code_puppy/agents/pack/shepherd.py +348 -0
- code_puppy/agents/pack/terrier.py +287 -0
- code_puppy/agents/pack/watchdog.py +367 -0
- code_puppy/agents/prompt_reviewer.py +145 -0
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +169 -0
- code_puppy/api/main.py +21 -0
- code_puppy/api/pty_manager.py +453 -0
- code_puppy/api/routers/__init__.py +12 -0
- code_puppy/api/routers/agents.py +36 -0
- code_puppy/api/routers/commands.py +217 -0
- code_puppy/api/routers/config.py +75 -0
- code_puppy/api/routers/sessions.py +234 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +692 -0
- code_puppy/chatgpt_codex_client.py +338 -0
- code_puppy/claude_cache_client.py +672 -0
- code_puppy/cli_runner.py +1073 -0
- code_puppy/command_line/__init__.py +1 -0
- code_puppy/command_line/add_model_menu.py +1092 -0
- code_puppy/command_line/agent_menu.py +662 -0
- code_puppy/command_line/attachments.py +395 -0
- code_puppy/command_line/autosave_menu.py +704 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +532 -0
- code_puppy/command_line/command_handler.py +293 -0
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +719 -0
- code_puppy/command_line/core_commands.py +867 -0
- code_puppy/command_line/diff_menu.py +865 -0
- code_puppy/command_line/file_path_completion.py +73 -0
- code_puppy/command_line/load_context_completion.py +52 -0
- code_puppy/command_line/mcp/__init__.py +10 -0
- code_puppy/command_line/mcp/base.py +32 -0
- code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
- code_puppy/command_line/mcp/custom_server_form.py +688 -0
- code_puppy/command_line/mcp/custom_server_installer.py +195 -0
- code_puppy/command_line/mcp/edit_command.py +148 -0
- code_puppy/command_line/mcp/handler.py +138 -0
- code_puppy/command_line/mcp/help_command.py +147 -0
- code_puppy/command_line/mcp/install_command.py +214 -0
- code_puppy/command_line/mcp/install_menu.py +705 -0
- code_puppy/command_line/mcp/list_command.py +94 -0
- code_puppy/command_line/mcp/logs_command.py +235 -0
- code_puppy/command_line/mcp/remove_command.py +82 -0
- code_puppy/command_line/mcp/restart_command.py +100 -0
- code_puppy/command_line/mcp/search_command.py +123 -0
- code_puppy/command_line/mcp/start_all_command.py +135 -0
- code_puppy/command_line/mcp/start_command.py +117 -0
- code_puppy/command_line/mcp/status_command.py +184 -0
- code_puppy/command_line/mcp/stop_all_command.py +112 -0
- code_puppy/command_line/mcp/stop_command.py +80 -0
- code_puppy/command_line/mcp/test_command.py +107 -0
- code_puppy/command_line/mcp/utils.py +129 -0
- code_puppy/command_line/mcp/wizard_utils.py +334 -0
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +197 -0
- code_puppy/command_line/model_settings_menu.py +932 -0
- code_puppy/command_line/motd.py +96 -0
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +342 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +846 -0
- code_puppy/command_line/session_commands.py +302 -0
- code_puppy/command_line/shell_passthrough.py +145 -0
- code_puppy/command_line/skills_completion.py +160 -0
- code_puppy/command_line/uc_menu.py +893 -0
- code_puppy/command_line/utils.py +93 -0
- code_puppy/command_line/wiggum_state.py +78 -0
- code_puppy/config.py +1770 -0
- code_puppy/error_logging.py +134 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +754 -0
- code_puppy/hook_engine/README.md +105 -0
- code_puppy/hook_engine/__init__.py +21 -0
- code_puppy/hook_engine/aliases.py +155 -0
- code_puppy/hook_engine/engine.py +221 -0
- code_puppy/hook_engine/executor.py +296 -0
- code_puppy/hook_engine/matcher.py +156 -0
- code_puppy/hook_engine/models.py +240 -0
- code_puppy/hook_engine/registry.py +106 -0
- code_puppy/hook_engine/validator.py +144 -0
- code_puppy/http_utils.py +361 -0
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +10 -0
- code_puppy/mcp_/__init__.py +66 -0
- code_puppy/mcp_/async_lifecycle.py +286 -0
- code_puppy/mcp_/blocking_startup.py +469 -0
- code_puppy/mcp_/captured_stdio_server.py +275 -0
- code_puppy/mcp_/circuit_breaker.py +290 -0
- code_puppy/mcp_/config_wizard.py +507 -0
- code_puppy/mcp_/dashboard.py +308 -0
- code_puppy/mcp_/error_isolation.py +407 -0
- code_puppy/mcp_/examples/retry_example.py +226 -0
- code_puppy/mcp_/health_monitor.py +589 -0
- code_puppy/mcp_/managed_server.py +428 -0
- code_puppy/mcp_/manager.py +807 -0
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/mcp_/registry.py +451 -0
- code_puppy/mcp_/retry_manager.py +337 -0
- code_puppy/mcp_/server_registry_catalog.py +1126 -0
- code_puppy/mcp_/status_tracker.py +355 -0
- code_puppy/mcp_/system_tools.py +209 -0
- code_puppy/mcp_prompts/__init__.py +1 -0
- code_puppy/mcp_prompts/hook_creator.py +103 -0
- code_puppy/messaging/__init__.py +255 -0
- code_puppy/messaging/bus.py +613 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +361 -0
- code_puppy/messaging/messages.py +569 -0
- code_puppy/messaging/queue_console.py +271 -0
- code_puppy/messaging/renderers.py +311 -0
- code_puppy/messaging/rich_renderer.py +1158 -0
- code_puppy/messaging/spinner/__init__.py +83 -0
- code_puppy/messaging/spinner/console_spinner.py +240 -0
- code_puppy/messaging/spinner/spinner_base.py +95 -0
- code_puppy/messaging/subagent_console.py +460 -0
- code_puppy/model_factory.py +848 -0
- code_puppy/model_switching.py +63 -0
- code_puppy/model_utils.py +168 -0
- code_puppy/models.json +174 -0
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +186 -0
- code_puppy/plugins/agent_skills/__init__.py +22 -0
- code_puppy/plugins/agent_skills/config.py +175 -0
- code_puppy/plugins/agent_skills/discovery.py +136 -0
- code_puppy/plugins/agent_skills/downloader.py +392 -0
- code_puppy/plugins/agent_skills/installer.py +22 -0
- code_puppy/plugins/agent_skills/metadata.py +219 -0
- code_puppy/plugins/agent_skills/prompt_builder.py +60 -0
- code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
- code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
- code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
- code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
- code_puppy/plugins/agent_skills/skills_menu.py +781 -0
- code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
- code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +706 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +133 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
- code_puppy/plugins/antigravity_oauth/storage.py +288 -0
- code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
- code_puppy/plugins/antigravity_oauth/token.py +167 -0
- code_puppy/plugins/antigravity_oauth/transport.py +863 -0
- code_puppy/plugins/antigravity_oauth/utils.py +168 -0
- code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
- code_puppy/plugins/chatgpt_oauth/config.py +52 -0
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +329 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +301 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +523 -0
- code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
- code_puppy/plugins/claude_code_hooks/config.py +137 -0
- code_puppy/plugins/claude_code_hooks/register_callbacks.py +175 -0
- code_puppy/plugins/claude_code_oauth/README.md +167 -0
- code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
- code_puppy/plugins/claude_code_oauth/__init__.py +25 -0
- code_puppy/plugins/claude_code_oauth/config.py +52 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
- code_puppy/plugins/claude_code_oauth/utils.py +640 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
- code_puppy/plugins/example_custom_command/README.md +280 -0
- code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
- code_puppy/plugins/file_permission_handler/__init__.py +4 -0
- code_puppy/plugins/file_permission_handler/register_callbacks.py +470 -0
- code_puppy/plugins/frontend_emitter/__init__.py +25 -0
- code_puppy/plugins/frontend_emitter/emitter.py +121 -0
- code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
- code_puppy/plugins/hook_creator/__init__.py +1 -0
- code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
- code_puppy/plugins/hook_manager/__init__.py +1 -0
- code_puppy/plugins/hook_manager/config.py +290 -0
- code_puppy/plugins/hook_manager/hooks_menu.py +564 -0
- code_puppy/plugins/hook_manager/register_callbacks.py +227 -0
- code_puppy/plugins/oauth_puppy_html.py +228 -0
- code_puppy/plugins/scheduler/__init__.py +1 -0
- code_puppy/plugins/scheduler/register_callbacks.py +88 -0
- code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
- code_puppy/plugins/scheduler/scheduler_wizard.py +341 -0
- code_puppy/plugins/shell_safety/__init__.py +6 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
- code_puppy/plugins/shell_safety/command_cache.py +156 -0
- code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
- code_puppy/plugins/synthetic_status/__init__.py +1 -0
- code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
- code_puppy/plugins/synthetic_status/status_api.py +147 -0
- code_puppy/plugins/universal_constructor/__init__.py +13 -0
- code_puppy/plugins/universal_constructor/models.py +138 -0
- code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
- code_puppy/plugins/universal_constructor/registry.py +302 -0
- code_puppy/plugins/universal_constructor/sandbox.py +584 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/pydantic_patches.py +356 -0
- code_puppy/reopenable_async_client.py +232 -0
- code_puppy/round_robin_model.py +150 -0
- code_puppy/scheduler/__init__.py +41 -0
- code_puppy/scheduler/__main__.py +9 -0
- code_puppy/scheduler/cli.py +118 -0
- code_puppy/scheduler/config.py +126 -0
- code_puppy/scheduler/daemon.py +280 -0
- code_puppy/scheduler/executor.py +155 -0
- code_puppy/scheduler/platform.py +19 -0
- code_puppy/scheduler/platform_unix.py +22 -0
- code_puppy/scheduler/platform_win.py +32 -0
- code_puppy/session_storage.py +338 -0
- code_puppy/status_display.py +257 -0
- code_puppy/summarization_agent.py +176 -0
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +501 -0
- code_puppy/tools/agent_tools.py +603 -0
- code_puppy/tools/ask_user_question/__init__.py +26 -0
- code_puppy/tools/ask_user_question/constants.py +73 -0
- code_puppy/tools/ask_user_question/demo_tui.py +55 -0
- code_puppy/tools/ask_user_question/handler.py +232 -0
- code_puppy/tools/ask_user_question/models.py +304 -0
- code_puppy/tools/ask_user_question/registration.py +26 -0
- code_puppy/tools/ask_user_question/renderers.py +309 -0
- code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
- code_puppy/tools/ask_user_question/theme.py +155 -0
- code_puppy/tools/ask_user_question/tui_loop.py +423 -0
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +289 -0
- code_puppy/tools/browser/browser_interactions.py +545 -0
- code_puppy/tools/browser/browser_locators.py +640 -0
- code_puppy/tools/browser/browser_manager.py +378 -0
- code_puppy/tools/browser/browser_navigation.py +251 -0
- code_puppy/tools/browser/browser_screenshot.py +179 -0
- code_puppy/tools/browser/browser_scripts.py +462 -0
- code_puppy/tools/browser/browser_workflows.py +221 -0
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +534 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +1346 -0
- code_puppy/tools/common.py +1409 -0
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +886 -0
- code_puppy/tools/file_operations.py +802 -0
- code_puppy/tools/scheduler_tools.py +412 -0
- code_puppy/tools/skills_tools.py +244 -0
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/tools/tools_content.py +51 -0
- code_puppy/tools/universal_constructor.py +889 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +82 -0
- codepp-0.0.437.dist-info/METADATA +766 -0
- codepp-0.0.437.dist-info/RECORD +288 -0
- codepp-0.0.437.dist-info/WHEEL +4 -0
- codepp-0.0.437.dist-info/entry_points.txt +3 -0
- 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
|
code_puppy/http_utils.py
ADDED
|
@@ -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
|