alita-sdk 0.3.457__py3-none-any.whl → 0.3.465__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 alita-sdk might be problematic. Click here for more details.
- alita_sdk/cli/__init__.py +10 -0
- alita_sdk/cli/__main__.py +17 -0
- alita_sdk/cli/agent/__init__.py +0 -0
- alita_sdk/cli/agent/default.py +176 -0
- alita_sdk/cli/agent_executor.py +155 -0
- alita_sdk/cli/agent_loader.py +197 -0
- alita_sdk/cli/agent_ui.py +218 -0
- alita_sdk/cli/agents.py +1911 -0
- alita_sdk/cli/callbacks.py +576 -0
- alita_sdk/cli/cli.py +159 -0
- alita_sdk/cli/config.py +164 -0
- alita_sdk/cli/formatting.py +182 -0
- alita_sdk/cli/input_handler.py +256 -0
- alita_sdk/cli/mcp_loader.py +315 -0
- alita_sdk/cli/toolkit.py +330 -0
- alita_sdk/cli/toolkit_loader.py +55 -0
- alita_sdk/cli/tools/__init__.py +36 -0
- alita_sdk/cli/tools/approval.py +224 -0
- alita_sdk/cli/tools/filesystem.py +905 -0
- alita_sdk/cli/tools/planning.py +403 -0
- alita_sdk/cli/tools/terminal.py +280 -0
- alita_sdk/runtime/clients/client.py +16 -1
- alita_sdk/runtime/langchain/constants.py +2 -1
- alita_sdk/runtime/langchain/langraph_agent.py +17 -5
- alita_sdk/runtime/langchain/utils.py +1 -1
- alita_sdk/runtime/tools/function.py +17 -5
- alita_sdk/runtime/tools/llm.py +65 -7
- alita_sdk/tools/base_indexer_toolkit.py +54 -2
- alita_sdk/tools/qtest/api_wrapper.py +871 -32
- alita_sdk/tools/sharepoint/api_wrapper.py +22 -2
- alita_sdk/tools/sharepoint/authorization_helper.py +17 -1
- {alita_sdk-0.3.457.dist-info → alita_sdk-0.3.465.dist-info}/METADATA +145 -2
- {alita_sdk-0.3.457.dist-info → alita_sdk-0.3.465.dist-info}/RECORD +37 -15
- alita_sdk-0.3.465.dist-info/entry_points.txt +2 -0
- {alita_sdk-0.3.457.dist-info → alita_sdk-0.3.465.dist-info}/WHEEL +0 -0
- {alita_sdk-0.3.457.dist-info → alita_sdk-0.3.465.dist-info}/licenses/LICENSE +0 -0
- {alita_sdk-0.3.457.dist-info → alita_sdk-0.3.465.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,905 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Filesystem tools for CLI agents.
|
|
3
|
+
|
|
4
|
+
Provides comprehensive file system operations restricted to specific directories.
|
|
5
|
+
Inspired by MCP filesystem server implementation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional, List, Dict, Any
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from langchain_core.tools import BaseTool
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ReadFileInput(BaseModel):
|
|
17
|
+
"""Input for reading a file."""
|
|
18
|
+
path: str = Field(description="Relative path to the file to read")
|
|
19
|
+
head: Optional[int] = Field(None, description="If provided, read only the first N lines")
|
|
20
|
+
tail: Optional[int] = Field(None, description="If provided, read only the last N lines")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ReadFileChunkInput(BaseModel):
|
|
24
|
+
"""Input for reading a file in chunks."""
|
|
25
|
+
path: str = Field(description="Relative path to the file to read")
|
|
26
|
+
start_line: int = Field(default=1, description="Starting line number (1-indexed)")
|
|
27
|
+
end_line: Optional[int] = Field(None, description="Ending line number (inclusive). If None, read to end of file")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ApplyPatchInput(BaseModel):
|
|
31
|
+
"""Input for applying multiple edits to a file."""
|
|
32
|
+
path: str = Field(description="Relative path to the file to edit")
|
|
33
|
+
edits: List[Dict[str, str]] = Field(
|
|
34
|
+
description="List of edits, each with 'old_text' and 'new_text' keys. Applied sequentially."
|
|
35
|
+
)
|
|
36
|
+
dry_run: bool = Field(default=False, description="If True, preview changes without applying them")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ReadMultipleFilesInput(BaseModel):
|
|
40
|
+
"""Input for reading multiple files."""
|
|
41
|
+
paths: List[str] = Field(
|
|
42
|
+
min_length=1,
|
|
43
|
+
description="Array of file paths to read. Each path must point to a valid file within allowed directories."
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class WriteFileInput(BaseModel):
|
|
48
|
+
"""Input for writing to a file."""
|
|
49
|
+
path: str = Field(description="Relative path to the file to write")
|
|
50
|
+
content: str = Field(description="Content to write to the file")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class EditFileInput(BaseModel):
|
|
54
|
+
"""Input for editing a file with precise replacements."""
|
|
55
|
+
path: str = Field(description="Relative path to the file to edit")
|
|
56
|
+
old_text: str = Field(description="Exact text to search for and replace")
|
|
57
|
+
new_text: str = Field(description="Text to replace with")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ListDirectoryInput(BaseModel):
|
|
61
|
+
"""Input for listing directory contents."""
|
|
62
|
+
path: str = Field(default=".", description="Relative path to the directory to list")
|
|
63
|
+
include_sizes: bool = Field(default=False, description="Include file sizes in the output")
|
|
64
|
+
sort_by: str = Field(default="name", description="Sort by 'name' or 'size'")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class DirectoryTreeInput(BaseModel):
|
|
68
|
+
"""Input for getting a directory tree."""
|
|
69
|
+
path: str = Field(default=".", description="Relative path to the directory")
|
|
70
|
+
max_depth: Optional[int] = Field(None, description="Maximum depth to traverse (None for unlimited)")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class SearchFilesInput(BaseModel):
|
|
74
|
+
"""Input for searching files."""
|
|
75
|
+
path: str = Field(default=".", description="Relative path to search from")
|
|
76
|
+
pattern: str = Field(description="Glob pattern to match (e.g., '*.py', '**/*.txt')")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class DeleteFileInput(BaseModel):
|
|
80
|
+
"""Input for deleting a file."""
|
|
81
|
+
path: str = Field(description="Relative path to the file to delete")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class MoveFileInput(BaseModel):
|
|
85
|
+
"""Input for moving/renaming a file."""
|
|
86
|
+
source: str = Field(description="Relative path to the source file")
|
|
87
|
+
destination: str = Field(description="Relative path to the destination")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class CreateDirectoryInput(BaseModel):
|
|
91
|
+
"""Input for creating a directory."""
|
|
92
|
+
path: str = Field(description="Relative path to the directory to create")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class GetFileInfoInput(BaseModel):
|
|
96
|
+
"""Input for getting file information."""
|
|
97
|
+
path: str = Field(description="Relative path to the file or directory")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class EmptyInput(BaseModel):
|
|
101
|
+
"""Empty input schema for tools that take no arguments."""
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class FileSystemTool(BaseTool):
|
|
106
|
+
"""Base class for filesystem tools with directory restriction."""
|
|
107
|
+
base_directory: str
|
|
108
|
+
|
|
109
|
+
def _resolve_path(self, relative_path: str) -> Path:
|
|
110
|
+
"""
|
|
111
|
+
Resolve and validate a path within the base directory.
|
|
112
|
+
|
|
113
|
+
Security: Ensures resolved path is within allowed directory.
|
|
114
|
+
"""
|
|
115
|
+
base = Path(self.base_directory).resolve()
|
|
116
|
+
|
|
117
|
+
# Handle both relative and absolute paths
|
|
118
|
+
if Path(relative_path).is_absolute():
|
|
119
|
+
target = Path(relative_path).resolve()
|
|
120
|
+
else:
|
|
121
|
+
target = (base / relative_path).resolve()
|
|
122
|
+
|
|
123
|
+
# Security check: ensure the resolved path is within base directory
|
|
124
|
+
try:
|
|
125
|
+
target.relative_to(base)
|
|
126
|
+
except ValueError:
|
|
127
|
+
raise ValueError(f"Access denied: path '{relative_path}' is outside allowed directory")
|
|
128
|
+
|
|
129
|
+
return target
|
|
130
|
+
|
|
131
|
+
def _format_size(self, size: int) -> str:
|
|
132
|
+
"""Format file size in human-readable format."""
|
|
133
|
+
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
|
|
134
|
+
if size < 1024.0:
|
|
135
|
+
return f"{size:.1f} {unit}"
|
|
136
|
+
size /= 1024.0
|
|
137
|
+
return f"{size:.1f} PB"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class ReadFileTool(FileSystemTool):
|
|
141
|
+
"""Read file contents with optional head/tail support."""
|
|
142
|
+
name: str = "filesystem_read_file"
|
|
143
|
+
description: str = (
|
|
144
|
+
"Read the complete contents of a file from the file system. "
|
|
145
|
+
"Handles various text encodings and provides detailed error messages if the file cannot be read. "
|
|
146
|
+
"Use 'head' parameter to read only the first N lines, or 'tail' parameter to read only the last N lines. "
|
|
147
|
+
"Only works within allowed directories."
|
|
148
|
+
)
|
|
149
|
+
args_schema: type[BaseModel] = ReadFileInput
|
|
150
|
+
|
|
151
|
+
def _run(self, path: str, head: Optional[int] = None, tail: Optional[int] = None) -> str:
|
|
152
|
+
"""Read a file with optional head/tail."""
|
|
153
|
+
try:
|
|
154
|
+
target = self._resolve_path(path)
|
|
155
|
+
|
|
156
|
+
if not target.exists():
|
|
157
|
+
return f"Error: File '{path}' does not exist"
|
|
158
|
+
|
|
159
|
+
if not target.is_file():
|
|
160
|
+
return f"Error: '{path}' is not a file"
|
|
161
|
+
|
|
162
|
+
if head and tail:
|
|
163
|
+
return "Error: Cannot specify both head and tail parameters simultaneously"
|
|
164
|
+
|
|
165
|
+
with open(target, 'r', encoding='utf-8') as f:
|
|
166
|
+
if tail:
|
|
167
|
+
lines = f.readlines()
|
|
168
|
+
content = ''.join(lines[-tail:])
|
|
169
|
+
elif head:
|
|
170
|
+
lines = []
|
|
171
|
+
for i, line in enumerate(f):
|
|
172
|
+
if i >= head:
|
|
173
|
+
break
|
|
174
|
+
lines.append(line)
|
|
175
|
+
content = ''.join(lines)
|
|
176
|
+
else:
|
|
177
|
+
content = f.read()
|
|
178
|
+
|
|
179
|
+
char_count = len(content)
|
|
180
|
+
line_count = content.count('\n') + (1 if content and not content.endswith('\n') else 0)
|
|
181
|
+
|
|
182
|
+
return f"Successfully read '{path}' ({char_count} characters, {line_count} lines):\n\n{content}"
|
|
183
|
+
except UnicodeDecodeError:
|
|
184
|
+
return f"Error: File '{path}' appears to be binary or uses an unsupported encoding"
|
|
185
|
+
except Exception as e:
|
|
186
|
+
return f"Error reading file '{path}': {str(e)}"
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class ReadFileChunkTool(FileSystemTool):
|
|
190
|
+
"""Read a file in chunks by line range."""
|
|
191
|
+
name: str = "filesystem_read_file_chunk"
|
|
192
|
+
description: str = (
|
|
193
|
+
"Read a specific range of lines from a file. This is efficient for large files where you only need a portion. "
|
|
194
|
+
"Specify start_line (1-indexed) and optionally end_line. If end_line is omitted, reads to end of file. "
|
|
195
|
+
"Use this to avoid loading entire large files into memory. "
|
|
196
|
+
"Only works within allowed directories."
|
|
197
|
+
)
|
|
198
|
+
args_schema: type[BaseModel] = ReadFileChunkInput
|
|
199
|
+
|
|
200
|
+
def _run(self, path: str, start_line: int = 1, end_line: Optional[int] = None) -> str:
|
|
201
|
+
"""Read a chunk of a file by line range."""
|
|
202
|
+
try:
|
|
203
|
+
target = self._resolve_path(path)
|
|
204
|
+
|
|
205
|
+
if not target.exists():
|
|
206
|
+
return f"Error: File '{path}' does not exist"
|
|
207
|
+
|
|
208
|
+
if not target.is_file():
|
|
209
|
+
return f"Error: '{path}' is not a file"
|
|
210
|
+
|
|
211
|
+
if start_line < 1:
|
|
212
|
+
return "Error: start_line must be >= 1"
|
|
213
|
+
|
|
214
|
+
if end_line is not None and end_line < start_line:
|
|
215
|
+
return "Error: end_line must be >= start_line"
|
|
216
|
+
|
|
217
|
+
lines = []
|
|
218
|
+
with open(target, 'r', encoding='utf-8') as f:
|
|
219
|
+
for i, line in enumerate(f, 1):
|
|
220
|
+
if i < start_line:
|
|
221
|
+
continue
|
|
222
|
+
if end_line is not None and i > end_line:
|
|
223
|
+
break
|
|
224
|
+
lines.append(line)
|
|
225
|
+
|
|
226
|
+
content = ''.join(lines)
|
|
227
|
+
actual_end = end_line if end_line else start_line + len(lines) - 1
|
|
228
|
+
|
|
229
|
+
if not lines:
|
|
230
|
+
return f"No lines found in range {start_line}-{actual_end} in '{path}'"
|
|
231
|
+
|
|
232
|
+
return f"Successfully read '{path}' lines {start_line}-{actual_end} ({len(content)} characters, {len(lines)} lines):\n\n{content}"
|
|
233
|
+
except UnicodeDecodeError:
|
|
234
|
+
return f"Error: File '{path}' appears to be binary or uses an unsupported encoding"
|
|
235
|
+
except Exception as e:
|
|
236
|
+
return f"Error reading file '{path}': {str(e)}"
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class ReadMultipleFilesTool(FileSystemTool):
|
|
240
|
+
"""Read multiple files simultaneously."""
|
|
241
|
+
name: str = "filesystem_read_multiple_files"
|
|
242
|
+
description: str = (
|
|
243
|
+
"Read the contents of multiple files simultaneously. This is more efficient than reading files one by one. "
|
|
244
|
+
"Each file's content is returned with its path as a reference. "
|
|
245
|
+
"Failed reads for individual files won't stop the entire operation. "
|
|
246
|
+
"Only works within allowed directories."
|
|
247
|
+
)
|
|
248
|
+
args_schema: type[BaseModel] = ReadMultipleFilesInput
|
|
249
|
+
|
|
250
|
+
def _run(self, paths: List[str]) -> str:
|
|
251
|
+
"""Read multiple files."""
|
|
252
|
+
results = []
|
|
253
|
+
|
|
254
|
+
for file_path in paths:
|
|
255
|
+
try:
|
|
256
|
+
target = self._resolve_path(file_path)
|
|
257
|
+
with open(target, 'r', encoding='utf-8') as f:
|
|
258
|
+
content = f.read()
|
|
259
|
+
results.append(f"{file_path}:\n{content}")
|
|
260
|
+
except Exception as e:
|
|
261
|
+
results.append(f"{file_path}: Error - {str(e)}")
|
|
262
|
+
|
|
263
|
+
return "\n\n---\n\n".join(results)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class WriteFileTool(FileSystemTool):
|
|
267
|
+
"""Write content to a file."""
|
|
268
|
+
name: str = "filesystem_write_file"
|
|
269
|
+
description: str = (
|
|
270
|
+
"Create a new file or completely overwrite an existing file with new content. "
|
|
271
|
+
"Use with caution as it will overwrite existing files without warning. "
|
|
272
|
+
"Handles text content with proper encoding. Creates parent directories if needed. "
|
|
273
|
+
"Only works within allowed directories."
|
|
274
|
+
)
|
|
275
|
+
args_schema: type[BaseModel] = WriteFileInput
|
|
276
|
+
|
|
277
|
+
def _run(self, path: str, content: str) -> str:
|
|
278
|
+
"""Write to a file."""
|
|
279
|
+
try:
|
|
280
|
+
target = self._resolve_path(path)
|
|
281
|
+
|
|
282
|
+
# Create parent directories if they don't exist
|
|
283
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
284
|
+
|
|
285
|
+
with open(target, 'w', encoding='utf-8') as f:
|
|
286
|
+
f.write(content)
|
|
287
|
+
|
|
288
|
+
size = len(content.encode('utf-8'))
|
|
289
|
+
return f"Successfully wrote to '{path}' ({self._format_size(size)})"
|
|
290
|
+
except Exception as e:
|
|
291
|
+
return f"Error writing to file '{path}': {str(e)}"
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class EditFileTool(FileSystemTool):
|
|
295
|
+
"""Edit file with precise text replacement."""
|
|
296
|
+
name: str = "filesystem_edit_file"
|
|
297
|
+
description: str = (
|
|
298
|
+
"Make precise edits to a text file by replacing exact text matches. "
|
|
299
|
+
"The old_text must match exactly (including whitespace and line breaks). "
|
|
300
|
+
"This is safer than rewriting entire files when making small changes. "
|
|
301
|
+
"Only works within allowed directories."
|
|
302
|
+
)
|
|
303
|
+
args_schema: type[BaseModel] = EditFileInput
|
|
304
|
+
|
|
305
|
+
def _run(self, path: str, old_text: str, new_text: str) -> str:
|
|
306
|
+
"""Edit a file by replacing exact text."""
|
|
307
|
+
try:
|
|
308
|
+
target = self._resolve_path(path)
|
|
309
|
+
|
|
310
|
+
if not target.exists():
|
|
311
|
+
return f"Error: File '{path}' does not exist"
|
|
312
|
+
|
|
313
|
+
if not target.is_file():
|
|
314
|
+
return f"Error: '{path}' is not a file"
|
|
315
|
+
|
|
316
|
+
# Read current content
|
|
317
|
+
with open(target, 'r', encoding='utf-8') as f:
|
|
318
|
+
content = f.read()
|
|
319
|
+
|
|
320
|
+
# Check if old_text exists
|
|
321
|
+
if old_text not in content:
|
|
322
|
+
return f"Error: Could not find the specified text in '{path}'"
|
|
323
|
+
|
|
324
|
+
# Count occurrences
|
|
325
|
+
occurrences = content.count(old_text)
|
|
326
|
+
if occurrences > 1:
|
|
327
|
+
return f"Error: Found {occurrences} occurrences of the text. Please be more specific to ensure correct replacement."
|
|
328
|
+
|
|
329
|
+
# Replace text
|
|
330
|
+
new_content = content.replace(old_text, new_text)
|
|
331
|
+
|
|
332
|
+
# Write back
|
|
333
|
+
with open(target, 'w', encoding='utf-8') as f:
|
|
334
|
+
f.write(new_content)
|
|
335
|
+
|
|
336
|
+
chars_before = len(old_text)
|
|
337
|
+
chars_after = len(new_text)
|
|
338
|
+
diff = chars_after - chars_before
|
|
339
|
+
|
|
340
|
+
return f"Successfully edited '{path}': replaced {chars_before} characters with {chars_after} characters ({diff:+d} character difference)"
|
|
341
|
+
except Exception as e:
|
|
342
|
+
return f"Error editing file '{path}': {str(e)}"
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class ApplyPatchTool(FileSystemTool):
|
|
346
|
+
"""Apply multiple edits to a file like a patch."""
|
|
347
|
+
name: str = "filesystem_apply_patch"
|
|
348
|
+
description: str = (
|
|
349
|
+
"Apply multiple precise edits to a file in a single operation, similar to applying a patch. "
|
|
350
|
+
"Each edit specifies 'old_text' (exact text to find) and 'new_text' (replacement text). "
|
|
351
|
+
"Edits are applied sequentially. Use dry_run=true to preview changes without applying them. "
|
|
352
|
+
"This is efficient for making multiple changes to large files. "
|
|
353
|
+
"Only works within allowed directories."
|
|
354
|
+
)
|
|
355
|
+
args_schema: type[BaseModel] = ApplyPatchInput
|
|
356
|
+
|
|
357
|
+
def _run(self, path: str, edits: List[Dict[str, str]], dry_run: bool = False) -> str:
|
|
358
|
+
"""Apply multiple edits to a file."""
|
|
359
|
+
try:
|
|
360
|
+
target = self._resolve_path(path)
|
|
361
|
+
|
|
362
|
+
if not target.exists():
|
|
363
|
+
return f"Error: File '{path}' does not exist"
|
|
364
|
+
|
|
365
|
+
if not target.is_file():
|
|
366
|
+
return f"Error: '{path}' is not a file"
|
|
367
|
+
|
|
368
|
+
# Read current content
|
|
369
|
+
with open(target, 'r', encoding='utf-8') as f:
|
|
370
|
+
original_content = f.read()
|
|
371
|
+
|
|
372
|
+
content = original_content
|
|
373
|
+
changes = []
|
|
374
|
+
|
|
375
|
+
# Apply edits sequentially
|
|
376
|
+
for i, edit in enumerate(edits, 1):
|
|
377
|
+
old_text = edit.get('old_text', '')
|
|
378
|
+
new_text = edit.get('new_text', '')
|
|
379
|
+
|
|
380
|
+
if not old_text:
|
|
381
|
+
return f"Error: Edit #{i} is missing 'old_text'"
|
|
382
|
+
|
|
383
|
+
if old_text not in content:
|
|
384
|
+
return f"Error: Edit #{i} - could not find the specified text in current content"
|
|
385
|
+
|
|
386
|
+
# Count occurrences
|
|
387
|
+
occurrences = content.count(old_text)
|
|
388
|
+
if occurrences > 1:
|
|
389
|
+
return f"Error: Edit #{i} - found {occurrences} occurrences. Please be more specific."
|
|
390
|
+
|
|
391
|
+
# Apply the edit
|
|
392
|
+
content = content.replace(old_text, new_text)
|
|
393
|
+
changes.append({
|
|
394
|
+
'edit_num': i,
|
|
395
|
+
'old_len': len(old_text),
|
|
396
|
+
'new_len': len(new_text),
|
|
397
|
+
'diff': len(new_text) - len(old_text)
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
if dry_run:
|
|
401
|
+
# Show preview in diff-like format
|
|
402
|
+
lines = [f"Preview of changes to '{path}' ({len(edits)} edits):\n"]
|
|
403
|
+
for change in changes:
|
|
404
|
+
lines.append(
|
|
405
|
+
f"Edit #{change['edit_num']}: "
|
|
406
|
+
f"{change['old_len']} → {change['new_len']} chars "
|
|
407
|
+
f"({change['diff']:+d})"
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
total_diff = sum(c['diff'] for c in changes)
|
|
411
|
+
lines.append(f"\nTotal change: {len(original_content)} → {len(content)} chars ({total_diff:+d})")
|
|
412
|
+
lines.append("\n[DRY RUN - No changes written to file]")
|
|
413
|
+
return "\n".join(lines)
|
|
414
|
+
|
|
415
|
+
# Write the modified content
|
|
416
|
+
with open(target, 'w', encoding='utf-8') as f:
|
|
417
|
+
f.write(content)
|
|
418
|
+
|
|
419
|
+
# Build success message
|
|
420
|
+
lines = [f"Successfully applied {len(edits)} edits to '{path}':\n"]
|
|
421
|
+
for change in changes:
|
|
422
|
+
lines.append(
|
|
423
|
+
f"Edit #{change['edit_num']}: "
|
|
424
|
+
f"{change['old_len']} → {change['new_len']} chars "
|
|
425
|
+
f"({change['diff']:+d})"
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
total_diff = sum(c['diff'] for c in changes)
|
|
429
|
+
lines.append(f"\nTotal change: {len(original_content)} → {len(content)} chars ({total_diff:+d})")
|
|
430
|
+
|
|
431
|
+
return "\n".join(lines)
|
|
432
|
+
except Exception as e:
|
|
433
|
+
return f"Error applying patch to '{path}': {str(e)}"
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
class ListDirectoryTool(FileSystemTool):
|
|
437
|
+
"""List directory contents."""
|
|
438
|
+
name: str = "filesystem_list_directory"
|
|
439
|
+
description: str = (
|
|
440
|
+
"Get a detailed listing of all files and directories in a specified path. "
|
|
441
|
+
"Results clearly distinguish between files and directories with [FILE] and [DIR] prefixes. "
|
|
442
|
+
"Can optionally include file sizes and sort by name or size. "
|
|
443
|
+
"Only works within allowed directories."
|
|
444
|
+
)
|
|
445
|
+
args_schema: type[BaseModel] = ListDirectoryInput
|
|
446
|
+
|
|
447
|
+
def _run(self, path: str = ".", include_sizes: bool = False, sort_by: str = "name") -> str:
|
|
448
|
+
"""List directory contents."""
|
|
449
|
+
try:
|
|
450
|
+
target = self._resolve_path(path)
|
|
451
|
+
|
|
452
|
+
if not target.exists():
|
|
453
|
+
return f"Error: Directory '{path}' does not exist"
|
|
454
|
+
|
|
455
|
+
if not target.is_dir():
|
|
456
|
+
return f"Error: '{path}' is not a directory"
|
|
457
|
+
|
|
458
|
+
entries = []
|
|
459
|
+
for entry in target.iterdir():
|
|
460
|
+
entry_info = {
|
|
461
|
+
'name': entry.name,
|
|
462
|
+
'is_dir': entry.is_dir(),
|
|
463
|
+
'size': entry.stat().st_size if entry.is_file() else 0
|
|
464
|
+
}
|
|
465
|
+
entries.append(entry_info)
|
|
466
|
+
|
|
467
|
+
# Sort entries
|
|
468
|
+
if sort_by == "size":
|
|
469
|
+
entries.sort(key=lambda x: x['size'], reverse=True)
|
|
470
|
+
else:
|
|
471
|
+
entries.sort(key=lambda x: x['name'].lower())
|
|
472
|
+
|
|
473
|
+
# Format output
|
|
474
|
+
lines = []
|
|
475
|
+
total_files = 0
|
|
476
|
+
total_dirs = 0
|
|
477
|
+
total_size = 0
|
|
478
|
+
|
|
479
|
+
for entry in entries:
|
|
480
|
+
prefix = "[DIR] " if entry['is_dir'] else "[FILE]"
|
|
481
|
+
name = entry['name']
|
|
482
|
+
|
|
483
|
+
if include_sizes and not entry['is_dir']:
|
|
484
|
+
size_str = self._format_size(entry['size'])
|
|
485
|
+
lines.append(f"{prefix} {name:<40} {size_str:>10}")
|
|
486
|
+
total_size += entry['size']
|
|
487
|
+
else:
|
|
488
|
+
lines.append(f"{prefix} {name}")
|
|
489
|
+
|
|
490
|
+
if entry['is_dir']:
|
|
491
|
+
total_dirs += 1
|
|
492
|
+
else:
|
|
493
|
+
total_files += 1
|
|
494
|
+
|
|
495
|
+
result = "\n".join(lines)
|
|
496
|
+
|
|
497
|
+
if include_sizes:
|
|
498
|
+
summary = f"\n\nTotal: {total_files} files, {total_dirs} directories"
|
|
499
|
+
if total_files > 0:
|
|
500
|
+
summary += f"\nCombined size: {self._format_size(total_size)}"
|
|
501
|
+
result += summary
|
|
502
|
+
|
|
503
|
+
return result if result else "Directory is empty"
|
|
504
|
+
except Exception as e:
|
|
505
|
+
return f"Error listing directory '{path}': {str(e)}"
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
class DirectoryTreeTool(FileSystemTool):
|
|
509
|
+
"""Get recursive directory tree."""
|
|
510
|
+
name: str = "filesystem_directory_tree"
|
|
511
|
+
description: str = (
|
|
512
|
+
"Get a recursive tree view of files and directories. "
|
|
513
|
+
"Shows the complete structure in an easy-to-read tree format. "
|
|
514
|
+
"Use max_depth to limit recursion depth. "
|
|
515
|
+
"Only works within allowed directories."
|
|
516
|
+
)
|
|
517
|
+
args_schema: type[BaseModel] = DirectoryTreeInput
|
|
518
|
+
|
|
519
|
+
def _build_tree(self, directory: Path, prefix: str = "", depth: int = 0, max_depth: Optional[int] = None) -> List[str]:
|
|
520
|
+
"""Recursively build directory tree."""
|
|
521
|
+
if max_depth is not None and depth >= max_depth:
|
|
522
|
+
return []
|
|
523
|
+
|
|
524
|
+
lines = []
|
|
525
|
+
try:
|
|
526
|
+
entries = sorted(directory.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower()))
|
|
527
|
+
|
|
528
|
+
for i, entry in enumerate(entries):
|
|
529
|
+
is_last = i == len(entries) - 1
|
|
530
|
+
current_prefix = "└── " if is_last else "├── "
|
|
531
|
+
next_prefix = " " if is_last else "│ "
|
|
532
|
+
|
|
533
|
+
if entry.is_dir():
|
|
534
|
+
lines.append(f"{prefix}{current_prefix}📁 {entry.name}/")
|
|
535
|
+
lines.extend(self._build_tree(entry, prefix + next_prefix, depth + 1, max_depth))
|
|
536
|
+
else:
|
|
537
|
+
size = self._format_size(entry.stat().st_size)
|
|
538
|
+
lines.append(f"{prefix}{current_prefix}📄 {entry.name} ({size})")
|
|
539
|
+
except PermissionError:
|
|
540
|
+
lines.append(f"{prefix}[Permission Denied]")
|
|
541
|
+
|
|
542
|
+
return lines
|
|
543
|
+
|
|
544
|
+
def _run(self, path: str = ".", max_depth: Optional[int] = None) -> str:
|
|
545
|
+
"""Get directory tree."""
|
|
546
|
+
try:
|
|
547
|
+
target = self._resolve_path(path)
|
|
548
|
+
|
|
549
|
+
if not target.exists():
|
|
550
|
+
return f"Error: Directory '{path}' does not exist"
|
|
551
|
+
|
|
552
|
+
if not target.is_dir():
|
|
553
|
+
return f"Error: '{path}' is not a directory"
|
|
554
|
+
|
|
555
|
+
lines = [f"📁 {target.name or path}/"]
|
|
556
|
+
lines.extend(self._build_tree(target, "", 0, max_depth))
|
|
557
|
+
|
|
558
|
+
return "\n".join(lines)
|
|
559
|
+
except Exception as e:
|
|
560
|
+
return f"Error building directory tree for '{path}': {str(e)}"
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
class SearchFilesTool(FileSystemTool):
|
|
564
|
+
"""Search for files matching a pattern."""
|
|
565
|
+
name: str = "filesystem_search_files"
|
|
566
|
+
description: str = (
|
|
567
|
+
"Recursively search for files and directories matching a glob pattern. "
|
|
568
|
+
"Use patterns like '*.py' for Python files in current dir, or '**/*.py' for all Python files recursively. "
|
|
569
|
+
"Returns full paths to all matching items. "
|
|
570
|
+
"Only searches within allowed directories."
|
|
571
|
+
)
|
|
572
|
+
args_schema: type[BaseModel] = SearchFilesInput
|
|
573
|
+
|
|
574
|
+
def _run(self, path: str = ".", pattern: str = "*") -> str:
|
|
575
|
+
"""Search for files."""
|
|
576
|
+
try:
|
|
577
|
+
target = self._resolve_path(path)
|
|
578
|
+
|
|
579
|
+
if not target.exists():
|
|
580
|
+
return f"Error: Directory '{path}' does not exist"
|
|
581
|
+
|
|
582
|
+
if not target.is_dir():
|
|
583
|
+
return f"Error: '{path}' is not a directory"
|
|
584
|
+
|
|
585
|
+
# Use glob to find matching files
|
|
586
|
+
if '**' in pattern:
|
|
587
|
+
matches = list(target.glob(pattern))
|
|
588
|
+
else:
|
|
589
|
+
matches = list(target.glob(pattern))
|
|
590
|
+
|
|
591
|
+
if not matches:
|
|
592
|
+
return f"No files matching '{pattern}' found in '{path}'"
|
|
593
|
+
|
|
594
|
+
# Format results
|
|
595
|
+
base = Path(self.base_directory).resolve()
|
|
596
|
+
results = []
|
|
597
|
+
|
|
598
|
+
for match in sorted(matches):
|
|
599
|
+
rel_path = match.relative_to(base)
|
|
600
|
+
if match.is_dir():
|
|
601
|
+
results.append(f"📁 {rel_path}/")
|
|
602
|
+
else:
|
|
603
|
+
size = self._format_size(match.stat().st_size)
|
|
604
|
+
results.append(f"📄 {rel_path} ({size})")
|
|
605
|
+
|
|
606
|
+
header = f"Found {len(matches)} matches for '{pattern}':\n\n"
|
|
607
|
+
return header + "\n".join(results)
|
|
608
|
+
except Exception as e:
|
|
609
|
+
return f"Error searching files in '{path}': {str(e)}"
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
class DeleteFileTool(FileSystemTool):
|
|
613
|
+
"""Delete a file."""
|
|
614
|
+
name: str = "filesystem_delete_file"
|
|
615
|
+
description: str = (
|
|
616
|
+
"Delete a file. Use with caution as this operation cannot be undone. "
|
|
617
|
+
"Only deletes files, not directories. "
|
|
618
|
+
"Only works within allowed directories."
|
|
619
|
+
)
|
|
620
|
+
args_schema: type[BaseModel] = DeleteFileInput
|
|
621
|
+
|
|
622
|
+
def _run(self, path: str) -> str:
|
|
623
|
+
"""Delete a file."""
|
|
624
|
+
try:
|
|
625
|
+
target = self._resolve_path(path)
|
|
626
|
+
|
|
627
|
+
if not target.exists():
|
|
628
|
+
return f"Error: File '{path}' does not exist"
|
|
629
|
+
|
|
630
|
+
if not target.is_file():
|
|
631
|
+
return f"Error: '{path}' is not a file (directories cannot be deleted with this tool)"
|
|
632
|
+
|
|
633
|
+
size = target.stat().st_size
|
|
634
|
+
target.unlink()
|
|
635
|
+
|
|
636
|
+
return f"Successfully deleted '{path}' ({self._format_size(size)})"
|
|
637
|
+
except Exception as e:
|
|
638
|
+
return f"Error deleting file '{path}': {str(e)}"
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
class MoveFileTool(FileSystemTool):
|
|
642
|
+
"""Move or rename files and directories."""
|
|
643
|
+
name: str = "filesystem_move_file"
|
|
644
|
+
description: str = (
|
|
645
|
+
"Move or rename files and directories. Can move files between directories and rename them in a single operation. "
|
|
646
|
+
"If the destination exists, the operation will fail. "
|
|
647
|
+
"Works across different directories and can be used for simple renaming within the same directory. "
|
|
648
|
+
"Both source and destination must be within allowed directories."
|
|
649
|
+
)
|
|
650
|
+
args_schema: type[BaseModel] = MoveFileInput
|
|
651
|
+
|
|
652
|
+
def _run(self, source: str, destination: str) -> str:
|
|
653
|
+
"""Move or rename a file."""
|
|
654
|
+
try:
|
|
655
|
+
source_path = self._resolve_path(source)
|
|
656
|
+
dest_path = self._resolve_path(destination)
|
|
657
|
+
|
|
658
|
+
if not source_path.exists():
|
|
659
|
+
return f"Error: Source '{source}' does not exist"
|
|
660
|
+
|
|
661
|
+
if dest_path.exists():
|
|
662
|
+
return f"Error: Destination '{destination}' already exists"
|
|
663
|
+
|
|
664
|
+
# Create parent directories if needed
|
|
665
|
+
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
666
|
+
|
|
667
|
+
source_path.rename(dest_path)
|
|
668
|
+
|
|
669
|
+
return f"Successfully moved '{source}' to '{destination}'"
|
|
670
|
+
except Exception as e:
|
|
671
|
+
return f"Error moving '{source}' to '{destination}': {str(e)}"
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
class CreateDirectoryTool(FileSystemTool):
|
|
675
|
+
"""Create a directory."""
|
|
676
|
+
name: str = "filesystem_create_directory"
|
|
677
|
+
description: str = (
|
|
678
|
+
"Create a new directory or ensure a directory exists. "
|
|
679
|
+
"Can create multiple nested directories in one operation. "
|
|
680
|
+
"If the directory already exists, this operation will succeed silently. "
|
|
681
|
+
"Only works within allowed directories."
|
|
682
|
+
)
|
|
683
|
+
args_schema: type[BaseModel] = CreateDirectoryInput
|
|
684
|
+
|
|
685
|
+
def _run(self, path: str) -> str:
|
|
686
|
+
"""Create a directory."""
|
|
687
|
+
try:
|
|
688
|
+
target = self._resolve_path(path)
|
|
689
|
+
|
|
690
|
+
if target.exists():
|
|
691
|
+
if target.is_dir():
|
|
692
|
+
return f"Directory '{path}' already exists"
|
|
693
|
+
else:
|
|
694
|
+
return f"Error: '{path}' exists but is not a directory"
|
|
695
|
+
|
|
696
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
697
|
+
|
|
698
|
+
return f"Successfully created directory '{path}'"
|
|
699
|
+
except Exception as e:
|
|
700
|
+
return f"Error creating directory '{path}': {str(e)}"
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
class GetFileInfoTool(FileSystemTool):
|
|
704
|
+
"""Get detailed file/directory information."""
|
|
705
|
+
name: str = "filesystem_get_file_info"
|
|
706
|
+
description: str = (
|
|
707
|
+
"Retrieve detailed metadata about a file or directory. "
|
|
708
|
+
"Returns comprehensive information including size, creation time, last modified time, permissions, and type. "
|
|
709
|
+
"This tool is perfect for understanding file characteristics without reading the actual content. "
|
|
710
|
+
"Only works within allowed directories."
|
|
711
|
+
)
|
|
712
|
+
args_schema: type[BaseModel] = GetFileInfoInput
|
|
713
|
+
|
|
714
|
+
def _run(self, path: str) -> str:
|
|
715
|
+
"""Get file information."""
|
|
716
|
+
try:
|
|
717
|
+
target = self._resolve_path(path)
|
|
718
|
+
|
|
719
|
+
if not target.exists():
|
|
720
|
+
return f"Error: Path '{path}' does not exist"
|
|
721
|
+
|
|
722
|
+
stat = target.stat()
|
|
723
|
+
|
|
724
|
+
info = {
|
|
725
|
+
"Path": str(path),
|
|
726
|
+
"Type": "Directory" if target.is_dir() else "File",
|
|
727
|
+
"Size": self._format_size(stat.st_size) if target.is_file() else "N/A",
|
|
728
|
+
"Created": datetime.fromtimestamp(stat.st_ctime).strftime("%Y-%m-%d %H:%M:%S"),
|
|
729
|
+
"Modified": datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M:%S"),
|
|
730
|
+
"Accessed": datetime.fromtimestamp(stat.st_atime).strftime("%Y-%m-%d %H:%M:%S"),
|
|
731
|
+
"Permissions": oct(stat.st_mode)[-3:],
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if target.is_file():
|
|
735
|
+
info["Readable"] = os.access(target, os.R_OK)
|
|
736
|
+
info["Writable"] = os.access(target, os.W_OK)
|
|
737
|
+
info["Executable"] = os.access(target, os.X_OK)
|
|
738
|
+
|
|
739
|
+
return "\n".join(f"{key}: {value}" for key, value in info.items())
|
|
740
|
+
except Exception as e:
|
|
741
|
+
return f"Error getting info for '{path}': {str(e)}"
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
class ListAllowedDirectoriesTool(FileSystemTool):
|
|
745
|
+
"""List allowed directories."""
|
|
746
|
+
name: str = "filesystem_list_allowed_directories"
|
|
747
|
+
description: str = (
|
|
748
|
+
"Returns the list of directories that are accessible. "
|
|
749
|
+
"Subdirectories within allowed directories are also accessible. "
|
|
750
|
+
"Use this to understand which directories and their nested paths are available before trying to access files."
|
|
751
|
+
)
|
|
752
|
+
args_schema: type[BaseModel] = EmptyInput
|
|
753
|
+
|
|
754
|
+
def _run(self) -> str:
|
|
755
|
+
"""List allowed directories."""
|
|
756
|
+
return f"Allowed directory:\n{self.base_directory}\n\nAll subdirectories within this path are accessible."
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
# Predefined tool presets for common use cases
|
|
760
|
+
FILESYSTEM_TOOL_PRESETS = {
|
|
761
|
+
'read_only': {
|
|
762
|
+
'exclude_tools': [
|
|
763
|
+
'filesystem_write_file',
|
|
764
|
+
'filesystem_edit_file',
|
|
765
|
+
'filesystem_apply_patch',
|
|
766
|
+
'filesystem_delete_file',
|
|
767
|
+
'filesystem_move_file',
|
|
768
|
+
'filesystem_create_directory',
|
|
769
|
+
]
|
|
770
|
+
},
|
|
771
|
+
'no_delete': {
|
|
772
|
+
'exclude_tools': ['filesystem_delete_file']
|
|
773
|
+
},
|
|
774
|
+
'basic': {
|
|
775
|
+
'include_tools': [
|
|
776
|
+
'filesystem_read_file',
|
|
777
|
+
'filesystem_write_file',
|
|
778
|
+
'filesystem_list_directory',
|
|
779
|
+
'filesystem_create_directory',
|
|
780
|
+
]
|
|
781
|
+
},
|
|
782
|
+
'minimal': {
|
|
783
|
+
'include_tools': [
|
|
784
|
+
'filesystem_read_file',
|
|
785
|
+
'filesystem_list_directory',
|
|
786
|
+
]
|
|
787
|
+
},
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
def get_filesystem_tools(
|
|
792
|
+
base_directory: str,
|
|
793
|
+
include_tools: Optional[List[str]] = None,
|
|
794
|
+
exclude_tools: Optional[List[str]] = None,
|
|
795
|
+
preset: Optional[str] = None
|
|
796
|
+
) -> List[BaseTool]:
|
|
797
|
+
"""
|
|
798
|
+
Get filesystem tools for the specified base directory.
|
|
799
|
+
|
|
800
|
+
Args:
|
|
801
|
+
base_directory: Absolute or relative path to the directory to restrict access to
|
|
802
|
+
include_tools: Optional list of tool names to include. If provided, only these tools are returned.
|
|
803
|
+
If None, all tools are included (unless excluded).
|
|
804
|
+
exclude_tools: Optional list of tool names to exclude. Applied after include_tools.
|
|
805
|
+
preset: Optional preset name to use predefined tool sets. Presets:
|
|
806
|
+
- 'read_only': Excludes all write/modify operations
|
|
807
|
+
- 'no_delete': All tools except delete
|
|
808
|
+
- 'basic': Read, write, list, create directory
|
|
809
|
+
- 'minimal': Only read and list
|
|
810
|
+
Note: If preset is used with include_tools or exclude_tools,
|
|
811
|
+
preset is applied first, then custom filters.
|
|
812
|
+
|
|
813
|
+
Returns:
|
|
814
|
+
List of filesystem tools based on preset and/or include/exclude filters
|
|
815
|
+
|
|
816
|
+
Available tool names:
|
|
817
|
+
- filesystem_read_file
|
|
818
|
+
- filesystem_read_file_chunk
|
|
819
|
+
- filesystem_read_multiple_files
|
|
820
|
+
- filesystem_write_file
|
|
821
|
+
- filesystem_edit_file
|
|
822
|
+
- filesystem_apply_patch
|
|
823
|
+
- filesystem_list_directory
|
|
824
|
+
- filesystem_directory_tree
|
|
825
|
+
- filesystem_search_files
|
|
826
|
+
- filesystem_delete_file
|
|
827
|
+
- filesystem_move_file
|
|
828
|
+
- filesystem_create_directory
|
|
829
|
+
- filesystem_get_file_info
|
|
830
|
+
- filesystem_list_allowed_directories
|
|
831
|
+
|
|
832
|
+
Examples:
|
|
833
|
+
# Get all tools
|
|
834
|
+
get_filesystem_tools('/path/to/dir')
|
|
835
|
+
|
|
836
|
+
# Only read operations
|
|
837
|
+
get_filesystem_tools('/path/to/dir',
|
|
838
|
+
include_tools=['filesystem_read_file', 'filesystem_list_directory'])
|
|
839
|
+
|
|
840
|
+
# All tools except delete and write
|
|
841
|
+
get_filesystem_tools('/path/to/dir',
|
|
842
|
+
exclude_tools=['filesystem_delete_file', 'filesystem_write_file'])
|
|
843
|
+
|
|
844
|
+
# Use preset for read-only mode
|
|
845
|
+
get_filesystem_tools('/path/to/dir', preset='read_only')
|
|
846
|
+
|
|
847
|
+
# Use preset and add custom exclusions
|
|
848
|
+
get_filesystem_tools('/path/to/dir', preset='read_only',
|
|
849
|
+
exclude_tools=['filesystem_search_files'])
|
|
850
|
+
"""
|
|
851
|
+
# Apply preset if specified
|
|
852
|
+
preset_include = None
|
|
853
|
+
preset_exclude = None
|
|
854
|
+
if preset:
|
|
855
|
+
if preset not in FILESYSTEM_TOOL_PRESETS:
|
|
856
|
+
raise ValueError(f"Unknown preset '{preset}'. Available: {list(FILESYSTEM_TOOL_PRESETS.keys())}")
|
|
857
|
+
preset_config = FILESYSTEM_TOOL_PRESETS[preset]
|
|
858
|
+
preset_include = preset_config.get('include_tools')
|
|
859
|
+
preset_exclude = preset_config.get('exclude_tools')
|
|
860
|
+
|
|
861
|
+
# Merge preset with custom filters
|
|
862
|
+
# Priority: custom include_tools > preset include > all tools
|
|
863
|
+
final_include = include_tools if include_tools is not None else preset_include
|
|
864
|
+
|
|
865
|
+
# Priority: custom exclude_tools + preset exclude
|
|
866
|
+
final_exclude = []
|
|
867
|
+
if preset_exclude:
|
|
868
|
+
final_exclude.extend(preset_exclude)
|
|
869
|
+
if exclude_tools:
|
|
870
|
+
final_exclude.extend(exclude_tools)
|
|
871
|
+
final_exclude = list(set(final_exclude)) if final_exclude else None
|
|
872
|
+
|
|
873
|
+
# Resolve to absolute path
|
|
874
|
+
base_dir = str(Path(base_directory).resolve())
|
|
875
|
+
|
|
876
|
+
# Define all available tools with their names
|
|
877
|
+
all_tools = {
|
|
878
|
+
'filesystem_read_file': ReadFileTool(base_directory=base_dir),
|
|
879
|
+
'filesystem_read_file_chunk': ReadFileChunkTool(base_directory=base_dir),
|
|
880
|
+
'filesystem_read_multiple_files': ReadMultipleFilesTool(base_directory=base_dir),
|
|
881
|
+
'filesystem_write_file': WriteFileTool(base_directory=base_dir),
|
|
882
|
+
'filesystem_edit_file': EditFileTool(base_directory=base_dir),
|
|
883
|
+
'filesystem_apply_patch': ApplyPatchTool(base_directory=base_dir),
|
|
884
|
+
'filesystem_list_directory': ListDirectoryTool(base_directory=base_dir),
|
|
885
|
+
'filesystem_directory_tree': DirectoryTreeTool(base_directory=base_dir),
|
|
886
|
+
'filesystem_search_files': SearchFilesTool(base_directory=base_dir),
|
|
887
|
+
'filesystem_delete_file': DeleteFileTool(base_directory=base_dir),
|
|
888
|
+
'filesystem_move_file': MoveFileTool(base_directory=base_dir),
|
|
889
|
+
'filesystem_create_directory': CreateDirectoryTool(base_directory=base_dir),
|
|
890
|
+
'filesystem_get_file_info': GetFileInfoTool(base_directory=base_dir),
|
|
891
|
+
'filesystem_list_allowed_directories': ListAllowedDirectoriesTool(base_directory=base_dir),
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
# Start with all tools or only included ones
|
|
895
|
+
if final_include is not None:
|
|
896
|
+
selected_tools = {name: tool for name, tool in all_tools.items() if name in final_include}
|
|
897
|
+
else:
|
|
898
|
+
selected_tools = all_tools.copy()
|
|
899
|
+
|
|
900
|
+
# Remove excluded tools
|
|
901
|
+
if final_exclude is not None:
|
|
902
|
+
for name in final_exclude:
|
|
903
|
+
selected_tools.pop(name, None)
|
|
904
|
+
|
|
905
|
+
return list(selected_tools.values())
|