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.
- code_puppy/__init__.py +7 -1
- code_puppy/agents/__init__.py +8 -8
- code_puppy/agents/agent_c_reviewer.py +155 -0
- code_puppy/agents/agent_code_puppy.py +9 -2
- code_puppy/agents/agent_code_reviewer.py +90 -0
- code_puppy/agents/agent_cpp_reviewer.py +132 -0
- code_puppy/agents/agent_creator_agent.py +48 -9
- code_puppy/agents/agent_golang_reviewer.py +151 -0
- code_puppy/agents/agent_javascript_reviewer.py +160 -0
- code_puppy/agents/agent_manager.py +146 -199
- code_puppy/agents/agent_pack_leader.py +383 -0
- code_puppy/agents/agent_planning.py +163 -0
- code_puppy/agents/agent_python_programmer.py +165 -0
- code_puppy/agents/agent_python_reviewer.py +90 -0
- code_puppy/agents/agent_qa_expert.py +163 -0
- code_puppy/agents/agent_qa_kitten.py +208 -0
- code_puppy/agents/agent_security_auditor.py +181 -0
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +166 -0
- code_puppy/agents/base_agent.py +1713 -1
- code_puppy/agents/event_stream_handler.py +350 -0
- code_puppy/agents/json_agent.py +12 -1
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +321 -0
- code_puppy/agents/pack/retriever.py +393 -0
- code_puppy/agents/pack/shepherd.py +348 -0
- code_puppy/agents/pack/terrier.py +287 -0
- code_puppy/agents/pack/watchdog.py +367 -0
- code_puppy/agents/prompt_reviewer.py +145 -0
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +169 -0
- code_puppy/api/main.py +21 -0
- code_puppy/api/pty_manager.py +446 -0
- code_puppy/api/routers/__init__.py +12 -0
- code_puppy/api/routers/agents.py +36 -0
- code_puppy/api/routers/commands.py +217 -0
- code_puppy/api/routers/config.py +74 -0
- code_puppy/api/routers/sessions.py +232 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +174 -4
- code_puppy/chatgpt_codex_client.py +283 -0
- code_puppy/claude_cache_client.py +586 -0
- code_puppy/cli_runner.py +916 -0
- code_puppy/command_line/add_model_menu.py +1079 -0
- code_puppy/command_line/agent_menu.py +395 -0
- code_puppy/command_line/attachments.py +395 -0
- code_puppy/command_line/autosave_menu.py +605 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +520 -0
- code_puppy/command_line/command_handler.py +233 -627
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +715 -0
- code_puppy/command_line/core_commands.py +792 -0
- code_puppy/command_line/diff_menu.py +863 -0
- code_puppy/command_line/load_context_completion.py +15 -22
- code_puppy/command_line/mcp/base.py +1 -4
- code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
- code_puppy/command_line/mcp/custom_server_form.py +688 -0
- code_puppy/command_line/mcp/custom_server_installer.py +195 -0
- code_puppy/command_line/mcp/edit_command.py +148 -0
- code_puppy/command_line/mcp/handler.py +9 -4
- code_puppy/command_line/mcp/help_command.py +6 -5
- code_puppy/command_line/mcp/install_command.py +16 -27
- code_puppy/command_line/mcp/install_menu.py +685 -0
- code_puppy/command_line/mcp/list_command.py +3 -3
- code_puppy/command_line/mcp/logs_command.py +174 -65
- code_puppy/command_line/mcp/remove_command.py +2 -2
- code_puppy/command_line/mcp/restart_command.py +12 -4
- code_puppy/command_line/mcp/search_command.py +17 -11
- code_puppy/command_line/mcp/start_all_command.py +22 -13
- code_puppy/command_line/mcp/start_command.py +50 -31
- code_puppy/command_line/mcp/status_command.py +6 -7
- code_puppy/command_line/mcp/stop_all_command.py +11 -8
- code_puppy/command_line/mcp/stop_command.py +11 -10
- code_puppy/command_line/mcp/test_command.py +2 -2
- code_puppy/command_line/mcp/utils.py +1 -1
- code_puppy/command_line/mcp/wizard_utils.py +22 -18
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +89 -30
- code_puppy/command_line/model_settings_menu.py +884 -0
- code_puppy/command_line/motd.py +14 -8
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +340 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +626 -75
- code_puppy/command_line/session_commands.py +296 -0
- code_puppy/command_line/utils.py +54 -0
- code_puppy/config.py +1181 -51
- code_puppy/error_logging.py +118 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +602 -0
- code_puppy/http_utils.py +220 -104
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +5 -594
- code_puppy/{mcp → mcp_}/__init__.py +17 -0
- code_puppy/{mcp → mcp_}/async_lifecycle.py +35 -4
- code_puppy/{mcp → mcp_}/blocking_startup.py +70 -43
- code_puppy/{mcp → mcp_}/captured_stdio_server.py +2 -2
- code_puppy/{mcp → mcp_}/config_wizard.py +5 -5
- code_puppy/{mcp → mcp_}/dashboard.py +15 -6
- code_puppy/{mcp → mcp_}/examples/retry_example.py +4 -1
- code_puppy/{mcp → mcp_}/managed_server.py +66 -39
- code_puppy/{mcp → mcp_}/manager.py +146 -52
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/{mcp → mcp_}/registry.py +6 -6
- code_puppy/{mcp → mcp_}/server_registry_catalog.py +25 -8
- code_puppy/messaging/__init__.py +199 -2
- code_puppy/messaging/bus.py +610 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +17 -48
- code_puppy/messaging/messages.py +500 -0
- code_puppy/messaging/queue_console.py +1 -24
- code_puppy/messaging/renderers.py +43 -146
- code_puppy/messaging/rich_renderer.py +1027 -0
- code_puppy/messaging/spinner/__init__.py +33 -5
- code_puppy/messaging/spinner/console_spinner.py +92 -52
- code_puppy/messaging/spinner/spinner_base.py +29 -0
- code_puppy/messaging/subagent_console.py +461 -0
- code_puppy/model_factory.py +686 -80
- code_puppy/model_utils.py +167 -0
- code_puppy/models.json +86 -104
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +164 -10
- code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
- code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +136 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
- code_puppy/plugins/antigravity_oauth/storage.py +271 -0
- code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
- code_puppy/plugins/antigravity_oauth/token.py +167 -0
- code_puppy/plugins/antigravity_oauth/transport.py +767 -0
- code_puppy/plugins/antigravity_oauth/utils.py +169 -0
- code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
- code_puppy/plugins/chatgpt_oauth/config.py +52 -0
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
- code_puppy/plugins/claude_code_oauth/README.md +167 -0
- code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
- code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
- code_puppy/plugins/claude_code_oauth/config.py +50 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/utils.py +518 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
- code_puppy/plugins/example_custom_command/README.md +280 -0
- code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
- code_puppy/plugins/file_permission_handler/__init__.py +4 -0
- code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
- code_puppy/plugins/frontend_emitter/__init__.py +25 -0
- code_puppy/plugins/frontend_emitter/emitter.py +121 -0
- code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
- code_puppy/plugins/oauth_puppy_html.py +228 -0
- code_puppy/plugins/shell_safety/__init__.py +6 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
- code_puppy/plugins/shell_safety/command_cache.py +156 -0
- code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/prompts/codex_system_prompt.md +310 -0
- code_puppy/pydantic_patches.py +131 -0
- code_puppy/reopenable_async_client.py +8 -8
- code_puppy/round_robin_model.py +10 -15
- code_puppy/session_storage.py +294 -0
- code_puppy/status_display.py +21 -4
- code_puppy/summarization_agent.py +52 -14
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +139 -6
- code_puppy/tools/agent_tools.py +548 -49
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +289 -0
- code_puppy/tools/browser/browser_interactions.py +545 -0
- code_puppy/tools/browser/browser_locators.py +640 -0
- code_puppy/tools/browser/browser_manager.py +316 -0
- code_puppy/tools/browser/browser_navigation.py +251 -0
- code_puppy/tools/browser/browser_screenshot.py +179 -0
- code_puppy/tools/browser/browser_scripts.py +462 -0
- code_puppy/tools/browser/browser_workflows.py +221 -0
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +521 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +941 -153
- code_puppy/tools/common.py +1146 -6
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +288 -89
- code_puppy/tools/file_operations.py +352 -266
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +30 -11
- code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
- code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
- {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/METADATA +184 -67
- code_puppy-0.0.366.dist-info/RECORD +217 -0
- {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
- {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +1 -0
- code_puppy/agent.py +0 -231
- code_puppy/agents/agent_orchestrator.json +0 -26
- code_puppy/agents/runtime_manager.py +0 -272
- code_puppy/command_line/mcp/add_command.py +0 -183
- code_puppy/command_line/meta_command_handler.py +0 -153
- code_puppy/message_history_processor.py +0 -490
- code_puppy/messaging/spinner/textual_spinner.py +0 -101
- code_puppy/state_management.py +0 -200
- code_puppy/tui/__init__.py +0 -10
- code_puppy/tui/app.py +0 -986
- code_puppy/tui/components/__init__.py +0 -21
- code_puppy/tui/components/chat_view.py +0 -550
- code_puppy/tui/components/command_history_modal.py +0 -218
- code_puppy/tui/components/copy_button.py +0 -139
- code_puppy/tui/components/custom_widgets.py +0 -63
- code_puppy/tui/components/human_input_modal.py +0 -175
- code_puppy/tui/components/input_area.py +0 -167
- code_puppy/tui/components/sidebar.py +0 -309
- code_puppy/tui/components/status_bar.py +0 -182
- code_puppy/tui/messages.py +0 -27
- code_puppy/tui/models/__init__.py +0 -8
- code_puppy/tui/models/chat_message.py +0 -25
- code_puppy/tui/models/command_history.py +0 -89
- code_puppy/tui/models/enums.py +0 -24
- code_puppy/tui/screens/__init__.py +0 -15
- code_puppy/tui/screens/help.py +0 -130
- code_puppy/tui/screens/mcp_install_wizard.py +0 -803
- code_puppy/tui/screens/settings.py +0 -290
- code_puppy/tui/screens/tools.py +0 -74
- code_puppy-0.0.169.data/data/code_puppy/models.json +0 -128
- code_puppy-0.0.169.dist-info/RECORD +0 -112
- /code_puppy/{mcp → mcp_}/circuit_breaker.py +0 -0
- /code_puppy/{mcp → mcp_}/error_isolation.py +0 -0
- /code_puppy/{mcp → mcp_}/health_monitor.py +0 -0
- /code_puppy/{mcp → mcp_}/retry_manager.py +0 -0
- /code_puppy/{mcp → mcp_}/status_tracker.py +0 -0
- /code_puppy/{mcp → mcp_}/system_tools.py +0 -0
- {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Screenshot tool for browser automation.
|
|
2
|
+
|
|
3
|
+
Captures screenshots and returns them via ToolReturn with BinaryContent
|
|
4
|
+
so multimodal models can directly see and analyze - no separate VQA agent needed.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from tempfile import gettempdir, mkdtemp
|
|
11
|
+
from typing import Any, Dict, Optional, Union
|
|
12
|
+
|
|
13
|
+
from pydantic_ai import BinaryContent, RunContext, ToolReturn
|
|
14
|
+
|
|
15
|
+
from code_puppy.messaging import emit_error, emit_info, emit_success
|
|
16
|
+
from code_puppy.tools.common import generate_group_id
|
|
17
|
+
|
|
18
|
+
from .browser_manager import get_session_browser_manager
|
|
19
|
+
|
|
20
|
+
_TEMP_SCREENSHOT_ROOT = Path(
|
|
21
|
+
mkdtemp(prefix="code_puppy_screenshots_", dir=gettempdir())
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _build_screenshot_path(timestamp: str) -> Path:
|
|
26
|
+
"""Return the target path for a screenshot."""
|
|
27
|
+
filename = f"screenshot_{timestamp}.png"
|
|
28
|
+
return _TEMP_SCREENSHOT_ROOT / filename
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def _capture_screenshot(
|
|
32
|
+
page,
|
|
33
|
+
full_page: bool = False,
|
|
34
|
+
element_selector: Optional[str] = None,
|
|
35
|
+
save_screenshot: bool = True,
|
|
36
|
+
group_id: Optional[str] = None,
|
|
37
|
+
) -> Dict[str, Any]:
|
|
38
|
+
"""Internal screenshot capture function."""
|
|
39
|
+
try:
|
|
40
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
|
41
|
+
|
|
42
|
+
# Take screenshot
|
|
43
|
+
if element_selector:
|
|
44
|
+
element = await page.locator(element_selector).first
|
|
45
|
+
if not await element.is_visible():
|
|
46
|
+
return {
|
|
47
|
+
"success": False,
|
|
48
|
+
"error": f"Element '{element_selector}' is not visible",
|
|
49
|
+
}
|
|
50
|
+
screenshot_bytes = await element.screenshot()
|
|
51
|
+
else:
|
|
52
|
+
screenshot_bytes = await page.screenshot(full_page=full_page)
|
|
53
|
+
|
|
54
|
+
result: Dict[str, Any] = {
|
|
55
|
+
"success": True,
|
|
56
|
+
"screenshot_bytes": screenshot_bytes,
|
|
57
|
+
"timestamp": timestamp,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if save_screenshot:
|
|
61
|
+
screenshot_path = _build_screenshot_path(timestamp)
|
|
62
|
+
screenshot_path.parent.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
with open(screenshot_path, "wb") as f:
|
|
64
|
+
f.write(screenshot_bytes)
|
|
65
|
+
result["screenshot_path"] = str(screenshot_path)
|
|
66
|
+
|
|
67
|
+
if group_id:
|
|
68
|
+
emit_success(
|
|
69
|
+
f"Screenshot saved: {screenshot_path}", message_group=group_id
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return result
|
|
73
|
+
|
|
74
|
+
except Exception as e:
|
|
75
|
+
return {"success": False, "error": str(e)}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async def take_screenshot(
|
|
79
|
+
full_page: bool = False,
|
|
80
|
+
element_selector: Optional[str] = None,
|
|
81
|
+
save_screenshot: bool = True,
|
|
82
|
+
) -> Union[ToolReturn, Dict[str, Any]]:
|
|
83
|
+
"""Take a screenshot of the browser page.
|
|
84
|
+
|
|
85
|
+
Returns a ToolReturn with BinaryContent so multimodal models can
|
|
86
|
+
directly see and analyze the screenshot.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
full_page: Whether to capture full page or just viewport.
|
|
90
|
+
element_selector: Optional selector to screenshot specific element.
|
|
91
|
+
save_screenshot: Whether to save the screenshot to disk.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
ToolReturn containing:
|
|
95
|
+
- return_value: Success message with screenshot path
|
|
96
|
+
- content: List with description and BinaryContent image
|
|
97
|
+
- metadata: Screenshot details (path, target, timestamp)
|
|
98
|
+
Or Dict with error info if failed.
|
|
99
|
+
"""
|
|
100
|
+
target = element_selector or ("full_page" if full_page else "viewport")
|
|
101
|
+
group_id = generate_group_id("browser_screenshot", target)
|
|
102
|
+
emit_info(f"BROWSER SCREENSHOT 📷 target={target}", message_group=group_id)
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
browser_manager = get_session_browser_manager()
|
|
106
|
+
page = await browser_manager.get_current_page()
|
|
107
|
+
|
|
108
|
+
if not page:
|
|
109
|
+
error_msg = "No active browser page. Navigate to a webpage first."
|
|
110
|
+
emit_error(error_msg, message_group=group_id)
|
|
111
|
+
return {"success": False, "error": error_msg}
|
|
112
|
+
|
|
113
|
+
result = await _capture_screenshot(
|
|
114
|
+
page,
|
|
115
|
+
full_page=full_page,
|
|
116
|
+
element_selector=element_selector,
|
|
117
|
+
save_screenshot=save_screenshot,
|
|
118
|
+
group_id=group_id,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if not result["success"]:
|
|
122
|
+
emit_error(result.get("error", "Screenshot failed"), message_group=group_id)
|
|
123
|
+
return {"success": False, "error": result.get("error")}
|
|
124
|
+
|
|
125
|
+
screenshot_path = result.get("screenshot_path", "(not saved)")
|
|
126
|
+
|
|
127
|
+
# Return as ToolReturn with BinaryContent so the model can SEE the image!
|
|
128
|
+
return ToolReturn(
|
|
129
|
+
return_value=f"Screenshot captured successfully. Saved to: {screenshot_path}",
|
|
130
|
+
content=[
|
|
131
|
+
f"Here's the browser screenshot ({target}):",
|
|
132
|
+
BinaryContent(
|
|
133
|
+
data=result["screenshot_bytes"],
|
|
134
|
+
media_type="image/png",
|
|
135
|
+
),
|
|
136
|
+
"Please analyze what you see and describe any relevant details.",
|
|
137
|
+
],
|
|
138
|
+
metadata={
|
|
139
|
+
"success": True,
|
|
140
|
+
"screenshot_path": screenshot_path,
|
|
141
|
+
"target": target,
|
|
142
|
+
"full_page": full_page,
|
|
143
|
+
"element_selector": element_selector,
|
|
144
|
+
"timestamp": time.time(),
|
|
145
|
+
},
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
except Exception as e:
|
|
149
|
+
error_msg = f"Screenshot failed: {str(e)}"
|
|
150
|
+
emit_error(error_msg, message_group=group_id)
|
|
151
|
+
return {"success": False, "error": error_msg}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def register_take_screenshot_and_analyze(agent):
|
|
155
|
+
"""Register the screenshot tool."""
|
|
156
|
+
|
|
157
|
+
@agent.tool
|
|
158
|
+
async def browser_screenshot_analyze(
|
|
159
|
+
context: RunContext,
|
|
160
|
+
full_page: bool = False,
|
|
161
|
+
element_selector: Optional[str] = None,
|
|
162
|
+
) -> Union[ToolReturn, Dict[str, Any]]:
|
|
163
|
+
"""
|
|
164
|
+
Take a screenshot of the browser page.
|
|
165
|
+
|
|
166
|
+
Returns the screenshot via ToolReturn with BinaryContent that you can
|
|
167
|
+
see directly. Use this to see what's displayed in the browser.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
full_page: Capture full page (True) or just viewport (False).
|
|
171
|
+
element_selector: Optional CSS selector to screenshot specific element.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
ToolReturn with the screenshot image you can analyze, or error dict.
|
|
175
|
+
"""
|
|
176
|
+
return await take_screenshot(
|
|
177
|
+
full_page=full_page,
|
|
178
|
+
element_selector=element_selector,
|
|
179
|
+
)
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
"""JavaScript execution and advanced page manipulation tools."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
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 execute_javascript(
|
|
14
|
+
script: str,
|
|
15
|
+
timeout: int = 30000,
|
|
16
|
+
) -> Dict[str, Any]:
|
|
17
|
+
"""Execute JavaScript code in the browser context."""
|
|
18
|
+
group_id = generate_group_id("browser_execute_js", script[:100])
|
|
19
|
+
emit_info(
|
|
20
|
+
f"BROWSER EXECUTE JS 📜 script='{script[:100]}{'...' if len(script) > 100 else ''}'",
|
|
21
|
+
message_group=group_id,
|
|
22
|
+
)
|
|
23
|
+
try:
|
|
24
|
+
browser_manager = get_session_browser_manager()
|
|
25
|
+
page = await browser_manager.get_current_page()
|
|
26
|
+
|
|
27
|
+
if not page:
|
|
28
|
+
return {"success": False, "error": "No active browser page available"}
|
|
29
|
+
|
|
30
|
+
# Execute JavaScript
|
|
31
|
+
# Note: page.evaluate() does NOT accept a timeout parameter
|
|
32
|
+
# The timeout arg to this function is kept for API compatibility but unused
|
|
33
|
+
result = await page.evaluate(script)
|
|
34
|
+
|
|
35
|
+
emit_success("JavaScript executed successfully", message_group=group_id)
|
|
36
|
+
|
|
37
|
+
return {"success": True, "script": script, "result": result}
|
|
38
|
+
|
|
39
|
+
except Exception as e:
|
|
40
|
+
emit_error(f"JavaScript execution failed: {str(e)}", message_group=group_id)
|
|
41
|
+
return {"success": False, "error": str(e), "script": script}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def scroll_page(
|
|
45
|
+
direction: str = "down",
|
|
46
|
+
amount: int = 3,
|
|
47
|
+
element_selector: Optional[str] = None,
|
|
48
|
+
) -> Dict[str, Any]:
|
|
49
|
+
"""Scroll the page or a specific element."""
|
|
50
|
+
target = element_selector or "page"
|
|
51
|
+
group_id = generate_group_id("browser_scroll", f"{direction}_{amount}_{target}")
|
|
52
|
+
emit_info(
|
|
53
|
+
f"BROWSER SCROLL 📋 direction={direction} amount={amount} target='{target}'",
|
|
54
|
+
message_group=group_id,
|
|
55
|
+
)
|
|
56
|
+
try:
|
|
57
|
+
browser_manager = get_session_browser_manager()
|
|
58
|
+
page = await browser_manager.get_current_page()
|
|
59
|
+
|
|
60
|
+
if not page:
|
|
61
|
+
return {"success": False, "error": "No active browser page available"}
|
|
62
|
+
|
|
63
|
+
if element_selector:
|
|
64
|
+
# Scroll specific element
|
|
65
|
+
element = page.locator(element_selector).first
|
|
66
|
+
await element.scroll_into_view_if_needed()
|
|
67
|
+
|
|
68
|
+
# Get element's current scroll position and dimensions
|
|
69
|
+
scroll_info = await element.evaluate("""
|
|
70
|
+
el => {
|
|
71
|
+
const rect = el.getBoundingClientRect();
|
|
72
|
+
return {
|
|
73
|
+
scrollTop: el.scrollTop,
|
|
74
|
+
scrollLeft: el.scrollLeft,
|
|
75
|
+
scrollHeight: el.scrollHeight,
|
|
76
|
+
scrollWidth: el.scrollWidth,
|
|
77
|
+
clientHeight: el.clientHeight,
|
|
78
|
+
clientWidth: el.clientWidth
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
""")
|
|
82
|
+
|
|
83
|
+
# Calculate scroll amount based on element size
|
|
84
|
+
scroll_amount = scroll_info["clientHeight"] * amount / 3
|
|
85
|
+
|
|
86
|
+
if direction.lower() == "down":
|
|
87
|
+
await element.evaluate(f"el => el.scrollTop += {scroll_amount}")
|
|
88
|
+
elif direction.lower() == "up":
|
|
89
|
+
await element.evaluate(f"el => el.scrollTop -= {scroll_amount}")
|
|
90
|
+
elif direction.lower() == "left":
|
|
91
|
+
await element.evaluate(f"el => el.scrollLeft -= {scroll_amount}")
|
|
92
|
+
elif direction.lower() == "right":
|
|
93
|
+
await element.evaluate(f"el => el.scrollLeft += {scroll_amount}")
|
|
94
|
+
|
|
95
|
+
target = f"element '{element_selector}'"
|
|
96
|
+
|
|
97
|
+
else:
|
|
98
|
+
# Scroll page
|
|
99
|
+
viewport_height = await page.evaluate("() => window.innerHeight")
|
|
100
|
+
scroll_amount = viewport_height * amount / 3
|
|
101
|
+
|
|
102
|
+
if direction.lower() == "down":
|
|
103
|
+
await page.evaluate(f"window.scrollBy(0, {scroll_amount})")
|
|
104
|
+
elif direction.lower() == "up":
|
|
105
|
+
await page.evaluate(f"window.scrollBy(0, -{scroll_amount})")
|
|
106
|
+
elif direction.lower() == "left":
|
|
107
|
+
await page.evaluate(f"window.scrollBy(-{scroll_amount}, 0)")
|
|
108
|
+
elif direction.lower() == "right":
|
|
109
|
+
await page.evaluate(f"window.scrollBy({scroll_amount}, 0)")
|
|
110
|
+
|
|
111
|
+
target = "page"
|
|
112
|
+
|
|
113
|
+
# Get current scroll position
|
|
114
|
+
scroll_pos = await page.evaluate("""
|
|
115
|
+
() => ({
|
|
116
|
+
x: window.pageXOffset,
|
|
117
|
+
y: window.pageYOffset
|
|
118
|
+
})
|
|
119
|
+
""")
|
|
120
|
+
|
|
121
|
+
emit_success(f"Scrolled {target} {direction}", message_group=group_id)
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
"success": True,
|
|
125
|
+
"direction": direction,
|
|
126
|
+
"amount": amount,
|
|
127
|
+
"target": target,
|
|
128
|
+
"scroll_position": scroll_pos,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
except Exception as e:
|
|
132
|
+
return {
|
|
133
|
+
"success": False,
|
|
134
|
+
"error": str(e),
|
|
135
|
+
"direction": direction,
|
|
136
|
+
"element_selector": element_selector,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
async def scroll_to_element(
|
|
141
|
+
selector: str,
|
|
142
|
+
timeout: int = 10000,
|
|
143
|
+
) -> Dict[str, Any]:
|
|
144
|
+
"""Scroll to bring an element into view."""
|
|
145
|
+
group_id = generate_group_id("browser_scroll_to_element", selector[:100])
|
|
146
|
+
emit_info(
|
|
147
|
+
f"BROWSER SCROLL TO ELEMENT 🎯 selector='{selector}'",
|
|
148
|
+
message_group=group_id,
|
|
149
|
+
)
|
|
150
|
+
try:
|
|
151
|
+
browser_manager = get_session_browser_manager()
|
|
152
|
+
page = await browser_manager.get_current_page()
|
|
153
|
+
|
|
154
|
+
if not page:
|
|
155
|
+
return {"success": False, "error": "No active browser page available"}
|
|
156
|
+
|
|
157
|
+
element = page.locator(selector).first
|
|
158
|
+
await element.wait_for(state="attached", timeout=timeout)
|
|
159
|
+
await element.scroll_into_view_if_needed()
|
|
160
|
+
|
|
161
|
+
# Check if element is now visible
|
|
162
|
+
is_visible = await element.is_visible()
|
|
163
|
+
|
|
164
|
+
emit_success(f"Scrolled to element: {selector}", message_group=group_id)
|
|
165
|
+
|
|
166
|
+
return {"success": True, "selector": selector, "visible": is_visible}
|
|
167
|
+
|
|
168
|
+
except Exception as e:
|
|
169
|
+
return {"success": False, "error": str(e), "selector": selector}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
async def set_viewport_size(
|
|
173
|
+
width: int,
|
|
174
|
+
height: int,
|
|
175
|
+
) -> Dict[str, Any]:
|
|
176
|
+
"""Set the viewport size."""
|
|
177
|
+
group_id = generate_group_id("browser_set_viewport", f"{width}x{height}")
|
|
178
|
+
emit_info(
|
|
179
|
+
f"BROWSER SET VIEWPORT 🖥️ size={width}x{height}",
|
|
180
|
+
message_group=group_id,
|
|
181
|
+
)
|
|
182
|
+
try:
|
|
183
|
+
browser_manager = get_session_browser_manager()
|
|
184
|
+
page = await browser_manager.get_current_page()
|
|
185
|
+
|
|
186
|
+
if not page:
|
|
187
|
+
return {"success": False, "error": "No active browser page available"}
|
|
188
|
+
|
|
189
|
+
await page.set_viewport_size({"width": width, "height": height})
|
|
190
|
+
|
|
191
|
+
emit_success(
|
|
192
|
+
f"Set viewport size to {width}x{height}",
|
|
193
|
+
message_group=group_id,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
return {"success": True, "width": width, "height": height}
|
|
197
|
+
|
|
198
|
+
except Exception as e:
|
|
199
|
+
return {"success": False, "error": str(e), "width": width, "height": height}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
async def wait_for_element(
|
|
203
|
+
selector: str,
|
|
204
|
+
state: str = "visible",
|
|
205
|
+
timeout: int = 30000,
|
|
206
|
+
) -> Dict[str, Any]:
|
|
207
|
+
"""Wait for an element to reach a specific state."""
|
|
208
|
+
group_id = generate_group_id("browser_wait_for_element", f"{selector[:50]}_{state}")
|
|
209
|
+
emit_info(
|
|
210
|
+
f"BROWSER WAIT FOR ELEMENT ⏱️ selector='{selector}' state={state} timeout={timeout}ms",
|
|
211
|
+
message_group=group_id,
|
|
212
|
+
)
|
|
213
|
+
try:
|
|
214
|
+
browser_manager = get_session_browser_manager()
|
|
215
|
+
page = await browser_manager.get_current_page()
|
|
216
|
+
|
|
217
|
+
if not page:
|
|
218
|
+
return {"success": False, "error": "No active browser page available"}
|
|
219
|
+
|
|
220
|
+
element = page.locator(selector).first
|
|
221
|
+
await element.wait_for(state=state, timeout=timeout)
|
|
222
|
+
|
|
223
|
+
emit_success(f"Element {selector} is now {state}", message_group=group_id)
|
|
224
|
+
|
|
225
|
+
return {"success": True, "selector": selector, "state": state}
|
|
226
|
+
|
|
227
|
+
except Exception as e:
|
|
228
|
+
return {"success": False, "error": str(e), "selector": selector, "state": state}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
async def highlight_element(
|
|
232
|
+
selector: str,
|
|
233
|
+
color: str = "red",
|
|
234
|
+
timeout: int = 10000,
|
|
235
|
+
) -> Dict[str, Any]:
|
|
236
|
+
"""Highlight an element with a colored border."""
|
|
237
|
+
group_id = generate_group_id(
|
|
238
|
+
"browser_highlight_element", f"{selector[:50]}_{color}"
|
|
239
|
+
)
|
|
240
|
+
emit_info(
|
|
241
|
+
f"BROWSER HIGHLIGHT ELEMENT 🔦 selector='{selector}' color={color}",
|
|
242
|
+
message_group=group_id,
|
|
243
|
+
)
|
|
244
|
+
try:
|
|
245
|
+
browser_manager = get_session_browser_manager()
|
|
246
|
+
page = await browser_manager.get_current_page()
|
|
247
|
+
|
|
248
|
+
if not page:
|
|
249
|
+
return {"success": False, "error": "No active browser page available"}
|
|
250
|
+
|
|
251
|
+
element = page.locator(selector).first
|
|
252
|
+
await element.wait_for(state="visible", timeout=timeout)
|
|
253
|
+
|
|
254
|
+
# Add highlight style
|
|
255
|
+
highlight_script = f"""
|
|
256
|
+
el => {{
|
|
257
|
+
el.style.outline = '3px solid {color}';
|
|
258
|
+
el.style.outlineOffset = '2px';
|
|
259
|
+
el.style.backgroundColor = '{color}20'; // 20% opacity
|
|
260
|
+
el.setAttribute('data-highlighted', 'true');
|
|
261
|
+
}}
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
await element.evaluate(highlight_script)
|
|
265
|
+
|
|
266
|
+
emit_success(f"Highlighted element: {selector}", message_group=group_id)
|
|
267
|
+
|
|
268
|
+
return {"success": True, "selector": selector, "color": color}
|
|
269
|
+
|
|
270
|
+
except Exception as e:
|
|
271
|
+
return {"success": False, "error": str(e), "selector": selector}
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
async def clear_highlights() -> Dict[str, Any]:
|
|
275
|
+
"""Clear all element highlights."""
|
|
276
|
+
group_id = generate_group_id("browser_clear_highlights")
|
|
277
|
+
emit_info(
|
|
278
|
+
"BROWSER CLEAR HIGHLIGHTS 🧹",
|
|
279
|
+
message_group=group_id,
|
|
280
|
+
)
|
|
281
|
+
try:
|
|
282
|
+
browser_manager = get_session_browser_manager()
|
|
283
|
+
page = await browser_manager.get_current_page()
|
|
284
|
+
|
|
285
|
+
if not page:
|
|
286
|
+
return {"success": False, "error": "No active browser page available"}
|
|
287
|
+
|
|
288
|
+
# Remove all highlights
|
|
289
|
+
clear_script = """
|
|
290
|
+
() => {
|
|
291
|
+
const highlighted = document.querySelectorAll('[data-highlighted="true"]');
|
|
292
|
+
highlighted.forEach(el => {
|
|
293
|
+
el.style.outline = '';
|
|
294
|
+
el.style.outlineOffset = '';
|
|
295
|
+
el.style.backgroundColor = '';
|
|
296
|
+
el.removeAttribute('data-highlighted');
|
|
297
|
+
});
|
|
298
|
+
return highlighted.length;
|
|
299
|
+
}
|
|
300
|
+
"""
|
|
301
|
+
|
|
302
|
+
count = await page.evaluate(clear_script)
|
|
303
|
+
|
|
304
|
+
emit_success(f"Cleared {count} highlights", message_group=group_id)
|
|
305
|
+
|
|
306
|
+
return {"success": True, "cleared_count": count}
|
|
307
|
+
|
|
308
|
+
except Exception as e:
|
|
309
|
+
return {"success": False, "error": str(e)}
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
# Tool registration functions
|
|
313
|
+
def register_execute_javascript(agent):
|
|
314
|
+
"""Register the JavaScript execution tool."""
|
|
315
|
+
|
|
316
|
+
@agent.tool
|
|
317
|
+
async def browser_execute_js(
|
|
318
|
+
context: RunContext,
|
|
319
|
+
script: str,
|
|
320
|
+
timeout: int = 30000,
|
|
321
|
+
) -> Dict[str, Any]:
|
|
322
|
+
"""
|
|
323
|
+
Execute JavaScript code in the browser context.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
script: JavaScript code to execute
|
|
327
|
+
timeout: Timeout in milliseconds
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
Dict with execution results
|
|
331
|
+
"""
|
|
332
|
+
return await execute_javascript(script, timeout)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def register_scroll_page(agent):
|
|
336
|
+
"""Register the scroll page tool."""
|
|
337
|
+
|
|
338
|
+
@agent.tool
|
|
339
|
+
async def browser_scroll(
|
|
340
|
+
context: RunContext,
|
|
341
|
+
direction: str = "down",
|
|
342
|
+
amount: int = 3,
|
|
343
|
+
element_selector: Optional[str] = None,
|
|
344
|
+
) -> Dict[str, Any]:
|
|
345
|
+
"""
|
|
346
|
+
Scroll the page or a specific element.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
direction: Scroll direction (up, down, left, right)
|
|
350
|
+
amount: Scroll amount multiplier (1-10)
|
|
351
|
+
element_selector: Optional selector to scroll specific element
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
Dict with scroll results
|
|
355
|
+
"""
|
|
356
|
+
return await scroll_page(direction, amount, element_selector)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def register_scroll_to_element(agent):
|
|
360
|
+
"""Register the scroll to element tool."""
|
|
361
|
+
|
|
362
|
+
@agent.tool
|
|
363
|
+
async def browser_scroll_to_element(
|
|
364
|
+
context: RunContext,
|
|
365
|
+
selector: str,
|
|
366
|
+
timeout: int = 10000,
|
|
367
|
+
) -> Dict[str, Any]:
|
|
368
|
+
"""
|
|
369
|
+
Scroll to bring an element into view.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
selector: CSS or XPath selector for the element
|
|
373
|
+
timeout: Timeout in milliseconds
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
Dict with scroll results
|
|
377
|
+
"""
|
|
378
|
+
return await scroll_to_element(selector, timeout)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def register_set_viewport_size(agent):
|
|
382
|
+
"""Register the viewport size tool."""
|
|
383
|
+
|
|
384
|
+
@agent.tool
|
|
385
|
+
async def browser_set_viewport(
|
|
386
|
+
context: RunContext,
|
|
387
|
+
width: int,
|
|
388
|
+
height: int,
|
|
389
|
+
) -> Dict[str, Any]:
|
|
390
|
+
"""
|
|
391
|
+
Set the browser viewport size.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
width: Viewport width in pixels
|
|
395
|
+
height: Viewport height in pixels
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
Dict with viewport size results
|
|
399
|
+
"""
|
|
400
|
+
return await set_viewport_size(width, height)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def register_wait_for_element(agent):
|
|
404
|
+
"""Register the wait for element tool."""
|
|
405
|
+
|
|
406
|
+
@agent.tool
|
|
407
|
+
async def browser_wait_for_element(
|
|
408
|
+
context: RunContext,
|
|
409
|
+
selector: str,
|
|
410
|
+
state: str = "visible",
|
|
411
|
+
timeout: int = 30000,
|
|
412
|
+
) -> Dict[str, Any]:
|
|
413
|
+
"""
|
|
414
|
+
Wait for an element to reach a specific state.
|
|
415
|
+
|
|
416
|
+
Args:
|
|
417
|
+
selector: CSS or XPath selector for the element
|
|
418
|
+
state: State to wait for (visible, hidden, attached, detached)
|
|
419
|
+
timeout: Timeout in milliseconds
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
Dict with wait results
|
|
423
|
+
"""
|
|
424
|
+
return await wait_for_element(selector, state, timeout)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def register_browser_highlight_element(agent):
|
|
428
|
+
"""Register the element highlighting tool."""
|
|
429
|
+
|
|
430
|
+
@agent.tool
|
|
431
|
+
async def browser_highlight_element(
|
|
432
|
+
context: RunContext,
|
|
433
|
+
selector: str,
|
|
434
|
+
color: str = "red",
|
|
435
|
+
timeout: int = 10000,
|
|
436
|
+
) -> Dict[str, Any]:
|
|
437
|
+
"""
|
|
438
|
+
Highlight an element with a colored border for visual identification.
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
selector: CSS or XPath selector for the element
|
|
442
|
+
color: Highlight color (red, blue, green, yellow, etc.)
|
|
443
|
+
timeout: Timeout in milliseconds
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
Dict with highlight results
|
|
447
|
+
"""
|
|
448
|
+
return await highlight_element(selector, color, timeout)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def register_browser_clear_highlights(agent):
|
|
452
|
+
"""Register the clear highlights tool."""
|
|
453
|
+
|
|
454
|
+
@agent.tool
|
|
455
|
+
async def browser_clear_highlights(context: RunContext) -> Dict[str, Any]:
|
|
456
|
+
"""
|
|
457
|
+
Clear all element highlights from the page.
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
Dict with clear results
|
|
461
|
+
"""
|
|
462
|
+
return await clear_highlights()
|