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.
Files changed (62) hide show
  1. groknroll/__init__.py +36 -0
  2. groknroll/__main__.py +9 -0
  3. groknroll/agents/__init__.py +18 -0
  4. groknroll/agents/agent_manager.py +187 -0
  5. groknroll/agents/base_agent.py +118 -0
  6. groknroll/agents/build_agent.py +231 -0
  7. groknroll/agents/plan_agent.py +215 -0
  8. groknroll/cli/__init__.py +7 -0
  9. groknroll/cli/enhanced_cli.py +372 -0
  10. groknroll/cli/large_codebase_cli.py +413 -0
  11. groknroll/cli/main.py +331 -0
  12. groknroll/cli/rlm_commands.py +258 -0
  13. groknroll/clients/__init__.py +63 -0
  14. groknroll/clients/anthropic.py +112 -0
  15. groknroll/clients/azure_openai.py +142 -0
  16. groknroll/clients/base_lm.py +33 -0
  17. groknroll/clients/gemini.py +162 -0
  18. groknroll/clients/litellm.py +105 -0
  19. groknroll/clients/openai.py +129 -0
  20. groknroll/clients/portkey.py +94 -0
  21. groknroll/core/__init__.py +9 -0
  22. groknroll/core/agent.py +339 -0
  23. groknroll/core/comms_utils.py +264 -0
  24. groknroll/core/context.py +251 -0
  25. groknroll/core/exceptions.py +181 -0
  26. groknroll/core/large_codebase.py +564 -0
  27. groknroll/core/lm_handler.py +206 -0
  28. groknroll/core/rlm.py +446 -0
  29. groknroll/core/rlm_codebase.py +448 -0
  30. groknroll/core/rlm_integration.py +256 -0
  31. groknroll/core/types.py +276 -0
  32. groknroll/environments/__init__.py +34 -0
  33. groknroll/environments/base_env.py +182 -0
  34. groknroll/environments/constants.py +32 -0
  35. groknroll/environments/docker_repl.py +336 -0
  36. groknroll/environments/local_repl.py +388 -0
  37. groknroll/environments/modal_repl.py +502 -0
  38. groknroll/environments/prime_repl.py +588 -0
  39. groknroll/logger/__init__.py +4 -0
  40. groknroll/logger/rlm_logger.py +63 -0
  41. groknroll/logger/verbose.py +393 -0
  42. groknroll/operations/__init__.py +15 -0
  43. groknroll/operations/bash_ops.py +447 -0
  44. groknroll/operations/file_ops.py +473 -0
  45. groknroll/operations/git_ops.py +620 -0
  46. groknroll/oracle/__init__.py +11 -0
  47. groknroll/oracle/codebase_indexer.py +238 -0
  48. groknroll/oracle/oracle_agent.py +278 -0
  49. groknroll/setup.py +34 -0
  50. groknroll/storage/__init__.py +14 -0
  51. groknroll/storage/database.py +272 -0
  52. groknroll/storage/models.py +128 -0
  53. groknroll/utils/__init__.py +0 -0
  54. groknroll/utils/parsing.py +168 -0
  55. groknroll/utils/prompts.py +146 -0
  56. groknroll/utils/rlm_utils.py +19 -0
  57. groknroll-2.0.0.dist-info/METADATA +246 -0
  58. groknroll-2.0.0.dist-info/RECORD +62 -0
  59. groknroll-2.0.0.dist-info/WHEEL +5 -0
  60. groknroll-2.0.0.dist-info/entry_points.txt +3 -0
  61. groknroll-2.0.0.dist-info/licenses/LICENSE +21 -0
  62. 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
+ )