deepagents-cli 0.0.3__py3-none-any.whl → 0.0.5__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 deepagents-cli might be problematic. Click here for more details.
- deepagents_cli/__init__.py +5 -0
- deepagents_cli/__main__.py +6 -0
- deepagents_cli/agent.py +278 -0
- deepagents_cli/cli.py +13 -0
- deepagents_cli/commands.py +89 -0
- deepagents_cli/config.py +138 -0
- deepagents_cli/execution.py +644 -0
- deepagents_cli/file_ops.py +347 -0
- deepagents_cli/input.py +249 -0
- deepagents_cli/main.py +226 -0
- deepagents_cli/py.typed +0 -0
- deepagents_cli/token_utils.py +63 -0
- deepagents_cli/tools.py +140 -0
- deepagents_cli/ui.py +489 -0
- deepagents_cli-0.0.5.dist-info/METADATA +18 -0
- deepagents_cli-0.0.5.dist-info/RECORD +19 -0
- deepagents_cli-0.0.5.dist-info/entry_points.txt +3 -0
- deepagents_cli-0.0.5.dist-info/top_level.txt +1 -0
- deepagents/__init__.py +0 -7
- deepagents/cli.py +0 -567
- deepagents/default_agent_prompt.md +0 -64
- deepagents/graph.py +0 -144
- deepagents/memory/__init__.py +0 -17
- deepagents/memory/backends/__init__.py +0 -15
- deepagents/memory/backends/composite.py +0 -250
- deepagents/memory/backends/filesystem.py +0 -330
- deepagents/memory/backends/state.py +0 -206
- deepagents/memory/backends/store.py +0 -351
- deepagents/memory/backends/utils.py +0 -319
- deepagents/memory/protocol.py +0 -164
- deepagents/middleware/__init__.py +0 -13
- deepagents/middleware/agent_memory.py +0 -207
- deepagents/middleware/filesystem.py +0 -615
- deepagents/middleware/patch_tool_calls.py +0 -44
- deepagents/middleware/subagents.py +0 -481
- deepagents/pretty_cli.py +0 -289
- deepagents_cli-0.0.3.dist-info/METADATA +0 -551
- deepagents_cli-0.0.3.dist-info/RECORD +0 -24
- deepagents_cli-0.0.3.dist-info/entry_points.txt +0 -2
- deepagents_cli-0.0.3.dist-info/licenses/LICENSE +0 -21
- deepagents_cli-0.0.3.dist-info/top_level.txt +0 -1
- {deepagents_cli-0.0.3.dist-info → deepagents_cli-0.0.5.dist-info}/WHEEL +0 -0
|
@@ -1,330 +0,0 @@
|
|
|
1
|
-
"""FilesystemBackend: Read and write files directly from the filesystem."""
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
import re
|
|
5
|
-
from datetime import datetime
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from typing import Any, Optional, TYPE_CHECKING
|
|
8
|
-
from langgraph.types import Command
|
|
9
|
-
|
|
10
|
-
if TYPE_CHECKING:
|
|
11
|
-
from langchain.tools import ToolRuntime
|
|
12
|
-
|
|
13
|
-
from .utils import check_empty_content, format_content_with_line_numbers, perform_string_replacement
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class FilesystemBackend:
|
|
17
|
-
"""Backend that reads and writes files directly from the filesystem.
|
|
18
|
-
|
|
19
|
-
Files are accessed using their actual filesystem paths. Relative paths are
|
|
20
|
-
resolved relative to the current working directory. Content is read/written
|
|
21
|
-
as plain text, and metadata (timestamps) are derived from filesystem stats.
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
def __init__(
|
|
25
|
-
self,
|
|
26
|
-
root_dir: Optional[str | Path] = None,
|
|
27
|
-
virtual_mode: bool = False
|
|
28
|
-
) -> None:
|
|
29
|
-
"""Initialize filesystem backend.
|
|
30
|
-
|
|
31
|
-
Args:
|
|
32
|
-
root_dir: Optional root directory for file operations. If provided,
|
|
33
|
-
all file paths will be resolved relative to this directory.
|
|
34
|
-
If not provided, uses the current working directory.
|
|
35
|
-
"""
|
|
36
|
-
self.cwd = Path(root_dir) if root_dir else Path.cwd()
|
|
37
|
-
self.virtual_mode = virtual_mode
|
|
38
|
-
|
|
39
|
-
def _resolve_path(self, key: str) -> Path:
|
|
40
|
-
"""Resolve a file path relative to cwd if not absolute.
|
|
41
|
-
|
|
42
|
-
Args:
|
|
43
|
-
key: File path (absolute or relative)
|
|
44
|
-
|
|
45
|
-
Returns:
|
|
46
|
-
Resolved absolute Path object
|
|
47
|
-
"""
|
|
48
|
-
if self.virtual_mode:
|
|
49
|
-
return self.cwd / key.lstrip('/')
|
|
50
|
-
path = Path(key)
|
|
51
|
-
if path.is_absolute():
|
|
52
|
-
return path
|
|
53
|
-
return self.cwd / path
|
|
54
|
-
|
|
55
|
-
def ls(self, path: str) -> list[str]:
|
|
56
|
-
"""List files from filesystem.
|
|
57
|
-
|
|
58
|
-
Args:
|
|
59
|
-
path: Absolute directory path to list files from.
|
|
60
|
-
|
|
61
|
-
Returns:
|
|
62
|
-
List of absolute file paths.
|
|
63
|
-
"""
|
|
64
|
-
dir_path = self._resolve_path(path)
|
|
65
|
-
if not dir_path.exists() or not dir_path.is_dir():
|
|
66
|
-
return []
|
|
67
|
-
|
|
68
|
-
results: list[str] = []
|
|
69
|
-
|
|
70
|
-
# Convert cwd to string for comparison
|
|
71
|
-
cwd_str = str(self.cwd)
|
|
72
|
-
if not cwd_str.endswith("/"):
|
|
73
|
-
cwd_str += "/"
|
|
74
|
-
|
|
75
|
-
# Walk the directory tree
|
|
76
|
-
try:
|
|
77
|
-
for path in dir_path.rglob("*"):
|
|
78
|
-
if path.is_file():
|
|
79
|
-
abs_path = str(path)
|
|
80
|
-
if not self.virtual_mode:
|
|
81
|
-
results.append(abs_path)
|
|
82
|
-
continue
|
|
83
|
-
# Strip the cwd prefix if present
|
|
84
|
-
if abs_path.startswith(cwd_str):
|
|
85
|
-
relative_path = abs_path[len(cwd_str):]
|
|
86
|
-
elif abs_path.startswith(str(self.cwd)):
|
|
87
|
-
# Handle case where cwd doesn't end with /
|
|
88
|
-
relative_path = abs_path[len(str(self.cwd)):].lstrip("/")
|
|
89
|
-
else:
|
|
90
|
-
# Path is outside cwd, return as-is or skip
|
|
91
|
-
relative_path = abs_path
|
|
92
|
-
|
|
93
|
-
results.append("/" + relative_path)
|
|
94
|
-
except (OSError, PermissionError):
|
|
95
|
-
pass
|
|
96
|
-
|
|
97
|
-
return sorted(results)
|
|
98
|
-
|
|
99
|
-
def read(
|
|
100
|
-
self,
|
|
101
|
-
file_path: str,
|
|
102
|
-
offset: int = 0,
|
|
103
|
-
limit: int = 2000,
|
|
104
|
-
) -> str:
|
|
105
|
-
"""Read file content with line numbers.
|
|
106
|
-
|
|
107
|
-
Args:
|
|
108
|
-
file_path: Absolute or relative file path
|
|
109
|
-
offset: Line offset to start reading from (0-indexed)
|
|
110
|
-
limit: Maximum number of lines to readReturns:
|
|
111
|
-
Formatted file content with line numbers, or error message.
|
|
112
|
-
"""
|
|
113
|
-
resolved_path = self._resolve_path(file_path)
|
|
114
|
-
|
|
115
|
-
if not resolved_path.exists() or not resolved_path.is_file():
|
|
116
|
-
return f"Error: File '{file_path}' not found"
|
|
117
|
-
|
|
118
|
-
try:
|
|
119
|
-
with open(resolved_path, "r", encoding="utf-8") as f:
|
|
120
|
-
content = f.read()
|
|
121
|
-
|
|
122
|
-
empty_msg = check_empty_content(content)
|
|
123
|
-
if empty_msg:
|
|
124
|
-
return empty_msg
|
|
125
|
-
|
|
126
|
-
lines = content.splitlines()
|
|
127
|
-
start_idx = offset
|
|
128
|
-
end_idx = min(start_idx + limit, len(lines))
|
|
129
|
-
|
|
130
|
-
if start_idx >= len(lines):
|
|
131
|
-
return f"Error: Line offset {offset} exceeds file length ({len(lines)} lines)"
|
|
132
|
-
|
|
133
|
-
selected_lines = lines[start_idx:end_idx]
|
|
134
|
-
return format_content_with_line_numbers(selected_lines, start_line=start_idx + 1)
|
|
135
|
-
except (OSError, UnicodeDecodeError) as e:
|
|
136
|
-
return f"Error reading file '{file_path}': {e}"
|
|
137
|
-
|
|
138
|
-
def write(
|
|
139
|
-
self,
|
|
140
|
-
file_path: str,
|
|
141
|
-
content: str,
|
|
142
|
-
) -> Command | str:
|
|
143
|
-
"""Create a new file with content.
|
|
144
|
-
|
|
145
|
-
Args:
|
|
146
|
-
file_path: Absolute or relative file path
|
|
147
|
-
content: File content as a stringReturns:
|
|
148
|
-
Success message or error if file already exists.
|
|
149
|
-
"""
|
|
150
|
-
resolved_path = self._resolve_path(file_path)
|
|
151
|
-
|
|
152
|
-
if resolved_path.exists():
|
|
153
|
-
return f"Cannot write to {file_path} because it already exists. Read and then make an edit, or write to a new path."
|
|
154
|
-
|
|
155
|
-
try:
|
|
156
|
-
# Create parent directories if needed
|
|
157
|
-
resolved_path.parent.mkdir(parents=True, exist_ok=True)
|
|
158
|
-
|
|
159
|
-
with open(resolved_path, "w", encoding="utf-8") as f:
|
|
160
|
-
f.write(content)
|
|
161
|
-
|
|
162
|
-
return f"Updated file {file_path}"
|
|
163
|
-
except (OSError, UnicodeEncodeError) as e:
|
|
164
|
-
return f"Error writing file '{file_path}': {e}"
|
|
165
|
-
|
|
166
|
-
def edit(
|
|
167
|
-
self,
|
|
168
|
-
file_path: str,
|
|
169
|
-
old_string: str,
|
|
170
|
-
new_string: str,
|
|
171
|
-
replace_all: bool = False,
|
|
172
|
-
) -> Command | str:
|
|
173
|
-
"""Edit a file by replacing string occurrences.
|
|
174
|
-
|
|
175
|
-
Args:
|
|
176
|
-
file_path: Absolute or relative file path
|
|
177
|
-
old_string: String to find and replace
|
|
178
|
-
new_string: Replacement string
|
|
179
|
-
replace_all: If True, replace all occurrencesReturns:
|
|
180
|
-
Success message or error message on failure.
|
|
181
|
-
"""
|
|
182
|
-
resolved_path = self._resolve_path(file_path)
|
|
183
|
-
|
|
184
|
-
if not resolved_path.exists() or not resolved_path.is_file():
|
|
185
|
-
return f"Error: File '{file_path}' not found"
|
|
186
|
-
|
|
187
|
-
try:
|
|
188
|
-
with open(resolved_path, "r", encoding="utf-8") as f:
|
|
189
|
-
content = f.read()
|
|
190
|
-
|
|
191
|
-
result = perform_string_replacement(content, old_string, new_string, replace_all)
|
|
192
|
-
|
|
193
|
-
if isinstance(result, str):
|
|
194
|
-
return result
|
|
195
|
-
|
|
196
|
-
new_content, occurrences = result
|
|
197
|
-
|
|
198
|
-
with open(resolved_path, "w", encoding="utf-8") as f:
|
|
199
|
-
f.write(new_content)
|
|
200
|
-
|
|
201
|
-
return f"Successfully replaced {occurrences} instance(s) of the string in '{file_path}'"
|
|
202
|
-
except (OSError, UnicodeDecodeError, UnicodeEncodeError) as e:
|
|
203
|
-
return f"Error editing file '{file_path}': {e}"
|
|
204
|
-
|
|
205
|
-
def delete(self, file_path: str) -> Command | None:
|
|
206
|
-
"""Delete file from filesystem.
|
|
207
|
-
|
|
208
|
-
Args:
|
|
209
|
-
file_path: File path to delete (absolute or relative to cwd)Returns:
|
|
210
|
-
None (direct filesystem modification)
|
|
211
|
-
"""
|
|
212
|
-
resolved_path = self._resolve_path(file_path)
|
|
213
|
-
|
|
214
|
-
if resolved_path.exists() and resolved_path.is_file():
|
|
215
|
-
resolved_path.unlink()
|
|
216
|
-
|
|
217
|
-
return None
|
|
218
|
-
|
|
219
|
-
def grep(
|
|
220
|
-
self,
|
|
221
|
-
pattern: str,
|
|
222
|
-
path: str = "/",
|
|
223
|
-
glob: Optional[str] = None,
|
|
224
|
-
output_mode: str = "files_with_matches",
|
|
225
|
-
) -> str:
|
|
226
|
-
"""Search for a pattern in files.
|
|
227
|
-
|
|
228
|
-
Args:
|
|
229
|
-
pattern: String pattern to search for
|
|
230
|
-
path: Path to search in (default "/")
|
|
231
|
-
glob: Optional glob pattern to filter files (e.g., "*.py")
|
|
232
|
-
output_mode: Output format - "files_with_matches", "content", or "count"Returns:
|
|
233
|
-
Formatted search results based on output_mode.
|
|
234
|
-
"""
|
|
235
|
-
regex = re.compile(re.escape(pattern))
|
|
236
|
-
|
|
237
|
-
if glob:
|
|
238
|
-
files_to_search = self.glob(glob)
|
|
239
|
-
else:
|
|
240
|
-
files_to_search = self.ls(path if path != "/" else None)
|
|
241
|
-
|
|
242
|
-
if path != "/":
|
|
243
|
-
files_to_search = [f for f in files_to_search if f.startswith(path)]
|
|
244
|
-
|
|
245
|
-
file_matches = {}
|
|
246
|
-
|
|
247
|
-
for fp in files_to_search:
|
|
248
|
-
resolved_path = self._resolve_path(fp)
|
|
249
|
-
|
|
250
|
-
if not resolved_path.exists() or not resolved_path.is_file():
|
|
251
|
-
continue
|
|
252
|
-
|
|
253
|
-
try:
|
|
254
|
-
with open(resolved_path, "r", encoding="utf-8") as f:
|
|
255
|
-
lines = f.readlines()
|
|
256
|
-
|
|
257
|
-
matches = []
|
|
258
|
-
for line_num, line in enumerate(lines, start=1):
|
|
259
|
-
if regex.search(line):
|
|
260
|
-
matches.append((line_num, line.rstrip()))
|
|
261
|
-
|
|
262
|
-
if matches:
|
|
263
|
-
file_matches[fp] = matches
|
|
264
|
-
except (OSError, UnicodeDecodeError):
|
|
265
|
-
continue
|
|
266
|
-
|
|
267
|
-
if not file_matches:
|
|
268
|
-
return f"No matches found for pattern: '{pattern}'"
|
|
269
|
-
|
|
270
|
-
if output_mode == "files_with_matches":
|
|
271
|
-
return "\n".join(sorted(file_matches.keys()))
|
|
272
|
-
elif output_mode == "count":
|
|
273
|
-
results = []
|
|
274
|
-
for fp in sorted(file_matches.keys()):
|
|
275
|
-
count = len(file_matches[fp])
|
|
276
|
-
results.append(f"{fp}: {count}")
|
|
277
|
-
return "\n".join(results)
|
|
278
|
-
else:
|
|
279
|
-
results = []
|
|
280
|
-
for fp in sorted(file_matches.keys()):
|
|
281
|
-
results.append(f"{fp}:")
|
|
282
|
-
for line_num, line in file_matches[fp]:
|
|
283
|
-
results.append(f" {line_num}: {line}")
|
|
284
|
-
return "\n".join(results)
|
|
285
|
-
|
|
286
|
-
def glob(self, pattern: str, path: str = "/") -> list[str]:
|
|
287
|
-
"""Find files matching a glob pattern.
|
|
288
|
-
|
|
289
|
-
Args:
|
|
290
|
-
pattern: Glob pattern (e.g., "**/*.py", "*.txt", "/subdir/**/*.md")
|
|
291
|
-
path: Base path to search from (default "/")Returns:
|
|
292
|
-
List of absolute file paths matching the pattern.
|
|
293
|
-
"""
|
|
294
|
-
if pattern.startswith("/"):
|
|
295
|
-
pattern = pattern.lstrip("/")
|
|
296
|
-
|
|
297
|
-
if path == "/":
|
|
298
|
-
search_path = self.cwd
|
|
299
|
-
else:
|
|
300
|
-
search_path = self._resolve_path(path)
|
|
301
|
-
|
|
302
|
-
if not search_path.exists() or not search_path.is_dir():
|
|
303
|
-
return []
|
|
304
|
-
|
|
305
|
-
results = []
|
|
306
|
-
|
|
307
|
-
try:
|
|
308
|
-
for matched_path in search_path.glob(pattern):
|
|
309
|
-
if matched_path.is_file():
|
|
310
|
-
abs_path = str(matched_path)
|
|
311
|
-
if not self.virtual_mode:
|
|
312
|
-
results.append(abs_path)
|
|
313
|
-
continue
|
|
314
|
-
|
|
315
|
-
cwd_str = str(self.cwd)
|
|
316
|
-
if not cwd_str.endswith("/"):
|
|
317
|
-
cwd_str += "/"
|
|
318
|
-
|
|
319
|
-
if abs_path.startswith(cwd_str):
|
|
320
|
-
relative_path = abs_path[len(cwd_str):]
|
|
321
|
-
elif abs_path.startswith(str(self.cwd)):
|
|
322
|
-
relative_path = abs_path[len(str(self.cwd)):].lstrip("/")
|
|
323
|
-
else:
|
|
324
|
-
relative_path = abs_path
|
|
325
|
-
|
|
326
|
-
results.append("/" + relative_path)
|
|
327
|
-
except (OSError, ValueError):
|
|
328
|
-
pass
|
|
329
|
-
|
|
330
|
-
return sorted(results)
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
"""StateBackend: Store files in LangGraph agent state (ephemeral)."""
|
|
2
|
-
|
|
3
|
-
import re
|
|
4
|
-
from typing import Any, Literal, Optional, TYPE_CHECKING
|
|
5
|
-
|
|
6
|
-
if TYPE_CHECKING:
|
|
7
|
-
from langchain.tools import ToolRuntime
|
|
8
|
-
|
|
9
|
-
from langchain_core.messages import ToolMessage
|
|
10
|
-
from langgraph.types import Command
|
|
11
|
-
|
|
12
|
-
from .utils import (
|
|
13
|
-
create_file_data,
|
|
14
|
-
update_file_data,
|
|
15
|
-
file_data_to_string,
|
|
16
|
-
format_read_response,
|
|
17
|
-
perform_string_replacement,
|
|
18
|
-
_glob_search_files,
|
|
19
|
-
_grep_search_files,
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class StateBackend:
|
|
24
|
-
"""Backend that stores files in agent state (ephemeral).
|
|
25
|
-
|
|
26
|
-
Uses LangGraph's state management and checkpointing. Files persist within
|
|
27
|
-
a conversation thread but not across threads. State is automatically
|
|
28
|
-
checkpointed after each agent step.
|
|
29
|
-
|
|
30
|
-
Special handling: Since LangGraph state must be updated via Command objects
|
|
31
|
-
(not direct mutation), operations return Command objects instead of None.
|
|
32
|
-
This is indicated by the uses_state=True flag.
|
|
33
|
-
"""
|
|
34
|
-
|
|
35
|
-
def __init__(self, runtime: "ToolRuntime"):
|
|
36
|
-
"""Initialize StateBackend with runtime.
|
|
37
|
-
|
|
38
|
-
Args:"""
|
|
39
|
-
self.runtime = runtime
|
|
40
|
-
|
|
41
|
-
def ls(self, path: str) -> list[str]:
|
|
42
|
-
"""List files from state.
|
|
43
|
-
|
|
44
|
-
Args:
|
|
45
|
-
path: Absolute path to directory.
|
|
46
|
-
|
|
47
|
-
Returns:
|
|
48
|
-
List of file paths.
|
|
49
|
-
"""
|
|
50
|
-
files = self.runtime.state.get("files", {})
|
|
51
|
-
keys = list(files.keys())
|
|
52
|
-
keys = [k for k in keys if k.startswith(path)]
|
|
53
|
-
return keys
|
|
54
|
-
|
|
55
|
-
def read(
|
|
56
|
-
self,
|
|
57
|
-
file_path: str,
|
|
58
|
-
offset: int = 0,
|
|
59
|
-
limit: int = 2000,
|
|
60
|
-
) -> str:
|
|
61
|
-
"""Read file content with line numbers.
|
|
62
|
-
|
|
63
|
-
Args:
|
|
64
|
-
file_path: Absolute file path
|
|
65
|
-
offset: Line offset to start reading from (0-indexed)
|
|
66
|
-
limit: Maximum number of lines to readReturns:
|
|
67
|
-
Formatted file content with line numbers, or error message.
|
|
68
|
-
"""
|
|
69
|
-
files = self.runtime.state.get("files", {})
|
|
70
|
-
file_data = files.get(file_path)
|
|
71
|
-
|
|
72
|
-
if file_data is None:
|
|
73
|
-
return f"Error: File '{file_path}' not found"
|
|
74
|
-
|
|
75
|
-
return format_read_response(file_data, offset, limit)
|
|
76
|
-
|
|
77
|
-
def write(
|
|
78
|
-
self,
|
|
79
|
-
file_path: str,
|
|
80
|
-
content: str,
|
|
81
|
-
) -> Command | str:
|
|
82
|
-
"""Create a new file with content.
|
|
83
|
-
|
|
84
|
-
Args:
|
|
85
|
-
file_path: Absolute file path
|
|
86
|
-
content: File content as a stringReturns:
|
|
87
|
-
Command object to update state, or error message if file exists.
|
|
88
|
-
"""
|
|
89
|
-
files = self.runtime.state.get("files", {})
|
|
90
|
-
|
|
91
|
-
if file_path in files:
|
|
92
|
-
return f"Cannot write to {file_path} because it already exists. Read and then make an edit, or write to a new path."
|
|
93
|
-
|
|
94
|
-
new_file_data = create_file_data(content)
|
|
95
|
-
tool_call_id = self.runtime.tool_call_id
|
|
96
|
-
|
|
97
|
-
return Command(
|
|
98
|
-
update={
|
|
99
|
-
"files": {file_path: new_file_data},
|
|
100
|
-
"messages": [
|
|
101
|
-
ToolMessage(
|
|
102
|
-
content=f"Updated file {file_path}",
|
|
103
|
-
tool_call_id=tool_call_id,
|
|
104
|
-
)
|
|
105
|
-
],
|
|
106
|
-
}
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
def edit(
|
|
110
|
-
self,
|
|
111
|
-
file_path: str,
|
|
112
|
-
old_string: str,
|
|
113
|
-
new_string: str,
|
|
114
|
-
replace_all: bool = False,
|
|
115
|
-
) -> Command | str:
|
|
116
|
-
"""Edit a file by replacing string occurrences.
|
|
117
|
-
|
|
118
|
-
Args:
|
|
119
|
-
file_path: Absolute file path
|
|
120
|
-
old_string: String to find and replace
|
|
121
|
-
new_string: Replacement string
|
|
122
|
-
replace_all: If True, replace all occurrencesReturns:
|
|
123
|
-
Command object to update state, or error message on failure.
|
|
124
|
-
"""
|
|
125
|
-
files = self.runtime.state.get("files", {})
|
|
126
|
-
file_data = files.get(file_path)
|
|
127
|
-
|
|
128
|
-
if file_data is None:
|
|
129
|
-
return f"Error: File '{file_path}' not found"
|
|
130
|
-
|
|
131
|
-
content = file_data_to_string(file_data)
|
|
132
|
-
result = perform_string_replacement(content, old_string, new_string, replace_all)
|
|
133
|
-
|
|
134
|
-
if isinstance(result, str):
|
|
135
|
-
return result
|
|
136
|
-
|
|
137
|
-
new_content, occurrences = result
|
|
138
|
-
new_file_data = update_file_data(file_data, new_content)
|
|
139
|
-
tool_call_id = self.runtime.tool_call_id
|
|
140
|
-
|
|
141
|
-
return Command(
|
|
142
|
-
update={
|
|
143
|
-
"files": {file_path: new_file_data},
|
|
144
|
-
"messages": [
|
|
145
|
-
ToolMessage(
|
|
146
|
-
content=f"Successfully replaced {occurrences} instance(s) of the string in '{file_path}'",
|
|
147
|
-
tool_call_id=tool_call_id,
|
|
148
|
-
)
|
|
149
|
-
],
|
|
150
|
-
}
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
def delete(self, file_path: str) -> Command | None:
|
|
154
|
-
"""Delete file from state via Command.
|
|
155
|
-
|
|
156
|
-
Args:
|
|
157
|
-
file_path: File path to deleteReturns:
|
|
158
|
-
Command object to update state (sets file to None for deletion).
|
|
159
|
-
"""
|
|
160
|
-
tool_call_id = self.runtime.tool_call_id
|
|
161
|
-
return Command(
|
|
162
|
-
update={
|
|
163
|
-
"files": {file_path: None},
|
|
164
|
-
"messages": [
|
|
165
|
-
ToolMessage(
|
|
166
|
-
content=f"Deleted file {file_path}",
|
|
167
|
-
tool_call_id=tool_call_id,
|
|
168
|
-
)
|
|
169
|
-
],
|
|
170
|
-
}
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
def grep(
|
|
174
|
-
self,
|
|
175
|
-
pattern: str,
|
|
176
|
-
path: str = "/",
|
|
177
|
-
glob: Optional[str] = None,
|
|
178
|
-
output_mode: Literal["files_with_matches", "content", "count"] = "files_with_matches",
|
|
179
|
-
) -> str:
|
|
180
|
-
"""Search for a pattern in files.
|
|
181
|
-
|
|
182
|
-
Args:
|
|
183
|
-
pattern: String pattern to search for
|
|
184
|
-
path: Path to search in (default "/")
|
|
185
|
-
glob: Optional glob pattern to filter files (e.g., "*.py")
|
|
186
|
-
output_mode: Output format - "files_with_matches", "content", or "count"Returns:
|
|
187
|
-
Formatted search results based on output_mode.
|
|
188
|
-
"""
|
|
189
|
-
files = self.runtime.state.get("files", {})
|
|
190
|
-
|
|
191
|
-
return _grep_search_files(files, pattern, path, glob, output_mode)
|
|
192
|
-
|
|
193
|
-
def glob(self, pattern: str, path: str = "/") -> list[str]:
|
|
194
|
-
"""Find files matching a glob pattern.
|
|
195
|
-
|
|
196
|
-
Args:
|
|
197
|
-
pattern: Glob pattern (e.g., "**/*.py", "*.txt", "/subdir/**/*.md")
|
|
198
|
-
path: Base path to search from (default "/")Returns:
|
|
199
|
-
List of absolute file paths matching the pattern.
|
|
200
|
-
"""
|
|
201
|
-
files = self.runtime.state.get("files", {})
|
|
202
|
-
|
|
203
|
-
result = _glob_search_files(files, pattern, path)
|
|
204
|
-
if result == "No files found":
|
|
205
|
-
return []
|
|
206
|
-
return result.split("\n")
|