vibesurf 0.1.0__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.

Potentially problematic release.


This version of vibesurf might be problematic. Click here for more details.

Files changed (70) hide show
  1. vibe_surf/__init__.py +12 -0
  2. vibe_surf/_version.py +34 -0
  3. vibe_surf/agents/__init__.py +0 -0
  4. vibe_surf/agents/browser_use_agent.py +1106 -0
  5. vibe_surf/agents/prompts/__init__.py +1 -0
  6. vibe_surf/agents/prompts/vibe_surf_prompt.py +176 -0
  7. vibe_surf/agents/report_writer_agent.py +360 -0
  8. vibe_surf/agents/vibe_surf_agent.py +1632 -0
  9. vibe_surf/backend/__init__.py +0 -0
  10. vibe_surf/backend/api/__init__.py +3 -0
  11. vibe_surf/backend/api/activity.py +243 -0
  12. vibe_surf/backend/api/config.py +740 -0
  13. vibe_surf/backend/api/files.py +322 -0
  14. vibe_surf/backend/api/models.py +257 -0
  15. vibe_surf/backend/api/task.py +300 -0
  16. vibe_surf/backend/database/__init__.py +13 -0
  17. vibe_surf/backend/database/manager.py +129 -0
  18. vibe_surf/backend/database/models.py +164 -0
  19. vibe_surf/backend/database/queries.py +922 -0
  20. vibe_surf/backend/database/schemas.py +100 -0
  21. vibe_surf/backend/llm_config.py +182 -0
  22. vibe_surf/backend/main.py +137 -0
  23. vibe_surf/backend/migrations/__init__.py +16 -0
  24. vibe_surf/backend/migrations/init_db.py +303 -0
  25. vibe_surf/backend/migrations/seed_data.py +236 -0
  26. vibe_surf/backend/shared_state.py +601 -0
  27. vibe_surf/backend/utils/__init__.py +7 -0
  28. vibe_surf/backend/utils/encryption.py +164 -0
  29. vibe_surf/backend/utils/llm_factory.py +225 -0
  30. vibe_surf/browser/__init__.py +8 -0
  31. vibe_surf/browser/agen_browser_profile.py +130 -0
  32. vibe_surf/browser/agent_browser_session.py +416 -0
  33. vibe_surf/browser/browser_manager.py +296 -0
  34. vibe_surf/browser/utils.py +790 -0
  35. vibe_surf/browser/watchdogs/__init__.py +0 -0
  36. vibe_surf/browser/watchdogs/action_watchdog.py +291 -0
  37. vibe_surf/browser/watchdogs/dom_watchdog.py +954 -0
  38. vibe_surf/chrome_extension/background.js +558 -0
  39. vibe_surf/chrome_extension/config.js +48 -0
  40. vibe_surf/chrome_extension/content.js +284 -0
  41. vibe_surf/chrome_extension/dev-reload.js +47 -0
  42. vibe_surf/chrome_extension/icons/convert-svg.js +33 -0
  43. vibe_surf/chrome_extension/icons/logo-preview.html +187 -0
  44. vibe_surf/chrome_extension/icons/logo.png +0 -0
  45. vibe_surf/chrome_extension/manifest.json +53 -0
  46. vibe_surf/chrome_extension/popup.html +134 -0
  47. vibe_surf/chrome_extension/scripts/api-client.js +473 -0
  48. vibe_surf/chrome_extension/scripts/main.js +491 -0
  49. vibe_surf/chrome_extension/scripts/markdown-it.min.js +3 -0
  50. vibe_surf/chrome_extension/scripts/session-manager.js +599 -0
  51. vibe_surf/chrome_extension/scripts/ui-manager.js +3687 -0
  52. vibe_surf/chrome_extension/sidepanel.html +347 -0
  53. vibe_surf/chrome_extension/styles/animations.css +471 -0
  54. vibe_surf/chrome_extension/styles/components.css +670 -0
  55. vibe_surf/chrome_extension/styles/main.css +2307 -0
  56. vibe_surf/chrome_extension/styles/settings.css +1100 -0
  57. vibe_surf/cli.py +357 -0
  58. vibe_surf/controller/__init__.py +0 -0
  59. vibe_surf/controller/file_system.py +53 -0
  60. vibe_surf/controller/mcp_client.py +68 -0
  61. vibe_surf/controller/vibesurf_controller.py +616 -0
  62. vibe_surf/controller/views.py +37 -0
  63. vibe_surf/llm/__init__.py +21 -0
  64. vibe_surf/llm/openai_compatible.py +237 -0
  65. vibesurf-0.1.0.dist-info/METADATA +97 -0
  66. vibesurf-0.1.0.dist-info/RECORD +70 -0
  67. vibesurf-0.1.0.dist-info/WHEEL +5 -0
  68. vibesurf-0.1.0.dist-info/entry_points.txt +2 -0
  69. vibesurf-0.1.0.dist-info/licenses/LICENSE +201 -0
  70. vibesurf-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,416 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import os
5
+ from pathlib import Path
6
+ from typing import List, Optional
7
+
8
+ from browser_use.browser.session import BrowserSession, CDPSession
9
+ from pydantic import Field
10
+ from browser_use.browser.events import (
11
+ NavigationCompleteEvent,
12
+ )
13
+ import time
14
+ from browser_use.browser.profile import BrowserProfile
15
+ from browser_use.browser.views import BrowserStateSummary
16
+ from browser_use.dom.views import TargetInfo
17
+ from vibe_surf.browser.agen_browser_profile import AgentBrowserProfile
18
+ from typing import Self
19
+
20
+ DEFAULT_BROWSER_PROFILE = AgentBrowserProfile()
21
+
22
+ class AgentBrowserSession(BrowserSession):
23
+ """Isolated browser session for a specific agent."""
24
+ browser_profile: AgentBrowserProfile = Field(
25
+ default_factory=lambda: DEFAULT_BROWSER_PROFILE,
26
+ description='BrowserProfile() options to use for the session, otherwise a default profile will be used',
27
+ )
28
+ main_browser_session: BrowserSession | None = Field(default=None)
29
+ connected_agent: bool = False
30
+ # Add a flag to control DVD animation (for future extensibility)
31
+ disable_dvd_animation: bool = Field(
32
+ default=True,
33
+ description="Disable the DVD screensaver animation on about:blank pages"
34
+ )
35
+ # Custom extensions to load
36
+ custom_extension_paths: List[str] = Field(
37
+ default_factory=list,
38
+ description="List of paths to custom Chrome extensions to load"
39
+ )
40
+
41
+ async def connect_agent(self, target_id: str) -> Self:
42
+ """Register agent to browser with optional target assignment."""
43
+ # First ensure the parent BrowserSession is properly connected
44
+ if not hasattr(self, '_cdp_client_root') or self._cdp_client_root is None:
45
+ await self.connect()
46
+
47
+ assigned_target_ids = self._cdp_session_pool.keys()
48
+ if target_id not in assigned_target_ids:
49
+ self.logger.info(f"Agent {self.id}: Assigned target {target_id}")
50
+ self.agent_focus = await CDPSession.for_target(self._cdp_client_root, target_id, new_socket=True,
51
+ cdp_url=self.cdp_url)
52
+ await self.agent_focus.cdp_client.send.Target.activateTarget(
53
+ params={'targetId': target_id})
54
+ await self.agent_focus.cdp_client.send.Runtime.runIfWaitingForDebugger(
55
+ session_id=self.agent_focus.session_id)
56
+ self._cdp_session_pool[target_id] = self.agent_focus
57
+ self.connected_agent = True
58
+ return self
59
+
60
+ async def disconnect_agent(self) -> None:
61
+ """Disconnect all agent-specific CDP sessions and cleanup security context."""
62
+ for session in self._cdp_session_pool.values():
63
+ await session.disconnect()
64
+ self._cdp_session_pool.clear()
65
+ self.connected_agent = False
66
+
67
+ async def _cdp_get_all_pages(
68
+ self,
69
+ include_http: bool = True,
70
+ include_about: bool = True,
71
+ include_pages: bool = True,
72
+ include_iframes: bool = False,
73
+ include_workers: bool = False,
74
+ include_chrome: bool = False,
75
+ include_chrome_extensions: bool = False,
76
+ include_chrome_error: bool = False,
77
+ ) -> list[TargetInfo]:
78
+ """Get all browser pages/tabs using CDP Target.getTargets."""
79
+ # Safety check - return empty list if browser not connected yet
80
+ if not self._cdp_client_root:
81
+ return []
82
+ targets = await self.cdp_client.send.Target.getTargets()
83
+ if self.connected_agent:
84
+ assigned_target_ids = self._cdp_session_pool.keys()
85
+ return [
86
+ t
87
+ for t in targets.get('targetInfos', [])
88
+ if self._is_valid_target(
89
+ t,
90
+ include_http=include_http,
91
+ include_about=include_about,
92
+ include_pages=include_pages,
93
+ include_iframes=include_iframes,
94
+ include_workers=include_workers,
95
+ include_chrome=include_chrome,
96
+ include_chrome_extensions=include_chrome_extensions,
97
+ include_chrome_error=include_chrome_error,
98
+ ) and t.get('targetId') in assigned_target_ids
99
+ ]
100
+ else:
101
+ # Filter for valid page/tab targets only
102
+ return [
103
+ t
104
+ for t in targets.get('targetInfos', [])
105
+ if self._is_valid_target(
106
+ t,
107
+ include_http=include_http,
108
+ include_about=include_about,
109
+ include_pages=include_pages,
110
+ include_iframes=include_iframes,
111
+ include_workers=include_workers,
112
+ include_chrome=include_chrome,
113
+ include_chrome_extensions=include_chrome_extensions,
114
+ include_chrome_error=include_chrome_error,
115
+ )
116
+ ]
117
+
118
+ async def attach_all_watchdogs(self) -> None:
119
+ """Initialize and attach all watchdogs EXCEPT AboutBlankWatchdog to disable DVD animation."""
120
+ # Prevent duplicate watchdog attachment
121
+ if hasattr(self, '_watchdogs_attached') and self._watchdogs_attached:
122
+ self.logger.debug('Watchdogs already attached, skipping duplicate attachment')
123
+ return
124
+
125
+ # Import all watchdogs except AboutBlankWatchdog
126
+ from vibe_surf.browser.watchdogs.action_watchdog import CustomActionWatchdog
127
+ from vibe_surf.browser.watchdogs.dom_watchdog import CustomDOMWatchdog
128
+
129
+ from browser_use.browser.downloads_watchdog import DownloadsWatchdog
130
+ from browser_use.browser.local_browser_watchdog import LocalBrowserWatchdog
131
+ from browser_use.browser.permissions_watchdog import PermissionsWatchdog
132
+ from browser_use.browser.popups_watchdog import PopupsWatchdog
133
+ from browser_use.browser.screenshot_watchdog import ScreenshotWatchdog
134
+ from browser_use.browser.security_watchdog import SecurityWatchdog
135
+
136
+ # NOTE: AboutBlankWatchdog is deliberately excluded to disable DVD animation
137
+
138
+ self.logger.info('🚫 VibeSurfBrowserSession: AboutBlankWatchdog disabled - no DVD animation will be shown')
139
+
140
+ # Initialize DownloadsWatchdog
141
+ DownloadsWatchdog.model_rebuild()
142
+ self._downloads_watchdog = DownloadsWatchdog(event_bus=self.event_bus, browser_session=self)
143
+ self._downloads_watchdog.attach_to_session()
144
+ if self.browser_profile.auto_download_pdfs:
145
+ self.logger.info('📄 PDF auto-download enabled for this session')
146
+
147
+ # Initialize LocalBrowserWatchdog
148
+ LocalBrowserWatchdog.model_rebuild()
149
+ self._local_browser_watchdog = LocalBrowserWatchdog(event_bus=self.event_bus, browser_session=self)
150
+ self._local_browser_watchdog.attach_to_session()
151
+
152
+ # Initialize SecurityWatchdog (hooks NavigationWatchdog and implements allowed_domains restriction)
153
+ SecurityWatchdog.model_rebuild()
154
+ self._security_watchdog = SecurityWatchdog(event_bus=self.event_bus, browser_session=self)
155
+ self._security_watchdog.attach_to_session()
156
+
157
+ # Initialize PopupsWatchdog (handles accepting and dismissing JS dialogs, alerts, confirm, onbeforeunload, etc.)
158
+ PopupsWatchdog.model_rebuild()
159
+ self._popups_watchdog = PopupsWatchdog(event_bus=self.event_bus, browser_session=self)
160
+ self._popups_watchdog.attach_to_session()
161
+
162
+ # Initialize PermissionsWatchdog (handles granting and revoking browser permissions like clipboard, microphone, camera, etc.)
163
+ PermissionsWatchdog.model_rebuild()
164
+ self._permissions_watchdog = PermissionsWatchdog(event_bus=self.event_bus, browser_session=self)
165
+ self._permissions_watchdog.attach_to_session()
166
+
167
+ # Initialize DefaultActionWatchdog (handles all default actions like click, type, scroll, go back, go forward, refresh, wait, send keys, upload file, scroll to text, etc.)
168
+ CustomActionWatchdog.model_rebuild()
169
+ self._default_action_watchdog = CustomActionWatchdog(event_bus=self.event_bus, browser_session=self)
170
+ self._default_action_watchdog.attach_to_session()
171
+
172
+ # Initialize ScreenshotWatchdog (handles taking screenshots of the browser)
173
+ ScreenshotWatchdog.model_rebuild()
174
+ self._screenshot_watchdog = ScreenshotWatchdog(event_bus=self.event_bus, browser_session=self)
175
+ self._screenshot_watchdog.attach_to_session()
176
+
177
+ # Initialize DOMWatchdog (handles building the DOM tree and detecting interactive elements, depends on ScreenshotWatchdog)
178
+ CustomDOMWatchdog.model_rebuild()
179
+ self._dom_watchdog = CustomDOMWatchdog(event_bus=self.event_bus, browser_session=self)
180
+ self._dom_watchdog.attach_to_session()
181
+
182
+ # Mark watchdogs as attached to prevent duplicate attachment
183
+ self._watchdogs_attached = True
184
+
185
+ self.logger.info('✅ VibeSurfBrowserSession: All watchdogs attached (AboutBlankWatchdog excluded)')
186
+
187
+ async def _ensure_minimal_about_blank_tab(self) -> None:
188
+ """
189
+ Ensure there's at least one about:blank tab without any animation.
190
+ This replaces AboutBlankWatchdog's functionality but without the DVD animation.
191
+ """
192
+ try:
193
+ # Get all page targets using CDP
194
+ page_targets = await self._cdp_get_all_pages()
195
+
196
+ # If no tabs exist at all, create one to keep browser alive
197
+ if len(page_targets) == 0:
198
+ self.logger.info('[VibeSurfBrowserSession] No tabs exist, creating new about:blank tab (no animation)')
199
+ from browser_use.browser.events import NavigateToUrlEvent
200
+ navigate_event = self.event_bus.dispatch(NavigateToUrlEvent(url='about:blank', new_tab=True))
201
+ await navigate_event
202
+ # Note: NO DVD screensaver injection here!
203
+
204
+ except Exception as e:
205
+ self.logger.error(f'[VibeSurfBrowserSession] Error ensuring about:blank tab: {e}')
206
+
207
+ async def on_BrowserStartEvent(self, event) -> dict[str, str]:
208
+ """Override to ensure minimal about:blank handling without animation."""
209
+ # Call parent implementation first
210
+ result = await super().on_BrowserStartEvent(event)
211
+
212
+ # Ensure we have at least one tab without animation
213
+ await self._ensure_minimal_about_blank_tab()
214
+
215
+ return result
216
+
217
+ def get_cdp_session_pool(self):
218
+ return self._cdp_session_pool
219
+
220
+ async def active_focus_page(self):
221
+ if self.agent_focus is None:
222
+ self.logger.info('No active focus page found, cannot active!')
223
+ return
224
+ await self.get_or_create_cdp_session(self.agent_focus.target_id, focus=True)
225
+
226
+ async def navigate_to_url(self, url: str, new_tab: bool = False) -> None:
227
+ """
228
+ Concurrent navigation method that bypasses serial bottlenecks in on_NavigateToUrlEvent.
229
+
230
+ This method performs minimal event dispatching and direct CDP calls for maximum concurrency.
231
+ """
232
+ if not self.agent_focus:
233
+ self.logger.warning('Cannot navigate - browser not connected')
234
+ return
235
+
236
+ target_id = None
237
+
238
+ try:
239
+ # Minimal target handling - avoid expensive _cdp_get_all_pages() call
240
+ if new_tab:
241
+ # Create new tab directly via CDP - no event system overhead
242
+ result = await self._cdp_client_root.send.Target.createTarget(
243
+ params={'url': 'about:blank', 'newWindow': False, 'background': False}
244
+ )
245
+ target_id = result['targetId']
246
+
247
+ # Create CDP session with dedicated WebSocket for this target
248
+ session = await self.get_or_create_cdp_session(target_id, focus=True)
249
+ self.agent_focus = session
250
+
251
+ # Activate target without events
252
+ await session.cdp_client.send.Target.activateTarget(params={'targetId': target_id})
253
+ await session.cdp_client.send.Runtime.runIfWaitingForDebugger(session_id=session.session_id)
254
+ else:
255
+ # Use current tab - no tab switching events
256
+ target_id = self.agent_focus.target_id
257
+
258
+ # Direct CDP navigation - bypasses all event system overhead
259
+ await self.agent_focus.cdp_client.send.Page.navigate(
260
+ params={
261
+ 'url': url,
262
+ 'transitionType': 'address_bar',
263
+ },
264
+ session_id=self.agent_focus.session_id,
265
+ )
266
+
267
+ # Minimal delay for navigation to start
268
+ await asyncio.sleep(0.2)
269
+
270
+ # Optional: Dispatch only essential completion event (non-blocking)
271
+ self.event_bus.dispatch(
272
+ NavigationCompleteEvent(
273
+ target_id=target_id,
274
+ url=url,
275
+ status=None,
276
+ )
277
+ )
278
+
279
+ except Exception as e:
280
+ self.logger.error(f'Concurrent navigation failed: {type(e).__name__}: {e}')
281
+ if target_id:
282
+ # Non-blocking error event
283
+ self.event_bus.dispatch(
284
+ NavigationCompleteEvent(
285
+ target_id=target_id,
286
+ url=url,
287
+ error_message=f'{type(e).__name__}: {e}',
288
+ )
289
+ )
290
+ raise
291
+
292
+ async def _wait_for_stable_network(self):
293
+ """Wait for page stability - simplified for CDP-only branch."""
294
+ start_time = time.time()
295
+ page_url = await self.get_current_page_url()
296
+ not_a_meaningful_website = page_url.lower().split(':', 1)[0] not in ('http', 'https')
297
+
298
+ # Wait for page stability using browser profile settings (main branch pattern)
299
+ if not not_a_meaningful_website:
300
+ self.logger.debug('🔍 DOMWatchdog.on_BrowserStateRequestEvent: ⏳ Waiting for page stability...')
301
+ try:
302
+ # Apply minimum wait time first (let page settle)
303
+ min_wait = self.browser_profile.minimum_wait_page_load_time
304
+ if min_wait > 0:
305
+ self.logger.debug(f'⏳ Minimum wait: {min_wait}s')
306
+ await asyncio.sleep(min_wait)
307
+
308
+ # Apply network idle wait time (for dynamic content like iframes)
309
+ network_idle_wait = self.browser_profile.wait_for_network_idle_page_load_time
310
+ if network_idle_wait > 0:
311
+ self.logger.debug(f'⏳ Network idle wait: {network_idle_wait}s')
312
+ await asyncio.sleep(network_idle_wait)
313
+
314
+ elapsed = time.time() - start_time
315
+ self.logger.debug(f'✅ Page stability wait completed in {elapsed:.2f}s')
316
+ except Exception as e:
317
+ self.logger.warning(
318
+ f'🔍 DOMWatchdog.on_BrowserStateRequestEvent: Network waiting failed: {e}, continuing anyway...'
319
+ )
320
+
321
+ async def take_screenshot(self, target_id: Optional[str] = None, format: str = 'png') -> str:
322
+ """
323
+ Concurrent screenshot method that bypasses serial bottlenecks in ScreenshotWatchdog.
324
+
325
+ This method performs direct CDP calls for maximum concurrency.
326
+ """
327
+ if target_id is None:
328
+ if not self.agent_focus:
329
+ self.logger.warning('No page focus to get html, please specify a target id.')
330
+ return ''
331
+ target_id = self.agent_focus.target_id
332
+ cdp_session = await self.get_or_create_cdp_session(target_id, focus=False)
333
+ await self._wait_for_stable_network()
334
+
335
+ try:
336
+ ready_state = await cdp_session.cdp_client.send.Runtime.evaluate(
337
+ params={'expression': 'document.readyState'}, session_id=cdp_session.session_id
338
+ )
339
+ except Exception:
340
+ pass
341
+
342
+ try:
343
+ from cdp_use.cdp.page import CaptureScreenshotParameters
344
+ # Direct CDP screenshot - bypasses all event system overhead
345
+ params = CaptureScreenshotParameters(format=format, captureBeyondViewport=False, quality=90)
346
+ result = await cdp_session.cdp_client.send.Page.captureScreenshot(
347
+ params=params,
348
+ session_id=cdp_session.session_id,
349
+ )
350
+ return result['data']
351
+
352
+ except Exception as e:
353
+ self.logger.error(f'Concurrent screenshot failed: {type(e).__name__}: {e}')
354
+ raise
355
+
356
+ async def get_html_content(self, target_id: Optional[str] = None) -> str:
357
+ """
358
+ Get html content of current page
359
+ :return:
360
+ """
361
+ if target_id is None:
362
+ if not self.agent_focus:
363
+ self.logger.warning('No page focus to get html, please specify a target id.')
364
+ return ''
365
+ target_id = self.agent_focus.target_id
366
+ cdp_session = await self.get_or_create_cdp_session(target_id, focus=True)
367
+ await self._wait_for_stable_network()
368
+
369
+ try:
370
+ ready_state = await cdp_session.cdp_client.send.Runtime.evaluate(
371
+ params={'expression': 'document.readyState'}, session_id=cdp_session.session_id
372
+ )
373
+ except Exception:
374
+ await self._wait_for_stable_network()
375
+
376
+ try:
377
+ # Get the HTML content
378
+ body_id = await cdp_session.cdp_client.send.DOM.getDocument(session_id=cdp_session.session_id)
379
+ page_html_result = await cdp_session.cdp_client.send.DOM.getOuterHTML(
380
+ params={'backendNodeId': body_id['root']['backendNodeId']}, session_id=cdp_session.session_id
381
+ )
382
+ except Exception as e:
383
+ raise RuntimeError(f"Couldn't extract page content: {e}")
384
+
385
+ page_html = page_html_result['outerHTML']
386
+ return page_html
387
+
388
+ async def get_browser_state_summary(
389
+ self,
390
+ cache_clickable_elements_hashes: bool = True,
391
+ include_screenshot: bool = True,
392
+ cached: bool = False,
393
+ include_recent_events: bool = False,
394
+ ) -> BrowserStateSummary:
395
+ if cached and self._cached_browser_state_summary is not None and self._cached_browser_state_summary.dom_state:
396
+ # Don't use cached state if it has 0 interactive elements
397
+ selector_map = self._cached_browser_state_summary.dom_state.selector_map
398
+
399
+ # Don't use cached state if we need a screenshot but the cached state doesn't have one
400
+ if include_screenshot and not self._cached_browser_state_summary.screenshot:
401
+ self.logger.debug('⚠️ Cached browser state has no screenshot, fetching fresh state with screenshot')
402
+ # Fall through to fetch fresh state with screenshot
403
+ elif selector_map and len(selector_map) > 0:
404
+ self.logger.debug('🔄 Using pre-cached browser state summary for open tab')
405
+ return self._cached_browser_state_summary
406
+ else:
407
+ self.logger.debug('⚠️ Cached browser state has 0 interactive elements, fetching fresh state')
408
+ # Fall through to fetch fresh state
409
+
410
+ browser_state = await self._dom_watchdog.get_browser_state_no_event_bus(
411
+ include_dom=True,
412
+ include_screenshot=include_screenshot,
413
+ cache_clickable_elements_hashes=cache_clickable_elements_hashes,
414
+ include_recent_events=include_recent_events
415
+ )
416
+ return browser_state