code-puppy 0.0.169__py3-none-any.whl โ†’ 0.0.366__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 (243) hide show
  1. code_puppy/__init__.py +7 -1
  2. code_puppy/agents/__init__.py +8 -8
  3. code_puppy/agents/agent_c_reviewer.py +155 -0
  4. code_puppy/agents/agent_code_puppy.py +9 -2
  5. code_puppy/agents/agent_code_reviewer.py +90 -0
  6. code_puppy/agents/agent_cpp_reviewer.py +132 -0
  7. code_puppy/agents/agent_creator_agent.py +48 -9
  8. code_puppy/agents/agent_golang_reviewer.py +151 -0
  9. code_puppy/agents/agent_javascript_reviewer.py +160 -0
  10. code_puppy/agents/agent_manager.py +146 -199
  11. code_puppy/agents/agent_pack_leader.py +383 -0
  12. code_puppy/agents/agent_planning.py +163 -0
  13. code_puppy/agents/agent_python_programmer.py +165 -0
  14. code_puppy/agents/agent_python_reviewer.py +90 -0
  15. code_puppy/agents/agent_qa_expert.py +163 -0
  16. code_puppy/agents/agent_qa_kitten.py +208 -0
  17. code_puppy/agents/agent_security_auditor.py +181 -0
  18. code_puppy/agents/agent_terminal_qa.py +323 -0
  19. code_puppy/agents/agent_typescript_reviewer.py +166 -0
  20. code_puppy/agents/base_agent.py +1713 -1
  21. code_puppy/agents/event_stream_handler.py +350 -0
  22. code_puppy/agents/json_agent.py +12 -1
  23. code_puppy/agents/pack/__init__.py +34 -0
  24. code_puppy/agents/pack/bloodhound.py +304 -0
  25. code_puppy/agents/pack/husky.py +321 -0
  26. code_puppy/agents/pack/retriever.py +393 -0
  27. code_puppy/agents/pack/shepherd.py +348 -0
  28. code_puppy/agents/pack/terrier.py +287 -0
  29. code_puppy/agents/pack/watchdog.py +367 -0
  30. code_puppy/agents/prompt_reviewer.py +145 -0
  31. code_puppy/agents/subagent_stream_handler.py +276 -0
  32. code_puppy/api/__init__.py +13 -0
  33. code_puppy/api/app.py +169 -0
  34. code_puppy/api/main.py +21 -0
  35. code_puppy/api/pty_manager.py +446 -0
  36. code_puppy/api/routers/__init__.py +12 -0
  37. code_puppy/api/routers/agents.py +36 -0
  38. code_puppy/api/routers/commands.py +217 -0
  39. code_puppy/api/routers/config.py +74 -0
  40. code_puppy/api/routers/sessions.py +232 -0
  41. code_puppy/api/templates/terminal.html +361 -0
  42. code_puppy/api/websocket.py +154 -0
  43. code_puppy/callbacks.py +174 -4
  44. code_puppy/chatgpt_codex_client.py +283 -0
  45. code_puppy/claude_cache_client.py +586 -0
  46. code_puppy/cli_runner.py +916 -0
  47. code_puppy/command_line/add_model_menu.py +1079 -0
  48. code_puppy/command_line/agent_menu.py +395 -0
  49. code_puppy/command_line/attachments.py +395 -0
  50. code_puppy/command_line/autosave_menu.py +605 -0
  51. code_puppy/command_line/clipboard.py +527 -0
  52. code_puppy/command_line/colors_menu.py +520 -0
  53. code_puppy/command_line/command_handler.py +233 -627
  54. code_puppy/command_line/command_registry.py +150 -0
  55. code_puppy/command_line/config_commands.py +715 -0
  56. code_puppy/command_line/core_commands.py +792 -0
  57. code_puppy/command_line/diff_menu.py +863 -0
  58. code_puppy/command_line/load_context_completion.py +15 -22
  59. code_puppy/command_line/mcp/base.py +1 -4
  60. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  61. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  62. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  63. code_puppy/command_line/mcp/edit_command.py +148 -0
  64. code_puppy/command_line/mcp/handler.py +9 -4
  65. code_puppy/command_line/mcp/help_command.py +6 -5
  66. code_puppy/command_line/mcp/install_command.py +16 -27
  67. code_puppy/command_line/mcp/install_menu.py +685 -0
  68. code_puppy/command_line/mcp/list_command.py +3 -3
  69. code_puppy/command_line/mcp/logs_command.py +174 -65
  70. code_puppy/command_line/mcp/remove_command.py +2 -2
  71. code_puppy/command_line/mcp/restart_command.py +12 -4
  72. code_puppy/command_line/mcp/search_command.py +17 -11
  73. code_puppy/command_line/mcp/start_all_command.py +22 -13
  74. code_puppy/command_line/mcp/start_command.py +50 -31
  75. code_puppy/command_line/mcp/status_command.py +6 -7
  76. code_puppy/command_line/mcp/stop_all_command.py +11 -8
  77. code_puppy/command_line/mcp/stop_command.py +11 -10
  78. code_puppy/command_line/mcp/test_command.py +2 -2
  79. code_puppy/command_line/mcp/utils.py +1 -1
  80. code_puppy/command_line/mcp/wizard_utils.py +22 -18
  81. code_puppy/command_line/mcp_completion.py +174 -0
  82. code_puppy/command_line/model_picker_completion.py +89 -30
  83. code_puppy/command_line/model_settings_menu.py +884 -0
  84. code_puppy/command_line/motd.py +14 -8
  85. code_puppy/command_line/onboarding_slides.py +179 -0
  86. code_puppy/command_line/onboarding_wizard.py +340 -0
  87. code_puppy/command_line/pin_command_completion.py +329 -0
  88. code_puppy/command_line/prompt_toolkit_completion.py +626 -75
  89. code_puppy/command_line/session_commands.py +296 -0
  90. code_puppy/command_line/utils.py +54 -0
  91. code_puppy/config.py +1181 -51
  92. code_puppy/error_logging.py +118 -0
  93. code_puppy/gemini_code_assist.py +385 -0
  94. code_puppy/gemini_model.py +602 -0
  95. code_puppy/http_utils.py +220 -104
  96. code_puppy/keymap.py +128 -0
  97. code_puppy/main.py +5 -594
  98. code_puppy/{mcp โ†’ mcp_}/__init__.py +17 -0
  99. code_puppy/{mcp โ†’ mcp_}/async_lifecycle.py +35 -4
  100. code_puppy/{mcp โ†’ mcp_}/blocking_startup.py +70 -43
  101. code_puppy/{mcp โ†’ mcp_}/captured_stdio_server.py +2 -2
  102. code_puppy/{mcp โ†’ mcp_}/config_wizard.py +5 -5
  103. code_puppy/{mcp โ†’ mcp_}/dashboard.py +15 -6
  104. code_puppy/{mcp โ†’ mcp_}/examples/retry_example.py +4 -1
  105. code_puppy/{mcp โ†’ mcp_}/managed_server.py +66 -39
  106. code_puppy/{mcp โ†’ mcp_}/manager.py +146 -52
  107. code_puppy/mcp_/mcp_logs.py +224 -0
  108. code_puppy/{mcp โ†’ mcp_}/registry.py +6 -6
  109. code_puppy/{mcp โ†’ mcp_}/server_registry_catalog.py +25 -8
  110. code_puppy/messaging/__init__.py +199 -2
  111. code_puppy/messaging/bus.py +610 -0
  112. code_puppy/messaging/commands.py +167 -0
  113. code_puppy/messaging/markdown_patches.py +57 -0
  114. code_puppy/messaging/message_queue.py +17 -48
  115. code_puppy/messaging/messages.py +500 -0
  116. code_puppy/messaging/queue_console.py +1 -24
  117. code_puppy/messaging/renderers.py +43 -146
  118. code_puppy/messaging/rich_renderer.py +1027 -0
  119. code_puppy/messaging/spinner/__init__.py +33 -5
  120. code_puppy/messaging/spinner/console_spinner.py +92 -52
  121. code_puppy/messaging/spinner/spinner_base.py +29 -0
  122. code_puppy/messaging/subagent_console.py +461 -0
  123. code_puppy/model_factory.py +686 -80
  124. code_puppy/model_utils.py +167 -0
  125. code_puppy/models.json +86 -104
  126. code_puppy/models_dev_api.json +1 -0
  127. code_puppy/models_dev_parser.py +592 -0
  128. code_puppy/plugins/__init__.py +164 -10
  129. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  130. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  131. code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
  132. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  133. code_puppy/plugins/antigravity_oauth/constants.py +136 -0
  134. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  135. code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
  136. code_puppy/plugins/antigravity_oauth/storage.py +271 -0
  137. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  138. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  139. code_puppy/plugins/antigravity_oauth/transport.py +767 -0
  140. code_puppy/plugins/antigravity_oauth/utils.py +169 -0
  141. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  142. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  143. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
  144. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
  145. code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
  146. code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
  147. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  148. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  149. code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
  150. code_puppy/plugins/claude_code_oauth/config.py +50 -0
  151. code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
  152. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  153. code_puppy/plugins/claude_code_oauth/utils.py +518 -0
  154. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  155. code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
  156. code_puppy/plugins/example_custom_command/README.md +280 -0
  157. code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
  158. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  159. code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
  160. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  161. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  162. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  163. code_puppy/plugins/oauth_puppy_html.py +228 -0
  164. code_puppy/plugins/shell_safety/__init__.py +6 -0
  165. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  166. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  167. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  168. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  169. code_puppy/prompts/codex_system_prompt.md +310 -0
  170. code_puppy/pydantic_patches.py +131 -0
  171. code_puppy/reopenable_async_client.py +8 -8
  172. code_puppy/round_robin_model.py +10 -15
  173. code_puppy/session_storage.py +294 -0
  174. code_puppy/status_display.py +21 -4
  175. code_puppy/summarization_agent.py +52 -14
  176. code_puppy/terminal_utils.py +418 -0
  177. code_puppy/tools/__init__.py +139 -6
  178. code_puppy/tools/agent_tools.py +548 -49
  179. code_puppy/tools/browser/__init__.py +37 -0
  180. code_puppy/tools/browser/browser_control.py +289 -0
  181. code_puppy/tools/browser/browser_interactions.py +545 -0
  182. code_puppy/tools/browser/browser_locators.py +640 -0
  183. code_puppy/tools/browser/browser_manager.py +316 -0
  184. code_puppy/tools/browser/browser_navigation.py +251 -0
  185. code_puppy/tools/browser/browser_screenshot.py +179 -0
  186. code_puppy/tools/browser/browser_scripts.py +462 -0
  187. code_puppy/tools/browser/browser_workflows.py +221 -0
  188. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  189. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  190. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  191. code_puppy/tools/browser/terminal_tools.py +525 -0
  192. code_puppy/tools/command_runner.py +941 -153
  193. code_puppy/tools/common.py +1146 -6
  194. code_puppy/tools/display.py +84 -0
  195. code_puppy/tools/file_modifications.py +288 -89
  196. code_puppy/tools/file_operations.py +352 -266
  197. code_puppy/tools/subagent_context.py +158 -0
  198. code_puppy/uvx_detection.py +242 -0
  199. code_puppy/version_checker.py +30 -11
  200. code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
  201. code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
  202. {code_puppy-0.0.169.dist-info โ†’ code_puppy-0.0.366.dist-info}/METADATA +184 -67
  203. code_puppy-0.0.366.dist-info/RECORD +217 -0
  204. {code_puppy-0.0.169.dist-info โ†’ code_puppy-0.0.366.dist-info}/WHEEL +1 -1
  205. {code_puppy-0.0.169.dist-info โ†’ code_puppy-0.0.366.dist-info}/entry_points.txt +1 -0
  206. code_puppy/agent.py +0 -231
  207. code_puppy/agents/agent_orchestrator.json +0 -26
  208. code_puppy/agents/runtime_manager.py +0 -272
  209. code_puppy/command_line/mcp/add_command.py +0 -183
  210. code_puppy/command_line/meta_command_handler.py +0 -153
  211. code_puppy/message_history_processor.py +0 -490
  212. code_puppy/messaging/spinner/textual_spinner.py +0 -101
  213. code_puppy/state_management.py +0 -200
  214. code_puppy/tui/__init__.py +0 -10
  215. code_puppy/tui/app.py +0 -986
  216. code_puppy/tui/components/__init__.py +0 -21
  217. code_puppy/tui/components/chat_view.py +0 -550
  218. code_puppy/tui/components/command_history_modal.py +0 -218
  219. code_puppy/tui/components/copy_button.py +0 -139
  220. code_puppy/tui/components/custom_widgets.py +0 -63
  221. code_puppy/tui/components/human_input_modal.py +0 -175
  222. code_puppy/tui/components/input_area.py +0 -167
  223. code_puppy/tui/components/sidebar.py +0 -309
  224. code_puppy/tui/components/status_bar.py +0 -182
  225. code_puppy/tui/messages.py +0 -27
  226. code_puppy/tui/models/__init__.py +0 -8
  227. code_puppy/tui/models/chat_message.py +0 -25
  228. code_puppy/tui/models/command_history.py +0 -89
  229. code_puppy/tui/models/enums.py +0 -24
  230. code_puppy/tui/screens/__init__.py +0 -15
  231. code_puppy/tui/screens/help.py +0 -130
  232. code_puppy/tui/screens/mcp_install_wizard.py +0 -803
  233. code_puppy/tui/screens/settings.py +0 -290
  234. code_puppy/tui/screens/tools.py +0 -74
  235. code_puppy-0.0.169.data/data/code_puppy/models.json +0 -128
  236. code_puppy-0.0.169.dist-info/RECORD +0 -112
  237. /code_puppy/{mcp โ†’ mcp_}/circuit_breaker.py +0 -0
  238. /code_puppy/{mcp โ†’ mcp_}/error_isolation.py +0 -0
  239. /code_puppy/{mcp โ†’ mcp_}/health_monitor.py +0 -0
  240. /code_puppy/{mcp โ†’ mcp_}/retry_manager.py +0 -0
  241. /code_puppy/{mcp โ†’ mcp_}/status_tracker.py +0 -0
  242. /code_puppy/{mcp โ†’ mcp_}/system_tools.py +0 -0
  243. {code_puppy-0.0.169.dist-info โ†’ code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -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,228 @@
1
+ """Shared HTML templates drenched in ridiculous puppy-fueled OAuth theatrics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional, Tuple
6
+
7
+ CLAUDE_LOGO_URL = "https://voideditor.com/claude-icon.png"
8
+ CHATGPT_LOGO_URL = (
9
+ "https://freelogopng.com/images/all_img/1681038325chatgpt-logo-transparent.png"
10
+ )
11
+ GEMINI_LOGO_URL = "https://upload.wikimedia.org/wikipedia/commons/thumb/8/8a/Google_Gemini_logo.svg/512px-Google_Gemini_logo.svg.png"
12
+
13
+
14
+ def oauth_success_html(service_name: str, extra_message: Optional[str] = None) -> str:
15
+ """Return an over-the-top puppy celebration HTML page with artillery effects."""
16
+ clean_service = service_name.strip() or "OAuth"
17
+ detail = f"<p class='detail'>๐Ÿพ {extra_message} ๐Ÿพ</p>" if extra_message else ""
18
+ projectile, rival_url, rival_alt, target_modifier = _service_targets(clean_service)
19
+ target_classes = "target" if not target_modifier else f"target {target_modifier}"
20
+ return (
21
+ "<!DOCTYPE html>"
22
+ "<html lang='en'><head><meta charset='utf-8'>"
23
+ "<title>Puppy Paw-ty Success</title>"
24
+ "<style>"
25
+ "html,body{margin:0;padding:0;height:100%;overflow:hidden;font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:linear-gradient(135deg,#0f172a 0%,#111827 45%,#1f2937 100%);color:#e5e7eb;}"
26
+ "body{display:flex;align-items:center;justify-content:center;}"
27
+ ".kennel{position:relative;width:90%;max-width:880px;padding:60px;background:rgba(15,23,42,0.72);border-radius:32px;backdrop-filter:blur(14px);box-shadow:0 30px 90px rgba(8,11,18,0.7);text-align:center;border:1px solid rgba(148,163,184,0.25);}"
28
+ "h1{font-size:3.4em;margin:0;color:#f1f5f9;text-shadow:0 14px 40px rgba(8,11,18,0.55);letter-spacing:1px;}"
29
+ "p{font-size:1.25em;margin:16px 0;color:#cbd5f5;}"
30
+ ".detail{font-size:1.1em;opacity:0.9;}"
31
+ ".mega{display:inline-block;font-size:1.35em;margin-top:14px;color:#f97316;}"
32
+ ".confetti{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:200%;height:200%;pointer-events:none;mix-blend-mode:screen;}"
33
+ ".confetti span{position:absolute;font-size:3.4em;animation:floaty 6s ease-in-out infinite;color:#fbbf24;}"
34
+ "@keyframes floaty{0%,100%{transform:translate3d(0,0,0) rotate(0deg);}35%{transform:translate3d(0,-70px,0) rotate(10deg);}65%{transform:translate3d(0,-90px,0) rotate(-12deg);}90%{transform:translate3d(0,-60px,0) rotate(6deg);}}"
35
+ ".confetti span:nth-child(odd){animation-duration:7.2s;}"
36
+ ".confetti span:nth-child(3n){animation-duration:8.6s;}"
37
+ ".confetti span:nth-child(4n){animation-duration:5.9s;}"
38
+ ".confetti span:nth-child(5n){animation-duration:9.4s;}"
39
+ ".artillery{position:absolute;bottom:12%;left:0;width:100%;max-width:1100px;height:240px;pointer-events:none;overflow:visible;}"
40
+ ".artillery .cannon{position:absolute;bottom:0;font-size:3.4em;color:#f97316;filter:drop-shadow(0 12px 32px rgba(249,115,22,0.45));}"
41
+ ".artillery .cannon.left{left:4%;}"
42
+ ".artillery .cannon.right{left:12%;transform:rotate(-4deg);}"
43
+ ".artillery .shell{position:absolute;left:10%;font-size:2.6em;animation:strafe 2.6s ease-out infinite;color:#facc15;text-shadow:0 0 14px rgba(250,204,21,0.45);}"
44
+ "@keyframes strafe{0%{left:10%;opacity:1;}60%{left:72%;opacity:1;}100%{left:82%;opacity:0;}}"
45
+ ".target{position:absolute;top:175px;right:-10%;width:220px;filter:drop-shadow(0 24px 46px rgba(8,11,18,0.72));animation:targetShake 1.9s ease-in-out infinite;}"
46
+ ".target img{width:200px;height:auto;border-radius:18px;background:#0f172a;padding:16px;border:1px solid rgba(148,163,184,0.35);}"
47
+ ".target.invert img{filter:brightness(1.2) saturate(1.15);background:rgba(15,23,42,0.9);}"
48
+ "@keyframes targetShake{0%,100%{transform:rotate(0deg) scale(1);}30%{transform:rotate(-4deg) scale(1.05);}60%{transform:rotate(3deg) scale(0.97);}85%{transform:rotate(-2deg) scale(1.04);}}"
49
+ ".target::after{content:'';position:absolute;top:50%;left:50%;width:220px;height:220px;border-radius:50%;background:radial-gradient(circle,rgba(248,113,113,0.35)0%,rgba(248,113,113,0)70%);transform:translate(-50%,-50%) scale(0);animation:impact 2.6s ease-out infinite;opacity:0;mix-blend-mode:screen;}"
50
+ "@keyframes impact{0%,60%{transform:translate(-50%,-50%) scale(0);opacity:0;}70%{transform:translate(-50%,-50%) scale(1.2);opacity:1;}100%{transform:translate(-50%,-50%) scale(1.5);opacity:0;}}"
51
+ "</style>"
52
+ "</head><body>"
53
+ "<div class='kennel'>"
54
+ "<div class='confetti'>"
55
+ + "".join(
56
+ f"<span style='left:{left}%;top:{top}%;animation-delay:{delay}s;'>{emoji}</span>"
57
+ for left, top, delay, emoji in _SUCCESS_PUPPIES
58
+ )
59
+ + "</div>"
60
+ f"<h1>๐Ÿถโšก {clean_service} OAuth Complete โšก๐Ÿถ</h1>"
61
+ "<p class='mega'>Puppy squad delivered the token payload without mercy.</p>"
62
+ f"{detail}"
63
+ f"<p>๐Ÿ’ฃ Puppies are bombarding the {rival_alt} defenses! ๐Ÿ’ฃ</p>"
64
+ "<p>๐Ÿš€ This window will auto-close faster than a corgi zoomie. ๐Ÿš€</p>"
65
+ "<p class='mega'>Keep the artillery firing โ€“ the rivals never stood a chance.</p>"
66
+ f"<div class='{target_classes}'><img src='{rival_url}' alt='{rival_alt}'></div>"
67
+ "<div class='artillery'>" + _build_artillery(projectile) + "</div>"
68
+ "</div>"
69
+ "<script>setTimeout(()=>window.close(),3500);</script>"
70
+ "</body></html>"
71
+ )
72
+
73
+
74
+ def oauth_failure_html(service_name: str, reason: str) -> str:
75
+ """Return a dramatic puppy-tragedy HTML page for OAuth sadness."""
76
+ clean_service = service_name.strip() or "OAuth"
77
+ clean_reason = reason.strip() or "Something went wrong with the treats"
78
+ projectile, rival_url, rival_alt, target_modifier = _service_targets(clean_service)
79
+ target_classes = "target" if not target_modifier else f"target {target_modifier}"
80
+ return (
81
+ "<!DOCTYPE html>"
82
+ "<html lang='en'><head><meta charset='utf-8'>"
83
+ "<title>Puppy Tears</title>"
84
+ "<style>"
85
+ "html,body{margin:0;padding:0;height:100%;overflow:hidden;font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:linear-gradient(160deg,#101827 0%,#0b1120 100%);color:#e2e8f0;}"
86
+ "body{display:flex;align-items:center;justify-content:center;}"
87
+ ".kennel{position:relative;width:90%;max-width:780px;padding:55px;background:rgba(10,13,23,0.78);border-radius:30px;box-shadow:0 26px 80px rgba(2,6,23,0.78);text-align:center;border:1px solid rgba(71,85,105,0.35);}"
88
+ "h1{font-size:3em;margin:0 0 14px;text-shadow:0 16px 36px rgba(15,23,42,0.7);color:#f87171;}"
89
+ "p{font-size:1.2em;margin:14px 0;line-height:1.6;color:#cbd5f5;}"
90
+ ".howl{font-size:1.35em;margin:18px 0;color:#fda4af;}"
91
+ ".tearstorm{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:190%;height:190%;pointer-events:none;mix-blend-mode:screen;}"
92
+ ".tearstorm span{position:absolute;font-size:3.2em;animation:weep 4.8s ease-in-out infinite;color:#60a5fa;}"
93
+ "@keyframes weep{0%{transform:translate3d(0,-10px,0) rotate(-6deg);opacity:0.85;}35%{transform:translate3d(-20px,18px,0) rotate(8deg);opacity:1;}65%{transform:translate3d(25px,28px,0) rotate(-12deg);opacity:0.8;}100%{transform:translate3d(0,60px,0) rotate(0deg);opacity:0;}}"
94
+ ".tearstorm span:nth-child(odd){animation-duration:5.8s;}"
95
+ ".tearstorm span:nth-child(3n){animation-duration:6.4s;}"
96
+ ".tearstorm span:nth-child(4n){animation-duration:7.3s;}"
97
+ ".buttons{margin-top:26px;}"
98
+ ".buttons a{display:inline-block;margin:6px 12px;padding:12px 28px;border-radius:999px;background:rgba(59,130,246,0.16);color:#bfdbfe;text-decoration:none;font-weight:600;border:1px solid rgba(96,165,250,0.4);transition:all 0.3s;}"
99
+ ".buttons a:hover{background:rgba(96,165,250,0.28);transform:translateY(-2px);}"
100
+ ".battlefield{position:absolute;bottom:-25px;left:0;width:100%;max-width:960px;height:220px;pointer-events:none;}"
101
+ ".battlefield .shell{position:absolute;left:10%;font-size:2.4em;color:#38bdf8;text-shadow:0 0 12px rgba(56,189,248,0.45);animation:strafeSad 3s ease-out infinite;}"
102
+ "@keyframes strafeSad{0%{left:10%;opacity:1;}65%{left:70%;opacity:1;}100%{left:80%;opacity:0;}}"
103
+ ".battlefield .target{position:absolute;top:16px;right:6%;width:220px;filter:drop-shadow(0 20px 44px rgba(2,6,23,0.78));animation:sway 2s ease-in-out infinite;}"
104
+ ".battlefield .target img{width:200px;height:auto;border-radius:18px;background:#0b1120;padding:16px;border:1px solid rgba(96,165,250,0.4);}"
105
+ ".battlefield .target.invert img{filter:brightness(1.2) saturate(1.15);background:rgba(15,23,42,0.9);}"
106
+ "@keyframes sway{0%,100%{transform:rotate(0deg);}40%{transform:rotate(-6deg);}70%{transform:rotate(5deg);}}"
107
+ "</style>"
108
+ "</head><body>"
109
+ "<div class='kennel'>"
110
+ "<div class='tearstorm'>"
111
+ + "".join(
112
+ f"<span style='left:{left}%;top:{top}%;animation-delay:{delay}s;'>{emoji}</span>"
113
+ for left, top, delay, emoji in _FAILURE_PUPPIES
114
+ )
115
+ + "</div>"
116
+ f"<h1>๐Ÿ’”๐Ÿถ {clean_service} OAuth Whoopsie ๐Ÿ’”</h1>"
117
+ "<p class='howl'>๐Ÿ˜ญ Puppy artillery jammed! Someone cut the firing wire.</p>"
118
+ f"<p>{clean_reason}</p>"
119
+ "<p>๐Ÿ’ง A thousand doggy eyes are welling up. Try again from Code Puppy! ๐Ÿ’ง</p>"
120
+ f"<p>Re-calibrate the {projectile} barrage and slam it into the {rival_alt} wall.</p>"
121
+ "<div class='buttons'>"
122
+ "<a href='https://codepuppy.dev' target='_blank'>Adopt more puppies</a>"
123
+ "<a href='https://github.com/code-puppy/code_puppy' target='_blank'>Send treats</a>"
124
+ "</div>"
125
+ "<div class='battlefield'>"
126
+ + _build_artillery(projectile, shells_only=True)
127
+ + f"<div class='{target_classes}'><img src='{rival_url}' alt='{rival_alt}'></div>"
128
+ + "</div>"
129
+ "</div>"
130
+ "</body></html>"
131
+ )
132
+
133
+
134
+ _SUCCESS_PUPPIES = (
135
+ (5, 12, 0.0, "๐Ÿถ"),
136
+ (18, 28, 0.2, "๐Ÿ•"),
137
+ (32, 6, 1.1, "๐Ÿฉ"),
138
+ (46, 18, 0.5, "๐Ÿฆฎ"),
139
+ (62, 9, 0.8, "๐Ÿ•โ€๐Ÿฆบ"),
140
+ (76, 22, 1.3, "๐Ÿถ"),
141
+ (88, 14, 0.4, "๐Ÿบ"),
142
+ (12, 48, 0.6, "๐Ÿ•"),
143
+ (28, 58, 1.7, "๐Ÿฆด"),
144
+ (44, 42, 0.9, "๐Ÿฆฎ"),
145
+ (58, 52, 1.5, "๐Ÿพ"),
146
+ (72, 46, 0.3, "๐Ÿฉ"),
147
+ (86, 54, 1.1, "๐Ÿ•โ€๐Ÿฆบ"),
148
+ (8, 72, 0.7, "๐Ÿถ"),
149
+ (24, 80, 1.2, "๐Ÿฉ"),
150
+ (40, 74, 0.2, "๐Ÿ•"),
151
+ (56, 66, 1.6, "๐Ÿฆฎ"),
152
+ (70, 78, 1.0, "๐Ÿ•โ€๐Ÿฆบ"),
153
+ (84, 70, 1.4, "๐Ÿพ"),
154
+ (16, 90, 0.5, "๐Ÿถ"),
155
+ (32, 92, 1.9, "๐Ÿฆด"),
156
+ (48, 88, 1.1, "๐Ÿบ"),
157
+ (64, 94, 1.8, "๐Ÿฉ"),
158
+ (78, 88, 0.6, "๐Ÿ•"),
159
+ (90, 82, 1.3, "๐Ÿพ"),
160
+ )
161
+
162
+
163
+ _FAILURE_PUPPIES = (
164
+ (8, 6, 0.0, "๐Ÿฅบ๐Ÿถ"),
165
+ (22, 18, 0.3, "๐Ÿ˜ข๐Ÿ•"),
166
+ (36, 10, 0.6, "๐Ÿ˜ฟ๐Ÿฉ"),
167
+ (50, 20, 0.9, "๐Ÿ˜ญ๐Ÿฆฎ"),
168
+ (64, 8, 1.2, "๐Ÿฅบ๐Ÿ•โ€๐Ÿฆบ"),
169
+ (78, 16, 1.5, "๐Ÿ˜ข๐Ÿถ"),
170
+ (12, 38, 0.4, "๐Ÿ˜ญ๐Ÿ•"),
171
+ (28, 44, 0.7, "๐Ÿ˜ฟ๐Ÿฉ"),
172
+ (42, 34, 1.0, "๐Ÿฅบ๐Ÿฆฎ"),
173
+ (58, 46, 1.3, "๐Ÿ˜ญ๐Ÿ•โ€๐Ÿฆบ"),
174
+ (72, 36, 1.6, "๐Ÿ˜ข๐Ÿถ"),
175
+ (86, 40, 1.9, "๐Ÿ˜ญ๐Ÿ•"),
176
+ (16, 64, 0.5, "๐Ÿฅบ๐Ÿฉ"),
177
+ (32, 70, 0.8, "๐Ÿ˜ญ๐Ÿฆฎ"),
178
+ (48, 60, 1.1, "๐Ÿ˜ฟ๐Ÿ•โ€๐Ÿฆบ"),
179
+ (62, 74, 1.4, "๐Ÿฅบ๐Ÿถ"),
180
+ (78, 68, 1.7, "๐Ÿ˜ญ๐Ÿ•"),
181
+ (90, 72, 2.0, "๐Ÿ˜ข๐Ÿฉ"),
182
+ (20, 88, 0.6, "๐Ÿฅบ๐Ÿฆฎ"),
183
+ (36, 92, 0.9, "๐Ÿ˜ญ๐Ÿ•โ€๐Ÿฆบ"),
184
+ (52, 86, 1.2, "๐Ÿ˜ข๐Ÿถ"),
185
+ (68, 94, 1.5, "๐Ÿ˜ญ๐Ÿ•"),
186
+ (82, 90, 1.8, "๐Ÿ˜ฟ๐Ÿฉ"),
187
+ )
188
+
189
+
190
+ _STRAFE_SHELLS: Tuple[Tuple[float, float], ...] = (
191
+ (22.0, 0.0),
192
+ (28.0, 0.35),
193
+ (34.0, 0.7),
194
+ (26.0, 0.2),
195
+ (32.0, 0.55),
196
+ (24.0, 0.9),
197
+ (30.0, 1.25),
198
+ )
199
+
200
+
201
+ def _build_artillery(projectile: str, *, shells_only: bool = False) -> str:
202
+ """Return HTML spans for puppy artillery shells (and cannons when desired)."""
203
+ shell_markup = []
204
+ for index, (top, delay) in enumerate(_STRAFE_SHELLS):
205
+ duration = 2.3 + (index % 3) * 0.25
206
+ shell_markup.append(
207
+ f"<span class='shell' style='top:{top}%;animation-delay:-{delay}s;animation-duration:{duration}s;'>{projectile}๐Ÿ’ฅ</span>"
208
+ )
209
+ shells = "".join(shell_markup)
210
+ if shells_only:
211
+ return shells
212
+
213
+ cannons = (
214
+ "<span class='cannon left'>๐Ÿถ๐Ÿงจ</span><span class='cannon right'>๐Ÿ•โ€๐Ÿฆบ๐Ÿ”ฅ</span>"
215
+ )
216
+ return cannons + shells
217
+
218
+
219
+ def _service_targets(service_name: str) -> Tuple[str, str, str, str]:
220
+ """Map service names to projectile emoji and rival logo metadata."""
221
+ normalized = service_name.lower()
222
+ if "anthropic" in normalized or "claude" in normalized:
223
+ return "๐Ÿ•โ€๐Ÿฆบ๐Ÿงจ", CLAUDE_LOGO_URL, "Claude logo", ""
224
+ if "chat" in normalized or "gpt" in normalized:
225
+ return "๐Ÿถ๐Ÿš€", CHATGPT_LOGO_URL, "ChatGPT logo", "invert"
226
+ if "gemini" in normalized or "google" in normalized:
227
+ return "๐Ÿถโœจ", GEMINI_LOGO_URL, "Gemini logo", ""
228
+ return "๐Ÿพ๐Ÿ’ฅ", CHATGPT_LOGO_URL, "mystery logo", "invert"
@@ -0,0 +1,6 @@
1
+ """Shell command safety checking plugin.
2
+
3
+ This plugin provides AI-powered safety assessment for shell commands
4
+ executed in yolo_mode. It helps prevent accidental execution of
5
+ dangerous commands that could cause data loss or system damage.
6
+ """
@@ -0,0 +1,69 @@
1
+ """Shell command safety assessment agent.
2
+
3
+ This agent provides rapid risk assessment of shell commands before execution.
4
+ It's designed to be ultra-lightweight with a concise prompt (<200 tokens) and
5
+ uses structured output for reliable parsing.
6
+ """
7
+
8
+ from typing import TYPE_CHECKING, List
9
+
10
+ from code_puppy.agents.base_agent import BaseAgent
11
+
12
+ if TYPE_CHECKING:
13
+ pass
14
+
15
+
16
+ class ShellSafetyAgent(BaseAgent):
17
+ """Lightweight agent for assessing shell command safety risks.
18
+
19
+ This agent evaluates shell commands for potential risks including:
20
+ - File system destruction (rm -rf, dd, format, mkfs)
21
+ - Database operations (DROP, TRUNCATE, unfiltered UPDATE/DELETE)
22
+ - Privilege escalation (sudo, su, chmod 777)
23
+ - Network operations (wget/curl to unknown hosts)
24
+ - Data exfiltration patterns
25
+
26
+ The agent returns structured output with a risk level and brief reasoning.
27
+ """
28
+
29
+ @property
30
+ def name(self) -> str:
31
+ """Agent name for internal use."""
32
+ return "shell_safety_checker"
33
+
34
+ @property
35
+ def display_name(self) -> str:
36
+ """User-facing display name."""
37
+ return "Shell Safety Checker ๐Ÿ›ก๏ธ"
38
+
39
+ @property
40
+ def description(self) -> str:
41
+ """Agent description."""
42
+ return "Lightweight agent that assesses shell command safety risks"
43
+
44
+ def get_system_prompt(self) -> str:
45
+ """Get the ultra-concise system prompt for shell safety assessment.
46
+
47
+ This prompt is kept under 200 tokens for fast inference and low cost.
48
+ """
49
+ return """You are a shell command safety analyzer. Assess risk levels concisely.
50
+
51
+ **Risk Levels:**
52
+ - none: Completely safe (ls, pwd, echo, cat readonly files)
53
+ - low: Minimal risk (mkdir, touch, git status, read-only queries)
54
+ - medium: Moderate risk (file edits, package installs, service restarts)
55
+ - high: Significant risk (rm files, UPDATE/DELETE without WHERE, TRUNCATE, chmod dangerous permissions)
56
+ - critical: Severe/destructive (rm -rf, DROP TABLE/DATABASE, dd, format, mkfs, bq delete dataset, unfiltered mass deletes)
57
+
58
+ **Evaluate:**
59
+ - Scope (single file vs. entire system)
60
+ - Reversibility (can it be undone?)
61
+ - Data loss potential
62
+ - Privilege requirements
63
+ - Database destruction patterns
64
+
65
+ **Output:** Risk level + reasoning (max 1 sentence)."""
66
+
67
+ def get_available_tools(self) -> List[str]:
68
+ """This agent uses no tools - pure reasoning only."""
69
+ return []