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.
- vibe_surf/_version.py +2 -2
- vibe_surf/agents/browser_use_agent.py +68 -45
- vibe_surf/agents/prompts/report_writer_prompt.py +73 -0
- vibe_surf/agents/prompts/vibe_surf_prompt.py +85 -172
- vibe_surf/agents/report_writer_agent.py +380 -226
- vibe_surf/agents/vibe_surf_agent.py +879 -825
- vibe_surf/agents/views.py +130 -0
- vibe_surf/backend/api/activity.py +3 -1
- vibe_surf/backend/api/browser.py +9 -5
- vibe_surf/backend/api/config.py +8 -5
- vibe_surf/backend/api/files.py +59 -50
- vibe_surf/backend/api/models.py +2 -2
- vibe_surf/backend/api/task.py +45 -12
- vibe_surf/backend/database/manager.py +24 -18
- vibe_surf/backend/database/queries.py +199 -192
- vibe_surf/backend/database/schemas.py +1 -1
- vibe_surf/backend/main.py +4 -2
- vibe_surf/backend/shared_state.py +28 -35
- vibe_surf/backend/utils/encryption.py +3 -1
- vibe_surf/backend/utils/llm_factory.py +41 -36
- vibe_surf/browser/agent_browser_session.py +0 -4
- vibe_surf/browser/browser_manager.py +14 -8
- vibe_surf/browser/utils.py +5 -3
- vibe_surf/browser/watchdogs/dom_watchdog.py +0 -45
- vibe_surf/chrome_extension/background.js +4 -0
- vibe_surf/chrome_extension/scripts/api-client.js +13 -0
- vibe_surf/chrome_extension/scripts/file-manager.js +27 -71
- vibe_surf/chrome_extension/scripts/session-manager.js +21 -3
- vibe_surf/chrome_extension/scripts/ui-manager.js +831 -48
- vibe_surf/chrome_extension/sidepanel.html +21 -4
- vibe_surf/chrome_extension/styles/activity.css +365 -5
- vibe_surf/chrome_extension/styles/input.css +139 -0
- vibe_surf/cli.py +4 -22
- vibe_surf/common.py +35 -0
- vibe_surf/llm/openai_compatible.py +148 -93
- vibe_surf/logger.py +99 -0
- vibe_surf/{controller/vibesurf_tools.py → tools/browser_use_tools.py} +233 -219
- vibe_surf/tools/file_system.py +415 -0
- vibe_surf/{controller → tools}/mcp_client.py +4 -3
- vibe_surf/tools/report_writer_tools.py +21 -0
- vibe_surf/tools/vibesurf_tools.py +657 -0
- vibe_surf/tools/views.py +120 -0
- {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/METADATA +6 -2
- {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/RECORD +49 -43
- vibe_surf/controller/file_system.py +0 -53
- vibe_surf/controller/views.py +0 -37
- /vibe_surf/{controller → tools}/__init__.py +0 -0
- {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/WHEEL +0 -0
- {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/entry_points.txt +0 -0
- {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
12
|
+
from vibe_surf.logger import get_logger
|
|
13
|
+
|
|
14
|
+
logger = get_logger(__name__)
|
|
14
15
|
|
|
15
16
|
|
|
16
|
-
class
|
|
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
|