hanzo-mcp 0.9.0__py3-none-any.whl → 0.9.2__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 -1
- hanzo_mcp/analytics/posthog_analytics.py +14 -1
- hanzo_mcp/cli.py +108 -4
- hanzo_mcp/server.py +11 -0
- hanzo_mcp/tools/__init__.py +3 -16
- hanzo_mcp/tools/agent/__init__.py +5 -0
- hanzo_mcp/tools/agent/agent.py +5 -0
- hanzo_mcp/tools/agent/agent_tool.py +3 -17
- hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +623 -0
- hanzo_mcp/tools/agent/clarification_tool.py +7 -1
- hanzo_mcp/tools/agent/claude_desktop_auth.py +16 -6
- hanzo_mcp/tools/agent/cli_agent_base.py +5 -0
- hanzo_mcp/tools/agent/cli_tools.py +26 -0
- hanzo_mcp/tools/agent/code_auth_tool.py +5 -0
- hanzo_mcp/tools/agent/critic_tool.py +7 -1
- hanzo_mcp/tools/agent/iching_tool.py +5 -0
- hanzo_mcp/tools/agent/network_tool.py +5 -0
- hanzo_mcp/tools/agent/review_tool.py +7 -1
- hanzo_mcp/tools/agent/swarm_alias.py +5 -0
- hanzo_mcp/tools/agent/swarm_tool.py +701 -0
- hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +554 -0
- hanzo_mcp/tools/agent/unified_cli_tools.py +5 -0
- hanzo_mcp/tools/common/auto_timeout.py +254 -0
- hanzo_mcp/tools/common/base.py +4 -0
- hanzo_mcp/tools/common/batch_tool.py +5 -0
- hanzo_mcp/tools/common/config_tool.py +5 -0
- hanzo_mcp/tools/common/critic_tool.py +5 -0
- hanzo_mcp/tools/common/paginated_base.py +4 -0
- hanzo_mcp/tools/common/permissions.py +38 -12
- hanzo_mcp/tools/common/personality.py +673 -980
- hanzo_mcp/tools/common/stats.py +5 -0
- hanzo_mcp/tools/common/thinking_tool.py +5 -0
- hanzo_mcp/tools/common/timeout_parser.py +103 -0
- hanzo_mcp/tools/common/tool_disable.py +5 -0
- hanzo_mcp/tools/common/tool_enable.py +5 -0
- hanzo_mcp/tools/common/tool_list.py +5 -0
- hanzo_mcp/tools/config/config_tool.py +5 -0
- hanzo_mcp/tools/config/mode_tool.py +5 -0
- hanzo_mcp/tools/database/graph.py +5 -0
- hanzo_mcp/tools/database/graph_add.py +5 -0
- hanzo_mcp/tools/database/graph_query.py +5 -0
- hanzo_mcp/tools/database/graph_remove.py +5 -0
- hanzo_mcp/tools/database/graph_search.py +5 -0
- hanzo_mcp/tools/database/graph_stats.py +5 -0
- hanzo_mcp/tools/database/sql.py +5 -0
- hanzo_mcp/tools/database/sql_query.py +2 -0
- hanzo_mcp/tools/database/sql_search.py +5 -0
- hanzo_mcp/tools/database/sql_stats.py +5 -0
- hanzo_mcp/tools/editor/neovim_command.py +5 -0
- hanzo_mcp/tools/editor/neovim_edit.py +7 -2
- hanzo_mcp/tools/editor/neovim_session.py +5 -0
- hanzo_mcp/tools/filesystem/__init__.py +23 -26
- hanzo_mcp/tools/filesystem/ast_tool.py +3 -4
- hanzo_mcp/tools/filesystem/base.py +2 -18
- hanzo_mcp/tools/filesystem/batch_search.py +825 -0
- hanzo_mcp/tools/filesystem/content_replace.py +5 -3
- hanzo_mcp/tools/filesystem/diff.py +5 -0
- hanzo_mcp/tools/filesystem/directory_tree.py +34 -281
- hanzo_mcp/tools/filesystem/directory_tree_paginated.py +345 -0
- hanzo_mcp/tools/filesystem/edit.py +6 -5
- hanzo_mcp/tools/filesystem/find.py +177 -311
- hanzo_mcp/tools/filesystem/find_files.py +370 -0
- hanzo_mcp/tools/filesystem/git_search.py +5 -3
- hanzo_mcp/tools/filesystem/grep.py +454 -0
- hanzo_mcp/tools/filesystem/multi_edit.py +6 -5
- hanzo_mcp/tools/filesystem/read.py +10 -9
- hanzo_mcp/tools/filesystem/rules_tool.py +6 -4
- hanzo_mcp/tools/filesystem/search_tool.py +728 -0
- hanzo_mcp/tools/filesystem/symbols_tool.py +510 -0
- hanzo_mcp/tools/filesystem/tree.py +273 -0
- hanzo_mcp/tools/filesystem/watch.py +6 -1
- hanzo_mcp/tools/filesystem/write.py +13 -7
- hanzo_mcp/tools/jupyter/jupyter.py +30 -2
- hanzo_mcp/tools/jupyter/notebook_edit.py +298 -0
- hanzo_mcp/tools/jupyter/notebook_read.py +148 -0
- hanzo_mcp/tools/llm/consensus_tool.py +8 -6
- hanzo_mcp/tools/llm/llm_manage.py +5 -0
- hanzo_mcp/tools/llm/llm_tool.py +2 -0
- hanzo_mcp/tools/llm/llm_unified.py +5 -0
- hanzo_mcp/tools/llm/provider_tools.py +5 -0
- hanzo_mcp/tools/lsp/lsp_tool.py +475 -622
- hanzo_mcp/tools/mcp/mcp_add.py +7 -2
- hanzo_mcp/tools/mcp/mcp_remove.py +15 -2
- hanzo_mcp/tools/mcp/mcp_stats.py +5 -0
- hanzo_mcp/tools/mcp/mcp_tool.py +5 -0
- hanzo_mcp/tools/memory/knowledge_tools.py +14 -0
- hanzo_mcp/tools/memory/memory_tools.py +17 -0
- hanzo_mcp/tools/search/find_tool.py +5 -3
- hanzo_mcp/tools/search/unified_search.py +3 -1
- hanzo_mcp/tools/shell/__init__.py +2 -14
- hanzo_mcp/tools/shell/base_process.py +4 -2
- hanzo_mcp/tools/shell/bash_tool.py +2 -0
- hanzo_mcp/tools/shell/command_executor.py +7 -7
- hanzo_mcp/tools/shell/logs.py +5 -0
- hanzo_mcp/tools/shell/npx.py +5 -0
- hanzo_mcp/tools/shell/npx_background.py +5 -0
- hanzo_mcp/tools/shell/npx_tool.py +5 -0
- hanzo_mcp/tools/shell/open.py +5 -0
- hanzo_mcp/tools/shell/pkill.py +5 -0
- hanzo_mcp/tools/shell/process_tool.py +5 -0
- hanzo_mcp/tools/shell/processes.py +5 -0
- hanzo_mcp/tools/shell/run_background.py +5 -0
- hanzo_mcp/tools/shell/run_command.py +2 -0
- hanzo_mcp/tools/shell/run_command_windows.py +5 -0
- hanzo_mcp/tools/shell/streaming_command.py +5 -0
- hanzo_mcp/tools/shell/uvx.py +5 -0
- hanzo_mcp/tools/shell/uvx_background.py +5 -0
- hanzo_mcp/tools/shell/uvx_tool.py +5 -0
- hanzo_mcp/tools/shell/zsh_tool.py +3 -0
- hanzo_mcp/tools/todo/todo.py +5 -0
- hanzo_mcp/tools/todo/todo_read.py +142 -0
- hanzo_mcp/tools/todo/todo_write.py +367 -0
- hanzo_mcp/tools/vector/__init__.py +42 -95
- hanzo_mcp/tools/vector/index_tool.py +5 -0
- hanzo_mcp/tools/vector/vector.py +5 -0
- hanzo_mcp/tools/vector/vector_index.py +5 -0
- hanzo_mcp/tools/vector/vector_search.py +5 -0
- {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.2.dist-info}/METADATA +1 -1
- hanzo_mcp-0.9.2.dist-info/RECORD +195 -0
- hanzo_mcp/tools/common/path_utils.py +0 -34
- hanzo_mcp/tools/compiler/__init__.py +0 -8
- hanzo_mcp/tools/compiler/sandboxed_compiler.py +0 -681
- hanzo_mcp/tools/environment/__init__.py +0 -8
- hanzo_mcp/tools/environment/environment_detector.py +0 -594
- hanzo_mcp/tools/filesystem/search.py +0 -1160
- hanzo_mcp/tools/framework/__init__.py +0 -8
- hanzo_mcp/tools/framework/framework_modes.py +0 -714
- hanzo_mcp/tools/memory/conversation_memory.py +0 -636
- hanzo_mcp/tools/shell/run_tool.py +0 -56
- hanzo_mcp/tools/vector/node_tool.py +0 -538
- hanzo_mcp/tools/vector/unified_vector.py +0 -384
- hanzo_mcp-0.9.0.dist-info/RECORD +0 -191
- {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.2.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.2.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.2.dist-info}/top_level.txt +0 -0
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
"""Unified find tool implementation.
|
|
2
2
|
|
|
3
|
-
This module provides the FindTool for finding
|
|
3
|
+
This module provides the FindTool for finding text patterns in files using
|
|
4
4
|
multiple search backends in order of preference: rg > ag > ack > grep.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
import os
|
|
8
7
|
import re
|
|
9
8
|
import json
|
|
10
9
|
import shutil
|
|
@@ -18,26 +17,21 @@ from typing import (
|
|
|
18
17
|
TypedDict,
|
|
19
18
|
final,
|
|
20
19
|
override,
|
|
21
|
-
Literal,
|
|
22
20
|
)
|
|
23
21
|
from pathlib import Path
|
|
24
22
|
|
|
25
23
|
from pydantic import Field
|
|
26
24
|
from mcp.server.fastmcp import Context as MCPContext
|
|
27
25
|
|
|
28
|
-
from hanzo_mcp.tools.
|
|
26
|
+
from hanzo_mcp.tools.common.auto_timeout import auto_timeout
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
import ffind
|
|
32
|
-
FFIND_AVAILABLE = True
|
|
33
|
-
except ImportError:
|
|
34
|
-
FFIND_AVAILABLE = False
|
|
28
|
+
from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
|
|
35
29
|
|
|
36
30
|
# Parameter types
|
|
37
31
|
Pattern = Annotated[
|
|
38
32
|
str,
|
|
39
33
|
Field(
|
|
40
|
-
description="Pattern to search for (
|
|
34
|
+
description="Pattern to search for (regex or literal)",
|
|
41
35
|
min_length=1,
|
|
42
36
|
),
|
|
43
37
|
]
|
|
@@ -50,18 +44,10 @@ SearchPath = Annotated[
|
|
|
50
44
|
),
|
|
51
45
|
]
|
|
52
46
|
|
|
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
|
-
|
|
61
47
|
Include = Annotated[
|
|
62
48
|
Optional[str],
|
|
63
49
|
Field(
|
|
64
|
-
description='File pattern to include (e.g. "*.js"
|
|
50
|
+
description='File pattern to include (e.g. "*.js")',
|
|
65
51
|
default=None,
|
|
66
52
|
),
|
|
67
53
|
]
|
|
@@ -78,22 +64,30 @@ CaseSensitive = Annotated[
|
|
|
78
64
|
bool,
|
|
79
65
|
Field(
|
|
80
66
|
description="Case sensitive search",
|
|
81
|
-
default=
|
|
67
|
+
default=True,
|
|
82
68
|
),
|
|
83
69
|
]
|
|
84
70
|
|
|
85
|
-
|
|
71
|
+
FixedStrings = Annotated[
|
|
86
72
|
bool,
|
|
87
73
|
Field(
|
|
88
|
-
description="
|
|
89
|
-
default=
|
|
74
|
+
description="Treat pattern as literal string, not regex",
|
|
75
|
+
default=False,
|
|
90
76
|
),
|
|
91
77
|
]
|
|
92
78
|
|
|
93
|
-
|
|
94
|
-
|
|
79
|
+
ShowContext = Annotated[
|
|
80
|
+
int,
|
|
95
81
|
Field(
|
|
96
|
-
description="
|
|
82
|
+
description="Lines of context to show around matches",
|
|
83
|
+
default=0,
|
|
84
|
+
),
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
Backend = Annotated[
|
|
88
|
+
Optional[str],
|
|
89
|
+
Field(
|
|
90
|
+
description="Force specific backend: rg, ag, ack, grep",
|
|
97
91
|
default=None,
|
|
98
92
|
),
|
|
99
93
|
]
|
|
@@ -104,17 +98,17 @@ class FindParams(TypedDict, total=False):
|
|
|
104
98
|
|
|
105
99
|
pattern: str
|
|
106
100
|
path: str
|
|
107
|
-
mode: Literal["name", "content", "both"]
|
|
108
101
|
include: Optional[str]
|
|
109
102
|
exclude: Optional[str]
|
|
110
103
|
case_sensitive: bool
|
|
111
|
-
|
|
112
|
-
|
|
104
|
+
fixed_strings: bool
|
|
105
|
+
show_context: int
|
|
106
|
+
backend: Optional[str]
|
|
113
107
|
|
|
114
108
|
|
|
115
109
|
@final
|
|
116
110
|
class FindTool(FilesystemBaseTool):
|
|
117
|
-
"""Unified find tool
|
|
111
|
+
"""Unified find tool with multiple backend support."""
|
|
118
112
|
|
|
119
113
|
def __init__(self, permission_manager):
|
|
120
114
|
"""Initialize the find tool."""
|
|
@@ -133,24 +127,17 @@ class FindTool(FilesystemBaseTool):
|
|
|
133
127
|
def description(self) -> str:
|
|
134
128
|
"""Get the tool description."""
|
|
135
129
|
backends = self._get_available_backends()
|
|
136
|
-
backend_str = ", ".join(backends) if backends else "fallback
|
|
130
|
+
backend_str = ", ".join(backends) if backends else "fallback grep"
|
|
137
131
|
|
|
138
|
-
return f"""Find
|
|
132
|
+
return f"""Find pattern in files (like ffind). Available: {backend_str}.
|
|
139
133
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
find "
|
|
143
|
-
find "
|
|
144
|
-
find "
|
|
134
|
+
Usage:
|
|
135
|
+
find "TODO"
|
|
136
|
+
find "error.*fatal" ./src
|
|
137
|
+
find "config" --include "*.json"
|
|
138
|
+
find "password" --exclude "*.log"
|
|
145
139
|
|
|
146
|
-
|
|
147
|
-
find "TODO" --mode content
|
|
148
|
-
find "error.*fatal" ./src --mode content
|
|
149
|
-
|
|
150
|
-
# Find both name and content
|
|
151
|
-
find "config" --mode both --include "*.json"
|
|
152
|
-
|
|
153
|
-
Supports wildcards for names, regex for content."""
|
|
140
|
+
Fast, intuitive file content search."""
|
|
154
141
|
|
|
155
142
|
def _get_available_backends(self) -> List[str]:
|
|
156
143
|
"""Get list of available search backends."""
|
|
@@ -162,6 +149,9 @@ Supports wildcards for names, regex for content."""
|
|
|
162
149
|
return self._available_backends
|
|
163
150
|
|
|
164
151
|
@override
|
|
152
|
+
@auto_timeout("find")
|
|
153
|
+
|
|
154
|
+
|
|
165
155
|
async def call(
|
|
166
156
|
self,
|
|
167
157
|
ctx: MCPContext,
|
|
@@ -176,15 +166,12 @@ Supports wildcards for names, regex for content."""
|
|
|
176
166
|
return "Error: pattern is required"
|
|
177
167
|
|
|
178
168
|
path = params.get("path", ".")
|
|
179
|
-
mode = params.get("mode", "name")
|
|
180
169
|
include = params.get("include")
|
|
181
170
|
exclude = params.get("exclude")
|
|
182
|
-
case_sensitive = params.get("case_sensitive",
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
# Expand path (handles ~, $HOME, etc.)
|
|
187
|
-
path = self.expand_path(path)
|
|
171
|
+
case_sensitive = params.get("case_sensitive", True)
|
|
172
|
+
fixed_strings = params.get("fixed_strings", False)
|
|
173
|
+
show_context = params.get("show_context", 0)
|
|
174
|
+
backend = params.get("backend")
|
|
188
175
|
|
|
189
176
|
# Validate path
|
|
190
177
|
path_validation = self.validate_path(path)
|
|
@@ -202,154 +189,93 @@ Supports wildcards for names, regex for content."""
|
|
|
202
189
|
if not exists:
|
|
203
190
|
return error_msg
|
|
204
191
|
|
|
205
|
-
|
|
192
|
+
# Select backend
|
|
193
|
+
available = self._get_available_backends()
|
|
206
194
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
elif
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
)
|
|
216
|
-
elif mode == "both":
|
|
217
|
-
return await self._find_both(
|
|
218
|
-
pattern, path, include, exclude, case_sensitive, recursive, max_results, tool_ctx
|
|
219
|
-
)
|
|
195
|
+
if backend:
|
|
196
|
+
# User specified backend
|
|
197
|
+
if backend not in available and backend != "grep":
|
|
198
|
+
return f"Error: Backend '{backend}' not available. Available: {', '.join(available + ['grep'])}"
|
|
199
|
+
selected_backend = backend
|
|
200
|
+
elif available:
|
|
201
|
+
# Use first available
|
|
202
|
+
selected_backend = available[0]
|
|
220
203
|
else:
|
|
221
|
-
|
|
222
|
-
|
|
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}'"
|
|
204
|
+
# Fallback
|
|
205
|
+
selected_backend = "grep"
|
|
270
206
|
|
|
271
|
-
|
|
272
|
-
output = [f"Found {len(results)} file(s) matching '{pattern}':"]
|
|
273
|
-
output.append("")
|
|
207
|
+
await tool_ctx.info(f"Using {selected_backend} to search for '{pattern}' in {path}")
|
|
274
208
|
|
|
275
|
-
|
|
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")
|
|
299
|
-
|
|
300
|
-
# Execute content search
|
|
209
|
+
# Execute search
|
|
301
210
|
if selected_backend == "rg":
|
|
302
|
-
return await self.
|
|
303
|
-
pattern,
|
|
211
|
+
return await self._run_ripgrep(
|
|
212
|
+
pattern,
|
|
213
|
+
path,
|
|
214
|
+
include,
|
|
215
|
+
exclude,
|
|
216
|
+
case_sensitive,
|
|
217
|
+
fixed_strings,
|
|
218
|
+
show_context,
|
|
219
|
+
tool_ctx,
|
|
304
220
|
)
|
|
305
221
|
elif selected_backend == "ag":
|
|
306
|
-
return await self.
|
|
307
|
-
pattern,
|
|
222
|
+
return await self._run_silver_searcher(
|
|
223
|
+
pattern,
|
|
224
|
+
path,
|
|
225
|
+
include,
|
|
226
|
+
exclude,
|
|
227
|
+
case_sensitive,
|
|
228
|
+
fixed_strings,
|
|
229
|
+
show_context,
|
|
230
|
+
tool_ctx,
|
|
308
231
|
)
|
|
309
232
|
elif selected_backend == "ack":
|
|
310
|
-
return await self.
|
|
311
|
-
pattern,
|
|
233
|
+
return await self._run_ack(
|
|
234
|
+
pattern,
|
|
235
|
+
path,
|
|
236
|
+
include,
|
|
237
|
+
exclude,
|
|
238
|
+
case_sensitive,
|
|
239
|
+
fixed_strings,
|
|
240
|
+
show_context,
|
|
241
|
+
tool_ctx,
|
|
312
242
|
)
|
|
313
243
|
else:
|
|
314
|
-
return await self.
|
|
315
|
-
pattern,
|
|
244
|
+
return await self._run_fallback_grep(
|
|
245
|
+
pattern,
|
|
246
|
+
path,
|
|
247
|
+
include,
|
|
248
|
+
exclude,
|
|
249
|
+
case_sensitive,
|
|
250
|
+
fixed_strings,
|
|
251
|
+
show_context,
|
|
252
|
+
tool_ctx,
|
|
316
253
|
)
|
|
317
254
|
|
|
318
|
-
async def
|
|
319
|
-
self,
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
|
255
|
+
async def _run_ripgrep(
|
|
256
|
+
self,
|
|
257
|
+
pattern,
|
|
258
|
+
path,
|
|
259
|
+
include,
|
|
260
|
+
exclude,
|
|
261
|
+
case_sensitive,
|
|
262
|
+
fixed_strings,
|
|
263
|
+
show_context,
|
|
264
|
+
tool_ctx,
|
|
341
265
|
) -> str:
|
|
342
|
-
"""Run ripgrep backend
|
|
266
|
+
"""Run ripgrep backend."""
|
|
343
267
|
cmd = ["rg", "--json"]
|
|
344
268
|
|
|
345
269
|
if not case_sensitive:
|
|
346
270
|
cmd.append("-i")
|
|
271
|
+
if fixed_strings:
|
|
272
|
+
cmd.append("-F")
|
|
273
|
+
if show_context > 0:
|
|
274
|
+
cmd.extend(["-C", str(show_context)])
|
|
347
275
|
if include:
|
|
348
276
|
cmd.extend(["-g", include])
|
|
349
277
|
if exclude:
|
|
350
278
|
cmd.extend(["-g", f"!{exclude}"])
|
|
351
|
-
if max_results:
|
|
352
|
-
cmd.extend(["-m", str(max_results)])
|
|
353
279
|
|
|
354
280
|
cmd.extend([pattern, path])
|
|
355
281
|
|
|
@@ -370,20 +296,30 @@ Supports wildcards for names, regex for content."""
|
|
|
370
296
|
await tool_ctx.error(f"Error running ripgrep: {str(e)}")
|
|
371
297
|
return f"Error running ripgrep: {str(e)}"
|
|
372
298
|
|
|
373
|
-
async def
|
|
374
|
-
self,
|
|
299
|
+
async def _run_silver_searcher(
|
|
300
|
+
self,
|
|
301
|
+
pattern,
|
|
302
|
+
path,
|
|
303
|
+
include,
|
|
304
|
+
exclude,
|
|
305
|
+
case_sensitive,
|
|
306
|
+
fixed_strings,
|
|
307
|
+
show_context,
|
|
308
|
+
tool_ctx,
|
|
375
309
|
) -> str:
|
|
376
|
-
"""Run silver searcher (ag) backend
|
|
310
|
+
"""Run silver searcher (ag) backend."""
|
|
377
311
|
cmd = ["ag", "--nocolor", "--nogroup"]
|
|
378
312
|
|
|
379
313
|
if not case_sensitive:
|
|
380
314
|
cmd.append("-i")
|
|
315
|
+
if fixed_strings:
|
|
316
|
+
cmd.append("-F")
|
|
317
|
+
if show_context > 0:
|
|
318
|
+
cmd.extend(["-C", str(show_context)])
|
|
381
319
|
if include:
|
|
382
320
|
cmd.extend(["-G", include])
|
|
383
321
|
if exclude:
|
|
384
322
|
cmd.extend(["--ignore", exclude])
|
|
385
|
-
if max_results:
|
|
386
|
-
cmd.extend(["-m", str(max_results)])
|
|
387
323
|
|
|
388
324
|
cmd.extend([pattern, path])
|
|
389
325
|
|
|
@@ -409,23 +345,35 @@ Supports wildcards for names, regex for content."""
|
|
|
409
345
|
await tool_ctx.error(f"Error running ag: {str(e)}")
|
|
410
346
|
return f"Error running ag: {str(e)}"
|
|
411
347
|
|
|
412
|
-
async def
|
|
413
|
-
self,
|
|
348
|
+
async def _run_ack(
|
|
349
|
+
self,
|
|
350
|
+
pattern,
|
|
351
|
+
path,
|
|
352
|
+
include,
|
|
353
|
+
exclude,
|
|
354
|
+
case_sensitive,
|
|
355
|
+
fixed_strings,
|
|
356
|
+
show_context,
|
|
357
|
+
tool_ctx,
|
|
414
358
|
) -> str:
|
|
415
|
-
"""Run ack backend
|
|
359
|
+
"""Run ack backend."""
|
|
416
360
|
cmd = ["ack", "--nocolor", "--nogroup"]
|
|
417
361
|
|
|
418
362
|
if not case_sensitive:
|
|
419
363
|
cmd.append("-i")
|
|
364
|
+
if fixed_strings:
|
|
365
|
+
cmd.append("-Q")
|
|
366
|
+
if show_context > 0:
|
|
367
|
+
cmd.extend(["-C", str(show_context)])
|
|
420
368
|
if include:
|
|
421
369
|
# ack uses different syntax for file patterns
|
|
422
|
-
cmd.extend(
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
370
|
+
cmd.extend(
|
|
371
|
+
[
|
|
372
|
+
"--type-add",
|
|
373
|
+
f"custom:ext:{include.replace('*.', '')}",
|
|
374
|
+
"--type=custom",
|
|
375
|
+
]
|
|
376
|
+
)
|
|
429
377
|
|
|
430
378
|
cmd.extend([pattern, path])
|
|
431
379
|
|
|
@@ -451,10 +399,18 @@ Supports wildcards for names, regex for content."""
|
|
|
451
399
|
await tool_ctx.error(f"Error running ack: {str(e)}")
|
|
452
400
|
return f"Error running ack: {str(e)}"
|
|
453
401
|
|
|
454
|
-
async def
|
|
455
|
-
self,
|
|
402
|
+
async def _run_fallback_grep(
|
|
403
|
+
self,
|
|
404
|
+
pattern,
|
|
405
|
+
path,
|
|
406
|
+
include,
|
|
407
|
+
exclude,
|
|
408
|
+
case_sensitive,
|
|
409
|
+
fixed_strings,
|
|
410
|
+
show_context,
|
|
411
|
+
tool_ctx,
|
|
456
412
|
) -> str:
|
|
457
|
-
"""Fallback Python implementation
|
|
413
|
+
"""Fallback Python implementation."""
|
|
458
414
|
await tool_ctx.info("Using fallback Python grep implementation")
|
|
459
415
|
|
|
460
416
|
try:
|
|
@@ -475,11 +431,17 @@ Supports wildcards for names, regex for content."""
|
|
|
475
431
|
return "No matching files found."
|
|
476
432
|
|
|
477
433
|
# Compile pattern
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
434
|
+
if fixed_strings:
|
|
435
|
+
pattern_re = re.escape(pattern)
|
|
436
|
+
else:
|
|
437
|
+
pattern_re = pattern
|
|
438
|
+
|
|
439
|
+
if not case_sensitive:
|
|
440
|
+
flags = re.IGNORECASE
|
|
441
|
+
else:
|
|
442
|
+
flags = 0
|
|
443
|
+
|
|
444
|
+
regex = re.compile(pattern_re, flags)
|
|
483
445
|
|
|
484
446
|
# Search files
|
|
485
447
|
results = []
|
|
@@ -492,20 +454,26 @@ Supports wildcards for names, regex for content."""
|
|
|
492
454
|
|
|
493
455
|
for i, line in enumerate(lines, 1):
|
|
494
456
|
if regex.search(line):
|
|
495
|
-
|
|
457
|
+
# Format result with context if requested
|
|
458
|
+
if show_context > 0:
|
|
459
|
+
start = max(0, i - show_context - 1)
|
|
460
|
+
end = min(len(lines), i + show_context)
|
|
461
|
+
|
|
462
|
+
context_lines = []
|
|
463
|
+
for j in range(start, end):
|
|
464
|
+
prefix = ":" if j + 1 == i else "-"
|
|
465
|
+
context_lines.append(f"{file_path}:{j + 1}{prefix}{lines[j].rstrip()}")
|
|
466
|
+
results.extend(context_lines)
|
|
467
|
+
results.append("") # Separator
|
|
468
|
+
else:
|
|
469
|
+
results.append(f"{file_path}:{i}:{line.rstrip()}")
|
|
496
470
|
total_matches += 1
|
|
497
|
-
|
|
498
|
-
if max_results and total_matches >= max_results:
|
|
499
|
-
break
|
|
500
471
|
|
|
501
472
|
except UnicodeDecodeError:
|
|
502
473
|
pass # Skip binary files
|
|
503
474
|
except Exception as e:
|
|
504
475
|
await tool_ctx.warning(f"Error reading {file_path}: {str(e)}")
|
|
505
476
|
|
|
506
|
-
if max_results and total_matches >= max_results:
|
|
507
|
-
break
|
|
508
|
-
|
|
509
477
|
if not results:
|
|
510
478
|
return "No matches found."
|
|
511
479
|
|
|
@@ -515,108 +483,6 @@ Supports wildcards for names, regex for content."""
|
|
|
515
483
|
await tool_ctx.error(f"Error in fallback grep: {str(e)}")
|
|
516
484
|
return f"Error in fallback grep: {str(e)}"
|
|
517
485
|
|
|
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
486
|
def _match_file_pattern(self, filename: str, include: Optional[str], exclude: Optional[str]) -> bool:
|
|
621
487
|
"""Check if filename matches include/exclude patterns."""
|
|
622
488
|
if include and not fnmatch.fnmatch(filename, include):
|
|
@@ -667,4 +533,4 @@ Supports wildcards for names, regex for content."""
|
|
|
667
533
|
|
|
668
534
|
def register(self, mcp_server) -> None:
|
|
669
535
|
"""Register this tool with the MCP server."""
|
|
670
|
-
pass
|
|
536
|
+
pass
|