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,620 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Git Operations Module
|
|
3
|
+
|
|
4
|
+
Provides git operations with smart commit messages and PR creation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import subprocess
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional, List, Dict, Any
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class GitStatus:
|
|
16
|
+
"""Git repository status"""
|
|
17
|
+
branch: str
|
|
18
|
+
staged: List[str]
|
|
19
|
+
unstaged: List[str]
|
|
20
|
+
untracked: List[str]
|
|
21
|
+
is_clean: bool
|
|
22
|
+
ahead: int
|
|
23
|
+
behind: int
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class GitResult:
|
|
28
|
+
"""Result of a git operation"""
|
|
29
|
+
success: bool
|
|
30
|
+
message: str
|
|
31
|
+
output: Optional[str] = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class GitOperations:
|
|
35
|
+
"""
|
|
36
|
+
Git operations for groknroll agent
|
|
37
|
+
|
|
38
|
+
Features:
|
|
39
|
+
- Git status and info
|
|
40
|
+
- Stage, commit, push
|
|
41
|
+
- Smart commit message generation
|
|
42
|
+
- PR creation (via gh CLI)
|
|
43
|
+
- Branch management
|
|
44
|
+
- Git history
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, project_path: Path):
|
|
48
|
+
"""
|
|
49
|
+
Initialize git operations
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
project_path: Git repository root
|
|
53
|
+
"""
|
|
54
|
+
self.project_path = project_path.resolve()
|
|
55
|
+
|
|
56
|
+
# Verify git is available
|
|
57
|
+
if not self._is_git_available():
|
|
58
|
+
raise RuntimeError("Git is not installed or not in PATH")
|
|
59
|
+
|
|
60
|
+
# Verify this is a git repo
|
|
61
|
+
if not self._is_git_repo():
|
|
62
|
+
raise RuntimeError(f"Not a git repository: {project_path}")
|
|
63
|
+
|
|
64
|
+
def status(self) -> GitStatus:
|
|
65
|
+
"""
|
|
66
|
+
Get git status
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
GitStatus with current repository state
|
|
70
|
+
"""
|
|
71
|
+
# Get current branch
|
|
72
|
+
branch_result = self._run_git(["branch", "--show-current"])
|
|
73
|
+
branch = branch_result.strip() if branch_result else "unknown"
|
|
74
|
+
|
|
75
|
+
# Get status
|
|
76
|
+
status_result = self._run_git(["status", "--porcelain"])
|
|
77
|
+
staged = []
|
|
78
|
+
unstaged = []
|
|
79
|
+
untracked = []
|
|
80
|
+
|
|
81
|
+
for line in status_result.splitlines():
|
|
82
|
+
if not line:
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
status_code = line[:2]
|
|
86
|
+
filepath = line[3:]
|
|
87
|
+
|
|
88
|
+
if status_code[0] in ("M", "A", "D", "R", "C"):
|
|
89
|
+
staged.append(filepath)
|
|
90
|
+
if status_code[1] in ("M", "D"):
|
|
91
|
+
unstaged.append(filepath)
|
|
92
|
+
if status_code == "??":
|
|
93
|
+
untracked.append(filepath)
|
|
94
|
+
|
|
95
|
+
# Get ahead/behind status
|
|
96
|
+
ahead, behind = self._get_ahead_behind()
|
|
97
|
+
|
|
98
|
+
return GitStatus(
|
|
99
|
+
branch=branch,
|
|
100
|
+
staged=staged,
|
|
101
|
+
unstaged=unstaged,
|
|
102
|
+
untracked=untracked,
|
|
103
|
+
is_clean=not (staged or unstaged or untracked),
|
|
104
|
+
ahead=ahead,
|
|
105
|
+
behind=behind
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def add(self, files: Optional[List[Path]] = None, all_files: bool = False) -> GitResult:
|
|
109
|
+
"""
|
|
110
|
+
Stage files
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
files: List of files to stage (or None with all_files=True)
|
|
114
|
+
all_files: Stage all changes
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
GitResult
|
|
118
|
+
"""
|
|
119
|
+
try:
|
|
120
|
+
if all_files:
|
|
121
|
+
self._run_git(["add", "-A"])
|
|
122
|
+
return GitResult(
|
|
123
|
+
success=True,
|
|
124
|
+
message="All changes staged"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
if not files:
|
|
128
|
+
return GitResult(
|
|
129
|
+
success=False,
|
|
130
|
+
message="No files specified and all_files=False"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Stage specific files
|
|
134
|
+
file_paths = [str(f) for f in files]
|
|
135
|
+
self._run_git(["add"] + file_paths)
|
|
136
|
+
|
|
137
|
+
return GitResult(
|
|
138
|
+
success=True,
|
|
139
|
+
message=f"Staged {len(files)} file(s)"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
except Exception as e:
|
|
143
|
+
return GitResult(
|
|
144
|
+
success=False,
|
|
145
|
+
message=f"Error staging files: {e}"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def commit(
|
|
149
|
+
self,
|
|
150
|
+
message: str,
|
|
151
|
+
files: Optional[List[Path]] = None,
|
|
152
|
+
amend: bool = False
|
|
153
|
+
) -> GitResult:
|
|
154
|
+
"""
|
|
155
|
+
Create commit
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
message: Commit message
|
|
159
|
+
files: Files to commit (or all staged if None)
|
|
160
|
+
amend: Amend previous commit
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
GitResult
|
|
164
|
+
"""
|
|
165
|
+
try:
|
|
166
|
+
# Stage files if provided
|
|
167
|
+
if files:
|
|
168
|
+
self.add(files)
|
|
169
|
+
|
|
170
|
+
# Build commit command
|
|
171
|
+
cmd = ["commit", "-m", message]
|
|
172
|
+
if amend:
|
|
173
|
+
cmd.append("--amend")
|
|
174
|
+
|
|
175
|
+
output = self._run_git(cmd)
|
|
176
|
+
|
|
177
|
+
return GitResult(
|
|
178
|
+
success=True,
|
|
179
|
+
message=f"Commit created: {message[:50]}...",
|
|
180
|
+
output=output
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
except Exception as e:
|
|
184
|
+
return GitResult(
|
|
185
|
+
success=False,
|
|
186
|
+
message=f"Error creating commit: {e}"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def push(
|
|
190
|
+
self,
|
|
191
|
+
remote: str = "origin",
|
|
192
|
+
branch: Optional[str] = None,
|
|
193
|
+
set_upstream: bool = False,
|
|
194
|
+
force: bool = False
|
|
195
|
+
) -> GitResult:
|
|
196
|
+
"""
|
|
197
|
+
Push to remote
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
remote: Remote name
|
|
201
|
+
branch: Branch name (current branch if None)
|
|
202
|
+
set_upstream: Set upstream tracking
|
|
203
|
+
force: Force push (use with caution!)
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
GitResult
|
|
207
|
+
"""
|
|
208
|
+
try:
|
|
209
|
+
# Get current branch if not specified
|
|
210
|
+
if branch is None:
|
|
211
|
+
status = self.status()
|
|
212
|
+
branch = status.branch
|
|
213
|
+
|
|
214
|
+
# Build push command
|
|
215
|
+
cmd = ["push"]
|
|
216
|
+
if set_upstream:
|
|
217
|
+
cmd.extend(["-u", remote, branch])
|
|
218
|
+
else:
|
|
219
|
+
cmd.extend([remote, branch])
|
|
220
|
+
|
|
221
|
+
if force:
|
|
222
|
+
cmd.append("--force")
|
|
223
|
+
|
|
224
|
+
output = self._run_git(cmd)
|
|
225
|
+
|
|
226
|
+
return GitResult(
|
|
227
|
+
success=True,
|
|
228
|
+
message=f"Pushed to {remote}/{branch}",
|
|
229
|
+
output=output
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
except Exception as e:
|
|
233
|
+
return GitResult(
|
|
234
|
+
success=False,
|
|
235
|
+
message=f"Error pushing: {e}"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
def pull(
|
|
239
|
+
self,
|
|
240
|
+
remote: str = "origin",
|
|
241
|
+
branch: Optional[str] = None,
|
|
242
|
+
rebase: bool = False
|
|
243
|
+
) -> GitResult:
|
|
244
|
+
"""
|
|
245
|
+
Pull from remote
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
remote: Remote name
|
|
249
|
+
branch: Branch name
|
|
250
|
+
rebase: Use rebase instead of merge
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
GitResult
|
|
254
|
+
"""
|
|
255
|
+
try:
|
|
256
|
+
cmd = ["pull"]
|
|
257
|
+
if rebase:
|
|
258
|
+
cmd.append("--rebase")
|
|
259
|
+
cmd.append(remote)
|
|
260
|
+
if branch:
|
|
261
|
+
cmd.append(branch)
|
|
262
|
+
|
|
263
|
+
output = self._run_git(cmd)
|
|
264
|
+
|
|
265
|
+
return GitResult(
|
|
266
|
+
success=True,
|
|
267
|
+
message=f"Pulled from {remote}",
|
|
268
|
+
output=output
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
except Exception as e:
|
|
272
|
+
return GitResult(
|
|
273
|
+
success=False,
|
|
274
|
+
message=f"Error pulling: {e}"
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def create_branch(self, branch_name: str, checkout: bool = True) -> GitResult:
|
|
278
|
+
"""
|
|
279
|
+
Create new branch
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
branch_name: Branch name
|
|
283
|
+
checkout: Checkout after creating
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
GitResult
|
|
287
|
+
"""
|
|
288
|
+
try:
|
|
289
|
+
if checkout:
|
|
290
|
+
self._run_git(["checkout", "-b", branch_name])
|
|
291
|
+
message = f"Created and checked out branch: {branch_name}"
|
|
292
|
+
else:
|
|
293
|
+
self._run_git(["branch", branch_name])
|
|
294
|
+
message = f"Created branch: {branch_name}"
|
|
295
|
+
|
|
296
|
+
return GitResult(
|
|
297
|
+
success=True,
|
|
298
|
+
message=message
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
except Exception as e:
|
|
302
|
+
return GitResult(
|
|
303
|
+
success=False,
|
|
304
|
+
message=f"Error creating branch: {e}"
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
def checkout(self, branch_name: str, create: bool = False) -> GitResult:
|
|
308
|
+
"""
|
|
309
|
+
Checkout branch
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
branch_name: Branch name
|
|
313
|
+
create: Create if doesn't exist
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
GitResult
|
|
317
|
+
"""
|
|
318
|
+
try:
|
|
319
|
+
if create:
|
|
320
|
+
self._run_git(["checkout", "-b", branch_name])
|
|
321
|
+
else:
|
|
322
|
+
self._run_git(["checkout", branch_name])
|
|
323
|
+
|
|
324
|
+
return GitResult(
|
|
325
|
+
success=True,
|
|
326
|
+
message=f"Checked out: {branch_name}"
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
except Exception as e:
|
|
330
|
+
return GitResult(
|
|
331
|
+
success=False,
|
|
332
|
+
message=f"Error checking out branch: {e}"
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
def generate_commit_message(self, context: Optional[str] = None) -> str:
|
|
336
|
+
"""
|
|
337
|
+
Generate smart commit message based on diff
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
context: Additional context for commit message
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
Generated commit message
|
|
344
|
+
"""
|
|
345
|
+
try:
|
|
346
|
+
# Get diff
|
|
347
|
+
diff = self._run_git(["diff", "--cached"])
|
|
348
|
+
|
|
349
|
+
if not diff:
|
|
350
|
+
return "chore: update files"
|
|
351
|
+
|
|
352
|
+
# Analyze diff to generate message
|
|
353
|
+
message = self._analyze_diff_for_message(diff, context)
|
|
354
|
+
|
|
355
|
+
return message
|
|
356
|
+
|
|
357
|
+
except Exception as e:
|
|
358
|
+
return f"chore: update files ({e})"
|
|
359
|
+
|
|
360
|
+
def create_pr(
|
|
361
|
+
self,
|
|
362
|
+
title: str,
|
|
363
|
+
body: Optional[str] = None,
|
|
364
|
+
base: str = "main",
|
|
365
|
+
draft: bool = False
|
|
366
|
+
) -> GitResult:
|
|
367
|
+
"""
|
|
368
|
+
Create pull request using GitHub CLI
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
title: PR title
|
|
372
|
+
body: PR description
|
|
373
|
+
base: Base branch
|
|
374
|
+
draft: Create as draft
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
GitResult with PR URL
|
|
378
|
+
"""
|
|
379
|
+
try:
|
|
380
|
+
# Check if gh CLI is available
|
|
381
|
+
if not self._is_gh_available():
|
|
382
|
+
return GitResult(
|
|
383
|
+
success=False,
|
|
384
|
+
message="GitHub CLI (gh) not installed. Install with: brew install gh"
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
# Build gh command
|
|
388
|
+
cmd = ["gh", "pr", "create", "--title", title, "--base", base]
|
|
389
|
+
|
|
390
|
+
if body:
|
|
391
|
+
cmd.extend(["--body", body])
|
|
392
|
+
|
|
393
|
+
if draft:
|
|
394
|
+
cmd.append("--draft")
|
|
395
|
+
|
|
396
|
+
output = subprocess.run(
|
|
397
|
+
cmd,
|
|
398
|
+
cwd=str(self.project_path),
|
|
399
|
+
capture_output=True,
|
|
400
|
+
text=True,
|
|
401
|
+
check=True
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
# Extract PR URL from output
|
|
405
|
+
pr_url = output.stdout.strip()
|
|
406
|
+
|
|
407
|
+
return GitResult(
|
|
408
|
+
success=True,
|
|
409
|
+
message=f"PR created: {pr_url}",
|
|
410
|
+
output=pr_url
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
except subprocess.CalledProcessError as e:
|
|
414
|
+
return GitResult(
|
|
415
|
+
success=False,
|
|
416
|
+
message=f"Error creating PR: {e.stderr}"
|
|
417
|
+
)
|
|
418
|
+
except Exception as e:
|
|
419
|
+
return GitResult(
|
|
420
|
+
success=False,
|
|
421
|
+
message=f"Error creating PR: {e}"
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
def log(self, limit: int = 10, oneline: bool = False) -> GitResult:
|
|
425
|
+
"""
|
|
426
|
+
Get git log
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
limit: Number of commits
|
|
430
|
+
oneline: One line per commit
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
GitResult with log output
|
|
434
|
+
"""
|
|
435
|
+
try:
|
|
436
|
+
cmd = ["log", f"-{limit}"]
|
|
437
|
+
if oneline:
|
|
438
|
+
cmd.append("--oneline")
|
|
439
|
+
|
|
440
|
+
output = self._run_git(cmd)
|
|
441
|
+
|
|
442
|
+
return GitResult(
|
|
443
|
+
success=True,
|
|
444
|
+
message="Git log retrieved",
|
|
445
|
+
output=output
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
except Exception as e:
|
|
449
|
+
return GitResult(
|
|
450
|
+
success=False,
|
|
451
|
+
message=f"Error getting log: {e}"
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
def diff(self, cached: bool = False, files: Optional[List[Path]] = None) -> GitResult:
|
|
455
|
+
"""
|
|
456
|
+
Get git diff
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
cached: Show staged changes
|
|
460
|
+
files: Specific files to diff
|
|
461
|
+
|
|
462
|
+
Returns:
|
|
463
|
+
GitResult with diff output
|
|
464
|
+
"""
|
|
465
|
+
try:
|
|
466
|
+
cmd = ["diff"]
|
|
467
|
+
if cached:
|
|
468
|
+
cmd.append("--cached")
|
|
469
|
+
|
|
470
|
+
if files:
|
|
471
|
+
cmd.extend([str(f) for f in files])
|
|
472
|
+
|
|
473
|
+
output = self._run_git(cmd)
|
|
474
|
+
|
|
475
|
+
return GitResult(
|
|
476
|
+
success=True,
|
|
477
|
+
message="Diff retrieved",
|
|
478
|
+
output=output
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
except Exception as e:
|
|
482
|
+
return GitResult(
|
|
483
|
+
success=False,
|
|
484
|
+
message=f"Error getting diff: {e}"
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
# =========================================================================
|
|
488
|
+
# Helper Methods
|
|
489
|
+
# =========================================================================
|
|
490
|
+
|
|
491
|
+
def _run_git(self, args: List[str]) -> str:
|
|
492
|
+
"""Run git command and return output"""
|
|
493
|
+
cmd = ["git"] + args
|
|
494
|
+
result = subprocess.run(
|
|
495
|
+
cmd,
|
|
496
|
+
cwd=str(self.project_path),
|
|
497
|
+
capture_output=True,
|
|
498
|
+
text=True,
|
|
499
|
+
check=True
|
|
500
|
+
)
|
|
501
|
+
return result.stdout
|
|
502
|
+
|
|
503
|
+
def _is_git_available(self) -> bool:
|
|
504
|
+
"""Check if git is installed"""
|
|
505
|
+
try:
|
|
506
|
+
subprocess.run(
|
|
507
|
+
["git", "--version"],
|
|
508
|
+
capture_output=True,
|
|
509
|
+
check=True
|
|
510
|
+
)
|
|
511
|
+
return True
|
|
512
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
513
|
+
return False
|
|
514
|
+
|
|
515
|
+
def _is_gh_available(self) -> bool:
|
|
516
|
+
"""Check if GitHub CLI is installed"""
|
|
517
|
+
try:
|
|
518
|
+
subprocess.run(
|
|
519
|
+
["gh", "--version"],
|
|
520
|
+
capture_output=True,
|
|
521
|
+
check=True
|
|
522
|
+
)
|
|
523
|
+
return True
|
|
524
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
525
|
+
return False
|
|
526
|
+
|
|
527
|
+
def _is_git_repo(self) -> bool:
|
|
528
|
+
"""Check if directory is a git repository"""
|
|
529
|
+
try:
|
|
530
|
+
self._run_git(["rev-parse", "--git-dir"])
|
|
531
|
+
return True
|
|
532
|
+
except subprocess.CalledProcessError:
|
|
533
|
+
return False
|
|
534
|
+
|
|
535
|
+
def _get_ahead_behind(self) -> tuple[int, int]:
|
|
536
|
+
"""Get ahead/behind commit count"""
|
|
537
|
+
try:
|
|
538
|
+
output = self._run_git(["rev-list", "--left-right", "--count", "@{upstream}...HEAD"])
|
|
539
|
+
behind, ahead = output.strip().split()
|
|
540
|
+
return int(ahead), int(behind)
|
|
541
|
+
except (subprocess.CalledProcessError, ValueError):
|
|
542
|
+
return 0, 0
|
|
543
|
+
|
|
544
|
+
def _analyze_diff_for_message(self, diff: str, context: Optional[str] = None) -> str:
|
|
545
|
+
"""
|
|
546
|
+
Analyze diff to generate commit message
|
|
547
|
+
|
|
548
|
+
Args:
|
|
549
|
+
diff: Git diff output
|
|
550
|
+
context: Additional context
|
|
551
|
+
|
|
552
|
+
Returns:
|
|
553
|
+
Generated commit message
|
|
554
|
+
"""
|
|
555
|
+
# Count changes
|
|
556
|
+
additions = diff.count("\n+") - diff.count("\n+++")
|
|
557
|
+
deletions = diff.count("\n-") - diff.count("\n---")
|
|
558
|
+
|
|
559
|
+
# Detect change type
|
|
560
|
+
if "def " in diff or "class " in diff:
|
|
561
|
+
change_type = "feat"
|
|
562
|
+
elif "import " in diff:
|
|
563
|
+
change_type = "refactor"
|
|
564
|
+
elif "test_" in diff or "Test" in diff:
|
|
565
|
+
change_type = "test"
|
|
566
|
+
elif "README" in diff or "# " in diff:
|
|
567
|
+
change_type = "docs"
|
|
568
|
+
elif "fix" in diff.lower() or "bug" in diff.lower():
|
|
569
|
+
change_type = "fix"
|
|
570
|
+
else:
|
|
571
|
+
change_type = "chore"
|
|
572
|
+
|
|
573
|
+
# Generate summary
|
|
574
|
+
if additions > deletions * 2:
|
|
575
|
+
summary = "add new functionality"
|
|
576
|
+
elif deletions > additions * 2:
|
|
577
|
+
summary = "remove deprecated code"
|
|
578
|
+
else:
|
|
579
|
+
summary = "update implementation"
|
|
580
|
+
|
|
581
|
+
# Add context if provided
|
|
582
|
+
if context:
|
|
583
|
+
message = f"{change_type}: {context}\n\n+{additions} -{deletions}"
|
|
584
|
+
else:
|
|
585
|
+
message = f"{change_type}: {summary}\n\n+{additions} -{deletions}"
|
|
586
|
+
|
|
587
|
+
return message
|
|
588
|
+
|
|
589
|
+
def get_remote_url(self, remote: str = "origin") -> Optional[str]:
|
|
590
|
+
"""Get remote URL"""
|
|
591
|
+
try:
|
|
592
|
+
output = self._run_git(["remote", "get-url", remote])
|
|
593
|
+
return output.strip()
|
|
594
|
+
except subprocess.CalledProcessError:
|
|
595
|
+
return None
|
|
596
|
+
|
|
597
|
+
def get_current_branch(self) -> Optional[str]:
|
|
598
|
+
"""Get current branch name"""
|
|
599
|
+
try:
|
|
600
|
+
output = self._run_git(["branch", "--show-current"])
|
|
601
|
+
return output.strip()
|
|
602
|
+
except subprocess.CalledProcessError:
|
|
603
|
+
return None
|
|
604
|
+
|
|
605
|
+
def list_branches(self, all_branches: bool = False) -> List[str]:
|
|
606
|
+
"""List branches"""
|
|
607
|
+
try:
|
|
608
|
+
cmd = ["branch"]
|
|
609
|
+
if all_branches:
|
|
610
|
+
cmd.append("-a")
|
|
611
|
+
|
|
612
|
+
output = self._run_git(cmd)
|
|
613
|
+
branches = [
|
|
614
|
+
line.strip().lstrip("* ").strip()
|
|
615
|
+
for line in output.splitlines()
|
|
616
|
+
if line.strip()
|
|
617
|
+
]
|
|
618
|
+
return branches
|
|
619
|
+
except subprocess.CalledProcessError:
|
|
620
|
+
return []
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Oracle Agent - RLM-based codebase knowledge system
|
|
3
|
+
|
|
4
|
+
The Oracle knows everything about the codebase and can answer questions
|
|
5
|
+
using RLM's unlimited context capabilities.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from groknroll.oracle.oracle_agent import OracleAgent
|
|
9
|
+
from groknroll.oracle.codebase_indexer import CodebaseIndexer
|
|
10
|
+
|
|
11
|
+
__all__ = ["OracleAgent", "CodebaseIndexer"]
|