newcode 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. code_puppy/__init__.py +10 -0
  2. code_puppy/__main__.py +10 -0
  3. code_puppy/agents/__init__.py +31 -0
  4. code_puppy/agents/agent_c_reviewer.py +155 -0
  5. code_puppy/agents/agent_code_puppy.py +147 -0
  6. code_puppy/agents/agent_code_reviewer.py +90 -0
  7. code_puppy/agents/agent_cpp_reviewer.py +132 -0
  8. code_puppy/agents/agent_creator_agent.py +630 -0
  9. code_puppy/agents/agent_golang_reviewer.py +151 -0
  10. code_puppy/agents/agent_helios.py +122 -0
  11. code_puppy/agents/agent_javascript_reviewer.py +160 -0
  12. code_puppy/agents/agent_manager.py +742 -0
  13. code_puppy/agents/agent_pack_leader.py +380 -0
  14. code_puppy/agents/agent_planning.py +165 -0
  15. code_puppy/agents/agent_python_programmer.py +167 -0
  16. code_puppy/agents/agent_python_reviewer.py +90 -0
  17. code_puppy/agents/agent_qa_expert.py +163 -0
  18. code_puppy/agents/agent_qa_kitten.py +208 -0
  19. code_puppy/agents/agent_scheduler.py +121 -0
  20. code_puppy/agents/agent_security_auditor.py +181 -0
  21. code_puppy/agents/agent_terminal_qa.py +323 -0
  22. code_puppy/agents/agent_typescript_reviewer.py +166 -0
  23. code_puppy/agents/base_agent.py +2145 -0
  24. code_puppy/agents/event_stream_handler.py +348 -0
  25. code_puppy/agents/json_agent.py +202 -0
  26. code_puppy/agents/pack/__init__.py +34 -0
  27. code_puppy/agents/pack/bloodhound.py +296 -0
  28. code_puppy/agents/pack/husky.py +307 -0
  29. code_puppy/agents/pack/retriever.py +380 -0
  30. code_puppy/agents/pack/shepherd.py +327 -0
  31. code_puppy/agents/pack/terrier.py +281 -0
  32. code_puppy/agents/pack/watchdog.py +357 -0
  33. code_puppy/agents/prompt_reviewer.py +145 -0
  34. code_puppy/agents/subagent_stream_handler.py +276 -0
  35. code_puppy/api/__init__.py +13 -0
  36. code_puppy/api/app.py +169 -0
  37. code_puppy/api/main.py +21 -0
  38. code_puppy/api/pty_manager.py +453 -0
  39. code_puppy/api/routers/__init__.py +12 -0
  40. code_puppy/api/routers/agents.py +36 -0
  41. code_puppy/api/routers/commands.py +217 -0
  42. code_puppy/api/routers/config.py +75 -0
  43. code_puppy/api/routers/sessions.py +234 -0
  44. code_puppy/api/templates/terminal.html +361 -0
  45. code_puppy/api/websocket.py +154 -0
  46. code_puppy/callbacks.py +674 -0
  47. code_puppy/chatgpt_codex_client.py +338 -0
  48. code_puppy/claude_cache_client.py +664 -0
  49. code_puppy/cli_runner.py +1038 -0
  50. code_puppy/command_line/__init__.py +1 -0
  51. code_puppy/command_line/add_model_menu.py +1092 -0
  52. code_puppy/command_line/agent_menu.py +662 -0
  53. code_puppy/command_line/attachments.py +395 -0
  54. code_puppy/command_line/autosave_menu.py +704 -0
  55. code_puppy/command_line/clipboard.py +527 -0
  56. code_puppy/command_line/colors_menu.py +526 -0
  57. code_puppy/command_line/command_handler.py +283 -0
  58. code_puppy/command_line/command_registry.py +150 -0
  59. code_puppy/command_line/config_commands.py +719 -0
  60. code_puppy/command_line/core_commands.py +853 -0
  61. code_puppy/command_line/diff_menu.py +865 -0
  62. code_puppy/command_line/file_path_completion.py +73 -0
  63. code_puppy/command_line/load_context_completion.py +52 -0
  64. code_puppy/command_line/mcp/__init__.py +10 -0
  65. code_puppy/command_line/mcp/base.py +32 -0
  66. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  67. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  68. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  69. code_puppy/command_line/mcp/edit_command.py +148 -0
  70. code_puppy/command_line/mcp/handler.py +138 -0
  71. code_puppy/command_line/mcp/help_command.py +147 -0
  72. code_puppy/command_line/mcp/install_command.py +214 -0
  73. code_puppy/command_line/mcp/install_menu.py +705 -0
  74. code_puppy/command_line/mcp/list_command.py +94 -0
  75. code_puppy/command_line/mcp/logs_command.py +235 -0
  76. code_puppy/command_line/mcp/remove_command.py +82 -0
  77. code_puppy/command_line/mcp/restart_command.py +100 -0
  78. code_puppy/command_line/mcp/search_command.py +123 -0
  79. code_puppy/command_line/mcp/start_all_command.py +135 -0
  80. code_puppy/command_line/mcp/start_command.py +117 -0
  81. code_puppy/command_line/mcp/status_command.py +184 -0
  82. code_puppy/command_line/mcp/stop_all_command.py +112 -0
  83. code_puppy/command_line/mcp/stop_command.py +80 -0
  84. code_puppy/command_line/mcp/test_command.py +107 -0
  85. code_puppy/command_line/mcp/utils.py +129 -0
  86. code_puppy/command_line/mcp/wizard_utils.py +334 -0
  87. code_puppy/command_line/mcp_completion.py +174 -0
  88. code_puppy/command_line/model_picker_completion.py +197 -0
  89. code_puppy/command_line/model_settings_menu.py +932 -0
  90. code_puppy/command_line/motd.py +91 -0
  91. code_puppy/command_line/onboarding_slides.py +179 -0
  92. code_puppy/command_line/onboarding_wizard.py +342 -0
  93. code_puppy/command_line/pin_command_completion.py +329 -0
  94. code_puppy/command_line/prompt_toolkit_completion.py +846 -0
  95. code_puppy/command_line/session_commands.py +302 -0
  96. code_puppy/command_line/skills_completion.py +160 -0
  97. code_puppy/command_line/uc_menu.py +893 -0
  98. code_puppy/command_line/utils.py +93 -0
  99. code_puppy/command_line/wiggum_state.py +78 -0
  100. code_puppy/config.py +1787 -0
  101. code_puppy/error_logging.py +133 -0
  102. code_puppy/gemini_code_assist.py +385 -0
  103. code_puppy/gemini_model.py +754 -0
  104. code_puppy/hook_engine/README.md +105 -0
  105. code_puppy/hook_engine/__init__.py +15 -0
  106. code_puppy/hook_engine/aliases.py +155 -0
  107. code_puppy/hook_engine/engine.py +195 -0
  108. code_puppy/hook_engine/executor.py +293 -0
  109. code_puppy/hook_engine/matcher.py +145 -0
  110. code_puppy/hook_engine/models.py +222 -0
  111. code_puppy/hook_engine/registry.py +106 -0
  112. code_puppy/hook_engine/validator.py +141 -0
  113. code_puppy/http_utils.py +361 -0
  114. code_puppy/keymap.py +128 -0
  115. code_puppy/main.py +10 -0
  116. code_puppy/mcp_/__init__.py +66 -0
  117. code_puppy/mcp_/async_lifecycle.py +286 -0
  118. code_puppy/mcp_/blocking_startup.py +469 -0
  119. code_puppy/mcp_/captured_stdio_server.py +275 -0
  120. code_puppy/mcp_/circuit_breaker.py +290 -0
  121. code_puppy/mcp_/config_wizard.py +507 -0
  122. code_puppy/mcp_/dashboard.py +308 -0
  123. code_puppy/mcp_/error_isolation.py +407 -0
  124. code_puppy/mcp_/examples/retry_example.py +226 -0
  125. code_puppy/mcp_/health_monitor.py +589 -0
  126. code_puppy/mcp_/managed_server.py +428 -0
  127. code_puppy/mcp_/manager.py +807 -0
  128. code_puppy/mcp_/mcp_logs.py +224 -0
  129. code_puppy/mcp_/registry.py +451 -0
  130. code_puppy/mcp_/retry_manager.py +337 -0
  131. code_puppy/mcp_/server_registry_catalog.py +1126 -0
  132. code_puppy/mcp_/status_tracker.py +355 -0
  133. code_puppy/mcp_/system_tools.py +209 -0
  134. code_puppy/mcp_prompts/__init__.py +1 -0
  135. code_puppy/mcp_prompts/hook_creator.py +103 -0
  136. code_puppy/messaging/__init__.py +255 -0
  137. code_puppy/messaging/bus.py +613 -0
  138. code_puppy/messaging/commands.py +167 -0
  139. code_puppy/messaging/markdown_patches.py +57 -0
  140. code_puppy/messaging/message_queue.py +361 -0
  141. code_puppy/messaging/messages.py +569 -0
  142. code_puppy/messaging/queue_console.py +271 -0
  143. code_puppy/messaging/renderers.py +311 -0
  144. code_puppy/messaging/rich_renderer.py +1153 -0
  145. code_puppy/messaging/spinner/__init__.py +83 -0
  146. code_puppy/messaging/spinner/console_spinner.py +240 -0
  147. code_puppy/messaging/spinner/spinner_base.py +96 -0
  148. code_puppy/messaging/subagent_console.py +460 -0
  149. code_puppy/model_factory.py +848 -0
  150. code_puppy/model_switching.py +63 -0
  151. code_puppy/model_utils.py +168 -0
  152. code_puppy/models.json +130 -0
  153. code_puppy/models_dev_api.json +1 -0
  154. code_puppy/models_dev_parser.py +592 -0
  155. code_puppy/plugins/__init__.py +186 -0
  156. code_puppy/plugins/agent_skills/__init__.py +22 -0
  157. code_puppy/plugins/agent_skills/config.py +175 -0
  158. code_puppy/plugins/agent_skills/discovery.py +136 -0
  159. code_puppy/plugins/agent_skills/downloader.py +392 -0
  160. code_puppy/plugins/agent_skills/installer.py +22 -0
  161. code_puppy/plugins/agent_skills/metadata.py +219 -0
  162. code_puppy/plugins/agent_skills/prompt_builder.py +100 -0
  163. code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
  164. code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
  165. code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
  166. code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
  167. code_puppy/plugins/agent_skills/skills_menu.py +781 -0
  168. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  169. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  170. code_puppy/plugins/antigravity_oauth/antigravity_model.py +706 -0
  171. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  172. code_puppy/plugins/antigravity_oauth/constants.py +133 -0
  173. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  174. code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
  175. code_puppy/plugins/antigravity_oauth/storage.py +288 -0
  176. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  177. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  178. code_puppy/plugins/antigravity_oauth/transport.py +863 -0
  179. code_puppy/plugins/antigravity_oauth/utils.py +168 -0
  180. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  181. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  182. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
  183. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
  184. code_puppy/plugins/chatgpt_oauth/test_plugin.py +295 -0
  185. code_puppy/plugins/chatgpt_oauth/utils.py +499 -0
  186. code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
  187. code_puppy/plugins/claude_code_hooks/config.py +131 -0
  188. code_puppy/plugins/claude_code_hooks/register_callbacks.py +163 -0
  189. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  190. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  191. code_puppy/plugins/claude_code_oauth/__init__.py +25 -0
  192. code_puppy/plugins/claude_code_oauth/config.py +52 -0
  193. code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
  194. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  195. code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
  196. code_puppy/plugins/claude_code_oauth/utils.py +601 -0
  197. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  198. code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
  199. code_puppy/plugins/example_custom_command/README.md +280 -0
  200. code_puppy/plugins/example_custom_command/register_callbacks.py +48 -0
  201. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  202. code_puppy/plugins/file_permission_handler/register_callbacks.py +528 -0
  203. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  204. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  205. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  206. code_puppy/plugins/hook_creator/__init__.py +1 -0
  207. code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
  208. code_puppy/plugins/hook_manager/__init__.py +1 -0
  209. code_puppy/plugins/hook_manager/config.py +277 -0
  210. code_puppy/plugins/hook_manager/hooks_menu.py +551 -0
  211. code_puppy/plugins/hook_manager/register_callbacks.py +205 -0
  212. code_puppy/plugins/oauth_puppy_html.py +224 -0
  213. code_puppy/plugins/scheduler/__init__.py +1 -0
  214. code_puppy/plugins/scheduler/register_callbacks.py +88 -0
  215. code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
  216. code_puppy/plugins/scheduler/scheduler_wizard.py +341 -0
  217. code_puppy/plugins/shell_safety/__init__.py +6 -0
  218. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  219. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  220. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  221. code_puppy/plugins/synthetic_status/__init__.py +1 -0
  222. code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
  223. code_puppy/plugins/synthetic_status/status_api.py +147 -0
  224. code_puppy/plugins/universal_constructor/__init__.py +13 -0
  225. code_puppy/plugins/universal_constructor/models.py +138 -0
  226. code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
  227. code_puppy/plugins/universal_constructor/registry.py +302 -0
  228. code_puppy/plugins/universal_constructor/sandbox.py +584 -0
  229. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  230. code_puppy/pydantic_patches.py +317 -0
  231. code_puppy/reopenable_async_client.py +232 -0
  232. code_puppy/round_robin_model.py +150 -0
  233. code_puppy/scheduler/__init__.py +41 -0
  234. code_puppy/scheduler/__main__.py +9 -0
  235. code_puppy/scheduler/cli.py +118 -0
  236. code_puppy/scheduler/config.py +126 -0
  237. code_puppy/scheduler/daemon.py +280 -0
  238. code_puppy/scheduler/executor.py +155 -0
  239. code_puppy/scheduler/platform.py +19 -0
  240. code_puppy/scheduler/platform_unix.py +22 -0
  241. code_puppy/scheduler/platform_win.py +32 -0
  242. code_puppy/session_storage.py +338 -0
  243. code_puppy/status_display.py +257 -0
  244. code_puppy/summarization_agent.py +176 -0
  245. code_puppy/terminal_utils.py +418 -0
  246. code_puppy/tools/__init__.py +470 -0
  247. code_puppy/tools/agent_tools.py +616 -0
  248. code_puppy/tools/ask_user_question/__init__.py +26 -0
  249. code_puppy/tools/ask_user_question/constants.py +73 -0
  250. code_puppy/tools/ask_user_question/demo_tui.py +55 -0
  251. code_puppy/tools/ask_user_question/handler.py +232 -0
  252. code_puppy/tools/ask_user_question/models.py +304 -0
  253. code_puppy/tools/ask_user_question/registration.py +36 -0
  254. code_puppy/tools/ask_user_question/renderers.py +309 -0
  255. code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
  256. code_puppy/tools/ask_user_question/theme.py +155 -0
  257. code_puppy/tools/ask_user_question/tui_loop.py +423 -0
  258. code_puppy/tools/browser/__init__.py +37 -0
  259. code_puppy/tools/browser/browser_control.py +289 -0
  260. code_puppy/tools/browser/browser_interactions.py +545 -0
  261. code_puppy/tools/browser/browser_locators.py +640 -0
  262. code_puppy/tools/browser/browser_manager.py +378 -0
  263. code_puppy/tools/browser/browser_navigation.py +251 -0
  264. code_puppy/tools/browser/browser_screenshot.py +179 -0
  265. code_puppy/tools/browser/browser_scripts.py +462 -0
  266. code_puppy/tools/browser/browser_workflows.py +221 -0
  267. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  268. code_puppy/tools/browser/terminal_command_tools.py +534 -0
  269. code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
  270. code_puppy/tools/browser/terminal_tools.py +525 -0
  271. code_puppy/tools/command_runner.py +1346 -0
  272. code_puppy/tools/common.py +1409 -0
  273. code_puppy/tools/display.py +84 -0
  274. code_puppy/tools/file_modifications.py +739 -0
  275. code_puppy/tools/file_operations.py +802 -0
  276. code_puppy/tools/scheduler_tools.py +412 -0
  277. code_puppy/tools/skills_tools.py +251 -0
  278. code_puppy/tools/subagent_context.py +158 -0
  279. code_puppy/tools/tools_content.py +51 -0
  280. code_puppy/tools/universal_constructor.py +889 -0
  281. code_puppy/uvx_detection.py +242 -0
  282. code_puppy/version_checker.py +82 -0
  283. newcode-0.1.1.data/data/code_puppy/models.json +130 -0
  284. newcode-0.1.1.data/data/code_puppy/models_dev_api.json +1 -0
  285. newcode-0.1.1.dist-info/METADATA +154 -0
  286. newcode-0.1.1.dist-info/RECORD +289 -0
  287. newcode-0.1.1.dist-info/WHEEL +4 -0
  288. newcode-0.1.1.dist-info/entry_points.txt +3 -0
  289. newcode-0.1.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,534 @@
1
+ """Terminal command execution tools for browser-based terminal automation.
2
+
3
+ This module provides tools for:
4
+ - Running commands in the terminal browser
5
+ - Sending special keys (Ctrl+C, Tab, arrows, etc.)
6
+ - Waiting for terminal output patterns
7
+
8
+ These tools use the ChromiumTerminalManager to manage the browser instance
9
+ and interact with the xterm.js terminal in the API.
10
+ """
11
+
12
+ import asyncio
13
+ import logging
14
+ import re
15
+ from typing import Any, Dict, List, Optional
16
+
17
+ from pydantic_ai import RunContext, ToolReturn
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 .terminal_screenshot_tools import terminal_read_output, terminal_screenshot
25
+ from .terminal_tools import get_session_manager
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ # Timeout defaults (seconds)
30
+ DEFAULT_COMMAND_TIMEOUT = 30.0
31
+ DEFAULT_OUTPUT_TIMEOUT = 30.0
32
+
33
+ # Time to wait for prompt to reappear after command (ms)
34
+ PROMPT_WAIT_MS = 500
35
+
36
+ # Modifier key mapping for Playwright
37
+ MODIFIER_MAP = {
38
+ "control": "Control",
39
+ "ctrl": "Control",
40
+ "shift": "Shift",
41
+ "alt": "Alt",
42
+ "meta": "Meta",
43
+ "command": "Meta",
44
+ "cmd": "Meta",
45
+ }
46
+
47
+ # JavaScript to robustly focus the xterm.js terminal
48
+ # xterm.js uses a hidden textarea to capture keyboard input
49
+ FOCUS_TERMINAL_JS = """
50
+ () => {
51
+ // Method 1: Find and focus the xterm helper textarea directly
52
+ // This is the element that actually receives keyboard input in xterm.js
53
+ const textareas = document.querySelectorAll('textarea.xterm-helper-textarea');
54
+ for (const textarea of textareas) {
55
+ textarea.focus();
56
+ // Also click on the parent to ensure xterm knows it's active
57
+ const xterm = textarea.closest('.xterm');
58
+ if (xterm) {
59
+ xterm.click();
60
+ }
61
+ return { success: true, method: 'textarea_focus', found: textareas.length };
62
+ }
63
+
64
+ // Method 2: Click on the xterm viewport/screen to trigger focus
65
+ const viewport = document.querySelector('.xterm-viewport') ||
66
+ document.querySelector('.xterm-screen');
67
+ if (viewport) {
68
+ viewport.click();
69
+ // Try textarea again after click
70
+ const ta = document.querySelector('textarea.xterm-helper-textarea');
71
+ if (ta) ta.focus();
72
+ return { success: true, method: 'viewport_click' };
73
+ }
74
+
75
+ // Method 3: Find any xterm element and click it
76
+ const xterm = document.querySelector('.xterm');
77
+ if (xterm) {
78
+ xterm.click();
79
+ const ta = xterm.querySelector('textarea');
80
+ if (ta) ta.focus();
81
+ return { success: true, method: 'xterm_click' };
82
+ }
83
+
84
+ // Method 4: Try the terminal container
85
+ const container = document.getElementById('terminal');
86
+ if (container) {
87
+ container.click();
88
+ return { success: true, method: 'container_click' };
89
+ }
90
+
91
+ return { success: false, error: 'Could not find terminal element' };
92
+ }
93
+ """
94
+
95
+
96
+ async def _focus_terminal(page) -> Dict[str, Any]:
97
+ """Focus the xterm.js terminal to receive keyboard input.
98
+
99
+ xterm.js uses a hidden textarea element to capture keyboard events.
100
+ We need to ensure this textarea is focused for keys to work.
101
+ """
102
+ try:
103
+ # First, try the JavaScript approach which is most reliable
104
+ result = await page.evaluate(FOCUS_TERMINAL_JS)
105
+
106
+ if result.get("success"):
107
+ # Give the browser a moment to process the focus
108
+ await asyncio.sleep(0.15)
109
+ return result
110
+
111
+ # Fallback: Try clicking on known selectors
112
+ selectors_to_try = [
113
+ "textarea.xterm-helper-textarea",
114
+ ".xterm-viewport",
115
+ ".xterm-screen",
116
+ ".xterm",
117
+ "#terminal",
118
+ ]
119
+
120
+ for selector in selectors_to_try:
121
+ element = await page.query_selector(selector)
122
+ if element:
123
+ await element.click()
124
+ await asyncio.sleep(0.1)
125
+ # If we clicked something other than textarea, try to focus textarea
126
+ if "textarea" not in selector:
127
+ textarea = await page.query_selector(
128
+ "textarea.xterm-helper-textarea"
129
+ )
130
+ if textarea:
131
+ await textarea.focus()
132
+ return {"success": True, "method": f"fallback_{selector}"}
133
+
134
+ return {"success": False, "error": "Could not find terminal element to focus"}
135
+
136
+ except Exception as e:
137
+ logger.warning(f"Error focusing terminal: {e}")
138
+ return {"success": False, "error": str(e)}
139
+
140
+
141
+ def _normalize_modifier(modifier: str) -> str:
142
+ """Normalize modifier name to Playwright format."""
143
+ return MODIFIER_MAP.get(modifier.lower(), modifier)
144
+
145
+
146
+ async def run_terminal_command(
147
+ command: str,
148
+ wait_for_prompt: bool = True,
149
+ timeout: float = DEFAULT_COMMAND_TIMEOUT,
150
+ capture_screenshot: bool = False,
151
+ ) -> Dict[str, Any]:
152
+ """Execute a command in the terminal browser.
153
+
154
+ Types the command into the xterm.js terminal and presses Enter to execute.
155
+ Optionally captures a screenshot that multimodal models can see directly.
156
+
157
+ Args:
158
+ command: The command string to execute.
159
+ wait_for_prompt: If True, wait briefly for command to process.
160
+ Defaults to True.
161
+ timeout: Maximum wait time in seconds. Defaults to 30.0.
162
+ capture_screenshot: If True, take a screenshot after execution.
163
+ The screenshot is returned as base64 data. Defaults to False.
164
+
165
+ Returns:
166
+ A dictionary containing:
167
+ - success (bool): True if command was sent.
168
+ - command (str): The command that was executed.
169
+ - base64_image (str, optional): Screenshot as base64 PNG (if captured).
170
+ - screenshot_path (str, optional): Path to saved screenshot.
171
+ - error (str, optional): Error message if unsuccessful.
172
+ """
173
+ group_id = generate_group_id("terminal_run_command", command[:50])
174
+ banner = format_terminal_banner("TERMINAL RUN COMMAND 💻")
175
+ emit_info(
176
+ Text.from_markup(f"{banner} [dim]{command}[/dim]"), message_group=group_id
177
+ )
178
+
179
+ try:
180
+ manager = get_session_manager()
181
+ page = await manager.get_current_page()
182
+
183
+ if not page:
184
+ error_msg = "No active terminal page. Open terminal first."
185
+ emit_error(error_msg, message_group=group_id)
186
+ return {"success": False, "error": error_msg, "command": command}
187
+
188
+ # Focus the terminal before typing
189
+ focus_result = await _focus_terminal(page)
190
+ if not focus_result.get("success"):
191
+ emit_info(
192
+ f"Warning: Could not focus terminal: {focus_result.get('error')}",
193
+ message_group=group_id,
194
+ )
195
+
196
+ # Type and execute command
197
+ await page.keyboard.type(command)
198
+ await page.keyboard.press("Enter")
199
+ emit_info(f"Command sent: {command}", message_group=group_id)
200
+
201
+ # Wait for command to process
202
+ if wait_for_prompt:
203
+ await asyncio.sleep(min(PROMPT_WAIT_MS / 1000, timeout))
204
+
205
+ result: Dict[str, Any] = {
206
+ "success": True,
207
+ "command": command,
208
+ }
209
+
210
+ # Capture screenshot if requested
211
+ if capture_screenshot:
212
+ screenshot_result = await terminal_screenshot()
213
+ if isinstance(screenshot_result, ToolReturn):
214
+ # Success: ToolReturn with metadata
215
+ result["screenshot_path"] = screenshot_result.metadata.get(
216
+ "screenshot_path"
217
+ )
218
+ result["media_type"] = "image/png"
219
+ elif isinstance(screenshot_result, dict) and screenshot_result.get(
220
+ "success"
221
+ ):
222
+ result["screenshot_path"] = screenshot_result.get("screenshot_path")
223
+ result["media_type"] = "image/png"
224
+
225
+ emit_success(f"Command executed: {command}", message_group=group_id)
226
+ return result
227
+
228
+ except Exception as e:
229
+ error_msg = f"Failed to run terminal command: {str(e)}"
230
+ emit_error(error_msg, message_group=group_id)
231
+ logger.exception("Error running terminal command")
232
+ return {"success": False, "error": error_msg, "command": command}
233
+
234
+
235
+ async def send_terminal_keys(
236
+ keys: str,
237
+ modifiers: Optional[List[str]] = None,
238
+ repeat: int = 1,
239
+ delay_ms: int = 50,
240
+ ) -> Dict[str, Any]:
241
+ """Send special keys or key combinations to the terminal.
242
+
243
+ Sends keyboard input to the xterm.js terminal, supporting special keys
244
+ and modifier combinations like Ctrl+C, Ctrl+D, Tab, Arrow keys, etc.
245
+
246
+ Args:
247
+ keys: The key(s) to send. Can be a single character or special key
248
+ like "Enter", "Tab", "ArrowUp", "ArrowDown", "ArrowLeft",
249
+ "ArrowRight", "Escape", "Backspace", "Delete", etc.
250
+ modifiers: Optional modifier keys to hold. Supported:
251
+ "Control"/"Ctrl", "Shift", "Alt", "Meta"/"Command"/"Cmd".
252
+ repeat: Number of times to press the key. Defaults to 1.
253
+ Use this to navigate multiple items, e.g., repeat=5 for ArrowDown.
254
+ delay_ms: Delay in milliseconds between repeated keypresses.
255
+ Defaults to 50ms. Increase if the TUI needs time to update.
256
+
257
+ Returns:
258
+ Dict with success, keys_sent, modifiers, repeat_count, and optional error.
259
+
260
+ Examples:
261
+ >>> await send_terminal_keys("c", modifiers=["Control"]) # Ctrl+C
262
+ >>> await send_terminal_keys("Tab") # Tab completion
263
+ >>> await send_terminal_keys("ArrowUp") # Previous command
264
+ >>> await send_terminal_keys("ArrowDown", repeat=5) # Navigate down 5 items
265
+ >>> await send_terminal_keys("ArrowRight", repeat=3, delay_ms=100) # Move right 3 times
266
+ """
267
+ modifiers = modifiers or []
268
+ repeat = max(1, repeat) # Ensure at least 1
269
+ normalized_modifiers = [_normalize_modifier(m) for m in modifiers]
270
+ modifier_str = "+".join(normalized_modifiers) if normalized_modifiers else ""
271
+ key_combo = f"{modifier_str}+{keys}" if modifier_str else keys
272
+
273
+ repeat_str = f" x{repeat}" if repeat > 1 else ""
274
+ group_id = generate_group_id("terminal_send_keys", f"{key_combo}{repeat_str}")
275
+ banner = format_terminal_banner("TERMINAL SEND KEYS ⌨️")
276
+ emit_info(
277
+ Text.from_markup(f"{banner} [bold cyan]{key_combo}{repeat_str}[/bold cyan]"),
278
+ message_group=group_id,
279
+ )
280
+
281
+ try:
282
+ manager = get_session_manager()
283
+ page = await manager.get_current_page()
284
+
285
+ if not page:
286
+ error_msg = "No active terminal page. Open terminal first."
287
+ emit_error(error_msg, message_group=group_id)
288
+ return {
289
+ "success": False,
290
+ "error": error_msg,
291
+ "keys_sent": keys,
292
+ "modifiers": modifiers,
293
+ }
294
+
295
+ # Focus terminal before sending keys
296
+ await _focus_terminal(page)
297
+
298
+ # Send key(s) the specified number of times
299
+ for i in range(repeat):
300
+ # Hold modifiers and press key
301
+ for modifier in normalized_modifiers:
302
+ await page.keyboard.down(modifier)
303
+
304
+ try:
305
+ if len(keys) > 1 or keys[0].isupper():
306
+ await page.keyboard.press(keys)
307
+ else:
308
+ await page.keyboard.type(keys)
309
+ finally:
310
+ for modifier in reversed(normalized_modifiers):
311
+ await page.keyboard.up(modifier)
312
+
313
+ # Delay between repeated keypresses (but not after the last one)
314
+ if repeat > 1 and i < repeat - 1:
315
+ await asyncio.sleep(delay_ms / 1000)
316
+
317
+ emit_success(f"Keys sent: {key_combo}{repeat_str}", message_group=group_id)
318
+ return {
319
+ "success": True,
320
+ "keys_sent": keys,
321
+ "modifiers": modifiers,
322
+ "repeat_count": repeat,
323
+ }
324
+
325
+ except Exception as e:
326
+ error_msg = f"Failed to send terminal keys: {str(e)}"
327
+ emit_error(error_msg, message_group=group_id)
328
+ logger.exception("Error sending terminal keys")
329
+ return {
330
+ "success": False,
331
+ "error": error_msg,
332
+ "keys_sent": keys,
333
+ "modifiers": modifiers,
334
+ "repeat_count": repeat,
335
+ }
336
+
337
+
338
+ async def wait_for_terminal_output(
339
+ pattern: Optional[str] = None,
340
+ timeout: float = DEFAULT_OUTPUT_TIMEOUT,
341
+ capture_screenshot: bool = False,
342
+ ) -> Dict[str, Any]:
343
+ """Wait for terminal output, optionally matching a pattern.
344
+
345
+ Reads the terminal text output and checks for a pattern match.
346
+ Uses DOM scraping to get actual text content.
347
+
348
+ Args:
349
+ pattern: Optional regex or text pattern to match.
350
+ If None, just reads current output.
351
+ timeout: Maximum wait time in seconds. Defaults to 30.0.
352
+ capture_screenshot: If True, include a screenshot. Defaults to False.
353
+
354
+ Returns:
355
+ Dict with:
356
+ - success (bool): True if output was read.
357
+ - matched (bool): True if pattern was found (when pattern given).
358
+ - output (str): The terminal text content.
359
+ - base64_image (str, optional): Screenshot if captured.
360
+ - error (str, optional): Error message if unsuccessful.
361
+ """
362
+ pattern_display = pattern[:50] if pattern else "any"
363
+ group_id = generate_group_id("terminal_wait_output", pattern_display)
364
+ banner = format_terminal_banner("TERMINAL WAIT OUTPUT 👁️")
365
+ emit_info(
366
+ Text.from_markup(f"{banner} [dim]pattern={pattern_display}[/dim]"),
367
+ message_group=group_id,
368
+ )
369
+
370
+ try:
371
+ # Read terminal text output
372
+ read_result = await terminal_read_output(lines=100)
373
+
374
+ if not read_result["success"]:
375
+ emit_error(
376
+ read_result.get("error", "Failed to read output"),
377
+ message_group=group_id,
378
+ )
379
+ return {
380
+ "success": False,
381
+ "error": read_result.get("error"),
382
+ "matched": False,
383
+ }
384
+
385
+ output_text = read_result["output"]
386
+
387
+ result: Dict[str, Any] = {
388
+ "success": True,
389
+ "output": output_text,
390
+ "line_count": read_result.get("line_count", 0),
391
+ }
392
+
393
+ # Check pattern match
394
+ if pattern:
395
+ try:
396
+ # Try regex match first
397
+ matched = bool(re.search(pattern, output_text, re.IGNORECASE))
398
+ except re.error:
399
+ # Fall back to simple substring match
400
+ matched = pattern.lower() in output_text.lower()
401
+
402
+ result["matched"] = matched
403
+ if matched:
404
+ emit_success(f"Pattern matched: {pattern}", message_group=group_id)
405
+ else:
406
+ emit_info(f"Pattern not found: {pattern}", message_group=group_id)
407
+ else:
408
+ result["matched"] = bool(output_text.strip())
409
+
410
+ # Capture screenshot if requested
411
+ if capture_screenshot:
412
+ screenshot_result = await terminal_screenshot()
413
+ if isinstance(screenshot_result, ToolReturn):
414
+ # Success: ToolReturn with metadata
415
+ result["screenshot_path"] = screenshot_result.metadata.get(
416
+ "screenshot_path"
417
+ )
418
+ result["media_type"] = "image/png"
419
+ elif isinstance(screenshot_result, dict) and screenshot_result.get(
420
+ "success"
421
+ ):
422
+ result["screenshot_path"] = screenshot_result.get("screenshot_path")
423
+ result["media_type"] = "image/png"
424
+
425
+ return result
426
+
427
+ except Exception as e:
428
+ error_msg = f"Failed to wait for terminal output: {str(e)}"
429
+ emit_error(error_msg, message_group=group_id)
430
+ logger.exception("Error waiting for terminal output")
431
+ return {"success": False, "error": error_msg, "matched": False}
432
+
433
+
434
+ # =============================================================================
435
+ # Tool Registration Functions
436
+ # =============================================================================
437
+
438
+
439
+ def register_run_terminal_command(agent):
440
+ """Register the terminal command execution tool."""
441
+
442
+ @agent.tool
443
+ async def terminal_run_command(
444
+ context: RunContext,
445
+ command: str,
446
+ wait_for_prompt: bool = True,
447
+ capture_screenshot: bool = False,
448
+ ) -> Dict[str, Any]:
449
+ """
450
+ Execute a command in the terminal browser.
451
+
452
+ Types the command and presses Enter. Optionally captures a screenshot
453
+ that you can see directly as base64 image data.
454
+
455
+ Args:
456
+ command: The command to execute.
457
+ wait_for_prompt: Wait briefly for command to process (default: True).
458
+ capture_screenshot: Capture screenshot after execution (default: False).
459
+ Set True if you need to see the terminal output visually.
460
+
461
+ Returns:
462
+ Dict with success, command, and optionally base64_image you can see.
463
+ """
464
+ # Session is set by invoke_agent via contextvar
465
+ return await run_terminal_command(
466
+ command=command,
467
+ wait_for_prompt=wait_for_prompt,
468
+ capture_screenshot=capture_screenshot,
469
+ )
470
+
471
+
472
+ def register_send_terminal_keys(agent):
473
+ """Register the terminal key sending tool."""
474
+
475
+ @agent.tool
476
+ async def terminal_send_keys(
477
+ context: RunContext,
478
+ keys: str,
479
+ modifiers: Optional[List[str]] = None,
480
+ repeat: int = 1,
481
+ delay_ms: int = 50,
482
+ ) -> Dict[str, Any]:
483
+ """
484
+ Send special keys or key combinations to the terminal.
485
+
486
+ Args:
487
+ keys: Key to send (e.g., "Enter", "Tab", "ArrowUp", "ArrowDown", "c").
488
+ modifiers: Modifier keys like ["Control"] for Ctrl+C.
489
+ repeat: Number of times to press the key. Use this to navigate
490
+ multiple items instead of calling this function multiple times!
491
+ Example: repeat=5 to press ArrowDown 5 times.
492
+ delay_ms: Milliseconds to wait between repeated keypresses (default 50).
493
+
494
+ Returns:
495
+ Dict with success, keys_sent, modifiers, repeat_count.
496
+
497
+ Examples:
498
+ - Navigate down 5 items: keys="ArrowDown", repeat=5
499
+ - Navigate right 3 times: keys="ArrowRight", repeat=3
500
+ - Ctrl+C: keys="c", modifiers=["Control"]
501
+ - Tab: keys="Tab"
502
+ """
503
+ # Session is set by invoke_agent via contextvar
504
+ return await send_terminal_keys(
505
+ keys=keys, modifiers=modifiers, repeat=repeat, delay_ms=delay_ms
506
+ )
507
+
508
+
509
+ def register_wait_terminal_output(agent):
510
+ """Register the terminal output waiting tool."""
511
+
512
+ @agent.tool
513
+ async def terminal_wait_output(
514
+ context: RunContext,
515
+ pattern: Optional[str] = None,
516
+ capture_screenshot: bool = False,
517
+ ) -> Dict[str, Any]:
518
+ """
519
+ Read terminal output and optionally match a pattern.
520
+
521
+ Extracts text from the terminal. Can check for pattern matches.
522
+
523
+ Args:
524
+ pattern: Optional regex or text to search for.
525
+ capture_screenshot: Include a screenshot you can see (default: False).
526
+
527
+ Returns:
528
+ Dict with output (text), matched (if pattern given), optionally base64_image.
529
+ """
530
+ # Session is set by invoke_agent via contextvar
531
+ return await wait_for_terminal_output(
532
+ pattern=pattern,
533
+ capture_screenshot=capture_screenshot,
534
+ )