cognautic-cli 1.1.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.
- cognautic/__init__.py +7 -0
- cognautic/ai_engine.py +2213 -0
- cognautic/auto_continuation.py +196 -0
- cognautic/cli.py +1064 -0
- cognautic/config.py +245 -0
- cognautic/file_tagger.py +194 -0
- cognautic/memory.py +419 -0
- cognautic/provider_endpoints.py +424 -0
- cognautic/rules.py +246 -0
- cognautic/tools/__init__.py +19 -0
- cognautic/tools/base.py +59 -0
- cognautic/tools/code_analysis.py +391 -0
- cognautic/tools/command_runner.py +292 -0
- cognautic/tools/file_operations.py +394 -0
- cognautic/tools/registry.py +115 -0
- cognautic/tools/response_control.py +48 -0
- cognautic/tools/web_search.py +336 -0
- cognautic/utils.py +297 -0
- cognautic/websocket_server.py +485 -0
- cognautic_cli-1.1.1.dist-info/METADATA +604 -0
- cognautic_cli-1.1.1.dist-info/RECORD +25 -0
- cognautic_cli-1.1.1.dist-info/WHEEL +5 -0
- cognautic_cli-1.1.1.dist-info/entry_points.txt +2 -0
- cognautic_cli-1.1.1.dist-info/licenses/LICENSE +21 -0
- cognautic_cli-1.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command runner tool for executing shell commands
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import subprocess
|
|
7
|
+
import psutil
|
|
8
|
+
from typing import List, Dict, Any, Optional
|
|
9
|
+
import signal
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
from .base import BaseTool, ToolResult, PermissionLevel
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CommandRunnerTool(BaseTool):
|
|
16
|
+
"""Tool for executing shell commands and system operations"""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
super().__init__(
|
|
20
|
+
name="command_runner",
|
|
21
|
+
description="Execute shell commands and system operations",
|
|
22
|
+
permission_level=PermissionLevel.SYSTEM_OPERATIONS
|
|
23
|
+
)
|
|
24
|
+
self.running_processes = {}
|
|
25
|
+
|
|
26
|
+
def get_capabilities(self) -> List[str]:
|
|
27
|
+
return [
|
|
28
|
+
"run_command",
|
|
29
|
+
"run_async_command",
|
|
30
|
+
"get_command_output",
|
|
31
|
+
"kill_process",
|
|
32
|
+
"check_process_status"
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
async def execute(self, operation: str, **kwargs) -> ToolResult:
|
|
36
|
+
"""Execute command operation"""
|
|
37
|
+
|
|
38
|
+
operations = {
|
|
39
|
+
'run_command': self._run_command,
|
|
40
|
+
'run_async_command': self._run_async_command,
|
|
41
|
+
'get_command_output': self._get_command_output,
|
|
42
|
+
'kill_process': self._kill_process,
|
|
43
|
+
'check_process_status': self._check_process_status
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if operation not in operations:
|
|
47
|
+
return ToolResult(
|
|
48
|
+
success=False,
|
|
49
|
+
error=f"Unknown operation: {operation}"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
result = await operations[operation](**kwargs)
|
|
54
|
+
return ToolResult(success=True, data=result)
|
|
55
|
+
except Exception as e:
|
|
56
|
+
return ToolResult(success=False, error=str(e))
|
|
57
|
+
|
|
58
|
+
async def _run_command(
|
|
59
|
+
self,
|
|
60
|
+
command: str,
|
|
61
|
+
cwd: str = None,
|
|
62
|
+
timeout: int = 300,
|
|
63
|
+
capture_output: bool = True,
|
|
64
|
+
shell: bool = True
|
|
65
|
+
) -> Dict[str, Any]:
|
|
66
|
+
"""Run a command synchronously (default timeout: 5 minutes for long-running commands)"""
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
if shell:
|
|
70
|
+
process = await asyncio.create_subprocess_shell(
|
|
71
|
+
command,
|
|
72
|
+
cwd=cwd,
|
|
73
|
+
stdout=subprocess.PIPE if capture_output else None,
|
|
74
|
+
stderr=subprocess.PIPE if capture_output else None
|
|
75
|
+
)
|
|
76
|
+
else:
|
|
77
|
+
cmd_parts = command.split()
|
|
78
|
+
process = await asyncio.create_subprocess_exec(
|
|
79
|
+
*cmd_parts,
|
|
80
|
+
cwd=cwd,
|
|
81
|
+
stdout=subprocess.PIPE if capture_output else None,
|
|
82
|
+
stderr=subprocess.PIPE if capture_output else None
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
stdout, stderr = await asyncio.wait_for(
|
|
87
|
+
process.communicate(),
|
|
88
|
+
timeout=timeout
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
'command': command,
|
|
93
|
+
'return_code': process.returncode,
|
|
94
|
+
'stdout': stdout.decode('utf-8') if stdout else '',
|
|
95
|
+
'stderr': stderr.decode('utf-8') if stderr else '',
|
|
96
|
+
'success': process.returncode == 0
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
except asyncio.TimeoutError:
|
|
100
|
+
process.kill()
|
|
101
|
+
await process.wait()
|
|
102
|
+
raise TimeoutError(f"Command timed out after {timeout} seconds")
|
|
103
|
+
|
|
104
|
+
except Exception as e:
|
|
105
|
+
return {
|
|
106
|
+
'command': command,
|
|
107
|
+
'return_code': -1,
|
|
108
|
+
'stdout': '',
|
|
109
|
+
'stderr': str(e),
|
|
110
|
+
'success': False,
|
|
111
|
+
'error': str(e)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async def _run_async_command(
|
|
115
|
+
self,
|
|
116
|
+
command: str,
|
|
117
|
+
cwd: str = None,
|
|
118
|
+
shell: bool = True
|
|
119
|
+
) -> Dict[str, Any]:
|
|
120
|
+
"""Run a command asynchronously and return process ID"""
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
if shell:
|
|
124
|
+
process = await asyncio.create_subprocess_shell(
|
|
125
|
+
command,
|
|
126
|
+
cwd=cwd,
|
|
127
|
+
stdout=subprocess.PIPE,
|
|
128
|
+
stderr=subprocess.PIPE
|
|
129
|
+
)
|
|
130
|
+
else:
|
|
131
|
+
cmd_parts = command.split()
|
|
132
|
+
process = await asyncio.create_subprocess_exec(
|
|
133
|
+
*cmd_parts,
|
|
134
|
+
cwd=cwd,
|
|
135
|
+
stdout=subprocess.PIPE,
|
|
136
|
+
stderr=subprocess.PIPE
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
process_id = str(process.pid)
|
|
140
|
+
self.running_processes[process_id] = {
|
|
141
|
+
'process': process,
|
|
142
|
+
'command': command,
|
|
143
|
+
'cwd': cwd,
|
|
144
|
+
'started_at': asyncio.get_event_loop().time()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
'process_id': process_id,
|
|
149
|
+
'command': command,
|
|
150
|
+
'pid': process.pid,
|
|
151
|
+
'status': 'running'
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
except Exception as e:
|
|
155
|
+
raise Exception(f"Failed to start async command: {str(e)}")
|
|
156
|
+
|
|
157
|
+
async def _get_command_output(self, process_id: str) -> Dict[str, Any]:
|
|
158
|
+
"""Get output from an async command"""
|
|
159
|
+
|
|
160
|
+
if process_id not in self.running_processes:
|
|
161
|
+
raise ValueError(f"Process ID not found: {process_id}")
|
|
162
|
+
|
|
163
|
+
process_info = self.running_processes[process_id]
|
|
164
|
+
process = process_info['process']
|
|
165
|
+
|
|
166
|
+
# Check if process is still running
|
|
167
|
+
if process.returncode is None:
|
|
168
|
+
# Process is still running
|
|
169
|
+
return {
|
|
170
|
+
'process_id': process_id,
|
|
171
|
+
'status': 'running',
|
|
172
|
+
'return_code': None,
|
|
173
|
+
'stdout': '',
|
|
174
|
+
'stderr': '',
|
|
175
|
+
'finished': False
|
|
176
|
+
}
|
|
177
|
+
else:
|
|
178
|
+
# Process has finished
|
|
179
|
+
try:
|
|
180
|
+
stdout, stderr = await process.communicate()
|
|
181
|
+
|
|
182
|
+
result = {
|
|
183
|
+
'process_id': process_id,
|
|
184
|
+
'status': 'finished',
|
|
185
|
+
'return_code': process.returncode,
|
|
186
|
+
'stdout': stdout.decode('utf-8') if stdout else '',
|
|
187
|
+
'stderr': stderr.decode('utf-8') if stderr else '',
|
|
188
|
+
'finished': True,
|
|
189
|
+
'success': process.returncode == 0
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
# Clean up
|
|
193
|
+
del self.running_processes[process_id]
|
|
194
|
+
return result
|
|
195
|
+
|
|
196
|
+
except Exception as e:
|
|
197
|
+
return {
|
|
198
|
+
'process_id': process_id,
|
|
199
|
+
'status': 'error',
|
|
200
|
+
'error': str(e),
|
|
201
|
+
'finished': True,
|
|
202
|
+
'success': False
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async def _kill_process(self, process_id: str, force: bool = False) -> Dict[str, Any]:
|
|
206
|
+
"""Kill a running process"""
|
|
207
|
+
|
|
208
|
+
if process_id not in self.running_processes:
|
|
209
|
+
raise ValueError(f"Process ID not found: {process_id}")
|
|
210
|
+
|
|
211
|
+
process_info = self.running_processes[process_id]
|
|
212
|
+
process = process_info['process']
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
if force:
|
|
216
|
+
process.kill()
|
|
217
|
+
else:
|
|
218
|
+
process.terminate()
|
|
219
|
+
|
|
220
|
+
# Wait for process to finish
|
|
221
|
+
await asyncio.wait_for(process.wait(), timeout=5.0)
|
|
222
|
+
|
|
223
|
+
# Clean up
|
|
224
|
+
del self.running_processes[process_id]
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
'process_id': process_id,
|
|
228
|
+
'killed': True,
|
|
229
|
+
'method': 'force' if force else 'terminate'
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
except asyncio.TimeoutError:
|
|
233
|
+
# Force kill if terminate didn't work
|
|
234
|
+
process.kill()
|
|
235
|
+
await process.wait()
|
|
236
|
+
del self.running_processes[process_id]
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
'process_id': process_id,
|
|
240
|
+
'killed': True,
|
|
241
|
+
'method': 'force (after timeout)'
|
|
242
|
+
}
|
|
243
|
+
except Exception as e:
|
|
244
|
+
return {
|
|
245
|
+
'process_id': process_id,
|
|
246
|
+
'killed': False,
|
|
247
|
+
'error': str(e)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async def _check_process_status(self, process_id: str = None) -> Dict[str, Any]:
|
|
251
|
+
"""Check status of running processes"""
|
|
252
|
+
|
|
253
|
+
if process_id:
|
|
254
|
+
# Check specific process
|
|
255
|
+
if process_id not in self.running_processes:
|
|
256
|
+
return {
|
|
257
|
+
'process_id': process_id,
|
|
258
|
+
'found': False
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
process_info = self.running_processes[process_id]
|
|
262
|
+
process = process_info['process']
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
'process_id': process_id,
|
|
266
|
+
'found': True,
|
|
267
|
+
'command': process_info['command'],
|
|
268
|
+
'pid': process.pid,
|
|
269
|
+
'status': 'running' if process.returncode is None else 'finished',
|
|
270
|
+
'return_code': process.returncode,
|
|
271
|
+
'started_at': process_info['started_at'],
|
|
272
|
+
'running_time': asyncio.get_event_loop().time() - process_info['started_at']
|
|
273
|
+
}
|
|
274
|
+
else:
|
|
275
|
+
# Check all processes
|
|
276
|
+
processes = []
|
|
277
|
+
for pid, info in self.running_processes.items():
|
|
278
|
+
process = info['process']
|
|
279
|
+
processes.append({
|
|
280
|
+
'process_id': pid,
|
|
281
|
+
'command': info['command'],
|
|
282
|
+
'pid': process.pid,
|
|
283
|
+
'status': 'running' if process.returncode is None else 'finished',
|
|
284
|
+
'return_code': process.returncode,
|
|
285
|
+
'started_at': info['started_at'],
|
|
286
|
+
'running_time': asyncio.get_event_loop().time() - info['started_at']
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
'total_processes': len(processes),
|
|
291
|
+
'processes': processes
|
|
292
|
+
}
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File operations tool for Cognautic CLI
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List, Dict, Any
|
|
9
|
+
import fnmatch
|
|
10
|
+
|
|
11
|
+
from .base import BaseTool, ToolResult, PermissionLevel
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FileOperationsTool(BaseTool):
|
|
15
|
+
"""Tool for file and directory operations"""
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
super().__init__(
|
|
19
|
+
name="file_operations",
|
|
20
|
+
description="Read, write, create, delete, and modify files and directories",
|
|
21
|
+
permission_level=PermissionLevel.SAFE_OPERATIONS
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def get_capabilities(self) -> List[str]:
|
|
25
|
+
return [
|
|
26
|
+
"read_file",
|
|
27
|
+
"read_file_lines",
|
|
28
|
+
"write_file",
|
|
29
|
+
"write_file_lines",
|
|
30
|
+
"create_file",
|
|
31
|
+
"create_directory",
|
|
32
|
+
"delete_file",
|
|
33
|
+
"list_directory",
|
|
34
|
+
"search_files",
|
|
35
|
+
"copy_file",
|
|
36
|
+
"move_file"
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
async def execute(self, operation: str, **kwargs) -> ToolResult:
|
|
40
|
+
"""Execute file operation"""
|
|
41
|
+
|
|
42
|
+
operations = {
|
|
43
|
+
'read_file': self._read_file,
|
|
44
|
+
'read_file_lines': self._read_file_lines,
|
|
45
|
+
'write_file': self._write_file,
|
|
46
|
+
'write_file_lines': self._write_file_lines,
|
|
47
|
+
'create_file': self._create_file,
|
|
48
|
+
'create_directory': self._create_directory,
|
|
49
|
+
'delete_file': self._delete_file,
|
|
50
|
+
'list_directory': self._list_directory,
|
|
51
|
+
'search_files': self._search_files,
|
|
52
|
+
'copy_file': self._copy_file,
|
|
53
|
+
'move_file': self._move_file
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if operation not in operations:
|
|
57
|
+
return ToolResult(
|
|
58
|
+
success=False,
|
|
59
|
+
error=f"Unknown operation: {operation}"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
result = await operations[operation](**kwargs)
|
|
64
|
+
return ToolResult(success=True, data=result)
|
|
65
|
+
except Exception as e:
|
|
66
|
+
return ToolResult(success=False, error=str(e))
|
|
67
|
+
|
|
68
|
+
async def _read_file(self, file_path: str, encoding: str = 'utf-8') -> Dict[str, Any]:
|
|
69
|
+
"""Read content from a file"""
|
|
70
|
+
path = Path(file_path)
|
|
71
|
+
if not path.exists():
|
|
72
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
73
|
+
|
|
74
|
+
if not path.is_file():
|
|
75
|
+
raise ValueError(f"Path is not a file: {file_path}")
|
|
76
|
+
|
|
77
|
+
with open(path, 'r', encoding=encoding) as f:
|
|
78
|
+
content = f.read()
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
'content': content,
|
|
82
|
+
'file_path': str(path),
|
|
83
|
+
'size': len(content)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async def _read_file_lines(self, file_path: str, start_line: int = 1, end_line: int = None, encoding: str = 'utf-8') -> Dict[str, Any]:
|
|
87
|
+
"""Read specific lines from a file (1-indexed)"""
|
|
88
|
+
path = Path(file_path)
|
|
89
|
+
if not path.exists():
|
|
90
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
91
|
+
|
|
92
|
+
if not path.is_file():
|
|
93
|
+
raise ValueError(f"Path is not a file: {file_path}")
|
|
94
|
+
|
|
95
|
+
with open(path, 'r', encoding=encoding) as f:
|
|
96
|
+
all_lines = f.readlines()
|
|
97
|
+
|
|
98
|
+
total_lines = len(all_lines)
|
|
99
|
+
|
|
100
|
+
# Validate line numbers (1-indexed)
|
|
101
|
+
if start_line < 1:
|
|
102
|
+
start_line = 1
|
|
103
|
+
if end_line is None or end_line > total_lines:
|
|
104
|
+
end_line = total_lines
|
|
105
|
+
|
|
106
|
+
# Convert to 0-indexed for slicing
|
|
107
|
+
start_idx = start_line - 1
|
|
108
|
+
end_idx = end_line
|
|
109
|
+
|
|
110
|
+
selected_lines = all_lines[start_idx:end_idx]
|
|
111
|
+
content = ''.join(selected_lines)
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
'content': content,
|
|
115
|
+
'file_path': str(path),
|
|
116
|
+
'start_line': start_line,
|
|
117
|
+
'end_line': end_line,
|
|
118
|
+
'total_lines': total_lines,
|
|
119
|
+
'lines_read': len(selected_lines)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async def _write_file(
|
|
123
|
+
self,
|
|
124
|
+
file_path: str,
|
|
125
|
+
content: str,
|
|
126
|
+
encoding: str = 'utf-8',
|
|
127
|
+
create_dirs: bool = True,
|
|
128
|
+
append: bool = False
|
|
129
|
+
) -> Dict[str, Any]:
|
|
130
|
+
"""Write content to a file (supports large files)"""
|
|
131
|
+
path = Path(file_path)
|
|
132
|
+
existed_before = path.exists()
|
|
133
|
+
|
|
134
|
+
if create_dirs:
|
|
135
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
136
|
+
|
|
137
|
+
# Handle large content by writing in chunks
|
|
138
|
+
mode = 'a' if append else 'w'
|
|
139
|
+
with open(path, mode, encoding=encoding) as f:
|
|
140
|
+
# Write in chunks to handle large files efficiently
|
|
141
|
+
chunk_size = 1024 * 1024 # 1MB chunks
|
|
142
|
+
for i in range(0, len(content), chunk_size):
|
|
143
|
+
f.write(content[i:i + chunk_size])
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
'file_path': str(path),
|
|
147
|
+
'size': path.stat().st_size,
|
|
148
|
+
'created': not existed_before
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async def _write_file_lines(
|
|
152
|
+
self,
|
|
153
|
+
file_path: str,
|
|
154
|
+
content: str,
|
|
155
|
+
start_line: int = 1,
|
|
156
|
+
end_line: int = None,
|
|
157
|
+
encoding: str = 'utf-8',
|
|
158
|
+
create_dirs: bool = True
|
|
159
|
+
) -> Dict[str, Any]:
|
|
160
|
+
"""Replace specific lines in a file (1-indexed)"""
|
|
161
|
+
path = Path(file_path)
|
|
162
|
+
|
|
163
|
+
if create_dirs:
|
|
164
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
165
|
+
|
|
166
|
+
# Read existing file if it exists
|
|
167
|
+
if path.exists():
|
|
168
|
+
with open(path, 'r', encoding=encoding) as f:
|
|
169
|
+
all_lines = f.readlines()
|
|
170
|
+
else:
|
|
171
|
+
all_lines = []
|
|
172
|
+
|
|
173
|
+
total_lines = len(all_lines)
|
|
174
|
+
|
|
175
|
+
# Validate line numbers (1-indexed)
|
|
176
|
+
if start_line < 1:
|
|
177
|
+
start_line = 1
|
|
178
|
+
if end_line is None:
|
|
179
|
+
end_line = start_line
|
|
180
|
+
|
|
181
|
+
# Convert to 0-indexed for slicing
|
|
182
|
+
start_idx = start_line - 1
|
|
183
|
+
end_idx = end_line
|
|
184
|
+
|
|
185
|
+
# Ensure content ends with newline if it doesn't
|
|
186
|
+
new_content = content
|
|
187
|
+
if new_content and not new_content.endswith('\n'):
|
|
188
|
+
new_content += '\n'
|
|
189
|
+
|
|
190
|
+
# Split new content into lines
|
|
191
|
+
new_lines = new_content.split('\n')
|
|
192
|
+
# Remove empty last line if content ended with newline
|
|
193
|
+
if new_lines and new_lines[-1] == '':
|
|
194
|
+
new_lines.pop()
|
|
195
|
+
# Add newlines back
|
|
196
|
+
new_lines = [line + '\n' for line in new_lines]
|
|
197
|
+
|
|
198
|
+
# If file is empty or we're appending beyond end
|
|
199
|
+
if start_idx >= total_lines:
|
|
200
|
+
# Pad with empty lines if needed
|
|
201
|
+
while len(all_lines) < start_idx:
|
|
202
|
+
all_lines.append('\n')
|
|
203
|
+
all_lines.extend(new_lines)
|
|
204
|
+
else:
|
|
205
|
+
# Replace the specified range
|
|
206
|
+
all_lines[start_idx:end_idx] = new_lines
|
|
207
|
+
|
|
208
|
+
# Write back to file
|
|
209
|
+
with open(path, 'w', encoding=encoding) as f:
|
|
210
|
+
f.writelines(all_lines)
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
'file_path': str(path),
|
|
214
|
+
'start_line': start_line,
|
|
215
|
+
'end_line': start_line + len(new_lines) - 1,
|
|
216
|
+
'lines_written': len(new_lines),
|
|
217
|
+
'total_lines': len(all_lines)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async def _create_file(self, file_path: str, content: str = "", encoding: str = 'utf-8') -> Dict[str, Any]:
|
|
221
|
+
"""Create a new file with content (alias for write_file)"""
|
|
222
|
+
return await self._write_file(file_path, content, encoding, create_dirs=True)
|
|
223
|
+
|
|
224
|
+
async def _create_directory(self, dir_path: str = None, path: str = None, parents: bool = True) -> Dict[str, Any]:
|
|
225
|
+
"""Create a directory (accepts either dir_path or path parameter)"""
|
|
226
|
+
# Accept both dir_path and path for flexibility
|
|
227
|
+
directory_path = dir_path or path
|
|
228
|
+
if not directory_path:
|
|
229
|
+
raise ValueError("Either 'dir_path' or 'path' parameter is required")
|
|
230
|
+
|
|
231
|
+
dir_obj = Path(directory_path)
|
|
232
|
+
dir_obj.mkdir(parents=parents, exist_ok=True)
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
'directory_path': str(dir_obj),
|
|
236
|
+
'created': True
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async def _delete_file(self, file_path: str) -> Dict[str, Any]:
|
|
240
|
+
"""Delete a file or directory"""
|
|
241
|
+
path = Path(file_path)
|
|
242
|
+
|
|
243
|
+
if not path.exists():
|
|
244
|
+
raise FileNotFoundError(f"Path not found: {file_path}")
|
|
245
|
+
|
|
246
|
+
if path.is_file():
|
|
247
|
+
path.unlink()
|
|
248
|
+
return {'deleted': str(path), 'type': 'file'}
|
|
249
|
+
elif path.is_dir():
|
|
250
|
+
shutil.rmtree(path)
|
|
251
|
+
return {'deleted': str(path), 'type': 'directory'}
|
|
252
|
+
else:
|
|
253
|
+
raise ValueError(f"Unknown path type: {file_path}")
|
|
254
|
+
|
|
255
|
+
async def _list_directory(
|
|
256
|
+
self,
|
|
257
|
+
dir_path: str,
|
|
258
|
+
recursive: bool = False,
|
|
259
|
+
include_hidden: bool = False
|
|
260
|
+
) -> List[Dict[str, Any]]:
|
|
261
|
+
"""List contents of a directory"""
|
|
262
|
+
path = Path(dir_path)
|
|
263
|
+
|
|
264
|
+
if not path.exists():
|
|
265
|
+
raise FileNotFoundError(f"Directory not found: {dir_path}")
|
|
266
|
+
|
|
267
|
+
if not path.is_dir():
|
|
268
|
+
raise ValueError(f"Path is not a directory: {dir_path}")
|
|
269
|
+
|
|
270
|
+
items = []
|
|
271
|
+
|
|
272
|
+
if recursive:
|
|
273
|
+
pattern = "**/*"
|
|
274
|
+
else:
|
|
275
|
+
pattern = "*"
|
|
276
|
+
|
|
277
|
+
for item in path.glob(pattern):
|
|
278
|
+
if not include_hidden and item.name.startswith('.'):
|
|
279
|
+
continue
|
|
280
|
+
|
|
281
|
+
item_info = {
|
|
282
|
+
'name': item.name,
|
|
283
|
+
'path': str(item),
|
|
284
|
+
'type': 'directory' if item.is_dir() else 'file',
|
|
285
|
+
'size': item.stat().st_size if item.is_file() else None,
|
|
286
|
+
'modified': item.stat().st_mtime
|
|
287
|
+
}
|
|
288
|
+
items.append(item_info)
|
|
289
|
+
|
|
290
|
+
return items
|
|
291
|
+
|
|
292
|
+
async def _search_files(
|
|
293
|
+
self,
|
|
294
|
+
search_path: str,
|
|
295
|
+
pattern: str,
|
|
296
|
+
content_search: str = None,
|
|
297
|
+
case_sensitive: bool = False
|
|
298
|
+
) -> List[Dict[str, Any]]:
|
|
299
|
+
"""Search for files by name pattern and optionally content"""
|
|
300
|
+
path = Path(search_path)
|
|
301
|
+
|
|
302
|
+
if not path.exists():
|
|
303
|
+
raise FileNotFoundError(f"Search path not found: {search_path}")
|
|
304
|
+
|
|
305
|
+
matches = []
|
|
306
|
+
|
|
307
|
+
# Search by filename pattern
|
|
308
|
+
for item in path.rglob("*"):
|
|
309
|
+
if item.is_file():
|
|
310
|
+
if fnmatch.fnmatch(item.name, pattern):
|
|
311
|
+
match_info = {
|
|
312
|
+
'path': str(item),
|
|
313
|
+
'name': item.name,
|
|
314
|
+
'size': item.stat().st_size,
|
|
315
|
+
'modified': item.stat().st_mtime,
|
|
316
|
+
'content_matches': []
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
# Search content if specified
|
|
320
|
+
if content_search:
|
|
321
|
+
try:
|
|
322
|
+
with open(item, 'r', encoding='utf-8', errors='ignore') as f:
|
|
323
|
+
content = f.read()
|
|
324
|
+
|
|
325
|
+
search_term = content_search if case_sensitive else content_search.lower()
|
|
326
|
+
search_content = content if case_sensitive else content.lower()
|
|
327
|
+
|
|
328
|
+
if search_term in search_content:
|
|
329
|
+
# Find line numbers with matches
|
|
330
|
+
lines = content.split('\n')
|
|
331
|
+
for i, line in enumerate(lines, 1):
|
|
332
|
+
line_search = line if case_sensitive else line.lower()
|
|
333
|
+
if search_term in line_search:
|
|
334
|
+
match_info['content_matches'].append({
|
|
335
|
+
'line_number': i,
|
|
336
|
+
'line_content': line.strip()
|
|
337
|
+
})
|
|
338
|
+
except Exception:
|
|
339
|
+
# Skip files that can't be read
|
|
340
|
+
continue
|
|
341
|
+
|
|
342
|
+
matches.append(match_info)
|
|
343
|
+
|
|
344
|
+
return matches
|
|
345
|
+
|
|
346
|
+
async def _copy_file(self, source: str, destination: str) -> Dict[str, Any]:
|
|
347
|
+
"""Copy a file or directory"""
|
|
348
|
+
src_path = Path(source)
|
|
349
|
+
dst_path = Path(destination)
|
|
350
|
+
|
|
351
|
+
if not src_path.exists():
|
|
352
|
+
raise FileNotFoundError(f"Source not found: {source}")
|
|
353
|
+
|
|
354
|
+
# Create destination directory if needed
|
|
355
|
+
if dst_path.suffix: # destination is a file
|
|
356
|
+
dst_path.parent.mkdir(parents=True, exist_ok=True)
|
|
357
|
+
else: # destination is a directory
|
|
358
|
+
dst_path.mkdir(parents=True, exist_ok=True)
|
|
359
|
+
|
|
360
|
+
if src_path.is_file():
|
|
361
|
+
if dst_path.is_dir():
|
|
362
|
+
dst_path = dst_path / src_path.name
|
|
363
|
+
shutil.copy2(src_path, dst_path)
|
|
364
|
+
return {'copied': str(src_path), 'to': str(dst_path), 'type': 'file'}
|
|
365
|
+
elif src_path.is_dir():
|
|
366
|
+
if dst_path.exists() and dst_path.is_dir():
|
|
367
|
+
dst_path = dst_path / src_path.name
|
|
368
|
+
shutil.copytree(src_path, dst_path)
|
|
369
|
+
return {'copied': str(src_path), 'to': str(dst_path), 'type': 'directory'}
|
|
370
|
+
else:
|
|
371
|
+
raise ValueError(f"Unknown source type: {source}")
|
|
372
|
+
|
|
373
|
+
async def _move_file(self, source: str, destination: str) -> Dict[str, Any]:
|
|
374
|
+
"""Move a file or directory"""
|
|
375
|
+
src_path = Path(source)
|
|
376
|
+
dst_path = Path(destination)
|
|
377
|
+
|
|
378
|
+
if not src_path.exists():
|
|
379
|
+
raise FileNotFoundError(f"Source not found: {source}")
|
|
380
|
+
|
|
381
|
+
# Create destination directory if needed
|
|
382
|
+
if dst_path.suffix: # destination is a file
|
|
383
|
+
dst_path.parent.mkdir(parents=True, exist_ok=True)
|
|
384
|
+
else: # destination is a directory
|
|
385
|
+
dst_path.mkdir(parents=True, exist_ok=True)
|
|
386
|
+
dst_path = dst_path / src_path.name
|
|
387
|
+
|
|
388
|
+
shutil.move(str(src_path), str(dst_path))
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
'moved': str(src_path),
|
|
392
|
+
'to': str(dst_path),
|
|
393
|
+
'type': 'directory' if dst_path.is_dir() else 'file'
|
|
394
|
+
}
|