hanzo-mcp 0.7.7__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.

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.0.dist-info}/METADATA +1 -1
  174. hanzo_mcp-0.8.0.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.0.dist-info}/WHEEL +0 -0
  177. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.0.dist-info}/entry_points.txt +0 -0
  178. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.0.dist-info}/top_level.txt +0 -0
@@ -1,59 +1,57 @@
1
1
  """Forgiving edit helper for AI-friendly text matching."""
2
2
 
3
- import difflib
4
3
  import re
5
- from typing import List, Optional, Tuple
4
+ import difflib
5
+ from typing import List, Tuple, Optional
6
6
 
7
7
 
8
8
  class ForgivingEditHelper:
9
9
  """Helper class to make text editing more forgiving for AI usage.
10
-
10
+
11
11
  This helper normalizes whitespace, handles partial matches, and provides
12
12
  suggestions when exact matches fail.
13
13
  """
14
-
14
+
15
15
  @staticmethod
16
16
  def normalize_whitespace(text: str) -> str:
17
17
  """Normalize whitespace while preserving structure.
18
-
18
+
19
19
  Args:
20
20
  text: Text to normalize
21
-
21
+
22
22
  Returns:
23
23
  Text with normalized whitespace
24
24
  """
25
25
  # Handle the input line by line
26
26
  lines = []
27
- for line in text.split('\n'):
27
+ for line in text.split("\n"):
28
28
  # Replace tabs with 4 spaces everywhere in the line
29
- line = line.replace('\t', ' ')
30
-
29
+ line = line.replace("\t", " ")
30
+
31
31
  # Split into indentation and content
32
32
  stripped = line.lstrip()
33
- indent = line[:len(line) - len(stripped)]
34
-
33
+ indent = line[: len(line) - len(stripped)]
34
+
35
35
  if stripped:
36
36
  # For content, normalize multiple spaces to single space
37
- content = re.sub(r' {2,}', ' ', stripped)
37
+ content = re.sub(r" {2,}", " ", stripped)
38
38
  lines.append(indent + content)
39
39
  else:
40
40
  lines.append(indent)
41
-
42
- return '\n'.join(lines)
43
-
41
+
42
+ return "\n".join(lines)
43
+
44
44
  @staticmethod
45
45
  def find_fuzzy_match(
46
- haystack: str,
47
- needle: str,
48
- threshold: float = 0.85
46
+ haystack: str, needle: str, threshold: float = 0.85
49
47
  ) -> Optional[Tuple[int, int, str]]:
50
48
  """Find a fuzzy match for the needle in the haystack.
51
-
49
+
52
50
  Args:
53
51
  haystack: Text to search in
54
52
  needle: Text to search for
55
53
  threshold: Similarity threshold (0-1)
56
-
54
+
57
55
  Returns:
58
56
  Tuple of (start_pos, end_pos, matched_text) or None
59
57
  """
@@ -61,150 +59,156 @@ class ForgivingEditHelper:
61
59
  if needle in haystack:
62
60
  start = haystack.index(needle)
63
61
  return (start, start + len(needle), needle)
64
-
62
+
65
63
  # Normalize for comparison
66
64
  norm_haystack = ForgivingEditHelper.normalize_whitespace(haystack)
67
65
  norm_needle = ForgivingEditHelper.normalize_whitespace(needle)
68
-
66
+
69
67
  # Try normalized exact match
70
68
  if norm_needle in norm_haystack:
71
69
  # Find the match in normalized text
72
70
  norm_start = norm_haystack.index(norm_needle)
73
-
71
+
74
72
  # Map back to original text
75
73
  # This is approximate but usually good enough
76
- lines_before = norm_haystack[:norm_start].count('\n')
77
-
74
+ lines_before = norm_haystack[:norm_start].count("\n")
75
+
78
76
  # Find corresponding position in original
79
- original_lines = haystack.split('\n')
80
- norm_lines = norm_haystack.split('\n')
81
-
77
+ original_lines = haystack.split("\n")
78
+ norm_lines = norm_haystack.split("\n")
79
+
82
80
  start_pos = sum(len(line) + 1 for line in original_lines[:lines_before])
83
-
81
+
84
82
  # Find end position by counting lines in needle
85
- needle_lines = norm_needle.count('\n') + 1
86
- end_pos = sum(len(line) + 1 for line in original_lines[:lines_before + needle_lines])
87
-
88
- matched = '\n'.join(original_lines[lines_before:lines_before + needle_lines])
83
+ needle_lines = norm_needle.count("\n") + 1
84
+ end_pos = sum(
85
+ len(line) + 1 for line in original_lines[: lines_before + needle_lines]
86
+ )
87
+
88
+ matched = "\n".join(
89
+ original_lines[lines_before : lines_before + needle_lines]
90
+ )
89
91
  return (start_pos, end_pos - 1, matched)
90
-
92
+
91
93
  # Try fuzzy matching on lines
92
- haystack_lines = haystack.split('\n')
93
- needle_lines = needle.split('\n')
94
-
94
+ haystack_lines = haystack.split("\n")
95
+ needle_lines = needle.split("\n")
96
+
95
97
  if len(needle_lines) == 1:
96
98
  # Single line - find best match
97
99
  needle_norm = ForgivingEditHelper.normalize_whitespace(needle)
98
100
  best_ratio = 0
99
101
  best_match = None
100
-
102
+
101
103
  for i, line in enumerate(haystack_lines):
102
104
  line_norm = ForgivingEditHelper.normalize_whitespace(line)
103
105
  ratio = difflib.SequenceMatcher(None, line_norm, needle_norm).ratio()
104
-
106
+
105
107
  if ratio > best_ratio and ratio >= threshold:
106
108
  best_ratio = ratio
107
109
  start_pos = sum(len(l) + 1 for l in haystack_lines[:i])
108
110
  best_match = (start_pos, start_pos + len(line), line)
109
-
111
+
110
112
  return best_match
111
-
113
+
112
114
  else:
113
115
  # Multi-line - find sequence match
114
116
  for i in range(len(haystack_lines) - len(needle_lines) + 1):
115
- candidate_lines = haystack_lines[i:i + len(needle_lines)]
116
- candidate = '\n'.join(candidate_lines)
117
+ candidate_lines = haystack_lines[i : i + len(needle_lines)]
118
+ candidate = "\n".join(candidate_lines)
117
119
  candidate_norm = ForgivingEditHelper.normalize_whitespace(candidate)
118
120
  needle_norm = ForgivingEditHelper.normalize_whitespace(needle)
119
-
120
- ratio = difflib.SequenceMatcher(None, candidate_norm, needle_norm).ratio()
121
-
121
+
122
+ ratio = difflib.SequenceMatcher(
123
+ None, candidate_norm, needle_norm
124
+ ).ratio()
125
+
122
126
  if ratio >= threshold:
123
127
  start_pos = sum(len(l) + 1 for l in haystack_lines[:i])
124
128
  return (start_pos, start_pos + len(candidate), candidate)
125
-
129
+
126
130
  return None
127
-
131
+
128
132
  @staticmethod
129
133
  def suggest_matches(
130
- haystack: str,
131
- needle: str,
132
- max_suggestions: int = 3
134
+ haystack: str, needle: str, max_suggestions: int = 3
133
135
  ) -> List[Tuple[float, str]]:
134
136
  """Suggest possible matches when exact match fails.
135
-
137
+
136
138
  Args:
137
139
  haystack: Text to search in
138
140
  needle: Text to search for
139
141
  max_suggestions: Maximum number of suggestions
140
-
142
+
141
143
  Returns:
142
144
  List of (similarity_score, text) tuples
143
145
  """
144
146
  suggestions = []
145
-
147
+
146
148
  # Normalize needle
147
149
  needle_norm = ForgivingEditHelper.normalize_whitespace(needle)
148
- needle_lines = needle.split('\n')
149
-
150
+ needle_lines = needle.split("\n")
151
+
150
152
  if len(needle_lines) == 1:
151
153
  # Single line - compare with all lines
152
- for line in haystack.split('\n'):
154
+ for line in haystack.split("\n"):
153
155
  if line.strip(): # Skip empty lines
154
156
  line_norm = ForgivingEditHelper.normalize_whitespace(line)
155
- ratio = difflib.SequenceMatcher(None, line_norm, needle_norm).ratio()
157
+ ratio = difflib.SequenceMatcher(
158
+ None, line_norm, needle_norm
159
+ ).ratio()
156
160
  if ratio > 0.5: # Only reasonably similar lines
157
161
  suggestions.append((ratio, line))
158
-
162
+
159
163
  else:
160
164
  # Multi-line - use sliding window
161
- haystack_lines = haystack.split('\n')
165
+ haystack_lines = haystack.split("\n")
162
166
  window_size = len(needle_lines)
163
-
167
+
164
168
  for i in range(len(haystack_lines) - window_size + 1):
165
- candidate_lines = haystack_lines[i:i + window_size]
166
- candidate = '\n'.join(candidate_lines)
169
+ candidate_lines = haystack_lines[i : i + window_size]
170
+ candidate = "\n".join(candidate_lines)
167
171
  candidate_norm = ForgivingEditHelper.normalize_whitespace(candidate)
168
-
169
- ratio = difflib.SequenceMatcher(None, candidate_norm, needle_norm).ratio()
172
+
173
+ ratio = difflib.SequenceMatcher(
174
+ None, candidate_norm, needle_norm
175
+ ).ratio()
170
176
  if ratio > 0.5:
171
177
  suggestions.append((ratio, candidate))
172
-
178
+
173
179
  # Sort by similarity and return top matches
174
180
  suggestions.sort(reverse=True, key=lambda x: x[0])
175
181
  return suggestions[:max_suggestions]
176
-
182
+
177
183
  @staticmethod
178
184
  def create_edit_suggestion(
179
- file_content: str,
180
- old_string: str,
181
- new_string: str
185
+ file_content: str, old_string: str, new_string: str
182
186
  ) -> dict:
183
187
  """Create a helpful edit suggestion when match fails.
184
-
188
+
185
189
  Args:
186
190
  file_content: Current file content
187
191
  old_string: String that couldn't be found
188
192
  new_string: Replacement string
189
-
193
+
190
194
  Returns:
191
195
  Dict with error message and suggestions
192
196
  """
193
197
  # Try fuzzy match
194
198
  fuzzy_match = ForgivingEditHelper.find_fuzzy_match(file_content, old_string)
195
-
199
+
196
200
  if fuzzy_match:
197
201
  _, _, matched_text = fuzzy_match
198
202
  return {
199
203
  "error": "Exact match not found, but found similar text",
200
204
  "found": matched_text,
201
205
  "suggestion": "Use this as old_string instead",
202
- "confidence": "high"
206
+ "confidence": "high",
203
207
  }
204
-
208
+
205
209
  # Get suggestions
206
210
  suggestions = ForgivingEditHelper.suggest_matches(file_content, old_string)
207
-
211
+
208
212
  if suggestions:
209
213
  return {
210
214
  "error": "Could not find exact or fuzzy match",
@@ -212,9 +216,9 @@ class ForgivingEditHelper:
212
216
  {"similarity": f"{score:.0%}", "text": text}
213
217
  for score, text in suggestions
214
218
  ],
215
- "hint": "Try using one of these suggestions as old_string"
219
+ "hint": "Try using one of these suggestions as old_string",
216
220
  }
217
-
221
+
218
222
  # No good matches - provide general help
219
223
  return {
220
224
  "error": "Could not find any matches",
@@ -222,32 +226,32 @@ class ForgivingEditHelper:
222
226
  "Check for whitespace differences (tabs vs spaces)",
223
227
  "Ensure you're including complete lines",
224
228
  "Try a smaller, more unique portion of text",
225
- "Use the streaming_command tool to view the file with visible whitespace"
226
- ]
229
+ "Use the streaming_command tool to view the file with visible whitespace",
230
+ ],
227
231
  }
228
-
232
+
229
233
  @staticmethod
230
234
  def prepare_edit_string(text: str) -> str:
231
235
  """Prepare a string for editing by handling common issues.
232
-
236
+
233
237
  Args:
234
238
  text: Text to prepare
235
-
239
+
236
240
  Returns:
237
241
  Cleaned text ready for editing
238
242
  """
239
243
  # Remove any line number prefixes (common in AI copy-paste)
240
244
  lines = []
241
- for line in text.split('\n'):
245
+ for line in text.split("\n"):
242
246
  # Remove common line number patterns while preserving indentation
243
247
  # Match patterns like "1: ", "123: ", "1| ", "1- ", etc.
244
248
  # But preserve the original indentation after the line number
245
- match = re.match(r'^(\d+[:\|\-])\s(.*)', line)
249
+ match = re.match(r"^(\d+[:\|\-])\s(.*)", line)
246
250
  if match:
247
251
  # Keep only the content part (group 2) which includes any indentation
248
252
  lines.append(match.group(2))
249
253
  else:
250
254
  # No line number pattern found, keep the line as-is
251
255
  lines.append(line)
252
-
253
- return '\n'.join(lines)
256
+
257
+ return "\n".join(lines)
@@ -1,23 +1,23 @@
1
1
  """Mode system for organizing development tools based on programmer personalities."""
2
2
 
3
3
  import os
4
+ from typing import Set, Dict, List, Optional
4
5
  from dataclasses import dataclass
5
- from typing import Dict, List, Optional, Set
6
6
 
7
7
  from hanzo_mcp.tools.common.personality import (
8
8
  ToolPersonality,
9
- register_default_personalities,
9
+ personalities,
10
10
  ensure_agent_enabled,
11
- personalities
12
11
  )
13
12
 
14
13
 
15
14
  @dataclass
16
15
  class Mode(ToolPersonality):
17
16
  """Development mode combining tool preferences and environment settings."""
17
+
18
18
  # Inherits all fields from ToolPersonality
19
19
  # Adds mode-specific functionality
20
-
20
+
21
21
  @property
22
22
  def is_active(self) -> bool:
23
23
  """Check if this mode is currently active."""
@@ -26,47 +26,47 @@ class Mode(ToolPersonality):
26
26
 
27
27
  class ModeRegistry:
28
28
  """Registry for development modes."""
29
-
29
+
30
30
  _modes: Dict[str, Mode] = {}
31
31
  _active_mode: Optional[str] = None
32
-
32
+
33
33
  @classmethod
34
34
  def register(cls, mode: Mode) -> None:
35
35
  """Register a development mode."""
36
36
  # Ensure agent is enabled if API keys present
37
37
  mode = ensure_agent_enabled(mode)
38
38
  cls._modes[mode.name] = mode
39
-
39
+
40
40
  @classmethod
41
41
  def get(cls, name: str) -> Optional[Mode]:
42
42
  """Get a mode by name."""
43
43
  return cls._modes.get(name)
44
-
44
+
45
45
  @classmethod
46
46
  def list(cls) -> List[Mode]:
47
47
  """List all registered modes."""
48
48
  return list(cls._modes.values())
49
-
49
+
50
50
  @classmethod
51
51
  def set_active(cls, name: str) -> None:
52
52
  """Set the active mode."""
53
53
  if name not in cls._modes:
54
54
  raise ValueError(f"Mode '{name}' not found")
55
55
  cls._active_mode = name
56
-
56
+
57
57
  # Apply environment variables from the mode
58
58
  mode = cls._modes[name]
59
59
  if mode.environment:
60
60
  for key, value in mode.environment.items():
61
61
  os.environ[key] = value
62
-
62
+
63
63
  @classmethod
64
64
  def get_active(cls) -> Optional[Mode]:
65
65
  """Get the active mode."""
66
66
  if cls._active_mode:
67
67
  return cls._modes.get(cls._active_mode)
68
68
  return None
69
-
69
+
70
70
  @classmethod
71
71
  def get_active_tools(cls) -> Set[str]:
72
72
  """Get the set of tools from the active mode."""
@@ -95,9 +95,9 @@ def get_mode_from_env() -> Optional[str]:
95
95
  """Get mode name from environment variables."""
96
96
  # Check for HANZO_MODE, PERSONALITY, or MODE env vars
97
97
  return (
98
- os.environ.get("HANZO_MODE") or
99
- os.environ.get("PERSONALITY") or
100
- os.environ.get("MODE")
98
+ os.environ.get("HANZO_MODE")
99
+ or os.environ.get("PERSONALITY")
100
+ or os.environ.get("MODE")
101
101
  )
102
102
 
103
103
 
@@ -112,5 +112,3 @@ def activate_mode_from_env():
112
112
  # Mode not found, ignore
113
113
  pass
114
114
  return False
115
-
116
-
@@ -1,50 +1,54 @@
1
1
  """Tool mode loader for dynamic tool configuration."""
2
2
 
3
3
  import os
4
- from typing import Dict, List, Optional, Set
4
+ from typing import Dict, Optional
5
5
 
6
- from hanzo_mcp.tools.common.mode import ModeRegistry, register_default_modes, activate_mode_from_env
6
+ from hanzo_mcp.tools.common.mode import (
7
+ ModeRegistry,
8
+ activate_mode_from_env,
9
+ register_default_modes,
10
+ )
7
11
 
8
12
 
9
13
  class ModeLoader:
10
14
  """Loads and manages tool modes for dynamic configuration."""
11
-
15
+
12
16
  @staticmethod
13
17
  def initialize_modes() -> None:
14
18
  """Initialize the mode system with defaults."""
15
19
  # Initialize modes
16
20
  register_default_modes()
17
-
21
+
18
22
  # Check for mode from environment
19
23
  activate_mode_from_env()
20
-
24
+
21
25
  # If no mode set, use default
22
26
  if not ModeRegistry.get_active():
23
27
  default_mode = os.environ.get("HANZO_DEFAULT_MODE", "hanzo")
24
28
  if ModeRegistry.get(default_mode):
25
29
  ModeRegistry.set_active(default_mode)
26
-
30
+
27
31
  @staticmethod
28
32
  def get_enabled_tools_from_mode(
29
33
  base_enabled_tools: Optional[Dict[str, bool]] = None,
30
- force_mode: Optional[str] = None
34
+ force_mode: Optional[str] = None,
31
35
  ) -> Dict[str, bool]:
32
36
  """Get enabled tools configuration from active mode.
33
-
37
+
34
38
  Args:
35
39
  base_enabled_tools: Base configuration to merge with
36
40
  force_mode: Force a specific mode (overrides active)
37
-
41
+
38
42
  Returns:
39
43
  Dictionary of tool enable states
40
44
  """
41
45
  # Initialize if needed
42
46
  if not ModeRegistry.list():
43
47
  ModeLoader.initialize_modes()
44
-
48
+
45
49
  # Get mode to use
46
50
  tools_list = None
47
-
51
+
48
52
  if force_mode:
49
53
  # Set and get mode
50
54
  if ModeRegistry.get(force_mode):
@@ -56,35 +60,36 @@ class ModeLoader:
56
60
  mode = ModeRegistry.get_active()
57
61
  if mode:
58
62
  tools_list = mode.tools
59
-
63
+
60
64
  if not tools_list:
61
65
  # No active mode, return base config
62
66
  return base_enabled_tools or {}
63
-
67
+
64
68
  # Start with base configuration
65
69
  result = base_enabled_tools.copy() if base_enabled_tools else {}
66
-
70
+
67
71
  # Get all possible tools from registry
68
72
  from hanzo_mcp.config.tool_config import TOOL_REGISTRY
73
+
69
74
  all_possible_tools = set(TOOL_REGISTRY.keys())
70
-
75
+
71
76
  # Disable all tools first (clean slate for mode)
72
77
  for tool in all_possible_tools:
73
78
  result[tool] = False
74
-
79
+
75
80
  # Enable tools from mode
76
81
  for tool in tools_list:
77
82
  result[tool] = True
78
-
83
+
79
84
  # Always enable mode tool (meta)
80
85
  result["mode"] = True
81
-
86
+
82
87
  return result
83
-
88
+
84
89
  @staticmethod
85
90
  def get_environment_from_mode() -> Dict[str, str]:
86
91
  """Get environment variables from active mode.
87
-
92
+
88
93
  Returns:
89
94
  Dictionary of environment variables
90
95
  """
@@ -92,14 +97,12 @@ class ModeLoader:
92
97
  mode = ModeRegistry.get_active()
93
98
  if mode and mode.environment:
94
99
  return mode.environment.copy()
95
-
100
+
96
101
  return {}
97
-
102
+
98
103
  @staticmethod
99
104
  def apply_environment_from_mode() -> None:
100
105
  """Apply environment variables from active mode."""
101
106
  env_vars = ModeLoader.get_environment_from_mode()
102
107
  for key, value in env_vars.items():
103
108
  os.environ[key] = value
104
-
105
-