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,276 @@
1
+ """Silenced event stream handler for sub-agents.
2
+
3
+ This handler suppresses all console output but still:
4
+ - Updates SubAgentConsoleManager with status/metrics
5
+ - Fires stream_event callbacks for the frontend emitter plugin
6
+ - Tracks tool calls, tokens, and status changes
7
+
8
+ Usage:
9
+ >>> from code_puppy.agents.subagent_stream_handler import subagent_stream_handler
10
+ >>> # In agent run:
11
+ >>> await subagent_stream_handler(ctx, events, session_id="my-session-123")
12
+ """
13
+
14
+ import asyncio
15
+ import logging
16
+ from collections.abc import AsyncIterable
17
+ from typing import Any, Optional
18
+
19
+ from pydantic_ai import PartDeltaEvent, PartEndEvent, PartStartEvent, RunContext
20
+ from pydantic_ai.messages import (
21
+ TextPart,
22
+ TextPartDelta,
23
+ ThinkingPart,
24
+ ThinkingPartDelta,
25
+ ToolCallPart,
26
+ ToolCallPartDelta,
27
+ )
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ # =============================================================================
33
+ # Callback Helper
34
+ # =============================================================================
35
+
36
+
37
+ def _fire_callback(event_type: str, event_data: Any, session_id: Optional[str]) -> None:
38
+ """Fire stream_event callback non-blocking.
39
+
40
+ Schedules the callback to run asynchronously without waiting for it.
41
+ Silently ignores errors if no event loop is running or if the callback
42
+ system is unavailable.
43
+
44
+ Args:
45
+ event_type: Type of the event ('part_start', 'part_delta', 'part_end')
46
+ event_data: Dictionary containing event-specific data
47
+ session_id: Optional session ID for the sub-agent
48
+ """
49
+ try:
50
+ from code_puppy import callbacks
51
+
52
+ loop = asyncio.get_running_loop()
53
+ loop.create_task(callbacks.on_stream_event(event_type, event_data, session_id))
54
+ except RuntimeError:
55
+ # No event loop running - this can happen during shutdown
56
+ logger.debug("No event loop available for stream event callback")
57
+ except ImportError:
58
+ # Callbacks module not available
59
+ logger.debug("Callbacks module not available for stream event")
60
+ except Exception as e:
61
+ # Don't let callback errors break the stream handler
62
+ logger.debug(f"Error firing stream event callback: {e}")
63
+
64
+
65
+ # =============================================================================
66
+ # Token Estimation
67
+ # =============================================================================
68
+
69
+
70
+ def _estimate_tokens(content: str) -> int:
71
+ """Estimate token count from content string.
72
+
73
+ Uses a rough heuristic: ~4 characters per token for English text.
74
+ This is a ballpark estimate - actual tokenization varies by model.
75
+
76
+ Args:
77
+ content: The text content to estimate tokens for
78
+
79
+ Returns:
80
+ Estimated token count (minimum 1 for non-empty content)
81
+ """
82
+ if not content:
83
+ return 0
84
+ # Rough estimate: 4 chars = 1 token, minimum 1 for any content
85
+ return max(1, len(content) // 4)
86
+
87
+
88
+ # =============================================================================
89
+ # Main Handler
90
+ # =============================================================================
91
+
92
+
93
+ async def subagent_stream_handler(
94
+ ctx: RunContext,
95
+ events: AsyncIterable[Any],
96
+ session_id: Optional[str] = None,
97
+ ) -> None:
98
+ """Silent event stream handler for sub-agents.
99
+
100
+ Processes streaming events without producing any console output.
101
+ Updates the SubAgentConsoleManager with status and metrics, and fires
102
+ stream_event callbacks for any registered listeners.
103
+
104
+ Args:
105
+ ctx: The pydantic-ai run context
106
+ events: Async iterable of streaming events (PartStartEvent,
107
+ PartDeltaEvent, PartEndEvent)
108
+ session_id: Session ID of the sub-agent for console manager updates.
109
+ If None, falls back to get_session_context().
110
+ """
111
+ # Late import to avoid circular dependencies
112
+ from code_puppy.messaging import get_session_context
113
+ from code_puppy.messaging.subagent_console import SubAgentConsoleManager
114
+
115
+ manager = SubAgentConsoleManager.get_instance()
116
+
117
+ # Resolve session_id, falling back to context if not provided
118
+ effective_session_id = session_id or get_session_context()
119
+
120
+ # Metrics tracking
121
+ token_count = 0
122
+ tool_call_count = 0
123
+ active_tool_parts: set[int] = set() # Track active tool call indices
124
+
125
+ async for event in events:
126
+ try:
127
+ await _handle_event(
128
+ event=event,
129
+ manager=manager,
130
+ session_id=effective_session_id,
131
+ token_count=token_count,
132
+ tool_call_count=tool_call_count,
133
+ active_tool_parts=active_tool_parts,
134
+ )
135
+
136
+ # Update metrics from returned values
137
+ # (we need to track these at this level since they're modified in _handle_event)
138
+ if isinstance(event, PartStartEvent):
139
+ if isinstance(event.part, ToolCallPart):
140
+ tool_call_count += 1
141
+ active_tool_parts.add(event.index)
142
+
143
+ elif isinstance(event, PartDeltaEvent):
144
+ delta = event.delta
145
+ if isinstance(delta, (TextPartDelta, ThinkingPartDelta)):
146
+ if delta.content_delta:
147
+ token_count += _estimate_tokens(delta.content_delta)
148
+
149
+ elif isinstance(event, PartEndEvent):
150
+ active_tool_parts.discard(event.index)
151
+
152
+ except Exception as e:
153
+ # Log but don't crash on event handling errors
154
+ logger.debug(f"Error handling stream event: {e}")
155
+ continue
156
+
157
+
158
+ async def _handle_event(
159
+ event: Any,
160
+ manager: Any, # SubAgentConsoleManager
161
+ session_id: Optional[str],
162
+ token_count: int,
163
+ tool_call_count: int,
164
+ active_tool_parts: set[int],
165
+ ) -> None:
166
+ """Handle a single streaming event.
167
+
168
+ Updates the console manager and fires callbacks for each event type.
169
+
170
+ Args:
171
+ event: The streaming event to handle
172
+ manager: SubAgentConsoleManager instance
173
+ session_id: Session ID for updates
174
+ token_count: Current token count
175
+ tool_call_count: Current tool call count
176
+ active_tool_parts: Set of active tool call indices
177
+ """
178
+ if session_id is None:
179
+ # Can't update manager without session_id
180
+ logger.debug("No session_id available for stream event")
181
+ return
182
+
183
+ # -------------------------------------------------------------------------
184
+ # PartStartEvent - Track new parts and update status
185
+ # -------------------------------------------------------------------------
186
+ if isinstance(event, PartStartEvent):
187
+ part = event.part
188
+ event_data = {
189
+ "index": event.index,
190
+ "part_type": type(part).__name__,
191
+ }
192
+
193
+ if isinstance(part, ThinkingPart):
194
+ manager.update_agent(session_id, status="thinking")
195
+ event_data["content"] = getattr(part, "content", None)
196
+
197
+ elif isinstance(part, TextPart):
198
+ manager.update_agent(session_id, status="running")
199
+ event_data["content"] = getattr(part, "content", None)
200
+
201
+ elif isinstance(part, ToolCallPart):
202
+ # tool_call_count is updated in the main handler
203
+ manager.update_agent(
204
+ session_id,
205
+ status="tool_calling",
206
+ tool_call_count=tool_call_count + 1, # +1 for this new one
207
+ current_tool=part.tool_name,
208
+ )
209
+ event_data["tool_name"] = part.tool_name
210
+ event_data["tool_call_id"] = getattr(part, "tool_call_id", None)
211
+
212
+ _fire_callback("part_start", event_data, session_id)
213
+
214
+ # -------------------------------------------------------------------------
215
+ # PartDeltaEvent - Track content deltas and update metrics
216
+ # -------------------------------------------------------------------------
217
+ elif isinstance(event, PartDeltaEvent):
218
+ delta = event.delta
219
+ event_data = {
220
+ "index": event.index,
221
+ "delta_type": type(delta).__name__,
222
+ }
223
+
224
+ if isinstance(delta, TextPartDelta):
225
+ content_delta = delta.content_delta
226
+ if content_delta:
227
+ # Token count is updated in main handler
228
+ new_token_count = token_count + _estimate_tokens(content_delta)
229
+ manager.update_agent(session_id, token_count=new_token_count)
230
+ event_data["content_delta"] = content_delta
231
+
232
+ elif isinstance(delta, ThinkingPartDelta):
233
+ content_delta = delta.content_delta
234
+ if content_delta:
235
+ new_token_count = token_count + _estimate_tokens(content_delta)
236
+ manager.update_agent(session_id, token_count=new_token_count)
237
+ event_data["content_delta"] = content_delta
238
+
239
+ elif isinstance(delta, ToolCallPartDelta):
240
+ # Tool call deltas might have partial args
241
+ event_data["args_delta"] = getattr(delta, "args_delta", None)
242
+ event_data["tool_name_delta"] = getattr(delta, "tool_name_delta", None)
243
+
244
+ _fire_callback("part_delta", event_data, session_id)
245
+
246
+ # -------------------------------------------------------------------------
247
+ # PartEndEvent - Track part completion and update status
248
+ # -------------------------------------------------------------------------
249
+ elif isinstance(event, PartEndEvent):
250
+ event_data = {
251
+ "index": event.index,
252
+ "next_part_kind": getattr(event, "next_part_kind", None),
253
+ }
254
+
255
+ # If this was a tool call part ending, check if we should reset status
256
+ if event.index in active_tool_parts:
257
+ # Remove this index from active parts (done in main handler)
258
+ # If no more active tool parts after removal, reset to running
259
+ remaining_active = active_tool_parts - {event.index}
260
+ if not remaining_active:
261
+ manager.update_agent(
262
+ session_id,
263
+ current_tool=None,
264
+ status="running",
265
+ )
266
+
267
+ _fire_callback("part_end", event_data, session_id)
268
+
269
+
270
+ # =============================================================================
271
+ # Exports
272
+ # =============================================================================
273
+
274
+ __all__ = [
275
+ "subagent_stream_handler",
276
+ ]
@@ -0,0 +1,13 @@
1
+ """Code Puppy REST API module.
2
+
3
+ This module provides a FastAPI-based REST API for Code Puppy configuration,
4
+ sessions, commands, and real-time WebSocket communication.
5
+
6
+ Exports:
7
+ create_app: Factory function to create the FastAPI application
8
+ main: Entry point to run the server
9
+ """
10
+
11
+ from code_puppy.api.app import create_app
12
+
13
+ __all__ = ["create_app"]
code_puppy/api/app.py ADDED
@@ -0,0 +1,169 @@
1
+ """FastAPI application factory for Code Puppy API."""
2
+
3
+ import asyncio
4
+ import logging
5
+ from contextlib import asynccontextmanager
6
+ from pathlib import Path
7
+ from typing import AsyncGenerator
8
+
9
+ from fastapi import FastAPI, Request
10
+ from fastapi.middleware.cors import CORSMiddleware
11
+ from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
12
+ from starlette.middleware.base import BaseHTTPMiddleware
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # Default request timeout (seconds) - fail fast!
17
+ REQUEST_TIMEOUT = 30.0
18
+
19
+
20
+ class TimeoutMiddleware(BaseHTTPMiddleware):
21
+ """Middleware to enforce request timeouts and prevent hanging requests."""
22
+
23
+ def __init__(self, app, timeout: float = REQUEST_TIMEOUT):
24
+ super().__init__(app)
25
+ self.timeout = timeout
26
+
27
+ async def dispatch(self, request: Request, call_next):
28
+ # Skip timeout for WebSocket upgrades and streaming endpoints
29
+ if request.headers.get(
30
+ "upgrade", ""
31
+ ).lower() == "websocket" or request.url.path.startswith("/ws/"):
32
+ return await call_next(request)
33
+
34
+ try:
35
+ return await asyncio.wait_for(
36
+ call_next(request),
37
+ timeout=self.timeout,
38
+ )
39
+ except asyncio.TimeoutError:
40
+ return JSONResponse(
41
+ status_code=504,
42
+ content={
43
+ "detail": f"Request timed out after {self.timeout}s",
44
+ "error": "timeout",
45
+ },
46
+ )
47
+
48
+
49
+ @asynccontextmanager
50
+ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
51
+ """Lifespan context manager for startup and shutdown events.
52
+
53
+ Handles graceful cleanup of resources when the server shuts down.
54
+ """
55
+ # Startup: nothing special needed yet, but this is where you'd do it
56
+ logger.info("🐶 Code Puppy API starting up...")
57
+ yield
58
+ # Shutdown: clean up all the things!
59
+ logger.info("🐶 Code Puppy API shutting down, cleaning up...")
60
+
61
+ # 1. Close all PTY sessions
62
+ try:
63
+ from code_puppy.api.pty_manager import get_pty_manager
64
+
65
+ pty_manager = get_pty_manager()
66
+ await pty_manager.close_all()
67
+ logger.info("✓ All PTY sessions closed")
68
+ except Exception as e:
69
+ logger.error(f"Error closing PTY sessions: {e}")
70
+
71
+ # 2. Remove PID file so /api status knows we're gone
72
+ try:
73
+ from code_puppy.config import STATE_DIR
74
+
75
+ pid_file = Path(STATE_DIR) / "api_server.pid"
76
+ if pid_file.exists():
77
+ pid_file.unlink()
78
+ logger.info("✓ PID file removed")
79
+ except Exception as e:
80
+ logger.error(f"Error removing PID file: {e}")
81
+
82
+
83
+ def create_app() -> FastAPI:
84
+ """Create and configure the FastAPI application."""
85
+ app = FastAPI(
86
+ lifespan=lifespan,
87
+ title="Code Puppy API",
88
+ description="REST API and Interactive Terminal for Code Puppy",
89
+ version="1.0.0",
90
+ docs_url="/docs",
91
+ redoc_url="/redoc",
92
+ )
93
+
94
+ # Timeout middleware - added first so it wraps everything
95
+ app.add_middleware(TimeoutMiddleware, timeout=REQUEST_TIMEOUT)
96
+
97
+ # CORS middleware for frontend access
98
+ app.add_middleware(
99
+ CORSMiddleware,
100
+ allow_origins=["*"], # Local/trusted
101
+ allow_credentials=True,
102
+ allow_methods=["*"],
103
+ allow_headers=["*"],
104
+ )
105
+
106
+ # Include routers
107
+ from code_puppy.api.routers import agents, commands, config, sessions
108
+
109
+ app.include_router(config.router, prefix="/api/config", tags=["config"])
110
+ app.include_router(commands.router, prefix="/api/commands", tags=["commands"])
111
+ app.include_router(sessions.router, prefix="/api/sessions", tags=["sessions"])
112
+ app.include_router(agents.router, prefix="/api/agents", tags=["agents"])
113
+
114
+ # WebSocket endpoints (events + terminal)
115
+ from code_puppy.api.websocket import setup_websocket
116
+
117
+ setup_websocket(app)
118
+
119
+ # Templates directory
120
+ templates_dir = Path(__file__).parent / "templates"
121
+
122
+ @app.get("/")
123
+ async def root():
124
+ """Landing page with links to terminal and docs."""
125
+ return HTMLResponse(
126
+ content="""
127
+ <!DOCTYPE html>
128
+ <html>
129
+ <head>
130
+ <title>Code Puppy 🐶</title>
131
+ <script src="https://cdn.tailwindcss.com"></script>
132
+ </head>
133
+ <body class="bg-gray-900 text-white min-h-screen flex items-center justify-center">
134
+ <div class="text-center">
135
+ <h1 class="text-6xl mb-4">🐶</h1>
136
+ <h2 class="text-3xl font-bold mb-8">Code Puppy</h2>
137
+ <div class="space-x-4">
138
+ <a href="/terminal" class="px-6 py-3 bg-blue-600 hover:bg-blue-700 rounded-lg text-lg font-semibold">
139
+ Open Terminal
140
+ </a>
141
+ <a href="/docs" class="px-6 py-3 bg-gray-700 hover:bg-gray-600 rounded-lg text-lg">
142
+ API Docs
143
+ </a>
144
+ </div>
145
+ <p class="mt-8 text-gray-400">
146
+ WebSocket: ws://localhost:8765/ws/terminal
147
+ </p>
148
+ </div>
149
+ </body>
150
+ </html>
151
+ """
152
+ )
153
+
154
+ @app.get("/terminal")
155
+ async def terminal_page():
156
+ """Serve the interactive terminal page."""
157
+ html_file = templates_dir / "terminal.html"
158
+ if html_file.exists():
159
+ return FileResponse(html_file, media_type="text/html")
160
+ return HTMLResponse(
161
+ content="<h1>Terminal template not found</h1>",
162
+ status_code=404,
163
+ )
164
+
165
+ @app.get("/health")
166
+ async def health():
167
+ return {"status": "healthy"}
168
+
169
+ return app
code_puppy/api/main.py ADDED
@@ -0,0 +1,21 @@
1
+ """Entry point for running the FastAPI server."""
2
+
3
+ import uvicorn
4
+
5
+ from code_puppy.api.app import create_app
6
+
7
+ app = create_app()
8
+
9
+
10
+ def main(host: str = "127.0.0.1", port: int = 8765) -> None:
11
+ """Run the FastAPI server.
12
+
13
+ Args:
14
+ host: The host address to bind to. Defaults to localhost.
15
+ port: The port number to listen on. Defaults to 8765.
16
+ """
17
+ uvicorn.run(app, host=host, port=port)
18
+
19
+
20
+ if __name__ == "__main__":
21
+ main()