connectonion 0.6.0__tar.gz → 0.6.1__tar.gz

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 (143) hide show
  1. {connectonion-0.6.0 → connectonion-0.6.1}/PKG-INFO +1 -1
  2. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/__init__.py +3 -2
  3. connectonion-0.6.1/connectonion/cli/browser_agent/browser.py +586 -0
  4. connectonion-0.6.1/connectonion/cli/browser_agent/scroll_strategies.py +276 -0
  5. connectonion-0.6.1/connectonion/cli/commands/eval_commands.py +286 -0
  6. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/cli/main.py +11 -0
  7. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/console.py +5 -5
  8. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/core/agent.py +13 -10
  9. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/core/llm.py +9 -19
  10. connectonion-0.6.1/connectonion/logger.py +470 -0
  11. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/network/__init__.py +3 -0
  12. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/network/asgi.py +122 -2
  13. connectonion-0.6.1/connectonion/network/connection.py +123 -0
  14. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/network/host.py +7 -5
  15. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/useful_plugins/__init__.py +4 -3
  16. connectonion-0.6.1/connectonion/useful_plugins/ui_stream.py +164 -0
  17. {connectonion-0.6.0 → connectonion-0.6.1}/docs/network/README.md +1 -0
  18. {connectonion-0.6.0 → connectonion-0.6.1}/pyproject.toml +1 -1
  19. connectonion-0.6.0/connectonion/cli/browser_agent/browser.py +0 -243
  20. connectonion-0.6.0/connectonion/logger.py +0 -300
  21. {connectonion-0.6.0 → connectonion-0.6.1}/.gitignore +0 -0
  22. {connectonion-0.6.0 → connectonion-0.6.1}/README.md +0 -0
  23. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/address.py +0 -0
  24. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/cli/__init__.py +0 -0
  25. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/cli/browser_agent/__init__.py +0 -0
  26. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/cli/browser_agent/prompt.md +0 -0
  27. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/cli/commands/__init__.py +0 -0
  28. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/cli/commands/auth_commands.py +0 -0
  29. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/cli/commands/browser_commands.py +0 -0
  30. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/cli/commands/copy_commands.py +0 -0
  31. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/cli/commands/create.py +0 -0
  32. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/cli/commands/deploy_commands.py +0 -0
  33. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/cli/commands/doctor_commands.py +0 -0
  34. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/cli/commands/init.py +0 -0
  35. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/cli/commands/project_cmd_lib.py +0 -0
  36. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/cli/commands/reset_commands.py +0 -0
  37. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/cli/commands/status_commands.py +0 -0
  38. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/cli/docs/co-vibecoding-principles-docs-contexts-all-in-one.md +0 -0
  39. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/cli/docs/connectonion.md +0 -0
  40. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/cli/docs.md +0 -0
  41. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/cli/templates/meta-agent/README.md +0 -0
  42. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/cli/templates/meta-agent/agent.py +0 -0
  43. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/cli/templates/meta-agent/prompts/answer_prompt.md +0 -0
  44. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/cli/templates/meta-agent/prompts/docs_retrieve_prompt.md +0 -0
  45. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/cli/templates/meta-agent/prompts/metagent.md +0 -0
  46. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/cli/templates/meta-agent/prompts/think_prompt.md +0 -0
  47. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/cli/templates/minimal/README.md +0 -0
  48. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/cli/templates/minimal/agent.py +0 -0
  49. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/cli/templates/playwright/README.md +0 -0
  50. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/cli/templates/playwright/agent.py +0 -0
  51. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/cli/templates/playwright/prompt.md +0 -0
  52. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/cli/templates/playwright/requirements.txt +0 -0
  53. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/cli/templates/web-research/agent.py +0 -0
  54. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/core/__init__.py +0 -0
  55. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/core/events.py +0 -0
  56. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/core/tool_executor.py +0 -0
  57. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/core/tool_factory.py +0 -0
  58. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/core/tool_registry.py +0 -0
  59. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/core/usage.py +0 -0
  60. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/debug/__init__.py +0 -0
  61. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/debug/auto_debug.py +0 -0
  62. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/debug/auto_debug_exception.py +0 -0
  63. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/debug/auto_debug_ui.py +0 -0
  64. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/debug/debug_explainer/__init__.py +0 -0
  65. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/debug/debug_explainer/explain_agent.py +0 -0
  66. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/debug/debug_explainer/explain_context.py +0 -0
  67. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/debug/debug_explainer/explainer_prompt.md +0 -0
  68. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/debug/debug_explainer/root_cause_analysis_prompt.md +0 -0
  69. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/debug/decorators.py +0 -0
  70. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/debug/execution_analyzer/__init__.py +0 -0
  71. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/debug/execution_analyzer/execution_analysis.py +0 -0
  72. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/debug/execution_analyzer/execution_analysis_prompt.md +0 -0
  73. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/debug/runtime_inspector/__init__.py +0 -0
  74. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/debug/runtime_inspector/agent.py +0 -0
  75. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/debug/runtime_inspector/prompts/debug_assistant.md +0 -0
  76. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/debug/runtime_inspector/runtime_inspector.py +0 -0
  77. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/debug/xray.py +0 -0
  78. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/llm_do.py +0 -0
  79. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/network/announce.py +0 -0
  80. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/network/connect.py +0 -0
  81. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/network/relay.py +0 -0
  82. {connectonion-0.6.0/connectonion → connectonion-0.6.1/connectonion/network}/static/docs.html +0 -0
  83. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/network/trust.py +0 -0
  84. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/network/trust_agents.py +0 -0
  85. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/network/trust_functions.py +0 -0
  86. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/prompt_files/__init__.py +0 -0
  87. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/prompt_files/analyze_contact.md +0 -0
  88. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/prompt_files/eval_expected.md +0 -0
  89. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/prompt_files/react_evaluate.md +0 -0
  90. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/prompt_files/react_plan.md +0 -0
  91. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/prompt_files/reflect.md +0 -0
  92. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/prompts.py +0 -0
  93. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/transcribe.py +0 -0
  94. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/tui/__init__.py +0 -0
  95. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/tui/chat.py +0 -0
  96. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/tui/divider.py +0 -0
  97. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/tui/dropdown.py +0 -0
  98. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/tui/footer.py +0 -0
  99. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/tui/fuzzy.py +0 -0
  100. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/tui/input.py +0 -0
  101. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/tui/keys.py +0 -0
  102. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/tui/pick.py +0 -0
  103. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/tui/providers.py +0 -0
  104. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/tui/status_bar.py +0 -0
  105. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/useful_events_handlers/__init__.py +0 -0
  106. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/useful_events_handlers/reflect.py +0 -0
  107. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/useful_plugins/calendar_plugin.py +0 -0
  108. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/useful_plugins/eval.py +0 -0
  109. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/useful_plugins/gmail_plugin.py +0 -0
  110. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/useful_plugins/image_result_formatter.py +0 -0
  111. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/useful_plugins/re_act.py +0 -0
  112. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/useful_plugins/shell_approval.py +0 -0
  113. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/useful_tools/__init__.py +0 -0
  114. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/useful_tools/diff_writer.py +0 -0
  115. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/useful_tools/get_emails.py +0 -0
  116. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/useful_tools/gmail.py +0 -0
  117. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/useful_tools/google_calendar.py +0 -0
  118. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/useful_tools/memory.py +0 -0
  119. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/useful_tools/microsoft_calendar.py +0 -0
  120. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/useful_tools/outlook.py +0 -0
  121. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/useful_tools/send_email.py +0 -0
  122. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/useful_tools/shell.py +0 -0
  123. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/useful_tools/slash_command.py +0 -0
  124. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/useful_tools/terminal.py +0 -0
  125. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/useful_tools/todo_list.py +0 -0
  126. {connectonion-0.6.0 → connectonion-0.6.1}/connectonion/useful_tools/web_fetch.py +0 -0
  127. {connectonion-0.6.0 → connectonion-0.6.1}/docs/README.md +0 -0
  128. {connectonion-0.6.0 → connectonion-0.6.1}/docs/cli/README.md +0 -0
  129. {connectonion-0.6.0 → connectonion-0.6.1}/docs/debug/README.md +0 -0
  130. {connectonion-0.6.0 → connectonion-0.6.1}/docs/integrations/README.md +0 -0
  131. {connectonion-0.6.0 → connectonion-0.6.1}/docs/templates/README.md +0 -0
  132. {connectonion-0.6.0 → connectonion-0.6.1}/docs/tui/README.md +0 -0
  133. {connectonion-0.6.0 → connectonion-0.6.1}/docs/useful_plugins/README.md +0 -0
  134. {connectonion-0.6.0 → connectonion-0.6.1}/docs/useful_tools/README.md +0 -0
  135. {connectonion-0.6.0 → connectonion-0.6.1}/examples/README.md +0 -0
  136. {connectonion-0.6.0 → connectonion-0.6.1}/examples/browser-agent/README.md +0 -0
  137. {connectonion-0.6.0 → connectonion-0.6.1}/examples/email-agent/README.md +0 -0
  138. {connectonion-0.6.0 → connectonion-0.6.1}/examples/simple-agent/README.md +0 -0
  139. {connectonion-0.6.0 → connectonion-0.6.1}/prompts/README.md +0 -0
  140. {connectonion-0.6.0 → connectonion-0.6.1}/prompts/formats/README.md +0 -0
  141. {connectonion-0.6.0 → connectonion-0.6.1}/tests/README.md +0 -0
  142. {connectonion-0.6.0 → connectonion-0.6.1}/tests/cli/README.md +0 -0
  143. {connectonion-0.6.0 → connectonion-0.6.1}/tests/cli/aws/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: connectonion
3
- Version: 0.6.0
3
+ Version: 0.6.1
4
4
  Summary: A simple Python framework for creating AI agents with behavior tracking
5
5
  Project-URL: Homepage, https://github.com/openonion/connectonion
6
6
  Project-URL: Documentation, https://docs.connectonion.com
@@ -1,6 +1,6 @@
1
1
  """ConnectOnion - A simple agent framework with behavior tracking."""
2
2
 
3
- __version__ = "0.6.0"
3
+ __version__ = "0.6.1"
4
4
 
5
5
  # Auto-load .env files for the entire framework
6
6
  from dotenv import load_dotenv
@@ -28,7 +28,7 @@ from .transcribe import transcribe
28
28
  from .prompts import load_system_prompt
29
29
  from .debug import xray, auto_debug_exception, replay, xray_replay
30
30
  from .useful_tools import send_email, get_emails, mark_read, mark_unread, Memory, Gmail, GoogleCalendar, Outlook, MicrosoftCalendar, WebFetch, Shell, DiffWriter, pick, yes_no, autocomplete, TodoList, SlashCommand
31
- from .network import connect, RemoteAgent, host, create_app
31
+ from .network import connect, RemoteAgent, host, create_app, Connection
32
32
  from .network import relay, announce
33
33
  from . import address
34
34
 
@@ -65,6 +65,7 @@ __all__ = [
65
65
  "RemoteAgent",
66
66
  "host",
67
67
  "create_app",
68
+ "Connection",
68
69
  "relay",
69
70
  "announce",
70
71
  "address",
@@ -0,0 +1,586 @@
1
+ """Browser Agent for CLI - Natural language browser automation.
2
+
3
+ This module provides a browser automation agent that understands natural language
4
+ requests for browser operations via the ConnectOnion CLI.
5
+
6
+ Features:
7
+ - Chrome profile support for persistent sessions (cookies, logins)
8
+ - AI-powered element finding using natural language
9
+ - Form handling: find, fill, submit
10
+ - Screenshot with viewport presets
11
+ - Universal scroll with AI strategy selection
12
+ - Manual login pause for 2FA/CAPTCHA
13
+ """
14
+
15
+ import os
16
+ import base64
17
+ from pathlib import Path
18
+ from datetime import datetime
19
+ from typing import Optional, List, Dict, Any
20
+ from connectonion import Agent, llm_do
21
+ from dotenv import load_dotenv
22
+ from pydantic import BaseModel, Field
23
+
24
+ # Default screenshots directory
25
+ SCREENSHOTS_DIR = Path.cwd() / ".tmp"
26
+
27
+ # Check Playwright availability
28
+ try:
29
+ from playwright.sync_api import sync_playwright, Page, Browser, Playwright
30
+ PLAYWRIGHT_AVAILABLE = True
31
+ except ImportError:
32
+ PLAYWRIGHT_AVAILABLE = False
33
+
34
+ # Path to the browser agent system prompt
35
+ PROMPT_PATH = Path(__file__).parent / "prompt.md"
36
+
37
+
38
+ class FormField(BaseModel):
39
+ """A form field on a web page."""
40
+ name: str = Field(..., description="Field name or identifier")
41
+ label: str = Field(..., description="User-facing label")
42
+ type: str = Field(..., description="Input type (text, email, select, etc.)")
43
+ value: Optional[str] = Field(None, description="Current value")
44
+ required: bool = Field(False, description="Is this field required?")
45
+ options: List[str] = Field(default_factory=list, description="Available options for select/radio")
46
+
47
+
48
+ class BrowserAutomation:
49
+ """Browser automation with natural language support.
50
+
51
+ Simple interface for complex web interactions.
52
+ Auto-initializes browser on creation for immediate use.
53
+ Supports Chrome profile for persistent sessions.
54
+ """
55
+
56
+ def __init__(self, use_chrome_profile: bool = False, headless: bool = True):
57
+ """Initialize browser automation.
58
+
59
+ Args:
60
+ use_chrome_profile: If True, uses your Chrome cookies/sessions.
61
+ Chrome must be closed before running.
62
+ headless: If True, browser runs without visible window (default True).
63
+ """
64
+ self.playwright: Optional[Playwright] = None
65
+ self.browser: Optional[Browser] = None
66
+ self.page: Optional[Page] = None
67
+ self.current_url: str = ""
68
+ self.form_data: Dict[str, Any] = {}
69
+ self.use_chrome_profile = use_chrome_profile
70
+ self._screenshots = []
71
+ self._headless = headless
72
+ # Auto-initialize browser so it's ready immediately
73
+ self._initialize_browser()
74
+
75
+ def _initialize_browser(self):
76
+ """Initialize the browser instance on startup."""
77
+ if not PLAYWRIGHT_AVAILABLE:
78
+ return
79
+ self.open_browser(headless=self._headless)
80
+
81
+ def open_browser(self, headless: bool = True) -> str:
82
+ """Open a new browser window.
83
+
84
+ Args:
85
+ headless: If True, browser runs without visible window.
86
+
87
+ Note: If use_chrome_profile=True, Chrome must be completely closed.
88
+ """
89
+ if not PLAYWRIGHT_AVAILABLE:
90
+ return "Browser tools not installed. Run: pip install playwright && playwright install chromium"
91
+
92
+ if self.browser:
93
+ return "Browser already open"
94
+
95
+ self.playwright = sync_playwright().start()
96
+
97
+ if self.use_chrome_profile:
98
+ # Use Chromium with Chrome profile copy
99
+ chromium_profile = Path.cwd() / "chromium_automation_profile"
100
+
101
+ if not chromium_profile.exists():
102
+ import shutil
103
+ home = Path.home()
104
+ if os.name == 'nt': # Windows
105
+ source_profile = home / "AppData/Local/Google/Chrome/User Data"
106
+ elif os.uname().sysname == 'Darwin': # macOS
107
+ source_profile = home / "Library/Application Support/Google/Chrome"
108
+ else: # Linux
109
+ source_profile = home / ".config/google-chrome"
110
+
111
+ if source_profile.exists():
112
+ shutil.copytree(
113
+ source_profile,
114
+ chromium_profile,
115
+ ignore=shutil.ignore_patterns('*Cache*', '*cache*', 'Service Worker', 'ShaderCache'),
116
+ dirs_exist_ok=True
117
+ )
118
+
119
+ self.browser = self.playwright.chromium.launch_persistent_context(
120
+ str(chromium_profile),
121
+ headless=headless,
122
+ args=['--disable-blink-features=AutomationControlled'],
123
+ ignore_default_args=['--enable-automation'],
124
+ timeout=120000,
125
+ )
126
+ self.page = self.browser.pages[0] if self.browser.pages else self.browser.new_page()
127
+ self.page.add_init_script("""
128
+ Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
129
+ """)
130
+ return f"Browser opened with Chrome profile: {chromium_profile}"
131
+ else:
132
+ self.browser = self.playwright.chromium.launch(headless=headless)
133
+ self.page = self.browser.new_page()
134
+ return "Browser opened successfully"
135
+
136
+ def go_to(self, url: str) -> str:
137
+ """Navigate to a URL."""
138
+ if not self.page:
139
+ self.open_browser()
140
+
141
+ if not url.startswith(('http://', 'https://')):
142
+ url = f'https://{url}' if '.' in url else f'http://{url}'
143
+
144
+ self.page.goto(url, wait_until='networkidle', timeout=30000)
145
+ self.page.wait_for_timeout(2000)
146
+ self.current_url = self.page.url
147
+ return f"Navigated to {self.current_url}"
148
+
149
+ def find_element_by_description(self, description: str) -> str:
150
+ """Find element using natural language description.
151
+
152
+ Uses AI to analyze HTML and find the best matching element.
153
+
154
+ Args:
155
+ description: e.g., "the submit button", "email input field"
156
+
157
+ Returns:
158
+ CSS selector for the element, or error message
159
+ """
160
+ if not self.page:
161
+ return "Browser not open"
162
+
163
+ html = self.page.content()
164
+
165
+ class ElementSelector(BaseModel):
166
+ selector: str = Field(..., description="CSS selector for the element")
167
+ confidence: float = Field(..., description="Confidence score 0-1")
168
+ explanation: str = Field(..., description="Why this element matches")
169
+
170
+ result = llm_do(
171
+ f"""Analyze this HTML and find the CSS selector for: "{description}"
172
+
173
+ HTML (first 15000 chars): {html[:15000]}
174
+
175
+ Return the most specific CSS selector that uniquely identifies this element.
176
+ """,
177
+ output=ElementSelector,
178
+ model="gpt-4o",
179
+ temperature=0.1
180
+ )
181
+
182
+ if self.page.locator(result.selector).count() > 0:
183
+ return result.selector
184
+ else:
185
+ return f"Found selector {result.selector} but element not on page"
186
+
187
+ def click(self, description: str) -> str:
188
+ """Click on an element using natural language description.
189
+
190
+ Args:
191
+ description: e.g., "the blue submit button", "link to contact page"
192
+ """
193
+ if not self.page:
194
+ return "Browser not open"
195
+
196
+ selector = self.find_element_by_description(description)
197
+
198
+ if selector.startswith("Could not") or selector.startswith("Found selector"):
199
+ if self.page.locator(f"text='{description}'").count() > 0:
200
+ self.page.click(f"text='{description}'")
201
+ return f"Clicked on '{description}' (by text)"
202
+ return selector
203
+
204
+ self.page.click(selector)
205
+ return f"Clicked on '{description}'"
206
+
207
+ def type_text(self, field_description: str, text: str) -> str:
208
+ """Type text into a form field.
209
+
210
+ Args:
211
+ field_description: e.g., "email field", "password input"
212
+ text: The text to type
213
+ """
214
+ if not self.page:
215
+ return "Browser not open"
216
+
217
+ selector = self.find_element_by_description(field_description)
218
+
219
+ if selector.startswith("Could not") or selector.startswith("Found selector"):
220
+ for fallback in [
221
+ f"input[placeholder*='{field_description}' i]",
222
+ f"[aria-label*='{field_description}' i]",
223
+ f"input[name*='{field_description}' i]"
224
+ ]:
225
+ if self.page.locator(fallback).count() > 0:
226
+ self.page.fill(fallback, text)
227
+ self.form_data[field_description] = text
228
+ return f"Typed into {field_description}"
229
+ return f"Could not find field '{field_description}'"
230
+
231
+ self.page.fill(selector, text)
232
+ self.form_data[field_description] = text
233
+ return f"Typed into {field_description}"
234
+
235
+ def get_text(self) -> str:
236
+ """Get all visible text from the page."""
237
+ if not self.page:
238
+ return "Browser not open"
239
+ return self.page.inner_text("body")
240
+
241
+ def get_current_url(self) -> str:
242
+ """Get the current page URL."""
243
+ if not self.page:
244
+ return "Browser not open"
245
+ return self.page.url
246
+
247
+ def get_current_page_html(self) -> str:
248
+ """Get the HTML content of the current page."""
249
+ if not self.page:
250
+ return "Browser not open"
251
+ return self.page.content()
252
+
253
+ def take_screenshot(self, url: str = None, path: str = "",
254
+ width: int = 1920, height: int = 1080,
255
+ full_page: bool = False) -> str:
256
+ """Take a screenshot of a URL or current page.
257
+
258
+ Args:
259
+ url: URL to screenshot (optional - uses current page if not provided)
260
+ path: Optional path to save (auto-generates if empty)
261
+ width: Viewport width in pixels (default 1920)
262
+ height: Viewport height in pixels (default 1080)
263
+ full_page: If True, captures entire page height
264
+
265
+ Returns:
266
+ Path to saved screenshot
267
+ """
268
+ if not PLAYWRIGHT_AVAILABLE:
269
+ return 'Browser tools not installed. Run: pip install playwright && playwright install chromium'
270
+
271
+ if not self.page:
272
+ return "Browser not open"
273
+
274
+ # Navigate if URL provided
275
+ if url:
276
+ self.go_to(url)
277
+
278
+ # Set viewport size
279
+ self.page.set_viewport_size({"width": width, "height": height})
280
+
281
+ # Generate filename if needed
282
+ if not path:
283
+ SCREENSHOTS_DIR.mkdir(parents=True, exist_ok=True)
284
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
285
+ path = str(SCREENSHOTS_DIR / f'screenshot_{timestamp}.png')
286
+ elif not path.startswith('/'):
287
+ SCREENSHOTS_DIR.mkdir(parents=True, exist_ok=True)
288
+ if not path.endswith(('.png', '.jpg', '.jpeg')):
289
+ path += '.png'
290
+ path = str(SCREENSHOTS_DIR / path)
291
+ elif not path.endswith(('.png', '.jpg', '.jpeg')):
292
+ path += '.png'
293
+
294
+ # Ensure directory exists
295
+ Path(path).parent.mkdir(parents=True, exist_ok=True)
296
+
297
+ # Take screenshot
298
+ self.page.screenshot(path=path, full_page=full_page)
299
+ self._screenshots.append(path)
300
+ return f'Screenshot saved: {path}'
301
+
302
+ def set_viewport(self, width: int, height: int) -> str:
303
+ """Set the browser viewport size."""
304
+ if not self.page:
305
+ return "Browser not open"
306
+ self.page.set_viewport_size({"width": width, "height": height})
307
+ return f"Viewport set to {width}x{height}"
308
+
309
+ def screenshot_mobile(self, url: str = None) -> str:
310
+ """Take screenshot with iPhone viewport (390x844)."""
311
+ if url:
312
+ self.go_to(url)
313
+ self.set_viewport(390, 844)
314
+ return self.take_screenshot()
315
+
316
+ def screenshot_tablet(self, url: str = None) -> str:
317
+ """Take screenshot with iPad viewport (768x1024)."""
318
+ if url:
319
+ self.go_to(url)
320
+ self.set_viewport(768, 1024)
321
+ return self.take_screenshot()
322
+
323
+ def screenshot_desktop(self, url: str = None) -> str:
324
+ """Take screenshot with desktop viewport (1920x1080)."""
325
+ if url:
326
+ self.go_to(url)
327
+ self.set_viewport(1920, 1080)
328
+ return self.take_screenshot()
329
+
330
+ def find_forms(self) -> List[FormField]:
331
+ """Find all form fields on the current page."""
332
+ if not self.page:
333
+ return []
334
+
335
+ fields_data = self.page.evaluate("""
336
+ () => {
337
+ const fields = [];
338
+ document.querySelectorAll('input, textarea, select').forEach(input => {
339
+ const label = input.labels?.[0]?.textContent ||
340
+ input.placeholder || input.name || input.id || 'Unknown';
341
+ fields.push({
342
+ name: input.name || input.id || label,
343
+ label: label.trim(),
344
+ type: input.type || input.tagName.toLowerCase(),
345
+ value: input.value || '',
346
+ required: input.required || false,
347
+ options: input.tagName === 'SELECT' ?
348
+ Array.from(input.options).map(o => o.text) : []
349
+ });
350
+ });
351
+ return fields;
352
+ }
353
+ """)
354
+ return [FormField(**field) for field in fields_data]
355
+
356
+ def fill_form(self, data: Dict[str, str]) -> str:
357
+ """Fill multiple form fields at once."""
358
+ if not self.page:
359
+ return "Browser not open"
360
+
361
+ results = []
362
+ for field_name, value in data.items():
363
+ result = self.type_text(field_name, value)
364
+ results.append(f"{field_name}: {result}")
365
+ return "\n".join(results)
366
+
367
+ def submit_form(self) -> str:
368
+ """Submit the current form."""
369
+ if not self.page:
370
+ return "Browser not open"
371
+
372
+ for selector in [
373
+ "button[type='submit']",
374
+ "input[type='submit']",
375
+ "button:has-text('Submit')",
376
+ "button:has-text('Send')",
377
+ "button:has-text('Continue')",
378
+ "button:has-text('Next')"
379
+ ]:
380
+ if self.page.locator(selector).count() > 0:
381
+ self.page.click(selector)
382
+ return "Form submitted"
383
+
384
+ return "Could not find submit button"
385
+
386
+ def select_option(self, field_description: str, option: str) -> str:
387
+ """Select an option from a dropdown."""
388
+ if not self.page:
389
+ return "Browser not open"
390
+
391
+ selector = self.find_element_by_description(field_description)
392
+ if selector.startswith("Could not"):
393
+ return selector
394
+
395
+ self.page.select_option(selector, label=option)
396
+ return f"Selected '{option}' in {field_description}"
397
+
398
+ def check_checkbox(self, description: str, checked: bool = True) -> str:
399
+ """Check or uncheck a checkbox."""
400
+ if not self.page:
401
+ return "Browser not open"
402
+
403
+ selector = self.find_element_by_description(description)
404
+ if selector.startswith("Could not"):
405
+ return selector
406
+
407
+ if checked:
408
+ self.page.check(selector)
409
+ return f"Checked {description}"
410
+ else:
411
+ self.page.uncheck(selector)
412
+ return f"Unchecked {description}"
413
+
414
+ def wait_for_element(self, description: str, timeout: int = 30) -> str:
415
+ """Wait for an element to appear."""
416
+ if not self.page:
417
+ return "Browser not open"
418
+
419
+ selector = self.find_element_by_description(description)
420
+ if selector.startswith("Could not"):
421
+ self.page.wait_for_selector(f"text='{description}'", timeout=timeout * 1000)
422
+ return f"Found text: '{description}'"
423
+
424
+ self.page.wait_for_selector(selector, timeout=timeout * 1000)
425
+ return f"Element appeared: {description}"
426
+
427
+ def wait_for_text(self, text: str, timeout: int = 30) -> str:
428
+ """Wait for specific text to appear on the page."""
429
+ if not self.page:
430
+ return "Browser not open"
431
+
432
+ self.page.wait_for_selector(f"text='{text}'", timeout=timeout * 1000)
433
+ return f"Found text: '{text}'"
434
+
435
+ def wait(self, seconds: float) -> str:
436
+ """Wait for a specified number of seconds."""
437
+ if not self.page:
438
+ return "Browser not open"
439
+ self.page.wait_for_timeout(seconds * 1000)
440
+ return f"Waited for {seconds} seconds"
441
+
442
+ def scroll(self, times: int = 5, description: str = "the main content area") -> str:
443
+ """Universal scroll with automatic strategy selection.
444
+
445
+ Tries multiple strategies until one works:
446
+ 1. AI-generated strategy (analyzes page structure)
447
+ 2. Element scrolling
448
+ 3. Page scrolling
449
+
450
+ Args:
451
+ times: Number of scroll iterations
452
+ description: What to scroll (e.g., "the email list")
453
+
454
+ Returns:
455
+ Status message with successful strategy
456
+ """
457
+ from . import scroll_strategies
458
+ return scroll_strategies.scroll_with_verification(
459
+ page=self.page,
460
+ take_screenshot=self.take_screenshot,
461
+ times=times,
462
+ description=description
463
+ )
464
+
465
+ def scroll_page(self, direction: str = "down", amount: int = 1000) -> str:
466
+ """Scroll the page in a direction.
467
+
468
+ Args:
469
+ direction: "down", "up", "top", or "bottom"
470
+ amount: Pixels to scroll (ignored for "bottom"/"top")
471
+ """
472
+ if not self.page:
473
+ return "Browser not open"
474
+
475
+ if direction == "bottom":
476
+ self.page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
477
+ return "Scrolled to bottom of page"
478
+ elif direction == "top":
479
+ self.page.evaluate("window.scrollTo(0, 0)")
480
+ return "Scrolled to top of page"
481
+ elif direction == "down":
482
+ self.page.evaluate(f"window.scrollBy(0, {amount})")
483
+ return f"Scrolled down {amount} pixels"
484
+ elif direction == "up":
485
+ self.page.evaluate(f"window.scrollBy(0, -{amount})")
486
+ return f"Scrolled up {amount} pixels"
487
+ else:
488
+ return f"Unknown direction: {direction}"
489
+
490
+ def scroll_element(self, selector: str, amount: int = 1000) -> str:
491
+ """Scroll a specific element by CSS selector."""
492
+ if not self.page:
493
+ return "Browser not open"
494
+
495
+ result = self.page.evaluate(f"""
496
+ (() => {{
497
+ const element = document.querySelector('{selector}');
498
+ if (!element) return 'Element not found: {selector}';
499
+ const beforeScroll = element.scrollTop;
500
+ element.scrollTop += {amount};
501
+ const afterScroll = element.scrollTop;
502
+ return `Scrolled from ${{beforeScroll}}px to ${{afterScroll}}px`;
503
+ }})()
504
+ """)
505
+ return result
506
+
507
+ def wait_for_manual_login(self, site_name: str = "the website") -> str:
508
+ """Pause automation for user to login manually.
509
+
510
+ Useful for sites with 2FA or CAPTCHA.
511
+
512
+ Args:
513
+ site_name: Name of the site (e.g., "Gmail")
514
+
515
+ Returns:
516
+ Confirmation when user is ready to continue
517
+ """
518
+ if not self.page:
519
+ return "Browser not open"
520
+
521
+ print(f"\n{'='*60}")
522
+ print(f" MANUAL LOGIN REQUIRED")
523
+ print(f"{'='*60}")
524
+ print(f"Please login to {site_name} in the browser window.")
525
+ print(f"Once you're logged in and ready to continue:")
526
+ print(f" Type 'yes' or 'Y' and press Enter")
527
+ print(f"{'='*60}\n")
528
+
529
+ while True:
530
+ response = input("Ready to continue? (yes/Y): ").strip().lower()
531
+ if response in ['yes', 'y']:
532
+ print("Continuing automation...\n")
533
+ return f"User confirmed login to {site_name} - continuing"
534
+ else:
535
+ print("Please type 'yes' or 'Y' when ready.")
536
+
537
+ def extract_data(self, selector: str) -> List[str]:
538
+ """Extract text from elements matching a selector."""
539
+ if not self.page:
540
+ return []
541
+
542
+ elements = self.page.locator(selector)
543
+ count = elements.count()
544
+ return [elements.nth(i).inner_text() for i in range(count)]
545
+
546
+ def close(self) -> str:
547
+ """Close the browser."""
548
+ if self.page:
549
+ self.page.close()
550
+ if self.browser:
551
+ self.browser.close()
552
+ if self.playwright:
553
+ self.playwright.stop()
554
+
555
+ self.page = None
556
+ self.browser = None
557
+ self.playwright = None
558
+ return "Browser closed"
559
+
560
+
561
+ def execute_browser_command(command: str) -> str:
562
+ """Execute a browser command using natural language.
563
+
564
+ Returns the agent's natural language response directly.
565
+ """
566
+ api_key = os.getenv('OPENONION_API_KEY')
567
+
568
+ if not api_key:
569
+ global_env = Path.home() / ".co" / "keys.env"
570
+ if global_env.exists():
571
+ load_dotenv(global_env)
572
+ api_key = os.getenv('OPENONION_API_KEY')
573
+
574
+ if not api_key:
575
+ return 'Browser agent requires authentication. Run: co auth'
576
+
577
+ browser = BrowserAutomation()
578
+ agent = Agent(
579
+ name="browser_cli",
580
+ model="co/gemini-2.5-pro",
581
+ api_key=api_key,
582
+ system_prompt=PROMPT_PATH,
583
+ tools=[browser],
584
+ max_iterations=20
585
+ )
586
+ return agent.input(command)