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,509 @@
|
|
|
1
|
+
"""Code analysis and review tools for Kairo Code"""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .base import Tool, ToolResult
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LintTool(Tool):
|
|
13
|
+
"""Run linters on code to find issues."""
|
|
14
|
+
|
|
15
|
+
name = "lint"
|
|
16
|
+
description = "Run linters (flake8/pylint for Python, eslint for JS) to find code issues"
|
|
17
|
+
parameters = {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"properties": {
|
|
20
|
+
"path": {"type": "string", "description": "File or directory to lint"},
|
|
21
|
+
"fix": {"type": "boolean", "description": "Auto-fix issues if possible (default: False)"},
|
|
22
|
+
},
|
|
23
|
+
"required": ["path"],
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
def execute(self, path: str, fix: bool = False) -> ToolResult:
|
|
27
|
+
path = Path(path)
|
|
28
|
+
|
|
29
|
+
if not path.exists():
|
|
30
|
+
return ToolResult(success=False, output="", error=f"Path not found: {path}")
|
|
31
|
+
|
|
32
|
+
# Detect language and run appropriate linter
|
|
33
|
+
if path.suffix == ".py" or (path.is_dir() and list(path.glob("**/*.py"))):
|
|
34
|
+
return self._lint_python(path, fix)
|
|
35
|
+
elif path.suffix in (".js", ".ts", ".jsx", ".tsx") or (path.is_dir() and list(path.glob("**/*.js"))):
|
|
36
|
+
return self._lint_javascript(path, fix)
|
|
37
|
+
else:
|
|
38
|
+
return ToolResult(success=False, output="", error="Unsupported file type for linting")
|
|
39
|
+
|
|
40
|
+
def _lint_python(self, path: Path, fix: bool) -> ToolResult:
|
|
41
|
+
# Try ruff first (fast), then flake8, then pylint
|
|
42
|
+
for linter in ["ruff", "flake8", "pylint"]:
|
|
43
|
+
try:
|
|
44
|
+
if linter == "ruff":
|
|
45
|
+
cmd = ["ruff", "check", str(path)]
|
|
46
|
+
if fix:
|
|
47
|
+
cmd.append("--fix")
|
|
48
|
+
elif linter == "flake8":
|
|
49
|
+
cmd = ["flake8", "--max-line-length=100", str(path)]
|
|
50
|
+
else:
|
|
51
|
+
cmd = ["pylint", "--output-format=text", "--score=no", str(path)]
|
|
52
|
+
|
|
53
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
54
|
+
|
|
55
|
+
output = result.stdout or result.stderr or "No issues found"
|
|
56
|
+
|
|
57
|
+
# Count issues
|
|
58
|
+
lines = output.strip().split("\n") if output.strip() else []
|
|
59
|
+
issue_count = len([l for l in lines if l.strip() and not l.startswith("*")])
|
|
60
|
+
|
|
61
|
+
return ToolResult(
|
|
62
|
+
success=True,
|
|
63
|
+
output=f"[{linter}] Found {issue_count} issues:\n\n{output[:5000]}"
|
|
64
|
+
)
|
|
65
|
+
except FileNotFoundError:
|
|
66
|
+
continue
|
|
67
|
+
except Exception as e:
|
|
68
|
+
return ToolResult(success=False, output="", error=str(e))
|
|
69
|
+
|
|
70
|
+
return ToolResult(
|
|
71
|
+
success=False,
|
|
72
|
+
output="",
|
|
73
|
+
error="No Python linter found. Install: pip install ruff flake8 pylint"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def _lint_javascript(self, path: Path, fix: bool) -> ToolResult:
|
|
77
|
+
try:
|
|
78
|
+
cmd = ["npx", "eslint", str(path), "--format=stylish"]
|
|
79
|
+
if fix:
|
|
80
|
+
cmd.append("--fix")
|
|
81
|
+
|
|
82
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
83
|
+
output = result.stdout or result.stderr or "No issues found"
|
|
84
|
+
|
|
85
|
+
return ToolResult(success=True, output=output[:5000])
|
|
86
|
+
except FileNotFoundError:
|
|
87
|
+
return ToolResult(success=False, output="", error="eslint not found. Run: npm install eslint")
|
|
88
|
+
except Exception as e:
|
|
89
|
+
return ToolResult(success=False, output="", error=str(e))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class TypeCheckTool(Tool):
|
|
93
|
+
"""Run type checkers on code."""
|
|
94
|
+
|
|
95
|
+
name = "typecheck"
|
|
96
|
+
description = "Run type checker (mypy for Python, tsc for TypeScript) to find type errors"
|
|
97
|
+
parameters = {
|
|
98
|
+
"type": "object",
|
|
99
|
+
"properties": {
|
|
100
|
+
"path": {"type": "string", "description": "File or directory to type check"},
|
|
101
|
+
"strict": {"type": "boolean", "description": "Use strict mode (default: False)"},
|
|
102
|
+
},
|
|
103
|
+
"required": ["path"],
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
def execute(self, path: str, strict: bool = False) -> ToolResult:
|
|
107
|
+
path = Path(path)
|
|
108
|
+
|
|
109
|
+
if not path.exists():
|
|
110
|
+
return ToolResult(success=False, output="", error=f"Path not found: {path}")
|
|
111
|
+
|
|
112
|
+
if path.suffix == ".py" or (path.is_dir() and list(path.glob("**/*.py"))):
|
|
113
|
+
return self._typecheck_python(path, strict)
|
|
114
|
+
elif path.suffix == ".ts" or (path.is_dir() and list(path.glob("**/*.ts"))):
|
|
115
|
+
return self._typecheck_typescript(path, strict)
|
|
116
|
+
else:
|
|
117
|
+
return ToolResult(success=False, output="", error="Unsupported file type")
|
|
118
|
+
|
|
119
|
+
def _typecheck_python(self, path: Path, strict: bool) -> ToolResult:
|
|
120
|
+
try:
|
|
121
|
+
cmd = ["mypy", str(path)]
|
|
122
|
+
if strict:
|
|
123
|
+
cmd.append("--strict")
|
|
124
|
+
|
|
125
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
|
126
|
+
output = result.stdout or result.stderr
|
|
127
|
+
|
|
128
|
+
# Parse results
|
|
129
|
+
lines = output.strip().split("\n") if output.strip() else []
|
|
130
|
+
error_count = len([l for l in lines if ": error:" in l])
|
|
131
|
+
|
|
132
|
+
return ToolResult(
|
|
133
|
+
success=result.returncode == 0,
|
|
134
|
+
output=f"Found {error_count} type errors:\n\n{output[:5000]}",
|
|
135
|
+
error=None if result.returncode == 0 else f"Type check failed with {error_count} errors"
|
|
136
|
+
)
|
|
137
|
+
except FileNotFoundError:
|
|
138
|
+
return ToolResult(success=False, output="", error="mypy not found. Install: pip install mypy")
|
|
139
|
+
except Exception as e:
|
|
140
|
+
return ToolResult(success=False, output="", error=str(e))
|
|
141
|
+
|
|
142
|
+
def _typecheck_typescript(self, path: Path, strict: bool) -> ToolResult:
|
|
143
|
+
try:
|
|
144
|
+
cmd = ["npx", "tsc", "--noEmit", str(path)]
|
|
145
|
+
if strict:
|
|
146
|
+
cmd.append("--strict")
|
|
147
|
+
|
|
148
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
|
149
|
+
output = result.stdout or result.stderr or "No type errors found"
|
|
150
|
+
|
|
151
|
+
return ToolResult(success=result.returncode == 0, output=output[:5000])
|
|
152
|
+
except FileNotFoundError:
|
|
153
|
+
return ToolResult(success=False, output="", error="TypeScript not found. Run: npm install typescript")
|
|
154
|
+
except Exception as e:
|
|
155
|
+
return ToolResult(success=False, output="", error=str(e))
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class RunTestsTool(Tool):
|
|
159
|
+
"""Run tests and show results."""
|
|
160
|
+
|
|
161
|
+
name = "run_tests"
|
|
162
|
+
description = "Run tests (pytest for Python, jest/vitest for JS) and show results"
|
|
163
|
+
parameters = {
|
|
164
|
+
"type": "object",
|
|
165
|
+
"properties": {
|
|
166
|
+
"path": {"type": "string", "description": "Test file/directory (default: auto-detect)"},
|
|
167
|
+
"verbose": {"type": "boolean", "description": "Show verbose output (default: False)"},
|
|
168
|
+
"filter": {"type": "string", "description": "Only run tests matching this pattern"},
|
|
169
|
+
},
|
|
170
|
+
"required": [],
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
def execute(self, path: str = ".", verbose: bool = False, filter: str = None) -> ToolResult:
|
|
174
|
+
# Auto-detect test framework
|
|
175
|
+
cwd = Path(path) if Path(path).is_dir() else Path(".")
|
|
176
|
+
|
|
177
|
+
# Check for Python tests
|
|
178
|
+
if list(cwd.glob("**/test_*.py")) or list(cwd.glob("**/*_test.py")) or (cwd / "pytest.ini").exists():
|
|
179
|
+
return self._run_pytest(path, verbose, filter)
|
|
180
|
+
|
|
181
|
+
# Check for JS tests
|
|
182
|
+
if (cwd / "package.json").exists():
|
|
183
|
+
return self._run_js_tests(cwd, verbose, filter)
|
|
184
|
+
|
|
185
|
+
return ToolResult(success=False, output="", error="No test framework detected")
|
|
186
|
+
|
|
187
|
+
def _run_pytest(self, path: str, verbose: bool, filter: str) -> ToolResult:
|
|
188
|
+
try:
|
|
189
|
+
cmd = ["pytest", path, "--tb=short"]
|
|
190
|
+
if verbose:
|
|
191
|
+
cmd.append("-v")
|
|
192
|
+
if filter:
|
|
193
|
+
cmd.extend(["-k", filter])
|
|
194
|
+
|
|
195
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
|
196
|
+
output = result.stdout + "\n" + result.stderr
|
|
197
|
+
|
|
198
|
+
# Parse summary
|
|
199
|
+
passed = output.count(" passed")
|
|
200
|
+
failed = output.count(" failed")
|
|
201
|
+
|
|
202
|
+
return ToolResult(
|
|
203
|
+
success=result.returncode == 0,
|
|
204
|
+
output=f"Tests: {passed} passed, {failed} failed\n\n{output[:8000]}",
|
|
205
|
+
error=None if result.returncode == 0 else f"{failed} tests failed"
|
|
206
|
+
)
|
|
207
|
+
except FileNotFoundError:
|
|
208
|
+
return ToolResult(success=False, output="", error="pytest not found. Install: pip install pytest")
|
|
209
|
+
except subprocess.TimeoutExpired:
|
|
210
|
+
return ToolResult(success=False, output="", error="Tests timed out after 5 minutes")
|
|
211
|
+
except Exception as e:
|
|
212
|
+
return ToolResult(success=False, output="", error=str(e))
|
|
213
|
+
|
|
214
|
+
def _run_js_tests(self, cwd: Path, verbose: bool, filter: str) -> ToolResult:
|
|
215
|
+
try:
|
|
216
|
+
# Try npm test first
|
|
217
|
+
cmd = ["npm", "test", "--"]
|
|
218
|
+
if filter:
|
|
219
|
+
cmd.extend(["--testNamePattern", filter])
|
|
220
|
+
|
|
221
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300, cwd=cwd)
|
|
222
|
+
output = result.stdout + "\n" + result.stderr
|
|
223
|
+
|
|
224
|
+
return ToolResult(
|
|
225
|
+
success=result.returncode == 0,
|
|
226
|
+
output=output[:8000]
|
|
227
|
+
)
|
|
228
|
+
except Exception as e:
|
|
229
|
+
return ToolResult(success=False, output="", error=str(e))
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class SecurityScanTool(Tool):
|
|
233
|
+
"""Scan code for security vulnerabilities."""
|
|
234
|
+
|
|
235
|
+
name = "security_scan"
|
|
236
|
+
description = "Scan code for security issues (bandit for Python, npm audit for JS)"
|
|
237
|
+
parameters = {
|
|
238
|
+
"type": "object",
|
|
239
|
+
"properties": {
|
|
240
|
+
"path": {"type": "string", "description": "File or directory to scan"},
|
|
241
|
+
"severity": {"type": "string", "description": "Minimum severity: low, medium, high (default: medium)"},
|
|
242
|
+
},
|
|
243
|
+
"required": ["path"],
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
def execute(self, path: str, severity: str = "medium") -> ToolResult:
|
|
247
|
+
path = Path(path)
|
|
248
|
+
results = []
|
|
249
|
+
|
|
250
|
+
# Python security scan
|
|
251
|
+
if path.suffix == ".py" or list(path.glob("**/*.py") if path.is_dir() else []):
|
|
252
|
+
py_result = self._scan_python(path, severity)
|
|
253
|
+
if py_result:
|
|
254
|
+
results.append(py_result)
|
|
255
|
+
|
|
256
|
+
# Check for requirements.txt vulnerabilities
|
|
257
|
+
req_files = list(path.glob("**/requirements*.txt") if path.is_dir() else [])
|
|
258
|
+
if req_files or (path / "requirements.txt").exists():
|
|
259
|
+
dep_result = self._scan_python_deps(path)
|
|
260
|
+
if dep_result:
|
|
261
|
+
results.append(dep_result)
|
|
262
|
+
|
|
263
|
+
# Node.js security scan
|
|
264
|
+
if (path / "package.json").exists() if path.is_dir() else False:
|
|
265
|
+
js_result = self._scan_javascript(path)
|
|
266
|
+
if js_result:
|
|
267
|
+
results.append(js_result)
|
|
268
|
+
|
|
269
|
+
if not results:
|
|
270
|
+
return ToolResult(success=True, output="No security issues found (or no scanners available)")
|
|
271
|
+
|
|
272
|
+
return ToolResult(
|
|
273
|
+
success=all(r.get("success", True) for r in results),
|
|
274
|
+
output="\n\n".join(r.get("output", "") for r in results)[:8000]
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def _scan_python(self, path: Path, severity: str) -> dict | None:
|
|
278
|
+
try:
|
|
279
|
+
sev_map = {"low": "l", "medium": "m", "high": "h"}
|
|
280
|
+
cmd = ["bandit", "-r", str(path), "-f", "txt", f"-l{sev_map.get(severity, 'm')}"]
|
|
281
|
+
|
|
282
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
|
283
|
+
output = result.stdout or "No issues found"
|
|
284
|
+
|
|
285
|
+
return {"success": True, "output": f"[Bandit Python Security Scan]\n{output}"}
|
|
286
|
+
except FileNotFoundError:
|
|
287
|
+
return None
|
|
288
|
+
except Exception:
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
def _scan_python_deps(self, path: Path) -> dict | None:
|
|
292
|
+
try:
|
|
293
|
+
cmd = ["safety", "check", "-r", str(path / "requirements.txt")]
|
|
294
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
295
|
+
output = result.stdout or result.stderr or "No vulnerable dependencies found"
|
|
296
|
+
|
|
297
|
+
return {"success": result.returncode == 0, "output": f"[Dependency Security]\n{output}"}
|
|
298
|
+
except FileNotFoundError:
|
|
299
|
+
return None
|
|
300
|
+
except Exception:
|
|
301
|
+
return None
|
|
302
|
+
|
|
303
|
+
def _scan_javascript(self, path: Path) -> dict | None:
|
|
304
|
+
try:
|
|
305
|
+
result = subprocess.run(
|
|
306
|
+
["npm", "audit", "--json"],
|
|
307
|
+
capture_output=True, text=True, timeout=60, cwd=path
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
try:
|
|
311
|
+
audit_data = json.loads(result.stdout)
|
|
312
|
+
vulns = audit_data.get("metadata", {}).get("vulnerabilities", {})
|
|
313
|
+
summary = f"Critical: {vulns.get('critical', 0)}, High: {vulns.get('high', 0)}, Medium: {vulns.get('moderate', 0)}"
|
|
314
|
+
return {"success": True, "output": f"[npm audit]\n{summary}\n\n{result.stdout[:2000]}"}
|
|
315
|
+
except json.JSONDecodeError:
|
|
316
|
+
return {"success": True, "output": f"[npm audit]\n{result.stdout[:2000]}"}
|
|
317
|
+
except Exception:
|
|
318
|
+
return None
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class CodeComplexityTool(Tool):
|
|
322
|
+
"""Analyze code complexity."""
|
|
323
|
+
|
|
324
|
+
name = "complexity"
|
|
325
|
+
description = "Analyze code complexity (cyclomatic complexity, lines of code, etc.)"
|
|
326
|
+
parameters = {
|
|
327
|
+
"type": "object",
|
|
328
|
+
"properties": {
|
|
329
|
+
"path": {"type": "string", "description": "File or directory to analyze"},
|
|
330
|
+
},
|
|
331
|
+
"required": ["path"],
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
def execute(self, path: str) -> ToolResult:
|
|
335
|
+
path = Path(path)
|
|
336
|
+
|
|
337
|
+
if not path.exists():
|
|
338
|
+
return ToolResult(success=False, output="", error=f"Path not found: {path}")
|
|
339
|
+
|
|
340
|
+
try:
|
|
341
|
+
# Try radon for Python
|
|
342
|
+
cmd = ["radon", "cc", str(path), "-s", "-a"]
|
|
343
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
344
|
+
|
|
345
|
+
if result.returncode == 0:
|
|
346
|
+
output = result.stdout or "No complexity data"
|
|
347
|
+
|
|
348
|
+
# Also get maintainability index
|
|
349
|
+
mi_result = subprocess.run(
|
|
350
|
+
["radon", "mi", str(path), "-s"],
|
|
351
|
+
capture_output=True, text=True, timeout=60
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
output += f"\n\n[Maintainability Index]\n{mi_result.stdout}"
|
|
355
|
+
|
|
356
|
+
return ToolResult(success=True, output=output[:5000])
|
|
357
|
+
else:
|
|
358
|
+
return ToolResult(success=False, output="", error=result.stderr)
|
|
359
|
+
|
|
360
|
+
except FileNotFoundError:
|
|
361
|
+
# Fallback: basic line count analysis
|
|
362
|
+
return self._basic_analysis(path)
|
|
363
|
+
except Exception as e:
|
|
364
|
+
return ToolResult(success=False, output="", error=str(e))
|
|
365
|
+
|
|
366
|
+
def _basic_analysis(self, path: Path) -> ToolResult:
|
|
367
|
+
"""Basic code analysis without external tools."""
|
|
368
|
+
stats = {"files": 0, "lines": 0, "code_lines": 0, "comment_lines": 0, "blank_lines": 0}
|
|
369
|
+
|
|
370
|
+
files = [path] if path.is_file() else list(path.rglob("*.py"))
|
|
371
|
+
|
|
372
|
+
for f in files:
|
|
373
|
+
try:
|
|
374
|
+
with open(f) as fp:
|
|
375
|
+
for line in fp:
|
|
376
|
+
stats["lines"] += 1
|
|
377
|
+
stripped = line.strip()
|
|
378
|
+
if not stripped:
|
|
379
|
+
stats["blank_lines"] += 1
|
|
380
|
+
elif stripped.startswith("#"):
|
|
381
|
+
stats["comment_lines"] += 1
|
|
382
|
+
else:
|
|
383
|
+
stats["code_lines"] += 1
|
|
384
|
+
stats["files"] += 1
|
|
385
|
+
except Exception:
|
|
386
|
+
continue
|
|
387
|
+
|
|
388
|
+
output = f"""Basic Code Analysis:
|
|
389
|
+
Files: {stats['files']}
|
|
390
|
+
Total Lines: {stats['lines']}
|
|
391
|
+
Code Lines: {stats['code_lines']}
|
|
392
|
+
Comment Lines: {stats['comment_lines']}
|
|
393
|
+
Blank Lines: {stats['blank_lines']}
|
|
394
|
+
Comment Ratio: {stats['comment_lines'] / max(stats['code_lines'], 1) * 100:.1f}%
|
|
395
|
+
"""
|
|
396
|
+
return ToolResult(success=True, output=output)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
class FindDeadCodeTool(Tool):
|
|
400
|
+
"""Find unused/dead code."""
|
|
401
|
+
|
|
402
|
+
name = "find_dead_code"
|
|
403
|
+
description = "Find unused imports, variables, and functions"
|
|
404
|
+
parameters = {
|
|
405
|
+
"type": "object",
|
|
406
|
+
"properties": {
|
|
407
|
+
"path": {"type": "string", "description": "File or directory to analyze"},
|
|
408
|
+
},
|
|
409
|
+
"required": ["path"],
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
def execute(self, path: str) -> ToolResult:
|
|
413
|
+
try:
|
|
414
|
+
# Try vulture for Python dead code detection
|
|
415
|
+
result = subprocess.run(
|
|
416
|
+
["vulture", path, "--min-confidence", "80"],
|
|
417
|
+
capture_output=True, text=True, timeout=120
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
output = result.stdout or "No dead code found"
|
|
421
|
+
lines = output.strip().split("\n") if output.strip() else []
|
|
422
|
+
count = len([l for l in lines if l.strip()])
|
|
423
|
+
|
|
424
|
+
return ToolResult(
|
|
425
|
+
success=True,
|
|
426
|
+
output=f"Found {count} potential dead code issues:\n\n{output[:5000]}"
|
|
427
|
+
)
|
|
428
|
+
except FileNotFoundError:
|
|
429
|
+
# Fallback: use pyflakes
|
|
430
|
+
try:
|
|
431
|
+
result = subprocess.run(
|
|
432
|
+
["pyflakes", path],
|
|
433
|
+
capture_output=True, text=True, timeout=60
|
|
434
|
+
)
|
|
435
|
+
output = result.stdout or "No issues found"
|
|
436
|
+
return ToolResult(success=True, output=f"[pyflakes]\n{output[:5000]}")
|
|
437
|
+
except FileNotFoundError:
|
|
438
|
+
return ToolResult(
|
|
439
|
+
success=False, output="",
|
|
440
|
+
error="No dead code detector found. Install: pip install vulture pyflakes"
|
|
441
|
+
)
|
|
442
|
+
except Exception as e:
|
|
443
|
+
return ToolResult(success=False, output="", error=str(e))
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
class GetDiagnosticsTool(Tool):
|
|
447
|
+
"""Get all diagnostics for a file/project."""
|
|
448
|
+
|
|
449
|
+
name = "get_diagnostics"
|
|
450
|
+
description = "Get all code diagnostics (errors, warnings) for a file or project"
|
|
451
|
+
parameters = {
|
|
452
|
+
"type": "object",
|
|
453
|
+
"properties": {
|
|
454
|
+
"path": {"type": "string", "description": "File or directory to check"},
|
|
455
|
+
},
|
|
456
|
+
"required": ["path"],
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
def execute(self, path: str) -> ToolResult:
|
|
460
|
+
path_obj = Path(path)
|
|
461
|
+
diagnostics = []
|
|
462
|
+
|
|
463
|
+
# Python syntax check
|
|
464
|
+
if path_obj.suffix == ".py" or path_obj.is_dir():
|
|
465
|
+
py_files = [path_obj] if path_obj.is_file() else list(path_obj.rglob("*.py"))
|
|
466
|
+
for f in py_files[:20]: # Limit to 20 files
|
|
467
|
+
try:
|
|
468
|
+
result = subprocess.run(
|
|
469
|
+
["python3", "-m", "py_compile", str(f)],
|
|
470
|
+
capture_output=True, text=True, timeout=10
|
|
471
|
+
)
|
|
472
|
+
if result.returncode != 0:
|
|
473
|
+
diagnostics.append(f"[ERROR] {f}: {result.stderr.strip()}")
|
|
474
|
+
except Exception:
|
|
475
|
+
pass
|
|
476
|
+
|
|
477
|
+
# Run quick lint
|
|
478
|
+
try:
|
|
479
|
+
result = subprocess.run(
|
|
480
|
+
["ruff", "check", path, "--output-format=text"],
|
|
481
|
+
capture_output=True, text=True, timeout=60
|
|
482
|
+
)
|
|
483
|
+
if result.stdout:
|
|
484
|
+
for line in result.stdout.strip().split("\n")[:50]:
|
|
485
|
+
if line.strip():
|
|
486
|
+
diagnostics.append(f"[WARN] {line}")
|
|
487
|
+
except FileNotFoundError:
|
|
488
|
+
# Try flake8
|
|
489
|
+
try:
|
|
490
|
+
result = subprocess.run(
|
|
491
|
+
["flake8", path, "--max-line-length=100"],
|
|
492
|
+
capture_output=True, text=True, timeout=60
|
|
493
|
+
)
|
|
494
|
+
if result.stdout:
|
|
495
|
+
for line in result.stdout.strip().split("\n")[:50]:
|
|
496
|
+
if line.strip():
|
|
497
|
+
diagnostics.append(f"[WARN] {line}")
|
|
498
|
+
except FileNotFoundError:
|
|
499
|
+
pass
|
|
500
|
+
except Exception:
|
|
501
|
+
pass
|
|
502
|
+
|
|
503
|
+
if not diagnostics:
|
|
504
|
+
return ToolResult(success=True, output="No diagnostics found - code looks clean!")
|
|
505
|
+
|
|
506
|
+
return ToolResult(
|
|
507
|
+
success=True,
|
|
508
|
+
output=f"Found {len(diagnostics)} diagnostics:\n\n" + "\n".join(diagnostics[:100])
|
|
509
|
+
)
|