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,51 +1,48 @@
1
1
  """Base classes for process execution tools."""
2
2
 
3
- import asyncio
4
3
  import os
5
- import subprocess
6
- import tempfile
7
- import time
8
4
  import uuid
5
+ import tempfile
6
+ import subprocess
9
7
  from abc import abstractmethod
8
+ from typing import Any, Dict, List, Optional, override
10
9
  from pathlib import Path
11
- from typing import Any, Dict, List, Optional, Tuple, override
12
-
13
- from mcp.server.fastmcp import Context as MCPContext
14
10
 
15
11
  from hanzo_mcp.tools.common.base import BaseTool
16
- from hanzo_mcp.tools.common.permissions import PermissionManager
17
12
  from hanzo_mcp.tools.common.truncate import truncate_response
13
+ from hanzo_mcp.tools.common.permissions import PermissionManager
14
+
18
15
  # Import moved to __init__ to avoid circular import
19
16
 
20
17
 
21
18
  class ProcessManager:
22
19
  """Singleton manager for background processes."""
23
-
20
+
24
21
  _instance = None
25
22
  _processes: Dict[str, Any] = {}
26
23
  _logs: Dict[str, str] = {}
27
24
  _log_dir = Path(tempfile.gettempdir()) / "hanzo_mcp_logs"
28
-
25
+
29
26
  def __new__(cls):
30
27
  if cls._instance is None:
31
28
  cls._instance = super().__new__(cls)
32
29
  cls._instance._log_dir.mkdir(exist_ok=True)
33
30
  return cls._instance
34
-
31
+
35
32
  def add_process(self, process_id: str, process: Any, log_file: str) -> None:
36
33
  """Add a process to track."""
37
34
  self._processes[process_id] = process
38
35
  self._logs[process_id] = log_file
39
-
36
+
40
37
  def get_process(self, process_id: str) -> Optional[Any]:
41
38
  """Get a tracked process."""
42
39
  return self._processes.get(process_id)
43
-
40
+
44
41
  def remove_process(self, process_id: str) -> None:
45
42
  """Remove a process from tracking."""
46
43
  self._processes.pop(process_id, None)
47
44
  self._logs.pop(process_id, None)
48
-
45
+
49
46
  def list_processes(self) -> Dict[str, Dict[str, Any]]:
50
47
  """List all tracked processes."""
51
48
  result = {}
@@ -54,45 +51,45 @@ class ProcessManager:
54
51
  result[pid] = {
55
52
  "pid": proc.pid,
56
53
  "running": True,
57
- "log_file": self._logs.get(pid)
54
+ "log_file": self._logs.get(pid),
58
55
  }
59
56
  else:
60
57
  result[pid] = {
61
58
  "pid": proc.pid,
62
59
  "running": False,
63
60
  "return_code": proc.returncode,
64
- "log_file": self._logs.get(pid)
61
+ "log_file": self._logs.get(pid),
65
62
  }
66
63
  # Clean up finished processes
67
64
  self.remove_process(pid)
68
65
  return result
69
-
66
+
70
67
  def get_log_file(self, process_id: str) -> Optional[Path]:
71
68
  """Get log file path for a process."""
72
69
  log_path = self._logs.get(process_id)
73
70
  return Path(log_path) if log_path else None
74
-
71
+
75
72
  @property
76
73
  def log_dir(self) -> Path:
77
74
  """Get the log directory."""
78
75
  return self._log_dir
79
-
76
+
80
77
  def create_log_file(self, process_id: str) -> Path:
81
78
  """Create a log file for a process.
82
-
79
+
83
80
  Args:
84
81
  process_id: Process identifier
85
-
82
+
86
83
  Returns:
87
84
  Path to the created log file
88
85
  """
89
86
  log_file = self._log_dir / f"{process_id}.log"
90
87
  log_file.touch()
91
88
  return log_file
92
-
89
+
93
90
  def mark_completed(self, process_id: str, return_code: int) -> None:
94
91
  """Mark a process as completed with the given return code.
95
-
92
+
96
93
  Args:
97
94
  process_id: Process identifier
98
95
  return_code: Process exit code
@@ -104,10 +101,10 @@ class ProcessManager:
104
101
 
105
102
  class BaseProcessTool(BaseTool):
106
103
  """Base class for all process execution tools."""
107
-
104
+
108
105
  def __init__(self, permission_manager: Optional[PermissionManager] = None):
109
106
  """Initialize the process tool.
110
-
107
+
111
108
  Args:
112
109
  permission_manager: Optional permission manager for access control
113
110
  """
@@ -116,46 +113,47 @@ class BaseProcessTool(BaseTool):
116
113
  self.process_manager = ProcessManager()
117
114
  # Import here to avoid circular import
118
115
  from hanzo_mcp.tools.shell.auto_background import AutoBackgroundExecutor
116
+
119
117
  self.auto_background_executor = AutoBackgroundExecutor(self.process_manager)
120
-
118
+
121
119
  @abstractmethod
122
120
  def get_command_args(self, command: str, **kwargs) -> List[str]:
123
121
  """Get the command arguments for subprocess.
124
-
122
+
125
123
  Args:
126
124
  command: The command or script to run
127
125
  **kwargs: Additional arguments specific to the tool
128
-
126
+
129
127
  Returns:
130
128
  List of command arguments for subprocess
131
129
  """
132
130
  pass
133
-
131
+
134
132
  @abstractmethod
135
133
  def get_tool_name(self) -> str:
136
134
  """Get the name of the tool being used (e.g., 'bash', 'uvx', 'npx')."""
137
135
  pass
138
-
136
+
139
137
  async def execute_sync(
140
138
  self,
141
139
  command: str,
142
140
  cwd: Optional[Path] = None,
143
141
  env: Optional[Dict[str, str]] = None,
144
142
  timeout: Optional[int] = None,
145
- **kwargs
143
+ **kwargs,
146
144
  ) -> str:
147
145
  """Execute a command with auto-backgrounding after 2 minutes.
148
-
146
+
149
147
  Args:
150
148
  command: Command to execute
151
149
  cwd: Working directory
152
150
  env: Environment variables
153
151
  timeout: Timeout in seconds (ignored - auto-backgrounds after 2 minutes)
154
152
  **kwargs: Additional tool-specific arguments
155
-
153
+
156
154
  Returns:
157
155
  Command output or background status
158
-
156
+
159
157
  Raises:
160
158
  RuntimeError: If command fails
161
159
  """
@@ -163,23 +161,25 @@ class BaseProcessTool(BaseTool):
163
161
  if self.permission_manager and cwd:
164
162
  if not self.permission_manager.is_path_allowed(str(cwd)):
165
163
  raise PermissionError(f"Access denied to path: {cwd}")
166
-
164
+
167
165
  # Get command arguments
168
166
  cmd_args = self.get_command_args(command, **kwargs)
169
-
167
+
170
168
  # Prepare environment
171
169
  process_env = os.environ.copy()
172
170
  if env:
173
171
  process_env.update(env)
174
-
172
+
175
173
  # Execute with auto-backgrounding
176
- output, was_backgrounded, process_id = await self.auto_background_executor.execute_with_auto_background(
177
- cmd_args=cmd_args,
178
- tool_name=self.get_tool_name(),
179
- cwd=cwd,
180
- env=process_env
174
+ output, was_backgrounded, process_id = (
175
+ await self.auto_background_executor.execute_with_auto_background(
176
+ cmd_args=cmd_args,
177
+ tool_name=self.get_tool_name(),
178
+ cwd=cwd,
179
+ env=process_env,
180
+ )
181
181
  )
182
-
182
+
183
183
  if was_backgrounded:
184
184
  return output
185
185
  else:
@@ -189,24 +189,24 @@ class BaseProcessTool(BaseTool):
189
189
  return truncate_response(
190
190
  output,
191
191
  max_tokens=25000,
192
- truncation_message="\n\n[Command output truncated due to token limit. Output may be available in logs or files.]"
192
+ truncation_message="\n\n[Command output truncated due to token limit. Output may be available in logs or files.]",
193
193
  )
194
-
194
+
195
195
  async def execute_background(
196
196
  self,
197
197
  command: str,
198
198
  cwd: Optional[Path] = None,
199
199
  env: Optional[Dict[str, str]] = None,
200
- **kwargs
200
+ **kwargs,
201
201
  ) -> Dict[str, Any]:
202
202
  """Execute a command in the background.
203
-
203
+
204
204
  Args:
205
205
  command: Command to execute
206
206
  cwd: Working directory
207
207
  env: Environment variables
208
208
  **kwargs: Additional tool-specific arguments
209
-
209
+
210
210
  Returns:
211
211
  Dict with process_id and log_file
212
212
  """
@@ -214,19 +214,19 @@ class BaseProcessTool(BaseTool):
214
214
  if self.permission_manager and cwd:
215
215
  if not self.permission_manager.is_path_allowed(str(cwd)):
216
216
  raise PermissionError(f"Access denied to path: {cwd}")
217
-
217
+
218
218
  # Generate process ID and log file
219
219
  process_id = f"{self.get_tool_name()}_{uuid.uuid4().hex[:8]}"
220
220
  log_file = self.process_manager.log_dir / f"{process_id}.log"
221
-
221
+
222
222
  # Get command arguments
223
223
  cmd_args = self.get_command_args(command, **kwargs)
224
-
224
+
225
225
  # Prepare environment
226
226
  process_env = os.environ.copy()
227
227
  if env:
228
228
  process_env.update(env)
229
-
229
+
230
230
  # Start process with output to log file
231
231
  with open(log_file, "w") as f:
232
232
  process = subprocess.Popen(
@@ -235,57 +235,57 @@ class BaseProcessTool(BaseTool):
235
235
  env=process_env,
236
236
  stdout=f,
237
237
  stderr=subprocess.STDOUT,
238
- text=True
238
+ text=True,
239
239
  )
240
-
240
+
241
241
  # Track the process
242
242
  self.process_manager.add_process(process_id, process, log_file)
243
-
243
+
244
244
  return {
245
245
  "process_id": process_id,
246
246
  "pid": process.pid,
247
247
  "log_file": str(log_file),
248
- "status": "started"
248
+ "status": "started",
249
249
  }
250
250
 
251
251
 
252
252
  class BaseBinaryTool(BaseProcessTool):
253
253
  """Base class for binary execution tools (like npx, uvx)."""
254
-
254
+
255
255
  @abstractmethod
256
256
  def get_binary_name(self) -> str:
257
257
  """Get the name of the binary to execute."""
258
258
  pass
259
-
259
+
260
260
  @override
261
261
  def get_command_args(self, command: str, **kwargs) -> List[str]:
262
262
  """Get command arguments for binary execution.
263
-
263
+
264
264
  Args:
265
265
  command: The package or command to run
266
266
  **kwargs: Additional arguments (args, flags, etc.)
267
-
267
+
268
268
  Returns:
269
269
  List of command arguments
270
270
  """
271
271
  cmd_args = [self.get_binary_name()]
272
-
272
+
273
273
  # Add any binary-specific flags
274
274
  if "flags" in kwargs:
275
275
  cmd_args.extend(kwargs["flags"])
276
-
276
+
277
277
  # Add the command/package
278
278
  cmd_args.append(command)
279
-
279
+
280
280
  # Add any additional arguments
281
281
  if "args" in kwargs:
282
282
  if isinstance(kwargs["args"], str):
283
283
  cmd_args.extend(kwargs["args"].split())
284
284
  else:
285
285
  cmd_args.extend(kwargs["args"])
286
-
286
+
287
287
  return cmd_args
288
-
288
+
289
289
  @override
290
290
  def get_tool_name(self) -> str:
291
291
  """Get the tool name (same as binary name by default)."""
@@ -294,44 +294,44 @@ class BaseBinaryTool(BaseProcessTool):
294
294
 
295
295
  class BaseScriptTool(BaseProcessTool):
296
296
  """Base class for script execution tools (like bash, python)."""
297
-
297
+
298
298
  @abstractmethod
299
299
  def get_interpreter(self) -> str:
300
300
  """Get the interpreter to use."""
301
301
  pass
302
-
302
+
303
303
  @abstractmethod
304
304
  def get_script_flags(self) -> List[str]:
305
305
  """Get default flags for the interpreter."""
306
306
  pass
307
-
307
+
308
308
  @override
309
309
  def get_command_args(self, command: str, **kwargs) -> List[str]:
310
310
  """Get command arguments for script execution.
311
-
311
+
312
312
  Args:
313
313
  command: The script content to execute
314
314
  **kwargs: Additional arguments
315
-
315
+
316
316
  Returns:
317
317
  List of command arguments
318
318
  """
319
319
  cmd_args = [self.get_interpreter()]
320
320
  cmd_args.extend(self.get_script_flags())
321
-
321
+
322
322
  # For inline scripts, use -c flag
323
323
  if not kwargs.get("is_file", False):
324
324
  cmd_args.extend(["-c", command])
325
325
  else:
326
326
  cmd_args.append(command)
327
-
327
+
328
328
  return cmd_args
329
-
330
- @override
329
+
330
+ @override
331
331
  def get_tool_name(self) -> str:
332
332
  """Get the tool name (interpreter name by default)."""
333
333
  return self.get_interpreter()
334
334
 
335
335
 
336
336
  # Import os at the top of the file
337
- import os
337
+ import os
@@ -14,8 +14,8 @@ import bashlex # type: ignore
14
14
  import libtmux
15
15
 
16
16
  from hanzo_mcp.tools.shell.base import (
17
- BashCommandStatus,
18
17
  CommandResult,
18
+ BashCommandStatus,
19
19
  )
20
20
 
21
21
 
@@ -355,7 +355,7 @@ class BashSession:
355
355
  error_message=(
356
356
  f"ERROR: Cannot execute multiple commands at once.\n"
357
357
  f"Please run each command separately OR chain them into a single command via && or ;\n"
358
- f"Provided commands:\n{'\n'.join(f'({i + 1}) {cmd}' for i, cmd in enumerate(splited_commands))}"
358
+ f"Provided commands:\n{chr(10).join(f'({i + 1}) {cmd}' for i, cmd in enumerate(splited_commands))}"
359
359
  ),
360
360
  command=command,
361
361
  status=BashCommandStatus.COMPLETED,
@@ -4,15 +4,14 @@ This module provides a BashSessionExecutor class that replaces the old CommandEx
4
4
  implementation with the new BashSession-based approach for better persistent execution.
5
5
  """
6
6
 
7
- import asyncio
8
- import logging
9
7
  import os
10
8
  import shlex
11
- import subprocess
9
+ import asyncio
10
+ import logging
12
11
  from typing import final
13
12
 
13
+ from hanzo_mcp.tools.shell.base import CommandResult, BashCommandStatus
14
14
  from hanzo_mcp.tools.common.permissions import PermissionManager
15
- from hanzo_mcp.tools.shell.base import BashCommandStatus, CommandResult
16
15
  from hanzo_mcp.tools.shell.session_manager import SessionManager
17
16
 
18
17
 
@@ -65,6 +64,7 @@ class BashSessionExecutor:
65
64
  if data is not None:
66
65
  try:
67
66
  import json
67
+
68
68
  logger = logging.getLogger(__name__)
69
69
  if isinstance(data, (dict, list)):
70
70
  data_str = json.dumps(data)
@@ -2,40 +2,36 @@
2
2
 
3
3
  import os
4
4
  import platform
5
- from pathlib import Path
6
5
  from typing import Optional, override
6
+ from pathlib import Path
7
7
 
8
+ from mcp.server import FastMCP
8
9
  from mcp.server.fastmcp import Context as MCPContext
9
10
 
10
11
  from hanzo_mcp.tools.shell.base_process import BaseScriptTool
11
- from mcp.server import FastMCP
12
12
 
13
13
 
14
14
  class BashTool(BaseScriptTool):
15
15
  """Tool for running shell commands."""
16
-
16
+
17
17
  name = "bash"
18
-
18
+
19
19
  def register(self, server: FastMCP) -> None:
20
20
  """Register the tool with the MCP server."""
21
21
  tool_self = self
22
-
22
+
23
23
  @server.tool(name=self.name, description=self.description)
24
24
  async def bash(
25
25
  ctx: MCPContext,
26
26
  command: str,
27
27
  cwd: Optional[str] = None,
28
28
  env: Optional[dict[str, str]] = None,
29
- timeout: Optional[int] = None
29
+ timeout: Optional[int] = None,
30
30
  ) -> str:
31
31
  return await tool_self.run(
32
- ctx,
33
- command=command,
34
- cwd=cwd,
35
- env=env,
36
- timeout=timeout
32
+ ctx, command=command, cwd=cwd, env=env, timeout=timeout
37
33
  )
38
-
34
+
39
35
  async def call(self, ctx: MCPContext, **params) -> str:
40
36
  """Call the tool with arguments."""
41
37
  return await self.run(
@@ -43,9 +39,9 @@ class BashTool(BaseScriptTool):
43
39
  command=params["command"],
44
40
  cwd=params.get("cwd"),
45
41
  env=params.get("env"),
46
- timeout=params.get("timeout")
42
+ timeout=params.get("timeout"),
47
43
  )
48
-
44
+
49
45
  @property
50
46
  @override
51
47
  def description(self) -> str:
@@ -60,19 +56,19 @@ bash "ls -la"
60
56
  bash "python server.py" # Auto-backgrounds after 2 minutes
61
57
  bash "git status && git diff"
62
58
  bash "npm run dev" --cwd ./frontend # Auto-backgrounds if needed"""
63
-
59
+
64
60
  @override
65
61
  def get_interpreter(self) -> str:
66
62
  """Get the shell interpreter."""
67
63
  if platform.system() == "Windows":
68
64
  return "cmd.exe"
69
-
65
+
70
66
  # Check for user's preferred shell from environment
71
67
  shell = os.environ.get("SHELL", "/bin/bash")
72
-
68
+
73
69
  # Extract just the shell name from the path
74
70
  shell_name = os.path.basename(shell)
75
-
71
+
76
72
  # Check if it's a supported shell and the config file exists
77
73
  if shell_name == "zsh":
78
74
  # Check for .zshrc
@@ -84,27 +80,27 @@ bash "npm run dev" --cwd ./frontend # Auto-backgrounds if needed"""
84
80
  fish_config = Path.home() / ".config" / "fish" / "config.fish"
85
81
  if fish_config.exists():
86
82
  return shell # Use full path to fish
87
-
83
+
88
84
  # Default to bash if no special shell config found
89
85
  return "bash"
90
-
86
+
91
87
  @override
92
88
  def get_script_flags(self) -> list[str]:
93
89
  """Get interpreter flags."""
94
90
  if platform.system() == "Windows":
95
91
  return ["/c"]
96
92
  return ["-c"]
97
-
93
+
98
94
  @override
99
95
  def get_tool_name(self) -> str:
100
96
  """Get the tool name."""
101
97
  if platform.system() == "Windows":
102
98
  return "shell"
103
-
99
+
104
100
  # Return the actual shell being used
105
101
  interpreter = self.get_interpreter()
106
102
  return os.path.basename(interpreter)
107
-
103
+
108
104
  @override
109
105
  async def run(
110
106
  self,
@@ -115,29 +111,26 @@ bash "npm run dev" --cwd ./frontend # Auto-backgrounds if needed"""
115
111
  timeout: Optional[int] = None,
116
112
  ) -> str:
117
113
  """Run a shell command with auto-backgrounding.
118
-
114
+
119
115
  Args:
120
116
  ctx: MCP context
121
117
  command: Shell command to execute
122
118
  cwd: Working directory
123
119
  env: Environment variables
124
120
  timeout: Command timeout in seconds (ignored - auto-backgrounds after 2 minutes)
125
-
121
+
126
122
  Returns:
127
123
  Command output or background status
128
124
  """
129
125
  # Prepare working directory
130
126
  work_dir = Path(cwd).resolve() if cwd else Path.cwd()
131
-
127
+
132
128
  # Always use execute_sync which now has auto-backgrounding
133
129
  output = await self.execute_sync(
134
- command,
135
- cwd=work_dir,
136
- env=env,
137
- timeout=timeout
130
+ command, cwd=work_dir, env=env, timeout=timeout
138
131
  )
139
132
  return output if output else "Command completed successfully (no output)"
140
133
 
141
134
 
142
135
  # Create tool instance
143
- bash_tool = BashTool()
136
+ bash_tool = BashTool()
@@ -4,20 +4,19 @@ This module provides tools for executing shell commands and scripts with
4
4
  comprehensive error handling, permissions checking, and progress tracking.
5
5
  """
6
6
 
7
- import asyncio
8
- import base64
9
7
  import os
10
8
  import re
9
+ import sys
11
10
  import shlex
11
+ import base64
12
12
  import shutil
13
- import sys
13
+ import asyncio
14
14
  import tempfile
15
- from collections.abc import Awaitable, Callable
16
15
  from typing import final
16
+ from collections.abc import Callable, Awaitable
17
17
 
18
-
19
- from hanzo_mcp.tools.common.permissions import PermissionManager
20
18
  from hanzo_mcp.tools.shell.base import CommandResult
19
+ from hanzo_mcp.tools.common.permissions import PermissionManager
21
20
 
22
21
 
23
22
  @final
@@ -183,6 +182,7 @@ class CommandExecutor:
183
182
  if data is not None:
184
183
  try:
185
184
  import json
185
+
186
186
  logger = logging.getLogger(__name__)
187
187
  if isinstance(data, (dict, list)):
188
188
  data_str = json.dumps(data)
@@ -312,11 +312,11 @@ class CommandExecutor:
312
312
  self._log(f"Escaped command: {escaped_command}")
313
313
 
314
314
  # Wrap command with appropriate shell invocation
315
- if shell_basename == "zsh":
316
- shell_cmd = f"{user_shell} -l -c '{escaped_command}'"
317
- elif shell_basename == "bash":
318
- shell_cmd = f"{user_shell} -l -c '{escaped_command}'"
319
- elif shell_basename == "fish":
315
+ if (
316
+ shell_basename == "zsh"
317
+ or shell_basename == "bash"
318
+ or shell_basename == "fish"
319
+ ):
320
320
  shell_cmd = f"{user_shell} -l -c '{escaped_command}'"
321
321
  else:
322
322
  # Default fallback
@@ -691,7 +691,7 @@ class CommandExecutor:
691
691
  match = re.match(r"([a-zA-Z]):\\(.*)", temp_path)
692
692
  if match:
693
693
  drive, path = match.groups()
694
- wsl_path = f"/mnt/{drive.lower()}/{path.replace('\\', '/')}"
694
+ wsl_path = f"/mnt/{drive.lower()}/{path.replace(chr(92), '/')}"
695
695
  else:
696
696
  wsl_path = temp_path.replace("\\", "/")
697
697
  self._log(f"WSL path conversion may be incomplete: {wsl_path}")