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,525 @@
1
+ """Terminal connection tools for managing terminal browser connections.
2
+
3
+ This module provides tools for:
4
+ - Checking if the Code Puppy API server is running
5
+ - Opening the terminal browser interface
6
+ - Closing the terminal browser
7
+
8
+ These tools use the ChromiumTerminalManager to manage the browser instance
9
+ and connect to the Code Puppy API server's terminal endpoint.
10
+ """
11
+
12
+ import contextvars
13
+ import logging
14
+ from typing import Any, Dict, Optional
15
+
16
+ import httpx
17
+ from pydantic_ai import RunContext
18
+ from rich.text import Text
19
+
20
+ from code_puppy.messaging import emit_error, emit_info, emit_success
21
+ from code_puppy.tools.browser import format_terminal_banner
22
+ from code_puppy.tools.common import generate_group_id
23
+
24
+ from .chromium_terminal_manager import get_chromium_terminal_manager
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ # Context variable for terminal session - properly inherits through async tasks
29
+ _terminal_session_var: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar(
30
+ "terminal_session", default=None
31
+ )
32
+
33
+
34
+ def set_terminal_session(session_id: Optional[str]) -> contextvars.Token:
35
+ """Set the terminal session ID for the current context.
36
+
37
+ This must be called BEFORE any tool calls that use the terminal.
38
+ The context will properly propagate to all subsequent async calls.
39
+
40
+ Args:
41
+ session_id: The session ID to use for terminal operations.
42
+
43
+ Returns:
44
+ A token that can be used to reset the context.
45
+ """
46
+ return _terminal_session_var.set(session_id)
47
+
48
+
49
+ def get_terminal_session() -> Optional[str]:
50
+ """Get the terminal session ID for the current context.
51
+
52
+ Returns:
53
+ The current session ID, or None if not set.
54
+ """
55
+ return _terminal_session_var.get()
56
+
57
+
58
+ def get_session_manager():
59
+ """Get the ChromiumTerminalManager for the current context's session."""
60
+ session_id = get_terminal_session()
61
+ return get_chromium_terminal_manager(session_id)
62
+
63
+
64
+ def _get_session_from_context(context: RunContext) -> str:
65
+ """Get the session ID for the current context.
66
+
67
+ If no session is set in the context var, generates one based on
68
+ the page URL that was opened (stored in the manager).
69
+
70
+ Args:
71
+ context: The pydantic-ai RunContext from a tool call.
72
+
73
+ Returns:
74
+ A session ID string for the terminal browser.
75
+ """
76
+ # First check if we have a session in the context var
77
+ session = get_terminal_session()
78
+ if session:
79
+ return session
80
+
81
+ # Fallback: return default (all tools share one browser)
82
+ return "default"
83
+
84
+
85
+ # Default timeout for health check requests (seconds)
86
+ HEALTH_CHECK_TIMEOUT = 5.0
87
+
88
+ # How long to wait for xterm.js to load in the terminal page (ms)
89
+ TERMINAL_LOAD_TIMEOUT = 10000
90
+
91
+
92
+ async def check_terminal_server(
93
+ host: str = "localhost", port: int = 8765
94
+ ) -> Dict[str, Any]:
95
+ """Check if the Code Puppy API server is running.
96
+
97
+ Attempts to connect to the /health endpoint of the API server to verify
98
+ it is running and responsive.
99
+
100
+ Args:
101
+ host: The hostname where the server is running. Defaults to "localhost".
102
+ port: The port number for the server. Defaults to 8765.
103
+
104
+ Returns:
105
+ A dictionary containing:
106
+ - success (bool): True if server is healthy, False otherwise.
107
+ - server_url (str): The full URL of the server (if successful).
108
+ - status (str): "healthy" if server is running (if successful).
109
+ - error (str): Error message describing the failure (if unsuccessful).
110
+
111
+ Example:
112
+ >>> result = await check_terminal_server()
113
+ >>> if result["success"]:
114
+ ... print(f"Server running at {result['server_url']}")
115
+ ... else:
116
+ ... print(f"Error: {result['error']}")
117
+ """
118
+ group_id = generate_group_id("terminal_check_server", f"{host}:{port}")
119
+ banner = format_terminal_banner("TERMINAL CHECK SERVER 🔍")
120
+ emit_info(
121
+ Text.from_markup(f"{banner} [bold cyan]{host}:{port}[/bold cyan]"),
122
+ message_group=group_id,
123
+ )
124
+
125
+ server_url = f"http://{host}:{port}"
126
+ health_url = f"{server_url}/health"
127
+
128
+ try:
129
+ async with httpx.AsyncClient(timeout=HEALTH_CHECK_TIMEOUT) as client:
130
+ response = await client.get(health_url)
131
+ response.raise_for_status()
132
+
133
+ # Parse the response to verify it's the expected health check
134
+ health_data = response.json()
135
+
136
+ if health_data.get("status") == "healthy":
137
+ emit_success(
138
+ f"Server is healthy at {server_url}",
139
+ message_group=group_id,
140
+ )
141
+ return {
142
+ "success": True,
143
+ "server_url": server_url,
144
+ "status": "healthy",
145
+ }
146
+ else:
147
+ # Server responded but not with expected health status
148
+ emit_error(
149
+ f"Server responded but health check failed: {health_data}",
150
+ message_group=group_id,
151
+ )
152
+ return {
153
+ "success": False,
154
+ "error": f"Unexpected health response: {health_data}",
155
+ }
156
+
157
+ except httpx.ConnectError:
158
+ error_msg = (
159
+ f"Server not running at {server_url}. "
160
+ "Please start the Code Puppy API server first."
161
+ )
162
+ emit_error(error_msg, message_group=group_id)
163
+ return {"success": False, "error": error_msg}
164
+
165
+ except httpx.TimeoutException:
166
+ error_msg = f"Connection to {server_url} timed out."
167
+ emit_error(error_msg, message_group=group_id)
168
+ return {"success": False, "error": error_msg}
169
+
170
+ except httpx.HTTPStatusError as e:
171
+ error_msg = f"Server returned error status {e.response.status_code}."
172
+ emit_error(error_msg, message_group=group_id)
173
+ return {"success": False, "error": error_msg}
174
+
175
+ except Exception as e:
176
+ error_msg = f"Failed to check server health: {str(e)}"
177
+ emit_error(error_msg, message_group=group_id)
178
+ logger.exception("Unexpected error checking terminal server")
179
+ return {"success": False, "error": error_msg}
180
+
181
+
182
+ async def open_terminal(host: str = "localhost", port: int = 8765) -> Dict[str, Any]:
183
+ """Open the terminal browser interface.
184
+
185
+ First checks if the API server is running, then opens a Chromium browser
186
+ and navigates to the terminal endpoint. Waits for the terminal (xterm.js)
187
+ to be fully loaded before returning.
188
+
189
+ Args:
190
+ host: The hostname where the server is running. Defaults to "localhost".
191
+ port: The port number for the server. Defaults to 8765.
192
+
193
+ Returns:
194
+ A dictionary containing:
195
+ - success (bool): True if terminal was opened successfully.
196
+ - url (str): The URL of the terminal page (if successful).
197
+ - page_title (str): The title of the terminal page (if successful).
198
+ - error (str): Error message describing the failure (if unsuccessful).
199
+
200
+ Example:
201
+ >>> result = await open_terminal()
202
+ >>> if result["success"]:
203
+ ... print(f"Terminal opened at {result['url']}")
204
+ ... else:
205
+ ... print(f"Error: {result['error']}")
206
+ """
207
+ group_id = generate_group_id("terminal_open", f"{host}:{port}")
208
+ banner = format_terminal_banner("TERMINAL OPEN 🖥️")
209
+ emit_info(
210
+ Text.from_markup(f"{banner} [bold cyan]{host}:{port}[/bold cyan]"),
211
+ message_group=group_id,
212
+ )
213
+
214
+ # First, check if the server is running
215
+ server_check = await check_terminal_server(host, port)
216
+ if not server_check["success"]:
217
+ return {
218
+ "success": False,
219
+ "error": (
220
+ f"Cannot open terminal: {server_check['error']} "
221
+ "Please start the API server with 'code-puppy api' first."
222
+ ),
223
+ }
224
+
225
+ terminal_url = f"http://{host}:{port}/terminal"
226
+
227
+ try:
228
+ # Get the ChromiumTerminalManager for this session and initialize browser
229
+ manager = get_session_manager()
230
+ await manager.async_initialize()
231
+
232
+ # Get the existing page (don't create a new one!) and navigate to terminal
233
+ # This avoids leaving an about:blank tab that causes focus issues
234
+ page = await manager.get_current_page()
235
+ if not page:
236
+ return {"success": False, "error": "Failed to get browser page"}
237
+
238
+ await page.goto(terminal_url)
239
+
240
+ # Wait for xterm.js to be loaded and ready
241
+ # The terminal container should have the xterm class when ready
242
+ try:
243
+ await page.wait_for_selector(
244
+ ".xterm",
245
+ timeout=TERMINAL_LOAD_TIMEOUT,
246
+ )
247
+ emit_info("Terminal xterm.js loaded", message_group=group_id)
248
+ except Exception as e:
249
+ logger.warning(f"Timeout waiting for xterm.js: {e}")
250
+ # Continue anyway - the page might still be usable
251
+
252
+ # Get page information
253
+ final_url = page.url
254
+ page_title = await page.title()
255
+
256
+ emit_success(
257
+ f"Terminal opened: {final_url}",
258
+ message_group=group_id,
259
+ )
260
+
261
+ return {
262
+ "success": True,
263
+ "url": final_url,
264
+ "page_title": page_title,
265
+ }
266
+
267
+ except Exception as e:
268
+ error_msg = f"Failed to open terminal: {str(e)}"
269
+ emit_error(error_msg, message_group=group_id)
270
+ logger.exception("Error opening terminal browser")
271
+ return {"success": False, "error": error_msg}
272
+
273
+
274
+ async def close_terminal() -> Dict[str, Any]:
275
+ """Close the terminal browser and clean up resources.
276
+
277
+ Closes the Chromium browser instance managed by ChromiumTerminalManager,
278
+ saving any browser state and releasing resources.
279
+
280
+ Returns:
281
+ A dictionary containing:
282
+ - success (bool): True if terminal was closed successfully.
283
+ - message (str): A message describing the result.
284
+ - error (str): Error message if closing failed (only if unsuccessful).
285
+
286
+ Example:
287
+ >>> result = await close_terminal()
288
+ >>> print(result["message"])
289
+ "Terminal closed"
290
+ """
291
+ group_id = generate_group_id("terminal_close")
292
+ banner = format_terminal_banner("TERMINAL CLOSE 🔒")
293
+ emit_info(
294
+ Text.from_markup(f"{banner}"),
295
+ message_group=group_id,
296
+ )
297
+
298
+ try:
299
+ manager = get_session_manager()
300
+ await manager.close()
301
+
302
+ emit_success("Terminal browser closed", message_group=group_id)
303
+
304
+ return {
305
+ "success": True,
306
+ "message": "Terminal closed",
307
+ }
308
+
309
+ except Exception as e:
310
+ error_msg = f"Failed to close terminal: {str(e)}"
311
+ emit_error(error_msg, message_group=group_id)
312
+ logger.exception("Error closing terminal browser")
313
+ return {"success": False, "error": error_msg}
314
+
315
+
316
+ async def start_api_server(port: int = 8765) -> Dict[str, Any]:
317
+ """Start the Code Puppy API server in the background.
318
+
319
+ This starts the API server that provides the terminal endpoint for
320
+ browser-based terminal testing. The server runs in the background
321
+ and persists until stopped with /api stop or the process is killed.
322
+
323
+ Args:
324
+ port: The port to run the server on (default: 8765).
325
+
326
+ Returns:
327
+ A dictionary containing:
328
+ - success (bool): True if server was started successfully.
329
+ - pid (int): Process ID of the server (if successful).
330
+ - url (str): URL where the server is running (if successful).
331
+ - already_running (bool): True if server was already running.
332
+ - error (str): Error message if start failed (only if unsuccessful).
333
+
334
+ Example:
335
+ >>> result = await start_api_server()
336
+ >>> if result["success"]:
337
+ ... print(f"Server running at {result['url']}")
338
+ """
339
+ import os
340
+ import subprocess
341
+ import sys
342
+ from pathlib import Path
343
+
344
+ from code_puppy.config import STATE_DIR
345
+
346
+ group_id = generate_group_id("start_api_server", str(port))
347
+ emit_info(
348
+ Text.from_markup(format_terminal_banner(f"START API SERVER 🚀 port:{port}")),
349
+ message_group=group_id,
350
+ )
351
+
352
+ pid_file = Path(STATE_DIR) / "api_server.pid"
353
+ server_url = f"http://127.0.0.1:{port}"
354
+
355
+ # Check if already running
356
+ if pid_file.exists():
357
+ try:
358
+ pid = int(pid_file.read_text().strip())
359
+ os.kill(pid, 0) # Check if process exists
360
+ emit_success(
361
+ f"API server already running (PID {pid})", message_group=group_id
362
+ )
363
+ return {
364
+ "success": True,
365
+ "pid": pid,
366
+ "url": server_url,
367
+ "already_running": True,
368
+ }
369
+ except (OSError, ValueError):
370
+ pid_file.unlink(missing_ok=True) # Stale PID file
371
+
372
+ try:
373
+ # Start the server in background
374
+ proc = subprocess.Popen(
375
+ [sys.executable, "-m", "code_puppy.api.main"],
376
+ stdout=subprocess.DEVNULL,
377
+ stderr=subprocess.DEVNULL,
378
+ start_new_session=True,
379
+ )
380
+ pid_file.parent.mkdir(parents=True, exist_ok=True)
381
+ pid_file.write_text(str(proc.pid))
382
+
383
+ emit_success(f"API server started (PID {proc.pid})", message_group=group_id)
384
+ emit_info(f"Server URL: {server_url}", message_group=group_id)
385
+ emit_info(f"Docs: {server_url}/docs", message_group=group_id)
386
+
387
+ return {
388
+ "success": True,
389
+ "pid": proc.pid,
390
+ "url": server_url,
391
+ "already_running": False,
392
+ }
393
+
394
+ except Exception as e:
395
+ error_msg = f"Failed to start API server: {str(e)}"
396
+ emit_error(error_msg, message_group=group_id)
397
+ logger.exception("Error starting API server")
398
+ return {"success": False, "error": error_msg}
399
+
400
+
401
+ # =============================================================================
402
+ # Tool Registration Functions
403
+ # =============================================================================
404
+
405
+
406
+ def register_check_terminal_server(agent):
407
+ """Register the terminal server health check tool with an agent.
408
+
409
+ Args:
410
+ agent: The pydantic-ai agent to register the tool with.
411
+ """
412
+
413
+ @agent.tool
414
+ async def terminal_check_server(
415
+ context: RunContext,
416
+ host: str = "localhost",
417
+ port: int = 8765,
418
+ ) -> Dict[str, Any]:
419
+ """
420
+ Check if the Code Puppy API server is running and healthy.
421
+
422
+ Args:
423
+ host: The hostname where the server is running (default: localhost)
424
+ port: The port number for the server (default: 8765)
425
+
426
+ Returns:
427
+ Dict with:
428
+ - success: True if server is healthy
429
+ - server_url: Full URL of the server (if successful)
430
+ - status: "healthy" if running (if successful)
431
+ - error: Error message (if unsuccessful)
432
+ """
433
+ return await check_terminal_server(host, port)
434
+
435
+
436
+ def register_open_terminal(agent):
437
+ """Register the terminal open tool with an agent.
438
+
439
+ Args:
440
+ agent: The pydantic-ai agent to register the tool with.
441
+ """
442
+
443
+ @agent.tool
444
+ async def terminal_open(
445
+ context: RunContext,
446
+ host: str = "localhost",
447
+ port: int = 8765,
448
+ ) -> Dict[str, Any]:
449
+ """
450
+ Open the terminal browser interface.
451
+
452
+ First checks if the API server is running, then opens a browser
453
+ to the terminal endpoint. Waits for xterm.js to load.
454
+
455
+ Args:
456
+ host: The hostname where the server is running (default: localhost)
457
+ port: The port number for the server (default: 8765)
458
+
459
+ Returns:
460
+ Dict with:
461
+ - success: True if terminal opened successfully
462
+ - url: URL of the terminal page (if successful)
463
+ - page_title: Title of the page (if successful)
464
+ - error: Error message (if unsuccessful)
465
+ """
466
+ # Session is set by invoke_agent via contextvar - just use it
467
+ return await open_terminal(host, port)
468
+
469
+
470
+ def register_close_terminal(agent):
471
+ """Register the terminal close tool with an agent.
472
+
473
+ Args:
474
+ agent: The pydantic-ai agent to register the tool with.
475
+ """
476
+
477
+ @agent.tool
478
+ async def terminal_close(
479
+ context: RunContext,
480
+ ) -> Dict[str, Any]:
481
+ """
482
+ Close the terminal browser and clean up resources.
483
+
484
+ Returns:
485
+ Dict with:
486
+ - success: True if terminal closed successfully
487
+ - message: Status message (if successful)
488
+ - error: Error message (if unsuccessful)
489
+ """
490
+ # Session is set by invoke_agent via contextvar - just use it
491
+ return await close_terminal()
492
+
493
+
494
+ def register_start_api_server(agent):
495
+ """Register the API server start tool with an agent.
496
+
497
+ Args:
498
+ agent: The pydantic-ai agent to register the tool with.
499
+ """
500
+
501
+ @agent.tool
502
+ async def start_api_server(
503
+ context: RunContext,
504
+ port: int = 8765,
505
+ ) -> Dict[str, Any]:
506
+ """
507
+ Start the Code Puppy API server in the background.
508
+
509
+ This starts the API server that provides the terminal endpoint.
510
+ Use this if terminal_check_server reports the server isn't running.
511
+
512
+ Args:
513
+ port: The port to run the server on (default: 8765)
514
+
515
+ Returns:
516
+ Dict with:
517
+ - success: True if server started successfully
518
+ - pid: Process ID of the server (if successful)
519
+ - url: URL where the server is running (if successful)
520
+ - already_running: True if server was already running
521
+ - error: Error message (if unsuccessful)
522
+ """
523
+ from . import terminal_tools
524
+
525
+ return await terminal_tools.start_api_server(port)