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
|
@@ -4,23 +4,27 @@ 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 asyncio
|
|
8
|
-
import fnmatch
|
|
9
|
-
import json
|
|
10
7
|
import re
|
|
11
|
-
import
|
|
8
|
+
import json
|
|
12
9
|
import shutil
|
|
10
|
+
import asyncio
|
|
11
|
+
import fnmatch
|
|
12
|
+
from typing import (
|
|
13
|
+
List,
|
|
14
|
+
Unpack,
|
|
15
|
+
Optional,
|
|
16
|
+
Annotated,
|
|
17
|
+
TypedDict,
|
|
18
|
+
final,
|
|
19
|
+
override,
|
|
20
|
+
)
|
|
13
21
|
from pathlib import Path
|
|
14
|
-
from typing import Annotated, TypedDict, Unpack, final, override, Optional, List, Dict, Any
|
|
15
22
|
|
|
16
|
-
from mcp.server.fastmcp import Context as MCPContext
|
|
17
23
|
from pydantic import Field
|
|
24
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
18
25
|
|
|
19
|
-
from hanzo_mcp.tools.common.context import ToolContext
|
|
20
|
-
from hanzo_mcp.tools.common.truncate import truncate_response
|
|
21
26
|
from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
|
|
22
27
|
|
|
23
|
-
|
|
24
28
|
# Parameter types
|
|
25
29
|
Pattern = Annotated[
|
|
26
30
|
str,
|
|
@@ -49,7 +53,7 @@ Include = Annotated[
|
|
|
49
53
|
Exclude = Annotated[
|
|
50
54
|
Optional[str],
|
|
51
55
|
Field(
|
|
52
|
-
description=
|
|
56
|
+
description="File pattern to exclude",
|
|
53
57
|
default=None,
|
|
54
58
|
),
|
|
55
59
|
]
|
|
@@ -89,6 +93,7 @@ Backend = Annotated[
|
|
|
89
93
|
|
|
90
94
|
class FindParams(TypedDict, total=False):
|
|
91
95
|
"""Parameters for find tool."""
|
|
96
|
+
|
|
92
97
|
pattern: str
|
|
93
98
|
path: str
|
|
94
99
|
include: Optional[str]
|
|
@@ -102,7 +107,7 @@ class FindParams(TypedDict, total=False):
|
|
|
102
107
|
@final
|
|
103
108
|
class FindTool(FilesystemBaseTool):
|
|
104
109
|
"""Unified find tool with multiple backend support."""
|
|
105
|
-
|
|
110
|
+
|
|
106
111
|
def __init__(self, permission_manager):
|
|
107
112
|
"""Initialize the find tool."""
|
|
108
113
|
super().__init__(permission_manager)
|
|
@@ -116,12 +121,12 @@ class FindTool(FilesystemBaseTool):
|
|
|
116
121
|
return "find"
|
|
117
122
|
|
|
118
123
|
@property
|
|
119
|
-
@override
|
|
124
|
+
@override
|
|
120
125
|
def description(self) -> str:
|
|
121
126
|
"""Get the tool description."""
|
|
122
127
|
backends = self._get_available_backends()
|
|
123
128
|
backend_str = ", ".join(backends) if backends else "fallback grep"
|
|
124
|
-
|
|
129
|
+
|
|
125
130
|
return f"""Find pattern in files (like ffind). Available: {backend_str}.
|
|
126
131
|
|
|
127
132
|
Usage:
|
|
@@ -154,7 +159,7 @@ Fast, intuitive file content search."""
|
|
|
154
159
|
pattern = params.get("pattern")
|
|
155
160
|
if not pattern:
|
|
156
161
|
return "Error: pattern is required"
|
|
157
|
-
|
|
162
|
+
|
|
158
163
|
path = params.get("path", ".")
|
|
159
164
|
include = params.get("include")
|
|
160
165
|
exclude = params.get("exclude")
|
|
@@ -162,7 +167,7 @@ Fast, intuitive file content search."""
|
|
|
162
167
|
fixed_strings = params.get("fixed_strings", False)
|
|
163
168
|
show_context = params.get("show_context", 0)
|
|
164
169
|
backend = params.get("backend")
|
|
165
|
-
|
|
170
|
+
|
|
166
171
|
# Validate path
|
|
167
172
|
path_validation = self.validate_path(path)
|
|
168
173
|
if path_validation.is_error:
|
|
@@ -181,7 +186,7 @@ Fast, intuitive file content search."""
|
|
|
181
186
|
|
|
182
187
|
# Select backend
|
|
183
188
|
available = self._get_available_backends()
|
|
184
|
-
|
|
189
|
+
|
|
185
190
|
if backend:
|
|
186
191
|
# User specified backend
|
|
187
192
|
if backend not in available and backend != "grep":
|
|
@@ -193,23 +198,71 @@ Fast, intuitive file content search."""
|
|
|
193
198
|
else:
|
|
194
199
|
# Fallback
|
|
195
200
|
selected_backend = "grep"
|
|
196
|
-
|
|
197
|
-
await tool_ctx.info(
|
|
198
|
-
|
|
201
|
+
|
|
202
|
+
await tool_ctx.info(
|
|
203
|
+
f"Using {selected_backend} to search for '{pattern}' in {path}"
|
|
204
|
+
)
|
|
205
|
+
|
|
199
206
|
# Execute search
|
|
200
207
|
if selected_backend == "rg":
|
|
201
|
-
return await self._run_ripgrep(
|
|
208
|
+
return await self._run_ripgrep(
|
|
209
|
+
pattern,
|
|
210
|
+
path,
|
|
211
|
+
include,
|
|
212
|
+
exclude,
|
|
213
|
+
case_sensitive,
|
|
214
|
+
fixed_strings,
|
|
215
|
+
show_context,
|
|
216
|
+
tool_ctx,
|
|
217
|
+
)
|
|
202
218
|
elif selected_backend == "ag":
|
|
203
|
-
return await self._run_silver_searcher(
|
|
219
|
+
return await self._run_silver_searcher(
|
|
220
|
+
pattern,
|
|
221
|
+
path,
|
|
222
|
+
include,
|
|
223
|
+
exclude,
|
|
224
|
+
case_sensitive,
|
|
225
|
+
fixed_strings,
|
|
226
|
+
show_context,
|
|
227
|
+
tool_ctx,
|
|
228
|
+
)
|
|
204
229
|
elif selected_backend == "ack":
|
|
205
|
-
return await self._run_ack(
|
|
230
|
+
return await self._run_ack(
|
|
231
|
+
pattern,
|
|
232
|
+
path,
|
|
233
|
+
include,
|
|
234
|
+
exclude,
|
|
235
|
+
case_sensitive,
|
|
236
|
+
fixed_strings,
|
|
237
|
+
show_context,
|
|
238
|
+
tool_ctx,
|
|
239
|
+
)
|
|
206
240
|
else:
|
|
207
|
-
return await self._run_fallback_grep(
|
|
241
|
+
return await self._run_fallback_grep(
|
|
242
|
+
pattern,
|
|
243
|
+
path,
|
|
244
|
+
include,
|
|
245
|
+
exclude,
|
|
246
|
+
case_sensitive,
|
|
247
|
+
fixed_strings,
|
|
248
|
+
show_context,
|
|
249
|
+
tool_ctx,
|
|
250
|
+
)
|
|
208
251
|
|
|
209
|
-
async def _run_ripgrep(
|
|
252
|
+
async def _run_ripgrep(
|
|
253
|
+
self,
|
|
254
|
+
pattern,
|
|
255
|
+
path,
|
|
256
|
+
include,
|
|
257
|
+
exclude,
|
|
258
|
+
case_sensitive,
|
|
259
|
+
fixed_strings,
|
|
260
|
+
show_context,
|
|
261
|
+
tool_ctx,
|
|
262
|
+
) -> str:
|
|
210
263
|
"""Run ripgrep backend."""
|
|
211
264
|
cmd = ["rg", "--json"]
|
|
212
|
-
|
|
265
|
+
|
|
213
266
|
if not case_sensitive:
|
|
214
267
|
cmd.append("-i")
|
|
215
268
|
if fixed_strings:
|
|
@@ -220,30 +273,40 @@ Fast, intuitive file content search."""
|
|
|
220
273
|
cmd.extend(["-g", include])
|
|
221
274
|
if exclude:
|
|
222
275
|
cmd.extend(["-g", f"!{exclude}"])
|
|
223
|
-
|
|
276
|
+
|
|
224
277
|
cmd.extend([pattern, path])
|
|
225
|
-
|
|
278
|
+
|
|
226
279
|
try:
|
|
227
280
|
process = await asyncio.create_subprocess_exec(
|
|
228
281
|
*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
|
229
282
|
)
|
|
230
|
-
|
|
283
|
+
|
|
231
284
|
stdout, stderr = await process.communicate()
|
|
232
|
-
|
|
285
|
+
|
|
233
286
|
if process.returncode not in [0, 1]: # 1 = no matches
|
|
234
287
|
await tool_ctx.error(f"ripgrep failed: {stderr.decode()}")
|
|
235
288
|
return f"Error: {stderr.decode()}"
|
|
236
|
-
|
|
289
|
+
|
|
237
290
|
return self._parse_ripgrep_output(stdout.decode())
|
|
238
|
-
|
|
291
|
+
|
|
239
292
|
except Exception as e:
|
|
240
293
|
await tool_ctx.error(f"Error running ripgrep: {str(e)}")
|
|
241
294
|
return f"Error running ripgrep: {str(e)}"
|
|
242
295
|
|
|
243
|
-
async def _run_silver_searcher(
|
|
296
|
+
async def _run_silver_searcher(
|
|
297
|
+
self,
|
|
298
|
+
pattern,
|
|
299
|
+
path,
|
|
300
|
+
include,
|
|
301
|
+
exclude,
|
|
302
|
+
case_sensitive,
|
|
303
|
+
fixed_strings,
|
|
304
|
+
show_context,
|
|
305
|
+
tool_ctx,
|
|
306
|
+
) -> str:
|
|
244
307
|
"""Run silver searcher (ag) backend."""
|
|
245
308
|
cmd = ["ag", "--nocolor", "--nogroup"]
|
|
246
|
-
|
|
309
|
+
|
|
247
310
|
if not case_sensitive:
|
|
248
311
|
cmd.append("-i")
|
|
249
312
|
if fixed_strings:
|
|
@@ -254,35 +317,45 @@ Fast, intuitive file content search."""
|
|
|
254
317
|
cmd.extend(["-G", include])
|
|
255
318
|
if exclude:
|
|
256
319
|
cmd.extend(["--ignore", exclude])
|
|
257
|
-
|
|
320
|
+
|
|
258
321
|
cmd.extend([pattern, path])
|
|
259
|
-
|
|
322
|
+
|
|
260
323
|
try:
|
|
261
324
|
process = await asyncio.create_subprocess_exec(
|
|
262
325
|
*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
|
263
326
|
)
|
|
264
|
-
|
|
327
|
+
|
|
265
328
|
stdout, stderr = await process.communicate()
|
|
266
|
-
|
|
329
|
+
|
|
267
330
|
if process.returncode not in [0, 1]:
|
|
268
331
|
await tool_ctx.error(f"ag failed: {stderr.decode()}")
|
|
269
332
|
return f"Error: {stderr.decode()}"
|
|
270
|
-
|
|
333
|
+
|
|
271
334
|
output = stdout.decode()
|
|
272
335
|
if not output.strip():
|
|
273
336
|
return "No matches found."
|
|
274
|
-
|
|
275
|
-
lines = output.strip().split(
|
|
337
|
+
|
|
338
|
+
lines = output.strip().split("\n")
|
|
276
339
|
return f"Found {len(lines)} matches:\n\n" + output
|
|
277
|
-
|
|
340
|
+
|
|
278
341
|
except Exception as e:
|
|
279
342
|
await tool_ctx.error(f"Error running ag: {str(e)}")
|
|
280
343
|
return f"Error running ag: {str(e)}"
|
|
281
344
|
|
|
282
|
-
async def _run_ack(
|
|
345
|
+
async def _run_ack(
|
|
346
|
+
self,
|
|
347
|
+
pattern,
|
|
348
|
+
path,
|
|
349
|
+
include,
|
|
350
|
+
exclude,
|
|
351
|
+
case_sensitive,
|
|
352
|
+
fixed_strings,
|
|
353
|
+
show_context,
|
|
354
|
+
tool_ctx,
|
|
355
|
+
) -> str:
|
|
283
356
|
"""Run ack backend."""
|
|
284
357
|
cmd = ["ack", "--nocolor", "--nogroup"]
|
|
285
|
-
|
|
358
|
+
|
|
286
359
|
if not case_sensitive:
|
|
287
360
|
cmd.append("-i")
|
|
288
361
|
if fixed_strings:
|
|
@@ -291,40 +364,56 @@ Fast, intuitive file content search."""
|
|
|
291
364
|
cmd.extend(["-C", str(show_context)])
|
|
292
365
|
if include:
|
|
293
366
|
# ack uses different syntax for file patterns
|
|
294
|
-
cmd.extend(
|
|
295
|
-
|
|
367
|
+
cmd.extend(
|
|
368
|
+
[
|
|
369
|
+
"--type-add",
|
|
370
|
+
f"custom:ext:{include.replace('*.', '')}",
|
|
371
|
+
"--type=custom",
|
|
372
|
+
]
|
|
373
|
+
)
|
|
374
|
+
|
|
296
375
|
cmd.extend([pattern, path])
|
|
297
|
-
|
|
376
|
+
|
|
298
377
|
try:
|
|
299
378
|
process = await asyncio.create_subprocess_exec(
|
|
300
379
|
*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
|
301
380
|
)
|
|
302
|
-
|
|
381
|
+
|
|
303
382
|
stdout, stderr = await process.communicate()
|
|
304
|
-
|
|
383
|
+
|
|
305
384
|
if process.returncode not in [0, 1]:
|
|
306
385
|
await tool_ctx.error(f"ack failed: {stderr.decode()}")
|
|
307
386
|
return f"Error: {stderr.decode()}"
|
|
308
|
-
|
|
387
|
+
|
|
309
388
|
output = stdout.decode()
|
|
310
389
|
if not output.strip():
|
|
311
390
|
return "No matches found."
|
|
312
|
-
|
|
313
|
-
lines = output.strip().split(
|
|
391
|
+
|
|
392
|
+
lines = output.strip().split("\n")
|
|
314
393
|
return f"Found {len(lines)} matches:\n\n" + output
|
|
315
|
-
|
|
394
|
+
|
|
316
395
|
except Exception as e:
|
|
317
396
|
await tool_ctx.error(f"Error running ack: {str(e)}")
|
|
318
397
|
return f"Error running ack: {str(e)}"
|
|
319
398
|
|
|
320
|
-
async def _run_fallback_grep(
|
|
399
|
+
async def _run_fallback_grep(
|
|
400
|
+
self,
|
|
401
|
+
pattern,
|
|
402
|
+
path,
|
|
403
|
+
include,
|
|
404
|
+
exclude,
|
|
405
|
+
case_sensitive,
|
|
406
|
+
fixed_strings,
|
|
407
|
+
show_context,
|
|
408
|
+
tool_ctx,
|
|
409
|
+
) -> str:
|
|
321
410
|
"""Fallback Python implementation."""
|
|
322
411
|
await tool_ctx.info("Using fallback Python grep implementation")
|
|
323
|
-
|
|
412
|
+
|
|
324
413
|
try:
|
|
325
414
|
input_path = Path(path)
|
|
326
415
|
matching_files = []
|
|
327
|
-
|
|
416
|
+
|
|
328
417
|
# Get files to search
|
|
329
418
|
if input_path.is_file():
|
|
330
419
|
if self._match_file_pattern(input_path.name, include, exclude):
|
|
@@ -334,64 +423,68 @@ Fast, intuitive file content search."""
|
|
|
334
423
|
if entry.is_file() and self.is_path_allowed(str(entry)):
|
|
335
424
|
if self._match_file_pattern(entry.name, include, exclude):
|
|
336
425
|
matching_files.append(entry)
|
|
337
|
-
|
|
426
|
+
|
|
338
427
|
if not matching_files:
|
|
339
428
|
return "No matching files found."
|
|
340
|
-
|
|
429
|
+
|
|
341
430
|
# Compile pattern
|
|
342
431
|
if fixed_strings:
|
|
343
432
|
pattern_re = re.escape(pattern)
|
|
344
433
|
else:
|
|
345
434
|
pattern_re = pattern
|
|
346
|
-
|
|
435
|
+
|
|
347
436
|
if not case_sensitive:
|
|
348
437
|
flags = re.IGNORECASE
|
|
349
438
|
else:
|
|
350
439
|
flags = 0
|
|
351
|
-
|
|
440
|
+
|
|
352
441
|
regex = re.compile(pattern_re, flags)
|
|
353
|
-
|
|
442
|
+
|
|
354
443
|
# Search files
|
|
355
444
|
results = []
|
|
356
445
|
total_matches = 0
|
|
357
|
-
|
|
446
|
+
|
|
358
447
|
for file_path in matching_files:
|
|
359
448
|
try:
|
|
360
449
|
with open(file_path, "r", encoding="utf-8") as f:
|
|
361
450
|
lines = f.readlines()
|
|
362
|
-
|
|
451
|
+
|
|
363
452
|
for i, line in enumerate(lines, 1):
|
|
364
453
|
if regex.search(line):
|
|
365
454
|
# Format result with context if requested
|
|
366
455
|
if show_context > 0:
|
|
367
456
|
start = max(0, i - show_context - 1)
|
|
368
457
|
end = min(len(lines), i + show_context)
|
|
369
|
-
|
|
458
|
+
|
|
370
459
|
context_lines = []
|
|
371
460
|
for j in range(start, end):
|
|
372
461
|
prefix = ":" if j + 1 == i else "-"
|
|
373
|
-
context_lines.append(
|
|
462
|
+
context_lines.append(
|
|
463
|
+
f"{file_path}:{j + 1}{prefix}{lines[j].rstrip()}"
|
|
464
|
+
)
|
|
374
465
|
results.extend(context_lines)
|
|
375
466
|
results.append("") # Separator
|
|
376
467
|
else:
|
|
377
468
|
results.append(f"{file_path}:{i}:{line.rstrip()}")
|
|
378
469
|
total_matches += 1
|
|
379
|
-
|
|
470
|
+
|
|
380
471
|
except UnicodeDecodeError:
|
|
381
472
|
pass # Skip binary files
|
|
382
473
|
except Exception as e:
|
|
383
474
|
await tool_ctx.warning(f"Error reading {file_path}: {str(e)}")
|
|
384
|
-
|
|
475
|
+
|
|
385
476
|
if not results:
|
|
386
477
|
return "No matches found."
|
|
387
|
-
|
|
478
|
+
|
|
388
479
|
return f"Found {total_matches} matches:\n\n" + "\n".join(results)
|
|
389
|
-
|
|
480
|
+
|
|
390
481
|
except Exception as e:
|
|
391
482
|
await tool_ctx.error(f"Error in fallback grep: {str(e)}")
|
|
392
483
|
return f"Error in fallback grep: {str(e)}"
|
|
393
484
|
|
|
394
|
-
def _match_file_pattern(
|
|
485
|
+
def _match_file_pattern(
|
|
486
|
+
self, filename: str, include: Optional[str], exclude: Optional[str]
|
|
487
|
+
) -> bool:
|
|
395
488
|
"""Check if filename matches include/exclude patterns."""
|
|
396
489
|
if include and not fnmatch.fnmatch(filename, include):
|
|
397
490
|
return False
|
|
@@ -403,42 +496,42 @@ Fast, intuitive file content search."""
|
|
|
403
496
|
"""Parse ripgrep JSON output."""
|
|
404
497
|
if not output.strip():
|
|
405
498
|
return "No matches found."
|
|
406
|
-
|
|
499
|
+
|
|
407
500
|
results = []
|
|
408
501
|
total_matches = 0
|
|
409
|
-
|
|
502
|
+
|
|
410
503
|
for line in output.splitlines():
|
|
411
504
|
if not line.strip():
|
|
412
505
|
continue
|
|
413
|
-
|
|
506
|
+
|
|
414
507
|
try:
|
|
415
508
|
data = json.loads(line)
|
|
416
|
-
|
|
509
|
+
|
|
417
510
|
if data.get("type") == "match":
|
|
418
511
|
match_data = data.get("data", {})
|
|
419
512
|
path = match_data.get("path", {}).get("text", "")
|
|
420
513
|
line_number = match_data.get("line_number", 0)
|
|
421
514
|
line_text = match_data.get("lines", {}).get("text", "").rstrip()
|
|
422
|
-
|
|
515
|
+
|
|
423
516
|
results.append(f"{path}:{line_number}:{line_text}")
|
|
424
517
|
total_matches += 1
|
|
425
|
-
|
|
518
|
+
|
|
426
519
|
elif data.get("type") == "context":
|
|
427
520
|
context_data = data.get("data", {})
|
|
428
521
|
path = context_data.get("path", {}).get("text", "")
|
|
429
522
|
line_number = context_data.get("line_number", 0)
|
|
430
523
|
line_text = context_data.get("lines", {}).get("text", "").rstrip()
|
|
431
|
-
|
|
524
|
+
|
|
432
525
|
results.append(f"{path}:{line_number}-{line_text}")
|
|
433
|
-
|
|
526
|
+
|
|
434
527
|
except json.JSONDecodeError:
|
|
435
528
|
pass
|
|
436
|
-
|
|
529
|
+
|
|
437
530
|
if not results:
|
|
438
531
|
return "No matches found."
|
|
439
|
-
|
|
532
|
+
|
|
440
533
|
return f"Found {total_matches} matches:\n\n" + "\n".join(results)
|
|
441
534
|
|
|
442
535
|
def register(self, mcp_server) -> None:
|
|
443
536
|
"""Register this tool with the MCP server."""
|
|
444
|
-
pass
|
|
537
|
+
pass
|