aloop 0.1.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.
Potentially problematic release.
This version of aloop might be problematic. Click here for more details.
- agent/__init__.py +0 -0
- agent/agent.py +182 -0
- agent/base.py +406 -0
- agent/context.py +126 -0
- agent/todo.py +149 -0
- agent/tool_executor.py +54 -0
- agent/verification.py +135 -0
- aloop-0.1.0.dist-info/METADATA +246 -0
- aloop-0.1.0.dist-info/RECORD +62 -0
- aloop-0.1.0.dist-info/WHEEL +5 -0
- aloop-0.1.0.dist-info/entry_points.txt +2 -0
- aloop-0.1.0.dist-info/licenses/LICENSE +21 -0
- aloop-0.1.0.dist-info/top_level.txt +9 -0
- cli.py +19 -0
- config.py +146 -0
- interactive.py +865 -0
- llm/__init__.py +51 -0
- llm/base.py +26 -0
- llm/compat.py +226 -0
- llm/content_utils.py +309 -0
- llm/litellm_adapter.py +450 -0
- llm/message_types.py +245 -0
- llm/model_manager.py +265 -0
- llm/retry.py +95 -0
- main.py +246 -0
- memory/__init__.py +20 -0
- memory/compressor.py +554 -0
- memory/manager.py +538 -0
- memory/serialization.py +82 -0
- memory/short_term.py +88 -0
- memory/token_tracker.py +203 -0
- memory/types.py +51 -0
- tools/__init__.py +6 -0
- tools/advanced_file_ops.py +557 -0
- tools/base.py +51 -0
- tools/calculator.py +50 -0
- tools/code_navigator.py +975 -0
- tools/explore.py +254 -0
- tools/file_ops.py +150 -0
- tools/git_tools.py +791 -0
- tools/notify.py +69 -0
- tools/parallel_execute.py +420 -0
- tools/session_manager.py +205 -0
- tools/shell.py +147 -0
- tools/shell_background.py +470 -0
- tools/smart_edit.py +491 -0
- tools/todo.py +130 -0
- tools/web_fetch.py +673 -0
- tools/web_search.py +61 -0
- utils/__init__.py +15 -0
- utils/logger.py +105 -0
- utils/model_pricing.py +49 -0
- utils/runtime.py +75 -0
- utils/terminal_ui.py +422 -0
- utils/tui/__init__.py +39 -0
- utils/tui/command_registry.py +49 -0
- utils/tui/components.py +306 -0
- utils/tui/input_handler.py +393 -0
- utils/tui/model_ui.py +204 -0
- utils/tui/progress.py +292 -0
- utils/tui/status_bar.py +178 -0
- utils/tui/theme.py +165 -0
tools/git_tools.py
ADDED
|
@@ -0,0 +1,791 @@
|
|
|
1
|
+
"""Git operation tools for AI agents.
|
|
2
|
+
|
|
3
|
+
This module provides comprehensive git tools for version control operations,
|
|
4
|
+
enabling agents to interact with git repositories effectively.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import os
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
from .base import BaseTool
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GitBaseTool(BaseTool):
|
|
15
|
+
"""Base class for git tools with common functionality."""
|
|
16
|
+
|
|
17
|
+
async def _run_git_command(self, args: List[str], cwd: Optional[str] = None) -> str:
|
|
18
|
+
"""Execute a git command and return the output.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
args: Git command arguments (without 'git' prefix)
|
|
22
|
+
cwd: Working directory (default: current directory)
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Command output or error message
|
|
26
|
+
"""
|
|
27
|
+
try:
|
|
28
|
+
process = await asyncio.create_subprocess_exec(
|
|
29
|
+
"git",
|
|
30
|
+
*args,
|
|
31
|
+
cwd=cwd or os.getcwd(),
|
|
32
|
+
stdout=asyncio.subprocess.PIPE,
|
|
33
|
+
stderr=asyncio.subprocess.PIPE,
|
|
34
|
+
)
|
|
35
|
+
try:
|
|
36
|
+
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=30)
|
|
37
|
+
except TimeoutError:
|
|
38
|
+
process.kill()
|
|
39
|
+
await process.communicate()
|
|
40
|
+
return "Error: Git command timed out"
|
|
41
|
+
|
|
42
|
+
stdout_text = stdout.decode() if stdout else ""
|
|
43
|
+
stderr_text = stderr.decode() if stderr else ""
|
|
44
|
+
|
|
45
|
+
if process.returncode != 0:
|
|
46
|
+
return f"Error: {stderr_text.strip() or stdout_text.strip()}"
|
|
47
|
+
return stdout_text.strip()
|
|
48
|
+
except FileNotFoundError:
|
|
49
|
+
return "Error: git command not found. Is git installed?"
|
|
50
|
+
except Exception as e:
|
|
51
|
+
return f"Error executing git command: {str(e)}"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class GitStatusTool(GitBaseTool):
|
|
55
|
+
"""Get the current git status of the repository."""
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def name(self) -> str:
|
|
59
|
+
return "git_status"
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def description(self) -> str:
|
|
63
|
+
return """Get the current git status of the repository.
|
|
64
|
+
|
|
65
|
+
Shows:
|
|
66
|
+
- Current branch
|
|
67
|
+
- Tracked/untracked files
|
|
68
|
+
- Modified/staged changes
|
|
69
|
+
- Branch divergence status
|
|
70
|
+
|
|
71
|
+
Use this to understand the current state before making changes."""
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def parameters(self) -> Dict[str, Any]:
|
|
75
|
+
return {
|
|
76
|
+
"path": {
|
|
77
|
+
"type": "string",
|
|
78
|
+
"description": "Path to git repository (default: current directory)",
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async def execute(self, path: str = ".") -> str:
|
|
83
|
+
"""Execute git status command."""
|
|
84
|
+
return await self._run_git_command(["status"], cwd=path)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class GitDiffTool(GitBaseTool):
|
|
88
|
+
"""Show changes between commits, commit and working tree, etc."""
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def name(self) -> str:
|
|
92
|
+
return "git_diff"
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def description(self) -> str:
|
|
96
|
+
return """Show file changes in the repository.
|
|
97
|
+
|
|
98
|
+
Modes:
|
|
99
|
+
1. Staged changes: diff between staging area and last commit
|
|
100
|
+
2. Unstaged changes: diff between working tree and staging area
|
|
101
|
+
3. Specific files: diff for specific files
|
|
102
|
+
4. Commits: diff between two commits or branches
|
|
103
|
+
|
|
104
|
+
Examples:
|
|
105
|
+
- Show all staged changes
|
|
106
|
+
- Show changes in specific file
|
|
107
|
+
- Compare branches"""
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def parameters(self) -> Dict[str, Any]:
|
|
111
|
+
return {
|
|
112
|
+
"mode": {
|
|
113
|
+
"type": "string",
|
|
114
|
+
"description": "Diff mode: staged, unstaged, files, or commits",
|
|
115
|
+
"enum": ["staged", "unstaged", "files", "commits"],
|
|
116
|
+
},
|
|
117
|
+
"files": {
|
|
118
|
+
"type": "array",
|
|
119
|
+
"items": {"type": "string"},
|
|
120
|
+
"description": "Specific files to diff (for 'files' mode)",
|
|
121
|
+
},
|
|
122
|
+
"commit_range": {
|
|
123
|
+
"type": "string",
|
|
124
|
+
"description": "Commit range like 'HEAD~1..HEAD' or 'branch1..branch2' (for 'commits' mode)",
|
|
125
|
+
},
|
|
126
|
+
"path": {"type": "string", "description": "Path to git repository"},
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async def execute(
|
|
130
|
+
self,
|
|
131
|
+
mode: str = "staged",
|
|
132
|
+
files: Optional[List[str]] = None,
|
|
133
|
+
commit_range: str = "",
|
|
134
|
+
path: str = ".",
|
|
135
|
+
**kwargs,
|
|
136
|
+
) -> str:
|
|
137
|
+
"""Execute git diff command."""
|
|
138
|
+
args = ["diff"]
|
|
139
|
+
|
|
140
|
+
if mode == "staged":
|
|
141
|
+
args.append("--cached")
|
|
142
|
+
elif mode == "unstaged":
|
|
143
|
+
# Default diff shows unstaged changes
|
|
144
|
+
pass
|
|
145
|
+
elif mode == "files" and files:
|
|
146
|
+
args.extend(files)
|
|
147
|
+
elif mode == "commits" and commit_range:
|
|
148
|
+
args.append(commit_range)
|
|
149
|
+
|
|
150
|
+
return await self._run_git_command(args, cwd=path)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class GitAddTool(GitBaseTool):
|
|
154
|
+
"""Stage files for commit."""
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def name(self) -> str:
|
|
158
|
+
return "git_add"
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def description(self) -> str:
|
|
162
|
+
return """Stage files for commit.
|
|
163
|
+
|
|
164
|
+
Use:
|
|
165
|
+
- Specific files: provide file paths
|
|
166
|
+
- All changes: use ["."]
|
|
167
|
+
- All tracked files: use ["-u"]
|
|
168
|
+
- All (including untracked): use ["-A"]
|
|
169
|
+
|
|
170
|
+
Always check git_status first to see what you're staging."""
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def parameters(self) -> Dict[str, Any]:
|
|
174
|
+
return {
|
|
175
|
+
"files": {
|
|
176
|
+
"type": "array",
|
|
177
|
+
"items": {"type": "string"},
|
|
178
|
+
"description": "Files to stage (e.g., ['file.py'], ['.'], ['-A'])",
|
|
179
|
+
},
|
|
180
|
+
"path": {"type": "string", "description": "Path to git repository"},
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async def execute(self, files: List[str], path: str = ".", **kwargs) -> str:
|
|
184
|
+
"""Execute git add command."""
|
|
185
|
+
if not files:
|
|
186
|
+
return "Error: No files specified"
|
|
187
|
+
|
|
188
|
+
args = ["add"] + files
|
|
189
|
+
result = await self._run_git_command(args, cwd=path)
|
|
190
|
+
|
|
191
|
+
if "Error" not in result:
|
|
192
|
+
# Show what was staged
|
|
193
|
+
staged = await self._run_git_command(["diff", "--cached", "--name-only"], cwd=path)
|
|
194
|
+
if staged:
|
|
195
|
+
return f"Staged files:\n{staged}"
|
|
196
|
+
return "Files staged successfully"
|
|
197
|
+
|
|
198
|
+
return result
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class GitCommitTool(GitBaseTool):
|
|
202
|
+
"""Create a commit with a message."""
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def name(self) -> str:
|
|
206
|
+
return "git_commit"
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def description(self) -> str:
|
|
210
|
+
return """Create a git commit.
|
|
211
|
+
|
|
212
|
+
Requirements:
|
|
213
|
+
- message: Commit message describing the changes
|
|
214
|
+
- verify: Run pre-commit hooks (default: false for safety)
|
|
215
|
+
|
|
216
|
+
Best practices:
|
|
217
|
+
- Use clear, descriptive messages
|
|
218
|
+
- Keep first line under 50 characters
|
|
219
|
+
- Use body for detailed explanation if needed
|
|
220
|
+
|
|
221
|
+
Example message format:
|
|
222
|
+
"feat: add user authentication"
|
|
223
|
+
"fix: resolve null pointer in parser"""
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def parameters(self) -> Dict[str, Any]:
|
|
227
|
+
return {
|
|
228
|
+
"message": {"type": "string", "description": "Commit message (required)"},
|
|
229
|
+
"verify": {
|
|
230
|
+
"type": "boolean",
|
|
231
|
+
"description": "Run pre-commit hooks (default: false)",
|
|
232
|
+
"default": False,
|
|
233
|
+
},
|
|
234
|
+
"path": {"type": "string", "description": "Path to git repository"},
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async def execute(self, message: str, verify: bool = False, path: str = ".", **kwargs) -> str:
|
|
238
|
+
"""Execute git commit command."""
|
|
239
|
+
if not message:
|
|
240
|
+
return "Error: Commit message is required"
|
|
241
|
+
|
|
242
|
+
args = ["commit", "-m", message]
|
|
243
|
+
if not verify:
|
|
244
|
+
args.append("--no-verify")
|
|
245
|
+
|
|
246
|
+
return await self._run_git_command(args, cwd=path)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class GitLogTool(GitBaseTool):
|
|
250
|
+
"""Show commit history."""
|
|
251
|
+
|
|
252
|
+
@property
|
|
253
|
+
def name(self) -> str:
|
|
254
|
+
return "git_log"
|
|
255
|
+
|
|
256
|
+
@property
|
|
257
|
+
def description(self) -> str:
|
|
258
|
+
return """Show git commit history.
|
|
259
|
+
|
|
260
|
+
Options:
|
|
261
|
+
- limit: Number of commits to show (default: 10)
|
|
262
|
+
- oneline: Compact format (default: true)
|
|
263
|
+
- branch: Specific branch to show history of
|
|
264
|
+
|
|
265
|
+
Returns commit hash, author, date, and message."""
|
|
266
|
+
|
|
267
|
+
@property
|
|
268
|
+
def parameters(self) -> Dict[str, Any]:
|
|
269
|
+
return {
|
|
270
|
+
"limit": {"type": "integer", "description": "Number of commits to show", "default": 10},
|
|
271
|
+
"oneline": {"type": "boolean", "description": "Use compact format", "default": True},
|
|
272
|
+
"branch": {"type": "string", "description": "Specific branch (optional)"},
|
|
273
|
+
"path": {"type": "string", "description": "Path to git repository"},
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async def execute(
|
|
277
|
+
self,
|
|
278
|
+
limit: int = 10,
|
|
279
|
+
oneline: bool = True,
|
|
280
|
+
branch: Optional[str] = None,
|
|
281
|
+
path: str = ".",
|
|
282
|
+
**kwargs,
|
|
283
|
+
) -> str:
|
|
284
|
+
"""Execute git log command."""
|
|
285
|
+
args = ["log"]
|
|
286
|
+
|
|
287
|
+
if oneline:
|
|
288
|
+
args.append("--oneline")
|
|
289
|
+
|
|
290
|
+
if limit > 0:
|
|
291
|
+
args.extend(["-n", str(limit)])
|
|
292
|
+
|
|
293
|
+
if branch:
|
|
294
|
+
args.append(branch)
|
|
295
|
+
|
|
296
|
+
return await self._run_git_command(args, cwd=path)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class GitBranchTool(GitBaseTool):
|
|
300
|
+
"""List, create, or delete branches."""
|
|
301
|
+
|
|
302
|
+
@property
|
|
303
|
+
def name(self) -> str:
|
|
304
|
+
return "git_branch"
|
|
305
|
+
|
|
306
|
+
@property
|
|
307
|
+
def description(self) -> str:
|
|
308
|
+
return """Manage git branches.
|
|
309
|
+
|
|
310
|
+
Operations:
|
|
311
|
+
- list: Show all branches (default)
|
|
312
|
+
- create: Create new branch
|
|
313
|
+
- delete: Delete a branch
|
|
314
|
+
- current: Show current branch
|
|
315
|
+
|
|
316
|
+
Examples:
|
|
317
|
+
- List all: no parameters
|
|
318
|
+
- Create: operation="create", branch="feature-x"
|
|
319
|
+
- Delete: operation="delete", branch="old-branch" """
|
|
320
|
+
|
|
321
|
+
@property
|
|
322
|
+
def parameters(self) -> Dict[str, Any]:
|
|
323
|
+
return {
|
|
324
|
+
"operation": {
|
|
325
|
+
"type": "string",
|
|
326
|
+
"description": "Operation: list, create, delete, or current",
|
|
327
|
+
"enum": ["list", "create", "delete", "current"],
|
|
328
|
+
"default": "list",
|
|
329
|
+
},
|
|
330
|
+
"branch": {
|
|
331
|
+
"type": "string",
|
|
332
|
+
"description": "Branch name (for create/delete operations)",
|
|
333
|
+
},
|
|
334
|
+
"force": {
|
|
335
|
+
"type": "boolean",
|
|
336
|
+
"description": "Force delete (default: false)",
|
|
337
|
+
"default": False,
|
|
338
|
+
},
|
|
339
|
+
"path": {"type": "string", "description": "Path to git repository"},
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async def execute(
|
|
343
|
+
self,
|
|
344
|
+
operation: str = "list",
|
|
345
|
+
branch: Optional[str] = None,
|
|
346
|
+
force: bool = False,
|
|
347
|
+
path: str = ".",
|
|
348
|
+
**kwargs,
|
|
349
|
+
) -> str:
|
|
350
|
+
"""Execute git branch command."""
|
|
351
|
+
if operation == "list":
|
|
352
|
+
return await self._run_git_command(["branch", "-v"], cwd=path)
|
|
353
|
+
|
|
354
|
+
elif operation == "current":
|
|
355
|
+
return await self._run_git_command(["branch", "--show-current"], cwd=path)
|
|
356
|
+
|
|
357
|
+
elif operation == "create":
|
|
358
|
+
if not branch:
|
|
359
|
+
return "Error: Branch name required for create operation"
|
|
360
|
+
return await self._run_git_command(["branch", branch], cwd=path)
|
|
361
|
+
|
|
362
|
+
elif operation == "delete":
|
|
363
|
+
if not branch:
|
|
364
|
+
return "Error: Branch name required for delete operation"
|
|
365
|
+
args = ["branch"]
|
|
366
|
+
if force:
|
|
367
|
+
args.append("-D")
|
|
368
|
+
else:
|
|
369
|
+
args.append("-d")
|
|
370
|
+
args.append(branch)
|
|
371
|
+
return await self._run_git_command(args, cwd=path)
|
|
372
|
+
|
|
373
|
+
return "Error: Unknown operation"
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class GitCheckoutTool(GitBaseTool):
|
|
377
|
+
"""Switch branches or restore files."""
|
|
378
|
+
|
|
379
|
+
@property
|
|
380
|
+
def name(self) -> str:
|
|
381
|
+
return "git_checkout"
|
|
382
|
+
|
|
383
|
+
@property
|
|
384
|
+
def description(self) -> str:
|
|
385
|
+
return """Switch branches or restore files.
|
|
386
|
+
|
|
387
|
+
Operations:
|
|
388
|
+
- branch: Switch to existing branch
|
|
389
|
+
- new_branch: Create and switch to new branch
|
|
390
|
+
- file: Restore file from last commit
|
|
391
|
+
- commit: Checkout specific commit (detached HEAD)
|
|
392
|
+
|
|
393
|
+
Examples:
|
|
394
|
+
- Switch: branch="main"
|
|
395
|
+
- Create: new_branch="feature"
|
|
396
|
+
- Restore: file="path/to/file.py" """
|
|
397
|
+
|
|
398
|
+
@property
|
|
399
|
+
def parameters(self) -> Dict[str, Any]:
|
|
400
|
+
return {
|
|
401
|
+
"branch": {"type": "string", "description": "Switch to existing branch"},
|
|
402
|
+
"new_branch": {"type": "string", "description": "Create and switch to new branch"},
|
|
403
|
+
"file": {"type": "string", "description": "Restore specific file from HEAD"},
|
|
404
|
+
"commit": {"type": "string", "description": "Checkout specific commit/tag"},
|
|
405
|
+
"path": {"type": "string", "description": "Path to git repository"},
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async def execute(
|
|
409
|
+
self,
|
|
410
|
+
branch: Optional[str] = None,
|
|
411
|
+
new_branch: Optional[str] = None,
|
|
412
|
+
file: Optional[str] = None,
|
|
413
|
+
commit: Optional[str] = None,
|
|
414
|
+
path: str = ".",
|
|
415
|
+
**kwargs,
|
|
416
|
+
) -> str:
|
|
417
|
+
"""Execute git checkout command."""
|
|
418
|
+
args = ["checkout"]
|
|
419
|
+
|
|
420
|
+
if new_branch:
|
|
421
|
+
result = await self._run_git_command(["checkout", "-b", new_branch], cwd=path)
|
|
422
|
+
if "Error" not in result:
|
|
423
|
+
return f"Created and switched to branch: {new_branch}"
|
|
424
|
+
return result
|
|
425
|
+
|
|
426
|
+
if branch:
|
|
427
|
+
args.append(branch)
|
|
428
|
+
return await self._run_git_command(args, cwd=path)
|
|
429
|
+
|
|
430
|
+
if file:
|
|
431
|
+
args.append("--")
|
|
432
|
+
args.append(file)
|
|
433
|
+
return await self._run_git_command(args, cwd=path)
|
|
434
|
+
|
|
435
|
+
if commit:
|
|
436
|
+
args.append(commit)
|
|
437
|
+
return await self._run_git_command(args, cwd=path)
|
|
438
|
+
|
|
439
|
+
return "Error: Specify branch, new_branch, file, or commit"
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
class GitPushTool(GitBaseTool):
|
|
443
|
+
"""Push commits to remote repository."""
|
|
444
|
+
|
|
445
|
+
@property
|
|
446
|
+
def name(self) -> str:
|
|
447
|
+
return "git_push"
|
|
448
|
+
|
|
449
|
+
@property
|
|
450
|
+
def description(self) -> str:
|
|
451
|
+
return """Push commits to remote repository.
|
|
452
|
+
|
|
453
|
+
Options:
|
|
454
|
+
- remote: Remote name (default: origin)
|
|
455
|
+
- branch: Branch to push (default: current branch)
|
|
456
|
+
- force: Force push (use with caution!)
|
|
457
|
+
|
|
458
|
+
WARNING: Force push can overwrite remote history. Use only when you know what you're doing."""
|
|
459
|
+
|
|
460
|
+
@property
|
|
461
|
+
def parameters(self) -> Dict[str, Any]:
|
|
462
|
+
return {
|
|
463
|
+
"remote": {
|
|
464
|
+
"type": "string",
|
|
465
|
+
"description": "Remote name (default: origin)",
|
|
466
|
+
"default": "origin",
|
|
467
|
+
},
|
|
468
|
+
"branch": {"type": "string", "description": "Branch to push (default: current branch)"},
|
|
469
|
+
"force": {
|
|
470
|
+
"type": "boolean",
|
|
471
|
+
"description": "Force push (WARNING: rewrites history)",
|
|
472
|
+
"default": False,
|
|
473
|
+
},
|
|
474
|
+
"set_upstream": {
|
|
475
|
+
"type": "boolean",
|
|
476
|
+
"description": "Set upstream tracking",
|
|
477
|
+
"default": False,
|
|
478
|
+
},
|
|
479
|
+
"path": {"type": "string", "description": "Path to git repository"},
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async def execute(
|
|
483
|
+
self,
|
|
484
|
+
remote: str = "origin",
|
|
485
|
+
branch: Optional[str] = None,
|
|
486
|
+
force: bool = False,
|
|
487
|
+
set_upstream: bool = False,
|
|
488
|
+
path: str = ".",
|
|
489
|
+
**kwargs,
|
|
490
|
+
) -> str:
|
|
491
|
+
"""Execute git push command."""
|
|
492
|
+
args = ["push"]
|
|
493
|
+
|
|
494
|
+
if force:
|
|
495
|
+
args.append("--force")
|
|
496
|
+
|
|
497
|
+
if set_upstream:
|
|
498
|
+
args.append("--set-upstream")
|
|
499
|
+
|
|
500
|
+
args.append(remote)
|
|
501
|
+
|
|
502
|
+
if branch:
|
|
503
|
+
args.append(branch)
|
|
504
|
+
else:
|
|
505
|
+
# Get current branch
|
|
506
|
+
branch = await self._run_git_command(["branch", "--show-current"], cwd=path)
|
|
507
|
+
if "Error" in branch:
|
|
508
|
+
return "Error: Could not determine current branch"
|
|
509
|
+
args.append(branch)
|
|
510
|
+
|
|
511
|
+
return await self._run_git_command(args, cwd=path)
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
class GitPullTool(GitBaseTool):
|
|
515
|
+
"""Pull changes from remote repository."""
|
|
516
|
+
|
|
517
|
+
@property
|
|
518
|
+
def name(self) -> str:
|
|
519
|
+
return "git_pull"
|
|
520
|
+
|
|
521
|
+
@property
|
|
522
|
+
def description(self) -> str:
|
|
523
|
+
return """Pull changes from remote repository.
|
|
524
|
+
|
|
525
|
+
Options:
|
|
526
|
+
- remote: Remote name (default: origin)
|
|
527
|
+
- branch: Branch to pull (default: current branch)
|
|
528
|
+
- rebase: Use rebase instead of merge (default: false)"""
|
|
529
|
+
|
|
530
|
+
@property
|
|
531
|
+
def parameters(self) -> Dict[str, Any]:
|
|
532
|
+
return {
|
|
533
|
+
"remote": {
|
|
534
|
+
"type": "string",
|
|
535
|
+
"description": "Remote name (default: origin)",
|
|
536
|
+
"default": "origin",
|
|
537
|
+
},
|
|
538
|
+
"branch": {"type": "string", "description": "Branch to pull (default: current branch)"},
|
|
539
|
+
"rebase": {
|
|
540
|
+
"type": "boolean",
|
|
541
|
+
"description": "Use rebase instead of merge",
|
|
542
|
+
"default": False,
|
|
543
|
+
},
|
|
544
|
+
"path": {"type": "string", "description": "Path to git repository"},
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async def execute(
|
|
548
|
+
self,
|
|
549
|
+
remote: str = "origin",
|
|
550
|
+
branch: Optional[str] = None,
|
|
551
|
+
rebase: bool = False,
|
|
552
|
+
path: str = ".",
|
|
553
|
+
**kwargs,
|
|
554
|
+
) -> str:
|
|
555
|
+
"""Execute git pull command."""
|
|
556
|
+
args = ["pull"]
|
|
557
|
+
|
|
558
|
+
if rebase:
|
|
559
|
+
args.append("--rebase")
|
|
560
|
+
|
|
561
|
+
args.append(remote)
|
|
562
|
+
|
|
563
|
+
if branch:
|
|
564
|
+
args.append(branch)
|
|
565
|
+
else:
|
|
566
|
+
# Get current branch
|
|
567
|
+
branch = await self._run_git_command(["branch", "--show-current"], cwd=path)
|
|
568
|
+
if "Error" in branch:
|
|
569
|
+
return "Error: Could not determine current branch"
|
|
570
|
+
args.append(branch)
|
|
571
|
+
|
|
572
|
+
return await self._run_git_command(args, cwd=path)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
class GitRemoteTool(GitBaseTool):
|
|
576
|
+
"""Manage remote repositories."""
|
|
577
|
+
|
|
578
|
+
@property
|
|
579
|
+
def name(self) -> str:
|
|
580
|
+
return "git_remote"
|
|
581
|
+
|
|
582
|
+
@property
|
|
583
|
+
def description(self) -> str:
|
|
584
|
+
return """Manage remote repositories.
|
|
585
|
+
|
|
586
|
+
Operations:
|
|
587
|
+
- list: Show all remotes
|
|
588
|
+
- add: Add a new remote
|
|
589
|
+
- remove: Remove a remote
|
|
590
|
+
- get-url: Get URL of a remote
|
|
591
|
+
- set-url: Change URL of a remote"""
|
|
592
|
+
|
|
593
|
+
@property
|
|
594
|
+
def parameters(self) -> Dict[str, Any]:
|
|
595
|
+
return {
|
|
596
|
+
"operation": {
|
|
597
|
+
"type": "string",
|
|
598
|
+
"description": "Operation: list, add, remove, get-url, set-url",
|
|
599
|
+
"enum": ["list", "add", "remove", "get-url", "set-url"],
|
|
600
|
+
},
|
|
601
|
+
"name": {"type": "string", "description": "Remote name"},
|
|
602
|
+
"url": {"type": "string", "description": "Remote URL (for add/set-url)"},
|
|
603
|
+
"path": {"type": "string", "description": "Path to git repository"},
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
async def execute(
|
|
607
|
+
self,
|
|
608
|
+
operation: str = "list",
|
|
609
|
+
name: Optional[str] = None,
|
|
610
|
+
url: Optional[str] = None,
|
|
611
|
+
path: str = ".",
|
|
612
|
+
**kwargs,
|
|
613
|
+
) -> str:
|
|
614
|
+
"""Execute git remote command."""
|
|
615
|
+
args = ["remote"]
|
|
616
|
+
|
|
617
|
+
if operation == "list":
|
|
618
|
+
args.append("-v")
|
|
619
|
+
return await self._run_git_command(args, cwd=path)
|
|
620
|
+
|
|
621
|
+
elif operation == "add":
|
|
622
|
+
if not name or not url:
|
|
623
|
+
return "Error: Both name and url required for add operation"
|
|
624
|
+
args.extend(["add", name, url])
|
|
625
|
+
return await self._run_git_command(args, cwd=path)
|
|
626
|
+
|
|
627
|
+
elif operation == "remove":
|
|
628
|
+
if not name:
|
|
629
|
+
return "Error: Name required for remove operation"
|
|
630
|
+
args.extend(["remove", name])
|
|
631
|
+
return await self._run_git_command(args, cwd=path)
|
|
632
|
+
|
|
633
|
+
elif operation == "get-url":
|
|
634
|
+
if not name:
|
|
635
|
+
return "Error: Name required for get-url operation"
|
|
636
|
+
args.extend(["get-url", name])
|
|
637
|
+
return await self._run_git_command(args, cwd=path)
|
|
638
|
+
|
|
639
|
+
elif operation == "set-url":
|
|
640
|
+
if not name or not url:
|
|
641
|
+
return "Error: Both name and url required for set-url operation"
|
|
642
|
+
args.extend(["set-url", name, url])
|
|
643
|
+
return await self._run_git_command(args, cwd=path)
|
|
644
|
+
|
|
645
|
+
return "Error: Unknown operation"
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
class GitStashTool(GitBaseTool):
|
|
649
|
+
"""Stash and restore changes."""
|
|
650
|
+
|
|
651
|
+
@property
|
|
652
|
+
def name(self) -> str:
|
|
653
|
+
return "git_stash"
|
|
654
|
+
|
|
655
|
+
@property
|
|
656
|
+
def description(self) -> str:
|
|
657
|
+
return """Stash temporary changes.
|
|
658
|
+
|
|
659
|
+
Operations:
|
|
660
|
+
- push: Stash current changes
|
|
661
|
+
- list: Show all stashes
|
|
662
|
+
- pop: Restore most recent stash
|
|
663
|
+
- drop: Delete most recent stash
|
|
664
|
+
|
|
665
|
+
Useful for temporarily saving work to switch branches."""
|
|
666
|
+
|
|
667
|
+
@property
|
|
668
|
+
def parameters(self) -> Dict[str, Any]:
|
|
669
|
+
return {
|
|
670
|
+
"operation": {
|
|
671
|
+
"type": "string",
|
|
672
|
+
"description": "Operation: push, list, pop, drop",
|
|
673
|
+
"enum": ["push", "list", "pop", "drop"],
|
|
674
|
+
},
|
|
675
|
+
"message": {"type": "string", "description": "Stash message (for push)"},
|
|
676
|
+
"path": {"type": "string", "description": "Path to git repository"},
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
async def execute(
|
|
680
|
+
self, operation: str = "push", message: Optional[str] = None, path: str = ".", **kwargs
|
|
681
|
+
) -> str:
|
|
682
|
+
"""Execute git stash command."""
|
|
683
|
+
args = ["stash"]
|
|
684
|
+
|
|
685
|
+
if operation == "push":
|
|
686
|
+
args.append("push")
|
|
687
|
+
if message:
|
|
688
|
+
args.extend(["-m", message])
|
|
689
|
+
return await self._run_git_command(args, cwd=path)
|
|
690
|
+
|
|
691
|
+
elif operation == "list":
|
|
692
|
+
args.append("list")
|
|
693
|
+
return await self._run_git_command(args, cwd=path)
|
|
694
|
+
|
|
695
|
+
elif operation == "pop":
|
|
696
|
+
args.append("pop")
|
|
697
|
+
return await self._run_git_command(args, cwd=path)
|
|
698
|
+
|
|
699
|
+
elif operation == "drop":
|
|
700
|
+
args.append("drop")
|
|
701
|
+
return await self._run_git_command(args, cwd=path)
|
|
702
|
+
|
|
703
|
+
return "Error: Unknown operation"
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
class GitCleanTool(GitBaseTool):
|
|
707
|
+
"""Remove untracked files from working tree."""
|
|
708
|
+
|
|
709
|
+
@property
|
|
710
|
+
def name(self) -> str:
|
|
711
|
+
return "git_clean"
|
|
712
|
+
|
|
713
|
+
@property
|
|
714
|
+
def description(self) -> str:
|
|
715
|
+
return """Remove untracked files from working tree.
|
|
716
|
+
|
|
717
|
+
IMPORTANT: This is a destructive operation!
|
|
718
|
+
|
|
719
|
+
Options:
|
|
720
|
+
- dry_run: Show what would be removed (default: true for safety)
|
|
721
|
+
- force: Actually remove files (must be explicitly set to true)
|
|
722
|
+
- directories: Also remove untracked directories
|
|
723
|
+
|
|
724
|
+
Always use dry_run first to see what will be deleted."""
|
|
725
|
+
|
|
726
|
+
@property
|
|
727
|
+
def parameters(self) -> Dict[str, Any]:
|
|
728
|
+
return {
|
|
729
|
+
"dry_run": {
|
|
730
|
+
"type": "boolean",
|
|
731
|
+
"description": "Show what would be removed without deleting",
|
|
732
|
+
"default": True,
|
|
733
|
+
},
|
|
734
|
+
"force": {
|
|
735
|
+
"type": "boolean",
|
|
736
|
+
"description": "Actually remove files (DANGEROUS)",
|
|
737
|
+
"default": False,
|
|
738
|
+
},
|
|
739
|
+
"directories": {
|
|
740
|
+
"type": "boolean",
|
|
741
|
+
"description": "Remove untracked directories too",
|
|
742
|
+
"default": False,
|
|
743
|
+
},
|
|
744
|
+
"path": {"type": "string", "description": "Path to git repository"},
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
async def execute(
|
|
748
|
+
self,
|
|
749
|
+
dry_run: bool = True,
|
|
750
|
+
force: bool = False,
|
|
751
|
+
directories: bool = False,
|
|
752
|
+
path: str = ".",
|
|
753
|
+
**kwargs,
|
|
754
|
+
) -> str:
|
|
755
|
+
"""Execute git clean command."""
|
|
756
|
+
if not force and not dry_run:
|
|
757
|
+
return "Error: force must be True to actually delete files. Use dry_run=True first."
|
|
758
|
+
|
|
759
|
+
args = ["clean"]
|
|
760
|
+
|
|
761
|
+
if dry_run:
|
|
762
|
+
args.append("-n") # Show what would be done
|
|
763
|
+
elif force:
|
|
764
|
+
args.append("-f") # Force delete
|
|
765
|
+
|
|
766
|
+
if directories:
|
|
767
|
+
args.append("-d") # Remove untracked directories
|
|
768
|
+
|
|
769
|
+
result = await self._run_git_command(args, cwd=path)
|
|
770
|
+
|
|
771
|
+
if dry_run and "Would remove" in result:
|
|
772
|
+
return f"DRY RUN - Files that would be removed:\n{result}"
|
|
773
|
+
|
|
774
|
+
return result
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
# All git tools for easy registration
|
|
778
|
+
GIT_TOOLS = [
|
|
779
|
+
GitStatusTool(),
|
|
780
|
+
GitDiffTool(),
|
|
781
|
+
GitAddTool(),
|
|
782
|
+
GitCommitTool(),
|
|
783
|
+
GitLogTool(),
|
|
784
|
+
GitBranchTool(),
|
|
785
|
+
GitCheckoutTool(),
|
|
786
|
+
GitPushTool(),
|
|
787
|
+
GitPullTool(),
|
|
788
|
+
GitRemoteTool(),
|
|
789
|
+
GitStashTool(),
|
|
790
|
+
GitCleanTool(),
|
|
791
|
+
]
|