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,564 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interactive TUI for managing Claude Code hooks.
|
|
3
|
+
|
|
4
|
+
Launch with /hooks to browse, enable/disable, inspect, and delete hooks
|
|
5
|
+
from both global (~/.code_puppy/hooks.json) and project (.claude/settings.json) sources.
|
|
6
|
+
|
|
7
|
+
Built with prompt_toolkit to match the existing skills_menu aesthetic exactly
|
|
8
|
+
(VSplit, FormattedTextControl, Frame).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
from typing import List, Optional
|
|
14
|
+
|
|
15
|
+
from prompt_toolkit.application import Application
|
|
16
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
17
|
+
from prompt_toolkit.layout import Dimension, Layout, VSplit, Window
|
|
18
|
+
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
19
|
+
from prompt_toolkit.widgets import Frame
|
|
20
|
+
|
|
21
|
+
from code_puppy.messaging import emit_error
|
|
22
|
+
|
|
23
|
+
from .config import (
|
|
24
|
+
HookEntry,
|
|
25
|
+
_load_global_hooks_config,
|
|
26
|
+
_load_project_hooks_config,
|
|
27
|
+
delete_hook,
|
|
28
|
+
flatten_all_hooks,
|
|
29
|
+
save_global_hooks_config,
|
|
30
|
+
save_hooks_config,
|
|
31
|
+
toggle_hook_enabled,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
PAGE_SIZE = 12
|
|
35
|
+
|
|
36
|
+
# Colour palette (matches skills_menu palette)
|
|
37
|
+
_C_ENABLED = "fg:ansigreen"
|
|
38
|
+
_C_DISABLED = "fg:ansired"
|
|
39
|
+
_C_SELECTED_BG = "bold"
|
|
40
|
+
_C_DIM = "fg:ansibrightblack"
|
|
41
|
+
_C_CYAN = "fg:ansicyan"
|
|
42
|
+
_C_YELLOW = "fg:ansiyellow"
|
|
43
|
+
_C_MAGENTA = "fg:ansimagenta"
|
|
44
|
+
_C_HEADER = "dim cyan"
|
|
45
|
+
_C_GLOBAL = "fg:ansiblue"
|
|
46
|
+
_C_PROJECT = "fg:ansigreen"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class HooksMenu:
|
|
50
|
+
"""Interactive TUI for managing hooks from both global and project sources."""
|
|
51
|
+
|
|
52
|
+
def __init__(self) -> None:
|
|
53
|
+
self.entries: List[HookEntry] = []
|
|
54
|
+
self.selected_idx: int = 0
|
|
55
|
+
self.current_page: int = 0
|
|
56
|
+
self.result: Optional[str] = None
|
|
57
|
+
self.status_message: str = ""
|
|
58
|
+
|
|
59
|
+
# prompt_toolkit controls (set during run())
|
|
60
|
+
self.list_control: Optional[FormattedTextControl] = None
|
|
61
|
+
self.detail_control: Optional[FormattedTextControl] = None
|
|
62
|
+
|
|
63
|
+
self._refresh_data()
|
|
64
|
+
|
|
65
|
+
# ------------------------------------------------------------------
|
|
66
|
+
# Data helpers
|
|
67
|
+
# ------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
def _refresh_data(self) -> None:
|
|
70
|
+
"""Reload hooks from both global and project sources."""
|
|
71
|
+
try:
|
|
72
|
+
self.entries = flatten_all_hooks()
|
|
73
|
+
# Clamp selection
|
|
74
|
+
if self.entries:
|
|
75
|
+
self.selected_idx = min(self.selected_idx, len(self.entries) - 1)
|
|
76
|
+
else:
|
|
77
|
+
self.selected_idx = 0
|
|
78
|
+
except Exception as exc:
|
|
79
|
+
emit_error(f"Failed to refresh hooks data: {exc}")
|
|
80
|
+
self.entries = []
|
|
81
|
+
|
|
82
|
+
def _current_entry(self) -> Optional[HookEntry]:
|
|
83
|
+
if 0 <= self.selected_idx < len(self.entries):
|
|
84
|
+
return self.entries[self.selected_idx]
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
def _save_current_entry(
|
|
88
|
+
self, entry: HookEntry, new_enabled: Optional[bool] = None
|
|
89
|
+
) -> None:
|
|
90
|
+
"""Persist changes to the current entry's source file."""
|
|
91
|
+
if entry.source == "global":
|
|
92
|
+
global_config = _load_global_hooks_config()
|
|
93
|
+
if new_enabled is not None:
|
|
94
|
+
global_config = toggle_hook_enabled(
|
|
95
|
+
global_config,
|
|
96
|
+
entry.event_type,
|
|
97
|
+
entry._group_index,
|
|
98
|
+
entry._hook_index,
|
|
99
|
+
new_enabled,
|
|
100
|
+
)
|
|
101
|
+
save_global_hooks_config(global_config)
|
|
102
|
+
else: # project
|
|
103
|
+
project_config = _load_project_hooks_config()
|
|
104
|
+
if new_enabled is not None:
|
|
105
|
+
project_config = toggle_hook_enabled(
|
|
106
|
+
project_config,
|
|
107
|
+
entry.event_type,
|
|
108
|
+
entry._group_index,
|
|
109
|
+
entry._hook_index,
|
|
110
|
+
new_enabled,
|
|
111
|
+
)
|
|
112
|
+
save_hooks_config(project_config)
|
|
113
|
+
|
|
114
|
+
# ------------------------------------------------------------------
|
|
115
|
+
# Actions triggered by key bindings
|
|
116
|
+
# ------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
def _toggle_current(self) -> None:
|
|
119
|
+
"""Toggle enabled/disabled on the selected hook."""
|
|
120
|
+
entry = self._current_entry()
|
|
121
|
+
if entry is None:
|
|
122
|
+
return
|
|
123
|
+
new_enabled = not entry.enabled
|
|
124
|
+
self._save_current_entry(entry, new_enabled)
|
|
125
|
+
self._refresh_data()
|
|
126
|
+
self.status_message = (
|
|
127
|
+
f"Hook {'enabled' if new_enabled else 'disabled'}: {entry.display_command}"
|
|
128
|
+
)
|
|
129
|
+
self.update_display()
|
|
130
|
+
|
|
131
|
+
def _delete_current(self) -> None:
|
|
132
|
+
"""Delete the selected hook (with guard against empty config)."""
|
|
133
|
+
entry = self._current_entry()
|
|
134
|
+
if entry is None:
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
if entry.source == "global":
|
|
138
|
+
global_config = _load_global_hooks_config()
|
|
139
|
+
global_config = delete_hook(
|
|
140
|
+
global_config,
|
|
141
|
+
entry.event_type,
|
|
142
|
+
entry._group_index,
|
|
143
|
+
entry._hook_index,
|
|
144
|
+
)
|
|
145
|
+
save_global_hooks_config(global_config)
|
|
146
|
+
else: # project
|
|
147
|
+
project_config = _load_project_hooks_config()
|
|
148
|
+
project_config = delete_hook(
|
|
149
|
+
project_config,
|
|
150
|
+
entry.event_type,
|
|
151
|
+
entry._group_index,
|
|
152
|
+
entry._hook_index,
|
|
153
|
+
)
|
|
154
|
+
save_hooks_config(project_config)
|
|
155
|
+
|
|
156
|
+
self._refresh_data()
|
|
157
|
+
self.status_message = f"Deleted hook: {entry.display_command}"
|
|
158
|
+
self.update_display()
|
|
159
|
+
|
|
160
|
+
def _enable_all(self) -> None:
|
|
161
|
+
"""Enable every hook in both project and global configs."""
|
|
162
|
+
import copy
|
|
163
|
+
|
|
164
|
+
# Enable all project hooks
|
|
165
|
+
project_config = _load_project_hooks_config()
|
|
166
|
+
project_cfg = copy.deepcopy(project_config)
|
|
167
|
+
for groups in project_cfg.values():
|
|
168
|
+
if not isinstance(groups, list):
|
|
169
|
+
continue
|
|
170
|
+
for group in groups:
|
|
171
|
+
for hook in group.get("hooks", []):
|
|
172
|
+
hook["enabled"] = True
|
|
173
|
+
save_hooks_config(project_cfg)
|
|
174
|
+
|
|
175
|
+
# Enable all global hooks
|
|
176
|
+
global_config = _load_global_hooks_config()
|
|
177
|
+
global_cfg = copy.deepcopy(global_config)
|
|
178
|
+
for groups in global_cfg.values():
|
|
179
|
+
if not isinstance(groups, list):
|
|
180
|
+
continue
|
|
181
|
+
for group in groups:
|
|
182
|
+
for hook in group.get("hooks", []):
|
|
183
|
+
hook["enabled"] = True
|
|
184
|
+
save_global_hooks_config(global_cfg)
|
|
185
|
+
|
|
186
|
+
self._refresh_data()
|
|
187
|
+
self.status_message = "All hooks enabled."
|
|
188
|
+
self.update_display()
|
|
189
|
+
|
|
190
|
+
def _disable_all(self) -> None:
|
|
191
|
+
"""Disable every hook in both project and global configs."""
|
|
192
|
+
import copy
|
|
193
|
+
|
|
194
|
+
# Disable all project hooks
|
|
195
|
+
project_config = _load_project_hooks_config()
|
|
196
|
+
project_cfg = copy.deepcopy(project_config)
|
|
197
|
+
for groups in project_cfg.values():
|
|
198
|
+
if not isinstance(groups, list):
|
|
199
|
+
continue
|
|
200
|
+
for group in groups:
|
|
201
|
+
for hook in group.get("hooks", []):
|
|
202
|
+
hook["enabled"] = False
|
|
203
|
+
save_hooks_config(project_cfg)
|
|
204
|
+
|
|
205
|
+
# Disable all global hooks
|
|
206
|
+
global_config = _load_global_hooks_config()
|
|
207
|
+
global_cfg = copy.deepcopy(global_config)
|
|
208
|
+
for groups in global_cfg.values():
|
|
209
|
+
if not isinstance(groups, list):
|
|
210
|
+
continue
|
|
211
|
+
for group in groups:
|
|
212
|
+
for hook in group.get("hooks", []):
|
|
213
|
+
hook["enabled"] = False
|
|
214
|
+
save_global_hooks_config(global_cfg)
|
|
215
|
+
|
|
216
|
+
self._refresh_data()
|
|
217
|
+
self.status_message = "All hooks disabled."
|
|
218
|
+
self.update_display()
|
|
219
|
+
|
|
220
|
+
# ------------------------------------------------------------------
|
|
221
|
+
# Rendering helpers
|
|
222
|
+
# ------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
def _render_list(self) -> List:
|
|
225
|
+
"""Render the left-hand hooks list panel."""
|
|
226
|
+
lines: List = []
|
|
227
|
+
|
|
228
|
+
total = len(self.entries)
|
|
229
|
+
enabled_count = sum(1 for e in self.entries if e.enabled)
|
|
230
|
+
project_count = sum(1 for e in self.entries if e.source == "project")
|
|
231
|
+
global_count = sum(1 for e in self.entries if e.source == "global")
|
|
232
|
+
|
|
233
|
+
header_color = _C_ENABLED if enabled_count > 0 else _C_DISABLED
|
|
234
|
+
lines.append((header_color, f" Hooks: {enabled_count}/{total} enabled"))
|
|
235
|
+
lines.append(("", f" ({project_count} project, {global_count} global)\n\n"))
|
|
236
|
+
|
|
237
|
+
if not self.entries:
|
|
238
|
+
lines.append((_C_YELLOW, " No hooks configured."))
|
|
239
|
+
lines.append(("", "\n"))
|
|
240
|
+
lines.append((_C_DIM, " Add hooks to .claude/settings.json (project)"))
|
|
241
|
+
lines.append(("", "\n"))
|
|
242
|
+
lines.append((_C_DIM, " or ~/.code_puppy/hooks.json (global)"))
|
|
243
|
+
lines.append(("", "\n\n"))
|
|
244
|
+
self._render_nav_hints(lines)
|
|
245
|
+
return lines
|
|
246
|
+
|
|
247
|
+
total_pages = max(1, (total + PAGE_SIZE - 1) // PAGE_SIZE)
|
|
248
|
+
start = self.current_page * PAGE_SIZE
|
|
249
|
+
end = min(start + PAGE_SIZE, total)
|
|
250
|
+
for i in range(start, end):
|
|
251
|
+
entry = self.entries[i]
|
|
252
|
+
is_selected = i == self.selected_idx
|
|
253
|
+
status_icon = "✓" if entry.enabled else "✗"
|
|
254
|
+
status_style = _C_ENABLED if entry.enabled else _C_DISABLED
|
|
255
|
+
source_indicator = "🌍" if entry.source == "global" else "📁"
|
|
256
|
+
prefix = " > " if is_selected else " "
|
|
257
|
+
|
|
258
|
+
if is_selected:
|
|
259
|
+
lines.append((_C_SELECTED_BG, prefix))
|
|
260
|
+
lines.append((status_style + " bold", status_icon))
|
|
261
|
+
lines.append(
|
|
262
|
+
(_C_SELECTED_BG, f" {source_indicator} [{entry.event_type}]")
|
|
263
|
+
)
|
|
264
|
+
lines.append((_C_SELECTED_BG, f" {entry.display_matcher}"))
|
|
265
|
+
else:
|
|
266
|
+
lines.append(("", prefix))
|
|
267
|
+
lines.append((status_style, status_icon))
|
|
268
|
+
source_color = _C_GLOBAL if entry.source == "global" else _C_PROJECT
|
|
269
|
+
lines.append((source_color, f" {source_indicator}"))
|
|
270
|
+
lines.append((_C_DIM, f" [{entry.event_type}]"))
|
|
271
|
+
lines.append(("", f" {entry.display_matcher}"))
|
|
272
|
+
lines.append(("", "\n"))
|
|
273
|
+
|
|
274
|
+
lines.append(("", "\n"))
|
|
275
|
+
lines.append((_C_DIM, f" Page {self.current_page + 1}/{total_pages}"))
|
|
276
|
+
lines.append(("", "\n"))
|
|
277
|
+
|
|
278
|
+
# Status message (shows result of last action)
|
|
279
|
+
if self.status_message:
|
|
280
|
+
lines.append(("", "\n"))
|
|
281
|
+
lines.append((_C_CYAN, f" {self.status_message}"))
|
|
282
|
+
lines.append(("", "\n"))
|
|
283
|
+
|
|
284
|
+
self._render_nav_hints(lines)
|
|
285
|
+
return lines
|
|
286
|
+
|
|
287
|
+
def _render_nav_hints(self, lines: List) -> None:
|
|
288
|
+
"""Append keyboard shortcut hints to lines."""
|
|
289
|
+
lines.append(("", "\n"))
|
|
290
|
+
lines.append((_C_DIM, " ↑/↓ j/k "))
|
|
291
|
+
lines.append(("", "Navigate\n"))
|
|
292
|
+
lines.append((_C_ENABLED, " Enter "))
|
|
293
|
+
lines.append(("", "Toggle enable/disable\n"))
|
|
294
|
+
lines.append((_C_DISABLED, " d "))
|
|
295
|
+
lines.append(("", "Delete hook\n"))
|
|
296
|
+
lines.append((_C_YELLOW, " A "))
|
|
297
|
+
lines.append(("", "Enable all\n"))
|
|
298
|
+
lines.append((_C_MAGENTA, " D "))
|
|
299
|
+
lines.append(("", "Disable all\n"))
|
|
300
|
+
lines.append((_C_YELLOW, " r "))
|
|
301
|
+
lines.append(("", "Refresh\n"))
|
|
302
|
+
lines.append((_C_DISABLED, " q/Esc "))
|
|
303
|
+
lines.append(("", "Exit"))
|
|
304
|
+
|
|
305
|
+
def _render_detail(self) -> List:
|
|
306
|
+
"""Render the right-hand hook detail panel."""
|
|
307
|
+
lines: List = []
|
|
308
|
+
lines.append((_C_HEADER, " HOOK DETAILS"))
|
|
309
|
+
lines.append(("", "\n\n"))
|
|
310
|
+
|
|
311
|
+
entry = self._current_entry()
|
|
312
|
+
if entry is None:
|
|
313
|
+
lines.append((_C_YELLOW, " No hook selected."))
|
|
314
|
+
lines.append(("", "\n\n"))
|
|
315
|
+
lines.append((_C_DIM, " Select a hook from the list"))
|
|
316
|
+
lines.append(("", "\n"))
|
|
317
|
+
lines.append((_C_DIM, " to view its details."))
|
|
318
|
+
return lines
|
|
319
|
+
|
|
320
|
+
# Status badge
|
|
321
|
+
status_text = "Enabled" if entry.enabled else "Disabled"
|
|
322
|
+
status_style = _C_ENABLED + " bold" if entry.enabled else _C_DISABLED + " bold"
|
|
323
|
+
lines.append(("bold", " Status: "))
|
|
324
|
+
lines.append((status_style, status_text))
|
|
325
|
+
lines.append(("", "\n\n"))
|
|
326
|
+
|
|
327
|
+
# Source indicator
|
|
328
|
+
source_label = (
|
|
329
|
+
"Global (~/.code_puppy/hooks.json)"
|
|
330
|
+
if entry.source == "global"
|
|
331
|
+
else "Project (.claude/settings.json)"
|
|
332
|
+
)
|
|
333
|
+
source_color = _C_GLOBAL if entry.source == "global" else _C_PROJECT
|
|
334
|
+
lines.append(("bold", " Source: "))
|
|
335
|
+
lines.append((source_color, source_label))
|
|
336
|
+
lines.append(("", "\n\n"))
|
|
337
|
+
|
|
338
|
+
# Event type
|
|
339
|
+
lines.append(("bold", " Event: "))
|
|
340
|
+
lines.append((_C_CYAN, entry.event_type))
|
|
341
|
+
lines.append(("", "\n\n"))
|
|
342
|
+
|
|
343
|
+
# Matcher
|
|
344
|
+
lines.append(("bold", " Matcher: "))
|
|
345
|
+
lines.append(("", "\n"))
|
|
346
|
+
for chunk in _wrap(entry.matcher, 50):
|
|
347
|
+
lines.append((_C_YELLOW, f" {chunk}"))
|
|
348
|
+
lines.append(("", "\n"))
|
|
349
|
+
lines.append(("", "\n"))
|
|
350
|
+
|
|
351
|
+
# Type
|
|
352
|
+
lines.append(("bold", " Type: "))
|
|
353
|
+
lines.append((_C_DIM, entry.hook_type))
|
|
354
|
+
lines.append(("", "\n\n"))
|
|
355
|
+
|
|
356
|
+
# Command / prompt
|
|
357
|
+
label = "Command:" if entry.hook_type == "command" else "Prompt: "
|
|
358
|
+
lines.append(("bold", f" {label}"))
|
|
359
|
+
lines.append(("", "\n"))
|
|
360
|
+
for chunk in _wrap(entry.command, 50):
|
|
361
|
+
lines.append((_C_DIM, f" {chunk}"))
|
|
362
|
+
lines.append(("", "\n"))
|
|
363
|
+
lines.append(("", "\n"))
|
|
364
|
+
|
|
365
|
+
# Timeout
|
|
366
|
+
lines.append(("bold", " Timeout: "))
|
|
367
|
+
lines.append((_C_DIM, f"{entry.timeout} ms"))
|
|
368
|
+
lines.append(("", "\n\n"))
|
|
369
|
+
|
|
370
|
+
# Hook ID
|
|
371
|
+
if entry.hook_id:
|
|
372
|
+
lines.append(("bold", " ID: "))
|
|
373
|
+
lines.append((_C_DIM, entry.hook_id))
|
|
374
|
+
lines.append(("", "\n\n"))
|
|
375
|
+
|
|
376
|
+
# Config location hint
|
|
377
|
+
lines.append((_C_DIM, f" Stored in {source_label}"))
|
|
378
|
+
lines.append(("", "\n"))
|
|
379
|
+
lines.append(
|
|
380
|
+
(_C_DIM, f" group #{entry._group_index} hook #{entry._hook_index}")
|
|
381
|
+
)
|
|
382
|
+
lines.append(("", "\n"))
|
|
383
|
+
|
|
384
|
+
return lines
|
|
385
|
+
|
|
386
|
+
def update_display(self) -> None:
|
|
387
|
+
"""Push freshly rendered text into the prompt_toolkit controls."""
|
|
388
|
+
if self.list_control:
|
|
389
|
+
self.list_control.text = self._render_list()
|
|
390
|
+
if self.detail_control:
|
|
391
|
+
self.detail_control.text = self._render_detail()
|
|
392
|
+
|
|
393
|
+
# ------------------------------------------------------------------
|
|
394
|
+
# Main entry point
|
|
395
|
+
# ------------------------------------------------------------------
|
|
396
|
+
|
|
397
|
+
def run(self) -> Optional[str]:
|
|
398
|
+
"""Launch the interactive TUI. Returns the exit reason string."""
|
|
399
|
+
self.result = None
|
|
400
|
+
|
|
401
|
+
self.list_control = FormattedTextControl(text="")
|
|
402
|
+
self.detail_control = FormattedTextControl(text="")
|
|
403
|
+
|
|
404
|
+
list_window = Window(
|
|
405
|
+
content=self.list_control, wrap_lines=True, width=Dimension(weight=40)
|
|
406
|
+
)
|
|
407
|
+
detail_window = Window(
|
|
408
|
+
content=self.detail_control, wrap_lines=True, width=Dimension(weight=60)
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
list_frame = Frame(list_window, width=Dimension(weight=40), title="Hooks")
|
|
412
|
+
detail_frame = Frame(detail_window, width=Dimension(weight=60), title="Details")
|
|
413
|
+
|
|
414
|
+
root_container = VSplit([list_frame, detail_frame])
|
|
415
|
+
kb = KeyBindings()
|
|
416
|
+
|
|
417
|
+
# --- Navigation ---
|
|
418
|
+
@kb.add("up")
|
|
419
|
+
@kb.add("c-p")
|
|
420
|
+
@kb.add("k")
|
|
421
|
+
def _move_up(event):
|
|
422
|
+
if self.selected_idx > 0:
|
|
423
|
+
self.selected_idx -= 1
|
|
424
|
+
self.current_page = self.selected_idx // PAGE_SIZE
|
|
425
|
+
self.update_display()
|
|
426
|
+
|
|
427
|
+
@kb.add("down")
|
|
428
|
+
@kb.add("c-n")
|
|
429
|
+
@kb.add("j")
|
|
430
|
+
def _move_down(event):
|
|
431
|
+
if self.selected_idx < len(self.entries) - 1:
|
|
432
|
+
self.selected_idx += 1
|
|
433
|
+
self.current_page = self.selected_idx // PAGE_SIZE
|
|
434
|
+
self.update_display()
|
|
435
|
+
|
|
436
|
+
@kb.add("left")
|
|
437
|
+
def _prev_page(event):
|
|
438
|
+
if self.current_page > 0:
|
|
439
|
+
self.current_page -= 1
|
|
440
|
+
self.selected_idx = self.current_page * PAGE_SIZE
|
|
441
|
+
self.update_display()
|
|
442
|
+
|
|
443
|
+
@kb.add("right")
|
|
444
|
+
def _next_page(event):
|
|
445
|
+
total_pages = max(1, (len(self.entries) + PAGE_SIZE - 1) // PAGE_SIZE)
|
|
446
|
+
if self.current_page < total_pages - 1:
|
|
447
|
+
self.current_page += 1
|
|
448
|
+
self.selected_idx = self.current_page * PAGE_SIZE
|
|
449
|
+
self.update_display()
|
|
450
|
+
|
|
451
|
+
# --- Actions ---
|
|
452
|
+
@kb.add("enter")
|
|
453
|
+
def _toggle(event):
|
|
454
|
+
self._toggle_current()
|
|
455
|
+
self.result = "changed"
|
|
456
|
+
|
|
457
|
+
@kb.add("d")
|
|
458
|
+
def _delete(event):
|
|
459
|
+
self._delete_current()
|
|
460
|
+
self.result = "changed"
|
|
461
|
+
|
|
462
|
+
@kb.add("A") # capital A = enable ALL
|
|
463
|
+
def _enable_all(event):
|
|
464
|
+
self._enable_all()
|
|
465
|
+
self.result = "changed"
|
|
466
|
+
|
|
467
|
+
@kb.add("D") # capital D = disable ALL
|
|
468
|
+
def _disable_all(event):
|
|
469
|
+
self._disable_all()
|
|
470
|
+
self.result = "changed"
|
|
471
|
+
|
|
472
|
+
@kb.add("r")
|
|
473
|
+
def _refresh(event):
|
|
474
|
+
self._refresh_data()
|
|
475
|
+
self.status_message = "Refreshed."
|
|
476
|
+
self.update_display()
|
|
477
|
+
|
|
478
|
+
# --- Exit ---
|
|
479
|
+
@kb.add("q")
|
|
480
|
+
@kb.add("escape")
|
|
481
|
+
def _quit(event):
|
|
482
|
+
self.result = "quit"
|
|
483
|
+
event.app.exit()
|
|
484
|
+
|
|
485
|
+
@kb.add("c-c")
|
|
486
|
+
def _quit_ctrl_c(event):
|
|
487
|
+
self.result = "quit"
|
|
488
|
+
event.app.exit()
|
|
489
|
+
|
|
490
|
+
layout = Layout(root_container)
|
|
491
|
+
app = Application(
|
|
492
|
+
layout=layout,
|
|
493
|
+
key_bindings=kb,
|
|
494
|
+
full_screen=False,
|
|
495
|
+
mouse_support=False,
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
try:
|
|
499
|
+
from code_puppy.tools.command_runner import set_awaiting_user_input
|
|
500
|
+
|
|
501
|
+
set_awaiting_user_input(True)
|
|
502
|
+
except Exception:
|
|
503
|
+
pass
|
|
504
|
+
|
|
505
|
+
# Enter alternate screen buffer
|
|
506
|
+
sys.stdout.write("\033[?1049h")
|
|
507
|
+
sys.stdout.write("\033[2J\033[H")
|
|
508
|
+
sys.stdout.flush()
|
|
509
|
+
time.sleep(0.05)
|
|
510
|
+
|
|
511
|
+
try:
|
|
512
|
+
self.update_display()
|
|
513
|
+
sys.stdout.write("\033[2J\033[H")
|
|
514
|
+
sys.stdout.flush()
|
|
515
|
+
app.run(in_thread=True)
|
|
516
|
+
finally:
|
|
517
|
+
sys.stdout.write("\033[?1049l")
|
|
518
|
+
sys.stdout.flush()
|
|
519
|
+
try:
|
|
520
|
+
import termios
|
|
521
|
+
|
|
522
|
+
termios.tcflush(sys.stdin.fileno(), termios.TCIFLUSH)
|
|
523
|
+
except Exception:
|
|
524
|
+
pass # ImportError on Windows, termios.error, or not a tty
|
|
525
|
+
time.sleep(0.1)
|
|
526
|
+
try:
|
|
527
|
+
from code_puppy.tools.command_runner import set_awaiting_user_input
|
|
528
|
+
|
|
529
|
+
set_awaiting_user_input(False)
|
|
530
|
+
except Exception:
|
|
531
|
+
pass
|
|
532
|
+
|
|
533
|
+
return self.result
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
# ---------------------------------------------------------------------------
|
|
537
|
+
# Helpers
|
|
538
|
+
# ---------------------------------------------------------------------------
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def _wrap(text: str, width: int) -> List[str]:
|
|
542
|
+
"""Wrap text to *width* characters, splitting on whitespace."""
|
|
543
|
+
words = text.split()
|
|
544
|
+
lines: List[str] = []
|
|
545
|
+
current: List[str] = []
|
|
546
|
+
length = 0
|
|
547
|
+
for word in words:
|
|
548
|
+
if length + len(word) + (1 if current else 0) <= width:
|
|
549
|
+
current.append(word)
|
|
550
|
+
length += len(word) + (1 if len(current) > 1 else 0)
|
|
551
|
+
else:
|
|
552
|
+
if current:
|
|
553
|
+
lines.append(" ".join(current))
|
|
554
|
+
current = [word]
|
|
555
|
+
length = len(word)
|
|
556
|
+
if current:
|
|
557
|
+
lines.append(" ".join(current))
|
|
558
|
+
return lines or [""]
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def show_hooks_menu() -> None:
|
|
562
|
+
"""Public entry point called from register_callbacks.py."""
|
|
563
|
+
menu = HooksMenu()
|
|
564
|
+
menu.run()
|