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
|
@@ -1,26 +1,23 @@
|
|
|
1
1
|
"""AST-aware multi-edit tool using treesitter for accurate code modifications."""
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import json
|
|
5
|
-
from typing import List, Dict, Any, Optional, Tuple, Set
|
|
3
|
+
from typing import Any, Dict, List, Tuple, Optional
|
|
6
4
|
from pathlib import Path
|
|
7
|
-
from dataclasses import dataclass
|
|
8
5
|
from collections import defaultdict
|
|
6
|
+
from dataclasses import dataclass
|
|
9
7
|
|
|
10
|
-
from hanzo_mcp.tools.common.base import BaseTool
|
|
11
|
-
from hanzo_mcp.tools.common.decorators import with_context_normalization
|
|
12
|
-
from hanzo_mcp.tools.common.paginated_response import AutoPaginatedResponse
|
|
13
8
|
from hanzo_mcp.types import MCPResourceDocument
|
|
9
|
+
from hanzo_mcp.tools.common.base import BaseTool
|
|
14
10
|
|
|
15
11
|
try:
|
|
16
12
|
import tree_sitter
|
|
13
|
+
import tree_sitter_go
|
|
14
|
+
import tree_sitter_cpp
|
|
15
|
+
import tree_sitter_java
|
|
16
|
+
import tree_sitter_rust
|
|
17
17
|
import tree_sitter_python
|
|
18
18
|
import tree_sitter_javascript
|
|
19
19
|
import tree_sitter_typescript
|
|
20
|
-
|
|
21
|
-
import tree_sitter_rust
|
|
22
|
-
import tree_sitter_java
|
|
23
|
-
import tree_sitter_cpp
|
|
20
|
+
|
|
24
21
|
TREESITTER_AVAILABLE = True
|
|
25
22
|
except ImportError:
|
|
26
23
|
TREESITTER_AVAILABLE = False
|
|
@@ -29,6 +26,7 @@ except ImportError:
|
|
|
29
26
|
@dataclass
|
|
30
27
|
class ASTMatch:
|
|
31
28
|
"""Represents an AST match with context."""
|
|
29
|
+
|
|
32
30
|
file_path: str
|
|
33
31
|
line_start: int
|
|
34
32
|
line_end: int
|
|
@@ -40,9 +38,10 @@ class ASTMatch:
|
|
|
40
38
|
semantic_context: Optional[str] = None
|
|
41
39
|
|
|
42
40
|
|
|
43
|
-
@dataclass
|
|
41
|
+
@dataclass
|
|
44
42
|
class EditOperation:
|
|
45
43
|
"""Enhanced edit operation with AST awareness."""
|
|
44
|
+
|
|
46
45
|
old_string: str
|
|
47
46
|
new_string: str
|
|
48
47
|
node_types: Optional[List[str]] = None # Restrict to specific AST node types
|
|
@@ -53,7 +52,7 @@ class EditOperation:
|
|
|
53
52
|
|
|
54
53
|
class ASTMultiEdit(BaseTool):
|
|
55
54
|
"""Multi-edit tool with AST awareness and automatic reference finding."""
|
|
56
|
-
|
|
55
|
+
|
|
57
56
|
name = "ast_multi_edit"
|
|
58
57
|
description = """Enhanced multi-edit with AST awareness and reference finding.
|
|
59
58
|
|
|
@@ -76,37 +75,37 @@ class ASTMultiEdit(BaseTool):
|
|
|
76
75
|
"node_types": ["call_expression"]}
|
|
77
76
|
])
|
|
78
77
|
"""
|
|
79
|
-
|
|
78
|
+
|
|
80
79
|
def __init__(self):
|
|
81
80
|
super().__init__()
|
|
82
81
|
self.parsers = {}
|
|
83
82
|
self.languages = {}
|
|
84
|
-
|
|
83
|
+
|
|
85
84
|
if TREESITTER_AVAILABLE:
|
|
86
85
|
self._init_parsers()
|
|
87
|
-
|
|
86
|
+
|
|
88
87
|
def _init_parsers(self):
|
|
89
88
|
"""Initialize treesitter parsers for supported languages."""
|
|
90
89
|
language_mapping = {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
90
|
+
".py": (tree_sitter_python, "python"),
|
|
91
|
+
".js": (tree_sitter_javascript, "javascript"),
|
|
92
|
+
".jsx": (tree_sitter_javascript, "javascript"),
|
|
93
|
+
".ts": (tree_sitter_typescript.typescript, "typescript"),
|
|
94
|
+
".tsx": (tree_sitter_typescript.tsx, "tsx"),
|
|
95
|
+
".go": (tree_sitter_go, "go"),
|
|
96
|
+
".rs": (tree_sitter_rust, "rust"),
|
|
97
|
+
".java": (tree_sitter_java, "java"),
|
|
98
|
+
".cpp": (tree_sitter_cpp, "cpp"),
|
|
99
|
+
".cc": (tree_sitter_cpp, "cpp"),
|
|
100
|
+
".cxx": (tree_sitter_cpp, "cpp"),
|
|
101
|
+
".h": (tree_sitter_cpp, "cpp"),
|
|
102
|
+
".hpp": (tree_sitter_cpp, "cpp"),
|
|
104
103
|
}
|
|
105
|
-
|
|
104
|
+
|
|
106
105
|
for ext, (module, name) in language_mapping.items():
|
|
107
106
|
try:
|
|
108
107
|
parser = tree_sitter.Parser()
|
|
109
|
-
if hasattr(module,
|
|
108
|
+
if hasattr(module, "language"):
|
|
110
109
|
parser.set_language(module.language())
|
|
111
110
|
else:
|
|
112
111
|
# For older tree-sitter bindings
|
|
@@ -116,138 +115,185 @@ class ASTMultiEdit(BaseTool):
|
|
|
116
115
|
self.languages[ext] = name
|
|
117
116
|
except Exception as e:
|
|
118
117
|
print(f"Failed to initialize parser for {ext}: {e}")
|
|
119
|
-
|
|
118
|
+
|
|
120
119
|
def _get_parser(self, file_path: str) -> Optional[tree_sitter.Parser]:
|
|
121
120
|
"""Get parser for file type."""
|
|
122
121
|
ext = Path(file_path).suffix.lower()
|
|
123
122
|
return self.parsers.get(ext)
|
|
124
|
-
|
|
123
|
+
|
|
125
124
|
def _parse_file(self, file_path: str, content: str) -> Optional[tree_sitter.Tree]:
|
|
126
125
|
"""Parse file content into AST."""
|
|
127
126
|
parser = self._get_parser(file_path)
|
|
128
127
|
if not parser:
|
|
129
128
|
return None
|
|
130
|
-
|
|
131
|
-
return parser.parse(bytes(content,
|
|
132
|
-
|
|
133
|
-
def _find_references(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
project_root: Optional[str] = None) -> List[ASTMatch]:
|
|
129
|
+
|
|
130
|
+
return parser.parse(bytes(content, "utf-8"))
|
|
131
|
+
|
|
132
|
+
def _find_references(
|
|
133
|
+
self, symbol: str, file_path: str, project_root: Optional[str] = None
|
|
134
|
+
) -> List[ASTMatch]:
|
|
137
135
|
"""Find all references to a symbol across the project."""
|
|
138
136
|
matches = []
|
|
139
|
-
|
|
137
|
+
|
|
140
138
|
if not project_root:
|
|
141
139
|
project_root = self._find_project_root(file_path)
|
|
142
|
-
|
|
140
|
+
|
|
143
141
|
# Get language-specific reference patterns
|
|
144
142
|
patterns = self._get_reference_patterns(symbol, file_path)
|
|
145
|
-
|
|
143
|
+
|
|
146
144
|
# Search across all relevant files
|
|
147
145
|
for pattern in patterns:
|
|
148
146
|
# Use grep_ast tool for efficient AST-aware search
|
|
149
147
|
results = self._search_with_ast(pattern, project_root)
|
|
150
148
|
matches.extend(results)
|
|
151
|
-
|
|
149
|
+
|
|
152
150
|
return matches
|
|
153
|
-
|
|
154
|
-
def _get_reference_patterns(
|
|
151
|
+
|
|
152
|
+
def _get_reference_patterns(
|
|
153
|
+
self, symbol: str, file_path: str
|
|
154
|
+
) -> List[Dict[str, Any]]:
|
|
155
155
|
"""Get language-specific patterns for finding references."""
|
|
156
156
|
ext = Path(file_path).suffix.lower()
|
|
157
|
-
lang = self.languages.get(ext,
|
|
158
|
-
|
|
157
|
+
lang = self.languages.get(ext, "generic")
|
|
158
|
+
|
|
159
159
|
patterns = []
|
|
160
|
-
|
|
161
|
-
if lang ==
|
|
160
|
+
|
|
161
|
+
if lang == "go":
|
|
162
162
|
# Go specific patterns
|
|
163
|
-
patterns.extend(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
163
|
+
patterns.extend(
|
|
164
|
+
[
|
|
165
|
+
# Function calls
|
|
166
|
+
{
|
|
167
|
+
"query": f'(call_expression function: (identifier) @func (#eq? @func "{symbol}"))',
|
|
168
|
+
"type": "call",
|
|
169
|
+
},
|
|
170
|
+
# Method calls
|
|
171
|
+
{
|
|
172
|
+
"query": f'(call_expression function: (selector_expression field: (field_identifier) @method (#eq? @method "{symbol}")))',
|
|
173
|
+
"type": "method_call",
|
|
174
|
+
},
|
|
175
|
+
# Function declarations
|
|
176
|
+
{
|
|
177
|
+
"query": f'(function_declaration name: (identifier) @name (#eq? @name "{symbol}"))',
|
|
178
|
+
"type": "declaration",
|
|
179
|
+
},
|
|
180
|
+
# Type references
|
|
181
|
+
{
|
|
182
|
+
"query": f'(type_identifier) @type (#eq? @type "{symbol}")',
|
|
183
|
+
"type": "type_ref",
|
|
184
|
+
},
|
|
185
|
+
]
|
|
186
|
+
)
|
|
187
|
+
elif lang in ["javascript", "typescript", "tsx"]:
|
|
188
|
+
patterns.extend(
|
|
189
|
+
[
|
|
190
|
+
# Function calls
|
|
191
|
+
{
|
|
192
|
+
"query": f'(call_expression function: (identifier) @func (#eq? @func "{symbol}"))',
|
|
193
|
+
"type": "call",
|
|
194
|
+
},
|
|
195
|
+
# Method calls
|
|
196
|
+
{
|
|
197
|
+
"query": f'(call_expression function: (member_expression property: (property_identifier) @prop (#eq? @prop "{symbol}")))',
|
|
198
|
+
"type": "method_call",
|
|
199
|
+
},
|
|
200
|
+
# Function declarations
|
|
201
|
+
{
|
|
202
|
+
"query": f'(function_declaration name: (identifier) @name (#eq? @name "{symbol}"))',
|
|
203
|
+
"type": "declaration",
|
|
204
|
+
},
|
|
205
|
+
# Variable declarations
|
|
206
|
+
{
|
|
207
|
+
"query": f'(variable_declarator name: (identifier) @var (#eq? @var "{symbol}"))',
|
|
208
|
+
"type": "variable",
|
|
209
|
+
},
|
|
210
|
+
]
|
|
211
|
+
)
|
|
212
|
+
elif lang == "python":
|
|
213
|
+
patterns.extend(
|
|
214
|
+
[
|
|
215
|
+
# Function calls
|
|
216
|
+
{
|
|
217
|
+
"query": f'(call function: (identifier) @func (#eq? @func "{symbol}"))',
|
|
218
|
+
"type": "call",
|
|
219
|
+
},
|
|
220
|
+
# Method calls
|
|
221
|
+
{
|
|
222
|
+
"query": f'(call function: (attribute attribute: (identifier) @attr (#eq? @attr "{symbol}")))',
|
|
223
|
+
"type": "method_call",
|
|
224
|
+
},
|
|
225
|
+
# Function definitions
|
|
226
|
+
{
|
|
227
|
+
"query": f'(function_definition name: (identifier) @name (#eq? @name "{symbol}"))',
|
|
228
|
+
"type": "declaration",
|
|
229
|
+
},
|
|
230
|
+
# Class definitions
|
|
231
|
+
{
|
|
232
|
+
"query": f'(class_definition name: (identifier) @name (#eq? @name "{symbol}"))',
|
|
233
|
+
"type": "class",
|
|
234
|
+
},
|
|
235
|
+
]
|
|
236
|
+
)
|
|
195
237
|
else:
|
|
196
238
|
# Generic patterns
|
|
197
239
|
patterns.append({"query": symbol, "type": "text"})
|
|
198
|
-
|
|
240
|
+
|
|
199
241
|
return patterns
|
|
200
|
-
|
|
242
|
+
|
|
201
243
|
def _search_with_ast(self, pattern: Dict[str, Any], root: str) -> List[ASTMatch]:
|
|
202
244
|
"""Search using AST patterns."""
|
|
203
245
|
matches = []
|
|
204
|
-
|
|
246
|
+
|
|
205
247
|
# This would integrate with grep_ast tool
|
|
206
248
|
# For now, simulate the search
|
|
207
249
|
import glob
|
|
208
|
-
|
|
250
|
+
|
|
209
251
|
for file_path in glob.glob(f"{root}/**/*.*", recursive=True):
|
|
210
252
|
if self._should_skip_file(file_path):
|
|
211
253
|
continue
|
|
212
|
-
|
|
254
|
+
|
|
213
255
|
try:
|
|
214
|
-
with open(file_path,
|
|
256
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
215
257
|
content = f.read()
|
|
216
|
-
|
|
258
|
+
|
|
217
259
|
tree = self._parse_file(file_path, content)
|
|
218
260
|
if tree and pattern["type"] != "text":
|
|
219
261
|
# Use treesitter query
|
|
220
262
|
matches.extend(self._query_ast(tree, pattern, file_path, content))
|
|
221
263
|
else:
|
|
222
264
|
# Fallback to text search
|
|
223
|
-
matches.extend(
|
|
224
|
-
|
|
265
|
+
matches.extend(
|
|
266
|
+
self._text_search(content, pattern["query"], file_path)
|
|
267
|
+
)
|
|
268
|
+
|
|
225
269
|
except Exception:
|
|
226
270
|
continue
|
|
227
|
-
|
|
271
|
+
|
|
228
272
|
return matches
|
|
229
|
-
|
|
230
|
-
def _query_ast(
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
273
|
+
|
|
274
|
+
def _query_ast(
|
|
275
|
+
self,
|
|
276
|
+
tree: tree_sitter.Tree,
|
|
277
|
+
pattern: Dict[str, Any],
|
|
278
|
+
file_path: str,
|
|
279
|
+
content: str,
|
|
280
|
+
) -> List[ASTMatch]:
|
|
235
281
|
"""Query AST with treesitter pattern."""
|
|
236
282
|
matches = []
|
|
237
|
-
|
|
283
|
+
|
|
238
284
|
try:
|
|
239
285
|
# Get language for query
|
|
240
286
|
lang_name = self.languages.get(Path(file_path).suffix.lower())
|
|
241
287
|
if not lang_name:
|
|
242
288
|
return matches
|
|
243
|
-
|
|
289
|
+
|
|
244
290
|
# Execute query
|
|
245
291
|
query = tree_sitter.Query(pattern["query"], lang_name)
|
|
246
292
|
captures = query.captures(tree.root_node)
|
|
247
|
-
|
|
248
|
-
lines = content.split(
|
|
249
|
-
|
|
250
|
-
for node,
|
|
293
|
+
|
|
294
|
+
lines = content.split("\n")
|
|
295
|
+
|
|
296
|
+
for node, _name in captures:
|
|
251
297
|
match = ASTMatch(
|
|
252
298
|
file_path=file_path,
|
|
253
299
|
line_start=node.start_point[0] + 1,
|
|
@@ -255,39 +301,47 @@ class ASTMultiEdit(BaseTool):
|
|
|
255
301
|
column_start=node.start_point[1],
|
|
256
302
|
column_end=node.end_point[1],
|
|
257
303
|
node_type=node.type,
|
|
258
|
-
text=content[node.start_byte:node.end_byte],
|
|
304
|
+
text=content[node.start_byte : node.end_byte],
|
|
259
305
|
parent_context=self._get_parent_context(node, content),
|
|
260
|
-
semantic_context=pattern["type"]
|
|
306
|
+
semantic_context=pattern["type"],
|
|
261
307
|
)
|
|
262
308
|
matches.append(match)
|
|
263
|
-
|
|
264
|
-
except Exception
|
|
309
|
+
|
|
310
|
+
except Exception:
|
|
265
311
|
# Fallback to simple search
|
|
266
312
|
pass
|
|
267
|
-
|
|
313
|
+
|
|
268
314
|
return matches
|
|
269
|
-
|
|
270
|
-
def _get_parent_context(
|
|
315
|
+
|
|
316
|
+
def _get_parent_context(
|
|
317
|
+
self, node: tree_sitter.Node, content: str
|
|
318
|
+
) -> Optional[str]:
|
|
271
319
|
"""Get parent context for better understanding."""
|
|
272
320
|
parent = node.parent
|
|
273
321
|
if parent:
|
|
274
322
|
# Get parent function/class name
|
|
275
|
-
if parent.type in [
|
|
323
|
+
if parent.type in [
|
|
324
|
+
"function_declaration",
|
|
325
|
+
"function_definition",
|
|
326
|
+
"method_definition",
|
|
327
|
+
]:
|
|
276
328
|
for child in parent.children:
|
|
277
|
-
if child.type ==
|
|
278
|
-
return f"function: {content[child.start_byte:child.end_byte]}"
|
|
279
|
-
elif parent.type in [
|
|
329
|
+
if child.type == "identifier":
|
|
330
|
+
return f"function: {content[child.start_byte : child.end_byte]}"
|
|
331
|
+
elif parent.type in ["class_declaration", "class_definition"]:
|
|
280
332
|
for child in parent.children:
|
|
281
|
-
if child.type ==
|
|
282
|
-
return f"class: {content[child.start_byte:child.end_byte]}"
|
|
283
|
-
|
|
333
|
+
if child.type == "identifier":
|
|
334
|
+
return f"class: {content[child.start_byte : child.end_byte]}"
|
|
335
|
+
|
|
284
336
|
return None
|
|
285
|
-
|
|
286
|
-
def _text_search(
|
|
337
|
+
|
|
338
|
+
def _text_search(
|
|
339
|
+
self, content: str, pattern: str, file_path: str
|
|
340
|
+
) -> List[ASTMatch]:
|
|
287
341
|
"""Fallback text search."""
|
|
288
342
|
matches = []
|
|
289
|
-
lines = content.split(
|
|
290
|
-
|
|
343
|
+
lines = content.split("\n")
|
|
344
|
+
|
|
291
345
|
for i, line in enumerate(lines):
|
|
292
346
|
if pattern in line:
|
|
293
347
|
col = line.find(pattern)
|
|
@@ -297,82 +351,99 @@ class ASTMultiEdit(BaseTool):
|
|
|
297
351
|
line_end=i + 1,
|
|
298
352
|
column_start=col,
|
|
299
353
|
column_end=col + len(pattern),
|
|
300
|
-
node_type=
|
|
354
|
+
node_type="text",
|
|
301
355
|
text=pattern,
|
|
302
|
-
semantic_context=
|
|
356
|
+
semantic_context="text_match",
|
|
303
357
|
)
|
|
304
358
|
matches.append(match)
|
|
305
|
-
|
|
359
|
+
|
|
306
360
|
return matches
|
|
307
|
-
|
|
361
|
+
|
|
308
362
|
def _should_skip_file(self, file_path: str) -> bool:
|
|
309
363
|
"""Check if file should be skipped."""
|
|
310
|
-
skip_dirs = {
|
|
311
|
-
|
|
312
|
-
|
|
364
|
+
skip_dirs = {
|
|
365
|
+
".git",
|
|
366
|
+
"node_modules",
|
|
367
|
+
"__pycache__",
|
|
368
|
+
".pytest_cache",
|
|
369
|
+
"venv",
|
|
370
|
+
".env",
|
|
371
|
+
}
|
|
372
|
+
skip_extensions = {".pyc", ".pyo", ".so", ".dylib", ".dll", ".exe"}
|
|
373
|
+
|
|
313
374
|
path = Path(file_path)
|
|
314
|
-
|
|
375
|
+
|
|
315
376
|
# Check directories
|
|
316
377
|
for part in path.parts:
|
|
317
378
|
if part in skip_dirs:
|
|
318
379
|
return True
|
|
319
|
-
|
|
380
|
+
|
|
320
381
|
# Check extensions
|
|
321
382
|
if path.suffix in skip_extensions:
|
|
322
383
|
return True
|
|
323
|
-
|
|
384
|
+
|
|
324
385
|
# Check if binary
|
|
325
386
|
try:
|
|
326
|
-
with open(file_path,
|
|
387
|
+
with open(file_path, "rb") as f:
|
|
327
388
|
chunk = f.read(512)
|
|
328
|
-
if b
|
|
389
|
+
if b"\0" in chunk:
|
|
329
390
|
return True
|
|
330
|
-
except:
|
|
391
|
+
except Exception:
|
|
331
392
|
return True
|
|
332
|
-
|
|
393
|
+
|
|
333
394
|
return False
|
|
334
|
-
|
|
395
|
+
|
|
335
396
|
def _find_project_root(self, file_path: str) -> str:
|
|
336
397
|
"""Find project root by looking for markers."""
|
|
337
|
-
markers = {
|
|
338
|
-
|
|
398
|
+
markers = {
|
|
399
|
+
".git",
|
|
400
|
+
"package.json",
|
|
401
|
+
"go.mod",
|
|
402
|
+
"Cargo.toml",
|
|
403
|
+
"pyproject.toml",
|
|
404
|
+
"setup.py",
|
|
405
|
+
}
|
|
406
|
+
|
|
339
407
|
path = Path(file_path).resolve()
|
|
340
408
|
for parent in path.parents:
|
|
341
409
|
for marker in markers:
|
|
342
410
|
if (parent / marker).exists():
|
|
343
411
|
return str(parent)
|
|
344
|
-
|
|
412
|
+
|
|
345
413
|
return str(path.parent)
|
|
346
|
-
|
|
347
|
-
def _group_matches_by_file(
|
|
414
|
+
|
|
415
|
+
def _group_matches_by_file(
|
|
416
|
+
self, matches: List[ASTMatch]
|
|
417
|
+
) -> Dict[str, List[ASTMatch]]:
|
|
348
418
|
"""Group matches by file for efficient editing."""
|
|
349
419
|
grouped = defaultdict(list)
|
|
350
420
|
for match in matches:
|
|
351
421
|
grouped[match.file_path].append(match)
|
|
352
422
|
return grouped
|
|
353
|
-
|
|
354
|
-
def _create_unique_context(
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
context_lines: int) -> str:
|
|
423
|
+
|
|
424
|
+
def _create_unique_context(
|
|
425
|
+
self, content: str, match: ASTMatch, context_lines: int
|
|
426
|
+
) -> str:
|
|
358
427
|
"""Create unique context for edit identification."""
|
|
359
|
-
lines = content.split(
|
|
360
|
-
|
|
428
|
+
lines = content.split("\n")
|
|
429
|
+
|
|
361
430
|
start_line = max(0, match.line_start - context_lines - 1)
|
|
362
431
|
end_line = min(len(lines), match.line_end + context_lines)
|
|
363
|
-
|
|
432
|
+
|
|
364
433
|
context_lines = lines[start_line:end_line]
|
|
365
|
-
return
|
|
366
|
-
|
|
367
|
-
async def run(
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
434
|
+
return "\n".join(context_lines)
|
|
435
|
+
|
|
436
|
+
async def run(
|
|
437
|
+
self,
|
|
438
|
+
file_path: str,
|
|
439
|
+
edits: List[Dict[str, Any]],
|
|
440
|
+
find_references: bool = False,
|
|
441
|
+
page_size: int = 50,
|
|
442
|
+
preview_only: bool = False,
|
|
443
|
+
**kwargs,
|
|
444
|
+
) -> MCPResourceDocument:
|
|
374
445
|
"""Execute AST-aware multi-edit operation.
|
|
375
|
-
|
|
446
|
+
|
|
376
447
|
Args:
|
|
377
448
|
file_path: Primary file to edit
|
|
378
449
|
edits: List of edit operations
|
|
@@ -380,10 +451,10 @@ class ASTMultiEdit(BaseTool):
|
|
|
380
451
|
page_size: Number of results per page
|
|
381
452
|
preview_only: Show what would be changed without applying
|
|
382
453
|
"""
|
|
383
|
-
|
|
454
|
+
|
|
384
455
|
if not TREESITTER_AVAILABLE:
|
|
385
456
|
return self._fallback_to_basic_edit(file_path, edits)
|
|
386
|
-
|
|
457
|
+
|
|
387
458
|
results = {
|
|
388
459
|
"primary_file": file_path,
|
|
389
460
|
"edits_requested": len(edits),
|
|
@@ -391,31 +462,33 @@ class ASTMultiEdit(BaseTool):
|
|
|
391
462
|
"matches_found": 0,
|
|
392
463
|
"edits_applied": 0,
|
|
393
464
|
"errors": [],
|
|
394
|
-
"changes": []
|
|
465
|
+
"changes": [],
|
|
395
466
|
}
|
|
396
|
-
|
|
467
|
+
|
|
397
468
|
# Convert edits to EditOperation objects
|
|
398
469
|
edit_ops = []
|
|
399
470
|
for edit in edits:
|
|
400
|
-
edit_ops.append(
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
471
|
+
edit_ops.append(
|
|
472
|
+
EditOperation(
|
|
473
|
+
old_string=edit["old_string"],
|
|
474
|
+
new_string=edit["new_string"],
|
|
475
|
+
node_types=edit.get("node_types"),
|
|
476
|
+
semantic_match=edit.get("semantic_match", False),
|
|
477
|
+
expect_count=edit.get("expect_count"),
|
|
478
|
+
context_lines=edit.get("context_lines", 5),
|
|
479
|
+
)
|
|
480
|
+
)
|
|
481
|
+
|
|
409
482
|
# Find all matches
|
|
410
483
|
all_matches = []
|
|
411
|
-
|
|
484
|
+
|
|
412
485
|
# First, analyze primary file
|
|
413
486
|
try:
|
|
414
|
-
with open(file_path,
|
|
487
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
415
488
|
content = f.read()
|
|
416
|
-
|
|
489
|
+
|
|
417
490
|
tree = self._parse_file(file_path, content)
|
|
418
|
-
|
|
491
|
+
|
|
419
492
|
for edit_op in edit_ops:
|
|
420
493
|
if edit_op.semantic_match and find_references:
|
|
421
494
|
# Find all references across codebase
|
|
@@ -426,132 +499,139 @@ class ASTMultiEdit(BaseTool):
|
|
|
426
499
|
pattern = {"query": edit_op.old_string, "type": "text"}
|
|
427
500
|
matches = self._query_ast(tree, pattern, file_path, content)
|
|
428
501
|
else:
|
|
429
|
-
matches = self._text_search(
|
|
430
|
-
|
|
502
|
+
matches = self._text_search(
|
|
503
|
+
content, edit_op.old_string, file_path
|
|
504
|
+
)
|
|
505
|
+
|
|
431
506
|
# Filter by node types if specified
|
|
432
507
|
if edit_op.node_types:
|
|
433
508
|
matches = [m for m in matches if m.node_type in edit_op.node_types]
|
|
434
|
-
|
|
509
|
+
|
|
435
510
|
# Check expected count
|
|
436
|
-
if
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
511
|
+
if (
|
|
512
|
+
edit_op.expect_count is not None
|
|
513
|
+
and len(matches) != edit_op.expect_count
|
|
514
|
+
):
|
|
515
|
+
results["errors"].append(
|
|
516
|
+
{
|
|
517
|
+
"edit": edit_op.old_string,
|
|
518
|
+
"expected": edit_op.expect_count,
|
|
519
|
+
"found": len(matches),
|
|
520
|
+
"locations": [
|
|
521
|
+
f"{m.file_path}:{m.line_start}" for m in matches[:5]
|
|
522
|
+
],
|
|
523
|
+
}
|
|
524
|
+
)
|
|
443
525
|
continue
|
|
444
|
-
|
|
526
|
+
|
|
445
527
|
all_matches.extend([(edit_op, match) for match in matches])
|
|
446
|
-
|
|
528
|
+
|
|
447
529
|
except Exception as e:
|
|
448
|
-
results["errors"].append({
|
|
449
|
-
"file": file_path,
|
|
450
|
-
"error": str(e)
|
|
451
|
-
})
|
|
530
|
+
results["errors"].append({"file": file_path, "error": str(e)})
|
|
452
531
|
return MCPResourceDocument(data=results)
|
|
453
|
-
|
|
532
|
+
|
|
454
533
|
results["matches_found"] = len(all_matches)
|
|
455
534
|
results["files_analyzed"] = len(set(m[1].file_path for m in all_matches))
|
|
456
|
-
|
|
535
|
+
|
|
457
536
|
if preview_only:
|
|
458
537
|
# Return preview of changes
|
|
459
538
|
preview = self._generate_preview(all_matches, page_size)
|
|
460
539
|
results["preview"] = preview
|
|
461
540
|
return MCPResourceDocument(data=results)
|
|
462
|
-
|
|
541
|
+
|
|
463
542
|
# Apply edits
|
|
464
543
|
changes_by_file = self._group_changes(all_matches)
|
|
465
|
-
|
|
544
|
+
|
|
466
545
|
for file_path, changes in changes_by_file.items():
|
|
467
546
|
try:
|
|
468
547
|
success = await self._apply_file_changes(file_path, changes)
|
|
469
548
|
if success:
|
|
470
549
|
results["edits_applied"] += len(changes)
|
|
471
|
-
results["changes"].append(
|
|
472
|
-
"file": file_path,
|
|
473
|
-
|
|
474
|
-
})
|
|
550
|
+
results["changes"].append(
|
|
551
|
+
{"file": file_path, "edits": len(changes)}
|
|
552
|
+
)
|
|
475
553
|
except Exception as e:
|
|
476
|
-
results["errors"].append({
|
|
477
|
-
|
|
478
|
-
"error": str(e)
|
|
479
|
-
})
|
|
480
|
-
|
|
554
|
+
results["errors"].append({"file": file_path, "error": str(e)})
|
|
555
|
+
|
|
481
556
|
return MCPResourceDocument(data=results)
|
|
482
|
-
|
|
483
|
-
def _group_changes(
|
|
557
|
+
|
|
558
|
+
def _group_changes(
|
|
559
|
+
self, matches: List[Tuple[EditOperation, ASTMatch]]
|
|
560
|
+
) -> Dict[str, List[Tuple[EditOperation, ASTMatch]]]:
|
|
484
561
|
"""Group changes by file."""
|
|
485
562
|
grouped = defaultdict(list)
|
|
486
563
|
for edit_op, match in matches:
|
|
487
564
|
grouped[match.file_path].append((edit_op, match))
|
|
488
565
|
return grouped
|
|
489
|
-
|
|
490
|
-
async def _apply_file_changes(
|
|
491
|
-
|
|
492
|
-
|
|
566
|
+
|
|
567
|
+
async def _apply_file_changes(
|
|
568
|
+
self, file_path: str, changes: List[Tuple[EditOperation, ASTMatch]]
|
|
569
|
+
) -> bool:
|
|
493
570
|
"""Apply changes to a single file."""
|
|
494
|
-
with open(file_path,
|
|
571
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
495
572
|
content = f.read()
|
|
496
|
-
|
|
573
|
+
|
|
497
574
|
# Sort changes by position (reverse order to maintain positions)
|
|
498
575
|
changes.sort(key=lambda x: (x[1].line_start, x[1].column_start), reverse=True)
|
|
499
|
-
|
|
500
|
-
lines = content.split(
|
|
501
|
-
|
|
576
|
+
|
|
577
|
+
lines = content.split("\n")
|
|
578
|
+
|
|
502
579
|
for edit_op, match in changes:
|
|
503
580
|
# Create unique context for this match
|
|
504
581
|
context = self._create_unique_context(content, match, edit_op.context_lines)
|
|
505
|
-
|
|
582
|
+
|
|
506
583
|
# Apply the edit
|
|
507
584
|
if match.line_start == match.line_end:
|
|
508
585
|
# Single line edit
|
|
509
586
|
line = lines[match.line_start - 1]
|
|
510
|
-
before = line[:match.column_start]
|
|
511
|
-
after = line[match.column_end:]
|
|
587
|
+
before = line[: match.column_start]
|
|
588
|
+
after = line[match.column_end :]
|
|
512
589
|
lines[match.line_start - 1] = before + edit_op.new_string + after
|
|
513
590
|
else:
|
|
514
591
|
# Multi-line edit
|
|
515
592
|
# Remove old lines
|
|
516
|
-
del lines[match.line_start - 1:match.line_end]
|
|
593
|
+
del lines[match.line_start - 1 : match.line_end]
|
|
517
594
|
# Insert new content
|
|
518
595
|
lines.insert(match.line_start - 1, edit_op.new_string)
|
|
519
|
-
|
|
596
|
+
|
|
520
597
|
# Write back
|
|
521
|
-
with open(file_path,
|
|
522
|
-
f.write(
|
|
523
|
-
|
|
598
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
599
|
+
f.write("\n".join(lines))
|
|
600
|
+
|
|
524
601
|
return True
|
|
525
|
-
|
|
526
|
-
def _generate_preview(
|
|
527
|
-
|
|
528
|
-
|
|
602
|
+
|
|
603
|
+
def _generate_preview(
|
|
604
|
+
self, matches: List[Tuple[EditOperation, ASTMatch]], page_size: int
|
|
605
|
+
) -> List[Dict[str, Any]]:
|
|
529
606
|
"""Generate preview of changes."""
|
|
530
607
|
preview = []
|
|
531
|
-
|
|
532
|
-
for
|
|
533
|
-
preview.append(
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
608
|
+
|
|
609
|
+
for _i, (edit_op, match) in enumerate(matches[:page_size]):
|
|
610
|
+
preview.append(
|
|
611
|
+
{
|
|
612
|
+
"file": match.file_path,
|
|
613
|
+
"line": match.line_start,
|
|
614
|
+
"column": match.column_start,
|
|
615
|
+
"node_type": match.node_type,
|
|
616
|
+
"context": match.parent_context,
|
|
617
|
+
"old": edit_op.old_string,
|
|
618
|
+
"new": edit_op.new_string,
|
|
619
|
+
"semantic_type": match.semantic_context,
|
|
620
|
+
}
|
|
621
|
+
)
|
|
622
|
+
|
|
544
623
|
if len(matches) > page_size:
|
|
545
|
-
preview.append({
|
|
546
|
-
|
|
547
|
-
})
|
|
548
|
-
|
|
624
|
+
preview.append({"note": f"... and {len(matches) - page_size} more matches"})
|
|
625
|
+
|
|
549
626
|
return preview
|
|
550
|
-
|
|
551
|
-
def _fallback_to_basic_edit(
|
|
627
|
+
|
|
628
|
+
def _fallback_to_basic_edit(
|
|
629
|
+
self, file_path: str, edits: List[Dict[str, Any]]
|
|
630
|
+
) -> MCPResourceDocument:
|
|
552
631
|
"""Fallback to basic multi-edit when treesitter not available."""
|
|
553
632
|
# Delegate to existing multi_edit tool
|
|
554
633
|
from hanzo_mcp.tools.filesystem.multi_edit import MultiEdit
|
|
634
|
+
|
|
555
635
|
basic_tool = MultiEdit()
|
|
556
636
|
return basic_tool.run(file_path, edits)
|
|
557
637
|
|
|
@@ -559,4 +639,4 @@ class ASTMultiEdit(BaseTool):
|
|
|
559
639
|
# Tool registration
|
|
560
640
|
def create_ast_multi_edit_tool():
|
|
561
641
|
"""Factory function to create AST multi-edit tool."""
|
|
562
|
-
return ASTMultiEdit()
|
|
642
|
+
return ASTMultiEdit()
|