kairo-code 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.
Files changed (144) hide show
  1. image-service/main.py +178 -0
  2. infra/chat/app/main.py +84 -0
  3. kairo/backend/__init__.py +0 -0
  4. kairo/backend/api/__init__.py +0 -0
  5. kairo/backend/api/admin/__init__.py +23 -0
  6. kairo/backend/api/admin/audit.py +54 -0
  7. kairo/backend/api/admin/content.py +142 -0
  8. kairo/backend/api/admin/incidents.py +148 -0
  9. kairo/backend/api/admin/stats.py +125 -0
  10. kairo/backend/api/admin/system.py +87 -0
  11. kairo/backend/api/admin/users.py +279 -0
  12. kairo/backend/api/agents.py +94 -0
  13. kairo/backend/api/api_keys.py +85 -0
  14. kairo/backend/api/auth.py +116 -0
  15. kairo/backend/api/billing.py +41 -0
  16. kairo/backend/api/chat.py +72 -0
  17. kairo/backend/api/conversations.py +125 -0
  18. kairo/backend/api/device_auth.py +100 -0
  19. kairo/backend/api/files.py +83 -0
  20. kairo/backend/api/health.py +36 -0
  21. kairo/backend/api/images.py +80 -0
  22. kairo/backend/api/openai_compat.py +225 -0
  23. kairo/backend/api/projects.py +102 -0
  24. kairo/backend/api/usage.py +32 -0
  25. kairo/backend/api/webhooks.py +79 -0
  26. kairo/backend/app.py +297 -0
  27. kairo/backend/config.py +179 -0
  28. kairo/backend/core/__init__.py +0 -0
  29. kairo/backend/core/admin_auth.py +24 -0
  30. kairo/backend/core/api_key_auth.py +55 -0
  31. kairo/backend/core/database.py +28 -0
  32. kairo/backend/core/dependencies.py +70 -0
  33. kairo/backend/core/logging.py +23 -0
  34. kairo/backend/core/rate_limit.py +73 -0
  35. kairo/backend/core/security.py +29 -0
  36. kairo/backend/models/__init__.py +19 -0
  37. kairo/backend/models/agent.py +30 -0
  38. kairo/backend/models/api_key.py +25 -0
  39. kairo/backend/models/api_usage.py +29 -0
  40. kairo/backend/models/audit_log.py +26 -0
  41. kairo/backend/models/conversation.py +48 -0
  42. kairo/backend/models/device_code.py +30 -0
  43. kairo/backend/models/feature_flag.py +21 -0
  44. kairo/backend/models/image_generation.py +24 -0
  45. kairo/backend/models/incident.py +28 -0
  46. kairo/backend/models/project.py +28 -0
  47. kairo/backend/models/uptime_record.py +24 -0
  48. kairo/backend/models/usage.py +24 -0
  49. kairo/backend/models/user.py +49 -0
  50. kairo/backend/schemas/__init__.py +0 -0
  51. kairo/backend/schemas/admin/__init__.py +0 -0
  52. kairo/backend/schemas/admin/audit.py +28 -0
  53. kairo/backend/schemas/admin/content.py +53 -0
  54. kairo/backend/schemas/admin/stats.py +77 -0
  55. kairo/backend/schemas/admin/system.py +44 -0
  56. kairo/backend/schemas/admin/users.py +48 -0
  57. kairo/backend/schemas/agent.py +42 -0
  58. kairo/backend/schemas/api_key.py +30 -0
  59. kairo/backend/schemas/auth.py +57 -0
  60. kairo/backend/schemas/chat.py +26 -0
  61. kairo/backend/schemas/conversation.py +39 -0
  62. kairo/backend/schemas/device_auth.py +40 -0
  63. kairo/backend/schemas/image.py +15 -0
  64. kairo/backend/schemas/openai_compat.py +76 -0
  65. kairo/backend/schemas/project.py +21 -0
  66. kairo/backend/schemas/status.py +81 -0
  67. kairo/backend/schemas/usage.py +15 -0
  68. kairo/backend/services/__init__.py +0 -0
  69. kairo/backend/services/admin/__init__.py +0 -0
  70. kairo/backend/services/admin/audit_service.py +78 -0
  71. kairo/backend/services/admin/content_service.py +119 -0
  72. kairo/backend/services/admin/incident_service.py +94 -0
  73. kairo/backend/services/admin/stats_service.py +281 -0
  74. kairo/backend/services/admin/system_service.py +126 -0
  75. kairo/backend/services/admin/user_service.py +157 -0
  76. kairo/backend/services/agent_service.py +107 -0
  77. kairo/backend/services/api_key_service.py +66 -0
  78. kairo/backend/services/api_usage_service.py +126 -0
  79. kairo/backend/services/auth_service.py +101 -0
  80. kairo/backend/services/chat_service.py +501 -0
  81. kairo/backend/services/conversation_service.py +264 -0
  82. kairo/backend/services/device_auth_service.py +193 -0
  83. kairo/backend/services/email_service.py +55 -0
  84. kairo/backend/services/image_service.py +181 -0
  85. kairo/backend/services/llm_service.py +186 -0
  86. kairo/backend/services/project_service.py +109 -0
  87. kairo/backend/services/status_service.py +167 -0
  88. kairo/backend/services/stripe_service.py +78 -0
  89. kairo/backend/services/usage_service.py +150 -0
  90. kairo/backend/services/web_search_service.py +96 -0
  91. kairo/migrations/env.py +60 -0
  92. kairo/migrations/versions/001_initial.py +55 -0
  93. kairo/migrations/versions/002_usage_tracking_and_indexes.py +66 -0
  94. kairo/migrations/versions/003_username_to_email.py +21 -0
  95. kairo/migrations/versions/004_add_plans_and_verification.py +67 -0
  96. kairo/migrations/versions/005_add_projects.py +52 -0
  97. kairo/migrations/versions/006_add_image_generation.py +63 -0
  98. kairo/migrations/versions/007_add_admin_portal.py +107 -0
  99. kairo/migrations/versions/008_add_device_code_auth.py +76 -0
  100. kairo/migrations/versions/009_add_status_page.py +65 -0
  101. kairo/tools/extract_claude_data.py +465 -0
  102. kairo/tools/filter_claude_data.py +303 -0
  103. kairo/tools/generate_curated_data.py +157 -0
  104. kairo/tools/mix_training_data.py +295 -0
  105. kairo_code/__init__.py +3 -0
  106. kairo_code/agents/__init__.py +25 -0
  107. kairo_code/agents/architect.py +98 -0
  108. kairo_code/agents/audit.py +100 -0
  109. kairo_code/agents/base.py +463 -0
  110. kairo_code/agents/coder.py +155 -0
  111. kairo_code/agents/database.py +77 -0
  112. kairo_code/agents/docs.py +88 -0
  113. kairo_code/agents/explorer.py +62 -0
  114. kairo_code/agents/guardian.py +80 -0
  115. kairo_code/agents/planner.py +66 -0
  116. kairo_code/agents/reviewer.py +91 -0
  117. kairo_code/agents/security.py +94 -0
  118. kairo_code/agents/terraform.py +88 -0
  119. kairo_code/agents/testing.py +97 -0
  120. kairo_code/agents/uiux.py +88 -0
  121. kairo_code/auth.py +232 -0
  122. kairo_code/config.py +172 -0
  123. kairo_code/conversation.py +173 -0
  124. kairo_code/heartbeat.py +63 -0
  125. kairo_code/llm.py +291 -0
  126. kairo_code/logging_config.py +156 -0
  127. kairo_code/main.py +818 -0
  128. kairo_code/router.py +217 -0
  129. kairo_code/sandbox.py +248 -0
  130. kairo_code/settings.py +183 -0
  131. kairo_code/tools/__init__.py +51 -0
  132. kairo_code/tools/analysis.py +509 -0
  133. kairo_code/tools/base.py +417 -0
  134. kairo_code/tools/code.py +58 -0
  135. kairo_code/tools/definitions.py +617 -0
  136. kairo_code/tools/files.py +315 -0
  137. kairo_code/tools/review.py +390 -0
  138. kairo_code/tools/search.py +185 -0
  139. kairo_code/ui.py +418 -0
  140. kairo_code-0.1.0.dist-info/METADATA +13 -0
  141. kairo_code-0.1.0.dist-info/RECORD +144 -0
  142. kairo_code-0.1.0.dist-info/WHEEL +5 -0
  143. kairo_code-0.1.0.dist-info/entry_points.txt +2 -0
  144. kairo_code-0.1.0.dist-info/top_level.txt +4 -0
@@ -0,0 +1,617 @@
1
+ """Concrete tool definitions for Kairo Code"""
2
+
3
+ import os
4
+ import re
5
+ import shutil
6
+ import subprocess
7
+ import shlex
8
+
9
+ from .base import Tool, ToolResult, ToolRegistry
10
+ from .files import read_file, write_file, edit_file, list_files, search_files, tree
11
+ from .search import web_search, web_fetch, format_search_results
12
+
13
+ # Import new analysis and review tools
14
+ from .analysis import (
15
+ LintTool,
16
+ TypeCheckTool,
17
+ RunTestsTool,
18
+ SecurityScanTool,
19
+ CodeComplexityTool,
20
+ FindDeadCodeTool,
21
+ GetDiagnosticsTool,
22
+ )
23
+ from .review import (
24
+ CodeReviewTool,
25
+ ModularityCheckTool,
26
+ DependencyGraphTool,
27
+ )
28
+
29
+
30
+ class ReadFileTool(Tool):
31
+ name = "read_file"
32
+ description = "Read the contents of a file with line numbers. Use offset/limit for large files."
33
+ parameters = {
34
+ "type": "object",
35
+ "properties": {
36
+ "path": {"type": "string", "description": "Path to the file to read"},
37
+ "offset": {"type": "integer", "description": "Line number to start from (0-indexed)"},
38
+ "limit": {"type": "integer", "description": "Max lines to read (default 500)"},
39
+ },
40
+ "required": ["path"],
41
+ }
42
+
43
+ def execute(self, path: str, offset: int = 0, limit: int = None) -> ToolResult:
44
+ try:
45
+ content = read_file(path, offset=offset, limit=limit)
46
+ return ToolResult(success=True, output=content)
47
+ except Exception as e:
48
+ return ToolResult(success=False, output="", error=str(e))
49
+
50
+
51
+ class WriteFileTool(Tool):
52
+ name = "write_file"
53
+ description = "Write content to a file, creating it if needed. For modifications, prefer edit_file instead."
54
+ parameters = {
55
+ "type": "object",
56
+ "properties": {
57
+ "path": {"type": "string", "description": "Path to write the file"},
58
+ "content": {"type": "string", "description": "Content to write"},
59
+ },
60
+ "required": ["path", "content"],
61
+ }
62
+
63
+ def execute(self, path: str = None, content: str = None) -> ToolResult:
64
+ # Validate required parameters
65
+ if not path:
66
+ return ToolResult(
67
+ success=False,
68
+ output="",
69
+ error="Missing 'path' parameter. Usage: write_file({\"path\": \"filename.py\", \"content\": \"code here\"})"
70
+ )
71
+ if content is None:
72
+ return ToolResult(
73
+ success=False,
74
+ output="",
75
+ error="Missing 'content' parameter. You MUST provide the file content. Usage: write_file({\"path\": \"filename.py\", \"content\": \"your code here\"})"
76
+ )
77
+
78
+ # Convert literal \n to actual newlines if needed
79
+ if '\\n' in content and '\n' not in content:
80
+ content = content.replace('\\n', '\n')
81
+ content = content.replace('\\t', '\t')
82
+ content = content.replace('\\"', '"')
83
+
84
+ # Strip trailing incomplete escape sequences
85
+ while content.endswith('\\') and not content.endswith('\\\\'):
86
+ content = content[:-1]
87
+
88
+ try:
89
+ result = write_file(path, content)
90
+ return ToolResult(success=True, output=result)
91
+ except Exception as e:
92
+ return ToolResult(success=False, output="", error=str(e))
93
+
94
+
95
+ class EditFileTool(Tool):
96
+ name = "edit_file"
97
+ description = "Make targeted edits by replacing old_string with new_string. PREFERRED over write_file for modifications."
98
+ parameters = {
99
+ "type": "object",
100
+ "properties": {
101
+ "path": {"type": "string", "description": "Path to the file to edit"},
102
+ "old_string": {"type": "string", "description": "Exact string to find (must be unique in file)"},
103
+ "new_string": {"type": "string", "description": "String to replace it with"},
104
+ },
105
+ "required": ["path", "old_string", "new_string"],
106
+ }
107
+
108
+ def execute(self, path: str, old_string: str, new_string: str) -> ToolResult:
109
+ try:
110
+ result = edit_file(path, old_string, new_string)
111
+ return ToolResult(success=True, output=result)
112
+ except Exception as e:
113
+ return ToolResult(success=False, output="", error=str(e))
114
+
115
+
116
+ class ListFilesTool(Tool):
117
+ name = "list_files"
118
+ description = "List files matching a glob pattern (e.g., '*.py', '**/*.js')"
119
+ parameters = {
120
+ "type": "object",
121
+ "properties": {
122
+ "pattern": {"type": "string", "description": "Glob pattern to match"},
123
+ "root": {"type": "string", "description": "Root directory (optional, defaults to cwd)"},
124
+ },
125
+ "required": ["pattern"],
126
+ }
127
+
128
+ def execute(self, pattern: str, root: str | None = None) -> ToolResult:
129
+ try:
130
+ files = list_files(pattern, root)
131
+ output = "\n".join(files) if files else "No files found"
132
+ return ToolResult(success=True, output=output)
133
+ except Exception as e:
134
+ return ToolResult(success=False, output="", error=str(e))
135
+
136
+
137
+ class SearchFilesTool(Tool):
138
+ name = "search_files"
139
+ description = "Search for text within files (like grep). Returns matching lines with file and line number."
140
+ parameters = {
141
+ "type": "object",
142
+ "properties": {
143
+ "query": {"type": "string", "description": "Text to search for (case-insensitive)"},
144
+ "pattern": {"type": "string", "description": "File glob pattern (default: **/*)"},
145
+ "context_lines": {"type": "integer", "description": "Lines of context around match (default: 0)"},
146
+ },
147
+ "required": ["query"],
148
+ }
149
+
150
+ def execute(self, query: str, pattern: str = "**/*", context_lines: int = 0) -> ToolResult:
151
+ try:
152
+ results = search_files(query, pattern, context_lines=context_lines)
153
+ if not results:
154
+ return ToolResult(success=True, output="No matches found")
155
+
156
+ output_lines = []
157
+ for r in results:
158
+ if "context" in r:
159
+ output_lines.append(f"\n{r['file']}:{r['line']}:")
160
+ output_lines.append(r["context"])
161
+ else:
162
+ output_lines.append(f"{r['file']}:{r['line']}: {r['content']}")
163
+
164
+ return ToolResult(success=True, output="\n".join(output_lines))
165
+ except Exception as e:
166
+ return ToolResult(success=False, output="", error=str(e))
167
+
168
+
169
+ class TreeTool(Tool):
170
+ name = "tree"
171
+ description = "Show directory structure as a tree view"
172
+ parameters = {
173
+ "type": "object",
174
+ "properties": {
175
+ "path": {"type": "string", "description": "Directory to show (default: current directory)"},
176
+ "max_depth": {"type": "integer", "description": "Maximum depth to traverse (default: 3)"},
177
+ },
178
+ "required": [],
179
+ }
180
+
181
+ def execute(self, path: str = ".", max_depth: int = 3) -> ToolResult:
182
+ try:
183
+ output = tree(path, max_depth=max_depth)
184
+ return ToolResult(success=True, output=output)
185
+ except Exception as e:
186
+ return ToolResult(success=False, output="", error=str(e))
187
+
188
+
189
+ class WebSearchTool(Tool):
190
+ name = "web_search"
191
+ description = "Search the web for current information using DuckDuckGo"
192
+ parameters = {
193
+ "type": "object",
194
+ "properties": {
195
+ "query": {"type": "string", "description": "Search query"},
196
+ },
197
+ "required": ["query"],
198
+ }
199
+
200
+ def execute(self, query: str) -> ToolResult:
201
+ try:
202
+ results = web_search(query, max_results=5)
203
+ output = format_search_results(results)
204
+ return ToolResult(success=True, output=output)
205
+ except Exception as e:
206
+ return ToolResult(success=False, output="", error=str(e))
207
+
208
+
209
+ class WebFetchTool(Tool):
210
+ name = "web_fetch"
211
+ description = "Fetch a URL and extract its text content. Use this to read API documentation, articles, etc."
212
+ parameters = {
213
+ "type": "object",
214
+ "properties": {
215
+ "url": {"type": "string", "description": "URL to fetch"},
216
+ },
217
+ "required": ["url"],
218
+ }
219
+
220
+ def execute(self, url: str) -> ToolResult:
221
+ try:
222
+ content = web_fetch(url, max_chars=8000)
223
+ return ToolResult(success=True, output=content)
224
+ except Exception as e:
225
+ return ToolResult(success=False, output="", error=str(e))
226
+
227
+
228
+ class BashTool(Tool):
229
+ name = "bash"
230
+ description = "Execute a shell command. Use for git, npm, pip, etc. Be careful with destructive commands."
231
+ parameters = {
232
+ "type": "object",
233
+ "properties": {
234
+ "command": {"type": "string", "description": "Shell command to execute"},
235
+ },
236
+ "required": ["command"],
237
+ }
238
+
239
+ # Comprehensive blocklist for dangerous commands
240
+ BLOCKED_PATTERNS = [
241
+ # Destructive file operations
242
+ "rm -rf /", "rm -rf /*", "rm -rf ~", "rm -rf $HOME",
243
+ "rm -rf .", "rm -r /",
244
+ # System damage
245
+ "mkfs", "dd if=", "> /dev/sd", "> /dev/null",
246
+ "chmod 777 /", "chmod -R 777 /",
247
+ "chown -R", ":(){:|:&};:", # Fork bomb
248
+ # Downloading and executing
249
+ "curl | bash", "curl | sh", "wget | bash", "wget | sh",
250
+ "curl -s | bash", "wget -q | sh",
251
+ # Network attacks
252
+ "nmap", "masscan",
253
+ # History/credential theft
254
+ "history -c", "> ~/.bash_history",
255
+ # Privilege escalation
256
+ "sudo rm", "sudo dd", "sudo mkfs",
257
+ ]
258
+
259
+ # Commands that need extra caution (will show warning)
260
+ WARN_PATTERNS = [
261
+ "rm -rf", "rm -r",
262
+ "git push --force", "git push -f",
263
+ "git reset --hard",
264
+ "drop table", "drop database",
265
+ "truncate",
266
+ ]
267
+
268
+ # Python command patterns that may fail if Python isn't installed
269
+ PYTHON_PATTERNS = ["python ", "python3 ", "python.exe"]
270
+
271
+ def _get_python_command(self) -> str | None:
272
+ """Get the available Python command, preferring python3."""
273
+ # Check environment variable set by main.py
274
+ env_cmd = os.environ.get("KAIRO_PYTHON_CMD")
275
+ if env_cmd and shutil.which(env_cmd):
276
+ return env_cmd
277
+ # Fallback: check directly
278
+ for cmd in ["python3", "python"]:
279
+ if shutil.which(cmd):
280
+ return cmd
281
+ return None
282
+
283
+ def _get_python_install_hint(self) -> str:
284
+ """Get platform-specific Python install instructions."""
285
+ import platform
286
+ system = platform.system().lower()
287
+ if "linux" in system:
288
+ if shutil.which("apt-get"):
289
+ return "sudo apt-get install python3"
290
+ elif shutil.which("dnf"):
291
+ return "sudo dnf install python3"
292
+ elif shutil.which("pacman"):
293
+ return "sudo pacman -S python"
294
+ return "Install Python 3 using your package manager"
295
+ elif "darwin" in system:
296
+ return "brew install python3"
297
+ elif "windows" in system:
298
+ return "winget install Python.Python.3"
299
+ return "Download from https://python.org"
300
+
301
+ def _is_python_command(self, command: str) -> bool:
302
+ """Check if command is trying to run Python."""
303
+ cmd_lower = command.lower().strip()
304
+ return any(cmd_lower.startswith(p) for p in self.PYTHON_PATTERNS)
305
+
306
+ def _rewrite_python_command(self, command: str) -> str:
307
+ """Rewrite 'python' to 'python3' if needed."""
308
+ python_cmd = self._get_python_command()
309
+ if not python_cmd:
310
+ return command # Can't help, let it fail with good error
311
+
312
+ # Replace 'python ' with the correct command
313
+ if command.strip().startswith("python "):
314
+ return python_cmd + command[6:]
315
+ return command
316
+
317
+ def _make_noninteractive(self, command: str) -> str:
318
+ """Add flags to make commands non-interactive (prevents hangs)."""
319
+ # apt/apt-get install needs -y for non-interactive mode
320
+ # Match apt-get install or apt install without -y
321
+ if re.search(r'\b(apt-get|apt)\s+install\b', command) and ' -y' not in command:
322
+ # Insert -y after install
323
+ command = re.sub(
324
+ r'\b(apt-get|apt)\s+install\b',
325
+ r'\1 install -y',
326
+ command
327
+ )
328
+ # pip install with --yes doesn't exist, but pip is non-interactive by default
329
+ return command
330
+
331
+ def execute(self, command: str) -> ToolResult:
332
+ # Check for blocked commands
333
+ command_lower = command.lower()
334
+ for pattern in self.BLOCKED_PATTERNS:
335
+ if pattern.lower() in command_lower:
336
+ return ToolResult(
337
+ success=False,
338
+ output="",
339
+ error=f"Command blocked for safety: contains '{pattern}'"
340
+ )
341
+
342
+ # Check for warning patterns (still execute but warn)
343
+ warnings = []
344
+ for pattern in self.WARN_PATTERNS:
345
+ if pattern.lower() in command_lower:
346
+ warnings.append(f"Warning: command contains '{pattern}'")
347
+
348
+ # Check if this is a Python command and handle accordingly
349
+ is_python_cmd = self._is_python_command(command)
350
+ if is_python_cmd:
351
+ python_cmd = self._get_python_command()
352
+ if not python_cmd:
353
+ # Python not installed at all
354
+ hint = self._get_python_install_hint()
355
+ return ToolResult(
356
+ success=False,
357
+ output="",
358
+ error=f"Python is not installed on this system.\n"
359
+ f"Install it with: {hint}\n"
360
+ f"Then try running the command again."
361
+ )
362
+ # Rewrite 'python' to the correct command (e.g., 'python3')
363
+ command = self._rewrite_python_command(command)
364
+
365
+ # Make interactive commands non-interactive (prevents hangs)
366
+ command = self._make_noninteractive(command)
367
+
368
+ try:
369
+ result = subprocess.run(
370
+ command,
371
+ shell=True,
372
+ capture_output=True,
373
+ text=True,
374
+ timeout=120, # 2 minute timeout
375
+ cwd=None, # Use current directory
376
+ )
377
+
378
+ output = result.stdout
379
+ if result.stderr:
380
+ output += f"\n[stderr]: {result.stderr}"
381
+
382
+ if warnings:
383
+ output = "\n".join(warnings) + "\n\n" + output
384
+
385
+ # Special handling for exit code 127 (command not found)
386
+ if result.returncode == 127:
387
+ error_msg = f"Exit code: 127 (command not found)"
388
+ if is_python_cmd:
389
+ hint = self._get_python_install_hint()
390
+ error_msg += f"\nPython may not be installed. Install with: {hint}"
391
+ return ToolResult(
392
+ success=False,
393
+ output=output[:10000],
394
+ error=error_msg,
395
+ )
396
+
397
+ return ToolResult(
398
+ success=result.returncode == 0,
399
+ output=output[:10000], # Limit output size
400
+ error=None if result.returncode == 0 else f"Exit code: {result.returncode}",
401
+ )
402
+
403
+ except subprocess.TimeoutExpired:
404
+ return ToolResult(
405
+ success=False,
406
+ output="",
407
+ error="Command timed out after 120 seconds"
408
+ )
409
+ except Exception as e:
410
+ return ToolResult(success=False, output="", error=str(e))
411
+
412
+
413
+ class GitStatusTool(Tool):
414
+ name = "git_status"
415
+ description = "Show git status (staged, unstaged, untracked files)"
416
+ parameters = {
417
+ "type": "object",
418
+ "properties": {},
419
+ "required": [],
420
+ }
421
+
422
+ def execute(self) -> ToolResult:
423
+ try:
424
+ result = subprocess.run(
425
+ ["git", "status", "--porcelain", "-b"],
426
+ capture_output=True,
427
+ text=True,
428
+ timeout=30,
429
+ )
430
+
431
+ if result.returncode != 0:
432
+ return ToolResult(
433
+ success=False,
434
+ output="",
435
+ error=result.stderr or "Not a git repository"
436
+ )
437
+
438
+ return ToolResult(success=True, output=result.stdout)
439
+ except Exception as e:
440
+ return ToolResult(success=False, output="", error=str(e))
441
+
442
+
443
+ class GitDiffTool(Tool):
444
+ name = "git_diff"
445
+ description = "Show git diff (unstaged changes, or staged with --staged)"
446
+ parameters = {
447
+ "type": "object",
448
+ "properties": {
449
+ "staged": {"type": "boolean", "description": "Show staged changes only"},
450
+ "file": {"type": "string", "description": "Specific file to diff (optional)"},
451
+ },
452
+ "required": [],
453
+ }
454
+
455
+ def execute(self, staged: bool = False, file: str = None) -> ToolResult:
456
+ try:
457
+ cmd = ["git", "diff"]
458
+ if staged:
459
+ cmd.append("--staged")
460
+ if file:
461
+ cmd.append(file)
462
+
463
+ result = subprocess.run(
464
+ cmd,
465
+ capture_output=True,
466
+ text=True,
467
+ timeout=30,
468
+ )
469
+
470
+ output = result.stdout or "(no changes)"
471
+ return ToolResult(success=True, output=output[:10000])
472
+ except Exception as e:
473
+ return ToolResult(success=False, output="", error=str(e))
474
+
475
+
476
+ class GitCommitTool(Tool):
477
+ name = "git_commit"
478
+ description = "Stage files and create a git commit"
479
+ parameters = {
480
+ "type": "object",
481
+ "properties": {
482
+ "message": {"type": "string", "description": "Commit message"},
483
+ "files": {
484
+ "type": "array",
485
+ "items": {"type": "string"},
486
+ "description": "Files to stage (use ['.'] for all)",
487
+ },
488
+ },
489
+ "required": ["message"],
490
+ }
491
+
492
+ def execute(self, message: str, files: list[str] | None = None) -> ToolResult:
493
+ try:
494
+ # Stage files
495
+ stage_files = files or ["."]
496
+ result = subprocess.run(
497
+ ["git", "add"] + stage_files,
498
+ capture_output=True, text=True, timeout=30,
499
+ )
500
+ if result.returncode != 0:
501
+ return ToolResult(success=False, output="", error=f"git add failed: {result.stderr}")
502
+
503
+ # Commit
504
+ result = subprocess.run(
505
+ ["git", "commit", "-m", message],
506
+ capture_output=True, text=True, timeout=30,
507
+ )
508
+ if result.returncode != 0:
509
+ return ToolResult(success=False, output="", error=result.stderr or "Nothing to commit")
510
+
511
+ return ToolResult(success=True, output=result.stdout)
512
+ except Exception as e:
513
+ return ToolResult(success=False, output="", error=str(e))
514
+
515
+
516
+ class GitPushTool(Tool):
517
+ name = "git_push"
518
+ description = "Push commits to remote repository"
519
+ parameters = {
520
+ "type": "object",
521
+ "properties": {
522
+ "remote": {"type": "string", "description": "Remote name (default: origin)"},
523
+ "branch": {"type": "string", "description": "Branch name (default: current branch)"},
524
+ },
525
+ "required": [],
526
+ }
527
+
528
+ def execute(self, remote: str = "origin", branch: str | None = None) -> ToolResult:
529
+ try:
530
+ cmd = ["git", "push", remote]
531
+ if branch:
532
+ cmd.append(branch)
533
+
534
+ result = subprocess.run(
535
+ cmd, capture_output=True, text=True, timeout=60,
536
+ )
537
+ output = result.stdout + result.stderr
538
+ if result.returncode != 0:
539
+ return ToolResult(success=False, output="", error=output or "Push failed")
540
+
541
+ return ToolResult(success=True, output=output or "Push successful")
542
+ except Exception as e:
543
+ return ToolResult(success=False, output="", error=str(e))
544
+
545
+
546
+ def create_default_registry() -> ToolRegistry:
547
+ """Create a registry with all default tools."""
548
+ registry = ToolRegistry()
549
+
550
+ # File tools
551
+ registry.register(ReadFileTool())
552
+ registry.register(WriteFileTool())
553
+ registry.register(EditFileTool())
554
+ registry.register(ListFilesTool())
555
+ registry.register(SearchFilesTool())
556
+ registry.register(TreeTool())
557
+
558
+ # Web tools
559
+ registry.register(WebSearchTool())
560
+ registry.register(WebFetchTool())
561
+
562
+ # Shell tools
563
+ registry.register(BashTool())
564
+
565
+ # Git tools
566
+ registry.register(GitStatusTool())
567
+ registry.register(GitDiffTool())
568
+ registry.register(GitCommitTool())
569
+ registry.register(GitPushTool())
570
+
571
+ # Code analysis tools
572
+ registry.register(LintTool())
573
+ registry.register(TypeCheckTool())
574
+ registry.register(RunTestsTool())
575
+ registry.register(SecurityScanTool())
576
+ registry.register(CodeComplexityTool())
577
+ registry.register(FindDeadCodeTool())
578
+ registry.register(GetDiagnosticsTool())
579
+
580
+ # Code review tools
581
+ registry.register(CodeReviewTool())
582
+ registry.register(ModularityCheckTool())
583
+ registry.register(DependencyGraphTool())
584
+
585
+ return registry
586
+
587
+
588
+ def create_readonly_registry() -> ToolRegistry:
589
+ """Create a registry with only read-only tools (for planning/exploring)."""
590
+ registry = ToolRegistry()
591
+
592
+ # Read-only file tools
593
+ registry.register(ReadFileTool())
594
+ registry.register(ListFilesTool())
595
+ registry.register(SearchFilesTool())
596
+ registry.register(TreeTool())
597
+
598
+ # Web tools (read-only)
599
+ registry.register(WebSearchTool())
600
+ registry.register(WebFetchTool())
601
+
602
+ # Git tools (read-only)
603
+ registry.register(GitStatusTool())
604
+ registry.register(GitDiffTool())
605
+
606
+ # Code analysis tools (read-only — they inspect but don't modify)
607
+ registry.register(LintTool())
608
+ registry.register(TypeCheckTool())
609
+ registry.register(SecurityScanTool())
610
+ registry.register(CodeComplexityTool())
611
+ registry.register(FindDeadCodeTool())
612
+ registry.register(GetDiagnosticsTool())
613
+ registry.register(CodeReviewTool())
614
+ registry.register(ModularityCheckTool())
615
+ registry.register(DependencyGraphTool())
616
+
617
+ return registry