hanzo-mcp 0.7.7__py3-none-any.whl → 0.8.1__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.

Files changed (178) hide show
  1. hanzo_mcp/__init__.py +6 -0
  2. hanzo_mcp/__main__.py +1 -1
  3. hanzo_mcp/analytics/__init__.py +2 -2
  4. hanzo_mcp/analytics/posthog_analytics.py +76 -82
  5. hanzo_mcp/cli.py +31 -36
  6. hanzo_mcp/cli_enhanced.py +94 -72
  7. hanzo_mcp/cli_plugin.py +27 -17
  8. hanzo_mcp/config/__init__.py +2 -2
  9. hanzo_mcp/config/settings.py +112 -88
  10. hanzo_mcp/config/tool_config.py +32 -34
  11. hanzo_mcp/dev_server.py +66 -67
  12. hanzo_mcp/prompts/__init__.py +94 -12
  13. hanzo_mcp/prompts/enhanced_prompts.py +809 -0
  14. hanzo_mcp/prompts/example_custom_prompt.py +6 -5
  15. hanzo_mcp/prompts/project_todo_reminder.py +0 -1
  16. hanzo_mcp/prompts/tool_explorer.py +10 -7
  17. hanzo_mcp/server.py +17 -21
  18. hanzo_mcp/server_enhanced.py +15 -22
  19. hanzo_mcp/tools/__init__.py +56 -28
  20. hanzo_mcp/tools/agent/__init__.py +16 -19
  21. hanzo_mcp/tools/agent/agent.py +82 -65
  22. hanzo_mcp/tools/agent/agent_tool.py +152 -122
  23. hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +66 -62
  24. hanzo_mcp/tools/agent/clarification_protocol.py +55 -50
  25. hanzo_mcp/tools/agent/clarification_tool.py +11 -10
  26. hanzo_mcp/tools/agent/claude_cli_tool.py +21 -20
  27. hanzo_mcp/tools/agent/claude_desktop_auth.py +130 -144
  28. hanzo_mcp/tools/agent/cli_agent_base.py +59 -53
  29. hanzo_mcp/tools/agent/code_auth.py +102 -107
  30. hanzo_mcp/tools/agent/code_auth_tool.py +28 -27
  31. hanzo_mcp/tools/agent/codex_cli_tool.py +20 -19
  32. hanzo_mcp/tools/agent/critic_tool.py +86 -73
  33. hanzo_mcp/tools/agent/gemini_cli_tool.py +21 -20
  34. hanzo_mcp/tools/agent/grok_cli_tool.py +21 -20
  35. hanzo_mcp/tools/agent/iching_tool.py +404 -139
  36. hanzo_mcp/tools/agent/network_tool.py +89 -73
  37. hanzo_mcp/tools/agent/prompt.py +2 -1
  38. hanzo_mcp/tools/agent/review_tool.py +101 -98
  39. hanzo_mcp/tools/agent/swarm_alias.py +87 -0
  40. hanzo_mcp/tools/agent/swarm_tool.py +246 -161
  41. hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +134 -92
  42. hanzo_mcp/tools/agent/tool_adapter.py +21 -11
  43. hanzo_mcp/tools/common/__init__.py +1 -1
  44. hanzo_mcp/tools/common/base.py +3 -5
  45. hanzo_mcp/tools/common/batch_tool.py +46 -39
  46. hanzo_mcp/tools/common/config_tool.py +120 -84
  47. hanzo_mcp/tools/common/context.py +1 -5
  48. hanzo_mcp/tools/common/context_fix.py +5 -3
  49. hanzo_mcp/tools/common/critic_tool.py +4 -8
  50. hanzo_mcp/tools/common/decorators.py +58 -56
  51. hanzo_mcp/tools/common/enhanced_base.py +29 -32
  52. hanzo_mcp/tools/common/fastmcp_pagination.py +91 -94
  53. hanzo_mcp/tools/common/forgiving_edit.py +91 -87
  54. hanzo_mcp/tools/common/mode.py +15 -17
  55. hanzo_mcp/tools/common/mode_loader.py +27 -24
  56. hanzo_mcp/tools/common/paginated_base.py +61 -53
  57. hanzo_mcp/tools/common/paginated_response.py +72 -79
  58. hanzo_mcp/tools/common/pagination.py +50 -53
  59. hanzo_mcp/tools/common/permissions.py +4 -4
  60. hanzo_mcp/tools/common/personality.py +186 -138
  61. hanzo_mcp/tools/common/plugin_loader.py +54 -54
  62. hanzo_mcp/tools/common/stats.py +65 -47
  63. hanzo_mcp/tools/common/test_helpers.py +31 -0
  64. hanzo_mcp/tools/common/thinking_tool.py +4 -8
  65. hanzo_mcp/tools/common/tool_disable.py +17 -12
  66. hanzo_mcp/tools/common/tool_enable.py +13 -14
  67. hanzo_mcp/tools/common/tool_list.py +36 -28
  68. hanzo_mcp/tools/common/truncate.py +23 -23
  69. hanzo_mcp/tools/config/__init__.py +4 -4
  70. hanzo_mcp/tools/config/config_tool.py +42 -29
  71. hanzo_mcp/tools/config/index_config.py +37 -34
  72. hanzo_mcp/tools/config/mode_tool.py +175 -55
  73. hanzo_mcp/tools/database/__init__.py +15 -12
  74. hanzo_mcp/tools/database/database_manager.py +77 -75
  75. hanzo_mcp/tools/database/graph.py +137 -91
  76. hanzo_mcp/tools/database/graph_add.py +30 -18
  77. hanzo_mcp/tools/database/graph_query.py +178 -102
  78. hanzo_mcp/tools/database/graph_remove.py +33 -28
  79. hanzo_mcp/tools/database/graph_search.py +97 -75
  80. hanzo_mcp/tools/database/graph_stats.py +91 -59
  81. hanzo_mcp/tools/database/sql.py +107 -79
  82. hanzo_mcp/tools/database/sql_query.py +30 -24
  83. hanzo_mcp/tools/database/sql_search.py +29 -25
  84. hanzo_mcp/tools/database/sql_stats.py +47 -35
  85. hanzo_mcp/tools/editor/neovim_command.py +25 -28
  86. hanzo_mcp/tools/editor/neovim_edit.py +21 -23
  87. hanzo_mcp/tools/editor/neovim_session.py +60 -54
  88. hanzo_mcp/tools/filesystem/__init__.py +31 -30
  89. hanzo_mcp/tools/filesystem/ast_multi_edit.py +329 -249
  90. hanzo_mcp/tools/filesystem/ast_tool.py +4 -4
  91. hanzo_mcp/tools/filesystem/base.py +1 -1
  92. hanzo_mcp/tools/filesystem/batch_search.py +316 -224
  93. hanzo_mcp/tools/filesystem/content_replace.py +4 -4
  94. hanzo_mcp/tools/filesystem/diff.py +71 -59
  95. hanzo_mcp/tools/filesystem/directory_tree.py +7 -7
  96. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +49 -37
  97. hanzo_mcp/tools/filesystem/edit.py +4 -4
  98. hanzo_mcp/tools/filesystem/find.py +173 -80
  99. hanzo_mcp/tools/filesystem/find_files.py +73 -52
  100. hanzo_mcp/tools/filesystem/git_search.py +157 -104
  101. hanzo_mcp/tools/filesystem/grep.py +8 -8
  102. hanzo_mcp/tools/filesystem/multi_edit.py +4 -8
  103. hanzo_mcp/tools/filesystem/read.py +12 -10
  104. hanzo_mcp/tools/filesystem/rules_tool.py +59 -43
  105. hanzo_mcp/tools/filesystem/search_tool.py +263 -207
  106. hanzo_mcp/tools/filesystem/symbols_tool.py +94 -54
  107. hanzo_mcp/tools/filesystem/tree.py +35 -33
  108. hanzo_mcp/tools/filesystem/unix_aliases.py +13 -18
  109. hanzo_mcp/tools/filesystem/watch.py +37 -36
  110. hanzo_mcp/tools/filesystem/write.py +4 -8
  111. hanzo_mcp/tools/jupyter/__init__.py +4 -4
  112. hanzo_mcp/tools/jupyter/base.py +4 -5
  113. hanzo_mcp/tools/jupyter/jupyter.py +67 -47
  114. hanzo_mcp/tools/jupyter/notebook_edit.py +4 -4
  115. hanzo_mcp/tools/jupyter/notebook_read.py +4 -7
  116. hanzo_mcp/tools/llm/__init__.py +5 -7
  117. hanzo_mcp/tools/llm/consensus_tool.py +72 -52
  118. hanzo_mcp/tools/llm/llm_manage.py +101 -60
  119. hanzo_mcp/tools/llm/llm_tool.py +226 -166
  120. hanzo_mcp/tools/llm/provider_tools.py +25 -26
  121. hanzo_mcp/tools/lsp/__init__.py +1 -1
  122. hanzo_mcp/tools/lsp/lsp_tool.py +228 -143
  123. hanzo_mcp/tools/mcp/__init__.py +2 -3
  124. hanzo_mcp/tools/mcp/mcp_add.py +27 -25
  125. hanzo_mcp/tools/mcp/mcp_remove.py +7 -8
  126. hanzo_mcp/tools/mcp/mcp_stats.py +23 -22
  127. hanzo_mcp/tools/mcp/mcp_tool.py +129 -98
  128. hanzo_mcp/tools/memory/__init__.py +39 -21
  129. hanzo_mcp/tools/memory/knowledge_tools.py +124 -99
  130. hanzo_mcp/tools/memory/memory_tools.py +90 -108
  131. hanzo_mcp/tools/search/__init__.py +7 -2
  132. hanzo_mcp/tools/search/find_tool.py +297 -212
  133. hanzo_mcp/tools/search/unified_search.py +366 -314
  134. hanzo_mcp/tools/shell/__init__.py +8 -7
  135. hanzo_mcp/tools/shell/auto_background.py +56 -49
  136. hanzo_mcp/tools/shell/base.py +1 -1
  137. hanzo_mcp/tools/shell/base_process.py +75 -75
  138. hanzo_mcp/tools/shell/bash_session.py +2 -2
  139. hanzo_mcp/tools/shell/bash_session_executor.py +4 -4
  140. hanzo_mcp/tools/shell/bash_tool.py +24 -31
  141. hanzo_mcp/tools/shell/command_executor.py +12 -12
  142. hanzo_mcp/tools/shell/logs.py +43 -33
  143. hanzo_mcp/tools/shell/npx.py +13 -13
  144. hanzo_mcp/tools/shell/npx_background.py +24 -21
  145. hanzo_mcp/tools/shell/npx_tool.py +18 -22
  146. hanzo_mcp/tools/shell/open.py +19 -21
  147. hanzo_mcp/tools/shell/pkill.py +31 -26
  148. hanzo_mcp/tools/shell/process_tool.py +32 -32
  149. hanzo_mcp/tools/shell/processes.py +57 -58
  150. hanzo_mcp/tools/shell/run_background.py +24 -25
  151. hanzo_mcp/tools/shell/run_command.py +5 -5
  152. hanzo_mcp/tools/shell/run_command_windows.py +5 -5
  153. hanzo_mcp/tools/shell/session_storage.py +3 -3
  154. hanzo_mcp/tools/shell/streaming_command.py +141 -126
  155. hanzo_mcp/tools/shell/uvx.py +24 -25
  156. hanzo_mcp/tools/shell/uvx_background.py +35 -33
  157. hanzo_mcp/tools/shell/uvx_tool.py +18 -22
  158. hanzo_mcp/tools/todo/__init__.py +6 -2
  159. hanzo_mcp/tools/todo/todo.py +50 -37
  160. hanzo_mcp/tools/todo/todo_read.py +5 -8
  161. hanzo_mcp/tools/todo/todo_write.py +5 -7
  162. hanzo_mcp/tools/vector/__init__.py +40 -28
  163. hanzo_mcp/tools/vector/ast_analyzer.py +176 -143
  164. hanzo_mcp/tools/vector/git_ingester.py +170 -179
  165. hanzo_mcp/tools/vector/index_tool.py +96 -44
  166. hanzo_mcp/tools/vector/infinity_store.py +283 -228
  167. hanzo_mcp/tools/vector/mock_infinity.py +39 -40
  168. hanzo_mcp/tools/vector/project_manager.py +88 -78
  169. hanzo_mcp/tools/vector/vector.py +59 -42
  170. hanzo_mcp/tools/vector/vector_index.py +30 -27
  171. hanzo_mcp/tools/vector/vector_search.py +64 -45
  172. hanzo_mcp/types.py +6 -4
  173. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/METADATA +1 -1
  174. hanzo_mcp-0.8.1.dist-info/RECORD +185 -0
  175. hanzo_mcp-0.7.7.dist-info/RECORD +0 -182
  176. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/WHEEL +0 -0
  177. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/entry_points.txt +0 -0
  178. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.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 os
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
- import tree_sitter_go
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
- '.py': (tree_sitter_python, 'python'),
92
- '.js': (tree_sitter_javascript, 'javascript'),
93
- '.jsx': (tree_sitter_javascript, 'javascript'),
94
- '.ts': (tree_sitter_typescript.typescript, 'typescript'),
95
- '.tsx': (tree_sitter_typescript.tsx, 'tsx'),
96
- '.go': (tree_sitter_go, 'go'),
97
- '.rs': (tree_sitter_rust, 'rust'),
98
- '.java': (tree_sitter_java, 'java'),
99
- '.cpp': (tree_sitter_cpp, 'cpp'),
100
- '.cc': (tree_sitter_cpp, 'cpp'),
101
- '.cxx': (tree_sitter_cpp, 'cpp'),
102
- '.h': (tree_sitter_cpp, 'cpp'),
103
- '.hpp': (tree_sitter_cpp, 'cpp'),
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, 'language'):
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, 'utf-8'))
132
-
133
- def _find_references(self,
134
- symbol: str,
135
- file_path: str,
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(self, symbol: str, file_path: str) -> List[Dict[str, Any]]:
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, 'generic')
158
-
157
+ lang = self.languages.get(ext, "generic")
158
+
159
159
  patterns = []
160
-
161
- if lang == 'go':
160
+
161
+ if lang == "go":
162
162
  # Go specific patterns
163
- patterns.extend([
164
- # Function calls
165
- {"query": f"(call_expression function: (identifier) @func (#eq? @func \"{symbol}\"))", "type": "call"},
166
- # Method calls
167
- {"query": f"(call_expression function: (selector_expression field: (field_identifier) @method (#eq? @method \"{symbol}\")))", "type": "method_call"},
168
- # Function declarations
169
- {"query": f"(function_declaration name: (identifier) @name (#eq? @name \"{symbol}\"))", "type": "declaration"},
170
- # Type references
171
- {"query": f"(type_identifier) @type (#eq? @type \"{symbol}\")", "type": "type_ref"},
172
- ])
173
- elif lang in ['javascript', 'typescript', 'tsx']:
174
- patterns.extend([
175
- # Function calls
176
- {"query": f"(call_expression function: (identifier) @func (#eq? @func \"{symbol}\"))", "type": "call"},
177
- # Method calls
178
- {"query": f"(call_expression function: (member_expression property: (property_identifier) @prop (#eq? @prop \"{symbol}\")))", "type": "method_call"},
179
- # Function declarations
180
- {"query": f"(function_declaration name: (identifier) @name (#eq? @name \"{symbol}\"))", "type": "declaration"},
181
- # Variable declarations
182
- {"query": f"(variable_declarator name: (identifier) @var (#eq? @var \"{symbol}\"))", "type": "variable"},
183
- ])
184
- elif lang == 'python':
185
- patterns.extend([
186
- # Function calls
187
- {"query": f"(call function: (identifier) @func (#eq? @func \"{symbol}\"))", "type": "call"},
188
- # Method calls
189
- {"query": f"(call function: (attribute attribute: (identifier) @attr (#eq? @attr \"{symbol}\")))", "type": "method_call"},
190
- # Function definitions
191
- {"query": f"(function_definition name: (identifier) @name (#eq? @name \"{symbol}\"))", "type": "declaration"},
192
- # Class definitions
193
- {"query": f"(class_definition name: (identifier) @name (#eq? @name \"{symbol}\"))", "type": "class"},
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, 'r', encoding='utf-8') as f:
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(self._text_search(content, pattern["query"], file_path))
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(self,
231
- tree: tree_sitter.Tree,
232
- pattern: Dict[str, Any],
233
- file_path: str,
234
- content: str) -> List[ASTMatch]:
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('\n')
249
-
250
- for node, name in captures:
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 as e:
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(self, node: tree_sitter.Node, content: str) -> Optional[str]:
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 ['function_declaration', 'function_definition', 'method_definition']:
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 == 'identifier':
278
- return f"function: {content[child.start_byte:child.end_byte]}"
279
- elif parent.type in ['class_declaration', 'class_definition']:
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 == 'identifier':
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(self, content: str, pattern: str, file_path: str) -> List[ASTMatch]:
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('\n')
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='text',
354
+ node_type="text",
301
355
  text=pattern,
302
- semantic_context='text_match'
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 = {'.git', 'node_modules', '__pycache__', '.pytest_cache', 'venv', '.env'}
311
- skip_extensions = {'.pyc', '.pyo', '.so', '.dylib', '.dll', '.exe'}
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, 'rb') as f:
387
+ with open(file_path, "rb") as f:
327
388
  chunk = f.read(512)
328
- if b'\0' in chunk:
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 = {'.git', 'package.json', 'go.mod', 'Cargo.toml', 'pyproject.toml', 'setup.py'}
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(self, matches: List[ASTMatch]) -> Dict[str, List[ASTMatch]]:
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(self,
355
- content: str,
356
- match: ASTMatch,
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('\n')
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 '\n'.join(context_lines)
366
-
367
- async def run(self,
368
- file_path: str,
369
- edits: List[Dict[str, Any]],
370
- find_references: bool = False,
371
- page_size: int = 50,
372
- preview_only: bool = False,
373
- **kwargs) -> MCPResourceDocument:
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(EditOperation(
401
- old_string=edit["old_string"],
402
- new_string=edit["new_string"],
403
- node_types=edit.get("node_types"),
404
- semantic_match=edit.get("semantic_match", False),
405
- expect_count=edit.get("expect_count"),
406
- context_lines=edit.get("context_lines", 5)
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, 'r', encoding='utf-8') as f:
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(content, edit_op.old_string, file_path)
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 edit_op.expect_count is not None and len(matches) != edit_op.expect_count:
437
- results["errors"].append({
438
- "edit": edit_op.old_string,
439
- "expected": edit_op.expect_count,
440
- "found": len(matches),
441
- "locations": [f"{m.file_path}:{m.line_start}" for m in matches[:5]]
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
- "edits": len(changes)
474
- })
550
+ results["changes"].append(
551
+ {"file": file_path, "edits": len(changes)}
552
+ )
475
553
  except Exception as e:
476
- results["errors"].append({
477
- "file": file_path,
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(self, matches: List[Tuple[EditOperation, ASTMatch]]) -> Dict[str, List[Tuple[EditOperation, ASTMatch]]]:
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(self,
491
- file_path: str,
492
- changes: List[Tuple[EditOperation, ASTMatch]]) -> bool:
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, 'r', encoding='utf-8') as f:
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('\n')
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, 'w', encoding='utf-8') as f:
522
- f.write('\n'.join(lines))
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(self,
527
- matches: List[Tuple[EditOperation, ASTMatch]],
528
- page_size: int) -> List[Dict[str, Any]]:
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 i, (edit_op, match) in enumerate(matches[:page_size]):
533
- preview.append({
534
- "file": match.file_path,
535
- "line": match.line_start,
536
- "column": match.column_start,
537
- "node_type": match.node_type,
538
- "context": match.parent_context,
539
- "old": edit_op.old_string,
540
- "new": edit_op.new_string,
541
- "semantic_type": match.semantic_context
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
- "note": f"... and {len(matches) - page_size} more matches"
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(self, file_path: str, edits: List[Dict[str, Any]]) -> MCPResourceDocument:
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()