codepp 0.0.437__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. code_puppy/__init__.py +10 -0
  2. code_puppy/__main__.py +10 -0
  3. code_puppy/agents/__init__.py +31 -0
  4. code_puppy/agents/agent_c_reviewer.py +155 -0
  5. code_puppy/agents/agent_code_puppy.py +117 -0
  6. code_puppy/agents/agent_code_reviewer.py +90 -0
  7. code_puppy/agents/agent_cpp_reviewer.py +132 -0
  8. code_puppy/agents/agent_creator_agent.py +638 -0
  9. code_puppy/agents/agent_golang_reviewer.py +151 -0
  10. code_puppy/agents/agent_helios.py +124 -0
  11. code_puppy/agents/agent_javascript_reviewer.py +160 -0
  12. code_puppy/agents/agent_manager.py +742 -0
  13. code_puppy/agents/agent_pack_leader.py +385 -0
  14. code_puppy/agents/agent_planning.py +165 -0
  15. code_puppy/agents/agent_python_programmer.py +169 -0
  16. code_puppy/agents/agent_python_reviewer.py +90 -0
  17. code_puppy/agents/agent_qa_expert.py +163 -0
  18. code_puppy/agents/agent_qa_kitten.py +208 -0
  19. code_puppy/agents/agent_scheduler.py +121 -0
  20. code_puppy/agents/agent_security_auditor.py +181 -0
  21. code_puppy/agents/agent_terminal_qa.py +323 -0
  22. code_puppy/agents/agent_typescript_reviewer.py +166 -0
  23. code_puppy/agents/base_agent.py +2156 -0
  24. code_puppy/agents/event_stream_handler.py +348 -0
  25. code_puppy/agents/json_agent.py +202 -0
  26. code_puppy/agents/pack/__init__.py +34 -0
  27. code_puppy/agents/pack/bloodhound.py +304 -0
  28. code_puppy/agents/pack/husky.py +327 -0
  29. code_puppy/agents/pack/retriever.py +393 -0
  30. code_puppy/agents/pack/shepherd.py +348 -0
  31. code_puppy/agents/pack/terrier.py +287 -0
  32. code_puppy/agents/pack/watchdog.py +367 -0
  33. code_puppy/agents/prompt_reviewer.py +145 -0
  34. code_puppy/agents/subagent_stream_handler.py +276 -0
  35. code_puppy/api/__init__.py +13 -0
  36. code_puppy/api/app.py +169 -0
  37. code_puppy/api/main.py +21 -0
  38. code_puppy/api/pty_manager.py +453 -0
  39. code_puppy/api/routers/__init__.py +12 -0
  40. code_puppy/api/routers/agents.py +36 -0
  41. code_puppy/api/routers/commands.py +217 -0
  42. code_puppy/api/routers/config.py +75 -0
  43. code_puppy/api/routers/sessions.py +234 -0
  44. code_puppy/api/templates/terminal.html +361 -0
  45. code_puppy/api/websocket.py +154 -0
  46. code_puppy/callbacks.py +692 -0
  47. code_puppy/chatgpt_codex_client.py +338 -0
  48. code_puppy/claude_cache_client.py +672 -0
  49. code_puppy/cli_runner.py +1073 -0
  50. code_puppy/command_line/__init__.py +1 -0
  51. code_puppy/command_line/add_model_menu.py +1092 -0
  52. code_puppy/command_line/agent_menu.py +662 -0
  53. code_puppy/command_line/attachments.py +395 -0
  54. code_puppy/command_line/autosave_menu.py +704 -0
  55. code_puppy/command_line/clipboard.py +527 -0
  56. code_puppy/command_line/colors_menu.py +532 -0
  57. code_puppy/command_line/command_handler.py +293 -0
  58. code_puppy/command_line/command_registry.py +150 -0
  59. code_puppy/command_line/config_commands.py +719 -0
  60. code_puppy/command_line/core_commands.py +867 -0
  61. code_puppy/command_line/diff_menu.py +865 -0
  62. code_puppy/command_line/file_path_completion.py +73 -0
  63. code_puppy/command_line/load_context_completion.py +52 -0
  64. code_puppy/command_line/mcp/__init__.py +10 -0
  65. code_puppy/command_line/mcp/base.py +32 -0
  66. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  67. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  68. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  69. code_puppy/command_line/mcp/edit_command.py +148 -0
  70. code_puppy/command_line/mcp/handler.py +138 -0
  71. code_puppy/command_line/mcp/help_command.py +147 -0
  72. code_puppy/command_line/mcp/install_command.py +214 -0
  73. code_puppy/command_line/mcp/install_menu.py +705 -0
  74. code_puppy/command_line/mcp/list_command.py +94 -0
  75. code_puppy/command_line/mcp/logs_command.py +235 -0
  76. code_puppy/command_line/mcp/remove_command.py +82 -0
  77. code_puppy/command_line/mcp/restart_command.py +100 -0
  78. code_puppy/command_line/mcp/search_command.py +123 -0
  79. code_puppy/command_line/mcp/start_all_command.py +135 -0
  80. code_puppy/command_line/mcp/start_command.py +117 -0
  81. code_puppy/command_line/mcp/status_command.py +184 -0
  82. code_puppy/command_line/mcp/stop_all_command.py +112 -0
  83. code_puppy/command_line/mcp/stop_command.py +80 -0
  84. code_puppy/command_line/mcp/test_command.py +107 -0
  85. code_puppy/command_line/mcp/utils.py +129 -0
  86. code_puppy/command_line/mcp/wizard_utils.py +334 -0
  87. code_puppy/command_line/mcp_completion.py +174 -0
  88. code_puppy/command_line/model_picker_completion.py +197 -0
  89. code_puppy/command_line/model_settings_menu.py +932 -0
  90. code_puppy/command_line/motd.py +96 -0
  91. code_puppy/command_line/onboarding_slides.py +179 -0
  92. code_puppy/command_line/onboarding_wizard.py +342 -0
  93. code_puppy/command_line/pin_command_completion.py +329 -0
  94. code_puppy/command_line/prompt_toolkit_completion.py +846 -0
  95. code_puppy/command_line/session_commands.py +302 -0
  96. code_puppy/command_line/shell_passthrough.py +145 -0
  97. code_puppy/command_line/skills_completion.py +160 -0
  98. code_puppy/command_line/uc_menu.py +893 -0
  99. code_puppy/command_line/utils.py +93 -0
  100. code_puppy/command_line/wiggum_state.py +78 -0
  101. code_puppy/config.py +1770 -0
  102. code_puppy/error_logging.py +134 -0
  103. code_puppy/gemini_code_assist.py +385 -0
  104. code_puppy/gemini_model.py +754 -0
  105. code_puppy/hook_engine/README.md +105 -0
  106. code_puppy/hook_engine/__init__.py +21 -0
  107. code_puppy/hook_engine/aliases.py +155 -0
  108. code_puppy/hook_engine/engine.py +221 -0
  109. code_puppy/hook_engine/executor.py +296 -0
  110. code_puppy/hook_engine/matcher.py +156 -0
  111. code_puppy/hook_engine/models.py +240 -0
  112. code_puppy/hook_engine/registry.py +106 -0
  113. code_puppy/hook_engine/validator.py +144 -0
  114. code_puppy/http_utils.py +361 -0
  115. code_puppy/keymap.py +128 -0
  116. code_puppy/main.py +10 -0
  117. code_puppy/mcp_/__init__.py +66 -0
  118. code_puppy/mcp_/async_lifecycle.py +286 -0
  119. code_puppy/mcp_/blocking_startup.py +469 -0
  120. code_puppy/mcp_/captured_stdio_server.py +275 -0
  121. code_puppy/mcp_/circuit_breaker.py +290 -0
  122. code_puppy/mcp_/config_wizard.py +507 -0
  123. code_puppy/mcp_/dashboard.py +308 -0
  124. code_puppy/mcp_/error_isolation.py +407 -0
  125. code_puppy/mcp_/examples/retry_example.py +226 -0
  126. code_puppy/mcp_/health_monitor.py +589 -0
  127. code_puppy/mcp_/managed_server.py +428 -0
  128. code_puppy/mcp_/manager.py +807 -0
  129. code_puppy/mcp_/mcp_logs.py +224 -0
  130. code_puppy/mcp_/registry.py +451 -0
  131. code_puppy/mcp_/retry_manager.py +337 -0
  132. code_puppy/mcp_/server_registry_catalog.py +1126 -0
  133. code_puppy/mcp_/status_tracker.py +355 -0
  134. code_puppy/mcp_/system_tools.py +209 -0
  135. code_puppy/mcp_prompts/__init__.py +1 -0
  136. code_puppy/mcp_prompts/hook_creator.py +103 -0
  137. code_puppy/messaging/__init__.py +255 -0
  138. code_puppy/messaging/bus.py +613 -0
  139. code_puppy/messaging/commands.py +167 -0
  140. code_puppy/messaging/markdown_patches.py +57 -0
  141. code_puppy/messaging/message_queue.py +361 -0
  142. code_puppy/messaging/messages.py +569 -0
  143. code_puppy/messaging/queue_console.py +271 -0
  144. code_puppy/messaging/renderers.py +311 -0
  145. code_puppy/messaging/rich_renderer.py +1158 -0
  146. code_puppy/messaging/spinner/__init__.py +83 -0
  147. code_puppy/messaging/spinner/console_spinner.py +240 -0
  148. code_puppy/messaging/spinner/spinner_base.py +95 -0
  149. code_puppy/messaging/subagent_console.py +460 -0
  150. code_puppy/model_factory.py +848 -0
  151. code_puppy/model_switching.py +63 -0
  152. code_puppy/model_utils.py +168 -0
  153. code_puppy/models.json +174 -0
  154. code_puppy/models_dev_api.json +1 -0
  155. code_puppy/models_dev_parser.py +592 -0
  156. code_puppy/plugins/__init__.py +186 -0
  157. code_puppy/plugins/agent_skills/__init__.py +22 -0
  158. code_puppy/plugins/agent_skills/config.py +175 -0
  159. code_puppy/plugins/agent_skills/discovery.py +136 -0
  160. code_puppy/plugins/agent_skills/downloader.py +392 -0
  161. code_puppy/plugins/agent_skills/installer.py +22 -0
  162. code_puppy/plugins/agent_skills/metadata.py +219 -0
  163. code_puppy/plugins/agent_skills/prompt_builder.py +60 -0
  164. code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
  165. code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
  166. code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
  167. code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
  168. code_puppy/plugins/agent_skills/skills_menu.py +781 -0
  169. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  170. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  171. code_puppy/plugins/antigravity_oauth/antigravity_model.py +706 -0
  172. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  173. code_puppy/plugins/antigravity_oauth/constants.py +133 -0
  174. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  175. code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
  176. code_puppy/plugins/antigravity_oauth/storage.py +288 -0
  177. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  178. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  179. code_puppy/plugins/antigravity_oauth/transport.py +863 -0
  180. code_puppy/plugins/antigravity_oauth/utils.py +168 -0
  181. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  182. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  183. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +329 -0
  184. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
  185. code_puppy/plugins/chatgpt_oauth/test_plugin.py +301 -0
  186. code_puppy/plugins/chatgpt_oauth/utils.py +523 -0
  187. code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
  188. code_puppy/plugins/claude_code_hooks/config.py +137 -0
  189. code_puppy/plugins/claude_code_hooks/register_callbacks.py +175 -0
  190. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  191. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  192. code_puppy/plugins/claude_code_oauth/__init__.py +25 -0
  193. code_puppy/plugins/claude_code_oauth/config.py +52 -0
  194. code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
  195. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  196. code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
  197. code_puppy/plugins/claude_code_oauth/utils.py +640 -0
  198. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  199. code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
  200. code_puppy/plugins/example_custom_command/README.md +280 -0
  201. code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
  202. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  203. code_puppy/plugins/file_permission_handler/register_callbacks.py +470 -0
  204. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  205. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  206. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  207. code_puppy/plugins/hook_creator/__init__.py +1 -0
  208. code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
  209. code_puppy/plugins/hook_manager/__init__.py +1 -0
  210. code_puppy/plugins/hook_manager/config.py +290 -0
  211. code_puppy/plugins/hook_manager/hooks_menu.py +564 -0
  212. code_puppy/plugins/hook_manager/register_callbacks.py +227 -0
  213. code_puppy/plugins/oauth_puppy_html.py +228 -0
  214. code_puppy/plugins/scheduler/__init__.py +1 -0
  215. code_puppy/plugins/scheduler/register_callbacks.py +88 -0
  216. code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
  217. code_puppy/plugins/scheduler/scheduler_wizard.py +341 -0
  218. code_puppy/plugins/shell_safety/__init__.py +6 -0
  219. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  220. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  221. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  222. code_puppy/plugins/synthetic_status/__init__.py +1 -0
  223. code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
  224. code_puppy/plugins/synthetic_status/status_api.py +147 -0
  225. code_puppy/plugins/universal_constructor/__init__.py +13 -0
  226. code_puppy/plugins/universal_constructor/models.py +138 -0
  227. code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
  228. code_puppy/plugins/universal_constructor/registry.py +302 -0
  229. code_puppy/plugins/universal_constructor/sandbox.py +584 -0
  230. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  231. code_puppy/pydantic_patches.py +356 -0
  232. code_puppy/reopenable_async_client.py +232 -0
  233. code_puppy/round_robin_model.py +150 -0
  234. code_puppy/scheduler/__init__.py +41 -0
  235. code_puppy/scheduler/__main__.py +9 -0
  236. code_puppy/scheduler/cli.py +118 -0
  237. code_puppy/scheduler/config.py +126 -0
  238. code_puppy/scheduler/daemon.py +280 -0
  239. code_puppy/scheduler/executor.py +155 -0
  240. code_puppy/scheduler/platform.py +19 -0
  241. code_puppy/scheduler/platform_unix.py +22 -0
  242. code_puppy/scheduler/platform_win.py +32 -0
  243. code_puppy/session_storage.py +338 -0
  244. code_puppy/status_display.py +257 -0
  245. code_puppy/summarization_agent.py +176 -0
  246. code_puppy/terminal_utils.py +418 -0
  247. code_puppy/tools/__init__.py +501 -0
  248. code_puppy/tools/agent_tools.py +603 -0
  249. code_puppy/tools/ask_user_question/__init__.py +26 -0
  250. code_puppy/tools/ask_user_question/constants.py +73 -0
  251. code_puppy/tools/ask_user_question/demo_tui.py +55 -0
  252. code_puppy/tools/ask_user_question/handler.py +232 -0
  253. code_puppy/tools/ask_user_question/models.py +304 -0
  254. code_puppy/tools/ask_user_question/registration.py +26 -0
  255. code_puppy/tools/ask_user_question/renderers.py +309 -0
  256. code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
  257. code_puppy/tools/ask_user_question/theme.py +155 -0
  258. code_puppy/tools/ask_user_question/tui_loop.py +423 -0
  259. code_puppy/tools/browser/__init__.py +37 -0
  260. code_puppy/tools/browser/browser_control.py +289 -0
  261. code_puppy/tools/browser/browser_interactions.py +545 -0
  262. code_puppy/tools/browser/browser_locators.py +640 -0
  263. code_puppy/tools/browser/browser_manager.py +378 -0
  264. code_puppy/tools/browser/browser_navigation.py +251 -0
  265. code_puppy/tools/browser/browser_screenshot.py +179 -0
  266. code_puppy/tools/browser/browser_scripts.py +462 -0
  267. code_puppy/tools/browser/browser_workflows.py +221 -0
  268. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  269. code_puppy/tools/browser/terminal_command_tools.py +534 -0
  270. code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
  271. code_puppy/tools/browser/terminal_tools.py +525 -0
  272. code_puppy/tools/command_runner.py +1346 -0
  273. code_puppy/tools/common.py +1409 -0
  274. code_puppy/tools/display.py +84 -0
  275. code_puppy/tools/file_modifications.py +886 -0
  276. code_puppy/tools/file_operations.py +802 -0
  277. code_puppy/tools/scheduler_tools.py +412 -0
  278. code_puppy/tools/skills_tools.py +244 -0
  279. code_puppy/tools/subagent_context.py +158 -0
  280. code_puppy/tools/tools_content.py +51 -0
  281. code_puppy/tools/universal_constructor.py +889 -0
  282. code_puppy/uvx_detection.py +242 -0
  283. code_puppy/version_checker.py +82 -0
  284. codepp-0.0.437.dist-info/METADATA +766 -0
  285. codepp-0.0.437.dist-info/RECORD +288 -0
  286. codepp-0.0.437.dist-info/WHEEL +4 -0
  287. codepp-0.0.437.dist-info/entry_points.txt +3 -0
  288. codepp-0.0.437.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,378 @@
1
+ """Playwright browser manager for browser automation.
2
+
3
+ Supports multiple simultaneous instances with unique profile directories.
4
+ """
5
+
6
+ import asyncio
7
+ import atexit
8
+ import contextvars
9
+ import os
10
+ from pathlib import Path
11
+ from typing import Callable, Dict, Optional
12
+
13
+ from playwright.async_api import Browser, BrowserContext, Page
14
+
15
+ from code_puppy import config
16
+ from code_puppy.messaging import emit_info, emit_success, emit_warning
17
+
18
+ # Registry for custom browser types from plugins (e.g., Camoufox for stealth browsing)
19
+ _CUSTOM_BROWSER_TYPES: Dict[str, Callable] = {}
20
+ _BROWSER_TYPES_LOADED: bool = False
21
+
22
+
23
+ def _load_plugin_browser_types() -> None:
24
+ """Load custom browser types from plugins.
25
+
26
+ This is called lazily on first browser initialization to allow plugins
27
+ to register custom browser providers (like Camoufox for stealth browsing).
28
+ """
29
+ global _CUSTOM_BROWSER_TYPES, _BROWSER_TYPES_LOADED
30
+
31
+ if _BROWSER_TYPES_LOADED:
32
+ return
33
+
34
+ _BROWSER_TYPES_LOADED = True
35
+
36
+ try:
37
+ from code_puppy.callbacks import on_register_browser_types
38
+
39
+ results = on_register_browser_types()
40
+ for result in results:
41
+ if isinstance(result, dict):
42
+ _CUSTOM_BROWSER_TYPES.update(result)
43
+ except Exception:
44
+ pass # Don't break if plugins fail to load
45
+
46
+
47
+ # Store active manager instances by session ID
48
+ _active_managers: dict[str, "BrowserManager"] = {}
49
+
50
+ # Context variable for browser session - properly inherits through async tasks
51
+ # This allows parallel agent invocations to each have their own browser instance
52
+ _browser_session_var: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar(
53
+ "browser_session", default=None
54
+ )
55
+
56
+
57
+ def set_browser_session(session_id: Optional[str]) -> contextvars.Token:
58
+ """Set the browser session ID for the current context.
59
+
60
+ This must be called BEFORE any tool calls that use the browser.
61
+ The context will properly propagate to all subsequent async calls.
62
+
63
+ Args:
64
+ session_id: The session ID to use for browser operations.
65
+
66
+ Returns:
67
+ A token that can be used to reset the context.
68
+ """
69
+ return _browser_session_var.set(session_id)
70
+
71
+
72
+ def get_browser_session() -> Optional[str]:
73
+ """Get the browser session ID for the current context.
74
+
75
+ Returns:
76
+ The current session ID, or None if not set.
77
+ """
78
+ return _browser_session_var.get()
79
+
80
+
81
+ def get_session_browser_manager() -> "BrowserManager":
82
+ """Get the BrowserManager for the current context's session.
83
+
84
+ This is the preferred way to get a browser manager in tool functions,
85
+ as it automatically uses the correct session ID for the current
86
+ agent context.
87
+
88
+ Returns:
89
+ A BrowserManager instance for the current session.
90
+ """
91
+ session_id = get_browser_session()
92
+ return get_browser_manager(session_id)
93
+
94
+
95
+ # Flag to track if cleanup has already run
96
+ _cleanup_done: bool = False
97
+
98
+
99
+ class BrowserManager:
100
+ """Browser manager for Playwright-based browser automation.
101
+
102
+ Supports multiple simultaneous instances, each with its own profile directory.
103
+ Uses Chromium by default for maximum compatibility.
104
+ """
105
+
106
+ _browser: Optional[Browser] = None
107
+ _context: Optional[BrowserContext] = None
108
+ _initialized: bool = False
109
+
110
+ def __init__(
111
+ self, session_id: Optional[str] = None, browser_type: Optional[str] = None
112
+ ):
113
+ """Initialize manager settings.
114
+
115
+ Args:
116
+ session_id: Optional session ID for this instance.
117
+ If None, uses 'default' as the session ID.
118
+ browser_type: Optional browser type to use. If None, uses Chromium.
119
+ Custom types can be registered via the register_browser_types hook.
120
+ """
121
+ self.session_id = session_id or "default"
122
+ self.browser_type = browser_type # None means default Chromium
123
+
124
+ # Default to headless=True (no browser spam during tests)
125
+ # Override with BROWSER_HEADLESS=false to see the browser
126
+ self.headless = os.getenv("BROWSER_HEADLESS", "true").lower() != "false"
127
+ self.homepage = "https://www.google.com"
128
+
129
+ # Unique profile directory per session for browser state
130
+ self.profile_dir = self._get_profile_directory()
131
+
132
+ def _get_profile_directory(self) -> Path:
133
+ """Get or create the profile directory for this session.
134
+
135
+ Each session gets its own profile directory under:
136
+ XDG_CACHE_HOME/code_puppy/browser_profiles/<session_id>/
137
+
138
+ This allows multiple instances to run simultaneously.
139
+ """
140
+ cache_dir = Path(config.CACHE_DIR)
141
+ profiles_base = cache_dir / "browser_profiles"
142
+ profile_path = profiles_base / self.session_id
143
+ profile_path.mkdir(parents=True, exist_ok=True, mode=0o700)
144
+ return profile_path
145
+
146
+ async def async_initialize(self) -> None:
147
+ """Initialize Chromium browser via Playwright."""
148
+ if self._initialized:
149
+ return
150
+
151
+ try:
152
+ emit_info(f"Initializing Chromium browser (session: {self.session_id})...")
153
+ await self._initialize_browser()
154
+ self._initialized = True
155
+
156
+ except Exception:
157
+ await self._cleanup()
158
+ raise
159
+
160
+ async def _initialize_browser(self) -> None:
161
+ """Initialize browser with persistent context.
162
+
163
+ Checks for custom browser types registered via plugins first,
164
+ then falls back to default Playwright Chromium.
165
+ """
166
+ # Load plugin browser types on first initialization
167
+ _load_plugin_browser_types()
168
+
169
+ # Check if a custom browser type was requested and is available
170
+ if self.browser_type and self.browser_type in _CUSTOM_BROWSER_TYPES:
171
+ emit_info(
172
+ f"Using custom browser type '{self.browser_type}' "
173
+ f"(session: {self.session_id})"
174
+ )
175
+ init_func = _CUSTOM_BROWSER_TYPES[self.browser_type]
176
+ # Custom init functions should set self._context and self._browser
177
+ await init_func(self)
178
+ self._initialized = True
179
+ return
180
+
181
+ # Default: use Playwright Chromium
182
+ from playwright.async_api import async_playwright
183
+
184
+ emit_info(f"Using persistent profile: {self.profile_dir}")
185
+
186
+ pw = await async_playwright().start()
187
+ # Use persistent context directory for Chromium to preserve browser state
188
+ context = await pw.chromium.launch_persistent_context(
189
+ user_data_dir=str(self.profile_dir), headless=self.headless
190
+ )
191
+ self._context = context
192
+ self._browser = context.browser
193
+ self._initialized = True
194
+
195
+ async def get_current_page(self) -> Optional[Page]:
196
+ """Get the currently active page. Lazily creates one if none exist."""
197
+ if not self._initialized or not self._context:
198
+ await self.async_initialize()
199
+
200
+ if not self._context:
201
+ return None
202
+
203
+ pages = self._context.pages
204
+ if pages:
205
+ return pages[0]
206
+
207
+ # Lazily create a new blank page without navigation
208
+ return await self._context.new_page()
209
+
210
+ async def new_page(self, url: Optional[str] = None) -> Page:
211
+ """Create a new page and optionally navigate to URL."""
212
+ if not self._initialized:
213
+ await self.async_initialize()
214
+
215
+ page = await self._context.new_page()
216
+ if url:
217
+ await page.goto(url)
218
+ return page
219
+
220
+ async def close_page(self, page: Page) -> None:
221
+ """Close a specific page."""
222
+ await page.close()
223
+
224
+ async def get_all_pages(self) -> list[Page]:
225
+ """Get all open pages."""
226
+ if not self._context:
227
+ return []
228
+ return self._context.pages
229
+
230
+ async def _cleanup(self, silent: bool = False) -> None:
231
+ """Clean up browser resources and save persistent state.
232
+
233
+ Args:
234
+ silent: If True, suppress all errors (used during shutdown).
235
+ """
236
+ try:
237
+ # Save browser state before closing (cookies, localStorage, etc.)
238
+ if self._context:
239
+ try:
240
+ storage_state_path = self.profile_dir / "storage_state.json"
241
+ await self._context.storage_state(path=str(storage_state_path))
242
+ if not silent:
243
+ emit_success(f"Browser state saved to {storage_state_path}")
244
+ except Exception as e:
245
+ if not silent:
246
+ emit_warning(f"Could not save storage state: {e}")
247
+
248
+ try:
249
+ await self._context.close()
250
+ except Exception:
251
+ pass # Ignore errors during context close
252
+ self._context = None
253
+
254
+ if self._browser:
255
+ try:
256
+ await self._browser.close()
257
+ except Exception:
258
+ pass # Ignore errors during browser close
259
+ self._browser = None
260
+
261
+ self._initialized = False
262
+
263
+ # Remove from active managers
264
+ if self.session_id in _active_managers:
265
+ del _active_managers[self.session_id]
266
+
267
+ except Exception as e:
268
+ if not silent:
269
+ emit_warning(f"Warning during cleanup: {e}")
270
+
271
+ async def close(self) -> None:
272
+ """Close the browser and clean up resources."""
273
+ await self._cleanup()
274
+ emit_info(f"Browser closed (session: {self.session_id})")
275
+
276
+
277
+ def get_browser_manager(
278
+ session_id: Optional[str] = None, browser_type: Optional[str] = None
279
+ ) -> BrowserManager:
280
+ """Get or create a BrowserManager instance.
281
+
282
+ Args:
283
+ session_id: Optional session ID. If provided and a manager with this
284
+ session exists, returns that manager. Otherwise creates a new one.
285
+ If None, uses 'default' as the session ID.
286
+ browser_type: Optional browser type to use for new managers.
287
+ Ignored if a manager for this session already exists.
288
+ Custom types can be registered via the register_browser_types hook.
289
+
290
+ Returns:
291
+ A BrowserManager instance.
292
+
293
+ Example:
294
+ # Default session (for single-agent use)
295
+ manager = get_browser_manager()
296
+
297
+ # Named session (for multi-agent use)
298
+ manager = get_browser_manager("qa-agent-1")
299
+
300
+ # Custom browser type (e.g., stealth browser from plugin)
301
+ manager = get_browser_manager("stealth-session", browser_type="camoufox")
302
+ """
303
+ session_id = session_id or "default"
304
+
305
+ if session_id not in _active_managers:
306
+ _active_managers[session_id] = BrowserManager(session_id, browser_type)
307
+
308
+ return _active_managers[session_id]
309
+
310
+
311
+ async def cleanup_all_browsers() -> None:
312
+ """Close all active browser manager instances.
313
+
314
+ This should be called before application exit to ensure all browser
315
+ connections are properly closed and no dangling futures remain.
316
+ """
317
+ global _cleanup_done
318
+
319
+ if _cleanup_done:
320
+ return
321
+
322
+ _cleanup_done = True
323
+
324
+ # Get a copy of the keys since we'll be modifying the dict during cleanup
325
+ session_ids = list(_active_managers.keys())
326
+
327
+ for session_id in session_ids:
328
+ manager = _active_managers.get(session_id)
329
+ if manager and manager._initialized:
330
+ try:
331
+ await manager._cleanup(silent=True)
332
+ except Exception:
333
+ pass # Silently ignore all errors during exit cleanup
334
+
335
+
336
+ def _sync_cleanup_browsers() -> None:
337
+ """Synchronous cleanup wrapper for use with atexit.
338
+
339
+ Creates a new event loop to run the async cleanup since the main
340
+ event loop may have already been closed when atexit handlers run.
341
+ """
342
+ global _cleanup_done
343
+
344
+ if _cleanup_done or not _active_managers:
345
+ return
346
+
347
+ try:
348
+ # Try to get the running loop first
349
+ try:
350
+ loop = asyncio.get_running_loop()
351
+ # If we're in an async context, schedule the cleanup
352
+ # but this is unlikely in atexit handlers
353
+ loop.create_task(cleanup_all_browsers())
354
+ return
355
+ except RuntimeError:
356
+ pass # No running loop, which is expected in atexit
357
+
358
+ # Create a new event loop for cleanup
359
+ loop = asyncio.new_event_loop()
360
+ asyncio.set_event_loop(loop)
361
+ try:
362
+ loop.run_until_complete(cleanup_all_browsers())
363
+ finally:
364
+ loop.close()
365
+ except Exception:
366
+ # Silently swallow ALL errors during exit cleanup
367
+ # We don't want to spam the user with errors on exit
368
+ pass
369
+
370
+
371
+ # Register the cleanup handler with atexit
372
+ # This ensures browsers are closed even if close_browser() isn't explicitly called
373
+ atexit.register(_sync_cleanup_browsers)
374
+
375
+
376
+ # Backwards compatibility aliases
377
+ CamoufoxManager = BrowserManager
378
+ get_camoufox_manager = get_browser_manager
@@ -0,0 +1,251 @@
1
+ """Browser navigation and control tools."""
2
+
3
+ from typing import Any, Dict
4
+
5
+ from pydantic_ai import RunContext
6
+
7
+ from code_puppy.messaging import emit_error, emit_info, emit_success
8
+ from code_puppy.tools.common import generate_group_id
9
+
10
+ from .browser_manager import get_session_browser_manager
11
+
12
+
13
+ async def navigate_to_url(url: str) -> Dict[str, Any]:
14
+ """Navigate to a specific URL."""
15
+ group_id = generate_group_id("browser_navigate", url)
16
+ emit_info(
17
+ f"BROWSER NAVIGATE 🌐 {url}",
18
+ message_group=group_id,
19
+ )
20
+ try:
21
+ browser_manager = get_session_browser_manager()
22
+ page = await browser_manager.get_current_page()
23
+
24
+ if not page:
25
+ return {"success": False, "error": "No active browser page available"}
26
+
27
+ # Navigate to URL
28
+ await page.goto(url, wait_until="domcontentloaded", timeout=30000)
29
+
30
+ # Get final URL (in case of redirects)
31
+ final_url = page.url
32
+ title = await page.title()
33
+
34
+ emit_success(f"Navigated to: {final_url}", message_group=group_id)
35
+
36
+ return {"success": True, "url": final_url, "title": title, "requested_url": url}
37
+
38
+ except Exception as e:
39
+ emit_error(f"Navigation failed: {str(e)}", message_group=group_id)
40
+ return {"success": False, "error": str(e), "url": url}
41
+
42
+
43
+ async def get_page_info() -> Dict[str, Any]:
44
+ """Get current page information."""
45
+ group_id = generate_group_id("browser_get_page_info")
46
+ emit_info(
47
+ "BROWSER GET PAGE INFO 📌",
48
+ message_group=group_id,
49
+ )
50
+ try:
51
+ browser_manager = get_session_browser_manager()
52
+ page = await browser_manager.get_current_page()
53
+
54
+ if not page:
55
+ return {"success": False, "error": "No active browser page available"}
56
+
57
+ url = page.url
58
+ title = await page.title()
59
+
60
+ return {"success": True, "url": url, "title": title}
61
+
62
+ except Exception as e:
63
+ return {"success": False, "error": str(e)}
64
+
65
+
66
+ async def go_back() -> Dict[str, Any]:
67
+ """Navigate back in browser history."""
68
+ group_id = generate_group_id("browser_go_back")
69
+ emit_info(
70
+ "BROWSER GO BACK ⬅️",
71
+ message_group=group_id,
72
+ )
73
+ try:
74
+ browser_manager = get_session_browser_manager()
75
+ page = await browser_manager.get_current_page()
76
+
77
+ if not page:
78
+ return {"success": False, "error": "No active browser page available"}
79
+
80
+ await page.go_back(wait_until="domcontentloaded")
81
+
82
+ return {"success": True, "url": page.url, "title": await page.title()}
83
+
84
+ except Exception as e:
85
+ return {"success": False, "error": str(e)}
86
+
87
+
88
+ async def go_forward() -> Dict[str, Any]:
89
+ """Navigate forward in browser history."""
90
+ group_id = generate_group_id("browser_go_forward")
91
+ emit_info(
92
+ "BROWSER GO FORWARD ➡️",
93
+ message_group=group_id,
94
+ )
95
+ try:
96
+ browser_manager = get_session_browser_manager()
97
+ page = await browser_manager.get_current_page()
98
+
99
+ if not page:
100
+ return {"success": False, "error": "No active browser page available"}
101
+
102
+ await page.go_forward(wait_until="domcontentloaded")
103
+
104
+ return {"success": True, "url": page.url, "title": await page.title()}
105
+
106
+ except Exception as e:
107
+ return {"success": False, "error": str(e)}
108
+
109
+
110
+ async def reload_page(wait_until: str = "domcontentloaded") -> Dict[str, Any]:
111
+ """Reload the current page."""
112
+ group_id = generate_group_id("browser_reload", wait_until)
113
+ emit_info(
114
+ f"BROWSER RELOAD 🔄 wait_until={wait_until}",
115
+ message_group=group_id,
116
+ )
117
+ try:
118
+ browser_manager = get_session_browser_manager()
119
+ page = await browser_manager.get_current_page()
120
+
121
+ if not page:
122
+ return {"success": False, "error": "No active browser page available"}
123
+
124
+ await page.reload(wait_until=wait_until)
125
+
126
+ return {"success": True, "url": page.url, "title": await page.title()}
127
+
128
+ except Exception as e:
129
+ return {"success": False, "error": str(e)}
130
+
131
+
132
+ async def wait_for_load_state(
133
+ state: str = "domcontentloaded", timeout: int = 30000
134
+ ) -> Dict[str, Any]:
135
+ """Wait for page to reach a specific load state."""
136
+ group_id = generate_group_id("browser_wait_for_load", f"{state}_{timeout}")
137
+ emit_info(
138
+ f"BROWSER WAIT FOR LOAD ⏱️ state={state} timeout={timeout}ms",
139
+ message_group=group_id,
140
+ )
141
+ try:
142
+ browser_manager = get_session_browser_manager()
143
+ page = await browser_manager.get_current_page()
144
+
145
+ if not page:
146
+ return {"success": False, "error": "No active browser page available"}
147
+
148
+ await page.wait_for_load_state(state, timeout=timeout)
149
+
150
+ return {"success": True, "state": state, "url": page.url}
151
+
152
+ except Exception as e:
153
+ return {"success": False, "error": str(e), "state": state}
154
+
155
+
156
+ def register_navigate_to_url(agent):
157
+ """Register the navigation tool."""
158
+
159
+ @agent.tool
160
+ async def browser_navigate(context: RunContext, url: str) -> Dict[str, Any]:
161
+ """
162
+ Navigate the browser to a specific URL.
163
+
164
+ Args:
165
+ url: The URL to navigate to (must include protocol like https://)
166
+
167
+ Returns:
168
+ Dict with navigation results including final URL and page title
169
+ """
170
+ return await navigate_to_url(url)
171
+
172
+
173
+ def register_get_page_info(agent):
174
+ """Register the page info tool."""
175
+
176
+ @agent.tool
177
+ async def browser_get_page_info(context: RunContext) -> Dict[str, Any]:
178
+ """
179
+ Get information about the current page.
180
+
181
+ Returns:
182
+ Dict with current URL and page title
183
+ """
184
+ return await get_page_info()
185
+
186
+
187
+ def register_browser_go_back(agent):
188
+ """Register browser go back tool."""
189
+
190
+ @agent.tool
191
+ async def browser_go_back(context: RunContext) -> Dict[str, Any]:
192
+ """
193
+ Navigate back in browser history.
194
+
195
+ Returns:
196
+ Dict with navigation results
197
+ """
198
+ return await go_back()
199
+
200
+
201
+ def register_browser_go_forward(agent):
202
+ """Register browser go forward tool."""
203
+
204
+ @agent.tool
205
+ async def browser_go_forward(context: RunContext) -> Dict[str, Any]:
206
+ """
207
+ Navigate forward in browser history.
208
+
209
+ Returns:
210
+ Dict with navigation results
211
+ """
212
+ return await go_forward()
213
+
214
+
215
+ def register_reload_page(agent):
216
+ """Register the page reload tool."""
217
+
218
+ @agent.tool
219
+ async def browser_reload(
220
+ context: RunContext, wait_until: str = "domcontentloaded"
221
+ ) -> Dict[str, Any]:
222
+ """
223
+ Reload the current page.
224
+
225
+ Args:
226
+ wait_until: Load state to wait for (networkidle, domcontentloaded, load)
227
+
228
+ Returns:
229
+ Dict with reload results
230
+ """
231
+ return await reload_page(wait_until)
232
+
233
+
234
+ def register_wait_for_load_state(agent):
235
+ """Register the wait for load state tool."""
236
+
237
+ @agent.tool
238
+ async def browser_wait_for_load(
239
+ context: RunContext, state: str = "domcontentloaded", timeout: int = 30000
240
+ ) -> Dict[str, Any]:
241
+ """
242
+ Wait for the page to reach a specific load state.
243
+
244
+ Args:
245
+ state: Load state to wait for (networkidle, domcontentloaded, load)
246
+ timeout: Timeout in milliseconds
247
+
248
+ Returns:
249
+ Dict with wait results
250
+ """
251
+ return await wait_for_load_state(state, timeout)