hanzo-mcp 0.8.8__py3-none-any.whl → 0.9.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 +1 -3
- hanzo_mcp/analytics/posthog_analytics.py +4 -17
- hanzo_mcp/bridge.py +9 -25
- hanzo_mcp/cli.py +8 -17
- hanzo_mcp/cli_enhanced.py +5 -14
- hanzo_mcp/cli_plugin.py +3 -9
- hanzo_mcp/config/settings.py +6 -20
- hanzo_mcp/config/tool_config.py +2 -4
- hanzo_mcp/core/base_agent.py +88 -88
- hanzo_mcp/core/model_registry.py +238 -210
- hanzo_mcp/dev_server.py +5 -15
- hanzo_mcp/prompts/__init__.py +2 -6
- hanzo_mcp/prompts/project_todo_reminder.py +3 -9
- hanzo_mcp/prompts/tool_explorer.py +1 -3
- hanzo_mcp/prompts/utils.py +7 -21
- hanzo_mcp/server.py +6 -7
- hanzo_mcp/tools/__init__.py +29 -32
- hanzo_mcp/tools/agent/__init__.py +2 -1
- hanzo_mcp/tools/agent/agent.py +10 -30
- hanzo_mcp/tools/agent/agent_tool.py +23 -17
- hanzo_mcp/tools/agent/claude_desktop_auth.py +3 -9
- hanzo_mcp/tools/agent/cli_agent_base.py +7 -24
- hanzo_mcp/tools/agent/cli_tools.py +76 -75
- hanzo_mcp/tools/agent/code_auth.py +1 -3
- hanzo_mcp/tools/agent/code_auth_tool.py +2 -6
- hanzo_mcp/tools/agent/critic_tool.py +8 -24
- hanzo_mcp/tools/agent/iching_tool.py +12 -36
- hanzo_mcp/tools/agent/network_tool.py +7 -18
- hanzo_mcp/tools/agent/prompt.py +1 -5
- hanzo_mcp/tools/agent/review_tool.py +10 -25
- hanzo_mcp/tools/agent/swarm_alias.py +1 -3
- hanzo_mcp/tools/agent/unified_cli_tools.py +38 -38
- hanzo_mcp/tools/common/batch_tool.py +15 -45
- hanzo_mcp/tools/common/config_tool.py +9 -28
- hanzo_mcp/tools/common/context.py +1 -3
- hanzo_mcp/tools/common/critic_tool.py +1 -3
- hanzo_mcp/tools/common/decorators.py +2 -6
- hanzo_mcp/tools/common/enhanced_base.py +2 -6
- hanzo_mcp/tools/common/fastmcp_pagination.py +4 -12
- hanzo_mcp/tools/common/forgiving_edit.py +9 -28
- hanzo_mcp/tools/common/mode.py +1 -5
- hanzo_mcp/tools/common/paginated_base.py +3 -11
- hanzo_mcp/tools/common/paginated_response.py +10 -30
- hanzo_mcp/tools/common/pagination.py +3 -9
- hanzo_mcp/tools/common/path_utils.py +34 -0
- hanzo_mcp/tools/common/permissions.py +14 -13
- hanzo_mcp/tools/common/personality.py +983 -701
- hanzo_mcp/tools/common/plugin_loader.py +3 -15
- hanzo_mcp/tools/common/stats.py +7 -19
- hanzo_mcp/tools/common/thinking_tool.py +1 -3
- hanzo_mcp/tools/common/tool_disable.py +2 -6
- hanzo_mcp/tools/common/tool_list.py +2 -6
- hanzo_mcp/tools/common/validation.py +1 -3
- hanzo_mcp/tools/compiler/__init__.py +8 -0
- hanzo_mcp/tools/compiler/sandboxed_compiler.py +681 -0
- hanzo_mcp/tools/config/config_tool.py +7 -13
- hanzo_mcp/tools/config/index_config.py +1 -3
- hanzo_mcp/tools/config/mode_tool.py +5 -15
- hanzo_mcp/tools/database/database_manager.py +3 -9
- hanzo_mcp/tools/database/graph.py +1 -3
- hanzo_mcp/tools/database/graph_add.py +3 -9
- hanzo_mcp/tools/database/graph_query.py +11 -34
- hanzo_mcp/tools/database/graph_remove.py +3 -9
- hanzo_mcp/tools/database/graph_search.py +6 -20
- hanzo_mcp/tools/database/graph_stats.py +11 -33
- hanzo_mcp/tools/database/sql.py +4 -12
- hanzo_mcp/tools/database/sql_query.py +6 -10
- hanzo_mcp/tools/database/sql_search.py +2 -6
- hanzo_mcp/tools/database/sql_stats.py +5 -15
- hanzo_mcp/tools/editor/neovim_command.py +1 -3
- hanzo_mcp/tools/editor/neovim_session.py +7 -13
- hanzo_mcp/tools/environment/__init__.py +8 -0
- hanzo_mcp/tools/environment/environment_detector.py +594 -0
- hanzo_mcp/tools/filesystem/__init__.py +28 -26
- hanzo_mcp/tools/filesystem/ast_multi_edit.py +14 -43
- hanzo_mcp/tools/filesystem/ast_tool.py +3 -0
- hanzo_mcp/tools/filesystem/base.py +20 -12
- hanzo_mcp/tools/filesystem/content_replace.py +7 -12
- hanzo_mcp/tools/filesystem/diff.py +2 -10
- hanzo_mcp/tools/filesystem/directory_tree.py +285 -51
- hanzo_mcp/tools/filesystem/edit.py +10 -18
- hanzo_mcp/tools/filesystem/find.py +312 -179
- hanzo_mcp/tools/filesystem/git_search.py +12 -24
- hanzo_mcp/tools/filesystem/multi_edit.py +10 -18
- hanzo_mcp/tools/filesystem/read.py +14 -30
- hanzo_mcp/tools/filesystem/rules_tool.py +9 -17
- hanzo_mcp/tools/filesystem/search.py +1160 -0
- hanzo_mcp/tools/filesystem/watch.py +2 -4
- hanzo_mcp/tools/filesystem/write.py +7 -10
- hanzo_mcp/tools/framework/__init__.py +8 -0
- hanzo_mcp/tools/framework/framework_modes.py +714 -0
- hanzo_mcp/tools/jupyter/base.py +6 -20
- hanzo_mcp/tools/jupyter/jupyter.py +4 -12
- hanzo_mcp/tools/llm/consensus_tool.py +8 -24
- hanzo_mcp/tools/llm/llm_manage.py +2 -6
- hanzo_mcp/tools/llm/llm_tool.py +17 -58
- hanzo_mcp/tools/llm/llm_unified.py +18 -59
- hanzo_mcp/tools/llm/provider_tools.py +1 -3
- hanzo_mcp/tools/lsp/lsp_tool.py +621 -481
- hanzo_mcp/tools/mcp/mcp_add.py +3 -5
- hanzo_mcp/tools/mcp/mcp_remove.py +1 -1
- hanzo_mcp/tools/mcp/mcp_stats.py +1 -3
- hanzo_mcp/tools/mcp/mcp_tool.py +9 -23
- hanzo_mcp/tools/memory/__init__.py +33 -40
- hanzo_mcp/tools/memory/conversation_memory.py +636 -0
- hanzo_mcp/tools/memory/knowledge_tools.py +7 -25
- hanzo_mcp/tools/memory/memory_tools.py +7 -19
- hanzo_mcp/tools/search/find_tool.py +12 -34
- hanzo_mcp/tools/search/unified_search.py +27 -81
- hanzo_mcp/tools/shell/__init__.py +16 -4
- hanzo_mcp/tools/shell/auto_background.py +2 -6
- hanzo_mcp/tools/shell/base.py +1 -5
- hanzo_mcp/tools/shell/base_process.py +5 -7
- hanzo_mcp/tools/shell/bash_session.py +7 -24
- hanzo_mcp/tools/shell/bash_session_executor.py +5 -15
- hanzo_mcp/tools/shell/bash_tool.py +3 -7
- hanzo_mcp/tools/shell/command_executor.py +26 -79
- hanzo_mcp/tools/shell/logs.py +4 -16
- hanzo_mcp/tools/shell/npx.py +2 -8
- hanzo_mcp/tools/shell/npx_tool.py +1 -3
- hanzo_mcp/tools/shell/pkill.py +4 -12
- hanzo_mcp/tools/shell/process_tool.py +2 -8
- hanzo_mcp/tools/shell/processes.py +5 -17
- hanzo_mcp/tools/shell/run_background.py +1 -3
- hanzo_mcp/tools/shell/run_command.py +1 -3
- hanzo_mcp/tools/shell/run_command_windows.py +1 -3
- hanzo_mcp/tools/shell/run_tool.py +56 -0
- hanzo_mcp/tools/shell/session_manager.py +2 -6
- hanzo_mcp/tools/shell/session_storage.py +2 -6
- hanzo_mcp/tools/shell/streaming_command.py +7 -23
- hanzo_mcp/tools/shell/uvx.py +4 -14
- hanzo_mcp/tools/shell/uvx_background.py +2 -6
- hanzo_mcp/tools/shell/uvx_tool.py +1 -3
- hanzo_mcp/tools/shell/zsh_tool.py +12 -20
- hanzo_mcp/tools/todo/todo.py +1 -3
- hanzo_mcp/tools/vector/__init__.py +97 -50
- hanzo_mcp/tools/vector/ast_analyzer.py +6 -20
- hanzo_mcp/tools/vector/git_ingester.py +10 -30
- hanzo_mcp/tools/vector/index_tool.py +3 -9
- hanzo_mcp/tools/vector/infinity_store.py +11 -30
- hanzo_mcp/tools/vector/mock_infinity.py +159 -0
- hanzo_mcp/tools/vector/node_tool.py +538 -0
- hanzo_mcp/tools/vector/project_manager.py +4 -12
- hanzo_mcp/tools/vector/unified_vector.py +384 -0
- hanzo_mcp/tools/vector/vector.py +2 -6
- hanzo_mcp/tools/vector/vector_index.py +8 -8
- hanzo_mcp/tools/vector/vector_search.py +7 -21
- {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/METADATA +2 -2
- hanzo_mcp-0.9.0.dist-info/RECORD +191 -0
- hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +0 -645
- hanzo_mcp/tools/agent/swarm_tool.py +0 -723
- hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +0 -577
- hanzo_mcp/tools/filesystem/batch_search.py +0 -900
- hanzo_mcp/tools/filesystem/directory_tree_paginated.py +0 -350
- hanzo_mcp/tools/filesystem/find_files.py +0 -369
- hanzo_mcp/tools/filesystem/grep.py +0 -467
- hanzo_mcp/tools/filesystem/search_tool.py +0 -767
- hanzo_mcp/tools/filesystem/symbols_tool.py +0 -515
- hanzo_mcp/tools/filesystem/tree.py +0 -270
- hanzo_mcp/tools/jupyter/notebook_edit.py +0 -317
- hanzo_mcp/tools/jupyter/notebook_read.py +0 -147
- hanzo_mcp/tools/todo/todo_read.py +0 -143
- hanzo_mcp/tools/todo/todo_write.py +0 -374
- hanzo_mcp-0.8.8.dist-info/RECORD +0 -192
- {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/top_level.txt +0 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"""Unified find tool implementation.
|
|
2
2
|
|
|
3
|
-
This module provides the FindTool for finding
|
|
3
|
+
This module provides the FindTool for finding files by name or content using
|
|
4
4
|
multiple search backends in order of preference: rg > ag > ack > grep.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import os
|
|
7
8
|
import re
|
|
8
9
|
import json
|
|
9
10
|
import shutil
|
|
@@ -17,6 +18,7 @@ from typing import (
|
|
|
17
18
|
TypedDict,
|
|
18
19
|
final,
|
|
19
20
|
override,
|
|
21
|
+
Literal,
|
|
20
22
|
)
|
|
21
23
|
from pathlib import Path
|
|
22
24
|
|
|
@@ -25,11 +27,17 @@ from mcp.server.fastmcp import Context as MCPContext
|
|
|
25
27
|
|
|
26
28
|
from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
|
|
27
29
|
|
|
30
|
+
try:
|
|
31
|
+
import ffind
|
|
32
|
+
FFIND_AVAILABLE = True
|
|
33
|
+
except ImportError:
|
|
34
|
+
FFIND_AVAILABLE = False
|
|
35
|
+
|
|
28
36
|
# Parameter types
|
|
29
37
|
Pattern = Annotated[
|
|
30
38
|
str,
|
|
31
39
|
Field(
|
|
32
|
-
description="Pattern to search for (
|
|
40
|
+
description="Pattern to search for (file name pattern or content regex/literal)",
|
|
33
41
|
min_length=1,
|
|
34
42
|
),
|
|
35
43
|
]
|
|
@@ -42,10 +50,18 @@ SearchPath = Annotated[
|
|
|
42
50
|
),
|
|
43
51
|
]
|
|
44
52
|
|
|
53
|
+
Mode = Annotated[
|
|
54
|
+
Literal["name", "content", "both"],
|
|
55
|
+
Field(
|
|
56
|
+
description="Search mode: 'name' for file names, 'content' for file contents, 'both' for both",
|
|
57
|
+
default="name",
|
|
58
|
+
),
|
|
59
|
+
]
|
|
60
|
+
|
|
45
61
|
Include = Annotated[
|
|
46
62
|
Optional[str],
|
|
47
63
|
Field(
|
|
48
|
-
description='File pattern to include (e.g. "*.js")',
|
|
64
|
+
description='File pattern to include (e.g. "*.js", "*.{ts,tsx}")',
|
|
49
65
|
default=None,
|
|
50
66
|
),
|
|
51
67
|
]
|
|
@@ -62,30 +78,22 @@ CaseSensitive = Annotated[
|
|
|
62
78
|
bool,
|
|
63
79
|
Field(
|
|
64
80
|
description="Case sensitive search",
|
|
65
|
-
default=True,
|
|
66
|
-
),
|
|
67
|
-
]
|
|
68
|
-
|
|
69
|
-
FixedStrings = Annotated[
|
|
70
|
-
bool,
|
|
71
|
-
Field(
|
|
72
|
-
description="Treat pattern as literal string, not regex",
|
|
73
81
|
default=False,
|
|
74
82
|
),
|
|
75
83
|
]
|
|
76
84
|
|
|
77
|
-
|
|
78
|
-
|
|
85
|
+
Recursive = Annotated[
|
|
86
|
+
bool,
|
|
79
87
|
Field(
|
|
80
|
-
description="
|
|
81
|
-
default=
|
|
88
|
+
description="Search recursively in subdirectories",
|
|
89
|
+
default=True,
|
|
82
90
|
),
|
|
83
91
|
]
|
|
84
92
|
|
|
85
|
-
|
|
86
|
-
Optional[
|
|
93
|
+
MaxResults = Annotated[
|
|
94
|
+
Optional[int],
|
|
87
95
|
Field(
|
|
88
|
-
description="
|
|
96
|
+
description="Maximum number of results to return",
|
|
89
97
|
default=None,
|
|
90
98
|
),
|
|
91
99
|
]
|
|
@@ -96,17 +104,17 @@ class FindParams(TypedDict, total=False):
|
|
|
96
104
|
|
|
97
105
|
pattern: str
|
|
98
106
|
path: str
|
|
107
|
+
mode: Literal["name", "content", "both"]
|
|
99
108
|
include: Optional[str]
|
|
100
109
|
exclude: Optional[str]
|
|
101
110
|
case_sensitive: bool
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
backend: Optional[str]
|
|
111
|
+
recursive: bool
|
|
112
|
+
max_results: Optional[int]
|
|
105
113
|
|
|
106
114
|
|
|
107
115
|
@final
|
|
108
116
|
class FindTool(FilesystemBaseTool):
|
|
109
|
-
"""Unified find tool
|
|
117
|
+
"""Unified find tool for searching by file name or content."""
|
|
110
118
|
|
|
111
119
|
def __init__(self, permission_manager):
|
|
112
120
|
"""Initialize the find tool."""
|
|
@@ -125,17 +133,24 @@ class FindTool(FilesystemBaseTool):
|
|
|
125
133
|
def description(self) -> str:
|
|
126
134
|
"""Get the tool description."""
|
|
127
135
|
backends = self._get_available_backends()
|
|
128
|
-
backend_str = ", ".join(backends) if backends else "fallback
|
|
136
|
+
backend_str = ", ".join(backends) if backends else "fallback search"
|
|
137
|
+
|
|
138
|
+
return f"""Find files by name, content, or both. Available backends: {backend_str}.
|
|
139
|
+
|
|
140
|
+
Examples:
|
|
141
|
+
# Find by file name (default mode)
|
|
142
|
+
find "*.py"
|
|
143
|
+
find "test_*" ./src
|
|
144
|
+
find "README.*" --case-sensitive
|
|
129
145
|
|
|
130
|
-
|
|
146
|
+
# Find by content
|
|
147
|
+
find "TODO" --mode content
|
|
148
|
+
find "error.*fatal" ./src --mode content
|
|
131
149
|
|
|
132
|
-
|
|
133
|
-
find "
|
|
134
|
-
find "error.*fatal" ./src
|
|
135
|
-
find "config" --include "*.json"
|
|
136
|
-
find "password" --exclude "*.log"
|
|
150
|
+
# Find both name and content
|
|
151
|
+
find "config" --mode both --include "*.json"
|
|
137
152
|
|
|
138
|
-
|
|
153
|
+
Supports wildcards for names, regex for content."""
|
|
139
154
|
|
|
140
155
|
def _get_available_backends(self) -> List[str]:
|
|
141
156
|
"""Get list of available search backends."""
|
|
@@ -161,12 +176,15 @@ Fast, intuitive file content search."""
|
|
|
161
176
|
return "Error: pattern is required"
|
|
162
177
|
|
|
163
178
|
path = params.get("path", ".")
|
|
179
|
+
mode = params.get("mode", "name")
|
|
164
180
|
include = params.get("include")
|
|
165
181
|
exclude = params.get("exclude")
|
|
166
|
-
case_sensitive = params.get("case_sensitive",
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
182
|
+
case_sensitive = params.get("case_sensitive", False)
|
|
183
|
+
recursive = params.get("recursive", True)
|
|
184
|
+
max_results = params.get("max_results")
|
|
185
|
+
|
|
186
|
+
# Expand path (handles ~, $HOME, etc.)
|
|
187
|
+
path = self.expand_path(path)
|
|
170
188
|
|
|
171
189
|
# Validate path
|
|
172
190
|
path_validation = self.validate_path(path)
|
|
@@ -184,95 +202,154 @@ Fast, intuitive file content search."""
|
|
|
184
202
|
if not exists:
|
|
185
203
|
return error_msg
|
|
186
204
|
|
|
187
|
-
|
|
188
|
-
available = self._get_available_backends()
|
|
205
|
+
await tool_ctx.info(f"Searching for '{pattern}' in {path} (mode: {mode})")
|
|
189
206
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
elif
|
|
196
|
-
|
|
197
|
-
|
|
207
|
+
# Route to appropriate search method
|
|
208
|
+
if mode == "name":
|
|
209
|
+
return await self._find_by_name(
|
|
210
|
+
pattern, path, include, exclude, case_sensitive, recursive, max_results, tool_ctx
|
|
211
|
+
)
|
|
212
|
+
elif mode == "content":
|
|
213
|
+
return await self._find_by_content(
|
|
214
|
+
pattern, path, include, exclude, case_sensitive, max_results, tool_ctx
|
|
215
|
+
)
|
|
216
|
+
elif mode == "both":
|
|
217
|
+
return await self._find_both(
|
|
218
|
+
pattern, path, include, exclude, case_sensitive, recursive, max_results, tool_ctx
|
|
219
|
+
)
|
|
198
220
|
else:
|
|
199
|
-
|
|
200
|
-
selected_backend = "grep"
|
|
221
|
+
return f"Error: Invalid mode '{mode}'. Use 'name', 'content', or 'both'."
|
|
201
222
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
223
|
+
async def _find_by_name(
|
|
224
|
+
self, pattern, path, include, exclude, case_sensitive, recursive, max_results, tool_ctx
|
|
225
|
+
) -> str:
|
|
226
|
+
"""Find files by name pattern."""
|
|
227
|
+
search_path = path or os.getcwd()
|
|
228
|
+
|
|
229
|
+
# If ffind is not available, fall back to basic implementation
|
|
230
|
+
if not FFIND_AVAILABLE:
|
|
231
|
+
return await self._find_files_fallback(
|
|
232
|
+
pattern, search_path, recursive, not case_sensitive, False, False, True, max_results or 100
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
# Use ffind for efficient searching
|
|
237
|
+
results = []
|
|
238
|
+
count = 0
|
|
239
|
+
|
|
240
|
+
# Configure ffind options
|
|
241
|
+
options = {
|
|
242
|
+
"pattern": pattern,
|
|
243
|
+
"path": search_path,
|
|
244
|
+
"recursive": recursive,
|
|
245
|
+
"ignore_case": not case_sensitive,
|
|
246
|
+
"hidden": False,
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
# Search with ffind
|
|
250
|
+
for filepath in ffind.find(**options):
|
|
251
|
+
# Check if it matches our include/exclude criteria
|
|
252
|
+
filename = os.path.basename(filepath)
|
|
253
|
+
if not self._match_file_pattern(filename, include, exclude):
|
|
254
|
+
continue
|
|
255
|
+
|
|
256
|
+
# Make path relative for cleaner output
|
|
257
|
+
try:
|
|
258
|
+
rel_path = os.path.relpath(filepath, search_path)
|
|
259
|
+
except ValueError:
|
|
260
|
+
rel_path = filepath
|
|
261
|
+
|
|
262
|
+
results.append(rel_path)
|
|
263
|
+
count += 1
|
|
264
|
+
|
|
265
|
+
if max_results and count >= max_results:
|
|
266
|
+
break
|
|
267
|
+
|
|
268
|
+
if not results:
|
|
269
|
+
return f"No files found matching '{pattern}'"
|
|
270
|
+
|
|
271
|
+
# Format output
|
|
272
|
+
output = [f"Found {len(results)} file(s) matching '{pattern}':"]
|
|
273
|
+
output.append("")
|
|
274
|
+
|
|
275
|
+
for filepath in sorted(results):
|
|
276
|
+
output.append(filepath)
|
|
277
|
+
|
|
278
|
+
if max_results and count >= max_results:
|
|
279
|
+
output.append(f"\n... (showing first {max_results} results)")
|
|
280
|
+
|
|
281
|
+
return "\n".join(output)
|
|
282
|
+
|
|
283
|
+
except Exception as e:
|
|
284
|
+
await tool_ctx.error(f"Error during name search: {str(e)}")
|
|
285
|
+
# Fall back to basic implementation
|
|
286
|
+
return await self._find_files_fallback(
|
|
287
|
+
pattern, search_path, recursive, not case_sensitive, False, False, True, max_results or 100
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
async def _find_by_content(
|
|
291
|
+
self, pattern, path, include, exclude, case_sensitive, max_results, tool_ctx
|
|
292
|
+
) -> str:
|
|
293
|
+
"""Find files by content pattern."""
|
|
294
|
+
# Select backend for content search
|
|
295
|
+
available = self._get_available_backends()
|
|
296
|
+
selected_backend = available[0] if available else "grep"
|
|
297
|
+
|
|
298
|
+
await tool_ctx.info(f"Using {selected_backend} for content search")
|
|
205
299
|
|
|
206
|
-
# Execute search
|
|
300
|
+
# Execute content search
|
|
207
301
|
if selected_backend == "rg":
|
|
208
|
-
return await self.
|
|
209
|
-
pattern,
|
|
210
|
-
path,
|
|
211
|
-
include,
|
|
212
|
-
exclude,
|
|
213
|
-
case_sensitive,
|
|
214
|
-
fixed_strings,
|
|
215
|
-
show_context,
|
|
216
|
-
tool_ctx,
|
|
302
|
+
return await self._run_ripgrep_content(
|
|
303
|
+
pattern, path, include, exclude, case_sensitive, max_results, tool_ctx
|
|
217
304
|
)
|
|
218
305
|
elif selected_backend == "ag":
|
|
219
|
-
return await self.
|
|
220
|
-
pattern,
|
|
221
|
-
path,
|
|
222
|
-
include,
|
|
223
|
-
exclude,
|
|
224
|
-
case_sensitive,
|
|
225
|
-
fixed_strings,
|
|
226
|
-
show_context,
|
|
227
|
-
tool_ctx,
|
|
306
|
+
return await self._run_silver_searcher_content(
|
|
307
|
+
pattern, path, include, exclude, case_sensitive, max_results, tool_ctx
|
|
228
308
|
)
|
|
229
309
|
elif selected_backend == "ack":
|
|
230
|
-
return await self.
|
|
231
|
-
pattern,
|
|
232
|
-
path,
|
|
233
|
-
include,
|
|
234
|
-
exclude,
|
|
235
|
-
case_sensitive,
|
|
236
|
-
fixed_strings,
|
|
237
|
-
show_context,
|
|
238
|
-
tool_ctx,
|
|
310
|
+
return await self._run_ack_content(
|
|
311
|
+
pattern, path, include, exclude, case_sensitive, max_results, tool_ctx
|
|
239
312
|
)
|
|
240
313
|
else:
|
|
241
|
-
return await self.
|
|
242
|
-
pattern,
|
|
243
|
-
path,
|
|
244
|
-
include,
|
|
245
|
-
exclude,
|
|
246
|
-
case_sensitive,
|
|
247
|
-
fixed_strings,
|
|
248
|
-
show_context,
|
|
249
|
-
tool_ctx,
|
|
314
|
+
return await self._run_fallback_grep_content(
|
|
315
|
+
pattern, path, include, exclude, case_sensitive, max_results, tool_ctx
|
|
250
316
|
)
|
|
251
317
|
|
|
252
|
-
async def
|
|
253
|
-
self,
|
|
254
|
-
pattern,
|
|
255
|
-
path,
|
|
256
|
-
include,
|
|
257
|
-
exclude,
|
|
258
|
-
case_sensitive,
|
|
259
|
-
fixed_strings,
|
|
260
|
-
show_context,
|
|
261
|
-
tool_ctx,
|
|
318
|
+
async def _find_both(
|
|
319
|
+
self, pattern, path, include, exclude, case_sensitive, recursive, max_results, tool_ctx
|
|
262
320
|
) -> str:
|
|
263
|
-
"""
|
|
321
|
+
"""Find files by both name and content."""
|
|
322
|
+
# Run both searches
|
|
323
|
+
name_results = await self._find_by_name(
|
|
324
|
+
pattern, path, include, exclude, case_sensitive, recursive, max_results, tool_ctx
|
|
325
|
+
)
|
|
326
|
+
content_results = await self._find_by_content(
|
|
327
|
+
pattern, path, include, exclude, case_sensitive, max_results, tool_ctx
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
# Combine results
|
|
331
|
+
output = ["=== NAME MATCHES ==="]
|
|
332
|
+
output.append(name_results)
|
|
333
|
+
output.append("")
|
|
334
|
+
output.append("=== CONTENT MATCHES ===")
|
|
335
|
+
output.append(content_results)
|
|
336
|
+
|
|
337
|
+
return "\n".join(output)
|
|
338
|
+
|
|
339
|
+
async def _run_ripgrep_content(
|
|
340
|
+
self, pattern, path, include, exclude, case_sensitive, max_results, tool_ctx
|
|
341
|
+
) -> str:
|
|
342
|
+
"""Run ripgrep backend for content search."""
|
|
264
343
|
cmd = ["rg", "--json"]
|
|
265
344
|
|
|
266
345
|
if not case_sensitive:
|
|
267
346
|
cmd.append("-i")
|
|
268
|
-
if fixed_strings:
|
|
269
|
-
cmd.append("-F")
|
|
270
|
-
if show_context > 0:
|
|
271
|
-
cmd.extend(["-C", str(show_context)])
|
|
272
347
|
if include:
|
|
273
348
|
cmd.extend(["-g", include])
|
|
274
349
|
if exclude:
|
|
275
350
|
cmd.extend(["-g", f"!{exclude}"])
|
|
351
|
+
if max_results:
|
|
352
|
+
cmd.extend(["-m", str(max_results)])
|
|
276
353
|
|
|
277
354
|
cmd.extend([pattern, path])
|
|
278
355
|
|
|
@@ -293,30 +370,20 @@ Fast, intuitive file content search."""
|
|
|
293
370
|
await tool_ctx.error(f"Error running ripgrep: {str(e)}")
|
|
294
371
|
return f"Error running ripgrep: {str(e)}"
|
|
295
372
|
|
|
296
|
-
async def
|
|
297
|
-
self,
|
|
298
|
-
pattern,
|
|
299
|
-
path,
|
|
300
|
-
include,
|
|
301
|
-
exclude,
|
|
302
|
-
case_sensitive,
|
|
303
|
-
fixed_strings,
|
|
304
|
-
show_context,
|
|
305
|
-
tool_ctx,
|
|
373
|
+
async def _run_silver_searcher_content(
|
|
374
|
+
self, pattern, path, include, exclude, case_sensitive, max_results, tool_ctx
|
|
306
375
|
) -> str:
|
|
307
|
-
"""Run silver searcher (ag) backend."""
|
|
376
|
+
"""Run silver searcher (ag) backend for content search."""
|
|
308
377
|
cmd = ["ag", "--nocolor", "--nogroup"]
|
|
309
378
|
|
|
310
379
|
if not case_sensitive:
|
|
311
380
|
cmd.append("-i")
|
|
312
|
-
if fixed_strings:
|
|
313
|
-
cmd.append("-F")
|
|
314
|
-
if show_context > 0:
|
|
315
|
-
cmd.extend(["-C", str(show_context)])
|
|
316
381
|
if include:
|
|
317
382
|
cmd.extend(["-G", include])
|
|
318
383
|
if exclude:
|
|
319
384
|
cmd.extend(["--ignore", exclude])
|
|
385
|
+
if max_results:
|
|
386
|
+
cmd.extend(["-m", str(max_results)])
|
|
320
387
|
|
|
321
388
|
cmd.extend([pattern, path])
|
|
322
389
|
|
|
@@ -342,35 +409,23 @@ Fast, intuitive file content search."""
|
|
|
342
409
|
await tool_ctx.error(f"Error running ag: {str(e)}")
|
|
343
410
|
return f"Error running ag: {str(e)}"
|
|
344
411
|
|
|
345
|
-
async def
|
|
346
|
-
self,
|
|
347
|
-
pattern,
|
|
348
|
-
path,
|
|
349
|
-
include,
|
|
350
|
-
exclude,
|
|
351
|
-
case_sensitive,
|
|
352
|
-
fixed_strings,
|
|
353
|
-
show_context,
|
|
354
|
-
tool_ctx,
|
|
412
|
+
async def _run_ack_content(
|
|
413
|
+
self, pattern, path, include, exclude, case_sensitive, max_results, tool_ctx
|
|
355
414
|
) -> str:
|
|
356
|
-
"""Run ack backend."""
|
|
415
|
+
"""Run ack backend for content search."""
|
|
357
416
|
cmd = ["ack", "--nocolor", "--nogroup"]
|
|
358
417
|
|
|
359
418
|
if not case_sensitive:
|
|
360
419
|
cmd.append("-i")
|
|
361
|
-
if fixed_strings:
|
|
362
|
-
cmd.append("-Q")
|
|
363
|
-
if show_context > 0:
|
|
364
|
-
cmd.extend(["-C", str(show_context)])
|
|
365
420
|
if include:
|
|
366
421
|
# ack uses different syntax for file patterns
|
|
367
|
-
cmd.extend(
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
)
|
|
422
|
+
cmd.extend([
|
|
423
|
+
"--type-add",
|
|
424
|
+
f"custom:ext:{include.replace('*.', '')}",
|
|
425
|
+
"--type=custom",
|
|
426
|
+
])
|
|
427
|
+
if max_results:
|
|
428
|
+
cmd.extend(["-m", str(max_results)])
|
|
374
429
|
|
|
375
430
|
cmd.extend([pattern, path])
|
|
376
431
|
|
|
@@ -396,18 +451,10 @@ Fast, intuitive file content search."""
|
|
|
396
451
|
await tool_ctx.error(f"Error running ack: {str(e)}")
|
|
397
452
|
return f"Error running ack: {str(e)}"
|
|
398
453
|
|
|
399
|
-
async def
|
|
400
|
-
self,
|
|
401
|
-
pattern,
|
|
402
|
-
path,
|
|
403
|
-
include,
|
|
404
|
-
exclude,
|
|
405
|
-
case_sensitive,
|
|
406
|
-
fixed_strings,
|
|
407
|
-
show_context,
|
|
408
|
-
tool_ctx,
|
|
454
|
+
async def _run_fallback_grep_content(
|
|
455
|
+
self, pattern, path, include, exclude, case_sensitive, max_results, tool_ctx
|
|
409
456
|
) -> str:
|
|
410
|
-
"""Fallback Python implementation."""
|
|
457
|
+
"""Fallback Python implementation for content search."""
|
|
411
458
|
await tool_ctx.info("Using fallback Python grep implementation")
|
|
412
459
|
|
|
413
460
|
try:
|
|
@@ -428,17 +475,11 @@ Fast, intuitive file content search."""
|
|
|
428
475
|
return "No matching files found."
|
|
429
476
|
|
|
430
477
|
# Compile pattern
|
|
431
|
-
if
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
if not case_sensitive:
|
|
437
|
-
flags = re.IGNORECASE
|
|
438
|
-
else:
|
|
439
|
-
flags = 0
|
|
440
|
-
|
|
441
|
-
regex = re.compile(pattern_re, flags)
|
|
478
|
+
flags = 0 if case_sensitive else re.IGNORECASE
|
|
479
|
+
try:
|
|
480
|
+
regex = re.compile(pattern, flags)
|
|
481
|
+
except re.error as e:
|
|
482
|
+
return f"Error: Invalid regex pattern: {e}"
|
|
442
483
|
|
|
443
484
|
# Search files
|
|
444
485
|
results = []
|
|
@@ -451,28 +492,20 @@ Fast, intuitive file content search."""
|
|
|
451
492
|
|
|
452
493
|
for i, line in enumerate(lines, 1):
|
|
453
494
|
if regex.search(line):
|
|
454
|
-
|
|
455
|
-
if show_context > 0:
|
|
456
|
-
start = max(0, i - show_context - 1)
|
|
457
|
-
end = min(len(lines), i + show_context)
|
|
458
|
-
|
|
459
|
-
context_lines = []
|
|
460
|
-
for j in range(start, end):
|
|
461
|
-
prefix = ":" if j + 1 == i else "-"
|
|
462
|
-
context_lines.append(
|
|
463
|
-
f"{file_path}:{j + 1}{prefix}{lines[j].rstrip()}"
|
|
464
|
-
)
|
|
465
|
-
results.extend(context_lines)
|
|
466
|
-
results.append("") # Separator
|
|
467
|
-
else:
|
|
468
|
-
results.append(f"{file_path}:{i}:{line.rstrip()}")
|
|
495
|
+
results.append(f"{file_path}:{i}:{line.rstrip()}")
|
|
469
496
|
total_matches += 1
|
|
497
|
+
|
|
498
|
+
if max_results and total_matches >= max_results:
|
|
499
|
+
break
|
|
470
500
|
|
|
471
501
|
except UnicodeDecodeError:
|
|
472
502
|
pass # Skip binary files
|
|
473
503
|
except Exception as e:
|
|
474
504
|
await tool_ctx.warning(f"Error reading {file_path}: {str(e)}")
|
|
475
505
|
|
|
506
|
+
if max_results and total_matches >= max_results:
|
|
507
|
+
break
|
|
508
|
+
|
|
476
509
|
if not results:
|
|
477
510
|
return "No matches found."
|
|
478
511
|
|
|
@@ -482,9 +515,109 @@ Fast, intuitive file content search."""
|
|
|
482
515
|
await tool_ctx.error(f"Error in fallback grep: {str(e)}")
|
|
483
516
|
return f"Error in fallback grep: {str(e)}"
|
|
484
517
|
|
|
485
|
-
def
|
|
486
|
-
self,
|
|
487
|
-
|
|
518
|
+
async def _find_files_fallback(
|
|
519
|
+
self,
|
|
520
|
+
pattern: str,
|
|
521
|
+
search_path: str,
|
|
522
|
+
recursive: bool,
|
|
523
|
+
ignore_case: bool,
|
|
524
|
+
hidden: bool,
|
|
525
|
+
dirs_only: bool,
|
|
526
|
+
files_only: bool,
|
|
527
|
+
max_results: int,
|
|
528
|
+
) -> str:
|
|
529
|
+
"""Fallback implementation for file name search when ffind is not available."""
|
|
530
|
+
results = []
|
|
531
|
+
count = 0
|
|
532
|
+
|
|
533
|
+
# Convert pattern for case-insensitive matching
|
|
534
|
+
if ignore_case:
|
|
535
|
+
pattern = pattern.lower()
|
|
536
|
+
|
|
537
|
+
try:
|
|
538
|
+
if recursive:
|
|
539
|
+
# Walk directory tree
|
|
540
|
+
for root, dirs, files in os.walk(search_path):
|
|
541
|
+
# Skip hidden directories if not requested
|
|
542
|
+
if not hidden:
|
|
543
|
+
dirs[:] = [d for d in dirs if not d.startswith(".")]
|
|
544
|
+
|
|
545
|
+
# Check directories
|
|
546
|
+
if not files_only:
|
|
547
|
+
for dirname in dirs:
|
|
548
|
+
if self._match_pattern(dirname, pattern, ignore_case):
|
|
549
|
+
filepath = os.path.join(root, dirname)
|
|
550
|
+
rel_path = os.path.relpath(filepath, search_path)
|
|
551
|
+
results.append(rel_path + "/")
|
|
552
|
+
count += 1
|
|
553
|
+
if count >= max_results:
|
|
554
|
+
break
|
|
555
|
+
|
|
556
|
+
# Check files
|
|
557
|
+
if not dirs_only:
|
|
558
|
+
for filename in files:
|
|
559
|
+
if not hidden and filename.startswith("."):
|
|
560
|
+
continue
|
|
561
|
+
|
|
562
|
+
if self._match_pattern(filename, pattern, ignore_case):
|
|
563
|
+
filepath = os.path.join(root, filename)
|
|
564
|
+
rel_path = os.path.relpath(filepath, search_path)
|
|
565
|
+
results.append(rel_path)
|
|
566
|
+
count += 1
|
|
567
|
+
if count >= max_results:
|
|
568
|
+
break
|
|
569
|
+
|
|
570
|
+
if count >= max_results:
|
|
571
|
+
break
|
|
572
|
+
else:
|
|
573
|
+
# Only search in the specified directory
|
|
574
|
+
for entry in os.listdir(search_path):
|
|
575
|
+
if not hidden and entry.startswith("."):
|
|
576
|
+
continue
|
|
577
|
+
|
|
578
|
+
filepath = os.path.join(search_path, entry)
|
|
579
|
+
is_dir = os.path.isdir(filepath)
|
|
580
|
+
|
|
581
|
+
if dirs_only and not is_dir:
|
|
582
|
+
continue
|
|
583
|
+
if files_only and is_dir:
|
|
584
|
+
continue
|
|
585
|
+
|
|
586
|
+
if self._match_pattern(entry, pattern, ignore_case):
|
|
587
|
+
results.append(entry + "/" if is_dir else entry)
|
|
588
|
+
count += 1
|
|
589
|
+
if count >= max_results:
|
|
590
|
+
break
|
|
591
|
+
|
|
592
|
+
if not results:
|
|
593
|
+
return f"No files found matching '{pattern}' (using fallback search)"
|
|
594
|
+
|
|
595
|
+
# Format output
|
|
596
|
+
output = [f"Found {len(results)} file(s) matching '{pattern}' (using fallback search):"]
|
|
597
|
+
output.append("")
|
|
598
|
+
|
|
599
|
+
for filepath in sorted(results):
|
|
600
|
+
output.append(filepath)
|
|
601
|
+
|
|
602
|
+
if count >= max_results:
|
|
603
|
+
output.append(f"\n... (showing first {max_results} results)")
|
|
604
|
+
|
|
605
|
+
if not FFIND_AVAILABLE:
|
|
606
|
+
output.append("\nNote: Install 'ffind' for faster searching: pip install ffind")
|
|
607
|
+
|
|
608
|
+
return "\n".join(output)
|
|
609
|
+
|
|
610
|
+
except Exception as e:
|
|
611
|
+
return f"Error searching for files: {str(e)}"
|
|
612
|
+
|
|
613
|
+
def _match_pattern(self, filename: str, pattern: str, ignore_case: bool) -> bool:
|
|
614
|
+
"""Check if filename matches pattern."""
|
|
615
|
+
if ignore_case:
|
|
616
|
+
return fnmatch.fnmatch(filename.lower(), pattern)
|
|
617
|
+
else:
|
|
618
|
+
return fnmatch.fnmatch(filename, pattern)
|
|
619
|
+
|
|
620
|
+
def _match_file_pattern(self, filename: str, include: Optional[str], exclude: Optional[str]) -> bool:
|
|
488
621
|
"""Check if filename matches include/exclude patterns."""
|
|
489
622
|
if include and not fnmatch.fnmatch(filename, include):
|
|
490
623
|
return False
|
|
@@ -534,4 +667,4 @@ Fast, intuitive file content search."""
|
|
|
534
667
|
|
|
535
668
|
def register(self, mcp_server) -> None:
|
|
536
669
|
"""Register this tool with the MCP server."""
|
|
537
|
-
pass
|
|
670
|
+
pass
|