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.
- hanzo_mcp/__init__.py +6 -0
- 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.7.dist-info → hanzo_mcp-0.8.1.dist-info}/METADATA +1 -1
- hanzo_mcp-0.8.1.dist-info/RECORD +185 -0
- hanzo_mcp-0.7.7.dist-info/RECORD +0 -182
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.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
|
-
|
|
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(
|
|
27
|
+
for line in text.split("\n"):
|
|
28
28
|
# Replace tabs with 4 spaces everywhere in the line
|
|
29
|
-
line = line.replace(
|
|
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
|
|
37
|
+
content = re.sub(r" {2,}", " ", stripped)
|
|
38
38
|
lines.append(indent + content)
|
|
39
39
|
else:
|
|
40
40
|
lines.append(indent)
|
|
41
|
-
|
|
42
|
-
return
|
|
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(
|
|
77
|
-
|
|
74
|
+
lines_before = norm_haystack[:norm_start].count("\n")
|
|
75
|
+
|
|
78
76
|
# Find corresponding position in original
|
|
79
|
-
original_lines = haystack.split(
|
|
80
|
-
norm_lines = norm_haystack.split(
|
|
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(
|
|
86
|
-
end_pos = sum(
|
|
87
|
-
|
|
88
|
-
|
|
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(
|
|
93
|
-
needle_lines = needle.split(
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
256
|
+
|
|
257
|
+
return "\n".join(lines)
|
hanzo_mcp/tools/common/mode.py
CHANGED
|
@@ -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
|
-
|
|
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")
|
|
99
|
-
os.environ.get("PERSONALITY")
|
|
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,
|
|
4
|
+
from typing import Dict, Optional
|
|
5
5
|
|
|
6
|
-
from hanzo_mcp.tools.common.mode import
|
|
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
|
-
|