codepp 0.0.437__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. code_puppy/__init__.py +10 -0
  2. code_puppy/__main__.py +10 -0
  3. code_puppy/agents/__init__.py +31 -0
  4. code_puppy/agents/agent_c_reviewer.py +155 -0
  5. code_puppy/agents/agent_code_puppy.py +117 -0
  6. code_puppy/agents/agent_code_reviewer.py +90 -0
  7. code_puppy/agents/agent_cpp_reviewer.py +132 -0
  8. code_puppy/agents/agent_creator_agent.py +638 -0
  9. code_puppy/agents/agent_golang_reviewer.py +151 -0
  10. code_puppy/agents/agent_helios.py +124 -0
  11. code_puppy/agents/agent_javascript_reviewer.py +160 -0
  12. code_puppy/agents/agent_manager.py +742 -0
  13. code_puppy/agents/agent_pack_leader.py +385 -0
  14. code_puppy/agents/agent_planning.py +165 -0
  15. code_puppy/agents/agent_python_programmer.py +169 -0
  16. code_puppy/agents/agent_python_reviewer.py +90 -0
  17. code_puppy/agents/agent_qa_expert.py +163 -0
  18. code_puppy/agents/agent_qa_kitten.py +208 -0
  19. code_puppy/agents/agent_scheduler.py +121 -0
  20. code_puppy/agents/agent_security_auditor.py +181 -0
  21. code_puppy/agents/agent_terminal_qa.py +323 -0
  22. code_puppy/agents/agent_typescript_reviewer.py +166 -0
  23. code_puppy/agents/base_agent.py +2156 -0
  24. code_puppy/agents/event_stream_handler.py +348 -0
  25. code_puppy/agents/json_agent.py +202 -0
  26. code_puppy/agents/pack/__init__.py +34 -0
  27. code_puppy/agents/pack/bloodhound.py +304 -0
  28. code_puppy/agents/pack/husky.py +327 -0
  29. code_puppy/agents/pack/retriever.py +393 -0
  30. code_puppy/agents/pack/shepherd.py +348 -0
  31. code_puppy/agents/pack/terrier.py +287 -0
  32. code_puppy/agents/pack/watchdog.py +367 -0
  33. code_puppy/agents/prompt_reviewer.py +145 -0
  34. code_puppy/agents/subagent_stream_handler.py +276 -0
  35. code_puppy/api/__init__.py +13 -0
  36. code_puppy/api/app.py +169 -0
  37. code_puppy/api/main.py +21 -0
  38. code_puppy/api/pty_manager.py +453 -0
  39. code_puppy/api/routers/__init__.py +12 -0
  40. code_puppy/api/routers/agents.py +36 -0
  41. code_puppy/api/routers/commands.py +217 -0
  42. code_puppy/api/routers/config.py +75 -0
  43. code_puppy/api/routers/sessions.py +234 -0
  44. code_puppy/api/templates/terminal.html +361 -0
  45. code_puppy/api/websocket.py +154 -0
  46. code_puppy/callbacks.py +692 -0
  47. code_puppy/chatgpt_codex_client.py +338 -0
  48. code_puppy/claude_cache_client.py +672 -0
  49. code_puppy/cli_runner.py +1073 -0
  50. code_puppy/command_line/__init__.py +1 -0
  51. code_puppy/command_line/add_model_menu.py +1092 -0
  52. code_puppy/command_line/agent_menu.py +662 -0
  53. code_puppy/command_line/attachments.py +395 -0
  54. code_puppy/command_line/autosave_menu.py +704 -0
  55. code_puppy/command_line/clipboard.py +527 -0
  56. code_puppy/command_line/colors_menu.py +532 -0
  57. code_puppy/command_line/command_handler.py +293 -0
  58. code_puppy/command_line/command_registry.py +150 -0
  59. code_puppy/command_line/config_commands.py +719 -0
  60. code_puppy/command_line/core_commands.py +867 -0
  61. code_puppy/command_line/diff_menu.py +865 -0
  62. code_puppy/command_line/file_path_completion.py +73 -0
  63. code_puppy/command_line/load_context_completion.py +52 -0
  64. code_puppy/command_line/mcp/__init__.py +10 -0
  65. code_puppy/command_line/mcp/base.py +32 -0
  66. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  67. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  68. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  69. code_puppy/command_line/mcp/edit_command.py +148 -0
  70. code_puppy/command_line/mcp/handler.py +138 -0
  71. code_puppy/command_line/mcp/help_command.py +147 -0
  72. code_puppy/command_line/mcp/install_command.py +214 -0
  73. code_puppy/command_line/mcp/install_menu.py +705 -0
  74. code_puppy/command_line/mcp/list_command.py +94 -0
  75. code_puppy/command_line/mcp/logs_command.py +235 -0
  76. code_puppy/command_line/mcp/remove_command.py +82 -0
  77. code_puppy/command_line/mcp/restart_command.py +100 -0
  78. code_puppy/command_line/mcp/search_command.py +123 -0
  79. code_puppy/command_line/mcp/start_all_command.py +135 -0
  80. code_puppy/command_line/mcp/start_command.py +117 -0
  81. code_puppy/command_line/mcp/status_command.py +184 -0
  82. code_puppy/command_line/mcp/stop_all_command.py +112 -0
  83. code_puppy/command_line/mcp/stop_command.py +80 -0
  84. code_puppy/command_line/mcp/test_command.py +107 -0
  85. code_puppy/command_line/mcp/utils.py +129 -0
  86. code_puppy/command_line/mcp/wizard_utils.py +334 -0
  87. code_puppy/command_line/mcp_completion.py +174 -0
  88. code_puppy/command_line/model_picker_completion.py +197 -0
  89. code_puppy/command_line/model_settings_menu.py +932 -0
  90. code_puppy/command_line/motd.py +96 -0
  91. code_puppy/command_line/onboarding_slides.py +179 -0
  92. code_puppy/command_line/onboarding_wizard.py +342 -0
  93. code_puppy/command_line/pin_command_completion.py +329 -0
  94. code_puppy/command_line/prompt_toolkit_completion.py +846 -0
  95. code_puppy/command_line/session_commands.py +302 -0
  96. code_puppy/command_line/shell_passthrough.py +145 -0
  97. code_puppy/command_line/skills_completion.py +160 -0
  98. code_puppy/command_line/uc_menu.py +893 -0
  99. code_puppy/command_line/utils.py +93 -0
  100. code_puppy/command_line/wiggum_state.py +78 -0
  101. code_puppy/config.py +1770 -0
  102. code_puppy/error_logging.py +134 -0
  103. code_puppy/gemini_code_assist.py +385 -0
  104. code_puppy/gemini_model.py +754 -0
  105. code_puppy/hook_engine/README.md +105 -0
  106. code_puppy/hook_engine/__init__.py +21 -0
  107. code_puppy/hook_engine/aliases.py +155 -0
  108. code_puppy/hook_engine/engine.py +221 -0
  109. code_puppy/hook_engine/executor.py +296 -0
  110. code_puppy/hook_engine/matcher.py +156 -0
  111. code_puppy/hook_engine/models.py +240 -0
  112. code_puppy/hook_engine/registry.py +106 -0
  113. code_puppy/hook_engine/validator.py +144 -0
  114. code_puppy/http_utils.py +361 -0
  115. code_puppy/keymap.py +128 -0
  116. code_puppy/main.py +10 -0
  117. code_puppy/mcp_/__init__.py +66 -0
  118. code_puppy/mcp_/async_lifecycle.py +286 -0
  119. code_puppy/mcp_/blocking_startup.py +469 -0
  120. code_puppy/mcp_/captured_stdio_server.py +275 -0
  121. code_puppy/mcp_/circuit_breaker.py +290 -0
  122. code_puppy/mcp_/config_wizard.py +507 -0
  123. code_puppy/mcp_/dashboard.py +308 -0
  124. code_puppy/mcp_/error_isolation.py +407 -0
  125. code_puppy/mcp_/examples/retry_example.py +226 -0
  126. code_puppy/mcp_/health_monitor.py +589 -0
  127. code_puppy/mcp_/managed_server.py +428 -0
  128. code_puppy/mcp_/manager.py +807 -0
  129. code_puppy/mcp_/mcp_logs.py +224 -0
  130. code_puppy/mcp_/registry.py +451 -0
  131. code_puppy/mcp_/retry_manager.py +337 -0
  132. code_puppy/mcp_/server_registry_catalog.py +1126 -0
  133. code_puppy/mcp_/status_tracker.py +355 -0
  134. code_puppy/mcp_/system_tools.py +209 -0
  135. code_puppy/mcp_prompts/__init__.py +1 -0
  136. code_puppy/mcp_prompts/hook_creator.py +103 -0
  137. code_puppy/messaging/__init__.py +255 -0
  138. code_puppy/messaging/bus.py +613 -0
  139. code_puppy/messaging/commands.py +167 -0
  140. code_puppy/messaging/markdown_patches.py +57 -0
  141. code_puppy/messaging/message_queue.py +361 -0
  142. code_puppy/messaging/messages.py +569 -0
  143. code_puppy/messaging/queue_console.py +271 -0
  144. code_puppy/messaging/renderers.py +311 -0
  145. code_puppy/messaging/rich_renderer.py +1158 -0
  146. code_puppy/messaging/spinner/__init__.py +83 -0
  147. code_puppy/messaging/spinner/console_spinner.py +240 -0
  148. code_puppy/messaging/spinner/spinner_base.py +95 -0
  149. code_puppy/messaging/subagent_console.py +460 -0
  150. code_puppy/model_factory.py +848 -0
  151. code_puppy/model_switching.py +63 -0
  152. code_puppy/model_utils.py +168 -0
  153. code_puppy/models.json +174 -0
  154. code_puppy/models_dev_api.json +1 -0
  155. code_puppy/models_dev_parser.py +592 -0
  156. code_puppy/plugins/__init__.py +186 -0
  157. code_puppy/plugins/agent_skills/__init__.py +22 -0
  158. code_puppy/plugins/agent_skills/config.py +175 -0
  159. code_puppy/plugins/agent_skills/discovery.py +136 -0
  160. code_puppy/plugins/agent_skills/downloader.py +392 -0
  161. code_puppy/plugins/agent_skills/installer.py +22 -0
  162. code_puppy/plugins/agent_skills/metadata.py +219 -0
  163. code_puppy/plugins/agent_skills/prompt_builder.py +60 -0
  164. code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
  165. code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
  166. code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
  167. code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
  168. code_puppy/plugins/agent_skills/skills_menu.py +781 -0
  169. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  170. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  171. code_puppy/plugins/antigravity_oauth/antigravity_model.py +706 -0
  172. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  173. code_puppy/plugins/antigravity_oauth/constants.py +133 -0
  174. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  175. code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
  176. code_puppy/plugins/antigravity_oauth/storage.py +288 -0
  177. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  178. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  179. code_puppy/plugins/antigravity_oauth/transport.py +863 -0
  180. code_puppy/plugins/antigravity_oauth/utils.py +168 -0
  181. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  182. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  183. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +329 -0
  184. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
  185. code_puppy/plugins/chatgpt_oauth/test_plugin.py +301 -0
  186. code_puppy/plugins/chatgpt_oauth/utils.py +523 -0
  187. code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
  188. code_puppy/plugins/claude_code_hooks/config.py +137 -0
  189. code_puppy/plugins/claude_code_hooks/register_callbacks.py +175 -0
  190. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  191. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  192. code_puppy/plugins/claude_code_oauth/__init__.py +25 -0
  193. code_puppy/plugins/claude_code_oauth/config.py +52 -0
  194. code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
  195. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  196. code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
  197. code_puppy/plugins/claude_code_oauth/utils.py +640 -0
  198. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  199. code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
  200. code_puppy/plugins/example_custom_command/README.md +280 -0
  201. code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
  202. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  203. code_puppy/plugins/file_permission_handler/register_callbacks.py +470 -0
  204. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  205. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  206. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  207. code_puppy/plugins/hook_creator/__init__.py +1 -0
  208. code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
  209. code_puppy/plugins/hook_manager/__init__.py +1 -0
  210. code_puppy/plugins/hook_manager/config.py +290 -0
  211. code_puppy/plugins/hook_manager/hooks_menu.py +564 -0
  212. code_puppy/plugins/hook_manager/register_callbacks.py +227 -0
  213. code_puppy/plugins/oauth_puppy_html.py +228 -0
  214. code_puppy/plugins/scheduler/__init__.py +1 -0
  215. code_puppy/plugins/scheduler/register_callbacks.py +88 -0
  216. code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
  217. code_puppy/plugins/scheduler/scheduler_wizard.py +341 -0
  218. code_puppy/plugins/shell_safety/__init__.py +6 -0
  219. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  220. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  221. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  222. code_puppy/plugins/synthetic_status/__init__.py +1 -0
  223. code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
  224. code_puppy/plugins/synthetic_status/status_api.py +147 -0
  225. code_puppy/plugins/universal_constructor/__init__.py +13 -0
  226. code_puppy/plugins/universal_constructor/models.py +138 -0
  227. code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
  228. code_puppy/plugins/universal_constructor/registry.py +302 -0
  229. code_puppy/plugins/universal_constructor/sandbox.py +584 -0
  230. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  231. code_puppy/pydantic_patches.py +356 -0
  232. code_puppy/reopenable_async_client.py +232 -0
  233. code_puppy/round_robin_model.py +150 -0
  234. code_puppy/scheduler/__init__.py +41 -0
  235. code_puppy/scheduler/__main__.py +9 -0
  236. code_puppy/scheduler/cli.py +118 -0
  237. code_puppy/scheduler/config.py +126 -0
  238. code_puppy/scheduler/daemon.py +280 -0
  239. code_puppy/scheduler/executor.py +155 -0
  240. code_puppy/scheduler/platform.py +19 -0
  241. code_puppy/scheduler/platform_unix.py +22 -0
  242. code_puppy/scheduler/platform_win.py +32 -0
  243. code_puppy/session_storage.py +338 -0
  244. code_puppy/status_display.py +257 -0
  245. code_puppy/summarization_agent.py +176 -0
  246. code_puppy/terminal_utils.py +418 -0
  247. code_puppy/tools/__init__.py +501 -0
  248. code_puppy/tools/agent_tools.py +603 -0
  249. code_puppy/tools/ask_user_question/__init__.py +26 -0
  250. code_puppy/tools/ask_user_question/constants.py +73 -0
  251. code_puppy/tools/ask_user_question/demo_tui.py +55 -0
  252. code_puppy/tools/ask_user_question/handler.py +232 -0
  253. code_puppy/tools/ask_user_question/models.py +304 -0
  254. code_puppy/tools/ask_user_question/registration.py +26 -0
  255. code_puppy/tools/ask_user_question/renderers.py +309 -0
  256. code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
  257. code_puppy/tools/ask_user_question/theme.py +155 -0
  258. code_puppy/tools/ask_user_question/tui_loop.py +423 -0
  259. code_puppy/tools/browser/__init__.py +37 -0
  260. code_puppy/tools/browser/browser_control.py +289 -0
  261. code_puppy/tools/browser/browser_interactions.py +545 -0
  262. code_puppy/tools/browser/browser_locators.py +640 -0
  263. code_puppy/tools/browser/browser_manager.py +378 -0
  264. code_puppy/tools/browser/browser_navigation.py +251 -0
  265. code_puppy/tools/browser/browser_screenshot.py +179 -0
  266. code_puppy/tools/browser/browser_scripts.py +462 -0
  267. code_puppy/tools/browser/browser_workflows.py +221 -0
  268. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  269. code_puppy/tools/browser/terminal_command_tools.py +534 -0
  270. code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
  271. code_puppy/tools/browser/terminal_tools.py +525 -0
  272. code_puppy/tools/command_runner.py +1346 -0
  273. code_puppy/tools/common.py +1409 -0
  274. code_puppy/tools/display.py +84 -0
  275. code_puppy/tools/file_modifications.py +886 -0
  276. code_puppy/tools/file_operations.py +802 -0
  277. code_puppy/tools/scheduler_tools.py +412 -0
  278. code_puppy/tools/skills_tools.py +244 -0
  279. code_puppy/tools/subagent_context.py +158 -0
  280. code_puppy/tools/tools_content.py +51 -0
  281. code_puppy/tools/universal_constructor.py +889 -0
  282. code_puppy/uvx_detection.py +242 -0
  283. code_puppy/version_checker.py +82 -0
  284. codepp-0.0.437.dist-info/METADATA +766 -0
  285. codepp-0.0.437.dist-info/RECORD +288 -0
  286. codepp-0.0.437.dist-info/WHEEL +4 -0
  287. codepp-0.0.437.dist-info/entry_points.txt +3 -0
  288. codepp-0.0.437.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,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()