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.
- superqode/__init__.py +33 -0
- superqode/acp/__init__.py +23 -0
- superqode/acp/client.py +913 -0
- superqode/acp/permission_screen.py +457 -0
- superqode/acp/types.py +480 -0
- superqode/acp_discovery.py +856 -0
- superqode/agent/__init__.py +22 -0
- superqode/agent/edit_strategies.py +334 -0
- superqode/agent/loop.py +892 -0
- superqode/agent/qe_report_templates.py +39 -0
- superqode/agent/system_prompts.py +353 -0
- superqode/agent_output.py +721 -0
- superqode/agent_stream.py +953 -0
- superqode/agents/__init__.py +59 -0
- superqode/agents/acp_registry.py +305 -0
- superqode/agents/client.py +249 -0
- superqode/agents/data/augmentcode.com.toml +51 -0
- superqode/agents/data/cagent.dev.toml +51 -0
- superqode/agents/data/claude.com.toml +60 -0
- superqode/agents/data/codeassistant.dev.toml +51 -0
- superqode/agents/data/codex.openai.com.toml +57 -0
- superqode/agents/data/fastagent.ai.toml +66 -0
- superqode/agents/data/geminicli.com.toml +77 -0
- superqode/agents/data/goose.block.xyz.toml +54 -0
- superqode/agents/data/junie.jetbrains.com.toml +56 -0
- superqode/agents/data/kimi.moonshot.cn.toml +57 -0
- superqode/agents/data/llmlingagent.dev.toml +51 -0
- superqode/agents/data/molt.bot.toml +49 -0
- superqode/agents/data/opencode.ai.toml +60 -0
- superqode/agents/data/stakpak.dev.toml +51 -0
- superqode/agents/data/vtcode.dev.toml +51 -0
- superqode/agents/discovery.py +266 -0
- superqode/agents/messaging.py +160 -0
- superqode/agents/persona.py +166 -0
- superqode/agents/registry.py +421 -0
- superqode/agents/schema.py +72 -0
- superqode/agents/unified.py +367 -0
- superqode/app/__init__.py +111 -0
- superqode/app/constants.py +314 -0
- superqode/app/css.py +366 -0
- superqode/app/models.py +118 -0
- superqode/app/suggester.py +125 -0
- superqode/app/widgets.py +1591 -0
- superqode/app_enhanced.py +399 -0
- superqode/app_main.py +17187 -0
- superqode/approval.py +312 -0
- superqode/atomic.py +296 -0
- superqode/commands/__init__.py +1 -0
- superqode/commands/acp.py +965 -0
- superqode/commands/agents.py +180 -0
- superqode/commands/auth.py +278 -0
- superqode/commands/config.py +374 -0
- superqode/commands/init.py +826 -0
- superqode/commands/providers.py +819 -0
- superqode/commands/qe.py +1145 -0
- superqode/commands/roles.py +380 -0
- superqode/commands/serve.py +172 -0
- superqode/commands/suggestions.py +127 -0
- superqode/commands/superqe.py +460 -0
- superqode/config/__init__.py +51 -0
- superqode/config/loader.py +812 -0
- superqode/config/schema.py +498 -0
- superqode/core/__init__.py +111 -0
- superqode/core/roles.py +281 -0
- superqode/danger.py +386 -0
- superqode/data/superqode-template.yaml +1522 -0
- superqode/design_system.py +1080 -0
- superqode/dialogs/__init__.py +6 -0
- superqode/dialogs/base.py +39 -0
- superqode/dialogs/model.py +130 -0
- superqode/dialogs/provider.py +870 -0
- superqode/diff_view.py +919 -0
- superqode/enterprise.py +21 -0
- superqode/evaluation/__init__.py +25 -0
- superqode/evaluation/adapters.py +93 -0
- superqode/evaluation/behaviors.py +89 -0
- superqode/evaluation/engine.py +209 -0
- superqode/evaluation/scenarios.py +96 -0
- superqode/execution/__init__.py +36 -0
- superqode/execution/linter.py +538 -0
- superqode/execution/modes.py +347 -0
- superqode/execution/resolver.py +283 -0
- superqode/execution/runner.py +642 -0
- superqode/file_explorer.py +811 -0
- superqode/file_viewer.py +471 -0
- superqode/flash.py +183 -0
- superqode/guidance/__init__.py +58 -0
- superqode/guidance/config.py +203 -0
- superqode/guidance/prompts.py +71 -0
- superqode/harness/__init__.py +54 -0
- superqode/harness/accelerator.py +291 -0
- superqode/harness/config.py +319 -0
- superqode/harness/validator.py +147 -0
- superqode/history.py +279 -0
- superqode/integrations/superopt_runner.py +124 -0
- superqode/logging/__init__.py +49 -0
- superqode/logging/adapters.py +219 -0
- superqode/logging/formatter.py +923 -0
- superqode/logging/integration.py +341 -0
- superqode/logging/sinks.py +170 -0
- superqode/logging/unified_log.py +417 -0
- superqode/lsp/__init__.py +26 -0
- superqode/lsp/client.py +544 -0
- superqode/main.py +1069 -0
- superqode/mcp/__init__.py +89 -0
- superqode/mcp/auth_storage.py +380 -0
- superqode/mcp/client.py +1236 -0
- superqode/mcp/config.py +319 -0
- superqode/mcp/integration.py +337 -0
- superqode/mcp/oauth.py +436 -0
- superqode/mcp/oauth_callback.py +385 -0
- superqode/mcp/types.py +290 -0
- superqode/memory/__init__.py +31 -0
- superqode/memory/feedback.py +342 -0
- superqode/memory/store.py +522 -0
- superqode/notifications.py +369 -0
- superqode/optimization/__init__.py +5 -0
- superqode/optimization/config.py +33 -0
- superqode/permissions/__init__.py +25 -0
- superqode/permissions/rules.py +488 -0
- superqode/plan.py +323 -0
- superqode/providers/__init__.py +33 -0
- superqode/providers/gateway/__init__.py +165 -0
- superqode/providers/gateway/base.py +228 -0
- superqode/providers/gateway/litellm_gateway.py +1170 -0
- superqode/providers/gateway/openresponses_gateway.py +436 -0
- superqode/providers/health.py +297 -0
- superqode/providers/huggingface/__init__.py +74 -0
- superqode/providers/huggingface/downloader.py +472 -0
- superqode/providers/huggingface/endpoints.py +442 -0
- superqode/providers/huggingface/hub.py +531 -0
- superqode/providers/huggingface/inference.py +394 -0
- superqode/providers/huggingface/transformers_runner.py +516 -0
- superqode/providers/local/__init__.py +100 -0
- superqode/providers/local/base.py +438 -0
- superqode/providers/local/discovery.py +418 -0
- superqode/providers/local/lmstudio.py +256 -0
- superqode/providers/local/mlx.py +457 -0
- superqode/providers/local/ollama.py +486 -0
- superqode/providers/local/sglang.py +268 -0
- superqode/providers/local/tgi.py +260 -0
- superqode/providers/local/tool_support.py +477 -0
- superqode/providers/local/vllm.py +258 -0
- superqode/providers/manager.py +1338 -0
- superqode/providers/models.py +1016 -0
- superqode/providers/models_dev.py +578 -0
- superqode/providers/openresponses/__init__.py +87 -0
- superqode/providers/openresponses/converters/__init__.py +17 -0
- superqode/providers/openresponses/converters/messages.py +343 -0
- superqode/providers/openresponses/converters/tools.py +268 -0
- superqode/providers/openresponses/schema/__init__.py +56 -0
- superqode/providers/openresponses/schema/models.py +585 -0
- superqode/providers/openresponses/streaming/__init__.py +5 -0
- superqode/providers/openresponses/streaming/parser.py +338 -0
- superqode/providers/openresponses/tools/__init__.py +21 -0
- superqode/providers/openresponses/tools/apply_patch.py +352 -0
- superqode/providers/openresponses/tools/code_interpreter.py +290 -0
- superqode/providers/openresponses/tools/file_search.py +333 -0
- superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
- superqode/providers/registry.py +716 -0
- superqode/providers/usage.py +332 -0
- superqode/pure_mode.py +384 -0
- superqode/qr/__init__.py +23 -0
- superqode/qr/dashboard.py +781 -0
- superqode/qr/generator.py +1018 -0
- superqode/qr/templates.py +135 -0
- superqode/safety/__init__.py +41 -0
- superqode/safety/sandbox.py +413 -0
- superqode/safety/warnings.py +256 -0
- superqode/server/__init__.py +33 -0
- superqode/server/lsp_server.py +775 -0
- superqode/server/web.py +250 -0
- superqode/session/__init__.py +25 -0
- superqode/session/persistence.py +580 -0
- superqode/session/sharing.py +477 -0
- superqode/session.py +475 -0
- superqode/sidebar.py +2991 -0
- superqode/stream_view.py +648 -0
- superqode/styles/__init__.py +3 -0
- superqode/superqe/__init__.py +184 -0
- superqode/superqe/acp_runner.py +1064 -0
- superqode/superqe/constitution/__init__.py +62 -0
- superqode/superqe/constitution/evaluator.py +308 -0
- superqode/superqe/constitution/loader.py +432 -0
- superqode/superqe/constitution/schema.py +250 -0
- superqode/superqe/events.py +591 -0
- superqode/superqe/frameworks/__init__.py +65 -0
- superqode/superqe/frameworks/base.py +234 -0
- superqode/superqe/frameworks/e2e.py +263 -0
- superqode/superqe/frameworks/executor.py +237 -0
- superqode/superqe/frameworks/javascript.py +409 -0
- superqode/superqe/frameworks/python.py +373 -0
- superqode/superqe/frameworks/registry.py +92 -0
- superqode/superqe/mcp_tools/__init__.py +47 -0
- superqode/superqe/mcp_tools/core_tools.py +418 -0
- superqode/superqe/mcp_tools/registry.py +230 -0
- superqode/superqe/mcp_tools/testing_tools.py +167 -0
- superqode/superqe/noise.py +89 -0
- superqode/superqe/orchestrator.py +778 -0
- superqode/superqe/roles.py +609 -0
- superqode/superqe/session.py +713 -0
- superqode/superqe/skills/__init__.py +57 -0
- superqode/superqe/skills/base.py +106 -0
- superqode/superqe/skills/core_skills.py +899 -0
- superqode/superqe/skills/registry.py +90 -0
- superqode/superqe/verifier.py +101 -0
- superqode/superqe_cli.py +76 -0
- superqode/tool_call.py +358 -0
- superqode/tools/__init__.py +93 -0
- superqode/tools/agent_tools.py +496 -0
- superqode/tools/base.py +324 -0
- superqode/tools/batch_tool.py +133 -0
- superqode/tools/diagnostics.py +311 -0
- superqode/tools/edit_tools.py +653 -0
- superqode/tools/enhanced_base.py +515 -0
- superqode/tools/file_tools.py +269 -0
- superqode/tools/file_tracking.py +45 -0
- superqode/tools/lsp_tools.py +610 -0
- superqode/tools/network_tools.py +350 -0
- superqode/tools/permissions.py +400 -0
- superqode/tools/question_tool.py +324 -0
- superqode/tools/search_tools.py +598 -0
- superqode/tools/shell_tools.py +259 -0
- superqode/tools/todo_tools.py +121 -0
- superqode/tools/validation.py +80 -0
- superqode/tools/web_tools.py +639 -0
- superqode/tui.py +1152 -0
- superqode/tui_integration.py +875 -0
- superqode/tui_widgets/__init__.py +27 -0
- superqode/tui_widgets/widgets/__init__.py +18 -0
- superqode/tui_widgets/widgets/progress.py +185 -0
- superqode/tui_widgets/widgets/tool_display.py +188 -0
- superqode/undo_manager.py +574 -0
- superqode/utils/__init__.py +5 -0
- superqode/utils/error_handling.py +323 -0
- superqode/utils/fuzzy.py +257 -0
- superqode/widgets/__init__.py +477 -0
- superqode/widgets/agent_collab.py +390 -0
- superqode/widgets/agent_store.py +936 -0
- superqode/widgets/agent_switcher.py +395 -0
- superqode/widgets/animation_manager.py +284 -0
- superqode/widgets/code_context.py +356 -0
- superqode/widgets/command_palette.py +412 -0
- superqode/widgets/connection_status.py +537 -0
- superqode/widgets/conversation_history.py +470 -0
- superqode/widgets/diff_indicator.py +155 -0
- superqode/widgets/enhanced_status_bar.py +385 -0
- superqode/widgets/enhanced_toast.py +476 -0
- superqode/widgets/file_browser.py +809 -0
- superqode/widgets/file_reference.py +585 -0
- superqode/widgets/issue_timeline.py +340 -0
- superqode/widgets/leader_key.py +264 -0
- superqode/widgets/mode_switcher.py +445 -0
- superqode/widgets/model_picker.py +234 -0
- superqode/widgets/permission_preview.py +1205 -0
- superqode/widgets/prompt.py +358 -0
- superqode/widgets/provider_connect.py +725 -0
- superqode/widgets/pty_shell.py +587 -0
- superqode/widgets/qe_dashboard.py +321 -0
- superqode/widgets/resizable_sidebar.py +377 -0
- superqode/widgets/response_changes.py +218 -0
- superqode/widgets/response_display.py +528 -0
- superqode/widgets/rich_tool_display.py +613 -0
- superqode/widgets/sidebar_panels.py +1180 -0
- superqode/widgets/slash_complete.py +356 -0
- superqode/widgets/split_view.py +612 -0
- superqode/widgets/status_bar.py +273 -0
- superqode/widgets/superqode_display.py +786 -0
- superqode/widgets/thinking_display.py +815 -0
- superqode/widgets/throbber.py +87 -0
- superqode/widgets/toast.py +206 -0
- superqode/widgets/unified_output.py +1073 -0
- superqode/workspace/__init__.py +75 -0
- superqode/workspace/artifacts.py +472 -0
- superqode/workspace/coordinator.py +353 -0
- superqode/workspace/diff_tracker.py +429 -0
- superqode/workspace/git_guard.py +373 -0
- superqode/workspace/git_snapshot.py +526 -0
- superqode/workspace/manager.py +750 -0
- superqode/workspace/snapshot.py +357 -0
- superqode/workspace/watcher.py +535 -0
- superqode/workspace/worktree.py +440 -0
- superqode-0.1.5.dist-info/METADATA +204 -0
- superqode-0.1.5.dist-info/RECORD +288 -0
- superqode-0.1.5.dist-info/WHEEL +5 -0
- superqode-0.1.5.dist-info/entry_points.txt +3 -0
- superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
- 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)})
|