lollmsbot 0.0.1__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.
- lollmsbot/__init__.py +1 -0
- lollmsbot/agent.py +1682 -0
- lollmsbot/channels/__init__.py +22 -0
- lollmsbot/channels/discord.py +408 -0
- lollmsbot/channels/http_api.py +449 -0
- lollmsbot/channels/telegram.py +272 -0
- lollmsbot/cli.py +217 -0
- lollmsbot/config.py +90 -0
- lollmsbot/gateway.py +606 -0
- lollmsbot/guardian.py +692 -0
- lollmsbot/heartbeat.py +826 -0
- lollmsbot/lollms_client.py +37 -0
- lollmsbot/skills.py +1483 -0
- lollmsbot/soul.py +482 -0
- lollmsbot/storage/__init__.py +245 -0
- lollmsbot/storage/sqlite_store.py +332 -0
- lollmsbot/tools/__init__.py +151 -0
- lollmsbot/tools/calendar.py +717 -0
- lollmsbot/tools/filesystem.py +663 -0
- lollmsbot/tools/http.py +498 -0
- lollmsbot/tools/shell.py +519 -0
- lollmsbot/ui/__init__.py +11 -0
- lollmsbot/ui/__main__.py +121 -0
- lollmsbot/ui/app.py +1122 -0
- lollmsbot/ui/routes.py +39 -0
- lollmsbot/wizard.py +1493 -0
- lollmsbot-0.0.1.dist-info/METADATA +25 -0
- lollmsbot-0.0.1.dist-info/RECORD +32 -0
- lollmsbot-0.0.1.dist-info/WHEEL +5 -0
- lollmsbot-0.0.1.dist-info/entry_points.txt +2 -0
- lollmsbot-0.0.1.dist-info/licenses/LICENSE +201 -0
- lollmsbot-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Filesystem tool for LollmsBot.
|
|
3
|
+
|
|
4
|
+
This module provides the FilesystemTool class for safe file and directory
|
|
5
|
+
operations within allowed directories. All paths are validated to prevent
|
|
6
|
+
directory traversal attacks.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import json
|
|
11
|
+
import zipfile
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, List, Set, Dict
|
|
15
|
+
|
|
16
|
+
from lollmsbot.agent import Tool, ToolResult
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class PathValidationResult:
|
|
21
|
+
"""Result of path validation."""
|
|
22
|
+
is_valid: bool
|
|
23
|
+
resolved_path: Path | None
|
|
24
|
+
error_message: str | None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class FilesystemTool(Tool):
|
|
28
|
+
"""Tool for safe filesystem operations within allowed directories.
|
|
29
|
+
|
|
30
|
+
This tool provides read, write, list, and existence check operations
|
|
31
|
+
while enforcing strict path validation to prevent directory traversal
|
|
32
|
+
and unauthorized access outside allowed directories.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
name: Unique identifier for the tool.
|
|
36
|
+
description: Human-readable description of what the tool does.
|
|
37
|
+
parameters: JSON Schema describing expected parameters for each method.
|
|
38
|
+
allowed_directories: Set of allowed base directories for operations.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
name: str = "filesystem"
|
|
42
|
+
description: str = (
|
|
43
|
+
"Perform safe filesystem operations including reading files, "
|
|
44
|
+
"writing files, listing directories, and checking file existence. "
|
|
45
|
+
"All operations are restricted to allowed directories. "
|
|
46
|
+
"Can create HTML, CSS, JavaScript, and other files. "
|
|
47
|
+
"Supports creating zip archives of multiple files."
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
parameters: dict[str, Any] = {
|
|
51
|
+
"type": "object",
|
|
52
|
+
"properties": {
|
|
53
|
+
"operation": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"enum": ["read_file", "write_file", "list_dir", "exists", "create_html_app", "create_zip"],
|
|
56
|
+
"description": "The filesystem operation to perform",
|
|
57
|
+
},
|
|
58
|
+
"path": {
|
|
59
|
+
"type": "string",
|
|
60
|
+
"description": "Relative path to the file or directory",
|
|
61
|
+
},
|
|
62
|
+
"content": {
|
|
63
|
+
"type": "string",
|
|
64
|
+
"description": "Content to write (required for write_file operation)",
|
|
65
|
+
},
|
|
66
|
+
"filename": {
|
|
67
|
+
"type": "string",
|
|
68
|
+
"description": "Filename for the HTML app (required for create_html_app)",
|
|
69
|
+
},
|
|
70
|
+
"html_content": {
|
|
71
|
+
"type": "string",
|
|
72
|
+
"description": "HTML content for the app (required for create_html_app)",
|
|
73
|
+
},
|
|
74
|
+
"files": {
|
|
75
|
+
"type": "array",
|
|
76
|
+
"description": "List of file dicts with 'path' and 'content' for batch operations (create_zip)",
|
|
77
|
+
"items": {
|
|
78
|
+
"type": "object",
|
|
79
|
+
"properties": {
|
|
80
|
+
"path": {"type": "string"},
|
|
81
|
+
"content": {"type": "string"},
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"zip_name": {
|
|
86
|
+
"type": "string",
|
|
87
|
+
"description": "Name for the zip file (required for create_zip)",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
"required": ["operation"],
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
def __init__(
|
|
94
|
+
self,
|
|
95
|
+
allowed_directories: List[str] | None = None,
|
|
96
|
+
default_encoding: str = "utf-8",
|
|
97
|
+
) -> None:
|
|
98
|
+
"""Initialize the FilesystemTool.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
allowed_directories: List of allowed base directories. If None,
|
|
102
|
+
defaults to current working directory.
|
|
103
|
+
default_encoding: Default encoding for file operations.
|
|
104
|
+
"""
|
|
105
|
+
self.allowed_directories: Set[Path] = set()
|
|
106
|
+
self.default_encoding: str = default_encoding
|
|
107
|
+
|
|
108
|
+
if allowed_directories:
|
|
109
|
+
for dir_path in allowed_directories:
|
|
110
|
+
resolved = Path(dir_path).resolve()
|
|
111
|
+
self.allowed_directories.add(resolved)
|
|
112
|
+
else:
|
|
113
|
+
# Default to current working directory
|
|
114
|
+
self.allowed_directories.add(Path.cwd().resolve())
|
|
115
|
+
|
|
116
|
+
# Add a shared output directory for generated files
|
|
117
|
+
self.output_dir = Path.home() / ".lollmsbot" / "output"
|
|
118
|
+
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
119
|
+
self.allowed_directories.add(self.output_dir.resolve())
|
|
120
|
+
|
|
121
|
+
# Create subdirectory for games/apps
|
|
122
|
+
self.apps_dir = self.output_dir / "apps"
|
|
123
|
+
self.apps_dir.mkdir(parents=True, exist_ok=True)
|
|
124
|
+
self.allowed_directories.add(self.apps_dir.resolve())
|
|
125
|
+
|
|
126
|
+
def _validate_path(self, path: str) -> PathValidationResult:
|
|
127
|
+
"""Validate that a path is within allowed directories.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
path: The path to validate (can be relative or absolute).
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
PathValidationResult indicating if the path is valid and safe.
|
|
134
|
+
"""
|
|
135
|
+
try:
|
|
136
|
+
# Resolve to absolute path, handling .. and symlinks
|
|
137
|
+
target_path = Path(path).resolve()
|
|
138
|
+
|
|
139
|
+
# Check if path is within any allowed directory
|
|
140
|
+
for allowed_dir in self.allowed_directories:
|
|
141
|
+
try:
|
|
142
|
+
# Check if target_path is within allowed_dir or equals it
|
|
143
|
+
target_path.relative_to(allowed_dir)
|
|
144
|
+
return PathValidationResult(
|
|
145
|
+
is_valid=True,
|
|
146
|
+
resolved_path=target_path,
|
|
147
|
+
error_message=None,
|
|
148
|
+
)
|
|
149
|
+
except ValueError:
|
|
150
|
+
# target_path is not under allowed_dir, try next
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
# Path is outside all allowed directories
|
|
154
|
+
allowed_strs = [str(d) for d in self.allowed_directories]
|
|
155
|
+
return PathValidationResult(
|
|
156
|
+
is_valid=False,
|
|
157
|
+
resolved_path=None,
|
|
158
|
+
error_message=(
|
|
159
|
+
f"Path '{path}' is outside allowed directories: "
|
|
160
|
+
f"{', '.join(allowed_strs)}"
|
|
161
|
+
),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
except (OSError, ValueError) as exc:
|
|
165
|
+
return PathValidationResult(
|
|
166
|
+
is_valid=False,
|
|
167
|
+
resolved_path=None,
|
|
168
|
+
error_message=f"Invalid path '{path}': {str(exc)}",
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
async def read_file(self, path: str) -> ToolResult:
|
|
172
|
+
"""Read contents of a file.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
path: Path to the file to read.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
ToolResult with file content on success, error on failure.
|
|
179
|
+
"""
|
|
180
|
+
validation = self._validate_path(path)
|
|
181
|
+
if not validation.is_valid:
|
|
182
|
+
return ToolResult(
|
|
183
|
+
success=False,
|
|
184
|
+
output=None,
|
|
185
|
+
error=validation.error_message,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
resolved_path = validation.resolved_path
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
if not resolved_path.exists():
|
|
192
|
+
return ToolResult(
|
|
193
|
+
success=False,
|
|
194
|
+
output=None,
|
|
195
|
+
error=f"File not found: {path}",
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if not resolved_path.is_file():
|
|
199
|
+
return ToolResult(
|
|
200
|
+
success=False,
|
|
201
|
+
output=None,
|
|
202
|
+
error=f"Path is not a file: {path}",
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Read file asynchronously
|
|
206
|
+
loop = asyncio.get_event_loop()
|
|
207
|
+
content = await loop.run_in_executor(
|
|
208
|
+
None,
|
|
209
|
+
lambda: resolved_path.read_text(encoding=self.default_encoding),
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
return ToolResult(
|
|
213
|
+
success=True,
|
|
214
|
+
output=content,
|
|
215
|
+
error=None,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
except PermissionError as exc:
|
|
219
|
+
return ToolResult(
|
|
220
|
+
success=False,
|
|
221
|
+
output=None,
|
|
222
|
+
error=f"Permission denied reading file '{path}': {str(exc)}",
|
|
223
|
+
)
|
|
224
|
+
except UnicodeDecodeError as exc:
|
|
225
|
+
return ToolResult(
|
|
226
|
+
success=False,
|
|
227
|
+
output=None,
|
|
228
|
+
error=f"File '{path}' is not a valid text file: {str(exc)}",
|
|
229
|
+
)
|
|
230
|
+
except Exception as exc:
|
|
231
|
+
return ToolResult(
|
|
232
|
+
success=False,
|
|
233
|
+
output=None,
|
|
234
|
+
error=f"Error reading file '{path}': {str(exc)}",
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
async def write_file(self, path: str, content: str) -> ToolResult:
|
|
238
|
+
"""Write content to a file.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
path: Path to the file to write.
|
|
242
|
+
content: Content to write to the file.
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
ToolResult indicating success or failure, with file info for delivery.
|
|
246
|
+
"""
|
|
247
|
+
validation = self._validate_path(path)
|
|
248
|
+
if not validation.is_valid:
|
|
249
|
+
return ToolResult(
|
|
250
|
+
success=False,
|
|
251
|
+
output=None,
|
|
252
|
+
error=validation.error_message,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
resolved_path = validation.resolved_path
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
# Ensure parent directory exists
|
|
259
|
+
parent_dir = resolved_path.parent
|
|
260
|
+
if not parent_dir.exists():
|
|
261
|
+
loop = asyncio.get_event_loop()
|
|
262
|
+
await loop.run_in_executor(
|
|
263
|
+
None,
|
|
264
|
+
lambda: parent_dir.mkdir(parents=True, exist_ok=True),
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# Write file asynchronously
|
|
268
|
+
loop = asyncio.get_event_loop()
|
|
269
|
+
await loop.run_in_executor(
|
|
270
|
+
None,
|
|
271
|
+
lambda: resolved_path.write_text(
|
|
272
|
+
content,
|
|
273
|
+
encoding=self.default_encoding,
|
|
274
|
+
),
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# Return file info for potential delivery
|
|
278
|
+
return ToolResult(
|
|
279
|
+
success=True,
|
|
280
|
+
output=f"Successfully wrote {len(content)} characters to {path}",
|
|
281
|
+
error=None,
|
|
282
|
+
files_to_send=[{
|
|
283
|
+
"path": str(resolved_path),
|
|
284
|
+
"filename": resolved_path.name,
|
|
285
|
+
"description": f"Generated file: {resolved_path.name} ({len(content)} chars)",
|
|
286
|
+
}],
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
except PermissionError as exc:
|
|
290
|
+
return ToolResult(
|
|
291
|
+
success=False,
|
|
292
|
+
output=None,
|
|
293
|
+
error=f"Permission denied writing file '{path}': {str(exc)}",
|
|
294
|
+
)
|
|
295
|
+
except Exception as exc:
|
|
296
|
+
return ToolResult(
|
|
297
|
+
success=False,
|
|
298
|
+
output=None,
|
|
299
|
+
error=f"Error writing file '{path}': {str(exc)}",
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
async def create_html_app(self, filename: str, html_content: str) -> ToolResult:
|
|
303
|
+
"""Create a complete HTML application file.
|
|
304
|
+
|
|
305
|
+
Convenience method for creating HTML files with proper structure.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
filename: Name for the HTML file (should end in .html).
|
|
309
|
+
html_content: The HTML content to write.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
ToolResult with file info for delivery.
|
|
313
|
+
"""
|
|
314
|
+
# Ensure .html extension
|
|
315
|
+
if not filename.endswith(".html"):
|
|
316
|
+
filename += ".html"
|
|
317
|
+
|
|
318
|
+
# Use apps directory for generated apps
|
|
319
|
+
output_path = self.apps_dir / filename
|
|
320
|
+
|
|
321
|
+
# Wrap in proper HTML structure if not already present
|
|
322
|
+
if not html_content.strip().startswith("<!DOCTYPE"):
|
|
323
|
+
full_html = f"""<!DOCTYPE html>
|
|
324
|
+
<html lang="en">
|
|
325
|
+
<head>
|
|
326
|
+
<meta charset="UTF-8">
|
|
327
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
328
|
+
<title>{filename.replace('.html', '').replace('_', ' ').title()}</title>
|
|
329
|
+
<style>
|
|
330
|
+
body {{
|
|
331
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
332
|
+
max-width: 1200px;
|
|
333
|
+
margin: 0 auto;
|
|
334
|
+
padding: 20px;
|
|
335
|
+
background: #1a1a2e;
|
|
336
|
+
color: #eee;
|
|
337
|
+
}}
|
|
338
|
+
canvas {{
|
|
339
|
+
display: block;
|
|
340
|
+
margin: 20px auto;
|
|
341
|
+
background: #16213e;
|
|
342
|
+
border-radius: 8px;
|
|
343
|
+
}}
|
|
344
|
+
.game-container {{
|
|
345
|
+
text-align: center;
|
|
346
|
+
}}
|
|
347
|
+
button {{
|
|
348
|
+
background: #0f3460;
|
|
349
|
+
color: white;
|
|
350
|
+
border: none;
|
|
351
|
+
padding: 10px 20px;
|
|
352
|
+
border-radius: 4px;
|
|
353
|
+
cursor: pointer;
|
|
354
|
+
margin: 5px;
|
|
355
|
+
}}
|
|
356
|
+
button:hover {{
|
|
357
|
+
background: #e94560;
|
|
358
|
+
}}
|
|
359
|
+
</style>
|
|
360
|
+
</head>
|
|
361
|
+
<body>
|
|
362
|
+
<div class="game-container">
|
|
363
|
+
{html_content}
|
|
364
|
+
</div>
|
|
365
|
+
</body>
|
|
366
|
+
</html>"""
|
|
367
|
+
else:
|
|
368
|
+
full_html = html_content
|
|
369
|
+
|
|
370
|
+
return await self.write_file(str(output_path), full_html)
|
|
371
|
+
|
|
372
|
+
async def create_zip(self, zip_name: str, files: List[Dict[str, str]]) -> ToolResult:
|
|
373
|
+
"""Create a zip archive containing multiple files.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
zip_name: Name for the zip file (should end in .zip).
|
|
377
|
+
files: List of dicts with 'path' and 'content' keys.
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
ToolResult with zip file info for delivery.
|
|
381
|
+
"""
|
|
382
|
+
# Ensure .zip extension
|
|
383
|
+
if not zip_name.endswith(".zip"):
|
|
384
|
+
zip_name += ".zip"
|
|
385
|
+
|
|
386
|
+
zip_path = self.output_dir / zip_name
|
|
387
|
+
|
|
388
|
+
try:
|
|
389
|
+
loop = asyncio.get_event_loop()
|
|
390
|
+
|
|
391
|
+
def create_zip():
|
|
392
|
+
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
|
|
393
|
+
for file_info in files:
|
|
394
|
+
file_path = file_info.get("path", "")
|
|
395
|
+
content = file_info.get("content", "")
|
|
396
|
+
|
|
397
|
+
# Write to zip with just the filename (no full path)
|
|
398
|
+
arcname = Path(file_path).name
|
|
399
|
+
zf.writestr(arcname, content)
|
|
400
|
+
|
|
401
|
+
return zip_path
|
|
402
|
+
|
|
403
|
+
await loop.run_in_executor(None, create_zip)
|
|
404
|
+
|
|
405
|
+
# Add to allowed directories for reading
|
|
406
|
+
self.allowed_directories.add(self.output_dir.resolve())
|
|
407
|
+
|
|
408
|
+
return ToolResult(
|
|
409
|
+
success=True,
|
|
410
|
+
output=f"Created zip archive with {len(files)} files: {zip_name}",
|
|
411
|
+
error=None,
|
|
412
|
+
files_to_send=[{
|
|
413
|
+
"path": str(zip_path),
|
|
414
|
+
"filename": zip_name,
|
|
415
|
+
"description": f"Archive containing {len(files)} files",
|
|
416
|
+
}],
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
except Exception as exc:
|
|
420
|
+
return ToolResult(
|
|
421
|
+
success=False,
|
|
422
|
+
output=None,
|
|
423
|
+
error=f"Error creating zip: {str(exc)}",
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
async def list_dir(self, path: str) -> ToolResult:
|
|
427
|
+
"""List contents of a directory.
|
|
428
|
+
|
|
429
|
+
Args:
|
|
430
|
+
path: Path to the directory to list.
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
ToolResult with list of entries on success.
|
|
434
|
+
"""
|
|
435
|
+
validation = self._validate_path(path)
|
|
436
|
+
if not validation.is_valid:
|
|
437
|
+
return ToolResult(
|
|
438
|
+
success=False,
|
|
439
|
+
output=None,
|
|
440
|
+
error=validation.error_message,
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
resolved_path = validation.resolved_path
|
|
444
|
+
|
|
445
|
+
try:
|
|
446
|
+
if not resolved_path.exists():
|
|
447
|
+
return ToolResult(
|
|
448
|
+
success=False,
|
|
449
|
+
output=None,
|
|
450
|
+
error=f"Directory not found: {path}",
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
if not resolved_path.is_dir():
|
|
454
|
+
return ToolResult(
|
|
455
|
+
success=False,
|
|
456
|
+
output=None,
|
|
457
|
+
error=f"Path is not a directory: {path}",
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
# List directory asynchronously
|
|
461
|
+
loop = asyncio.get_event_loop()
|
|
462
|
+
entries = await loop.run_in_executor(None, lambda: list(resolved_path.iterdir()))
|
|
463
|
+
|
|
464
|
+
# Format entries with metadata
|
|
465
|
+
result_entries: List[dict[str, Any]] = []
|
|
466
|
+
for entry in entries:
|
|
467
|
+
try:
|
|
468
|
+
stat = entry.stat()
|
|
469
|
+
entry_info = {
|
|
470
|
+
"name": entry.name,
|
|
471
|
+
"path": str(entry.relative_to(resolved_path)),
|
|
472
|
+
"type": "directory" if entry.is_dir() else "file",
|
|
473
|
+
"size": stat.st_size if entry.is_file() else None,
|
|
474
|
+
}
|
|
475
|
+
result_entries.append(entry_info)
|
|
476
|
+
except (OSError, PermissionError):
|
|
477
|
+
# Skip entries we can't stat
|
|
478
|
+
result_entries.append({
|
|
479
|
+
"name": entry.name,
|
|
480
|
+
"path": str(entry.relative_to(resolved_path)),
|
|
481
|
+
"type": "unknown",
|
|
482
|
+
"size": None,
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
return ToolResult(
|
|
486
|
+
success=True,
|
|
487
|
+
output={
|
|
488
|
+
"path": str(resolved_path),
|
|
489
|
+
"entries": result_entries,
|
|
490
|
+
"count": len(result_entries),
|
|
491
|
+
},
|
|
492
|
+
error=None,
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
except PermissionError as exc:
|
|
496
|
+
return ToolResult(
|
|
497
|
+
success=False,
|
|
498
|
+
output=None,
|
|
499
|
+
error=f"Permission denied listing directory '{path}': {str(exc)}",
|
|
500
|
+
)
|
|
501
|
+
except Exception as exc:
|
|
502
|
+
return ToolResult(
|
|
503
|
+
success=False,
|
|
504
|
+
output=None,
|
|
505
|
+
error=f"Error listing directory '{path}': {str(exc)}",
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
async def exists(self, path: str) -> ToolResult:
|
|
509
|
+
"""Check if a file or directory exists.
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
path: Path to check.
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
ToolResult with existence status and type information.
|
|
516
|
+
"""
|
|
517
|
+
validation = self._validate_path(path)
|
|
518
|
+
if not validation.is_valid:
|
|
519
|
+
return ToolResult(
|
|
520
|
+
success=False,
|
|
521
|
+
output=None,
|
|
522
|
+
error=validation.error_message,
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
resolved_path = validation.resolved_path
|
|
526
|
+
|
|
527
|
+
try:
|
|
528
|
+
loop = asyncio.get_event_loop()
|
|
529
|
+
exists_result = await loop.run_in_executor(None, resolved_path.exists)
|
|
530
|
+
|
|
531
|
+
if not exists_result:
|
|
532
|
+
return ToolResult(
|
|
533
|
+
success=True,
|
|
534
|
+
output={
|
|
535
|
+
"exists": False,
|
|
536
|
+
"type": None,
|
|
537
|
+
"path": str(resolved_path),
|
|
538
|
+
},
|
|
539
|
+
error=None,
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
# Determine type
|
|
543
|
+
is_file = await loop.run_in_executor(None, resolved_path.is_file)
|
|
544
|
+
is_dir = await loop.run_in_executor(None, resolved_path.is_dir)
|
|
545
|
+
|
|
546
|
+
path_type = "file" if is_file else "directory" if is_dir else "other"
|
|
547
|
+
|
|
548
|
+
return ToolResult(
|
|
549
|
+
success=True,
|
|
550
|
+
output={
|
|
551
|
+
"exists": True,
|
|
552
|
+
"type": path_type,
|
|
553
|
+
"path": str(resolved_path),
|
|
554
|
+
},
|
|
555
|
+
error=None,
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
except Exception as exc:
|
|
559
|
+
return ToolResult(
|
|
560
|
+
success=False,
|
|
561
|
+
output=None,
|
|
562
|
+
error=f"Error checking existence of '{path}': {str(exc)}",
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
async def execute(self, **params: Any) -> ToolResult:
|
|
566
|
+
"""Execute a filesystem operation based on parameters.
|
|
567
|
+
|
|
568
|
+
This is the main entry point for the Tool base class. It dispatches
|
|
569
|
+
to the appropriate method based on the 'operation' parameter.
|
|
570
|
+
|
|
571
|
+
Args:
|
|
572
|
+
**params: Parameters must include:
|
|
573
|
+
- operation: One of 'read_file', 'write_file', 'list_dir', 'exists', 'create_html_app', 'create_zip'
|
|
574
|
+
- path: Path for the operation (not needed for create_html_app, create_zip)
|
|
575
|
+
- content: Required for 'write_file' operation
|
|
576
|
+
- filename: Required for 'create_html_app'
|
|
577
|
+
- html_content: Required for 'create_html_app'
|
|
578
|
+
- files: Required for 'create_zip'
|
|
579
|
+
- zip_name: Required for 'create_zip'
|
|
580
|
+
|
|
581
|
+
Returns:
|
|
582
|
+
ToolResult from the executed operation.
|
|
583
|
+
"""
|
|
584
|
+
operation = params.get("operation")
|
|
585
|
+
path = params.get("path")
|
|
586
|
+
|
|
587
|
+
if not operation:
|
|
588
|
+
return ToolResult(
|
|
589
|
+
success=False,
|
|
590
|
+
output=None,
|
|
591
|
+
error="Missing required parameter: 'operation'",
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
# Special cases that don't need path
|
|
595
|
+
if operation == "create_html_app":
|
|
596
|
+
filename = params.get("filename")
|
|
597
|
+
html_content = params.get("html_content")
|
|
598
|
+
if not filename:
|
|
599
|
+
return ToolResult(
|
|
600
|
+
success=False,
|
|
601
|
+
output=None,
|
|
602
|
+
error="Missing required parameter: 'filename' for create_html_app",
|
|
603
|
+
)
|
|
604
|
+
if html_content is None:
|
|
605
|
+
return ToolResult(
|
|
606
|
+
success=False,
|
|
607
|
+
output=None,
|
|
608
|
+
error="Missing required parameter: 'html_content' for create_html_app",
|
|
609
|
+
)
|
|
610
|
+
return await self.create_html_app(filename, html_content)
|
|
611
|
+
|
|
612
|
+
if operation == "create_zip":
|
|
613
|
+
zip_name = params.get("zip_name")
|
|
614
|
+
files = params.get("files", [])
|
|
615
|
+
if not zip_name:
|
|
616
|
+
return ToolResult(
|
|
617
|
+
success=False,
|
|
618
|
+
output=None,
|
|
619
|
+
error="Missing required parameter: 'zip_name' for create_zip",
|
|
620
|
+
)
|
|
621
|
+
if not files:
|
|
622
|
+
return ToolResult(
|
|
623
|
+
success=False,
|
|
624
|
+
output=None,
|
|
625
|
+
error="Missing required parameter: 'files' for create_zip",
|
|
626
|
+
)
|
|
627
|
+
return await self.create_zip(zip_name, files)
|
|
628
|
+
|
|
629
|
+
# All other operations need path
|
|
630
|
+
if not path:
|
|
631
|
+
return ToolResult(
|
|
632
|
+
success=False,
|
|
633
|
+
output=None,
|
|
634
|
+
error="Missing required parameter: 'path'",
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
# Dispatch to appropriate method
|
|
638
|
+
if operation == "read_file":
|
|
639
|
+
return await self.read_file(path)
|
|
640
|
+
|
|
641
|
+
elif operation == "write_file":
|
|
642
|
+
content = params.get("content")
|
|
643
|
+
if content is None:
|
|
644
|
+
return ToolResult(
|
|
645
|
+
success=False,
|
|
646
|
+
output=None,
|
|
647
|
+
error="Missing required parameter 'content' for write_file operation",
|
|
648
|
+
)
|
|
649
|
+
return await self.write_file(path, content)
|
|
650
|
+
|
|
651
|
+
elif operation == "list_dir":
|
|
652
|
+
return await self.list_dir(path)
|
|
653
|
+
|
|
654
|
+
elif operation == "exists":
|
|
655
|
+
return await self.exists(path)
|
|
656
|
+
|
|
657
|
+
else:
|
|
658
|
+
return ToolResult(
|
|
659
|
+
success=False,
|
|
660
|
+
output=None,
|
|
661
|
+
error=f"Unknown operation: '{operation}'. "
|
|
662
|
+
f"Valid operations are: read_file, write_file, list_dir, exists, create_html_app, create_zip",
|
|
663
|
+
)
|