vibesurf 0.1.10__py3-none-any.whl → 0.1.11__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 (51) hide show
  1. vibe_surf/_version.py +2 -2
  2. vibe_surf/agents/browser_use_agent.py +68 -45
  3. vibe_surf/agents/prompts/report_writer_prompt.py +73 -0
  4. vibe_surf/agents/prompts/vibe_surf_prompt.py +85 -172
  5. vibe_surf/agents/report_writer_agent.py +380 -226
  6. vibe_surf/agents/vibe_surf_agent.py +879 -825
  7. vibe_surf/agents/views.py +130 -0
  8. vibe_surf/backend/api/activity.py +3 -1
  9. vibe_surf/backend/api/browser.py +9 -5
  10. vibe_surf/backend/api/config.py +8 -5
  11. vibe_surf/backend/api/files.py +59 -50
  12. vibe_surf/backend/api/models.py +2 -2
  13. vibe_surf/backend/api/task.py +45 -12
  14. vibe_surf/backend/database/manager.py +24 -18
  15. vibe_surf/backend/database/queries.py +199 -192
  16. vibe_surf/backend/database/schemas.py +1 -1
  17. vibe_surf/backend/main.py +4 -2
  18. vibe_surf/backend/shared_state.py +28 -35
  19. vibe_surf/backend/utils/encryption.py +3 -1
  20. vibe_surf/backend/utils/llm_factory.py +41 -36
  21. vibe_surf/browser/agent_browser_session.py +0 -4
  22. vibe_surf/browser/browser_manager.py +14 -8
  23. vibe_surf/browser/utils.py +5 -3
  24. vibe_surf/browser/watchdogs/dom_watchdog.py +0 -45
  25. vibe_surf/chrome_extension/background.js +4 -0
  26. vibe_surf/chrome_extension/scripts/api-client.js +13 -0
  27. vibe_surf/chrome_extension/scripts/file-manager.js +27 -71
  28. vibe_surf/chrome_extension/scripts/session-manager.js +21 -3
  29. vibe_surf/chrome_extension/scripts/ui-manager.js +831 -48
  30. vibe_surf/chrome_extension/sidepanel.html +21 -4
  31. vibe_surf/chrome_extension/styles/activity.css +365 -5
  32. vibe_surf/chrome_extension/styles/input.css +139 -0
  33. vibe_surf/cli.py +4 -22
  34. vibe_surf/common.py +35 -0
  35. vibe_surf/llm/openai_compatible.py +148 -93
  36. vibe_surf/logger.py +99 -0
  37. vibe_surf/{controller/vibesurf_tools.py → tools/browser_use_tools.py} +233 -219
  38. vibe_surf/tools/file_system.py +415 -0
  39. vibe_surf/{controller → tools}/mcp_client.py +4 -3
  40. vibe_surf/tools/report_writer_tools.py +21 -0
  41. vibe_surf/tools/vibesurf_tools.py +657 -0
  42. vibe_surf/tools/views.py +120 -0
  43. {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/METADATA +6 -2
  44. {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/RECORD +49 -43
  45. vibe_surf/controller/file_system.py +0 -53
  46. vibe_surf/controller/views.py +0 -37
  47. /vibe_surf/{controller → tools}/__init__.py +0 -0
  48. {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/WHEEL +0 -0
  49. {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/entry_points.txt +0 -0
  50. {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/licenses/LICENSE +0 -0
  51. {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,415 @@
1
+ import asyncio
2
+ import pdb
3
+ import re
4
+ import os
5
+ from pathlib import Path
6
+ from browser_use.filesystem.file_system import FileSystem, FileSystemError, INVALID_FILENAME_ERROR_MESSAGE, \
7
+ FileSystemState
8
+ from browser_use.filesystem.file_system import BaseFile, MarkdownFile, TxtFile, JsonFile, CsvFile, PdfFile
9
+
10
+
11
+ class PythonFile(BaseFile):
12
+ """Plain text file implementation"""
13
+
14
+ @property
15
+ def extension(self) -> str:
16
+ return 'py'
17
+
18
+
19
+ class HtmlFile(BaseFile):
20
+ """Plain text file implementation"""
21
+
22
+ @property
23
+ def extension(self) -> str:
24
+ return 'html'
25
+
26
+
27
+ class JSFile(BaseFile):
28
+ """Plain text file implementation"""
29
+
30
+ @property
31
+ def extension(self) -> str:
32
+ return 'js'
33
+
34
+
35
+ class CustomFileSystem(FileSystem):
36
+ def __init__(self, base_dir: str | Path, create_default_files: bool = False):
37
+ # Handle the Path conversion before calling super().__init__
38
+ self.base_dir = Path(base_dir).absolute() if isinstance(base_dir, str) else base_dir
39
+ self.base_dir.mkdir(parents=True, exist_ok=True)
40
+
41
+ # Create and use a dedicated subfolder for all operations
42
+ self.data_dir = self.base_dir
43
+
44
+ self.data_dir.mkdir(exist_ok=True)
45
+
46
+ self._file_types: dict[str, type[BaseFile]] = {
47
+ 'md': MarkdownFile,
48
+ 'txt': TxtFile,
49
+ 'json': JsonFile,
50
+ 'csv': CsvFile,
51
+ 'pdf': PdfFile,
52
+ 'py': PythonFile,
53
+ 'html': HtmlFile,
54
+ 'js': JSFile,
55
+ }
56
+
57
+ self.files = {}
58
+ if create_default_files:
59
+ self.default_files = ['todo.md']
60
+ self._create_default_files()
61
+
62
+ self.extracted_content_count = 0
63
+
64
+ async def display_file(self, full_filename: str) -> str | None:
65
+ """Display file content using file-specific display method"""
66
+ if not self.file_exist(full_filename):
67
+ return f"{full_filename} does not exist."
68
+
69
+ file_content = await self.read_file(full_filename)
70
+
71
+ return file_content
72
+
73
+ async def read_file(self, full_filename: str, external_file: bool = False) -> str:
74
+ """Read file content using file-specific read method and return appropriate message to LLM"""
75
+ try:
76
+ full_filepath = full_filename if external_file else str(self.data_dir / full_filename)
77
+ is_file_exist = await self.file_exist(full_filepath)
78
+ if not is_file_exist:
79
+ return f"Error: File '{full_filepath}' not found."
80
+ try:
81
+ _, extension = self._parse_filename(full_filename)
82
+ except Exception:
83
+ return f'Error: Invalid filename format {full_filename}. Must be alphanumeric with a supported extension.'
84
+ if extension != 'pdf' and extension in self._file_types.keys():
85
+ with open(str(full_filepath), 'r', encoding="utf-8") as f:
86
+ content = f.read()
87
+ return f'Read from file {full_filename}.\n<content>\n{content}\n</content>'
88
+
89
+ elif extension == 'pdf':
90
+ import pypdf
91
+
92
+ reader = pypdf.PdfReader(full_filepath)
93
+ num_pages = len(reader.pages)
94
+ MAX_PDF_PAGES = 10
95
+ extra_pages = num_pages - MAX_PDF_PAGES
96
+ extracted_text = ''
97
+ for page in reader.pages[:MAX_PDF_PAGES]:
98
+ extracted_text += page.extract_text()
99
+ extra_pages_text = f'{extra_pages} more pages...' if extra_pages > 0 else ''
100
+ return f'Read from file {full_filename}.\n<content>\n{extracted_text}\n{extra_pages_text}</content>'
101
+ else:
102
+ return f'Error: Cannot read content from file {full_filename}.'
103
+ except FileNotFoundError:
104
+ return f"Error: File '{full_filepath}' not found."
105
+ except PermissionError:
106
+ return f"Error: Permission denied to read file '{full_filepath}'."
107
+ except Exception as e:
108
+ return f"Error: Could not read file '{full_filepath}': {str(e)}."
109
+
110
+ async def copy_file(self, src_filename: str, dst_filename: str, external_src_file: bool = False) -> str:
111
+ """Copy a file to the FileSystem from src (can be external) to dst filename"""
112
+ import shutil
113
+ from concurrent.futures import ThreadPoolExecutor
114
+
115
+ # Check if destination file already exists
116
+ if self.get_file(dst_filename):
117
+ return f"Error: Destination file '{dst_filename}' already exists."
118
+
119
+ try:
120
+ src_path = src_filename if external_src_file else (self.data_dir / src_filename)
121
+ dst_path = self.data_dir / dst_filename
122
+ dst_path.parent.mkdir(parents=True, exist_ok=True)
123
+ # Check if source file exists
124
+ if not src_path.exists() if hasattr(src_path, 'exists') else not Path(src_path).exists():
125
+ return f"Error: Source file '{src_filename}' not found."
126
+
127
+ # Use shutil to copy file
128
+ with ThreadPoolExecutor() as executor:
129
+ await asyncio.get_event_loop().run_in_executor(executor, shutil.copy2, str(src_path), str(dst_path))
130
+
131
+ # Read the copied file content and create file object for internal tracking
132
+ # content = self.read_file(dst_filename)
133
+ # dst_name, dst_extension = self._parse_filename(dst_filename)
134
+ # file_class = self._get_file_type_class(dst_extension)
135
+ #
136
+ # if file_class:
137
+ # dst_file = file_class(name=dst_name, content=content)
138
+ # self.files[dst_filename] = dst_file
139
+
140
+ source_type = "external file" if external_src_file else "file"
141
+ return f"{source_type.capitalize()} '{src_filename}' copied to '{dst_filename}' successfully."
142
+
143
+ except FileNotFoundError:
144
+ return f"Error: Source file '{src_filename}' not found."
145
+ except PermissionError:
146
+ return f"Error: Permission denied to access files."
147
+ except Exception as e:
148
+ return f"Error: Could not copy file '{src_filename}' to '{dst_filename}'. {str(e)}"
149
+
150
+ async def rename_file(self, old_filename: str, new_filename: str) -> str:
151
+ """Rename a file within the FileSystem from old_filename to new_filename"""
152
+ import shutil
153
+ from concurrent.futures import ThreadPoolExecutor
154
+
155
+ # Check if old file exists
156
+ file_exist = await self.file_exist(old_filename)
157
+ if not file_exist:
158
+ return f"Error: Source File '{old_filename}' not found."
159
+
160
+ try:
161
+ new_file_path = os.path.join(os.path.dirname(old_filename), new_filename)
162
+ old_path = self.data_dir / old_filename
163
+ new_path = self.data_dir / new_file_path
164
+
165
+ # Use shutil to move/rename file
166
+ with ThreadPoolExecutor() as executor:
167
+ await asyncio.get_event_loop().run_in_executor(executor, shutil.move, str(old_path), str(new_path))
168
+
169
+ # Update internal file tracking
170
+ # old_file = self.files[old_filename]
171
+ # del self.files[old_filename]
172
+ #
173
+ # # Update file object name if needed
174
+ # new_name, new_extension = self._parse_filename(new_file_path)
175
+ # old_file.name = new_name
176
+ # self.files[new_file_path] = old_file
177
+
178
+ return f"File '{old_filename}' renamed to '{new_file_path}' successfully."
179
+
180
+ except Exception as e:
181
+ return f"Error: Could not rename file '{old_filename}' to '{new_file_path}'. {str(e)}"
182
+
183
+ async def move_file(self, old_filename: str, new_filename: str) -> str:
184
+ """Move a file within the FileSystem from old_filename to new_filename"""
185
+ import shutil
186
+ from concurrent.futures import ThreadPoolExecutor
187
+
188
+ # Check if old file exists
189
+ src_file_exist = await self.file_exist(old_filename)
190
+ if not src_file_exist:
191
+ return f"Error: Source File '{old_filename}' not found."
192
+
193
+ # Check if new filename already exists
194
+ dst_file_exist = await self.file_exist(new_filename)
195
+ if dst_file_exist:
196
+ return f"Error: Destination File '{new_filename}' already exists."
197
+
198
+ try:
199
+ old_path = self.data_dir / old_filename
200
+ new_path = self.data_dir / new_filename
201
+ new_path.parent.mkdir(parents=True, exist_ok=True)
202
+ # Use shutil to move file
203
+ with ThreadPoolExecutor() as executor:
204
+ await asyncio.get_event_loop().run_in_executor(executor, shutil.move, str(old_path), str(new_path))
205
+
206
+ # Update internal file tracking
207
+ # old_file = self.files[old_filename]
208
+ # del self.files[old_filename]
209
+ #
210
+ # # Update file object name if needed
211
+ # new_name, new_extension = self._parse_filename(new_filename)
212
+ # old_file.name = new_name
213
+ # self.files[new_filename] = old_file
214
+
215
+ return f"File '{old_filename}' moved to '{new_filename}' successfully."
216
+
217
+ except Exception as e:
218
+ return f"Error: Could not move file '{old_filename}' to '{new_filename}'. {str(e)}"
219
+
220
+ def get_absolute_path(self, full_filename: str) -> str:
221
+ full_path = self.data_dir.absolute() / full_filename
222
+ return str(full_path)
223
+
224
+ def _is_valid_filename(self, file_name: str) -> bool:
225
+ """Check if filename matches the required pattern: name.extension"""
226
+ # Build extensions pattern from _file_types
227
+ file_name = os.path.basename(file_name)
228
+ extensions = '|'.join(self._file_types.keys())
229
+ pattern = rf'^[a-zA-Z0-9_\-]+\.({extensions})$'
230
+ return bool(re.match(pattern, file_name))
231
+
232
+ async def write_file(self, full_filename: str, content: str) -> str:
233
+ """Write content to file using file-specific write method"""
234
+ if not self._is_valid_filename(full_filename):
235
+ return INVALID_FILENAME_ERROR_MESSAGE
236
+
237
+ try:
238
+ full_path = self.data_dir / full_filename
239
+ full_path.parent.mkdir(parents=True, exist_ok=True)
240
+ name_without_ext, extension = self._parse_filename(full_filename)
241
+ file_class = self._get_file_type_class(extension)
242
+ if not file_class:
243
+ raise ValueError(f"Error: Invalid file extension '{extension}' for file '{full_filename}'.")
244
+
245
+ # Create or get existing file using full filename as key
246
+ if full_filename in self.files:
247
+ file_obj = self.files[full_filename]
248
+ else:
249
+ file_obj = file_class(name=name_without_ext)
250
+ self.files[full_filename] = file_obj # Use full filename as key
251
+
252
+ # Use file-specific write method
253
+ await file_obj.write(content, self.data_dir)
254
+ return f'Data written to file {full_filename} successfully.'
255
+ except FileSystemError as e:
256
+ return str(e)
257
+ except Exception as e:
258
+ return f"Error: Could not write to file '{full_filename}'. {str(e)}"
259
+
260
+ async def file_exist(self, full_filename: str) -> bool:
261
+ full_file_path = self.data_dir / full_filename
262
+ return bool(full_file_path.exists())
263
+
264
+ async def create_file(self, full_filename: str) -> str:
265
+ """Create a file with empty content"""
266
+ if not self._is_valid_filename(full_filename):
267
+ return INVALID_FILENAME_ERROR_MESSAGE
268
+
269
+ try:
270
+ full_path = self.data_dir / full_filename
271
+ full_path.parent.mkdir(parents=True, exist_ok=True)
272
+ name_without_ext, extension = self._parse_filename(full_filename)
273
+ file_class = self._get_file_type_class(extension)
274
+ if not file_class:
275
+ raise ValueError(f"Error: Invalid file extension '{extension}' for file '{full_filename}'.")
276
+
277
+ # Create or get existing file using full filename as key
278
+ if full_filename in self.files:
279
+ file_obj = self.files[full_filename]
280
+ else:
281
+ file_obj = file_class(name=name_without_ext)
282
+ self.files[full_filename] = file_obj # Use full filename as key
283
+
284
+ # Use file-specific write method
285
+ await file_obj.write('', self.data_dir)
286
+ return f'Create file {full_filename} successfully.'
287
+ except FileSystemError as e:
288
+ return str(e)
289
+ except Exception as e:
290
+ return f"Error: Could not write to file '{full_filename}'. {str(e)}"
291
+
292
+ async def list_directory(self, directory_path: str = "") -> str:
293
+ """List contents of a directory within the file system (data_dir only)"""
294
+ try:
295
+ # Construct the full path within data_dir
296
+ if directory_path and directory_path.strip() != ".":
297
+ # Remove leading slash if present and ensure relative path
298
+ directory_path = directory_path.lstrip('/')
299
+ full_path = self.data_dir / directory_path
300
+ else:
301
+ full_path = self.data_dir
302
+
303
+ # Ensure the path is within data_dir for security
304
+ try:
305
+ full_path = full_path.resolve()
306
+ self.data_dir.resolve()
307
+ if not str(full_path).startswith(str(self.data_dir.resolve())):
308
+ return f"Error: Access denied. Path '{directory_path}' is outside the file system."
309
+ except Exception:
310
+ return f"Error: Invalid directory path '{directory_path}'."
311
+
312
+ # Check if directory exists
313
+ if not full_path.exists():
314
+ return f"Error: Directory '{directory_path or '.'}' does not exist."
315
+
316
+ if not full_path.is_dir():
317
+ return f"Error: '{directory_path or '.'}' is not a directory."
318
+
319
+ # List directory contents
320
+ items = []
321
+ for item in sorted(full_path.iterdir()):
322
+ relative_path = item.relative_to(full_path)
323
+ if item.is_dir():
324
+ items.append(f"📁 {relative_path}/")
325
+ else:
326
+ file_size = item.stat().st_size
327
+ if file_size < 1024:
328
+ size_str = f"{file_size}B"
329
+ elif file_size < 1024 * 1024:
330
+ size_str = f"{file_size // 1024}KB"
331
+ else:
332
+ size_str = f"{file_size // (1024 * 1024)}MB"
333
+ items.append(f"📄 {relative_path} ({size_str})")
334
+
335
+ if not items:
336
+ return f"Directory '{directory_path or '.'}' is empty."
337
+
338
+ directory_display = directory_path or "."
339
+ return f"Contents of directory '{directory_display}':\n" + "\n".join(items)
340
+
341
+ except Exception as e:
342
+ return f"Error: Could not list directory '{directory_path or '.'}': {str(e)}"
343
+
344
+ async def create_directory(self, directory_path: str) -> str:
345
+ """Create a directory within the file system (data_dir only)"""
346
+ try:
347
+ if not directory_path or not directory_path.strip():
348
+ return "Error: Directory path cannot be empty."
349
+
350
+ # Remove leading slash if present and ensure relative path
351
+ directory_path = directory_path.strip().lstrip('/')
352
+ full_path = self.data_dir / directory_path
353
+
354
+ # Ensure the path is within data_dir for security
355
+ try:
356
+ full_path = full_path.resolve()
357
+ self.data_dir.resolve()
358
+ if not str(full_path).startswith(str(self.data_dir.resolve())):
359
+ return f"Error: Access denied. Cannot create directory '{directory_path}' outside the file system."
360
+ except Exception:
361
+ return f"Error: Invalid directory path '{directory_path}'."
362
+
363
+ # Check if directory already exists
364
+ if full_path.exists():
365
+ if full_path.is_dir():
366
+ return f"Directory '{directory_path}' already exists."
367
+ else:
368
+ return f"Error: '{directory_path}' already exists as a file."
369
+
370
+ # Create directory (including parent directories)
371
+ full_path.mkdir(parents=True, exist_ok=True)
372
+
373
+ return f"Directory '{directory_path}' created successfully."
374
+
375
+ except Exception as e:
376
+ return f"Error: Could not create directory '{directory_path}': {str(e)}"
377
+
378
+ @classmethod
379
+ def from_state(cls, state: FileSystemState) -> 'FileSystem':
380
+ """Restore file system from serializable state at the exact same location"""
381
+ # Create file system without default files
382
+ fs = cls(base_dir=Path(state.base_dir), create_default_files=False)
383
+ fs.extracted_content_count = state.extracted_content_count
384
+
385
+ # Restore all files
386
+ for full_filename, file_data in state.files.items():
387
+ file_type = file_data['type']
388
+ file_info = file_data['data']
389
+
390
+ # Create the appropriate file object based on type
391
+ if file_type == 'MarkdownFile':
392
+ file_obj = MarkdownFile(**file_info)
393
+ elif file_type == 'TxtFile':
394
+ file_obj = TxtFile(**file_info)
395
+ elif file_type == 'JsonFile':
396
+ file_obj = JsonFile(**file_info)
397
+ elif file_type == 'CsvFile':
398
+ file_obj = CsvFile(**file_info)
399
+ elif file_type == 'PdfFile':
400
+ file_obj = PdfFile(**file_info)
401
+ elif file_type == 'JSFile':
402
+ file_obj = JSFile(**file_info)
403
+ elif file_type == 'PythonFile':
404
+ file_obj = PythonFile(**file_info)
405
+ elif file_type == 'HtmlFile':
406
+ file_obj = HtmlFile(**file_info)
407
+ else:
408
+ # Skip unknown file types
409
+ continue
410
+
411
+ # Add to files dict and sync to disk
412
+ fs.files[full_filename] = file_obj
413
+ file_obj.sync_to_disk_sync(fs.data_dir)
414
+
415
+ return fs
@@ -3,17 +3,18 @@ import logging
3
3
  import time
4
4
  from typing import Any
5
5
 
6
-
7
6
  from browser_use.telemetry import MCPClientTelemetryEvent, ProductTelemetry
8
7
  from browser_use.utils import get_browser_use_version
9
8
  from browser_use.mcp.client import MCPClient
10
9
  from mcp import ClientSession, StdioServerParameters, types
11
10
  from mcp.client.stdio import stdio_client
12
11
 
13
- logger = logging.getLogger(__name__)
12
+ from vibe_surf.logger import get_logger
13
+
14
+ logger = get_logger(__name__)
14
15
 
15
16
 
16
- class VibeSurfMCPClient(MCPClient):
17
+ class CustomMCPClient(MCPClient):
17
18
  async def connect(self, timeout: int = 200) -> None:
18
19
  """Connect to the MCP server and discover available tools."""
19
20
  if self._connected:
@@ -0,0 +1,21 @@
1
+ from browser_use.tools.registry.service import Registry
2
+ from vibe_surf.tools.vibesurf_tools import VibeSurfTools
3
+ from vibe_surf.tools.file_system import CustomFileSystem
4
+ from browser_use.tools.views import NoParamsAction
5
+
6
+
7
+ class ReportWriterTools(VibeSurfTools):
8
+ def __init__(self, exclude_actions: list[str] = []):
9
+ self.registry = Registry(exclude_actions)
10
+ self._register_file_actions()
11
+ self._register_done_action()
12
+
13
+ def _register_done_action(self):
14
+ @self.registry.action(
15
+ description="Finish writing report.",
16
+ param_model=NoParamsAction
17
+ )
18
+ async def task_done(
19
+ _: NoParamsAction,
20
+ ):
21
+ pass