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,261 @@
|
|
|
1
|
+
"""Callback registration for frontend event emission.
|
|
2
|
+
|
|
3
|
+
This module registers callbacks for various agent events and emits them
|
|
4
|
+
to subscribed WebSocket handlers via the emitter module.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import time
|
|
9
|
+
from typing import Any, Dict, Optional
|
|
10
|
+
|
|
11
|
+
from code_puppy.callbacks import register_callback
|
|
12
|
+
from code_puppy.plugins.frontend_emitter.emitter import emit_event
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def on_pre_tool_call(
|
|
18
|
+
tool_name: str, tool_args: Dict[str, Any], context: Any = None
|
|
19
|
+
) -> None:
|
|
20
|
+
"""Emit an event when a tool call starts.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
tool_name: Name of the tool being called
|
|
24
|
+
tool_args: Arguments being passed to the tool
|
|
25
|
+
context: Optional context data for the tool call
|
|
26
|
+
"""
|
|
27
|
+
try:
|
|
28
|
+
emit_event(
|
|
29
|
+
"tool_call_start",
|
|
30
|
+
{
|
|
31
|
+
"tool_name": tool_name,
|
|
32
|
+
"tool_args": _sanitize_args(tool_args),
|
|
33
|
+
"start_time": time.time(),
|
|
34
|
+
},
|
|
35
|
+
)
|
|
36
|
+
logger.debug(f"Emitted tool_call_start for {tool_name}")
|
|
37
|
+
except Exception as e:
|
|
38
|
+
logger.error(f"Failed to emit pre_tool_call event: {e}")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def on_post_tool_call(
|
|
42
|
+
tool_name: str,
|
|
43
|
+
tool_args: Dict[str, Any],
|
|
44
|
+
result: Any,
|
|
45
|
+
duration_ms: float,
|
|
46
|
+
context: Any = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Emit an event when a tool call completes.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
tool_name: Name of the tool that was called
|
|
52
|
+
tool_args: Arguments that were passed to the tool
|
|
53
|
+
result: The result returned by the tool
|
|
54
|
+
duration_ms: Execution time in milliseconds
|
|
55
|
+
context: Optional context data for the tool call
|
|
56
|
+
"""
|
|
57
|
+
try:
|
|
58
|
+
emit_event(
|
|
59
|
+
"tool_call_complete",
|
|
60
|
+
{
|
|
61
|
+
"tool_name": tool_name,
|
|
62
|
+
"tool_args": _sanitize_args(tool_args),
|
|
63
|
+
"duration_ms": duration_ms,
|
|
64
|
+
"success": _is_successful_result(result),
|
|
65
|
+
"result_summary": _summarize_result(result),
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
logger.debug(
|
|
69
|
+
f"Emitted tool_call_complete for {tool_name} ({duration_ms:.2f}ms)"
|
|
70
|
+
)
|
|
71
|
+
except Exception as e:
|
|
72
|
+
logger.error(f"Failed to emit post_tool_call event: {e}")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def on_stream_event(
|
|
76
|
+
event_type: str, event_data: Any, agent_session_id: Optional[str] = None
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Emit streaming events from the agent.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
event_type: Type of the streaming event
|
|
82
|
+
event_data: Data associated with the event
|
|
83
|
+
agent_session_id: Optional session ID of the agent emitting the event
|
|
84
|
+
"""
|
|
85
|
+
try:
|
|
86
|
+
emit_event(
|
|
87
|
+
"stream_event",
|
|
88
|
+
{
|
|
89
|
+
"event_type": event_type,
|
|
90
|
+
"event_data": _sanitize_event_data(event_data),
|
|
91
|
+
"agent_session_id": agent_session_id,
|
|
92
|
+
},
|
|
93
|
+
)
|
|
94
|
+
logger.debug(f"Emitted stream_event: {event_type}")
|
|
95
|
+
except Exception as e:
|
|
96
|
+
logger.error(f"Failed to emit stream_event: {e}")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def on_invoke_agent(*args: Any, **kwargs: Any) -> None:
|
|
100
|
+
"""Emit an event when an agent is invoked.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
*args: Positional arguments from the invoke_agent callback
|
|
104
|
+
**kwargs: Keyword arguments from the invoke_agent callback
|
|
105
|
+
"""
|
|
106
|
+
try:
|
|
107
|
+
# Extract relevant info from args/kwargs
|
|
108
|
+
agent_info = {
|
|
109
|
+
"agent_name": kwargs.get("agent_name") or (args[0] if args else None),
|
|
110
|
+
"session_id": kwargs.get("session_id"),
|
|
111
|
+
"prompt_preview": _truncate_string(
|
|
112
|
+
kwargs.get("prompt") or (args[1] if len(args) > 1 else None),
|
|
113
|
+
max_length=200,
|
|
114
|
+
),
|
|
115
|
+
}
|
|
116
|
+
emit_event("agent_invoked", agent_info)
|
|
117
|
+
logger.debug(f"Emitted agent_invoked: {agent_info.get('agent_name')}")
|
|
118
|
+
except Exception as e:
|
|
119
|
+
logger.error(f"Failed to emit invoke_agent event: {e}")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _sanitize_args(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
123
|
+
"""Sanitize tool arguments for safe emission.
|
|
124
|
+
|
|
125
|
+
Truncates large values and removes potentially sensitive data.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
args: The raw tool arguments
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Sanitized arguments safe for emission
|
|
132
|
+
"""
|
|
133
|
+
if not isinstance(args, dict):
|
|
134
|
+
return {}
|
|
135
|
+
|
|
136
|
+
sanitized: Dict[str, Any] = {}
|
|
137
|
+
for key, value in args.items():
|
|
138
|
+
if isinstance(value, str):
|
|
139
|
+
sanitized[key] = _truncate_string(value, max_length=500)
|
|
140
|
+
elif isinstance(value, (int, float, bool, type(None))):
|
|
141
|
+
sanitized[key] = value
|
|
142
|
+
elif isinstance(value, (list, dict)):
|
|
143
|
+
# Just indicate the type and length for complex types
|
|
144
|
+
sanitized[key] = f"<{type(value).__name__}[{len(value)}]>"
|
|
145
|
+
else:
|
|
146
|
+
sanitized[key] = f"<{type(value).__name__}>"
|
|
147
|
+
|
|
148
|
+
return sanitized
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _sanitize_event_data(data: Any) -> Any:
|
|
152
|
+
"""Sanitize event data for safe emission.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
data: The raw event data
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Sanitized data safe for emission
|
|
159
|
+
"""
|
|
160
|
+
if data is None:
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
if isinstance(data, str):
|
|
164
|
+
return _truncate_string(data, max_length=1000)
|
|
165
|
+
|
|
166
|
+
if isinstance(data, (int, float, bool)):
|
|
167
|
+
return data
|
|
168
|
+
|
|
169
|
+
if isinstance(data, dict):
|
|
170
|
+
return {k: _sanitize_event_data(v) for k, v in list(data.items())[:20]}
|
|
171
|
+
|
|
172
|
+
if isinstance(data, (list, tuple)):
|
|
173
|
+
return [_sanitize_event_data(item) for item in data[:20]]
|
|
174
|
+
|
|
175
|
+
return f"<{type(data).__name__}>"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _is_successful_result(result: Any) -> bool:
|
|
179
|
+
"""Determine if a tool result indicates success.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
result: The tool result
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
True if the result appears successful
|
|
186
|
+
"""
|
|
187
|
+
if result is None:
|
|
188
|
+
return True # No result often means success
|
|
189
|
+
|
|
190
|
+
if isinstance(result, dict):
|
|
191
|
+
# Check for error indicators
|
|
192
|
+
if result.get("error"):
|
|
193
|
+
return False
|
|
194
|
+
if result.get("success") is False:
|
|
195
|
+
return False
|
|
196
|
+
return True
|
|
197
|
+
|
|
198
|
+
if isinstance(result, bool):
|
|
199
|
+
return result
|
|
200
|
+
|
|
201
|
+
return True # Default to success
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _summarize_result(result: Any) -> str:
|
|
205
|
+
"""Create a brief summary of a tool result.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
result: The tool result
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
A string summary of the result
|
|
212
|
+
"""
|
|
213
|
+
if result is None:
|
|
214
|
+
return "<no result>"
|
|
215
|
+
|
|
216
|
+
if isinstance(result, str):
|
|
217
|
+
return _truncate_string(result, max_length=200)
|
|
218
|
+
|
|
219
|
+
if isinstance(result, dict):
|
|
220
|
+
if "error" in result:
|
|
221
|
+
return f"Error: {_truncate_string(str(result['error']), max_length=100)}"
|
|
222
|
+
if "message" in result:
|
|
223
|
+
return _truncate_string(str(result["message"]), max_length=100)
|
|
224
|
+
return f"<dict with {len(result)} keys>"
|
|
225
|
+
|
|
226
|
+
if isinstance(result, (list, tuple)):
|
|
227
|
+
return f"<{type(result).__name__}[{len(result)}]>"
|
|
228
|
+
|
|
229
|
+
return _truncate_string(str(result), max_length=200)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _truncate_string(value: Any, max_length: int = 100) -> Optional[str]:
|
|
233
|
+
"""Truncate a string value if it exceeds max_length.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
value: The value to truncate (will be converted to str)
|
|
237
|
+
max_length: Maximum length before truncation
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Truncated string or None if value is None
|
|
241
|
+
"""
|
|
242
|
+
if value is None:
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
s = str(value)
|
|
246
|
+
if len(s) > max_length:
|
|
247
|
+
return s[: max_length - 3] + "..."
|
|
248
|
+
return s
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def register() -> None:
|
|
252
|
+
"""Register all frontend emitter callbacks."""
|
|
253
|
+
register_callback("pre_tool_call", on_pre_tool_call)
|
|
254
|
+
register_callback("post_tool_call", on_post_tool_call)
|
|
255
|
+
register_callback("stream_event", on_stream_event)
|
|
256
|
+
register_callback("invoke_agent", on_invoke_agent)
|
|
257
|
+
logger.debug("Frontend emitter callbacks registered")
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
# Auto-register callbacks when this module is imported
|
|
261
|
+
register()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Hook Creator Plugin
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hook Creator Plugin - Simple command that injects MCP prompt
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from code_puppy.callbacks import register_callback
|
|
6
|
+
from code_puppy.mcp_prompts.hook_creator import HOOK_CREATION_PROMPT
|
|
7
|
+
from code_puppy.messaging import emit_info
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _custom_help():
|
|
11
|
+
"""Help entries for create-hook commands."""
|
|
12
|
+
return [
|
|
13
|
+
("create-hook", "Get help creating Code Puppy hooks"),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _handle_custom_command(command: str, name: str):
|
|
18
|
+
"""Handle /create-hook command.
|
|
19
|
+
|
|
20
|
+
Displays hook creation documentation and sends to model with context.
|
|
21
|
+
"""
|
|
22
|
+
if name != "create-hook":
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
emit_info(HOOK_CREATION_PROMPT)
|
|
26
|
+
|
|
27
|
+
# Send the prompt to the model with the hook docs as context
|
|
28
|
+
return "I need help creating a hook for Code Puppy. Here's the documentation above. Can you help me?"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Register the custom command
|
|
32
|
+
register_callback("custom_command_help", _custom_help)
|
|
33
|
+
register_callback("custom_command", _handle_custom_command)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Hook Manager plugin – interactive TUI for managing Claude Code hooks."""
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Helpers for reading and writing hook configurations from both global and project sources.
|
|
3
|
+
|
|
4
|
+
Supports:
|
|
5
|
+
- Global hooks: ~/.code_puppy/hooks.json
|
|
6
|
+
- Project hooks: .claude/settings.json
|
|
7
|
+
|
|
8
|
+
Hooks from both sources are loaded and can be managed independently in the TUI.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import copy
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Dict, List, Literal, Optional
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
_SETTINGS_FILENAME = ".claude/settings.json"
|
|
21
|
+
_GLOBAL_HOOKS_FILE = os.path.expanduser("~/.code_puppy/hooks.json")
|
|
22
|
+
|
|
23
|
+
HookSource = Literal["project", "global"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _find_settings_path() -> Path:
|
|
27
|
+
"""Return the path to .claude/settings.json, searching from cwd upward."""
|
|
28
|
+
cwd = Path.cwd()
|
|
29
|
+
for parent in [cwd, *cwd.parents]:
|
|
30
|
+
candidate = parent / _SETTINGS_FILENAME
|
|
31
|
+
if candidate.exists():
|
|
32
|
+
return candidate
|
|
33
|
+
return cwd / _SETTINGS_FILENAME
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _load_global_hooks_config() -> Dict[str, Any]:
|
|
37
|
+
"""Load hooks from ~/.code_puppy/hooks.json."""
|
|
38
|
+
path = Path(_GLOBAL_HOOKS_FILE)
|
|
39
|
+
if not path.exists():
|
|
40
|
+
return {}
|
|
41
|
+
try:
|
|
42
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
43
|
+
# Handle both wrapped {"hooks": {...}} and direct format
|
|
44
|
+
if "hooks" in data and isinstance(data["hooks"], dict):
|
|
45
|
+
return data.get("hooks", {})
|
|
46
|
+
return data
|
|
47
|
+
except Exception as exc:
|
|
48
|
+
logger.warning("Failed to parse global hooks from %s: %s", path, exc)
|
|
49
|
+
return {}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _load_project_hooks_config() -> Dict[str, Any]:
|
|
53
|
+
"""Load hooks from .claude/settings.json."""
|
|
54
|
+
path = _find_settings_path()
|
|
55
|
+
if not path.exists():
|
|
56
|
+
return {}
|
|
57
|
+
try:
|
|
58
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
59
|
+
return data.get("hooks", {})
|
|
60
|
+
except Exception as exc:
|
|
61
|
+
logger.warning("Failed to parse project hooks from %s: %s", path, exc)
|
|
62
|
+
return {}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def load_hooks_config() -> Dict[str, Any]:
|
|
66
|
+
"""Load raw hooks config from .claude/settings.json (project only).
|
|
67
|
+
|
|
68
|
+
Returns the value of the top-level "hooks" key, or {} if absent/unreadable.
|
|
69
|
+
Note: For the TUI, we load hooks from both sources separately.
|
|
70
|
+
"""
|
|
71
|
+
return _load_project_hooks_config()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def load_all_hooks_config() -> Dict[str, Any]:
|
|
75
|
+
"""Load and merge hooks from both global and project sources.
|
|
76
|
+
|
|
77
|
+
Returns a merged configuration with all hooks.
|
|
78
|
+
"""
|
|
79
|
+
global_hooks = _load_global_hooks_config()
|
|
80
|
+
project_hooks = _load_project_hooks_config()
|
|
81
|
+
|
|
82
|
+
# Simple merge: combine hook groups
|
|
83
|
+
merged = {}
|
|
84
|
+
for event_type in set(list(global_hooks.keys()) + list(project_hooks.keys())):
|
|
85
|
+
if event_type.startswith("_"):
|
|
86
|
+
# Skip comment keys
|
|
87
|
+
merged[event_type] = project_hooks.get(event_type) or global_hooks.get(
|
|
88
|
+
event_type
|
|
89
|
+
)
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
global_groups = (
|
|
93
|
+
global_hooks.get(event_type, [])
|
|
94
|
+
if isinstance(global_hooks.get(event_type), list)
|
|
95
|
+
else []
|
|
96
|
+
)
|
|
97
|
+
project_groups = (
|
|
98
|
+
project_hooks.get(event_type, [])
|
|
99
|
+
if isinstance(project_hooks.get(event_type), list)
|
|
100
|
+
else []
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if global_groups or project_groups:
|
|
104
|
+
merged[event_type] = global_groups + project_groups
|
|
105
|
+
|
|
106
|
+
return merged
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def save_hooks_config(hooks: Dict[str, Any]) -> Path:
|
|
110
|
+
"""Persist hooks config back to .claude/settings.json.
|
|
111
|
+
|
|
112
|
+
Performs a read-modify-write so other top-level keys are preserved.
|
|
113
|
+
Returns the path written.
|
|
114
|
+
"""
|
|
115
|
+
path = _find_settings_path()
|
|
116
|
+
existing: Dict[str, Any] = {}
|
|
117
|
+
if path.exists():
|
|
118
|
+
try:
|
|
119
|
+
existing = json.loads(path.read_text(encoding="utf-8"))
|
|
120
|
+
except Exception:
|
|
121
|
+
existing = {}
|
|
122
|
+
existing["hooks"] = hooks
|
|
123
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
124
|
+
path.write_text(json.dumps(existing, indent=2) + "\n", encoding="utf-8")
|
|
125
|
+
logger.debug("Saved hooks config to %s", path)
|
|
126
|
+
return path
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def save_global_hooks_config(hooks: Dict[str, Any]) -> Path:
|
|
130
|
+
"""Persist hooks config to ~/.code_puppy/hooks.json.
|
|
131
|
+
|
|
132
|
+
Returns the path written.
|
|
133
|
+
"""
|
|
134
|
+
path = Path(_GLOBAL_HOOKS_FILE)
|
|
135
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
136
|
+
path.write_text(json.dumps(hooks, indent=2) + "\n", encoding="utf-8")
|
|
137
|
+
logger.debug("Saved global hooks config to %s", path)
|
|
138
|
+
return path
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class HookEntry:
|
|
142
|
+
"""Flat, mutable representation of a single hook for the TUI."""
|
|
143
|
+
|
|
144
|
+
__slots__ = (
|
|
145
|
+
"event_type",
|
|
146
|
+
"matcher",
|
|
147
|
+
"hook_type",
|
|
148
|
+
"command",
|
|
149
|
+
"timeout",
|
|
150
|
+
"enabled",
|
|
151
|
+
"hook_id",
|
|
152
|
+
"source",
|
|
153
|
+
"_group_index",
|
|
154
|
+
"_hook_index",
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def __init__(
|
|
158
|
+
self,
|
|
159
|
+
event_type: str,
|
|
160
|
+
matcher: str,
|
|
161
|
+
hook_type: str,
|
|
162
|
+
command: str,
|
|
163
|
+
timeout: int = 5000,
|
|
164
|
+
enabled: bool = True,
|
|
165
|
+
hook_id: Optional[str] = None,
|
|
166
|
+
source: HookSource = "project",
|
|
167
|
+
group_index: int = 0,
|
|
168
|
+
hook_index: int = 0,
|
|
169
|
+
) -> None:
|
|
170
|
+
self.event_type = event_type
|
|
171
|
+
self.matcher = matcher
|
|
172
|
+
self.hook_type = hook_type
|
|
173
|
+
self.command = command
|
|
174
|
+
self.timeout = timeout
|
|
175
|
+
self.enabled = enabled
|
|
176
|
+
self.hook_id = hook_id
|
|
177
|
+
self.source = source
|
|
178
|
+
self._group_index = group_index
|
|
179
|
+
self._hook_index = hook_index
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def display_command(self) -> str:
|
|
183
|
+
"""Command truncated to 60 chars for list display."""
|
|
184
|
+
cmd = self.command
|
|
185
|
+
return cmd[:57] + "..." if len(cmd) > 60 else cmd
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def display_matcher(self) -> str:
|
|
189
|
+
"""Matcher truncated to 40 chars."""
|
|
190
|
+
m = self.matcher
|
|
191
|
+
return m[:37] + "..." if len(m) > 40 else m
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def flatten_hooks(
|
|
195
|
+
hooks_config: Dict[str, Any], source: HookSource = "project"
|
|
196
|
+
) -> List[HookEntry]:
|
|
197
|
+
"""Convert nested hooks config into a flat list of HookEntry objects.
|
|
198
|
+
|
|
199
|
+
Each entry remembers its group_index and hook_index for round-trip
|
|
200
|
+
serialisation, and which source it came from.
|
|
201
|
+
"""
|
|
202
|
+
entries: List[HookEntry] = []
|
|
203
|
+
for event_type, groups in hooks_config.items():
|
|
204
|
+
if event_type.startswith("_"):
|
|
205
|
+
# Skip comment keys
|
|
206
|
+
continue
|
|
207
|
+
if not isinstance(groups, list):
|
|
208
|
+
continue
|
|
209
|
+
for g_idx, group in enumerate(groups):
|
|
210
|
+
if not isinstance(group, dict):
|
|
211
|
+
continue
|
|
212
|
+
matcher = group.get("matcher", "*")
|
|
213
|
+
for h_idx, hook in enumerate(group.get("hooks", [])):
|
|
214
|
+
if not isinstance(hook, dict):
|
|
215
|
+
continue
|
|
216
|
+
command = hook.get("command") or hook.get("prompt", "")
|
|
217
|
+
entries.append(
|
|
218
|
+
HookEntry(
|
|
219
|
+
event_type=event_type,
|
|
220
|
+
matcher=matcher,
|
|
221
|
+
hook_type=hook.get("type", "command"),
|
|
222
|
+
command=command,
|
|
223
|
+
timeout=hook.get("timeout", 5000),
|
|
224
|
+
enabled=hook.get("enabled", True),
|
|
225
|
+
hook_id=hook.get("id"),
|
|
226
|
+
source=source,
|
|
227
|
+
group_index=g_idx,
|
|
228
|
+
hook_index=h_idx,
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
return entries
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def flatten_all_hooks() -> List[HookEntry]:
|
|
235
|
+
"""Load and flatten hooks from both global and project sources.
|
|
236
|
+
|
|
237
|
+
Returns a combined list with source information for each hook.
|
|
238
|
+
"""
|
|
239
|
+
global_config = _load_global_hooks_config()
|
|
240
|
+
project_config = _load_project_hooks_config()
|
|
241
|
+
|
|
242
|
+
global_entries = flatten_hooks(global_config, source="global")
|
|
243
|
+
project_entries = flatten_hooks(project_config, source="project")
|
|
244
|
+
|
|
245
|
+
# Project hooks first for easier viewing
|
|
246
|
+
return project_entries + global_entries
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def toggle_hook_enabled(
|
|
250
|
+
hooks_config: Dict[str, Any],
|
|
251
|
+
event_type: str,
|
|
252
|
+
group_index: int,
|
|
253
|
+
hook_index: int,
|
|
254
|
+
enabled: bool,
|
|
255
|
+
) -> Dict[str, Any]:
|
|
256
|
+
"""Return a deep copy of hooks_config with the specified hook toggled.
|
|
257
|
+
|
|
258
|
+
Does NOT write to disk – call save_hooks_config() afterwards.
|
|
259
|
+
"""
|
|
260
|
+
cfg = copy.deepcopy(hooks_config)
|
|
261
|
+
try:
|
|
262
|
+
hook = cfg[event_type][group_index]["hooks"][hook_index]
|
|
263
|
+
hook["enabled"] = enabled
|
|
264
|
+
except (KeyError, IndexError, TypeError) as exc:
|
|
265
|
+
logger.warning("toggle_hook_enabled: cannot find hook (%s)", exc)
|
|
266
|
+
return cfg
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def delete_hook(
|
|
270
|
+
hooks_config: Dict[str, Any],
|
|
271
|
+
event_type: str,
|
|
272
|
+
group_index: int,
|
|
273
|
+
hook_index: int,
|
|
274
|
+
) -> Dict[str, Any]:
|
|
275
|
+
"""Return a deep copy of hooks_config with the specified hook removed.
|
|
276
|
+
|
|
277
|
+
Empty groups and event keys are pruned automatically.
|
|
278
|
+
Does NOT write to disk – call save_hooks_config() afterwards.
|
|
279
|
+
"""
|
|
280
|
+
cfg = copy.deepcopy(hooks_config)
|
|
281
|
+
try:
|
|
282
|
+
group = cfg[event_type][group_index]
|
|
283
|
+
group["hooks"].pop(hook_index)
|
|
284
|
+
if not group["hooks"]:
|
|
285
|
+
cfg[event_type].pop(group_index)
|
|
286
|
+
if not cfg[event_type]:
|
|
287
|
+
del cfg[event_type]
|
|
288
|
+
except (KeyError, IndexError, TypeError) as exc:
|
|
289
|
+
logger.warning("delete_hook: cannot remove hook (%s)", exc)
|
|
290
|
+
return cfg
|