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,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
+ )