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.

Files changed (62) hide show
  1. agent/__init__.py +0 -0
  2. agent/agent.py +182 -0
  3. agent/base.py +406 -0
  4. agent/context.py +126 -0
  5. agent/todo.py +149 -0
  6. agent/tool_executor.py +54 -0
  7. agent/verification.py +135 -0
  8. aloop-0.1.0.dist-info/METADATA +246 -0
  9. aloop-0.1.0.dist-info/RECORD +62 -0
  10. aloop-0.1.0.dist-info/WHEEL +5 -0
  11. aloop-0.1.0.dist-info/entry_points.txt +2 -0
  12. aloop-0.1.0.dist-info/licenses/LICENSE +21 -0
  13. aloop-0.1.0.dist-info/top_level.txt +9 -0
  14. cli.py +19 -0
  15. config.py +146 -0
  16. interactive.py +865 -0
  17. llm/__init__.py +51 -0
  18. llm/base.py +26 -0
  19. llm/compat.py +226 -0
  20. llm/content_utils.py +309 -0
  21. llm/litellm_adapter.py +450 -0
  22. llm/message_types.py +245 -0
  23. llm/model_manager.py +265 -0
  24. llm/retry.py +95 -0
  25. main.py +246 -0
  26. memory/__init__.py +20 -0
  27. memory/compressor.py +554 -0
  28. memory/manager.py +538 -0
  29. memory/serialization.py +82 -0
  30. memory/short_term.py +88 -0
  31. memory/token_tracker.py +203 -0
  32. memory/types.py +51 -0
  33. tools/__init__.py +6 -0
  34. tools/advanced_file_ops.py +557 -0
  35. tools/base.py +51 -0
  36. tools/calculator.py +50 -0
  37. tools/code_navigator.py +975 -0
  38. tools/explore.py +254 -0
  39. tools/file_ops.py +150 -0
  40. tools/git_tools.py +791 -0
  41. tools/notify.py +69 -0
  42. tools/parallel_execute.py +420 -0
  43. tools/session_manager.py +205 -0
  44. tools/shell.py +147 -0
  45. tools/shell_background.py +470 -0
  46. tools/smart_edit.py +491 -0
  47. tools/todo.py +130 -0
  48. tools/web_fetch.py +673 -0
  49. tools/web_search.py +61 -0
  50. utils/__init__.py +15 -0
  51. utils/logger.py +105 -0
  52. utils/model_pricing.py +49 -0
  53. utils/runtime.py +75 -0
  54. utils/terminal_ui.py +422 -0
  55. utils/tui/__init__.py +39 -0
  56. utils/tui/command_registry.py +49 -0
  57. utils/tui/components.py +306 -0
  58. utils/tui/input_handler.py +393 -0
  59. utils/tui/model_ui.py +204 -0
  60. utils/tui/progress.py +292 -0
  61. utils/tui/status_bar.py +178 -0
  62. 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
+ ]