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