devorch 0.1.2__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.
- cli/commands/ask.py +0 -0
- cli/commands/edit.py +0 -0
- cli/commands/run.py +0 -0
- cli/main.py +1527 -0
- config/permissions.py +285 -0
- config/profiles.py +0 -0
- config/settings.py +185 -0
- core/agent.py +433 -0
- core/context.py +0 -0
- core/executor.py +213 -0
- core/modes.py +141 -0
- core/planner.py +16 -0
- core/sessions.py +271 -0
- core/tasks.py +142 -0
- devorch-0.1.2.dist-info/METADATA +528 -0
- devorch-0.1.2.dist-info/RECORD +50 -0
- devorch-0.1.2.dist-info/WHEEL +4 -0
- devorch-0.1.2.dist-info/entry_points.txt +2 -0
- providers/__init__.py +130 -0
- providers/anthropic.py +127 -0
- providers/base.py +51 -0
- providers/custom.py +186 -0
- providers/deepseek.py +170 -0
- providers/gemini.py +192 -0
- providers/github_copilot.py +196 -0
- providers/groq.py +158 -0
- providers/kimi.py +181 -0
- providers/lmstudio.py +147 -0
- providers/local.py +214 -0
- providers/mistral.py +161 -0
- providers/openai.py +114 -0
- providers/openrouter.py +195 -0
- providers/together.py +159 -0
- schemas/message.py +32 -0
- schemas/task.py +115 -0
- schemas/tool.py +0 -0
- tools/__init__.py +30 -0
- tools/base.py +40 -0
- tools/edit.py +236 -0
- tools/filesystem.py +224 -0
- tools/git.py +0 -0
- tools/grep.py +280 -0
- tools/search.py +150 -0
- tools/shell.py +55 -0
- tools/task.py +91 -0
- tools/terminal.py +123 -0
- tools/terminal_session.py +298 -0
- tools/tests.py +0 -0
- tools/websearch.py +166 -0
- utils/logger.py +52 -0
tools/edit.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Edit Tool - Make targeted edits to files.
|
|
3
|
+
Supports find/replace, line-based edits, and smart code modifications.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import difflib
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
from typing import Any, Literal
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
from tools.base import Tool
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class EditToolSchema(BaseModel):
|
|
17
|
+
action: Literal["replace", "replace_lines", "insert", "delete", "patch"] = Field(
|
|
18
|
+
..., description="Edit action: replace (find/replace), replace_lines, insert, delete, patch"
|
|
19
|
+
)
|
|
20
|
+
path: str = Field(..., description="Path to the file to edit.")
|
|
21
|
+
find: str = Field(default="", description="Text or pattern to find (for replace action).")
|
|
22
|
+
replace_with: str = Field(default="", description="Replacement text.")
|
|
23
|
+
line_start: int = Field(
|
|
24
|
+
default=0, description="Start line number (1-indexed, for line-based actions)."
|
|
25
|
+
)
|
|
26
|
+
line_end: int = Field(default=0, description="End line number (for replace_lines/delete).")
|
|
27
|
+
content: str = Field(default="", description="Content for insert or patch action.")
|
|
28
|
+
regex: bool = Field(default=False, description="Treat 'find' as regex pattern.")
|
|
29
|
+
count: int = Field(default=1, description="Number of replacements (0 = all).")
|
|
30
|
+
dry_run: bool = Field(default=False, description="Preview changes without applying.")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class EditTool(Tool):
|
|
34
|
+
"""
|
|
35
|
+
Make targeted edits to files with precise control.
|
|
36
|
+
Returns a diff showing the changes made.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
name = "edit"
|
|
40
|
+
description = """Make targeted edits to files. More precise than full file writes.
|
|
41
|
+
|
|
42
|
+
Actions:
|
|
43
|
+
- replace: Find and replace text (supports regex)
|
|
44
|
+
- replace_lines: Replace specific line range with new content
|
|
45
|
+
- insert: Insert content at a specific line
|
|
46
|
+
- delete: Delete lines in a range
|
|
47
|
+
- patch: Apply a unified diff patch
|
|
48
|
+
|
|
49
|
+
Examples:
|
|
50
|
+
- Replace text: action="replace", path="file.py", find="old_func", replace_with="new_func"
|
|
51
|
+
- Replace all: action="replace", path="file.py", find="TODO", replace_with="DONE", count=0
|
|
52
|
+
- Regex replace: action="replace", find="def (\\w+)\\(", replace_with="async def \\1(", regex=true
|
|
53
|
+
- Replace lines 10-15: action="replace_lines", line_start=10, line_end=15, content="new code"
|
|
54
|
+
- Insert at line 5: action="insert", line_start=5, content="# New comment"
|
|
55
|
+
- Delete lines 20-25: action="delete", line_start=20, line_end=25
|
|
56
|
+
- Preview changes: dry_run=true
|
|
57
|
+
|
|
58
|
+
Always use dry_run=true first to preview significant changes!"""
|
|
59
|
+
args_schema = EditToolSchema
|
|
60
|
+
|
|
61
|
+
def _read_file(self, path: str) -> list[str]:
|
|
62
|
+
"""Read file and return lines."""
|
|
63
|
+
with open(path, encoding="utf-8") as f:
|
|
64
|
+
return f.readlines()
|
|
65
|
+
|
|
66
|
+
def _write_file(self, path: str, lines: list[str]):
|
|
67
|
+
"""Write lines to file."""
|
|
68
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
69
|
+
f.writelines(lines)
|
|
70
|
+
|
|
71
|
+
def _generate_diff(self, original: list[str], modified: list[str], path: str) -> str:
|
|
72
|
+
"""Generate a unified diff between original and modified content."""
|
|
73
|
+
diff = difflib.unified_diff(
|
|
74
|
+
original, modified, fromfile=f"a/{path}", tofile=f"b/{path}", lineterm=""
|
|
75
|
+
)
|
|
76
|
+
return "\n".join(diff)
|
|
77
|
+
|
|
78
|
+
def _replace_text(
|
|
79
|
+
self, lines: list[str], find: str, replace_with: str, regex: bool = False, count: int = 1
|
|
80
|
+
) -> tuple[list[str], int]:
|
|
81
|
+
"""Replace text in content. Returns (new_lines, replacement_count)."""
|
|
82
|
+
content = "".join(lines)
|
|
83
|
+
replacements = 0
|
|
84
|
+
|
|
85
|
+
if regex:
|
|
86
|
+
try:
|
|
87
|
+
pattern = re.compile(find)
|
|
88
|
+
if count == 0:
|
|
89
|
+
new_content, replacements = pattern.subn(replace_with, content)
|
|
90
|
+
else:
|
|
91
|
+
new_content, replacements = pattern.subn(replace_with, content, count=count)
|
|
92
|
+
except re.error as e:
|
|
93
|
+
raise ValueError(f"Invalid regex: {e}") from e
|
|
94
|
+
else:
|
|
95
|
+
if count == 0:
|
|
96
|
+
replacements = content.count(find)
|
|
97
|
+
new_content = content.replace(find, replace_with)
|
|
98
|
+
else:
|
|
99
|
+
new_content = content.replace(find, replace_with, count)
|
|
100
|
+
replacements = min(content.count(find), count)
|
|
101
|
+
|
|
102
|
+
# Preserve line structure
|
|
103
|
+
if new_content and not new_content.endswith("\n") and lines and lines[-1].endswith("\n"):
|
|
104
|
+
new_content += "\n"
|
|
105
|
+
|
|
106
|
+
new_lines = new_content.splitlines(keepends=True)
|
|
107
|
+
return new_lines, replacements
|
|
108
|
+
|
|
109
|
+
def _replace_lines(self, lines: list[str], start: int, end: int, new_content: str) -> list[str]:
|
|
110
|
+
"""Replace lines in range with new content."""
|
|
111
|
+
# Convert to 0-indexed
|
|
112
|
+
start_idx = max(0, start - 1)
|
|
113
|
+
end_idx = min(len(lines), end)
|
|
114
|
+
|
|
115
|
+
# Ensure new content ends with newline
|
|
116
|
+
if new_content and not new_content.endswith("\n"):
|
|
117
|
+
new_content += "\n"
|
|
118
|
+
|
|
119
|
+
new_lines = new_content.splitlines(keepends=True) if new_content else []
|
|
120
|
+
|
|
121
|
+
return lines[:start_idx] + new_lines + lines[end_idx:]
|
|
122
|
+
|
|
123
|
+
def _insert_lines(self, lines: list[str], at_line: int, content: str) -> list[str]:
|
|
124
|
+
"""Insert content at a specific line."""
|
|
125
|
+
# Convert to 0-indexed
|
|
126
|
+
insert_idx = max(0, min(len(lines), at_line - 1))
|
|
127
|
+
|
|
128
|
+
# Ensure content ends with newline
|
|
129
|
+
if content and not content.endswith("\n"):
|
|
130
|
+
content += "\n"
|
|
131
|
+
|
|
132
|
+
new_lines = content.splitlines(keepends=True) if content else []
|
|
133
|
+
|
|
134
|
+
return lines[:insert_idx] + new_lines + lines[insert_idx:]
|
|
135
|
+
|
|
136
|
+
def _delete_lines(self, lines: list[str], start: int, end: int) -> list[str]:
|
|
137
|
+
"""Delete lines in range."""
|
|
138
|
+
start_idx = max(0, start - 1)
|
|
139
|
+
end_idx = min(len(lines), end)
|
|
140
|
+
|
|
141
|
+
return lines[:start_idx] + lines[end_idx:]
|
|
142
|
+
|
|
143
|
+
def run(self, arguments: dict[str, Any]) -> Any:
|
|
144
|
+
action = arguments.get("action")
|
|
145
|
+
path = arguments.get("path")
|
|
146
|
+
find = arguments.get("find", "")
|
|
147
|
+
replace_with = arguments.get("replace_with", "")
|
|
148
|
+
line_start = arguments.get("line_start", 0)
|
|
149
|
+
line_end = arguments.get("line_end", 0)
|
|
150
|
+
content = arguments.get("content", "")
|
|
151
|
+
regex = arguments.get("regex", False)
|
|
152
|
+
count = arguments.get("count", 1)
|
|
153
|
+
dry_run = arguments.get("dry_run", False)
|
|
154
|
+
|
|
155
|
+
if not path:
|
|
156
|
+
return "Error: Path not provided."
|
|
157
|
+
|
|
158
|
+
if not os.path.isfile(path):
|
|
159
|
+
return f"Error: File '{path}' not found."
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
original_lines = self._read_file(path)
|
|
163
|
+
modified_lines = original_lines.copy()
|
|
164
|
+
change_summary = ""
|
|
165
|
+
|
|
166
|
+
if action == "replace":
|
|
167
|
+
if not find:
|
|
168
|
+
return "Error: 'find' parameter required for replace action."
|
|
169
|
+
|
|
170
|
+
modified_lines, num_replacements = self._replace_text(
|
|
171
|
+
modified_lines, find, replace_with, regex, count
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
if num_replacements == 0:
|
|
175
|
+
return f"No matches found for: {find}"
|
|
176
|
+
|
|
177
|
+
change_summary = f"Replaced {num_replacements} occurrence(s)"
|
|
178
|
+
|
|
179
|
+
elif action == "replace_lines":
|
|
180
|
+
if line_start <= 0:
|
|
181
|
+
return "Error: line_start must be >= 1"
|
|
182
|
+
if line_end <= 0:
|
|
183
|
+
line_end = line_start # Replace single line
|
|
184
|
+
|
|
185
|
+
if line_start > len(original_lines):
|
|
186
|
+
return f"Error: line_start ({line_start}) exceeds file length ({len(original_lines)})"
|
|
187
|
+
|
|
188
|
+
modified_lines = self._replace_lines(modified_lines, line_start, line_end, content)
|
|
189
|
+
change_summary = f"Replaced lines {line_start}-{line_end}"
|
|
190
|
+
|
|
191
|
+
elif action == "insert":
|
|
192
|
+
if line_start <= 0:
|
|
193
|
+
return "Error: line_start must be >= 1"
|
|
194
|
+
if not content:
|
|
195
|
+
return "Error: 'content' parameter required for insert action."
|
|
196
|
+
|
|
197
|
+
modified_lines = self._insert_lines(modified_lines, line_start, content)
|
|
198
|
+
inserted_count = len(content.splitlines())
|
|
199
|
+
change_summary = f"Inserted {inserted_count} line(s) at line {line_start}"
|
|
200
|
+
|
|
201
|
+
elif action == "delete":
|
|
202
|
+
if line_start <= 0:
|
|
203
|
+
return "Error: line_start must be >= 1"
|
|
204
|
+
if line_end <= 0:
|
|
205
|
+
line_end = line_start # Delete single line
|
|
206
|
+
|
|
207
|
+
if line_start > len(original_lines):
|
|
208
|
+
return f"Error: line_start ({line_start}) exceeds file length ({len(original_lines)})"
|
|
209
|
+
|
|
210
|
+
modified_lines = self._delete_lines(modified_lines, line_start, line_end)
|
|
211
|
+
deleted_count = min(line_end, len(original_lines)) - line_start + 1
|
|
212
|
+
change_summary = f"Deleted {deleted_count} line(s) ({line_start}-{line_end})"
|
|
213
|
+
|
|
214
|
+
elif action == "patch":
|
|
215
|
+
# Apply a unified diff patch (more complex, basic implementation)
|
|
216
|
+
return "Error: Patch action not yet implemented. Use replace_lines instead."
|
|
217
|
+
|
|
218
|
+
else:
|
|
219
|
+
return f"Error: Unknown action '{action}'"
|
|
220
|
+
|
|
221
|
+
# Generate diff
|
|
222
|
+
diff = self._generate_diff(original_lines, modified_lines, path)
|
|
223
|
+
|
|
224
|
+
if not diff:
|
|
225
|
+
return "No changes needed."
|
|
226
|
+
|
|
227
|
+
if dry_run:
|
|
228
|
+
return f"[DRY RUN] {change_summary}\n\nPreview:\n{diff}"
|
|
229
|
+
|
|
230
|
+
# Apply changes
|
|
231
|
+
self._write_file(path, modified_lines)
|
|
232
|
+
|
|
233
|
+
return f"{change_summary}\n\nDiff:\n{diff}"
|
|
234
|
+
|
|
235
|
+
except Exception as e:
|
|
236
|
+
return f"Edit error: {str(e)}"
|
tools/filesystem.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Any, Literal
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
from tools.base import Tool
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FilesystemToolSchema(BaseModel):
|
|
10
|
+
action: Literal["read", "write", "list", "read_lines", "info"] = Field(
|
|
11
|
+
..., description="Action: read (full file), read_lines (specific lines), write, list, info"
|
|
12
|
+
)
|
|
13
|
+
path: str = Field(..., description="Path to the file or directory.")
|
|
14
|
+
content: str = Field(default="", description="Content to write (only for 'write' action).")
|
|
15
|
+
start_line: int = Field(default=1, description="Start line number for read_lines (1-indexed).")
|
|
16
|
+
end_line: int = Field(
|
|
17
|
+
default=0, description="End line number for read_lines (0 = to end of file)."
|
|
18
|
+
)
|
|
19
|
+
max_lines: int = Field(default=200, description="Maximum lines to return for read action.")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FilesystemTool(Tool):
|
|
23
|
+
name = "filesystem"
|
|
24
|
+
description = """Read, write, or list files and directories with line-specific control.
|
|
25
|
+
|
|
26
|
+
Actions:
|
|
27
|
+
- read: Read file content (with line numbers, truncated at max_lines)
|
|
28
|
+
- read_lines: Read specific line range (start_line to end_line)
|
|
29
|
+
- write: Write content to file
|
|
30
|
+
- list: List directory contents with file info
|
|
31
|
+
- info: Get file metadata (size, modified date, line count)
|
|
32
|
+
|
|
33
|
+
Examples:
|
|
34
|
+
- Read first 50 lines: action="read", path="file.py", max_lines=50
|
|
35
|
+
- Read lines 100-150: action="read_lines", path="file.py", start_line=100, end_line=150
|
|
36
|
+
- Get file info: action="info", path="file.py"
|
|
37
|
+
"""
|
|
38
|
+
args_schema = FilesystemToolSchema
|
|
39
|
+
|
|
40
|
+
def _format_size(self, size: int) -> str:
|
|
41
|
+
"""Format file size in human-readable format."""
|
|
42
|
+
for unit in ["B", "KB", "MB", "GB"]:
|
|
43
|
+
if size < 1024:
|
|
44
|
+
return f"{size:.1f}{unit}"
|
|
45
|
+
size /= 1024
|
|
46
|
+
return f"{size:.1f}TB"
|
|
47
|
+
|
|
48
|
+
def _count_lines(self, path: str) -> int:
|
|
49
|
+
"""Count lines in a file efficiently."""
|
|
50
|
+
try:
|
|
51
|
+
with open(path, encoding="utf-8", errors="ignore") as f:
|
|
52
|
+
return sum(1 for _ in f)
|
|
53
|
+
except Exception:
|
|
54
|
+
return -1
|
|
55
|
+
|
|
56
|
+
def _read_with_line_numbers(
|
|
57
|
+
self, path: str, start_line: int = 1, end_line: int = 0, max_lines: int = 200
|
|
58
|
+
) -> str:
|
|
59
|
+
"""Read file with line numbers, supporting line ranges."""
|
|
60
|
+
try:
|
|
61
|
+
with open(path, encoding="utf-8", errors="ignore") as f:
|
|
62
|
+
lines = f.readlines()
|
|
63
|
+
|
|
64
|
+
total_lines = len(lines)
|
|
65
|
+
|
|
66
|
+
# Adjust line numbers (1-indexed)
|
|
67
|
+
start_idx = max(0, start_line - 1)
|
|
68
|
+
if end_line > 0:
|
|
69
|
+
end_idx = min(total_lines, end_line)
|
|
70
|
+
else:
|
|
71
|
+
end_idx = min(start_idx + max_lines, total_lines)
|
|
72
|
+
|
|
73
|
+
# Build output with line numbers
|
|
74
|
+
output_lines = []
|
|
75
|
+
line_num_width = len(str(end_idx))
|
|
76
|
+
|
|
77
|
+
for i in range(start_idx, end_idx):
|
|
78
|
+
line_num = str(i + 1).rjust(line_num_width)
|
|
79
|
+
output_lines.append(f"{line_num} | {lines[i].rstrip()}")
|
|
80
|
+
|
|
81
|
+
# Add truncation notice if needed
|
|
82
|
+
header = f"[{path}] Lines {start_idx + 1}-{end_idx} of {total_lines}\n"
|
|
83
|
+
|
|
84
|
+
if end_idx < total_lines and end_line == 0:
|
|
85
|
+
footer = f"\n... ({total_lines - end_idx} more lines)"
|
|
86
|
+
else:
|
|
87
|
+
footer = ""
|
|
88
|
+
|
|
89
|
+
return header + "\n".join(output_lines) + footer
|
|
90
|
+
|
|
91
|
+
except Exception as e:
|
|
92
|
+
return f"Error reading file: {str(e)}"
|
|
93
|
+
|
|
94
|
+
def _list_directory(self, path: str) -> str:
|
|
95
|
+
"""List directory with file info."""
|
|
96
|
+
try:
|
|
97
|
+
items = os.listdir(path)
|
|
98
|
+
if not items:
|
|
99
|
+
return "Directory is empty."
|
|
100
|
+
|
|
101
|
+
output = []
|
|
102
|
+
dirs = []
|
|
103
|
+
files = []
|
|
104
|
+
|
|
105
|
+
for item in sorted(items):
|
|
106
|
+
item_path = os.path.join(path, item)
|
|
107
|
+
if os.path.isdir(item_path):
|
|
108
|
+
dirs.append(f" [DIR] {item}/")
|
|
109
|
+
else:
|
|
110
|
+
try:
|
|
111
|
+
size = os.path.getsize(item_path)
|
|
112
|
+
size_str = self._format_size(size)
|
|
113
|
+
files.append(f" {size_str:>8} {item}")
|
|
114
|
+
except Exception:
|
|
115
|
+
files.append(f" {'?':>8} {item}")
|
|
116
|
+
|
|
117
|
+
if dirs:
|
|
118
|
+
output.append("Directories:")
|
|
119
|
+
output.extend(dirs)
|
|
120
|
+
if files:
|
|
121
|
+
if dirs:
|
|
122
|
+
output.append("")
|
|
123
|
+
output.append("Files:")
|
|
124
|
+
output.extend(files)
|
|
125
|
+
|
|
126
|
+
return "\n".join(output)
|
|
127
|
+
|
|
128
|
+
except Exception as e:
|
|
129
|
+
return f"Error listing directory: {str(e)}"
|
|
130
|
+
|
|
131
|
+
def _get_file_info(self, path: str) -> str:
|
|
132
|
+
"""Get file metadata."""
|
|
133
|
+
try:
|
|
134
|
+
stat = os.stat(path)
|
|
135
|
+
size = self._format_size(stat.st_size)
|
|
136
|
+
modified = os.path.getmtime(path)
|
|
137
|
+
|
|
138
|
+
from datetime import datetime
|
|
139
|
+
|
|
140
|
+
mod_date = datetime.fromtimestamp(modified).strftime("%Y-%m-%d %H:%M:%S")
|
|
141
|
+
|
|
142
|
+
info = [
|
|
143
|
+
f"Path: {os.path.abspath(path)}",
|
|
144
|
+
f"Size: {size} ({stat.st_size} bytes)",
|
|
145
|
+
f"Modified: {mod_date}",
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
# Add line count for text files
|
|
149
|
+
ext = os.path.splitext(path)[1].lower()
|
|
150
|
+
text_exts = {
|
|
151
|
+
".py",
|
|
152
|
+
".js",
|
|
153
|
+
".ts",
|
|
154
|
+
".json",
|
|
155
|
+
".yaml",
|
|
156
|
+
".yml",
|
|
157
|
+
".md",
|
|
158
|
+
".txt",
|
|
159
|
+
".html",
|
|
160
|
+
".css",
|
|
161
|
+
}
|
|
162
|
+
if ext in text_exts or not ext:
|
|
163
|
+
line_count = self._count_lines(path)
|
|
164
|
+
if line_count >= 0:
|
|
165
|
+
info.append(f"Lines: {line_count}")
|
|
166
|
+
|
|
167
|
+
return "\n".join(info)
|
|
168
|
+
|
|
169
|
+
except Exception as e:
|
|
170
|
+
return f"Error getting file info: {str(e)}"
|
|
171
|
+
|
|
172
|
+
def run(self, arguments: dict[str, Any]) -> Any:
|
|
173
|
+
action = arguments.get("action")
|
|
174
|
+
path = arguments.get("path")
|
|
175
|
+
content = arguments.get("content", "")
|
|
176
|
+
start_line = arguments.get("start_line", 1)
|
|
177
|
+
end_line = arguments.get("end_line", 0)
|
|
178
|
+
max_lines = min(arguments.get("max_lines", 200), 500) # Cap at 500
|
|
179
|
+
|
|
180
|
+
if not path:
|
|
181
|
+
return "Error: Path not provided."
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
if action == "read":
|
|
185
|
+
if not os.path.isfile(path):
|
|
186
|
+
return f"Error: File '{path}' not found."
|
|
187
|
+
return self._read_with_line_numbers(path, max_lines=max_lines)
|
|
188
|
+
|
|
189
|
+
elif action == "read_lines":
|
|
190
|
+
if not os.path.isfile(path):
|
|
191
|
+
return f"Error: File '{path}' not found."
|
|
192
|
+
return self._read_with_line_numbers(path, start_line, end_line, max_lines=500)
|
|
193
|
+
|
|
194
|
+
elif action == "write":
|
|
195
|
+
# Create directory if needed
|
|
196
|
+
dir_path = os.path.dirname(path)
|
|
197
|
+
if dir_path and not os.path.exists(dir_path):
|
|
198
|
+
os.makedirs(dir_path)
|
|
199
|
+
|
|
200
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
201
|
+
f.write(content)
|
|
202
|
+
|
|
203
|
+
line_count = content.count("\n") + (
|
|
204
|
+
1 if content and not content.endswith("\n") else 0
|
|
205
|
+
)
|
|
206
|
+
return f"Successfully wrote {line_count} lines to {path}"
|
|
207
|
+
|
|
208
|
+
elif action == "list":
|
|
209
|
+
if not os.path.exists(path):
|
|
210
|
+
return f"Error: Path '{path}' does not exist."
|
|
211
|
+
if os.path.isfile(path):
|
|
212
|
+
return self._get_file_info(path)
|
|
213
|
+
return self._list_directory(path)
|
|
214
|
+
|
|
215
|
+
elif action == "info":
|
|
216
|
+
if not os.path.exists(path):
|
|
217
|
+
return f"Error: Path '{path}' does not exist."
|
|
218
|
+
return self._get_file_info(path)
|
|
219
|
+
|
|
220
|
+
else:
|
|
221
|
+
return f"Error: Unknown action '{action}'. Use: read, read_lines, write, list, info"
|
|
222
|
+
|
|
223
|
+
except Exception as e:
|
|
224
|
+
return f"Filesystem error: {str(e)}"
|
tools/git.py
ADDED
|
File without changes
|