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.
- image-service/main.py +178 -0
- infra/chat/app/main.py +84 -0
- kairo/backend/__init__.py +0 -0
- kairo/backend/api/__init__.py +0 -0
- kairo/backend/api/admin/__init__.py +23 -0
- kairo/backend/api/admin/audit.py +54 -0
- kairo/backend/api/admin/content.py +142 -0
- kairo/backend/api/admin/incidents.py +148 -0
- kairo/backend/api/admin/stats.py +125 -0
- kairo/backend/api/admin/system.py +87 -0
- kairo/backend/api/admin/users.py +279 -0
- kairo/backend/api/agents.py +94 -0
- kairo/backend/api/api_keys.py +85 -0
- kairo/backend/api/auth.py +116 -0
- kairo/backend/api/billing.py +41 -0
- kairo/backend/api/chat.py +72 -0
- kairo/backend/api/conversations.py +125 -0
- kairo/backend/api/device_auth.py +100 -0
- kairo/backend/api/files.py +83 -0
- kairo/backend/api/health.py +36 -0
- kairo/backend/api/images.py +80 -0
- kairo/backend/api/openai_compat.py +225 -0
- kairo/backend/api/projects.py +102 -0
- kairo/backend/api/usage.py +32 -0
- kairo/backend/api/webhooks.py +79 -0
- kairo/backend/app.py +297 -0
- kairo/backend/config.py +179 -0
- kairo/backend/core/__init__.py +0 -0
- kairo/backend/core/admin_auth.py +24 -0
- kairo/backend/core/api_key_auth.py +55 -0
- kairo/backend/core/database.py +28 -0
- kairo/backend/core/dependencies.py +70 -0
- kairo/backend/core/logging.py +23 -0
- kairo/backend/core/rate_limit.py +73 -0
- kairo/backend/core/security.py +29 -0
- kairo/backend/models/__init__.py +19 -0
- kairo/backend/models/agent.py +30 -0
- kairo/backend/models/api_key.py +25 -0
- kairo/backend/models/api_usage.py +29 -0
- kairo/backend/models/audit_log.py +26 -0
- kairo/backend/models/conversation.py +48 -0
- kairo/backend/models/device_code.py +30 -0
- kairo/backend/models/feature_flag.py +21 -0
- kairo/backend/models/image_generation.py +24 -0
- kairo/backend/models/incident.py +28 -0
- kairo/backend/models/project.py +28 -0
- kairo/backend/models/uptime_record.py +24 -0
- kairo/backend/models/usage.py +24 -0
- kairo/backend/models/user.py +49 -0
- kairo/backend/schemas/__init__.py +0 -0
- kairo/backend/schemas/admin/__init__.py +0 -0
- kairo/backend/schemas/admin/audit.py +28 -0
- kairo/backend/schemas/admin/content.py +53 -0
- kairo/backend/schemas/admin/stats.py +77 -0
- kairo/backend/schemas/admin/system.py +44 -0
- kairo/backend/schemas/admin/users.py +48 -0
- kairo/backend/schemas/agent.py +42 -0
- kairo/backend/schemas/api_key.py +30 -0
- kairo/backend/schemas/auth.py +57 -0
- kairo/backend/schemas/chat.py +26 -0
- kairo/backend/schemas/conversation.py +39 -0
- kairo/backend/schemas/device_auth.py +40 -0
- kairo/backend/schemas/image.py +15 -0
- kairo/backend/schemas/openai_compat.py +76 -0
- kairo/backend/schemas/project.py +21 -0
- kairo/backend/schemas/status.py +81 -0
- kairo/backend/schemas/usage.py +15 -0
- kairo/backend/services/__init__.py +0 -0
- kairo/backend/services/admin/__init__.py +0 -0
- kairo/backend/services/admin/audit_service.py +78 -0
- kairo/backend/services/admin/content_service.py +119 -0
- kairo/backend/services/admin/incident_service.py +94 -0
- kairo/backend/services/admin/stats_service.py +281 -0
- kairo/backend/services/admin/system_service.py +126 -0
- kairo/backend/services/admin/user_service.py +157 -0
- kairo/backend/services/agent_service.py +107 -0
- kairo/backend/services/api_key_service.py +66 -0
- kairo/backend/services/api_usage_service.py +126 -0
- kairo/backend/services/auth_service.py +101 -0
- kairo/backend/services/chat_service.py +501 -0
- kairo/backend/services/conversation_service.py +264 -0
- kairo/backend/services/device_auth_service.py +193 -0
- kairo/backend/services/email_service.py +55 -0
- kairo/backend/services/image_service.py +181 -0
- kairo/backend/services/llm_service.py +186 -0
- kairo/backend/services/project_service.py +109 -0
- kairo/backend/services/status_service.py +167 -0
- kairo/backend/services/stripe_service.py +78 -0
- kairo/backend/services/usage_service.py +150 -0
- kairo/backend/services/web_search_service.py +96 -0
- kairo/migrations/env.py +60 -0
- kairo/migrations/versions/001_initial.py +55 -0
- kairo/migrations/versions/002_usage_tracking_and_indexes.py +66 -0
- kairo/migrations/versions/003_username_to_email.py +21 -0
- kairo/migrations/versions/004_add_plans_and_verification.py +67 -0
- kairo/migrations/versions/005_add_projects.py +52 -0
- kairo/migrations/versions/006_add_image_generation.py +63 -0
- kairo/migrations/versions/007_add_admin_portal.py +107 -0
- kairo/migrations/versions/008_add_device_code_auth.py +76 -0
- kairo/migrations/versions/009_add_status_page.py +65 -0
- kairo/tools/extract_claude_data.py +465 -0
- kairo/tools/filter_claude_data.py +303 -0
- kairo/tools/generate_curated_data.py +157 -0
- kairo/tools/mix_training_data.py +295 -0
- kairo_code/__init__.py +3 -0
- kairo_code/agents/__init__.py +25 -0
- kairo_code/agents/architect.py +98 -0
- kairo_code/agents/audit.py +100 -0
- kairo_code/agents/base.py +463 -0
- kairo_code/agents/coder.py +155 -0
- kairo_code/agents/database.py +77 -0
- kairo_code/agents/docs.py +88 -0
- kairo_code/agents/explorer.py +62 -0
- kairo_code/agents/guardian.py +80 -0
- kairo_code/agents/planner.py +66 -0
- kairo_code/agents/reviewer.py +91 -0
- kairo_code/agents/security.py +94 -0
- kairo_code/agents/terraform.py +88 -0
- kairo_code/agents/testing.py +97 -0
- kairo_code/agents/uiux.py +88 -0
- kairo_code/auth.py +232 -0
- kairo_code/config.py +172 -0
- kairo_code/conversation.py +173 -0
- kairo_code/heartbeat.py +63 -0
- kairo_code/llm.py +291 -0
- kairo_code/logging_config.py +156 -0
- kairo_code/main.py +818 -0
- kairo_code/router.py +217 -0
- kairo_code/sandbox.py +248 -0
- kairo_code/settings.py +183 -0
- kairo_code/tools/__init__.py +51 -0
- kairo_code/tools/analysis.py +509 -0
- kairo_code/tools/base.py +417 -0
- kairo_code/tools/code.py +58 -0
- kairo_code/tools/definitions.py +617 -0
- kairo_code/tools/files.py +315 -0
- kairo_code/tools/review.py +390 -0
- kairo_code/tools/search.py +185 -0
- kairo_code/ui.py +418 -0
- kairo_code-0.1.0.dist-info/METADATA +13 -0
- kairo_code-0.1.0.dist-info/RECORD +144 -0
- kairo_code-0.1.0.dist-info/WHEEL +5 -0
- kairo_code-0.1.0.dist-info/entry_points.txt +2 -0
- 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
|