hanzo-mcp 0.7.6__py3-none-any.whl → 0.8.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.
Potentially problematic release.
This version of hanzo-mcp might be problematic. Click here for more details.
- hanzo_mcp/__init__.py +7 -1
- hanzo_mcp/__main__.py +1 -1
- hanzo_mcp/analytics/__init__.py +2 -2
- hanzo_mcp/analytics/posthog_analytics.py +76 -82
- hanzo_mcp/cli.py +31 -36
- hanzo_mcp/cli_enhanced.py +94 -72
- hanzo_mcp/cli_plugin.py +27 -17
- hanzo_mcp/config/__init__.py +2 -2
- hanzo_mcp/config/settings.py +112 -88
- hanzo_mcp/config/tool_config.py +32 -34
- hanzo_mcp/dev_server.py +66 -67
- hanzo_mcp/prompts/__init__.py +94 -12
- hanzo_mcp/prompts/enhanced_prompts.py +809 -0
- hanzo_mcp/prompts/example_custom_prompt.py +6 -5
- hanzo_mcp/prompts/project_todo_reminder.py +0 -1
- hanzo_mcp/prompts/tool_explorer.py +10 -7
- hanzo_mcp/server.py +17 -21
- hanzo_mcp/server_enhanced.py +15 -22
- hanzo_mcp/tools/__init__.py +56 -28
- hanzo_mcp/tools/agent/__init__.py +16 -19
- hanzo_mcp/tools/agent/agent.py +82 -65
- hanzo_mcp/tools/agent/agent_tool.py +152 -122
- hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +66 -62
- hanzo_mcp/tools/agent/clarification_protocol.py +55 -50
- hanzo_mcp/tools/agent/clarification_tool.py +11 -10
- hanzo_mcp/tools/agent/claude_cli_tool.py +21 -20
- hanzo_mcp/tools/agent/claude_desktop_auth.py +130 -144
- hanzo_mcp/tools/agent/cli_agent_base.py +59 -53
- hanzo_mcp/tools/agent/code_auth.py +102 -107
- hanzo_mcp/tools/agent/code_auth_tool.py +28 -27
- hanzo_mcp/tools/agent/codex_cli_tool.py +20 -19
- hanzo_mcp/tools/agent/critic_tool.py +86 -73
- hanzo_mcp/tools/agent/gemini_cli_tool.py +21 -20
- hanzo_mcp/tools/agent/grok_cli_tool.py +21 -20
- hanzo_mcp/tools/agent/iching_tool.py +404 -139
- hanzo_mcp/tools/agent/network_tool.py +89 -73
- hanzo_mcp/tools/agent/prompt.py +2 -1
- hanzo_mcp/tools/agent/review_tool.py +101 -98
- hanzo_mcp/tools/agent/swarm_alias.py +87 -0
- hanzo_mcp/tools/agent/swarm_tool.py +246 -161
- hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +134 -92
- hanzo_mcp/tools/agent/tool_adapter.py +21 -11
- hanzo_mcp/tools/common/__init__.py +1 -1
- hanzo_mcp/tools/common/base.py +3 -5
- hanzo_mcp/tools/common/batch_tool.py +46 -39
- hanzo_mcp/tools/common/config_tool.py +120 -84
- hanzo_mcp/tools/common/context.py +1 -5
- hanzo_mcp/tools/common/context_fix.py +5 -3
- hanzo_mcp/tools/common/critic_tool.py +4 -8
- hanzo_mcp/tools/common/decorators.py +58 -56
- hanzo_mcp/tools/common/enhanced_base.py +29 -32
- hanzo_mcp/tools/common/fastmcp_pagination.py +91 -94
- hanzo_mcp/tools/common/forgiving_edit.py +91 -87
- hanzo_mcp/tools/common/mode.py +15 -17
- hanzo_mcp/tools/common/mode_loader.py +27 -24
- hanzo_mcp/tools/common/paginated_base.py +61 -53
- hanzo_mcp/tools/common/paginated_response.py +72 -79
- hanzo_mcp/tools/common/pagination.py +50 -53
- hanzo_mcp/tools/common/permissions.py +4 -4
- hanzo_mcp/tools/common/personality.py +186 -138
- hanzo_mcp/tools/common/plugin_loader.py +54 -54
- hanzo_mcp/tools/common/stats.py +65 -47
- hanzo_mcp/tools/common/test_helpers.py +31 -0
- hanzo_mcp/tools/common/thinking_tool.py +4 -8
- hanzo_mcp/tools/common/tool_disable.py +17 -12
- hanzo_mcp/tools/common/tool_enable.py +13 -14
- hanzo_mcp/tools/common/tool_list.py +36 -28
- hanzo_mcp/tools/common/truncate.py +23 -23
- hanzo_mcp/tools/config/__init__.py +4 -4
- hanzo_mcp/tools/config/config_tool.py +42 -29
- hanzo_mcp/tools/config/index_config.py +37 -34
- hanzo_mcp/tools/config/mode_tool.py +175 -55
- hanzo_mcp/tools/database/__init__.py +15 -12
- hanzo_mcp/tools/database/database_manager.py +77 -75
- hanzo_mcp/tools/database/graph.py +137 -91
- hanzo_mcp/tools/database/graph_add.py +30 -18
- hanzo_mcp/tools/database/graph_query.py +178 -102
- hanzo_mcp/tools/database/graph_remove.py +33 -28
- hanzo_mcp/tools/database/graph_search.py +97 -75
- hanzo_mcp/tools/database/graph_stats.py +91 -59
- hanzo_mcp/tools/database/sql.py +107 -79
- hanzo_mcp/tools/database/sql_query.py +30 -24
- hanzo_mcp/tools/database/sql_search.py +29 -25
- hanzo_mcp/tools/database/sql_stats.py +47 -35
- hanzo_mcp/tools/editor/neovim_command.py +25 -28
- hanzo_mcp/tools/editor/neovim_edit.py +21 -23
- hanzo_mcp/tools/editor/neovim_session.py +60 -54
- hanzo_mcp/tools/filesystem/__init__.py +31 -30
- hanzo_mcp/tools/filesystem/ast_multi_edit.py +329 -249
- hanzo_mcp/tools/filesystem/ast_tool.py +4 -4
- hanzo_mcp/tools/filesystem/base.py +1 -1
- hanzo_mcp/tools/filesystem/batch_search.py +316 -224
- hanzo_mcp/tools/filesystem/content_replace.py +4 -4
- hanzo_mcp/tools/filesystem/diff.py +71 -59
- hanzo_mcp/tools/filesystem/directory_tree.py +7 -7
- hanzo_mcp/tools/filesystem/directory_tree_paginated.py +49 -37
- hanzo_mcp/tools/filesystem/edit.py +4 -4
- hanzo_mcp/tools/filesystem/find.py +173 -80
- hanzo_mcp/tools/filesystem/find_files.py +73 -52
- hanzo_mcp/tools/filesystem/git_search.py +157 -104
- hanzo_mcp/tools/filesystem/grep.py +8 -8
- hanzo_mcp/tools/filesystem/multi_edit.py +4 -8
- hanzo_mcp/tools/filesystem/read.py +12 -10
- hanzo_mcp/tools/filesystem/rules_tool.py +59 -43
- hanzo_mcp/tools/filesystem/search_tool.py +263 -207
- hanzo_mcp/tools/filesystem/symbols_tool.py +94 -54
- hanzo_mcp/tools/filesystem/tree.py +35 -33
- hanzo_mcp/tools/filesystem/unix_aliases.py +13 -18
- hanzo_mcp/tools/filesystem/watch.py +37 -36
- hanzo_mcp/tools/filesystem/write.py +4 -8
- hanzo_mcp/tools/jupyter/__init__.py +4 -4
- hanzo_mcp/tools/jupyter/base.py +4 -5
- hanzo_mcp/tools/jupyter/jupyter.py +67 -47
- hanzo_mcp/tools/jupyter/notebook_edit.py +4 -4
- hanzo_mcp/tools/jupyter/notebook_read.py +4 -7
- hanzo_mcp/tools/llm/__init__.py +5 -7
- hanzo_mcp/tools/llm/consensus_tool.py +72 -52
- hanzo_mcp/tools/llm/llm_manage.py +101 -60
- hanzo_mcp/tools/llm/llm_tool.py +226 -166
- hanzo_mcp/tools/llm/provider_tools.py +25 -26
- hanzo_mcp/tools/lsp/__init__.py +1 -1
- hanzo_mcp/tools/lsp/lsp_tool.py +228 -143
- hanzo_mcp/tools/mcp/__init__.py +2 -3
- hanzo_mcp/tools/mcp/mcp_add.py +27 -25
- hanzo_mcp/tools/mcp/mcp_remove.py +7 -8
- hanzo_mcp/tools/mcp/mcp_stats.py +23 -22
- hanzo_mcp/tools/mcp/mcp_tool.py +129 -98
- hanzo_mcp/tools/memory/__init__.py +39 -21
- hanzo_mcp/tools/memory/knowledge_tools.py +124 -99
- hanzo_mcp/tools/memory/memory_tools.py +90 -108
- hanzo_mcp/tools/search/__init__.py +7 -2
- hanzo_mcp/tools/search/find_tool.py +297 -212
- hanzo_mcp/tools/search/unified_search.py +366 -314
- hanzo_mcp/tools/shell/__init__.py +8 -7
- hanzo_mcp/tools/shell/auto_background.py +56 -49
- hanzo_mcp/tools/shell/base.py +1 -1
- hanzo_mcp/tools/shell/base_process.py +75 -75
- hanzo_mcp/tools/shell/bash_session.py +2 -2
- hanzo_mcp/tools/shell/bash_session_executor.py +4 -4
- hanzo_mcp/tools/shell/bash_tool.py +24 -31
- hanzo_mcp/tools/shell/command_executor.py +12 -12
- hanzo_mcp/tools/shell/logs.py +43 -33
- hanzo_mcp/tools/shell/npx.py +13 -13
- hanzo_mcp/tools/shell/npx_background.py +24 -21
- hanzo_mcp/tools/shell/npx_tool.py +18 -22
- hanzo_mcp/tools/shell/open.py +19 -21
- hanzo_mcp/tools/shell/pkill.py +31 -26
- hanzo_mcp/tools/shell/process_tool.py +32 -32
- hanzo_mcp/tools/shell/processes.py +57 -58
- hanzo_mcp/tools/shell/run_background.py +24 -25
- hanzo_mcp/tools/shell/run_command.py +5 -5
- hanzo_mcp/tools/shell/run_command_windows.py +5 -5
- hanzo_mcp/tools/shell/session_storage.py +3 -3
- hanzo_mcp/tools/shell/streaming_command.py +141 -126
- hanzo_mcp/tools/shell/uvx.py +24 -25
- hanzo_mcp/tools/shell/uvx_background.py +35 -33
- hanzo_mcp/tools/shell/uvx_tool.py +18 -22
- hanzo_mcp/tools/todo/__init__.py +6 -2
- hanzo_mcp/tools/todo/todo.py +50 -37
- hanzo_mcp/tools/todo/todo_read.py +5 -8
- hanzo_mcp/tools/todo/todo_write.py +5 -7
- hanzo_mcp/tools/vector/__init__.py +40 -28
- hanzo_mcp/tools/vector/ast_analyzer.py +176 -143
- hanzo_mcp/tools/vector/git_ingester.py +170 -179
- hanzo_mcp/tools/vector/index_tool.py +96 -44
- hanzo_mcp/tools/vector/infinity_store.py +283 -228
- hanzo_mcp/tools/vector/mock_infinity.py +39 -40
- hanzo_mcp/tools/vector/project_manager.py +88 -78
- hanzo_mcp/tools/vector/vector.py +59 -42
- hanzo_mcp/tools/vector/vector_index.py +30 -27
- hanzo_mcp/tools/vector/vector_search.py +64 -45
- hanzo_mcp/types.py +6 -4
- {hanzo_mcp-0.7.6.dist-info → hanzo_mcp-0.8.0.dist-info}/METADATA +1 -1
- hanzo_mcp-0.8.0.dist-info/RECORD +185 -0
- hanzo_mcp-0.7.6.dist-info/RECORD +0 -182
- {hanzo_mcp-0.7.6.dist-info → hanzo_mcp-0.8.0.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.7.6.dist-info → hanzo_mcp-0.8.0.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.7.6.dist-info → hanzo_mcp-0.8.0.dist-info}/top_level.txt +0 -0
|
@@ -10,28 +10,37 @@ This tool consolidates all search capabilities and runs them concurrently:
|
|
|
10
10
|
Results are combined, deduplicated, and ranked for comprehensive search coverage.
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
-
import asyncio
|
|
14
13
|
import re
|
|
15
|
-
|
|
16
|
-
from pathlib import Path
|
|
17
|
-
from typing import Annotated, Dict, List, Optional, Set, Tuple, TypedDict, Unpack, final, override
|
|
14
|
+
import asyncio
|
|
18
15
|
from enum import Enum
|
|
16
|
+
from typing import (
|
|
17
|
+
Dict,
|
|
18
|
+
List,
|
|
19
|
+
Unpack,
|
|
20
|
+
Optional,
|
|
21
|
+
Annotated,
|
|
22
|
+
TypedDict,
|
|
23
|
+
final,
|
|
24
|
+
override,
|
|
25
|
+
)
|
|
26
|
+
from dataclasses import dataclass
|
|
19
27
|
|
|
20
|
-
from mcp.server.fastmcp import Context as MCPContext
|
|
21
|
-
from mcp.server import FastMCP
|
|
22
28
|
from pydantic import Field
|
|
29
|
+
from mcp.server import FastMCP
|
|
30
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
23
31
|
|
|
24
32
|
from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
|
|
25
33
|
from hanzo_mcp.tools.filesystem.grep import Grep
|
|
26
|
-
from hanzo_mcp.tools.filesystem.symbols_tool import SymbolsTool
|
|
27
|
-
from hanzo_mcp.tools.filesystem.git_search import GitSearchTool
|
|
28
|
-
from hanzo_mcp.tools.vector.vector_search import VectorSearchTool
|
|
29
34
|
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
35
|
+
from hanzo_mcp.tools.vector.vector_search import VectorSearchTool
|
|
36
|
+
from hanzo_mcp.tools.filesystem.git_search import GitSearchTool
|
|
30
37
|
from hanzo_mcp.tools.vector.project_manager import ProjectVectorManager
|
|
38
|
+
from hanzo_mcp.tools.filesystem.symbols_tool import SymbolsTool
|
|
31
39
|
|
|
32
40
|
|
|
33
41
|
class SearchType(Enum):
|
|
34
42
|
"""Types of searches that can be performed."""
|
|
43
|
+
|
|
35
44
|
GREP = "grep"
|
|
36
45
|
GREP_AST = "grep_ast"
|
|
37
46
|
VECTOR = "vector"
|
|
@@ -42,6 +51,7 @@ class SearchType(Enum):
|
|
|
42
51
|
@dataclass
|
|
43
52
|
class SearchResult:
|
|
44
53
|
"""Search result from any search type."""
|
|
54
|
+
|
|
45
55
|
file_path: str
|
|
46
56
|
line_number: Optional[int]
|
|
47
57
|
content: str
|
|
@@ -134,6 +144,7 @@ IncludeContext = Annotated[
|
|
|
134
144
|
|
|
135
145
|
class UnifiedSearchParams(TypedDict):
|
|
136
146
|
"""Parameters for search."""
|
|
147
|
+
|
|
137
148
|
pattern: Pattern
|
|
138
149
|
path: SearchPath
|
|
139
150
|
include: Include
|
|
@@ -149,34 +160,37 @@ class UnifiedSearchParams(TypedDict):
|
|
|
149
160
|
@final
|
|
150
161
|
class SearchTool(FilesystemBaseTool):
|
|
151
162
|
"""Search tool that runs multiple search types in parallel."""
|
|
152
|
-
|
|
153
|
-
def __init__(
|
|
154
|
-
|
|
163
|
+
|
|
164
|
+
def __init__(
|
|
165
|
+
self,
|
|
166
|
+
permission_manager: PermissionManager,
|
|
167
|
+
project_manager: Optional[ProjectVectorManager] = None,
|
|
168
|
+
):
|
|
155
169
|
"""Initialize the search tool.
|
|
156
|
-
|
|
170
|
+
|
|
157
171
|
Args:
|
|
158
172
|
permission_manager: Permission manager for access control
|
|
159
173
|
project_manager: Optional project manager for vector search
|
|
160
174
|
"""
|
|
161
175
|
super().__init__(permission_manager)
|
|
162
176
|
self.project_manager = project_manager
|
|
163
|
-
|
|
177
|
+
|
|
164
178
|
# Initialize component tools
|
|
165
179
|
self.grep_tool = Grep(permission_manager)
|
|
166
180
|
self.grep_ast_tool = SymbolsTool(permission_manager)
|
|
167
181
|
self.git_search_tool = GitSearchTool(permission_manager)
|
|
168
|
-
|
|
182
|
+
|
|
169
183
|
# Vector search is optional
|
|
170
184
|
self.vector_tool = None
|
|
171
185
|
if project_manager:
|
|
172
186
|
self.vector_tool = VectorSearchTool(permission_manager, project_manager)
|
|
173
|
-
|
|
187
|
+
|
|
174
188
|
@property
|
|
175
189
|
@override
|
|
176
190
|
def name(self) -> str:
|
|
177
191
|
"""Get the tool name."""
|
|
178
192
|
return "search"
|
|
179
|
-
|
|
193
|
+
|
|
180
194
|
@property
|
|
181
195
|
@override
|
|
182
196
|
def description(self) -> str:
|
|
@@ -200,75 +214,76 @@ Examples:
|
|
|
200
214
|
- Track changes: pattern="bug fix" (searches git history too)
|
|
201
215
|
|
|
202
216
|
This is the recommended search tool for comprehensive results."""
|
|
203
|
-
|
|
217
|
+
|
|
204
218
|
def _analyze_pattern(self, pattern: str) -> Dict[str, bool]:
|
|
205
219
|
"""Analyze the pattern to determine optimal search strategies.
|
|
206
|
-
|
|
220
|
+
|
|
207
221
|
Args:
|
|
208
222
|
pattern: The search pattern
|
|
209
|
-
|
|
223
|
+
|
|
210
224
|
Returns:
|
|
211
225
|
Dictionary of search type recommendations
|
|
212
226
|
"""
|
|
213
227
|
# Check if pattern looks like regex
|
|
214
|
-
regex_chars = r
|
|
228
|
+
regex_chars = r"[.*+?^${}()|[\]\\]"
|
|
215
229
|
has_regex = bool(re.search(regex_chars, pattern))
|
|
216
|
-
|
|
230
|
+
|
|
217
231
|
# Check if pattern looks like a symbol name
|
|
218
|
-
is_symbol = bool(re.match(r
|
|
219
|
-
|
|
232
|
+
is_symbol = bool(re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", pattern))
|
|
233
|
+
|
|
220
234
|
# Check if pattern is natural language
|
|
221
235
|
words = pattern.split()
|
|
222
236
|
is_natural_language = len(words) > 2 and not has_regex
|
|
223
|
-
|
|
237
|
+
|
|
224
238
|
return {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
239
|
+
"use_grep": True, # Always useful
|
|
240
|
+
"use_grep_ast": not has_regex, # AST doesn't handle regex well
|
|
241
|
+
"use_vector": is_natural_language or len(pattern) > 10,
|
|
242
|
+
"use_git": True, # Always check history
|
|
243
|
+
"use_symbol": is_symbol or "def" in pattern or "class" in pattern,
|
|
230
244
|
}
|
|
231
|
-
|
|
232
|
-
async def _run_grep_search(
|
|
233
|
-
|
|
245
|
+
|
|
246
|
+
async def _run_grep_search(
|
|
247
|
+
self, pattern: str, path: str, include: str, tool_ctx, max_results: int
|
|
248
|
+
) -> List[SearchResult]:
|
|
234
249
|
"""Run grep search and parse results."""
|
|
235
250
|
try:
|
|
236
251
|
result = await self.grep_tool.call(
|
|
237
|
-
tool_ctx.mcp_context,
|
|
238
|
-
pattern=pattern,
|
|
239
|
-
path=path,
|
|
240
|
-
include=include
|
|
252
|
+
tool_ctx.mcp_context, pattern=pattern, path=path, include=include
|
|
241
253
|
)
|
|
242
|
-
|
|
254
|
+
|
|
243
255
|
results = []
|
|
244
256
|
if "Found" in result and "matches" in result:
|
|
245
|
-
lines = result.split(
|
|
257
|
+
lines = result.split("\n")
|
|
246
258
|
for line in lines[2:]: # Skip header
|
|
247
|
-
if
|
|
259
|
+
if ":" in line and line.strip():
|
|
248
260
|
try:
|
|
249
|
-
parts = line.split(
|
|
261
|
+
parts = line.split(":", 2)
|
|
250
262
|
if len(parts) >= 3:
|
|
251
|
-
results.append(
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
263
|
+
results.append(
|
|
264
|
+
SearchResult(
|
|
265
|
+
file_path=parts[0],
|
|
266
|
+
line_number=int(parts[1]),
|
|
267
|
+
content=parts[2].strip(),
|
|
268
|
+
search_type=SearchType.GREP,
|
|
269
|
+
score=1.0, # Exact matches get perfect score
|
|
270
|
+
)
|
|
271
|
+
)
|
|
258
272
|
if len(results) >= max_results:
|
|
259
273
|
break
|
|
260
274
|
except ValueError:
|
|
261
275
|
continue
|
|
262
|
-
|
|
276
|
+
|
|
263
277
|
await tool_ctx.info(f"Grep found {len(results)} results")
|
|
264
278
|
return results
|
|
265
|
-
|
|
279
|
+
|
|
266
280
|
except Exception as e:
|
|
267
281
|
await tool_ctx.error(f"Grep search failed: {e}")
|
|
268
282
|
return []
|
|
269
|
-
|
|
270
|
-
async def _run_grep_ast_search(
|
|
271
|
-
|
|
283
|
+
|
|
284
|
+
async def _run_grep_ast_search(
|
|
285
|
+
self, pattern: str, path: str, tool_ctx, max_results: int
|
|
286
|
+
) -> List[SearchResult]:
|
|
272
287
|
"""Run AST-aware search and parse results."""
|
|
273
288
|
try:
|
|
274
289
|
result = await self.grep_ast_tool.call(
|
|
@@ -276,107 +291,117 @@ This is the recommended search tool for comprehensive results."""
|
|
|
276
291
|
pattern=pattern,
|
|
277
292
|
path=path,
|
|
278
293
|
ignore_case=True,
|
|
279
|
-
line_number=True
|
|
294
|
+
line_number=True,
|
|
280
295
|
)
|
|
281
|
-
|
|
296
|
+
|
|
282
297
|
results = []
|
|
283
298
|
if result and not result.startswith("No matches"):
|
|
284
299
|
current_file = None
|
|
285
300
|
current_context = []
|
|
286
|
-
|
|
287
|
-
for line in result.split(
|
|
288
|
-
if line.endswith(
|
|
301
|
+
|
|
302
|
+
for line in result.split("\n"):
|
|
303
|
+
if line.endswith(":") and "/" in line:
|
|
289
304
|
current_file = line[:-1]
|
|
290
305
|
current_context = []
|
|
291
|
-
elif current_file and
|
|
306
|
+
elif current_file and ":" in line:
|
|
292
307
|
try:
|
|
293
308
|
# Try to parse line with number
|
|
294
|
-
parts = line.split(
|
|
309
|
+
parts = line.split(":", 1)
|
|
295
310
|
line_num = int(parts[0].strip())
|
|
296
311
|
content = parts[1].strip() if len(parts) > 1 else ""
|
|
297
|
-
|
|
298
|
-
results.append(
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
312
|
+
|
|
313
|
+
results.append(
|
|
314
|
+
SearchResult(
|
|
315
|
+
file_path=current_file,
|
|
316
|
+
line_number=line_num,
|
|
317
|
+
content=content,
|
|
318
|
+
search_type=SearchType.GREP_AST,
|
|
319
|
+
score=0.95, # High score for AST matches
|
|
320
|
+
context=(
|
|
321
|
+
" > ".join(current_context)
|
|
322
|
+
if current_context
|
|
323
|
+
else None
|
|
324
|
+
),
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
|
|
307
328
|
if len(results) >= max_results:
|
|
308
329
|
break
|
|
309
330
|
except ValueError:
|
|
310
331
|
# This might be context info
|
|
311
332
|
if line.strip():
|
|
312
333
|
current_context.append(line.strip())
|
|
313
|
-
|
|
334
|
+
|
|
314
335
|
await tool_ctx.info(f"AST search found {len(results)} results")
|
|
315
336
|
return results
|
|
316
|
-
|
|
337
|
+
|
|
317
338
|
except Exception as e:
|
|
318
339
|
await tool_ctx.error(f"AST search failed: {e}")
|
|
319
340
|
return []
|
|
320
|
-
|
|
321
|
-
async def _run_vector_search(
|
|
322
|
-
|
|
341
|
+
|
|
342
|
+
async def _run_vector_search(
|
|
343
|
+
self, pattern: str, path: str, tool_ctx, max_results: int
|
|
344
|
+
) -> List[SearchResult]:
|
|
323
345
|
"""Run semantic vector search."""
|
|
324
346
|
if not self.vector_tool:
|
|
325
347
|
return []
|
|
326
|
-
|
|
348
|
+
|
|
327
349
|
try:
|
|
328
350
|
# Determine search scope
|
|
329
351
|
search_scope = "current" if path == "." else "all"
|
|
330
|
-
|
|
352
|
+
|
|
331
353
|
result = await self.vector_tool.call(
|
|
332
354
|
tool_ctx.mcp_context,
|
|
333
355
|
query=pattern,
|
|
334
356
|
limit=max_results,
|
|
335
357
|
score_threshold=0.3,
|
|
336
358
|
search_scope=search_scope,
|
|
337
|
-
include_content=True
|
|
359
|
+
include_content=True,
|
|
338
360
|
)
|
|
339
|
-
|
|
361
|
+
|
|
340
362
|
results = []
|
|
341
363
|
if "Found" in result:
|
|
342
364
|
# Parse vector search results
|
|
343
|
-
lines = result.split(
|
|
365
|
+
lines = result.split("\n")
|
|
344
366
|
current_file = None
|
|
345
367
|
current_score = 0.0
|
|
346
|
-
|
|
368
|
+
|
|
347
369
|
for line in lines:
|
|
348
370
|
if "Result" in line and "Score:" in line:
|
|
349
371
|
# Extract score and file
|
|
350
|
-
score_match = re.search(r
|
|
372
|
+
score_match = re.search(r"Score: ([\d.]+)%", line)
|
|
351
373
|
if score_match:
|
|
352
374
|
current_score = float(score_match.group(1)) / 100.0
|
|
353
|
-
|
|
354
|
-
file_match = re.search(r
|
|
375
|
+
|
|
376
|
+
file_match = re.search(r" - ([^\s]+)$", line)
|
|
355
377
|
if file_match:
|
|
356
378
|
current_file = file_match.group(1)
|
|
357
|
-
|
|
358
|
-
elif current_file and line.strip() and not line.startswith(
|
|
379
|
+
|
|
380
|
+
elif current_file and line.strip() and not line.startswith("-"):
|
|
359
381
|
# Content line
|
|
360
|
-
results.append(
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
382
|
+
results.append(
|
|
383
|
+
SearchResult(
|
|
384
|
+
file_path=current_file,
|
|
385
|
+
line_number=None,
|
|
386
|
+
content=line.strip()[:200], # Limit content length
|
|
387
|
+
search_type=SearchType.VECTOR,
|
|
388
|
+
score=current_score,
|
|
389
|
+
)
|
|
390
|
+
)
|
|
391
|
+
|
|
368
392
|
if len(results) >= max_results:
|
|
369
393
|
break
|
|
370
|
-
|
|
394
|
+
|
|
371
395
|
await tool_ctx.info(f"Vector search found {len(results)} results")
|
|
372
396
|
return results
|
|
373
|
-
|
|
397
|
+
|
|
374
398
|
except Exception as e:
|
|
375
399
|
await tool_ctx.error(f"Vector search failed: {e}")
|
|
376
400
|
return []
|
|
377
|
-
|
|
378
|
-
async def _run_git_search(
|
|
379
|
-
|
|
401
|
+
|
|
402
|
+
async def _run_git_search(
|
|
403
|
+
self, pattern: str, path: str, tool_ctx, max_results: int
|
|
404
|
+
) -> List[SearchResult]:
|
|
380
405
|
"""Run git history search."""
|
|
381
406
|
try:
|
|
382
407
|
# Search in both content and commits
|
|
@@ -386,51 +411,58 @@ This is the recommended search tool for comprehensive results."""
|
|
|
386
411
|
pattern=pattern,
|
|
387
412
|
path=path,
|
|
388
413
|
search_type="content",
|
|
389
|
-
max_count=max_results // 2
|
|
414
|
+
max_count=max_results // 2,
|
|
390
415
|
),
|
|
391
416
|
self.git_search_tool.call(
|
|
392
417
|
tool_ctx.mcp_context,
|
|
393
418
|
pattern=pattern,
|
|
394
419
|
path=path,
|
|
395
420
|
search_type="commits",
|
|
396
|
-
max_count=max_results // 2
|
|
397
|
-
)
|
|
421
|
+
max_count=max_results // 2,
|
|
422
|
+
),
|
|
398
423
|
]
|
|
399
|
-
|
|
424
|
+
|
|
400
425
|
git_results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
401
|
-
|
|
426
|
+
|
|
402
427
|
results = []
|
|
403
|
-
for
|
|
428
|
+
for _i, result in enumerate(git_results):
|
|
404
429
|
if isinstance(result, Exception):
|
|
405
430
|
continue
|
|
406
|
-
|
|
431
|
+
|
|
407
432
|
if "Found" in result:
|
|
408
433
|
# Parse git results
|
|
409
|
-
lines = result.split(
|
|
434
|
+
lines = result.split("\n")
|
|
410
435
|
for line in lines:
|
|
411
|
-
if
|
|
412
|
-
parts = line.split(
|
|
436
|
+
if ":" in line and line.strip():
|
|
437
|
+
parts = line.split(":", 2)
|
|
413
438
|
if len(parts) >= 2:
|
|
414
|
-
results.append(
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
439
|
+
results.append(
|
|
440
|
+
SearchResult(
|
|
441
|
+
file_path=parts[0].strip(),
|
|
442
|
+
line_number=None,
|
|
443
|
+
content=(
|
|
444
|
+
parts[-1].strip()
|
|
445
|
+
if len(parts) > 2
|
|
446
|
+
else line
|
|
447
|
+
),
|
|
448
|
+
search_type=SearchType.GIT,
|
|
449
|
+
score=0.8, # Good score for git matches
|
|
450
|
+
)
|
|
451
|
+
)
|
|
452
|
+
|
|
422
453
|
if len(results) >= max_results:
|
|
423
454
|
break
|
|
424
|
-
|
|
455
|
+
|
|
425
456
|
await tool_ctx.info(f"Git search found {len(results)} results")
|
|
426
457
|
return results
|
|
427
|
-
|
|
458
|
+
|
|
428
459
|
except Exception as e:
|
|
429
460
|
await tool_ctx.error(f"Git search failed: {e}")
|
|
430
461
|
return []
|
|
431
|
-
|
|
432
|
-
async def _run_symbol_search(
|
|
433
|
-
|
|
462
|
+
|
|
463
|
+
async def _run_symbol_search(
|
|
464
|
+
self, pattern: str, path: str, tool_ctx, max_results: int
|
|
465
|
+
) -> List[SearchResult]:
|
|
434
466
|
"""Search for symbol definitions using grep with specific patterns."""
|
|
435
467
|
try:
|
|
436
468
|
# Create patterns for common symbol definitions
|
|
@@ -441,67 +473,68 @@ This is the recommended search tool for comprehensive results."""
|
|
|
441
473
|
f"let\\s+{pattern}\\s*=", # JS/TS let
|
|
442
474
|
f"var\\s+{pattern}\\s*=", # JS/TS var
|
|
443
475
|
]
|
|
444
|
-
|
|
476
|
+
|
|
445
477
|
# Run grep searches in parallel for each pattern
|
|
446
478
|
tasks = []
|
|
447
479
|
for sp in symbol_patterns:
|
|
448
480
|
tasks.append(
|
|
449
481
|
self.grep_tool.call(
|
|
450
|
-
tool_ctx.mcp_context,
|
|
451
|
-
pattern=sp,
|
|
452
|
-
path=path,
|
|
453
|
-
include="*"
|
|
482
|
+
tool_ctx.mcp_context, pattern=sp, path=path, include="*"
|
|
454
483
|
)
|
|
455
484
|
)
|
|
456
|
-
|
|
485
|
+
|
|
457
486
|
grep_results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
458
|
-
|
|
487
|
+
|
|
459
488
|
results = []
|
|
460
489
|
for result in grep_results:
|
|
461
490
|
if isinstance(result, Exception):
|
|
462
491
|
continue
|
|
463
|
-
|
|
492
|
+
|
|
464
493
|
if "Found" in result and "matches" in result:
|
|
465
|
-
lines = result.split(
|
|
494
|
+
lines = result.split("\n")
|
|
466
495
|
for line in lines[2:]: # Skip header
|
|
467
|
-
if
|
|
496
|
+
if ":" in line and line.strip():
|
|
468
497
|
try:
|
|
469
|
-
parts = line.split(
|
|
498
|
+
parts = line.split(":", 2)
|
|
470
499
|
if len(parts) >= 3:
|
|
471
|
-
results.append(
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
500
|
+
results.append(
|
|
501
|
+
SearchResult(
|
|
502
|
+
file_path=parts[0],
|
|
503
|
+
line_number=int(parts[1]),
|
|
504
|
+
content=parts[2].strip(),
|
|
505
|
+
search_type=SearchType.SYMBOL,
|
|
506
|
+
score=0.98, # Very high score for symbol definitions
|
|
507
|
+
)
|
|
508
|
+
)
|
|
478
509
|
if len(results) >= max_results:
|
|
479
510
|
break
|
|
480
511
|
except ValueError:
|
|
481
512
|
continue
|
|
482
|
-
|
|
513
|
+
|
|
483
514
|
await tool_ctx.info(f"Symbol search found {len(results)} results")
|
|
484
515
|
return results
|
|
485
|
-
|
|
516
|
+
|
|
486
517
|
except Exception as e:
|
|
487
518
|
await tool_ctx.error(f"Symbol search failed: {e}")
|
|
488
519
|
return []
|
|
489
|
-
|
|
490
|
-
def _deduplicate_results(
|
|
520
|
+
|
|
521
|
+
def _deduplicate_results(
|
|
522
|
+
self, all_results: List[SearchResult]
|
|
523
|
+
) -> List[SearchResult]:
|
|
491
524
|
"""Deduplicate results, keeping the highest scoring version."""
|
|
492
525
|
seen = {}
|
|
493
|
-
|
|
526
|
+
|
|
494
527
|
for result in all_results:
|
|
495
528
|
key = (result.file_path, result.line_number)
|
|
496
|
-
|
|
529
|
+
|
|
497
530
|
if key not in seen or result.score > seen[key].score:
|
|
498
531
|
seen[key] = result
|
|
499
532
|
elif key in seen and result.context and not seen[key].context:
|
|
500
533
|
# Add context if missing
|
|
501
534
|
seen[key].context = result.context
|
|
502
|
-
|
|
535
|
+
|
|
503
536
|
return list(seen.values())
|
|
504
|
-
|
|
537
|
+
|
|
505
538
|
def _rank_results(self, results: List[SearchResult]) -> List[SearchResult]:
|
|
506
539
|
"""Rank results by relevance score and search type priority."""
|
|
507
540
|
# Define search type priorities
|
|
@@ -510,17 +543,16 @@ This is the recommended search tool for comprehensive results."""
|
|
|
510
543
|
SearchType.GREP: 4,
|
|
511
544
|
SearchType.GREP_AST: 3,
|
|
512
545
|
SearchType.GIT: 2,
|
|
513
|
-
SearchType.VECTOR: 1
|
|
546
|
+
SearchType.VECTOR: 1,
|
|
514
547
|
}
|
|
515
|
-
|
|
548
|
+
|
|
516
549
|
# Sort by score (descending) and then by type priority
|
|
517
550
|
results.sort(
|
|
518
|
-
key=lambda r: (r.score, type_priority.get(r.search_type, 0)),
|
|
519
|
-
reverse=True
|
|
551
|
+
key=lambda r: (r.score, type_priority.get(r.search_type, 0)), reverse=True
|
|
520
552
|
)
|
|
521
|
-
|
|
553
|
+
|
|
522
554
|
return results
|
|
523
|
-
|
|
555
|
+
|
|
524
556
|
@override
|
|
525
557
|
async def call(
|
|
526
558
|
self,
|
|
@@ -529,71 +561,88 @@ This is the recommended search tool for comprehensive results."""
|
|
|
529
561
|
) -> str:
|
|
530
562
|
"""Execute search across all enabled search types."""
|
|
531
563
|
import time
|
|
564
|
+
|
|
532
565
|
start_time = time.time()
|
|
533
|
-
|
|
566
|
+
|
|
534
567
|
tool_ctx = self.create_tool_context(ctx)
|
|
535
|
-
|
|
568
|
+
|
|
536
569
|
# Extract parameters
|
|
537
570
|
pattern = params["pattern"]
|
|
538
571
|
path = params.get("path", ".")
|
|
539
572
|
include = params.get("include", "*")
|
|
540
573
|
max_results = params.get("max_results", 50)
|
|
541
574
|
include_context = params.get("include_context", True)
|
|
542
|
-
|
|
575
|
+
|
|
543
576
|
# Validate path
|
|
544
577
|
path_validation = self.validate_path(path)
|
|
545
578
|
if path_validation.is_error:
|
|
546
579
|
await tool_ctx.error(path_validation.error_message)
|
|
547
580
|
return f"Error: {path_validation.error_message}"
|
|
548
|
-
|
|
581
|
+
|
|
549
582
|
# Check permissions
|
|
550
583
|
allowed, error_msg = await self.check_path_allowed(path, tool_ctx)
|
|
551
584
|
if not allowed:
|
|
552
585
|
return error_msg
|
|
553
|
-
|
|
586
|
+
|
|
554
587
|
# Check existence
|
|
555
588
|
exists, error_msg = await self.check_path_exists(path, tool_ctx)
|
|
556
589
|
if not exists:
|
|
557
590
|
return error_msg
|
|
558
|
-
|
|
591
|
+
|
|
559
592
|
# Analyze pattern to determine best search strategies
|
|
560
593
|
pattern_analysis = self._analyze_pattern(pattern)
|
|
561
|
-
|
|
594
|
+
|
|
562
595
|
await tool_ctx.info(f"Starting search for '{pattern}' in {path}")
|
|
563
|
-
|
|
596
|
+
|
|
564
597
|
# Build list of search tasks based on enabled types and pattern analysis
|
|
565
598
|
search_tasks = []
|
|
566
599
|
search_names = []
|
|
567
|
-
|
|
568
|
-
if params.get("enable_grep", True) and pattern_analysis[
|
|
569
|
-
search_tasks.append(
|
|
600
|
+
|
|
601
|
+
if params.get("enable_grep", True) and pattern_analysis["use_grep"]:
|
|
602
|
+
search_tasks.append(
|
|
603
|
+
self._run_grep_search(pattern, path, include, tool_ctx, max_results)
|
|
604
|
+
)
|
|
570
605
|
search_names.append("grep")
|
|
571
|
-
|
|
572
|
-
if params.get("enable_grep_ast", True) and pattern_analysis[
|
|
573
|
-
search_tasks.append(
|
|
606
|
+
|
|
607
|
+
if params.get("enable_grep_ast", True) and pattern_analysis["use_grep_ast"]:
|
|
608
|
+
search_tasks.append(
|
|
609
|
+
self._run_grep_ast_search(pattern, path, tool_ctx, max_results)
|
|
610
|
+
)
|
|
574
611
|
search_names.append("grep_ast")
|
|
575
|
-
|
|
576
|
-
if
|
|
577
|
-
|
|
612
|
+
|
|
613
|
+
if (
|
|
614
|
+
params.get("enable_vector", True)
|
|
615
|
+
and self.vector_tool
|
|
616
|
+
and pattern_analysis["use_vector"]
|
|
617
|
+
):
|
|
618
|
+
search_tasks.append(
|
|
619
|
+
self._run_vector_search(pattern, path, tool_ctx, max_results)
|
|
620
|
+
)
|
|
578
621
|
search_names.append("vector")
|
|
579
|
-
|
|
580
|
-
if params.get("enable_git", True) and pattern_analysis[
|
|
581
|
-
search_tasks.append(
|
|
622
|
+
|
|
623
|
+
if params.get("enable_git", True) and pattern_analysis["use_git"]:
|
|
624
|
+
search_tasks.append(
|
|
625
|
+
self._run_git_search(pattern, path, tool_ctx, max_results)
|
|
626
|
+
)
|
|
582
627
|
search_names.append("git")
|
|
583
|
-
|
|
584
|
-
if params.get("enable_symbol", True) and pattern_analysis[
|
|
585
|
-
search_tasks.append(
|
|
628
|
+
|
|
629
|
+
if params.get("enable_symbol", True) and pattern_analysis["use_symbol"]:
|
|
630
|
+
search_tasks.append(
|
|
631
|
+
self._run_symbol_search(pattern, path, tool_ctx, max_results)
|
|
632
|
+
)
|
|
586
633
|
search_names.append("symbol")
|
|
587
|
-
|
|
588
|
-
await tool_ctx.info(
|
|
589
|
-
|
|
634
|
+
|
|
635
|
+
await tool_ctx.info(
|
|
636
|
+
f"Running {len(search_tasks)} search types in parallel: {', '.join(search_names)}"
|
|
637
|
+
)
|
|
638
|
+
|
|
590
639
|
# Run all searches in parallel
|
|
591
640
|
search_results = await asyncio.gather(*search_tasks, return_exceptions=True)
|
|
592
|
-
|
|
641
|
+
|
|
593
642
|
# Collect all results
|
|
594
643
|
all_results = []
|
|
595
644
|
results_by_type = {}
|
|
596
|
-
|
|
645
|
+
|
|
597
646
|
for search_type, results in zip(search_names, search_results):
|
|
598
647
|
if isinstance(results, Exception):
|
|
599
648
|
await tool_ctx.error(f"{search_type} search failed: {results}")
|
|
@@ -601,87 +650,94 @@ This is the recommended search tool for comprehensive results."""
|
|
|
601
650
|
else:
|
|
602
651
|
results_by_type[search_type] = results
|
|
603
652
|
all_results.extend(results)
|
|
604
|
-
|
|
653
|
+
|
|
605
654
|
# Deduplicate and rank results
|
|
606
655
|
unique_results = self._deduplicate_results(all_results)
|
|
607
656
|
ranked_results = self._rank_results(unique_results)
|
|
608
|
-
|
|
657
|
+
|
|
609
658
|
# Limit total results
|
|
610
659
|
final_results = ranked_results[:max_results]
|
|
611
|
-
|
|
660
|
+
|
|
612
661
|
# Calculate search time
|
|
613
662
|
search_time = (time.time() - start_time) * 1000
|
|
614
|
-
|
|
663
|
+
|
|
615
664
|
# Format output
|
|
616
665
|
return self._format_results(
|
|
617
666
|
pattern=pattern,
|
|
618
667
|
results=final_results,
|
|
619
668
|
results_by_type=results_by_type,
|
|
620
669
|
search_time_ms=search_time,
|
|
621
|
-
include_context=include_context
|
|
670
|
+
include_context=include_context,
|
|
622
671
|
)
|
|
623
|
-
|
|
624
|
-
def _format_results(
|
|
625
|
-
|
|
626
|
-
|
|
672
|
+
|
|
673
|
+
def _format_results(
|
|
674
|
+
self,
|
|
675
|
+
pattern: str,
|
|
676
|
+
results: List[SearchResult],
|
|
677
|
+
results_by_type: Dict[str, List[SearchResult]],
|
|
678
|
+
search_time_ms: float,
|
|
679
|
+
include_context: bool,
|
|
680
|
+
) -> str:
|
|
627
681
|
"""Format search results for display."""
|
|
628
682
|
output = []
|
|
629
|
-
|
|
683
|
+
|
|
630
684
|
# Header
|
|
631
685
|
output.append(f"=== Unified Search Results ===")
|
|
632
686
|
output.append(f"Pattern: '{pattern}'")
|
|
633
687
|
output.append(f"Total results: {len(results)}")
|
|
634
688
|
output.append(f"Search time: {search_time_ms:.1f}ms")
|
|
635
|
-
|
|
689
|
+
|
|
636
690
|
# Summary by type
|
|
637
691
|
output.append("\nResults by type:")
|
|
638
692
|
for search_type, type_results in results_by_type.items():
|
|
639
693
|
if type_results:
|
|
640
694
|
output.append(f" {search_type}: {len(type_results)} matches")
|
|
641
|
-
|
|
695
|
+
|
|
642
696
|
if not results:
|
|
643
697
|
output.append("\nNo results found.")
|
|
644
698
|
return "\n".join(output)
|
|
645
|
-
|
|
699
|
+
|
|
646
700
|
# Group results by file
|
|
647
701
|
results_by_file = {}
|
|
648
702
|
for result in results:
|
|
649
703
|
if result.file_path not in results_by_file:
|
|
650
704
|
results_by_file[result.file_path] = []
|
|
651
705
|
results_by_file[result.file_path].append(result)
|
|
652
|
-
|
|
706
|
+
|
|
653
707
|
# Display results
|
|
654
708
|
output.append(f"\n=== Results ({len(results)} total) ===\n")
|
|
655
|
-
|
|
709
|
+
|
|
656
710
|
for file_path, file_results in results_by_file.items():
|
|
657
711
|
output.append(f"{file_path}")
|
|
658
712
|
output.append("-" * len(file_path))
|
|
659
|
-
|
|
713
|
+
|
|
660
714
|
# Sort by line number
|
|
661
715
|
file_results.sort(key=lambda r: r.line_number or 0)
|
|
662
|
-
|
|
716
|
+
|
|
663
717
|
for result in file_results:
|
|
664
718
|
# Format result line
|
|
665
719
|
score_str = f"[{result.search_type.value} {result.score:.2f}]"
|
|
666
|
-
|
|
720
|
+
|
|
667
721
|
if result.line_number:
|
|
668
|
-
output.append(
|
|
722
|
+
output.append(
|
|
723
|
+
f" {result.line_number:>4}: {score_str} {result.content}"
|
|
724
|
+
)
|
|
669
725
|
else:
|
|
670
726
|
output.append(f" {score_str} {result.content}")
|
|
671
|
-
|
|
727
|
+
|
|
672
728
|
# Add context if available and requested
|
|
673
729
|
if include_context and result.context:
|
|
674
730
|
output.append(f" Context: {result.context}")
|
|
675
|
-
|
|
731
|
+
|
|
676
732
|
output.append("") # Empty line between files
|
|
677
|
-
|
|
733
|
+
|
|
678
734
|
return "\n".join(output)
|
|
679
|
-
|
|
735
|
+
|
|
680
736
|
@override
|
|
681
737
|
def register(self, mcp_server: FastMCP) -> None:
|
|
682
738
|
"""Register the search tool with the MCP server."""
|
|
683
739
|
tool_self = self
|
|
684
|
-
|
|
740
|
+
|
|
685
741
|
@mcp_server.tool(name=self.name, description=self.description)
|
|
686
742
|
async def search(
|
|
687
743
|
ctx: MCPContext,
|
|
@@ -708,4 +764,4 @@ This is the recommended search tool for comprehensive results."""
|
|
|
708
764
|
enable_git=enable_git,
|
|
709
765
|
enable_symbol=enable_symbol,
|
|
710
766
|
include_context=include_context,
|
|
711
|
-
)
|
|
767
|
+
)
|