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,447 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bash Operations Module
|
|
3
|
+
|
|
4
|
+
Provides bash command execution with output capture and PTY support.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import subprocess
|
|
8
|
+
import os
|
|
9
|
+
import select
|
|
10
|
+
import pty
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional, Dict, Any, List
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
import shlex
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class BashResult:
|
|
19
|
+
"""Result of a bash command execution"""
|
|
20
|
+
success: bool
|
|
21
|
+
stdout: str
|
|
22
|
+
stderr: str
|
|
23
|
+
exit_code: int
|
|
24
|
+
command: str
|
|
25
|
+
cwd: Optional[Path] = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class BashOperations:
|
|
29
|
+
"""
|
|
30
|
+
Bash operations for groknroll agent
|
|
31
|
+
|
|
32
|
+
Features:
|
|
33
|
+
- Execute shell commands with output capture
|
|
34
|
+
- PTY support for interactive commands
|
|
35
|
+
- Working directory management
|
|
36
|
+
- Environment variable handling
|
|
37
|
+
- Command history and safety checks
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
project_path: Path,
|
|
43
|
+
default_shell: str = "/bin/bash",
|
|
44
|
+
timeout: int = 300
|
|
45
|
+
):
|
|
46
|
+
"""
|
|
47
|
+
Initialize bash operations
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
project_path: Root project path (default working directory)
|
|
51
|
+
default_shell: Shell to use for execution
|
|
52
|
+
timeout: Default command timeout in seconds
|
|
53
|
+
"""
|
|
54
|
+
self.project_path = project_path.resolve()
|
|
55
|
+
self.default_shell = default_shell
|
|
56
|
+
self.default_timeout = timeout
|
|
57
|
+
self.command_history: List[BashResult] = []
|
|
58
|
+
|
|
59
|
+
# Dangerous commands that require confirmation
|
|
60
|
+
self.dangerous_commands = {
|
|
61
|
+
"rm -rf /",
|
|
62
|
+
"dd if=",
|
|
63
|
+
"mkfs",
|
|
64
|
+
":(){ :|:& };:", # Fork bomb
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
def execute(
|
|
68
|
+
self,
|
|
69
|
+
command: str,
|
|
70
|
+
cwd: Optional[Path] = None,
|
|
71
|
+
env: Optional[Dict[str, str]] = None,
|
|
72
|
+
timeout: Optional[int] = None,
|
|
73
|
+
shell: bool = True,
|
|
74
|
+
capture_output: bool = True
|
|
75
|
+
) -> BashResult:
|
|
76
|
+
"""
|
|
77
|
+
Execute bash command
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
command: Command to execute
|
|
81
|
+
cwd: Working directory (defaults to project_path)
|
|
82
|
+
env: Environment variables (merged with os.environ)
|
|
83
|
+
timeout: Command timeout in seconds
|
|
84
|
+
shell: Execute in shell (required for pipes, redirects, etc.)
|
|
85
|
+
capture_output: Capture stdout/stderr
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
BashResult
|
|
89
|
+
"""
|
|
90
|
+
# Safety check
|
|
91
|
+
if self._is_dangerous(command):
|
|
92
|
+
return BashResult(
|
|
93
|
+
success=False,
|
|
94
|
+
stdout="",
|
|
95
|
+
stderr=f"Dangerous command blocked: {command}",
|
|
96
|
+
exit_code=-1,
|
|
97
|
+
command=command,
|
|
98
|
+
cwd=cwd
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Resolve working directory
|
|
102
|
+
work_dir = self._resolve_path(cwd) if cwd else self.project_path
|
|
103
|
+
|
|
104
|
+
# Prepare environment
|
|
105
|
+
exec_env = os.environ.copy()
|
|
106
|
+
if env:
|
|
107
|
+
exec_env.update(env)
|
|
108
|
+
|
|
109
|
+
# Set timeout
|
|
110
|
+
exec_timeout = timeout or self.default_timeout
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
# Execute command
|
|
114
|
+
if shell:
|
|
115
|
+
result = subprocess.run(
|
|
116
|
+
command,
|
|
117
|
+
shell=True,
|
|
118
|
+
cwd=str(work_dir),
|
|
119
|
+
env=exec_env,
|
|
120
|
+
capture_output=capture_output,
|
|
121
|
+
text=True,
|
|
122
|
+
timeout=exec_timeout,
|
|
123
|
+
executable=self.default_shell
|
|
124
|
+
)
|
|
125
|
+
else:
|
|
126
|
+
# Parse command into args
|
|
127
|
+
args = shlex.split(command)
|
|
128
|
+
result = subprocess.run(
|
|
129
|
+
args,
|
|
130
|
+
cwd=str(work_dir),
|
|
131
|
+
env=exec_env,
|
|
132
|
+
capture_output=capture_output,
|
|
133
|
+
text=True,
|
|
134
|
+
timeout=exec_timeout
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
bash_result = BashResult(
|
|
138
|
+
success=result.returncode == 0,
|
|
139
|
+
stdout=result.stdout if capture_output else "",
|
|
140
|
+
stderr=result.stderr if capture_output else "",
|
|
141
|
+
exit_code=result.returncode,
|
|
142
|
+
command=command,
|
|
143
|
+
cwd=work_dir
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Add to history
|
|
147
|
+
self.command_history.append(bash_result)
|
|
148
|
+
|
|
149
|
+
return bash_result
|
|
150
|
+
|
|
151
|
+
except subprocess.TimeoutExpired:
|
|
152
|
+
return BashResult(
|
|
153
|
+
success=False,
|
|
154
|
+
stdout="",
|
|
155
|
+
stderr=f"Command timed out after {exec_timeout} seconds",
|
|
156
|
+
exit_code=-1,
|
|
157
|
+
command=command,
|
|
158
|
+
cwd=work_dir
|
|
159
|
+
)
|
|
160
|
+
except Exception as e:
|
|
161
|
+
return BashResult(
|
|
162
|
+
success=False,
|
|
163
|
+
stdout="",
|
|
164
|
+
stderr=f"Error executing command: {e}",
|
|
165
|
+
exit_code=-1,
|
|
166
|
+
command=command,
|
|
167
|
+
cwd=work_dir
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def execute_interactive(
|
|
171
|
+
self,
|
|
172
|
+
command: str,
|
|
173
|
+
cwd: Optional[Path] = None,
|
|
174
|
+
env: Optional[Dict[str, str]] = None
|
|
175
|
+
) -> BashResult:
|
|
176
|
+
"""
|
|
177
|
+
Execute command with PTY (for interactive commands like vim, top, etc.)
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
command: Command to execute
|
|
181
|
+
cwd: Working directory
|
|
182
|
+
env: Environment variables
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
BashResult
|
|
186
|
+
"""
|
|
187
|
+
# Safety check
|
|
188
|
+
if self._is_dangerous(command):
|
|
189
|
+
return BashResult(
|
|
190
|
+
success=False,
|
|
191
|
+
stdout="",
|
|
192
|
+
stderr=f"Dangerous command blocked: {command}",
|
|
193
|
+
exit_code=-1,
|
|
194
|
+
command=command,
|
|
195
|
+
cwd=cwd
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Resolve working directory
|
|
199
|
+
work_dir = self._resolve_path(cwd) if cwd else self.project_path
|
|
200
|
+
|
|
201
|
+
# Prepare environment
|
|
202
|
+
exec_env = os.environ.copy()
|
|
203
|
+
if env:
|
|
204
|
+
exec_env.update(env)
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
# Create PTY
|
|
208
|
+
master, slave = pty.openpty()
|
|
209
|
+
|
|
210
|
+
# Execute command in PTY
|
|
211
|
+
process = subprocess.Popen(
|
|
212
|
+
command,
|
|
213
|
+
shell=True,
|
|
214
|
+
cwd=str(work_dir),
|
|
215
|
+
env=exec_env,
|
|
216
|
+
stdin=slave,
|
|
217
|
+
stdout=slave,
|
|
218
|
+
stderr=slave,
|
|
219
|
+
executable=self.default_shell
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Close slave (parent doesn't need it)
|
|
223
|
+
os.close(slave)
|
|
224
|
+
|
|
225
|
+
# Wait for process to complete
|
|
226
|
+
process.wait()
|
|
227
|
+
|
|
228
|
+
# Read output
|
|
229
|
+
output = b""
|
|
230
|
+
while True:
|
|
231
|
+
try:
|
|
232
|
+
# Use select to check if data is available
|
|
233
|
+
ready, _, _ = select.select([master], [], [], 0.1)
|
|
234
|
+
if ready:
|
|
235
|
+
chunk = os.read(master, 4096)
|
|
236
|
+
if not chunk:
|
|
237
|
+
break
|
|
238
|
+
output += chunk
|
|
239
|
+
else:
|
|
240
|
+
break
|
|
241
|
+
except OSError:
|
|
242
|
+
break
|
|
243
|
+
|
|
244
|
+
os.close(master)
|
|
245
|
+
|
|
246
|
+
bash_result = BashResult(
|
|
247
|
+
success=process.returncode == 0,
|
|
248
|
+
stdout=output.decode('utf-8', errors='replace'),
|
|
249
|
+
stderr="",
|
|
250
|
+
exit_code=process.returncode,
|
|
251
|
+
command=command,
|
|
252
|
+
cwd=work_dir
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
self.command_history.append(bash_result)
|
|
256
|
+
|
|
257
|
+
return bash_result
|
|
258
|
+
|
|
259
|
+
except Exception as e:
|
|
260
|
+
return BashResult(
|
|
261
|
+
success=False,
|
|
262
|
+
stdout="",
|
|
263
|
+
stderr=f"Error executing interactive command: {e}",
|
|
264
|
+
exit_code=-1,
|
|
265
|
+
command=command,
|
|
266
|
+
cwd=work_dir
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
def execute_pipeline(
|
|
270
|
+
self,
|
|
271
|
+
commands: List[str],
|
|
272
|
+
cwd: Optional[Path] = None,
|
|
273
|
+
env: Optional[Dict[str, str]] = None,
|
|
274
|
+
timeout: Optional[int] = None
|
|
275
|
+
) -> BashResult:
|
|
276
|
+
"""
|
|
277
|
+
Execute pipeline of commands (command1 | command2 | ...)
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
commands: List of commands to pipe together
|
|
281
|
+
cwd: Working directory
|
|
282
|
+
env: Environment variables
|
|
283
|
+
timeout: Command timeout
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
BashResult
|
|
287
|
+
"""
|
|
288
|
+
# Create pipeline command
|
|
289
|
+
pipeline = " | ".join(commands)
|
|
290
|
+
|
|
291
|
+
return self.execute(
|
|
292
|
+
command=pipeline,
|
|
293
|
+
cwd=cwd,
|
|
294
|
+
env=env,
|
|
295
|
+
timeout=timeout,
|
|
296
|
+
shell=True
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
def execute_script(
|
|
300
|
+
self,
|
|
301
|
+
script_path: Path,
|
|
302
|
+
args: Optional[List[str]] = None,
|
|
303
|
+
cwd: Optional[Path] = None,
|
|
304
|
+
env: Optional[Dict[str, str]] = None,
|
|
305
|
+
timeout: Optional[int] = None
|
|
306
|
+
) -> BashResult:
|
|
307
|
+
"""
|
|
308
|
+
Execute bash script
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
script_path: Path to script file
|
|
312
|
+
args: Script arguments
|
|
313
|
+
cwd: Working directory
|
|
314
|
+
env: Environment variables
|
|
315
|
+
timeout: Command timeout
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
BashResult
|
|
319
|
+
"""
|
|
320
|
+
script_file = self._resolve_path(script_path)
|
|
321
|
+
|
|
322
|
+
if not script_file.exists():
|
|
323
|
+
return BashResult(
|
|
324
|
+
success=False,
|
|
325
|
+
stdout="",
|
|
326
|
+
stderr=f"Script not found: {script_path}",
|
|
327
|
+
exit_code=-1,
|
|
328
|
+
command=f"bash {script_path}",
|
|
329
|
+
cwd=cwd
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# Build command
|
|
333
|
+
command_parts = [self.default_shell, str(script_file)]
|
|
334
|
+
if args:
|
|
335
|
+
command_parts.extend(args)
|
|
336
|
+
|
|
337
|
+
command = " ".join(shlex.quote(part) for part in command_parts)
|
|
338
|
+
|
|
339
|
+
return self.execute(
|
|
340
|
+
command=command,
|
|
341
|
+
cwd=cwd,
|
|
342
|
+
env=env,
|
|
343
|
+
timeout=timeout,
|
|
344
|
+
shell=False
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
def get_history(self, limit: int = 10) -> List[BashResult]:
|
|
348
|
+
"""Get recent command history"""
|
|
349
|
+
return self.command_history[-limit:]
|
|
350
|
+
|
|
351
|
+
def clear_history(self) -> None:
|
|
352
|
+
"""Clear command history"""
|
|
353
|
+
self.command_history.clear()
|
|
354
|
+
|
|
355
|
+
# =========================================================================
|
|
356
|
+
# Common Commands (Convenience Methods)
|
|
357
|
+
# =========================================================================
|
|
358
|
+
|
|
359
|
+
def ls(self, path: Optional[Path] = None, options: str = "-la") -> BashResult:
|
|
360
|
+
"""List directory contents"""
|
|
361
|
+
target = self._resolve_path(path) if path else self.project_path
|
|
362
|
+
return self.execute(f"ls {options} {shlex.quote(str(target))}")
|
|
363
|
+
|
|
364
|
+
def pwd(self) -> BashResult:
|
|
365
|
+
"""Print working directory"""
|
|
366
|
+
return self.execute("pwd", cwd=self.project_path)
|
|
367
|
+
|
|
368
|
+
def which(self, command: str) -> BashResult:
|
|
369
|
+
"""Find command location"""
|
|
370
|
+
return self.execute(f"which {shlex.quote(command)}")
|
|
371
|
+
|
|
372
|
+
def env_vars(self) -> BashResult:
|
|
373
|
+
"""List environment variables"""
|
|
374
|
+
return self.execute("env")
|
|
375
|
+
|
|
376
|
+
def ps(self, options: str = "aux") -> BashResult:
|
|
377
|
+
"""List processes"""
|
|
378
|
+
return self.execute(f"ps {options}")
|
|
379
|
+
|
|
380
|
+
def grep(
|
|
381
|
+
self,
|
|
382
|
+
pattern: str,
|
|
383
|
+
path: Optional[Path] = None,
|
|
384
|
+
options: str = "-r"
|
|
385
|
+
) -> BashResult:
|
|
386
|
+
"""Search for pattern in files"""
|
|
387
|
+
target = self._resolve_path(path) if path else self.project_path
|
|
388
|
+
return self.execute(
|
|
389
|
+
f"grep {options} {shlex.quote(pattern)} {shlex.quote(str(target))}"
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
def find(
|
|
393
|
+
self,
|
|
394
|
+
path: Optional[Path] = None,
|
|
395
|
+
name: Optional[str] = None,
|
|
396
|
+
type_filter: Optional[str] = None
|
|
397
|
+
) -> BashResult:
|
|
398
|
+
"""Find files"""
|
|
399
|
+
target = self._resolve_path(path) if path else self.project_path
|
|
400
|
+
command_parts = ["find", shlex.quote(str(target))]
|
|
401
|
+
|
|
402
|
+
if type_filter:
|
|
403
|
+
command_parts.extend(["-type", type_filter])
|
|
404
|
+
|
|
405
|
+
if name:
|
|
406
|
+
command_parts.extend(["-name", shlex.quote(name)])
|
|
407
|
+
|
|
408
|
+
return self.execute(" ".join(command_parts))
|
|
409
|
+
|
|
410
|
+
# =========================================================================
|
|
411
|
+
# Helper Methods
|
|
412
|
+
# =========================================================================
|
|
413
|
+
|
|
414
|
+
def _resolve_path(self, path: Path) -> Path:
|
|
415
|
+
"""Resolve path relative to project root"""
|
|
416
|
+
if path.is_absolute():
|
|
417
|
+
return path.resolve()
|
|
418
|
+
return (self.project_path / path).resolve()
|
|
419
|
+
|
|
420
|
+
def _is_dangerous(self, command: str) -> bool:
|
|
421
|
+
"""Check if command is potentially dangerous"""
|
|
422
|
+
# Check against known dangerous patterns
|
|
423
|
+
for dangerous in self.dangerous_commands:
|
|
424
|
+
if dangerous in command:
|
|
425
|
+
return True
|
|
426
|
+
|
|
427
|
+
# Additional checks
|
|
428
|
+
dangerous_patterns = [
|
|
429
|
+
"rm -rf /",
|
|
430
|
+
"> /dev/sda",
|
|
431
|
+
"chmod -R 777 /",
|
|
432
|
+
]
|
|
433
|
+
|
|
434
|
+
for pattern in dangerous_patterns:
|
|
435
|
+
if pattern in command:
|
|
436
|
+
return True
|
|
437
|
+
|
|
438
|
+
return False
|
|
439
|
+
|
|
440
|
+
def test_command_available(self, command: str) -> bool:
|
|
441
|
+
"""Test if command is available in PATH"""
|
|
442
|
+
result = self.which(command)
|
|
443
|
+
return result.success
|
|
444
|
+
|
|
445
|
+
def get_shell_version(self) -> BashResult:
|
|
446
|
+
"""Get shell version"""
|
|
447
|
+
return self.execute(f"{self.default_shell} --version")
|