groknroll 2.0.0__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.
- groknroll/__init__.py +36 -0
- groknroll/__main__.py +9 -0
- groknroll/agents/__init__.py +18 -0
- groknroll/agents/agent_manager.py +187 -0
- groknroll/agents/base_agent.py +118 -0
- groknroll/agents/build_agent.py +231 -0
- groknroll/agents/plan_agent.py +215 -0
- groknroll/cli/__init__.py +7 -0
- groknroll/cli/enhanced_cli.py +372 -0
- groknroll/cli/large_codebase_cli.py +413 -0
- groknroll/cli/main.py +331 -0
- groknroll/cli/rlm_commands.py +258 -0
- groknroll/clients/__init__.py +63 -0
- groknroll/clients/anthropic.py +112 -0
- groknroll/clients/azure_openai.py +142 -0
- groknroll/clients/base_lm.py +33 -0
- groknroll/clients/gemini.py +162 -0
- groknroll/clients/litellm.py +105 -0
- groknroll/clients/openai.py +129 -0
- groknroll/clients/portkey.py +94 -0
- groknroll/core/__init__.py +9 -0
- groknroll/core/agent.py +339 -0
- groknroll/core/comms_utils.py +264 -0
- groknroll/core/context.py +251 -0
- groknroll/core/exceptions.py +181 -0
- groknroll/core/large_codebase.py +564 -0
- groknroll/core/lm_handler.py +206 -0
- groknroll/core/rlm.py +446 -0
- groknroll/core/rlm_codebase.py +448 -0
- groknroll/core/rlm_integration.py +256 -0
- groknroll/core/types.py +276 -0
- groknroll/environments/__init__.py +34 -0
- groknroll/environments/base_env.py +182 -0
- groknroll/environments/constants.py +32 -0
- groknroll/environments/docker_repl.py +336 -0
- groknroll/environments/local_repl.py +388 -0
- groknroll/environments/modal_repl.py +502 -0
- groknroll/environments/prime_repl.py +588 -0
- groknroll/logger/__init__.py +4 -0
- groknroll/logger/rlm_logger.py +63 -0
- groknroll/logger/verbose.py +393 -0
- groknroll/operations/__init__.py +15 -0
- groknroll/operations/bash_ops.py +447 -0
- groknroll/operations/file_ops.py +473 -0
- groknroll/operations/git_ops.py +620 -0
- groknroll/oracle/__init__.py +11 -0
- groknroll/oracle/codebase_indexer.py +238 -0
- groknroll/oracle/oracle_agent.py +278 -0
- groknroll/setup.py +34 -0
- groknroll/storage/__init__.py +14 -0
- groknroll/storage/database.py +272 -0
- groknroll/storage/models.py +128 -0
- groknroll/utils/__init__.py +0 -0
- groknroll/utils/parsing.py +168 -0
- groknroll/utils/prompts.py +146 -0
- groknroll/utils/rlm_utils.py +19 -0
- groknroll-2.0.0.dist-info/METADATA +246 -0
- groknroll-2.0.0.dist-info/RECORD +62 -0
- groknroll-2.0.0.dist-info/WHEEL +5 -0
- groknroll-2.0.0.dist-info/entry_points.txt +3 -0
- groknroll-2.0.0.dist-info/licenses/LICENSE +21 -0
- groknroll-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File Operations Module
|
|
3
|
+
|
|
4
|
+
Provides file read, write, edit, and delete operations with safety checks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import shutil
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import List, Optional, Dict, Any
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
import difflib
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class FileEdit:
|
|
16
|
+
"""Represents a file edit operation"""
|
|
17
|
+
old_text: str
|
|
18
|
+
new_text: str
|
|
19
|
+
start_line: Optional[int] = None # 1-indexed
|
|
20
|
+
end_line: Optional[int] = None # 1-indexed
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class FileOperationResult:
|
|
25
|
+
"""Result of a file operation"""
|
|
26
|
+
success: bool
|
|
27
|
+
message: str
|
|
28
|
+
path: Optional[Path] = None
|
|
29
|
+
backup_path: Optional[Path] = None
|
|
30
|
+
diff: Optional[str] = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class FileOperations:
|
|
34
|
+
"""
|
|
35
|
+
File operations for groknroll agent
|
|
36
|
+
|
|
37
|
+
Features:
|
|
38
|
+
- Read files with encoding detection
|
|
39
|
+
- Write new files with parent directory creation
|
|
40
|
+
- Edit files with backup and diff
|
|
41
|
+
- Delete files with backup option
|
|
42
|
+
- Safety checks and validation
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, project_path: Path, create_backups: bool = True):
|
|
46
|
+
"""
|
|
47
|
+
Initialize file operations
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
project_path: Root project path
|
|
51
|
+
create_backups: Whether to create backups before modifications
|
|
52
|
+
"""
|
|
53
|
+
self.project_path = project_path.resolve()
|
|
54
|
+
self.create_backups = create_backups
|
|
55
|
+
self.backup_dir = self.project_path / ".groknroll" / "backups"
|
|
56
|
+
|
|
57
|
+
if create_backups:
|
|
58
|
+
self.backup_dir.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
|
|
60
|
+
def read_file(self, path: Path, encoding: str = "utf-8") -> FileOperationResult:
|
|
61
|
+
"""
|
|
62
|
+
Read file contents
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
path: File path (relative to project or absolute)
|
|
66
|
+
encoding: File encoding
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
FileOperationResult with file contents in message
|
|
70
|
+
"""
|
|
71
|
+
try:
|
|
72
|
+
file_path = self._resolve_path(path)
|
|
73
|
+
|
|
74
|
+
if not file_path.exists():
|
|
75
|
+
return FileOperationResult(
|
|
76
|
+
success=False,
|
|
77
|
+
message=f"File not found: {path}",
|
|
78
|
+
path=file_path
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if not file_path.is_file():
|
|
82
|
+
return FileOperationResult(
|
|
83
|
+
success=False,
|
|
84
|
+
message=f"Not a file: {path}",
|
|
85
|
+
path=file_path
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
content = file_path.read_text(encoding=encoding)
|
|
89
|
+
|
|
90
|
+
return FileOperationResult(
|
|
91
|
+
success=True,
|
|
92
|
+
message=content,
|
|
93
|
+
path=file_path
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
except UnicodeDecodeError as e:
|
|
97
|
+
return FileOperationResult(
|
|
98
|
+
success=False,
|
|
99
|
+
message=f"Encoding error: {e}. Try different encoding.",
|
|
100
|
+
path=file_path if 'file_path' in locals() else None
|
|
101
|
+
)
|
|
102
|
+
except Exception as e:
|
|
103
|
+
return FileOperationResult(
|
|
104
|
+
success=False,
|
|
105
|
+
message=f"Error reading file: {e}",
|
|
106
|
+
path=file_path if 'file_path' in locals() else None
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def write_file(
|
|
110
|
+
self,
|
|
111
|
+
path: Path,
|
|
112
|
+
content: str,
|
|
113
|
+
encoding: str = "utf-8",
|
|
114
|
+
overwrite: bool = False
|
|
115
|
+
) -> FileOperationResult:
|
|
116
|
+
"""
|
|
117
|
+
Write content to file
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
path: File path
|
|
121
|
+
content: File content
|
|
122
|
+
encoding: File encoding
|
|
123
|
+
overwrite: Allow overwriting existing files
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
FileOperationResult
|
|
127
|
+
"""
|
|
128
|
+
try:
|
|
129
|
+
file_path = self._resolve_path(path)
|
|
130
|
+
|
|
131
|
+
# Check if file exists
|
|
132
|
+
if file_path.exists() and not overwrite:
|
|
133
|
+
return FileOperationResult(
|
|
134
|
+
success=False,
|
|
135
|
+
message=f"File already exists: {path}. Use overwrite=True to replace.",
|
|
136
|
+
path=file_path
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Create backup if file exists
|
|
140
|
+
backup_path = None
|
|
141
|
+
if file_path.exists() and self.create_backups:
|
|
142
|
+
backup_path = self._create_backup(file_path)
|
|
143
|
+
|
|
144
|
+
# Create parent directories
|
|
145
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
146
|
+
|
|
147
|
+
# Write file
|
|
148
|
+
file_path.write_text(content, encoding=encoding)
|
|
149
|
+
|
|
150
|
+
return FileOperationResult(
|
|
151
|
+
success=True,
|
|
152
|
+
message=f"File written: {file_path.relative_to(self.project_path)}",
|
|
153
|
+
path=file_path,
|
|
154
|
+
backup_path=backup_path
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
except Exception as e:
|
|
158
|
+
return FileOperationResult(
|
|
159
|
+
success=False,
|
|
160
|
+
message=f"Error writing file: {e}",
|
|
161
|
+
path=file_path if 'file_path' in locals() else None
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def edit_file(
|
|
165
|
+
self,
|
|
166
|
+
path: Path,
|
|
167
|
+
edits: List[FileEdit],
|
|
168
|
+
encoding: str = "utf-8"
|
|
169
|
+
) -> FileOperationResult:
|
|
170
|
+
"""
|
|
171
|
+
Edit file with multiple edits
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
path: File path
|
|
175
|
+
edits: List of FileEdit operations
|
|
176
|
+
encoding: File encoding
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
FileOperationResult with diff
|
|
180
|
+
"""
|
|
181
|
+
try:
|
|
182
|
+
file_path = self._resolve_path(path)
|
|
183
|
+
|
|
184
|
+
if not file_path.exists():
|
|
185
|
+
return FileOperationResult(
|
|
186
|
+
success=False,
|
|
187
|
+
message=f"File not found: {path}",
|
|
188
|
+
path=file_path
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Read original content
|
|
192
|
+
original_content = file_path.read_text(encoding=encoding)
|
|
193
|
+
lines = original_content.splitlines(keepends=True)
|
|
194
|
+
|
|
195
|
+
# Apply edits
|
|
196
|
+
modified_content = original_content
|
|
197
|
+
for edit in edits:
|
|
198
|
+
if edit.start_line is not None and edit.end_line is not None:
|
|
199
|
+
# Line-based edit
|
|
200
|
+
modified_content = self._apply_line_edit(
|
|
201
|
+
modified_content, edit.old_text, edit.new_text,
|
|
202
|
+
edit.start_line, edit.end_line
|
|
203
|
+
)
|
|
204
|
+
else:
|
|
205
|
+
# String replacement edit
|
|
206
|
+
if edit.old_text not in modified_content:
|
|
207
|
+
return FileOperationResult(
|
|
208
|
+
success=False,
|
|
209
|
+
message=f"Text not found in file: {edit.old_text[:50]}...",
|
|
210
|
+
path=file_path
|
|
211
|
+
)
|
|
212
|
+
modified_content = modified_content.replace(
|
|
213
|
+
edit.old_text, edit.new_text, 1
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# Create diff
|
|
217
|
+
diff = self._create_diff(
|
|
218
|
+
original_content, modified_content, str(path)
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Create backup
|
|
222
|
+
backup_path = None
|
|
223
|
+
if self.create_backups:
|
|
224
|
+
backup_path = self._create_backup(file_path)
|
|
225
|
+
|
|
226
|
+
# Write modified content
|
|
227
|
+
file_path.write_text(modified_content, encoding=encoding)
|
|
228
|
+
|
|
229
|
+
return FileOperationResult(
|
|
230
|
+
success=True,
|
|
231
|
+
message=f"File edited: {file_path.relative_to(self.project_path)}",
|
|
232
|
+
path=file_path,
|
|
233
|
+
backup_path=backup_path,
|
|
234
|
+
diff=diff
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
except Exception as e:
|
|
238
|
+
return FileOperationResult(
|
|
239
|
+
success=False,
|
|
240
|
+
message=f"Error editing file: {e}",
|
|
241
|
+
path=file_path if 'file_path' in locals() else None
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
def delete_file(self, path: Path, backup: bool = True) -> FileOperationResult:
|
|
245
|
+
"""
|
|
246
|
+
Delete file
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
path: File path
|
|
250
|
+
backup: Create backup before deleting
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
FileOperationResult
|
|
254
|
+
"""
|
|
255
|
+
try:
|
|
256
|
+
file_path = self._resolve_path(path)
|
|
257
|
+
|
|
258
|
+
if not file_path.exists():
|
|
259
|
+
return FileOperationResult(
|
|
260
|
+
success=False,
|
|
261
|
+
message=f"File not found: {path}",
|
|
262
|
+
path=file_path
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
if not file_path.is_file():
|
|
266
|
+
return FileOperationResult(
|
|
267
|
+
success=False,
|
|
268
|
+
message=f"Not a file: {path}",
|
|
269
|
+
path=file_path
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# Create backup
|
|
273
|
+
backup_path = None
|
|
274
|
+
if backup and self.create_backups:
|
|
275
|
+
backup_path = self._create_backup(file_path)
|
|
276
|
+
|
|
277
|
+
# Delete file
|
|
278
|
+
file_path.unlink()
|
|
279
|
+
|
|
280
|
+
return FileOperationResult(
|
|
281
|
+
success=True,
|
|
282
|
+
message=f"File deleted: {file_path.relative_to(self.project_path)}",
|
|
283
|
+
path=file_path,
|
|
284
|
+
backup_path=backup_path
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
except Exception as e:
|
|
288
|
+
return FileOperationResult(
|
|
289
|
+
success=False,
|
|
290
|
+
message=f"Error deleting file: {e}",
|
|
291
|
+
path=file_path if 'file_path' in locals() else None
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
def list_files(
|
|
295
|
+
self,
|
|
296
|
+
directory: Optional[Path] = None,
|
|
297
|
+
pattern: str = "*",
|
|
298
|
+
recursive: bool = False
|
|
299
|
+
) -> FileOperationResult:
|
|
300
|
+
"""
|
|
301
|
+
List files in directory
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
directory: Directory path (defaults to project root)
|
|
305
|
+
pattern: Glob pattern
|
|
306
|
+
recursive: Recursive search
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
FileOperationResult with list of files in message
|
|
310
|
+
"""
|
|
311
|
+
try:
|
|
312
|
+
dir_path = self._resolve_path(directory) if directory else self.project_path
|
|
313
|
+
|
|
314
|
+
if not dir_path.exists():
|
|
315
|
+
return FileOperationResult(
|
|
316
|
+
success=False,
|
|
317
|
+
message=f"Directory not found: {directory}",
|
|
318
|
+
path=dir_path
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
if not dir_path.is_dir():
|
|
322
|
+
return FileOperationResult(
|
|
323
|
+
success=False,
|
|
324
|
+
message=f"Not a directory: {directory}",
|
|
325
|
+
path=dir_path
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# List files
|
|
329
|
+
if recursive:
|
|
330
|
+
files = list(dir_path.rglob(pattern))
|
|
331
|
+
else:
|
|
332
|
+
files = list(dir_path.glob(pattern))
|
|
333
|
+
|
|
334
|
+
# Filter to files only
|
|
335
|
+
files = [f for f in files if f.is_file()]
|
|
336
|
+
|
|
337
|
+
# Create relative paths
|
|
338
|
+
relative_files = [
|
|
339
|
+
str(f.relative_to(self.project_path))
|
|
340
|
+
for f in files
|
|
341
|
+
]
|
|
342
|
+
|
|
343
|
+
file_list = "\n".join(sorted(relative_files))
|
|
344
|
+
|
|
345
|
+
return FileOperationResult(
|
|
346
|
+
success=True,
|
|
347
|
+
message=file_list,
|
|
348
|
+
path=dir_path
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
except Exception as e:
|
|
352
|
+
return FileOperationResult(
|
|
353
|
+
success=False,
|
|
354
|
+
message=f"Error listing files: {e}",
|
|
355
|
+
path=dir_path if 'dir_path' in locals() else None
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
# =========================================================================
|
|
359
|
+
# Helper Methods
|
|
360
|
+
# =========================================================================
|
|
361
|
+
|
|
362
|
+
def _resolve_path(self, path: Path) -> Path:
|
|
363
|
+
"""Resolve path relative to project root"""
|
|
364
|
+
if path.is_absolute():
|
|
365
|
+
return path.resolve()
|
|
366
|
+
return (self.project_path / path).resolve()
|
|
367
|
+
|
|
368
|
+
def _create_backup(self, file_path: Path) -> Path:
|
|
369
|
+
"""Create backup of file"""
|
|
370
|
+
from datetime import datetime
|
|
371
|
+
|
|
372
|
+
# Create backup filename with timestamp
|
|
373
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
374
|
+
backup_name = f"{file_path.name}.{timestamp}.bak"
|
|
375
|
+
backup_path = self.backup_dir / backup_name
|
|
376
|
+
|
|
377
|
+
# Copy file
|
|
378
|
+
shutil.copy2(file_path, backup_path)
|
|
379
|
+
|
|
380
|
+
return backup_path
|
|
381
|
+
|
|
382
|
+
def _create_diff(
|
|
383
|
+
self,
|
|
384
|
+
original: str,
|
|
385
|
+
modified: str,
|
|
386
|
+
filename: str
|
|
387
|
+
) -> str:
|
|
388
|
+
"""Create unified diff between original and modified content"""
|
|
389
|
+
original_lines = original.splitlines(keepends=True)
|
|
390
|
+
modified_lines = modified.splitlines(keepends=True)
|
|
391
|
+
|
|
392
|
+
diff = difflib.unified_diff(
|
|
393
|
+
original_lines,
|
|
394
|
+
modified_lines,
|
|
395
|
+
fromfile=f"a/{filename}",
|
|
396
|
+
tofile=f"b/{filename}",
|
|
397
|
+
lineterm=""
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
return "".join(diff)
|
|
401
|
+
|
|
402
|
+
def _apply_line_edit(
|
|
403
|
+
self,
|
|
404
|
+
content: str,
|
|
405
|
+
old_text: str,
|
|
406
|
+
new_text: str,
|
|
407
|
+
start_line: int,
|
|
408
|
+
end_line: int
|
|
409
|
+
) -> str:
|
|
410
|
+
"""Apply line-based edit to content"""
|
|
411
|
+
lines = content.splitlines(keepends=True)
|
|
412
|
+
|
|
413
|
+
# Validate line numbers (1-indexed)
|
|
414
|
+
if start_line < 1 or end_line > len(lines):
|
|
415
|
+
raise ValueError(f"Invalid line range: {start_line}-{end_line}")
|
|
416
|
+
|
|
417
|
+
# Extract range (convert to 0-indexed)
|
|
418
|
+
range_text = "".join(lines[start_line-1:end_line])
|
|
419
|
+
|
|
420
|
+
# Verify old_text matches
|
|
421
|
+
if old_text not in range_text:
|
|
422
|
+
raise ValueError(
|
|
423
|
+
f"Text not found in specified line range: {old_text[:50]}..."
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
# Replace in range
|
|
427
|
+
modified_range = range_text.replace(old_text, new_text, 1)
|
|
428
|
+
|
|
429
|
+
# Reconstruct content
|
|
430
|
+
result_lines = (
|
|
431
|
+
lines[:start_line-1] +
|
|
432
|
+
[modified_range] +
|
|
433
|
+
lines[end_line:]
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
return "".join(result_lines)
|
|
437
|
+
|
|
438
|
+
def restore_backup(self, backup_path: Path) -> FileOperationResult:
|
|
439
|
+
"""Restore file from backup"""
|
|
440
|
+
try:
|
|
441
|
+
if not backup_path.exists():
|
|
442
|
+
return FileOperationResult(
|
|
443
|
+
success=False,
|
|
444
|
+
message=f"Backup not found: {backup_path}"
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
# Extract original filename from backup
|
|
448
|
+
# Format: filename.ext.YYYYMMDD_HHMMSS.bak
|
|
449
|
+
parts = backup_path.name.split(".")
|
|
450
|
+
if len(parts) < 3:
|
|
451
|
+
return FileOperationResult(
|
|
452
|
+
success=False,
|
|
453
|
+
message=f"Invalid backup filename: {backup_path.name}"
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
original_name = ".".join(parts[:-2]) # Remove timestamp and .bak
|
|
457
|
+
original_path = self.project_path / original_name
|
|
458
|
+
|
|
459
|
+
# Restore file
|
|
460
|
+
shutil.copy2(backup_path, original_path)
|
|
461
|
+
|
|
462
|
+
return FileOperationResult(
|
|
463
|
+
success=True,
|
|
464
|
+
message=f"File restored from backup: {original_path.relative_to(self.project_path)}",
|
|
465
|
+
path=original_path,
|
|
466
|
+
backup_path=backup_path
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
except Exception as e:
|
|
470
|
+
return FileOperationResult(
|
|
471
|
+
success=False,
|
|
472
|
+
message=f"Error restoring backup: {e}"
|
|
473
|
+
)
|