superqode 0.1.5__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 (288) hide show
  1. superqode/__init__.py +33 -0
  2. superqode/acp/__init__.py +23 -0
  3. superqode/acp/client.py +913 -0
  4. superqode/acp/permission_screen.py +457 -0
  5. superqode/acp/types.py +480 -0
  6. superqode/acp_discovery.py +856 -0
  7. superqode/agent/__init__.py +22 -0
  8. superqode/agent/edit_strategies.py +334 -0
  9. superqode/agent/loop.py +892 -0
  10. superqode/agent/qe_report_templates.py +39 -0
  11. superqode/agent/system_prompts.py +353 -0
  12. superqode/agent_output.py +721 -0
  13. superqode/agent_stream.py +953 -0
  14. superqode/agents/__init__.py +59 -0
  15. superqode/agents/acp_registry.py +305 -0
  16. superqode/agents/client.py +249 -0
  17. superqode/agents/data/augmentcode.com.toml +51 -0
  18. superqode/agents/data/cagent.dev.toml +51 -0
  19. superqode/agents/data/claude.com.toml +60 -0
  20. superqode/agents/data/codeassistant.dev.toml +51 -0
  21. superqode/agents/data/codex.openai.com.toml +57 -0
  22. superqode/agents/data/fastagent.ai.toml +66 -0
  23. superqode/agents/data/geminicli.com.toml +77 -0
  24. superqode/agents/data/goose.block.xyz.toml +54 -0
  25. superqode/agents/data/junie.jetbrains.com.toml +56 -0
  26. superqode/agents/data/kimi.moonshot.cn.toml +57 -0
  27. superqode/agents/data/llmlingagent.dev.toml +51 -0
  28. superqode/agents/data/molt.bot.toml +49 -0
  29. superqode/agents/data/opencode.ai.toml +60 -0
  30. superqode/agents/data/stakpak.dev.toml +51 -0
  31. superqode/agents/data/vtcode.dev.toml +51 -0
  32. superqode/agents/discovery.py +266 -0
  33. superqode/agents/messaging.py +160 -0
  34. superqode/agents/persona.py +166 -0
  35. superqode/agents/registry.py +421 -0
  36. superqode/agents/schema.py +72 -0
  37. superqode/agents/unified.py +367 -0
  38. superqode/app/__init__.py +111 -0
  39. superqode/app/constants.py +314 -0
  40. superqode/app/css.py +366 -0
  41. superqode/app/models.py +118 -0
  42. superqode/app/suggester.py +125 -0
  43. superqode/app/widgets.py +1591 -0
  44. superqode/app_enhanced.py +399 -0
  45. superqode/app_main.py +17187 -0
  46. superqode/approval.py +312 -0
  47. superqode/atomic.py +296 -0
  48. superqode/commands/__init__.py +1 -0
  49. superqode/commands/acp.py +965 -0
  50. superqode/commands/agents.py +180 -0
  51. superqode/commands/auth.py +278 -0
  52. superqode/commands/config.py +374 -0
  53. superqode/commands/init.py +826 -0
  54. superqode/commands/providers.py +819 -0
  55. superqode/commands/qe.py +1145 -0
  56. superqode/commands/roles.py +380 -0
  57. superqode/commands/serve.py +172 -0
  58. superqode/commands/suggestions.py +127 -0
  59. superqode/commands/superqe.py +460 -0
  60. superqode/config/__init__.py +51 -0
  61. superqode/config/loader.py +812 -0
  62. superqode/config/schema.py +498 -0
  63. superqode/core/__init__.py +111 -0
  64. superqode/core/roles.py +281 -0
  65. superqode/danger.py +386 -0
  66. superqode/data/superqode-template.yaml +1522 -0
  67. superqode/design_system.py +1080 -0
  68. superqode/dialogs/__init__.py +6 -0
  69. superqode/dialogs/base.py +39 -0
  70. superqode/dialogs/model.py +130 -0
  71. superqode/dialogs/provider.py +870 -0
  72. superqode/diff_view.py +919 -0
  73. superqode/enterprise.py +21 -0
  74. superqode/evaluation/__init__.py +25 -0
  75. superqode/evaluation/adapters.py +93 -0
  76. superqode/evaluation/behaviors.py +89 -0
  77. superqode/evaluation/engine.py +209 -0
  78. superqode/evaluation/scenarios.py +96 -0
  79. superqode/execution/__init__.py +36 -0
  80. superqode/execution/linter.py +538 -0
  81. superqode/execution/modes.py +347 -0
  82. superqode/execution/resolver.py +283 -0
  83. superqode/execution/runner.py +642 -0
  84. superqode/file_explorer.py +811 -0
  85. superqode/file_viewer.py +471 -0
  86. superqode/flash.py +183 -0
  87. superqode/guidance/__init__.py +58 -0
  88. superqode/guidance/config.py +203 -0
  89. superqode/guidance/prompts.py +71 -0
  90. superqode/harness/__init__.py +54 -0
  91. superqode/harness/accelerator.py +291 -0
  92. superqode/harness/config.py +319 -0
  93. superqode/harness/validator.py +147 -0
  94. superqode/history.py +279 -0
  95. superqode/integrations/superopt_runner.py +124 -0
  96. superqode/logging/__init__.py +49 -0
  97. superqode/logging/adapters.py +219 -0
  98. superqode/logging/formatter.py +923 -0
  99. superqode/logging/integration.py +341 -0
  100. superqode/logging/sinks.py +170 -0
  101. superqode/logging/unified_log.py +417 -0
  102. superqode/lsp/__init__.py +26 -0
  103. superqode/lsp/client.py +544 -0
  104. superqode/main.py +1069 -0
  105. superqode/mcp/__init__.py +89 -0
  106. superqode/mcp/auth_storage.py +380 -0
  107. superqode/mcp/client.py +1236 -0
  108. superqode/mcp/config.py +319 -0
  109. superqode/mcp/integration.py +337 -0
  110. superqode/mcp/oauth.py +436 -0
  111. superqode/mcp/oauth_callback.py +385 -0
  112. superqode/mcp/types.py +290 -0
  113. superqode/memory/__init__.py +31 -0
  114. superqode/memory/feedback.py +342 -0
  115. superqode/memory/store.py +522 -0
  116. superqode/notifications.py +369 -0
  117. superqode/optimization/__init__.py +5 -0
  118. superqode/optimization/config.py +33 -0
  119. superqode/permissions/__init__.py +25 -0
  120. superqode/permissions/rules.py +488 -0
  121. superqode/plan.py +323 -0
  122. superqode/providers/__init__.py +33 -0
  123. superqode/providers/gateway/__init__.py +165 -0
  124. superqode/providers/gateway/base.py +228 -0
  125. superqode/providers/gateway/litellm_gateway.py +1170 -0
  126. superqode/providers/gateway/openresponses_gateway.py +436 -0
  127. superqode/providers/health.py +297 -0
  128. superqode/providers/huggingface/__init__.py +74 -0
  129. superqode/providers/huggingface/downloader.py +472 -0
  130. superqode/providers/huggingface/endpoints.py +442 -0
  131. superqode/providers/huggingface/hub.py +531 -0
  132. superqode/providers/huggingface/inference.py +394 -0
  133. superqode/providers/huggingface/transformers_runner.py +516 -0
  134. superqode/providers/local/__init__.py +100 -0
  135. superqode/providers/local/base.py +438 -0
  136. superqode/providers/local/discovery.py +418 -0
  137. superqode/providers/local/lmstudio.py +256 -0
  138. superqode/providers/local/mlx.py +457 -0
  139. superqode/providers/local/ollama.py +486 -0
  140. superqode/providers/local/sglang.py +268 -0
  141. superqode/providers/local/tgi.py +260 -0
  142. superqode/providers/local/tool_support.py +477 -0
  143. superqode/providers/local/vllm.py +258 -0
  144. superqode/providers/manager.py +1338 -0
  145. superqode/providers/models.py +1016 -0
  146. superqode/providers/models_dev.py +578 -0
  147. superqode/providers/openresponses/__init__.py +87 -0
  148. superqode/providers/openresponses/converters/__init__.py +17 -0
  149. superqode/providers/openresponses/converters/messages.py +343 -0
  150. superqode/providers/openresponses/converters/tools.py +268 -0
  151. superqode/providers/openresponses/schema/__init__.py +56 -0
  152. superqode/providers/openresponses/schema/models.py +585 -0
  153. superqode/providers/openresponses/streaming/__init__.py +5 -0
  154. superqode/providers/openresponses/streaming/parser.py +338 -0
  155. superqode/providers/openresponses/tools/__init__.py +21 -0
  156. superqode/providers/openresponses/tools/apply_patch.py +352 -0
  157. superqode/providers/openresponses/tools/code_interpreter.py +290 -0
  158. superqode/providers/openresponses/tools/file_search.py +333 -0
  159. superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
  160. superqode/providers/registry.py +716 -0
  161. superqode/providers/usage.py +332 -0
  162. superqode/pure_mode.py +384 -0
  163. superqode/qr/__init__.py +23 -0
  164. superqode/qr/dashboard.py +781 -0
  165. superqode/qr/generator.py +1018 -0
  166. superqode/qr/templates.py +135 -0
  167. superqode/safety/__init__.py +41 -0
  168. superqode/safety/sandbox.py +413 -0
  169. superqode/safety/warnings.py +256 -0
  170. superqode/server/__init__.py +33 -0
  171. superqode/server/lsp_server.py +775 -0
  172. superqode/server/web.py +250 -0
  173. superqode/session/__init__.py +25 -0
  174. superqode/session/persistence.py +580 -0
  175. superqode/session/sharing.py +477 -0
  176. superqode/session.py +475 -0
  177. superqode/sidebar.py +2991 -0
  178. superqode/stream_view.py +648 -0
  179. superqode/styles/__init__.py +3 -0
  180. superqode/superqe/__init__.py +184 -0
  181. superqode/superqe/acp_runner.py +1064 -0
  182. superqode/superqe/constitution/__init__.py +62 -0
  183. superqode/superqe/constitution/evaluator.py +308 -0
  184. superqode/superqe/constitution/loader.py +432 -0
  185. superqode/superqe/constitution/schema.py +250 -0
  186. superqode/superqe/events.py +591 -0
  187. superqode/superqe/frameworks/__init__.py +65 -0
  188. superqode/superqe/frameworks/base.py +234 -0
  189. superqode/superqe/frameworks/e2e.py +263 -0
  190. superqode/superqe/frameworks/executor.py +237 -0
  191. superqode/superqe/frameworks/javascript.py +409 -0
  192. superqode/superqe/frameworks/python.py +373 -0
  193. superqode/superqe/frameworks/registry.py +92 -0
  194. superqode/superqe/mcp_tools/__init__.py +47 -0
  195. superqode/superqe/mcp_tools/core_tools.py +418 -0
  196. superqode/superqe/mcp_tools/registry.py +230 -0
  197. superqode/superqe/mcp_tools/testing_tools.py +167 -0
  198. superqode/superqe/noise.py +89 -0
  199. superqode/superqe/orchestrator.py +778 -0
  200. superqode/superqe/roles.py +609 -0
  201. superqode/superqe/session.py +713 -0
  202. superqode/superqe/skills/__init__.py +57 -0
  203. superqode/superqe/skills/base.py +106 -0
  204. superqode/superqe/skills/core_skills.py +899 -0
  205. superqode/superqe/skills/registry.py +90 -0
  206. superqode/superqe/verifier.py +101 -0
  207. superqode/superqe_cli.py +76 -0
  208. superqode/tool_call.py +358 -0
  209. superqode/tools/__init__.py +93 -0
  210. superqode/tools/agent_tools.py +496 -0
  211. superqode/tools/base.py +324 -0
  212. superqode/tools/batch_tool.py +133 -0
  213. superqode/tools/diagnostics.py +311 -0
  214. superqode/tools/edit_tools.py +653 -0
  215. superqode/tools/enhanced_base.py +515 -0
  216. superqode/tools/file_tools.py +269 -0
  217. superqode/tools/file_tracking.py +45 -0
  218. superqode/tools/lsp_tools.py +610 -0
  219. superqode/tools/network_tools.py +350 -0
  220. superqode/tools/permissions.py +400 -0
  221. superqode/tools/question_tool.py +324 -0
  222. superqode/tools/search_tools.py +598 -0
  223. superqode/tools/shell_tools.py +259 -0
  224. superqode/tools/todo_tools.py +121 -0
  225. superqode/tools/validation.py +80 -0
  226. superqode/tools/web_tools.py +639 -0
  227. superqode/tui.py +1152 -0
  228. superqode/tui_integration.py +875 -0
  229. superqode/tui_widgets/__init__.py +27 -0
  230. superqode/tui_widgets/widgets/__init__.py +18 -0
  231. superqode/tui_widgets/widgets/progress.py +185 -0
  232. superqode/tui_widgets/widgets/tool_display.py +188 -0
  233. superqode/undo_manager.py +574 -0
  234. superqode/utils/__init__.py +5 -0
  235. superqode/utils/error_handling.py +323 -0
  236. superqode/utils/fuzzy.py +257 -0
  237. superqode/widgets/__init__.py +477 -0
  238. superqode/widgets/agent_collab.py +390 -0
  239. superqode/widgets/agent_store.py +936 -0
  240. superqode/widgets/agent_switcher.py +395 -0
  241. superqode/widgets/animation_manager.py +284 -0
  242. superqode/widgets/code_context.py +356 -0
  243. superqode/widgets/command_palette.py +412 -0
  244. superqode/widgets/connection_status.py +537 -0
  245. superqode/widgets/conversation_history.py +470 -0
  246. superqode/widgets/diff_indicator.py +155 -0
  247. superqode/widgets/enhanced_status_bar.py +385 -0
  248. superqode/widgets/enhanced_toast.py +476 -0
  249. superqode/widgets/file_browser.py +809 -0
  250. superqode/widgets/file_reference.py +585 -0
  251. superqode/widgets/issue_timeline.py +340 -0
  252. superqode/widgets/leader_key.py +264 -0
  253. superqode/widgets/mode_switcher.py +445 -0
  254. superqode/widgets/model_picker.py +234 -0
  255. superqode/widgets/permission_preview.py +1205 -0
  256. superqode/widgets/prompt.py +358 -0
  257. superqode/widgets/provider_connect.py +725 -0
  258. superqode/widgets/pty_shell.py +587 -0
  259. superqode/widgets/qe_dashboard.py +321 -0
  260. superqode/widgets/resizable_sidebar.py +377 -0
  261. superqode/widgets/response_changes.py +218 -0
  262. superqode/widgets/response_display.py +528 -0
  263. superqode/widgets/rich_tool_display.py +613 -0
  264. superqode/widgets/sidebar_panels.py +1180 -0
  265. superqode/widgets/slash_complete.py +356 -0
  266. superqode/widgets/split_view.py +612 -0
  267. superqode/widgets/status_bar.py +273 -0
  268. superqode/widgets/superqode_display.py +786 -0
  269. superqode/widgets/thinking_display.py +815 -0
  270. superqode/widgets/throbber.py +87 -0
  271. superqode/widgets/toast.py +206 -0
  272. superqode/widgets/unified_output.py +1073 -0
  273. superqode/workspace/__init__.py +75 -0
  274. superqode/workspace/artifacts.py +472 -0
  275. superqode/workspace/coordinator.py +353 -0
  276. superqode/workspace/diff_tracker.py +429 -0
  277. superqode/workspace/git_guard.py +373 -0
  278. superqode/workspace/git_snapshot.py +526 -0
  279. superqode/workspace/manager.py +750 -0
  280. superqode/workspace/snapshot.py +357 -0
  281. superqode/workspace/watcher.py +535 -0
  282. superqode/workspace/worktree.py +440 -0
  283. superqode-0.1.5.dist-info/METADATA +204 -0
  284. superqode-0.1.5.dist-info/RECORD +288 -0
  285. superqode-0.1.5.dist-info/WHEEL +5 -0
  286. superqode-0.1.5.dist-info/entry_points.txt +3 -0
  287. superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
  288. superqode-0.1.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,598 @@
1
+ """
2
+ Search Tools - Code Search with ripgrep/grep.
3
+
4
+ Provides multiple search strategies:
5
+ - GrepTool: Text pattern search (ripgrep/grep)
6
+ - GlobTool: File pattern matching
7
+ - CodeSearchTool: Semantic code search (symbols, definitions, references)
8
+ """
9
+
10
+ import asyncio
11
+ import re
12
+ import shutil
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+ from typing import Any, Dict, List, Optional, Tuple
16
+
17
+ from .base import Tool, ToolResult, ToolContext
18
+ from .validation import validate_path_in_working_directory
19
+
20
+
21
+ class GrepTool(Tool):
22
+ """Search for text patterns in files using ripgrep or grep."""
23
+
24
+ MAX_RESULTS = 100
25
+
26
+ @property
27
+ def name(self) -> str:
28
+ return "grep"
29
+
30
+ @property
31
+ def description(self) -> str:
32
+ return "Search for a pattern in files. Uses ripgrep if available, falls back to grep."
33
+
34
+ @property
35
+ def parameters(self) -> Dict[str, Any]:
36
+ return {
37
+ "type": "object",
38
+ "properties": {
39
+ "pattern": {"type": "string", "description": "Search pattern (regex supported)"},
40
+ "path": {
41
+ "type": "string",
42
+ "description": "Directory or file to search (default: current directory)",
43
+ },
44
+ "include": {
45
+ "type": "string",
46
+ "description": "File pattern to include (e.g., '*.py')",
47
+ },
48
+ "case_sensitive": {
49
+ "type": "boolean",
50
+ "description": "Case sensitive search (default: false)",
51
+ },
52
+ },
53
+ "required": ["pattern"],
54
+ }
55
+
56
+ async def execute(self, args: Dict[str, Any], ctx: ToolContext) -> ToolResult:
57
+ pattern = args.get("pattern", "")
58
+ path = args.get("path", ".")
59
+ include = args.get("include")
60
+ case_sensitive = args.get("case_sensitive", False)
61
+
62
+ if not pattern:
63
+ return ToolResult(success=False, output="", error="Pattern is required")
64
+
65
+ try:
66
+ # Validate and resolve path - ensures it stays within working directory
67
+ search_path = validate_path_in_working_directory(path, ctx.working_directory)
68
+ except ValueError as e:
69
+ return ToolResult(success=False, output="", error=str(e))
70
+
71
+ # Check for ripgrep first, fall back to grep
72
+ rg_path = shutil.which("rg")
73
+
74
+ if rg_path:
75
+ cmd = self._build_rg_command(pattern, search_path, include, case_sensitive)
76
+ else:
77
+ cmd = self._build_grep_command(pattern, search_path, include, case_sensitive)
78
+
79
+ try:
80
+ process = await asyncio.create_subprocess_shell(
81
+ cmd,
82
+ stdout=asyncio.subprocess.PIPE,
83
+ stderr=asyncio.subprocess.PIPE,
84
+ cwd=str(ctx.working_directory),
85
+ )
86
+
87
+ stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=30)
88
+
89
+ output = stdout.decode("utf-8", errors="replace")
90
+
91
+ # Limit results
92
+ lines = output.strip().split("\n")
93
+ if len(lines) > self.MAX_RESULTS:
94
+ output = "\n".join(lines[: self.MAX_RESULTS])
95
+ output += f"\n\n[Showing first {self.MAX_RESULTS} of {len(lines)} results]"
96
+
97
+ if not output.strip():
98
+ return ToolResult(success=True, output="No matches found", metadata={"matches": 0})
99
+
100
+ return ToolResult(success=True, output=output, metadata={"matches": len(lines)})
101
+
102
+ except asyncio.TimeoutError:
103
+ return ToolResult(success=False, output="", error="Search timed out")
104
+ except Exception as e:
105
+ return ToolResult(success=False, output="", error=str(e))
106
+
107
+ def _build_rg_command(
108
+ self, pattern: str, path: Path, include: str, case_sensitive: bool
109
+ ) -> str:
110
+ """Build ripgrep command."""
111
+ cmd_parts = ["rg", "--line-number", "--no-heading"]
112
+
113
+ if not case_sensitive:
114
+ cmd_parts.append("-i")
115
+
116
+ if include:
117
+ cmd_parts.extend(["-g", f"'{include}'"])
118
+
119
+ # Escape pattern for shell
120
+ escaped_pattern = pattern.replace("'", "'\\''")
121
+ cmd_parts.append(f"'{escaped_pattern}'")
122
+ cmd_parts.append(f"'{path}'")
123
+
124
+ return " ".join(cmd_parts)
125
+
126
+ def _build_grep_command(
127
+ self, pattern: str, path: Path, include: str, case_sensitive: bool
128
+ ) -> str:
129
+ """Build grep command."""
130
+ cmd_parts = ["grep", "-rn"]
131
+
132
+ if not case_sensitive:
133
+ cmd_parts.append("-i")
134
+
135
+ if include:
136
+ cmd_parts.extend(["--include", f"'{include}'"])
137
+
138
+ escaped_pattern = pattern.replace("'", "'\\''")
139
+ cmd_parts.append(f"'{escaped_pattern}'")
140
+ cmd_parts.append(f"'{path}'")
141
+
142
+ return " ".join(cmd_parts)
143
+
144
+
145
+ class GlobTool(Tool):
146
+ """Find files matching a pattern."""
147
+
148
+ MAX_RESULTS = 200
149
+
150
+ @property
151
+ def name(self) -> str:
152
+ return "glob"
153
+
154
+ @property
155
+ def description(self) -> str:
156
+ return "Find files matching a glob pattern (e.g., '**/*.py')."
157
+
158
+ @property
159
+ def parameters(self) -> Dict[str, Any]:
160
+ return {
161
+ "type": "object",
162
+ "properties": {
163
+ "pattern": {
164
+ "type": "string",
165
+ "description": "Glob pattern (e.g., '**/*.py', 'src/**/*.ts')",
166
+ },
167
+ "path": {
168
+ "type": "string",
169
+ "description": "Base directory to search from (default: current directory)",
170
+ },
171
+ },
172
+ "required": ["pattern"],
173
+ }
174
+
175
+ async def execute(self, args: Dict[str, Any], ctx: ToolContext) -> ToolResult:
176
+ pattern = args.get("pattern", "")
177
+ path = args.get("path", ".")
178
+
179
+ if not pattern:
180
+ return ToolResult(success=False, output="", error="Pattern is required")
181
+
182
+ try:
183
+ # Validate and resolve path - ensures it stays within working directory
184
+ base_path = validate_path_in_working_directory(path, ctx.working_directory)
185
+ except ValueError as e:
186
+ return ToolResult(success=False, output="", error=str(e))
187
+
188
+ try:
189
+ # Use pathlib glob
190
+ matches = list(base_path.glob(pattern))
191
+
192
+ # Filter out hidden files and common ignore patterns
193
+ filtered = []
194
+ for m in matches:
195
+ parts = m.relative_to(base_path).parts
196
+ if any(
197
+ p.startswith(".") or p in ("node_modules", "__pycache__", "venv") for p in parts
198
+ ):
199
+ continue
200
+ filtered.append(m)
201
+
202
+ # Limit results
203
+ if len(filtered) > self.MAX_RESULTS:
204
+ filtered = filtered[: self.MAX_RESULTS]
205
+ truncated = True
206
+ else:
207
+ truncated = False
208
+
209
+ # Format output
210
+ output_lines = [str(m.relative_to(ctx.working_directory)) for m in filtered]
211
+ output = "\n".join(output_lines)
212
+
213
+ if truncated:
214
+ output += f"\n\n[Showing first {self.MAX_RESULTS} results]"
215
+
216
+ if not output:
217
+ return ToolResult(
218
+ success=True, output="No files found matching pattern", metadata={"matches": 0}
219
+ )
220
+
221
+ return ToolResult(success=True, output=output, metadata={"matches": len(filtered)})
222
+
223
+ except Exception as e:
224
+ return ToolResult(success=False, output="", error=str(e))
225
+
226
+
227
+ @dataclass
228
+ class Symbol:
229
+ """A code symbol (function, class, variable, etc.)."""
230
+
231
+ name: str
232
+ kind: str # function, class, method, variable, etc.
233
+ file: str
234
+ line: int
235
+ signature: str = ""
236
+
237
+
238
+ class CodeSearchTool(Tool):
239
+ """
240
+ Semantic code search - find symbols, definitions, and references.
241
+
242
+ Supports:
243
+ - Symbol search (find functions, classes, methods by name)
244
+ - Definition search (where is X defined?)
245
+ - Reference search (where is X used?)
246
+ - Import search (what imports X?)
247
+
248
+ Uses regex-based heuristics for broad language support.
249
+ Can integrate with LSP for more accurate results when available.
250
+ """
251
+
252
+ MAX_RESULTS = 50
253
+
254
+ # Language-specific patterns for symbol extraction
255
+ PATTERNS = {
256
+ "python": {
257
+ "function": r"^(\s*)def\s+(\w+)\s*\([^)]*\)",
258
+ "class": r"^(\s*)class\s+(\w+)\s*[:\(]",
259
+ "method": r"^(\s+)def\s+(\w+)\s*\(self[^)]*\)",
260
+ "variable": r"^(\w+)\s*=\s*",
261
+ "import": r"^(?:from\s+[\w.]+\s+)?import\s+(.+)",
262
+ },
263
+ "javascript": {
264
+ "function": r"^(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(",
265
+ "class": r"^(?:export\s+)?class\s+(\w+)",
266
+ "method": r"^\s+(?:async\s+)?(\w+)\s*\([^)]*\)\s*{",
267
+ "const": r"^(?:export\s+)?const\s+(\w+)\s*=",
268
+ "let": r"^(?:export\s+)?let\s+(\w+)\s*=",
269
+ "import": r"^import\s+(?:{[^}]+}|\*\s+as\s+\w+|\w+)\s+from",
270
+ },
271
+ "typescript": {
272
+ "function": r"^(?:export\s+)?(?:async\s+)?function\s+(\w+)",
273
+ "class": r"^(?:export\s+)?(?:abstract\s+)?class\s+(\w+)",
274
+ "interface": r"^(?:export\s+)?interface\s+(\w+)",
275
+ "type": r"^(?:export\s+)?type\s+(\w+)\s*=",
276
+ "method": r"^\s+(?:public|private|protected)?\s*(?:async\s+)?(\w+)\s*\(",
277
+ "const": r"^(?:export\s+)?const\s+(\w+)\s*[=:]",
278
+ },
279
+ "go": {
280
+ "function": r"^func\s+(\w+)\s*\(",
281
+ "method": r"^func\s+\([^)]+\)\s+(\w+)\s*\(",
282
+ "type": r"^type\s+(\w+)\s+",
283
+ "const": r"^const\s+(\w+)\s*=",
284
+ "var": r"^var\s+(\w+)\s+",
285
+ },
286
+ "rust": {
287
+ "function": r"^(?:pub\s+)?(?:async\s+)?fn\s+(\w+)",
288
+ "struct": r"^(?:pub\s+)?struct\s+(\w+)",
289
+ "enum": r"^(?:pub\s+)?enum\s+(\w+)",
290
+ "trait": r"^(?:pub\s+)?trait\s+(\w+)",
291
+ "impl": r"^impl(?:<[^>]+>)?\s+(\w+)",
292
+ },
293
+ }
294
+
295
+ # File extensions to language mapping
296
+ EXTENSIONS = {
297
+ ".py": "python",
298
+ ".pyi": "python",
299
+ ".js": "javascript",
300
+ ".jsx": "javascript",
301
+ ".mjs": "javascript",
302
+ ".ts": "typescript",
303
+ ".tsx": "typescript",
304
+ ".go": "go",
305
+ ".rs": "rust",
306
+ }
307
+
308
+ @property
309
+ def name(self) -> str:
310
+ return "code_search"
311
+
312
+ @property
313
+ def description(self) -> str:
314
+ return "Search for code symbols (functions, classes, methods). Find definitions and references."
315
+
316
+ @property
317
+ def parameters(self) -> Dict[str, Any]:
318
+ return {
319
+ "type": "object",
320
+ "properties": {
321
+ "query": {"type": "string", "description": "Symbol name or pattern to search for"},
322
+ "kind": {
323
+ "type": "string",
324
+ "enum": ["symbol", "definition", "reference", "import"],
325
+ "description": "Search type: symbol (find symbol defs), definition (where defined), reference (where used), import (import statements)",
326
+ },
327
+ "path": {
328
+ "type": "string",
329
+ "description": "Directory to search in (default: current directory)",
330
+ },
331
+ "language": {
332
+ "type": "string",
333
+ "description": "Filter by language (python, javascript, typescript, go, rust)",
334
+ },
335
+ "symbol_type": {
336
+ "type": "string",
337
+ "description": "Filter by symbol type (function, class, method, variable, etc.)",
338
+ },
339
+ },
340
+ "required": ["query"],
341
+ }
342
+
343
+ async def execute(self, args: Dict[str, Any], ctx: ToolContext) -> ToolResult:
344
+ query = args.get("query", "")
345
+ kind = args.get("kind", "symbol")
346
+ path = args.get("path", ".")
347
+ language = args.get("language")
348
+ symbol_type = args.get("symbol_type")
349
+
350
+ if not query:
351
+ return ToolResult(success=False, output="", error="Query is required")
352
+
353
+ try:
354
+ # Validate and resolve path - ensures it stays within working directory
355
+ search_path = validate_path_in_working_directory(path, ctx.working_directory)
356
+ except ValueError as e:
357
+ return ToolResult(success=False, output="", error=str(e))
358
+
359
+ if not search_path.exists():
360
+ return ToolResult(success=False, output="", error=f"Path not found: {path}")
361
+
362
+ try:
363
+ # Try LSP first for more accurate results
364
+ lsp_results = await self._try_lsp_search(query, kind, search_path, ctx)
365
+ if lsp_results:
366
+ return self._format_results(lsp_results, query, kind)
367
+
368
+ # Fall back to regex-based search
369
+ if kind == "symbol" or kind == "definition":
370
+ results = await self._search_definitions(
371
+ query, search_path, ctx, language, symbol_type
372
+ )
373
+ elif kind == "reference":
374
+ results = await self._search_references(query, search_path, ctx, language)
375
+ elif kind == "import":
376
+ results = await self._search_imports(query, search_path, ctx, language)
377
+ else:
378
+ results = await self._search_definitions(
379
+ query, search_path, ctx, language, symbol_type
380
+ )
381
+
382
+ return self._format_results(results, query, kind)
383
+
384
+ except Exception as e:
385
+ return ToolResult(success=False, output="", error=f"Search error: {str(e)}")
386
+
387
+ async def _try_lsp_search(
388
+ self, query: str, kind: str, path: Path, ctx: ToolContext
389
+ ) -> Optional[List[Symbol]]:
390
+ """Try to use LSP for more accurate search."""
391
+ # TODO: Integrate with LSP workspace/symbol request
392
+ return None
393
+
394
+ async def _search_definitions(
395
+ self,
396
+ query: str,
397
+ path: Path,
398
+ ctx: ToolContext,
399
+ language: Optional[str],
400
+ symbol_type: Optional[str],
401
+ ) -> List[Symbol]:
402
+ """Search for symbol definitions using regex patterns."""
403
+ results = []
404
+ query_pattern = re.compile(re.escape(query), re.IGNORECASE)
405
+
406
+ # Find files
407
+ for file_path in self._find_code_files(path, language):
408
+ lang = self._get_language(file_path)
409
+ if not lang:
410
+ continue
411
+
412
+ patterns = self.PATTERNS.get(lang, {})
413
+ if symbol_type:
414
+ patterns = {k: v for k, v in patterns.items() if k == symbol_type}
415
+
416
+ try:
417
+ content = file_path.read_text(errors="replace")
418
+ lines = content.split("\n")
419
+
420
+ for line_num, line in enumerate(lines, 1):
421
+ for kind_name, pattern in patterns.items():
422
+ match = re.match(pattern, line)
423
+ if match:
424
+ # Extract symbol name (last group usually)
425
+ groups = match.groups()
426
+ name = groups[-1] if groups else ""
427
+
428
+ # Handle comma-separated names (imports)
429
+ if "," in name:
430
+ names = [n.strip() for n in name.split(",")]
431
+ else:
432
+ names = [name]
433
+
434
+ for n in names:
435
+ if query_pattern.search(n):
436
+ rel_path = file_path.relative_to(ctx.working_directory)
437
+ results.append(
438
+ Symbol(
439
+ name=n,
440
+ kind=kind_name,
441
+ file=str(rel_path),
442
+ line=line_num,
443
+ signature=line.strip()[:100],
444
+ )
445
+ )
446
+
447
+ except Exception:
448
+ continue
449
+
450
+ return results[: self.MAX_RESULTS]
451
+
452
+ async def _search_references(
453
+ self, query: str, path: Path, ctx: ToolContext, language: Optional[str]
454
+ ) -> List[Symbol]:
455
+ """Search for references to a symbol."""
456
+ results = []
457
+
458
+ # Use ripgrep for fast search
459
+ rg_path = shutil.which("rg")
460
+ if rg_path:
461
+ cmd = f"rg -n --no-heading '\\b{query}\\b'"
462
+ if language:
463
+ ext_map = {
464
+ "python": "py",
465
+ "javascript": "js",
466
+ "typescript": "ts",
467
+ "go": "go",
468
+ "rust": "rs",
469
+ }
470
+ if language in ext_map:
471
+ cmd += f" -t {ext_map[language]}"
472
+ cmd += f" '{path}'"
473
+
474
+ try:
475
+ process = await asyncio.create_subprocess_shell(
476
+ cmd,
477
+ stdout=asyncio.subprocess.PIPE,
478
+ stderr=asyncio.subprocess.PIPE,
479
+ cwd=str(ctx.working_directory),
480
+ )
481
+
482
+ stdout, _ = await asyncio.wait_for(process.communicate(), timeout=30)
483
+
484
+ for line in stdout.decode("utf-8", errors="replace").split("\n"):
485
+ if ":" in line:
486
+ parts = line.split(":", 2)
487
+ if len(parts) >= 2:
488
+ file_path = parts[0]
489
+ try:
490
+ line_num = int(parts[1])
491
+ content = parts[2] if len(parts) > 2 else ""
492
+ results.append(
493
+ Symbol(
494
+ name=query,
495
+ kind="reference",
496
+ file=file_path,
497
+ line=line_num,
498
+ signature=content.strip()[:100],
499
+ )
500
+ )
501
+ except ValueError:
502
+ continue
503
+
504
+ except Exception:
505
+ pass
506
+
507
+ return results[: self.MAX_RESULTS]
508
+
509
+ async def _search_imports(
510
+ self, query: str, path: Path, ctx: ToolContext, language: Optional[str]
511
+ ) -> List[Symbol]:
512
+ """Search for import statements mentioning a symbol."""
513
+ results = []
514
+ query_lower = query.lower()
515
+
516
+ for file_path in self._find_code_files(path, language):
517
+ lang = self._get_language(file_path)
518
+ if not lang:
519
+ continue
520
+
521
+ import_pattern = self.PATTERNS.get(lang, {}).get("import")
522
+ if not import_pattern:
523
+ continue
524
+
525
+ try:
526
+ content = file_path.read_text(errors="replace")
527
+ lines = content.split("\n")
528
+
529
+ for line_num, line in enumerate(lines, 1):
530
+ if query_lower in line.lower():
531
+ if re.match(import_pattern, line.strip()):
532
+ rel_path = file_path.relative_to(ctx.working_directory)
533
+ results.append(
534
+ Symbol(
535
+ name=query,
536
+ kind="import",
537
+ file=str(rel_path),
538
+ line=line_num,
539
+ signature=line.strip()[:100],
540
+ )
541
+ )
542
+
543
+ except Exception:
544
+ continue
545
+
546
+ return results[: self.MAX_RESULTS]
547
+
548
+ def _find_code_files(self, path: Path, language: Optional[str]) -> List[Path]:
549
+ """Find code files in a directory."""
550
+ files = []
551
+
552
+ if language:
553
+ # Filter by language
554
+ exts = [ext for ext, lang in self.EXTENSIONS.items() if lang == language]
555
+ else:
556
+ exts = list(self.EXTENSIONS.keys())
557
+
558
+ if path.is_file():
559
+ if path.suffix in exts:
560
+ return [path]
561
+ return []
562
+
563
+ for ext in exts:
564
+ for file_path in path.rglob(f"*{ext}"):
565
+ # Skip common ignore patterns
566
+ parts = file_path.parts
567
+ if any(
568
+ p in ["node_modules", "__pycache__", ".git", "venv", ".venv", "dist", "build"]
569
+ for p in parts
570
+ ):
571
+ continue
572
+ files.append(file_path)
573
+
574
+ return files
575
+
576
+ def _get_language(self, path: Path) -> Optional[str]:
577
+ """Get language from file extension."""
578
+ return self.EXTENSIONS.get(path.suffix.lower())
579
+
580
+ def _format_results(self, results: List[Symbol], query: str, kind: str) -> ToolResult:
581
+ """Format search results."""
582
+ if not results:
583
+ return ToolResult(
584
+ success=True, output=f"No {kind}s found for '{query}'", metadata={"count": 0}
585
+ )
586
+
587
+ output_lines = []
588
+ for sym in results:
589
+ output_lines.append(f"{sym.file}:{sym.line} [{sym.kind}] {sym.name}")
590
+ if sym.signature:
591
+ output_lines.append(f" {sym.signature}")
592
+
593
+ output = "\n".join(output_lines)
594
+
595
+ if len(results) >= self.MAX_RESULTS:
596
+ output += f"\n\n[Showing first {self.MAX_RESULTS} results]"
597
+
598
+ return ToolResult(success=True, output=output, metadata={"count": len(results)})