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,105 @@
|
|
|
1
|
+
# Hook Engine
|
|
2
|
+
|
|
3
|
+
A standalone, testable hook execution system for processing events and executing
|
|
4
|
+
configured hooks with pattern matching, timeout handling, and blocking capabilities.
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
The Hook Engine provides a complete system for implementing event-driven automation
|
|
9
|
+
through configurable hooks. It is compatible with Anthropic's Claude Code
|
|
10
|
+
`.claude/settings.json` format.
|
|
11
|
+
|
|
12
|
+
Features:
|
|
13
|
+
- **Pattern matching** - Wildcards, file extensions, `&&` / `||` compound logic, regex
|
|
14
|
+
- **Event types** - PreToolUse, PostToolUse, SessionStart, Stop, and more
|
|
15
|
+
- **Async execution** - Non-blocking subprocess execution with per-hook timeouts
|
|
16
|
+
- **Claude Code compatible stdin** - JSON payload on stdin, env vars for compatibility
|
|
17
|
+
- **Blocking capability** - Exit code 1 vetoes the tool call
|
|
18
|
+
- **Once-per-session** - Hooks that only run once per session
|
|
19
|
+
- **Comprehensive validation** - Clear error messages for misconfigured hooks
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from code_puppy.hook_engine import HookEngine, EventData
|
|
25
|
+
|
|
26
|
+
config = {
|
|
27
|
+
"PreToolUse": [{
|
|
28
|
+
"matcher": "Bash|agent_run_shell_command",
|
|
29
|
+
"hooks": [{
|
|
30
|
+
"type": "command",
|
|
31
|
+
"command": "bash .claude/hooks/my-check.sh",
|
|
32
|
+
"timeout": 5000
|
|
33
|
+
}]
|
|
34
|
+
}]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
import asyncio
|
|
38
|
+
|
|
39
|
+
engine = HookEngine(config)
|
|
40
|
+
event_data = EventData(
|
|
41
|
+
event_type="PreToolUse",
|
|
42
|
+
tool_name="agent_run_shell_command",
|
|
43
|
+
tool_args={"command": "git status"}
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
async def main():
|
|
47
|
+
result = await engine.process_event("PreToolUse", event_data)
|
|
48
|
+
if result.blocked:
|
|
49
|
+
print(f"Blocked: {result.blocking_reason}")
|
|
50
|
+
|
|
51
|
+
asyncio.run(main())
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Hook Input Format
|
|
55
|
+
|
|
56
|
+
Scripts receive JSON on stdin (Claude Code compatible):
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"session_id": "codepuppy-session",
|
|
61
|
+
"hook_event_name": "PreToolUse",
|
|
62
|
+
"tool_name": "agent_run_shell_command",
|
|
63
|
+
"tool_input": {"command": "git status"},
|
|
64
|
+
"cwd": "/path/to/project",
|
|
65
|
+
"permission_mode": "default"
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Also available as environment variables: `CLAUDE_TOOL_INPUT`, `CLAUDE_TOOL_NAME`,
|
|
70
|
+
`CLAUDE_PROJECT_DIR`, `CLAUDE_HOOK_EVENT`, `CLAUDE_FILE_PATH`.
|
|
71
|
+
|
|
72
|
+
## Exit Codes
|
|
73
|
+
|
|
74
|
+
- `0` - Allow (stdout shown in transcript)
|
|
75
|
+
- `1` - Block (stderr shown as block reason)
|
|
76
|
+
- `2` - Error feedback to Claude without blocking
|
|
77
|
+
|
|
78
|
+
See `docs/HOOKS.md` for the full user-facing guide.
|
|
79
|
+
|
|
80
|
+
## Tool Name Compatibility
|
|
81
|
+
|
|
82
|
+
Hooks can be written using **either** the provider's tool name **or** code_puppy's
|
|
83
|
+
internal tool name — the matcher treats them as equivalent.
|
|
84
|
+
|
|
85
|
+
### Claude Code → code_puppy
|
|
86
|
+
|
|
87
|
+
| Claude Code (`matcher`) | code_puppy internal | Notes |
|
|
88
|
+
|-------------------------|---------------------|-------|
|
|
89
|
+
| `Bash` | `agent_run_shell_command` | Shell execution |
|
|
90
|
+
| `Glob` | `list_files` | File glob / directory listing |
|
|
91
|
+
| `Read` | `read_file` | Read file contents |
|
|
92
|
+
| `Grep` | `grep` | Text search |
|
|
93
|
+
| `Edit` | `replace_in_file` | Patch / partial edit |
|
|
94
|
+
| `Write` | `create_file` | Full-file overwrite |
|
|
95
|
+
| `Delete` | `delete_file` | File deletion |
|
|
96
|
+
| `AskUserQuestion` | `ask_user_question` | Interactive user prompt |
|
|
97
|
+
| `Task` | `invoke_agent` | Sub-agent / task spawn |
|
|
98
|
+
| `Skill` | `activate_skill` | Skill activation |
|
|
99
|
+
| `ToolSearch` | `list_or_search_skills` | Skill/tool discovery |
|
|
100
|
+
|
|
101
|
+
Provider aliases for **Gemini**, **Codex**, and **Swarm** are reserved in
|
|
102
|
+
`aliases.py` and will be populated once their MCP tool vocabularies are verified.
|
|
103
|
+
|
|
104
|
+
Both directions work — `"matcher": "Bash"` and `"matcher": "agent_run_shell_command"`
|
|
105
|
+
are identical at match time.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Hook engine package for Code Puppy."""
|
|
2
|
+
|
|
3
|
+
from . import aliases
|
|
4
|
+
from .engine import HookEngine
|
|
5
|
+
from .models import (
|
|
6
|
+
EventData,
|
|
7
|
+
ExecutionResult,
|
|
8
|
+
HookConfig,
|
|
9
|
+
HookRegistry,
|
|
10
|
+
ProcessEventResult,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"HookEngine",
|
|
15
|
+
"HookConfig",
|
|
16
|
+
"EventData",
|
|
17
|
+
"ExecutionResult",
|
|
18
|
+
"ProcessEventResult",
|
|
19
|
+
"HookRegistry",
|
|
20
|
+
"aliases",
|
|
21
|
+
]
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool name alias registry — maps each AI provider's tool names to code_puppy's
|
|
3
|
+
internal tool names, enabling hooks written for any provider to fire correctly.
|
|
4
|
+
|
|
5
|
+
Structure
|
|
6
|
+
---------
|
|
7
|
+
Each provider block defines a dict[str, str] mapping:
|
|
8
|
+
"<Provider tool name>" -> "<code_puppy internal tool name>"
|
|
9
|
+
|
|
10
|
+
The mapping is bidirectional at lookup time: a hook matcher that names *either*
|
|
11
|
+
the provider tool OR the internal tool will match the same event.
|
|
12
|
+
|
|
13
|
+
Adding a new provider
|
|
14
|
+
---------------------
|
|
15
|
+
1. Add a new section following the pattern below.
|
|
16
|
+
2. Register it in PROVIDER_ALIASES at the bottom of this file.
|
|
17
|
+
3. That's it — the matcher picks it up automatically.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from typing import Dict, FrozenSet, Optional
|
|
21
|
+
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
# Claude Code (Anthropic)
|
|
24
|
+
# Source: `claude mcp serve` → tools/list (verified against v2.1.52)
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
CLAUDE_CODE_ALIASES: Dict[str, str] = {
|
|
27
|
+
# Shell execution
|
|
28
|
+
"Bash": "agent_run_shell_command",
|
|
29
|
+
# File system — read
|
|
30
|
+
"Glob": "list_files",
|
|
31
|
+
"Read": "read_file",
|
|
32
|
+
"Grep": "grep",
|
|
33
|
+
# File system — write
|
|
34
|
+
"Edit": "replace_in_file",
|
|
35
|
+
"Write": "create_file", # Write = full overwrite
|
|
36
|
+
# File system — delete
|
|
37
|
+
"Delete": "delete_file",
|
|
38
|
+
# User interaction
|
|
39
|
+
"AskUserQuestion": "ask_user_question",
|
|
40
|
+
# Agent / task orchestration
|
|
41
|
+
"Task": "invoke_agent",
|
|
42
|
+
# Skills
|
|
43
|
+
"Skill": "activate_skill",
|
|
44
|
+
"ToolSearch": "list_or_search_skills",
|
|
45
|
+
# NOTE: the tools below have no direct code_puppy equivalent yet.
|
|
46
|
+
# They are listed here for documentation and future mapping:
|
|
47
|
+
# "TaskOutput" -> (no equivalent)
|
|
48
|
+
# "TaskStop" -> (no equivalent)
|
|
49
|
+
# "WebFetch" -> (no equivalent — see browser_navigate)
|
|
50
|
+
# "WebSearch" -> (no equivalent)
|
|
51
|
+
# "NotebookEdit" -> (no equivalent)
|
|
52
|
+
# "TodoWrite" -> (no equivalent)
|
|
53
|
+
# "EnterPlanMode" -> (no equivalent)
|
|
54
|
+
# "ExitPlanMode" -> (no equivalent)
|
|
55
|
+
# "EnterWorktree" -> (no equivalent)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
# Gemini (Google)
|
|
61
|
+
# TODO: populate once Gemini MCP tool names are verified.
|
|
62
|
+
# Run `gemini mcp serve` (or equivalent) and inspect the tools/list response,
|
|
63
|
+
# then add entries following the same pattern as CLAUDE_CODE_ALIASES above.
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
GEMINI_ALIASES: Dict[str, str] = {
|
|
66
|
+
# Add Gemini → code_puppy tool mappings here
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
# Codex (OpenAI)
|
|
72
|
+
# TODO: populate once Codex MCP tool names are verified.
|
|
73
|
+
# Run the Codex MCP server, inspect tools/list, and add entries here.
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
CODEX_ALIASES: Dict[str, str] = {
|
|
76
|
+
# Add Codex → code_puppy tool mappings here
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
# Swarm (internal / multi-agent)
|
|
82
|
+
# TODO: populate if Swarm exposes its own canonical tool name vocabulary.
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
SWARM_ALIASES: Dict[str, str] = {
|
|
85
|
+
# Add Swarm → code_puppy tool mappings here
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
# Master registry — all active alias tables, merged at module load time.
|
|
91
|
+
# To disable a provider's aliases, remove its entry from this dict.
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
PROVIDER_ALIASES: Dict[str, Dict[str, str]] = {
|
|
94
|
+
"claude": CLAUDE_CODE_ALIASES,
|
|
95
|
+
"gemini": GEMINI_ALIASES, # placeholder — empty until populated
|
|
96
|
+
"codex": CODEX_ALIASES, # placeholder — empty until populated
|
|
97
|
+
"swarm": SWARM_ALIASES, # placeholder — empty until populated
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
# Flattened lookup structures — built once at import time for O(1) access.
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _build_lookup() -> Dict[str, FrozenSet[str]]:
|
|
107
|
+
"""
|
|
108
|
+
Return a dict mapping every known name (provider *and* internal) to the
|
|
109
|
+
full set of equivalent names, including itself.
|
|
110
|
+
|
|
111
|
+
Example result entry:
|
|
112
|
+
"Bash" -> frozenset({"Bash", "agent_run_shell_command"})
|
|
113
|
+
"agent_run_shell_command" -> frozenset({"Bash", "agent_run_shell_command"})
|
|
114
|
+
"""
|
|
115
|
+
groups: Dict[str, set] = {}
|
|
116
|
+
|
|
117
|
+
for provider_aliases in PROVIDER_ALIASES.values():
|
|
118
|
+
for provider_name, internal_name in provider_aliases.items():
|
|
119
|
+
# Collect all names that map to the same internal tool
|
|
120
|
+
key = internal_name.lower()
|
|
121
|
+
if key not in groups:
|
|
122
|
+
groups[key] = {internal_name}
|
|
123
|
+
groups[key].add(provider_name)
|
|
124
|
+
|
|
125
|
+
# Build the final lookup: every alias points to the frozen group
|
|
126
|
+
lookup: Dict[str, FrozenSet[str]] = {}
|
|
127
|
+
for group in groups.values():
|
|
128
|
+
frozen = frozenset(group)
|
|
129
|
+
for name in group:
|
|
130
|
+
lookup[name.lower()] = frozen
|
|
131
|
+
return lookup
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# Module-level singleton — import this in matcher.py
|
|
135
|
+
ALIAS_LOOKUP: Dict[str, FrozenSet[str]] = _build_lookup()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def get_aliases(tool_name: str) -> FrozenSet[str]:
|
|
139
|
+
"""
|
|
140
|
+
Return all known equivalent names for *tool_name* (including itself).
|
|
141
|
+
Returns a frozenset containing only *tool_name* when no aliases exist.
|
|
142
|
+
"""
|
|
143
|
+
return ALIAS_LOOKUP.get(tool_name.lower(), frozenset({tool_name}))
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def resolve_internal_name(provider_tool_name: str) -> Optional[str]:
|
|
147
|
+
"""
|
|
148
|
+
Return the code_puppy internal tool name for a given provider tool name,
|
|
149
|
+
or None if the name is not a known provider alias.
|
|
150
|
+
"""
|
|
151
|
+
for provider_aliases in PROVIDER_ALIASES.values():
|
|
152
|
+
for pname, internal in provider_aliases.items():
|
|
153
|
+
if pname.lower() == provider_tool_name.lower():
|
|
154
|
+
return internal
|
|
155
|
+
return None
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main HookEngine orchestration class.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
from .executor import execute_hooks_sequential, get_blocking_result
|
|
10
|
+
from .matcher import matches
|
|
11
|
+
from .models import (
|
|
12
|
+
EventData,
|
|
13
|
+
HookConfig,
|
|
14
|
+
HookRegistry,
|
|
15
|
+
ProcessEventResult,
|
|
16
|
+
)
|
|
17
|
+
from .registry import build_registry_from_config, get_registry_stats
|
|
18
|
+
from .validator import (
|
|
19
|
+
format_validation_report,
|
|
20
|
+
get_config_suggestions,
|
|
21
|
+
validate_hooks_config,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class HookEngine:
|
|
28
|
+
"""
|
|
29
|
+
Main hook engine for processing events and executing hooks.
|
|
30
|
+
|
|
31
|
+
Coordinates all hook engine components:
|
|
32
|
+
- Loads and validates configuration
|
|
33
|
+
- Matches events against hook patterns
|
|
34
|
+
- Executes hooks with timeout and error handling
|
|
35
|
+
- Aggregates results and determines blocking status
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
config: Optional[Dict[str, Any]] = None,
|
|
41
|
+
strict_validation: bool = True,
|
|
42
|
+
env_vars: Optional[Dict[str, str]] = None,
|
|
43
|
+
):
|
|
44
|
+
self.env_vars = env_vars or {}
|
|
45
|
+
self.strict_validation = strict_validation
|
|
46
|
+
self._registry: Optional[HookRegistry] = None
|
|
47
|
+
|
|
48
|
+
if config:
|
|
49
|
+
self.load_config(config)
|
|
50
|
+
else:
|
|
51
|
+
self._registry = HookRegistry()
|
|
52
|
+
|
|
53
|
+
def load_config(self, config: Dict[str, Any]) -> None:
|
|
54
|
+
is_valid, errors = validate_hooks_config(config)
|
|
55
|
+
|
|
56
|
+
if not is_valid:
|
|
57
|
+
error_msg = format_validation_report(
|
|
58
|
+
is_valid, errors, get_config_suggestions(config, errors)
|
|
59
|
+
)
|
|
60
|
+
if self.strict_validation:
|
|
61
|
+
raise ValueError(f"Invalid hook configuration:\n{error_msg}")
|
|
62
|
+
else:
|
|
63
|
+
logger.warning(f"Hook configuration has errors:\n{error_msg}")
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
self._registry = build_registry_from_config(config)
|
|
67
|
+
logger.info(
|
|
68
|
+
f"Loaded hook configuration: {self._registry.count_hooks()} total hooks"
|
|
69
|
+
)
|
|
70
|
+
except Exception as e:
|
|
71
|
+
if self.strict_validation:
|
|
72
|
+
raise ValueError(f"Failed to build hook registry: {e}") from e
|
|
73
|
+
else:
|
|
74
|
+
logger.error(f"Failed to build hook registry: {e}", exc_info=True)
|
|
75
|
+
self._registry = HookRegistry()
|
|
76
|
+
|
|
77
|
+
def reload_config(self, config: Dict[str, Any]) -> None:
|
|
78
|
+
self.load_config(config)
|
|
79
|
+
|
|
80
|
+
async def process_event(
|
|
81
|
+
self,
|
|
82
|
+
event_type: str,
|
|
83
|
+
event_data: EventData,
|
|
84
|
+
sequential: bool = True,
|
|
85
|
+
stop_on_block: bool = True,
|
|
86
|
+
) -> ProcessEventResult:
|
|
87
|
+
"""Process an event through the hook engine."""
|
|
88
|
+
start_time = time.perf_counter()
|
|
89
|
+
|
|
90
|
+
if not self._registry:
|
|
91
|
+
return ProcessEventResult(
|
|
92
|
+
blocked=False, executed_hooks=0, results=[], total_duration_ms=0.0
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
all_hooks = self._registry.get_hooks_for_event(event_type)
|
|
96
|
+
|
|
97
|
+
if not all_hooks:
|
|
98
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
99
|
+
return ProcessEventResult(
|
|
100
|
+
blocked=False,
|
|
101
|
+
executed_hooks=0,
|
|
102
|
+
results=[],
|
|
103
|
+
total_duration_ms=duration_ms,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
matching_hooks = self._filter_hooks_by_matcher(
|
|
107
|
+
all_hooks, event_data.tool_name, event_data.tool_args
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if not matching_hooks:
|
|
111
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
112
|
+
return ProcessEventResult(
|
|
113
|
+
blocked=False,
|
|
114
|
+
executed_hooks=0,
|
|
115
|
+
results=[],
|
|
116
|
+
total_duration_ms=duration_ms,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
logger.debug(
|
|
120
|
+
f"Processing {event_type}: {len(matching_hooks)} matching hook(s) for tool '{event_data.tool_name}'"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if sequential:
|
|
124
|
+
results = await execute_hooks_sequential(
|
|
125
|
+
matching_hooks, event_data, self.env_vars, stop_on_block=stop_on_block
|
|
126
|
+
)
|
|
127
|
+
else:
|
|
128
|
+
from .executor import execute_hooks_parallel
|
|
129
|
+
|
|
130
|
+
results = await execute_hooks_parallel(
|
|
131
|
+
matching_hooks, event_data, self.env_vars
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
for hook, result in zip(matching_hooks, results):
|
|
135
|
+
if hook.once and result.success:
|
|
136
|
+
self._registry.mark_hook_executed(hook.id)
|
|
137
|
+
|
|
138
|
+
blocking_result = get_blocking_result(results)
|
|
139
|
+
blocked = blocking_result is not None
|
|
140
|
+
blocking_reason = None
|
|
141
|
+
|
|
142
|
+
if blocked:
|
|
143
|
+
blocking_reason = (
|
|
144
|
+
f"Hook '{blocking_result.hook_command}' failed: "
|
|
145
|
+
f"{blocking_result.error or blocking_result.stderr or 'blocked (no details provided)'}"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
149
|
+
return ProcessEventResult(
|
|
150
|
+
blocked=blocked,
|
|
151
|
+
executed_hooks=len(results),
|
|
152
|
+
results=results,
|
|
153
|
+
blocking_reason=blocking_reason,
|
|
154
|
+
total_duration_ms=duration_ms,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def _filter_hooks_by_matcher(
|
|
158
|
+
self,
|
|
159
|
+
hooks: List[HookConfig],
|
|
160
|
+
tool_name: str,
|
|
161
|
+
tool_args: Dict[str, Any],
|
|
162
|
+
) -> List[HookConfig]:
|
|
163
|
+
matching_hooks = []
|
|
164
|
+
for hook in hooks:
|
|
165
|
+
try:
|
|
166
|
+
if matches(hook.matcher, tool_name, tool_args):
|
|
167
|
+
matching_hooks.append(hook)
|
|
168
|
+
except Exception as e:
|
|
169
|
+
logger.error(
|
|
170
|
+
f"Error matching hook '{hook.matcher}': {e}", exc_info=True
|
|
171
|
+
)
|
|
172
|
+
return matching_hooks
|
|
173
|
+
|
|
174
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
175
|
+
if not self._registry:
|
|
176
|
+
return {"total_hooks": 0, "error": "No registry loaded"}
|
|
177
|
+
return get_registry_stats(self._registry)
|
|
178
|
+
|
|
179
|
+
def get_hooks_for_event(self, event_type: str) -> List[HookConfig]:
|
|
180
|
+
if not self._registry:
|
|
181
|
+
return []
|
|
182
|
+
return self._registry.get_hooks_for_event(event_type)
|
|
183
|
+
|
|
184
|
+
def count_hooks(self, event_type: Optional[str] = None) -> int:
|
|
185
|
+
if not self._registry:
|
|
186
|
+
return 0
|
|
187
|
+
return self._registry.count_hooks(event_type)
|
|
188
|
+
|
|
189
|
+
def reset_once_hooks(self) -> None:
|
|
190
|
+
if self._registry:
|
|
191
|
+
self._registry.reset_once_hooks()
|
|
192
|
+
|
|
193
|
+
def add_hook(self, event_type: str, hook: HookConfig) -> None:
|
|
194
|
+
if not self._registry:
|
|
195
|
+
self._registry = HookRegistry()
|
|
196
|
+
self._registry.add_hook(event_type, hook)
|
|
197
|
+
|
|
198
|
+
def remove_hook(self, event_type: str, hook_id: str) -> bool:
|
|
199
|
+
if not self._registry:
|
|
200
|
+
return False
|
|
201
|
+
return self._registry.remove_hook(event_type, hook_id)
|
|
202
|
+
|
|
203
|
+
def set_env_vars(self, env_vars: Dict[str, str]) -> None:
|
|
204
|
+
self.env_vars = env_vars
|
|
205
|
+
|
|
206
|
+
def update_env_vars(self, env_vars: Dict[str, str]) -> None:
|
|
207
|
+
self.env_vars.update(env_vars)
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def is_loaded(self) -> bool:
|
|
211
|
+
return self._registry is not None
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def registry(self) -> Optional[HookRegistry]:
|
|
215
|
+
return self._registry
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def validate_config_file(config: Dict[str, Any]) -> str:
|
|
219
|
+
is_valid, errors = validate_hooks_config(config)
|
|
220
|
+
suggestions = get_config_suggestions(config, errors) if not is_valid else []
|
|
221
|
+
return format_validation_report(is_valid, errors, suggestions)
|