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