hanzo-mcp 0.3.4__py3-none-any.whl → 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of hanzo-mcp might be problematic. Click here for more details.
- hanzo_mcp/__init__.py +1 -1
- hanzo_mcp/cli.py +123 -160
- hanzo_mcp/cli_enhanced.py +438 -0
- hanzo_mcp/config/__init__.py +19 -0
- hanzo_mcp/config/settings.py +388 -0
- hanzo_mcp/config/tool_config.py +197 -0
- hanzo_mcp/prompts/__init__.py +117 -0
- hanzo_mcp/prompts/compact_conversation.py +77 -0
- hanzo_mcp/prompts/create_release.py +38 -0
- hanzo_mcp/prompts/project_system.py +120 -0
- hanzo_mcp/prompts/project_todo_reminder.py +111 -0
- hanzo_mcp/prompts/utils.py +286 -0
- hanzo_mcp/server.py +120 -98
- hanzo_mcp/tools/__init__.py +107 -31
- hanzo_mcp/tools/agent/__init__.py +8 -11
- hanzo_mcp/tools/agent/agent_tool.py +290 -224
- hanzo_mcp/tools/agent/prompt.py +16 -13
- hanzo_mcp/tools/agent/tool_adapter.py +9 -9
- hanzo_mcp/tools/common/__init__.py +17 -16
- hanzo_mcp/tools/common/base.py +79 -110
- hanzo_mcp/tools/common/batch_tool.py +330 -0
- hanzo_mcp/tools/common/context.py +26 -292
- hanzo_mcp/tools/common/permissions.py +12 -12
- hanzo_mcp/tools/common/thinking_tool.py +153 -0
- hanzo_mcp/tools/common/validation.py +1 -63
- hanzo_mcp/tools/filesystem/__init__.py +88 -41
- hanzo_mcp/tools/filesystem/base.py +32 -24
- hanzo_mcp/tools/filesystem/content_replace.py +114 -107
- hanzo_mcp/tools/filesystem/directory_tree.py +129 -105
- hanzo_mcp/tools/filesystem/edit.py +279 -0
- hanzo_mcp/tools/filesystem/grep.py +458 -0
- hanzo_mcp/tools/filesystem/grep_ast_tool.py +250 -0
- hanzo_mcp/tools/filesystem/multi_edit.py +362 -0
- hanzo_mcp/tools/filesystem/read.py +255 -0
- hanzo_mcp/tools/filesystem/write.py +156 -0
- hanzo_mcp/tools/jupyter/__init__.py +41 -29
- hanzo_mcp/tools/jupyter/base.py +66 -57
- hanzo_mcp/tools/jupyter/{edit_notebook.py → notebook_edit.py} +162 -139
- hanzo_mcp/tools/jupyter/notebook_read.py +152 -0
- hanzo_mcp/tools/shell/__init__.py +29 -20
- hanzo_mcp/tools/shell/base.py +87 -45
- hanzo_mcp/tools/shell/bash_session.py +731 -0
- hanzo_mcp/tools/shell/bash_session_executor.py +295 -0
- hanzo_mcp/tools/shell/command_executor.py +435 -384
- hanzo_mcp/tools/shell/run_command.py +284 -131
- hanzo_mcp/tools/shell/run_command_windows.py +328 -0
- hanzo_mcp/tools/shell/session_manager.py +196 -0
- hanzo_mcp/tools/shell/session_storage.py +325 -0
- hanzo_mcp/tools/todo/__init__.py +66 -0
- hanzo_mcp/tools/todo/base.py +319 -0
- hanzo_mcp/tools/todo/todo_read.py +148 -0
- hanzo_mcp/tools/todo/todo_write.py +378 -0
- hanzo_mcp/tools/vector/__init__.py +95 -0
- hanzo_mcp/tools/vector/infinity_store.py +365 -0
- hanzo_mcp/tools/vector/project_manager.py +361 -0
- hanzo_mcp/tools/vector/vector_index.py +115 -0
- hanzo_mcp/tools/vector/vector_search.py +215 -0
- {hanzo_mcp-0.3.4.dist-info → hanzo_mcp-0.5.0.dist-info}/METADATA +35 -3
- hanzo_mcp-0.5.0.dist-info/RECORD +63 -0
- {hanzo_mcp-0.3.4.dist-info → hanzo_mcp-0.5.0.dist-info}/WHEEL +1 -1
- hanzo_mcp/tools/agent/base_provider.py +0 -73
- hanzo_mcp/tools/agent/litellm_provider.py +0 -45
- hanzo_mcp/tools/agent/lmstudio_agent.py +0 -385
- hanzo_mcp/tools/agent/lmstudio_provider.py +0 -219
- hanzo_mcp/tools/agent/provider_registry.py +0 -120
- hanzo_mcp/tools/common/error_handling.py +0 -86
- hanzo_mcp/tools/common/logging_config.py +0 -115
- hanzo_mcp/tools/common/session.py +0 -91
- hanzo_mcp/tools/common/think_tool.py +0 -123
- hanzo_mcp/tools/common/version_tool.py +0 -120
- hanzo_mcp/tools/filesystem/edit_file.py +0 -287
- hanzo_mcp/tools/filesystem/get_file_info.py +0 -170
- hanzo_mcp/tools/filesystem/read_files.py +0 -198
- hanzo_mcp/tools/filesystem/search_content.py +0 -275
- hanzo_mcp/tools/filesystem/write_file.py +0 -162
- hanzo_mcp/tools/jupyter/notebook_operations.py +0 -514
- hanzo_mcp/tools/jupyter/read_notebook.py +0 -165
- hanzo_mcp/tools/project/__init__.py +0 -64
- hanzo_mcp/tools/project/analysis.py +0 -882
- hanzo_mcp/tools/project/base.py +0 -66
- hanzo_mcp/tools/project/project_analyze.py +0 -173
- hanzo_mcp/tools/shell/run_script.py +0 -215
- hanzo_mcp/tools/shell/script_tool.py +0 -244
- hanzo_mcp-0.3.4.dist-info/RECORD +0 -53
- {hanzo_mcp-0.3.4.dist-info → hanzo_mcp-0.5.0.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.3.4.dist-info → hanzo_mcp-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {hanzo_mcp-0.3.4.dist-info → hanzo_mcp-0.5.0.dist-info}/top_level.txt +0 -0
|
@@ -7,19 +7,16 @@ comprehensive error handling, permissions checking, and progress tracking.
|
|
|
7
7
|
import asyncio
|
|
8
8
|
import base64
|
|
9
9
|
import os
|
|
10
|
+
import re
|
|
10
11
|
import shlex
|
|
12
|
+
import shutil
|
|
11
13
|
import sys
|
|
12
14
|
import tempfile
|
|
13
15
|
from collections.abc import Awaitable, Callable
|
|
14
|
-
from
|
|
15
|
-
from typing import Dict, Optional, final
|
|
16
|
+
from typing import final
|
|
16
17
|
|
|
17
|
-
from mcp.server.fastmcp import Context as MCPContext
|
|
18
|
-
from mcp.server.fastmcp import FastMCP
|
|
19
18
|
|
|
20
|
-
from hanzo_mcp.tools.common.context import create_tool_context
|
|
21
19
|
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
22
|
-
from hanzo_mcp.tools.common.session import SessionManager
|
|
23
20
|
from hanzo_mcp.tools.shell.base import CommandResult
|
|
24
21
|
|
|
25
22
|
|
|
@@ -43,111 +40,135 @@ class CommandExecutor:
|
|
|
43
40
|
self.permission_manager: PermissionManager = permission_manager
|
|
44
41
|
self.verbose: bool = verbose
|
|
45
42
|
|
|
46
|
-
# Session management (initialized on first use with session ID)
|
|
47
|
-
self.session_manager: Optional[SessionManager] = None
|
|
48
|
-
|
|
49
43
|
# Excluded commands or patterns
|
|
50
44
|
self.excluded_commands: list[str] = ["rm"]
|
|
51
45
|
|
|
52
46
|
# Map of supported interpreters with special handling
|
|
53
|
-
self.special_interpreters:
|
|
47
|
+
self.special_interpreters: dict[
|
|
54
48
|
str,
|
|
55
|
-
Callable[
|
|
56
|
-
[str, str, str], dict[str, str]], Optional[float | None | None,
|
|
57
|
-
Awaitable[CommandResult],
|
|
58
|
-
],
|
|
49
|
+
Callable[[str, str, str], dict[str, str]] | Awaitable[CommandResult],
|
|
59
50
|
] = {
|
|
60
51
|
"fish": self._handle_fish_script,
|
|
61
52
|
}
|
|
62
53
|
|
|
63
|
-
def
|
|
64
|
-
"""
|
|
54
|
+
def _get_shell_by_type(self, shell_type: str) -> tuple[str, str]:
|
|
55
|
+
"""Get shell information for a specified shell type.
|
|
65
56
|
|
|
66
57
|
Args:
|
|
67
|
-
|
|
58
|
+
shell_type: The shell name to use (e.g., "bash", "cmd", "powershell")
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Tuple of (shell_basename, shell_path)
|
|
68
62
|
"""
|
|
69
|
-
|
|
70
|
-
|
|
63
|
+
shell_path = shutil.which(shell_type)
|
|
64
|
+
if shell_path is None:
|
|
65
|
+
# Shell not found, fall back to default
|
|
66
|
+
self._log(f"Requested shell '{shell_type}' not found, using system default")
|
|
67
|
+
return self._get_system_shell()
|
|
71
68
|
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
if sys.platform == "win32":
|
|
70
|
+
shell_path = shell_path.lower()
|
|
71
|
+
|
|
72
|
+
shell_basename = os.path.basename(shell_path).lower()
|
|
73
|
+
return shell_basename, shell_path
|
|
74
|
+
|
|
75
|
+
def _get_system_shell(self, shell_type: str | None = None) -> tuple[str, str]:
|
|
76
|
+
"""Get the system's default shell based on the platform.
|
|
74
77
|
|
|
75
78
|
Args:
|
|
76
|
-
|
|
77
|
-
"""
|
|
78
|
-
if command not in self.excluded_commands:
|
|
79
|
-
self.excluded_commands.append(command)
|
|
79
|
+
shell_type: Optional specific shell to use instead of system default
|
|
80
80
|
|
|
81
|
-
def _get_preferred_shell(self) -> str:
|
|
82
|
-
"""Get the best available shell for the current environment.
|
|
83
|
-
|
|
84
|
-
Tries to find the best shell in this order:
|
|
85
|
-
1. User's SHELL environment variable
|
|
86
|
-
2. zsh
|
|
87
|
-
3. bash
|
|
88
|
-
4. fish
|
|
89
|
-
5. sh (fallback)
|
|
90
|
-
|
|
91
81
|
Returns:
|
|
92
|
-
|
|
82
|
+
Tuple of (shell_basename, shell_path)
|
|
93
83
|
"""
|
|
94
|
-
#
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
"
|
|
102
|
-
"
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
"/bin/
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
for
|
|
112
|
-
if os.path.exists(shell_path) and os.access(shell_path, os.X_OK):
|
|
113
|
-
return shell_path
|
|
114
|
-
|
|
115
|
-
# Fallback to /bin/sh which should exist on most systems
|
|
116
|
-
return "/bin/sh"
|
|
117
|
-
|
|
118
|
-
def get_session_manager(self, session_id: str) -> SessionManager:
|
|
119
|
-
"""Get the session manager for the given session ID.
|
|
84
|
+
# If a specific shell is requested, use that
|
|
85
|
+
if shell_type is not None:
|
|
86
|
+
return self._get_shell_by_type(shell_type)
|
|
87
|
+
|
|
88
|
+
# Otherwise use system default
|
|
89
|
+
if sys.platform == "win32":
|
|
90
|
+
# On Windows, default to Command Prompt
|
|
91
|
+
comspec = os.environ.get("COMSPEC", "cmd.exe").lower()
|
|
92
|
+
return "cmd", comspec
|
|
93
|
+
else:
|
|
94
|
+
# Unix systems
|
|
95
|
+
user_shell = os.environ.get("SHELL", "/bin/bash")
|
|
96
|
+
return os.path.basename(user_shell).lower(), user_shell
|
|
97
|
+
|
|
98
|
+
def _format_win32_shell_command(
|
|
99
|
+
self, shell_basename, user_shell, command, use_login_shell=True
|
|
100
|
+
):
|
|
101
|
+
"""Format a command for execution with the appropriate Windows shell.
|
|
120
102
|
|
|
121
103
|
Args:
|
|
122
|
-
|
|
104
|
+
shell_basename: The basename of the shell
|
|
105
|
+
user_shell: The full path to the shell
|
|
106
|
+
command: The command to execute
|
|
107
|
+
use_login_shell: Whether to use login shell settings
|
|
123
108
|
|
|
124
109
|
Returns:
|
|
125
|
-
|
|
110
|
+
Formatted shell command string
|
|
126
111
|
"""
|
|
127
|
-
|
|
112
|
+
formatted_command = ""
|
|
113
|
+
|
|
114
|
+
if shell_basename in ["wsl", "wsl.exe"]:
|
|
115
|
+
# For WSL, handle commands with shell operators differently
|
|
116
|
+
if any(char in command for char in ";&|<>(){}[]$\"'`"):
|
|
117
|
+
# For commands with special characters, use a more reliable approach
|
|
118
|
+
# with bash -c and double quotes around the entire command
|
|
119
|
+
escaped_command = command.replace('"', '\\"')
|
|
120
|
+
if use_login_shell:
|
|
121
|
+
formatted_command = f'{user_shell} bash -l -c "{escaped_command}"'
|
|
122
|
+
else:
|
|
123
|
+
formatted_command = f'{user_shell} bash -c "{escaped_command}"'
|
|
124
|
+
else:
|
|
125
|
+
# # For simple commands without special characters
|
|
126
|
+
# # Still respect login shell preference
|
|
127
|
+
# if use_login_shell:
|
|
128
|
+
# formatted_command = f"{user_shell} bash -l -c \"{command}\""
|
|
129
|
+
# else:
|
|
130
|
+
formatted_command = f"{user_shell} {command}"
|
|
131
|
+
|
|
132
|
+
elif shell_basename in ["powershell", "powershell.exe", "pwsh", "pwsh.exe"]:
|
|
133
|
+
# For PowerShell, escape double quotes with backslash most robust
|
|
134
|
+
escaped_command = command.replace('"', '\\"')
|
|
135
|
+
formatted_command = f'"{user_shell}" -Command "{escaped_command}"'
|
|
128
136
|
|
|
129
|
-
|
|
130
|
-
|
|
137
|
+
else:
|
|
138
|
+
# For CMD, use the /c parameter and wrap in double quotes
|
|
139
|
+
# CMD doesn't have an explicit login shell concept
|
|
140
|
+
formatted_command = f'"{user_shell}" /c "{command}"'
|
|
141
|
+
|
|
142
|
+
self._log(
|
|
143
|
+
"Win32 Shell Results",
|
|
144
|
+
{
|
|
145
|
+
"shell_basename": shell_basename,
|
|
146
|
+
"user_shell": user_shell,
|
|
147
|
+
"command": command,
|
|
148
|
+
"use_login_shell": use_login_shell,
|
|
149
|
+
"formatted_command": formatted_command,
|
|
150
|
+
},
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
return formatted_command
|
|
154
|
+
|
|
155
|
+
def allow_command(self, command: str) -> None:
|
|
156
|
+
"""Allow a specific command that might otherwise be excluded.
|
|
131
157
|
|
|
132
158
|
Args:
|
|
133
|
-
|
|
134
|
-
path: The path to set as the current working directory
|
|
159
|
+
command: The command to allow
|
|
135
160
|
"""
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
session.set_working_dir(Path(expanded_path))
|
|
161
|
+
if command in self.excluded_commands:
|
|
162
|
+
self.excluded_commands.remove(command)
|
|
139
163
|
|
|
140
|
-
def
|
|
141
|
-
"""
|
|
164
|
+
def deny_command(self, command: str) -> None:
|
|
165
|
+
"""Deny a specific command, adding it to the excluded list.
|
|
142
166
|
|
|
143
167
|
Args:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
Returns:
|
|
147
|
-
The current working directory
|
|
168
|
+
command: The command to deny
|
|
148
169
|
"""
|
|
149
|
-
|
|
150
|
-
|
|
170
|
+
if command not in self.excluded_commands:
|
|
171
|
+
self.excluded_commands.append(command)
|
|
151
172
|
|
|
152
173
|
def _log(self, message: str, data: object | None = None) -> None:
|
|
153
174
|
"""Log a message if verbose logging is enabled.
|
|
@@ -155,9 +176,6 @@ class CommandExecutor:
|
|
|
155
176
|
Args:
|
|
156
177
|
message: The message to log
|
|
157
178
|
data: Optional data to include with the message
|
|
158
|
-
|
|
159
|
-
Note:
|
|
160
|
-
Always logs to stderr to avoid interfering with stdio transport
|
|
161
179
|
"""
|
|
162
180
|
if not self.verbose:
|
|
163
181
|
return
|
|
@@ -208,20 +226,20 @@ class CommandExecutor:
|
|
|
208
226
|
self,
|
|
209
227
|
command: str,
|
|
210
228
|
cwd: str | None = None,
|
|
229
|
+
shell_type: str | None = None,
|
|
211
230
|
env: dict[str, str] | None = None,
|
|
212
231
|
timeout: float | None = 60.0,
|
|
213
232
|
use_login_shell: bool = True,
|
|
214
|
-
session_id: str | None = None,
|
|
215
233
|
) -> CommandResult:
|
|
216
234
|
"""Execute a shell command with safety checks.
|
|
217
235
|
|
|
218
236
|
Args:
|
|
219
237
|
command: The command to execute
|
|
220
|
-
cwd: Optional working directory
|
|
238
|
+
cwd: Optional working directory
|
|
221
239
|
env: Optional environment variables
|
|
222
240
|
timeout: Optional timeout in seconds
|
|
223
241
|
use_login_shell: Whether to use login shell. default true (loads ~/.zshrc, ~/.bashrc, etc.)
|
|
224
|
-
|
|
242
|
+
shell_type: Optional shell to use (e.g., "cmd", "powershell", "wsl", "bash")
|
|
225
243
|
|
|
226
244
|
Returns:
|
|
227
245
|
CommandResult containing execution results
|
|
@@ -234,50 +252,17 @@ class CommandExecutor:
|
|
|
234
252
|
return_code=1, error_message=f"Command not allowed: {command}"
|
|
235
253
|
)
|
|
236
254
|
|
|
237
|
-
# Use session working directory if no cwd specified and session_id provided
|
|
238
|
-
effective_cwd = cwd
|
|
239
|
-
if session_id and not cwd:
|
|
240
|
-
effective_cwd = self.get_working_dir(session_id)
|
|
241
|
-
self._log(f"Using session working directory: {effective_cwd}")
|
|
242
|
-
|
|
243
|
-
# Check if it's a cd command and update session working directory
|
|
244
|
-
is_cd_command = False
|
|
245
|
-
if session_id and command.strip().startswith("cd "):
|
|
246
|
-
is_cd_command = True
|
|
247
|
-
args = shlex.split(command)
|
|
248
|
-
if len(args) > 1:
|
|
249
|
-
target_dir = args[1]
|
|
250
|
-
# Handle relative paths
|
|
251
|
-
if not os.path.isabs(target_dir):
|
|
252
|
-
session_cwd = self.get_working_dir(session_id)
|
|
253
|
-
target_dir = os.path.join(session_cwd, target_dir)
|
|
254
|
-
|
|
255
|
-
# Expand user paths
|
|
256
|
-
target_dir = os.path.expanduser(target_dir)
|
|
257
|
-
|
|
258
|
-
# Normalize path
|
|
259
|
-
target_dir = os.path.normpath(target_dir)
|
|
260
|
-
|
|
261
|
-
if os.path.isdir(target_dir):
|
|
262
|
-
self.set_working_dir(session_id, target_dir)
|
|
263
|
-
self._log(f"Updated session working directory: {target_dir}")
|
|
264
|
-
return CommandResult(return_code=0, stdout="")
|
|
265
|
-
else:
|
|
266
|
-
return CommandResult(
|
|
267
|
-
return_code=1, error_message=f"Directory does not exist: {target_dir}"
|
|
268
|
-
)
|
|
269
|
-
|
|
270
255
|
# Check working directory permissions if specified
|
|
271
|
-
if
|
|
272
|
-
if not os.path.isdir(
|
|
256
|
+
if cwd:
|
|
257
|
+
if not os.path.isdir(cwd):
|
|
273
258
|
return CommandResult(
|
|
274
259
|
return_code=1,
|
|
275
|
-
error_message=f"Working directory does not exist: {
|
|
260
|
+
error_message=f"Working directory does not exist: {cwd}",
|
|
276
261
|
)
|
|
277
262
|
|
|
278
|
-
if not self.permission_manager.is_path_allowed(
|
|
263
|
+
if not self.permission_manager.is_path_allowed(cwd):
|
|
279
264
|
return CommandResult(
|
|
280
|
-
return_code=1, error_message=f"Working directory not allowed: {
|
|
265
|
+
return_code=1, error_message=f"Working directory not allowed: {cwd}"
|
|
281
266
|
)
|
|
282
267
|
|
|
283
268
|
# Set up environment
|
|
@@ -286,56 +271,84 @@ class CommandExecutor:
|
|
|
286
271
|
command_env.update(env)
|
|
287
272
|
|
|
288
273
|
try:
|
|
289
|
-
#
|
|
290
|
-
|
|
291
|
-
needs_shell = any(op in command for op in shell_operators)
|
|
274
|
+
# Get shell information
|
|
275
|
+
shell_basename, user_shell = self._get_system_shell(shell_type)
|
|
292
276
|
|
|
293
|
-
if
|
|
294
|
-
#
|
|
295
|
-
|
|
277
|
+
if sys.platform == "win32":
|
|
278
|
+
# On Windows, always use shell execution
|
|
279
|
+
self._log(f"Using shell on Windows: {user_shell} ({shell_basename})")
|
|
296
280
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
user_shell
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
self._log(f"Using login shell: {user_shell}")
|
|
303
|
-
|
|
304
|
-
# Wrap command with appropriate shell invocation
|
|
305
|
-
if shell_basename == "zsh":
|
|
306
|
-
shell_cmd = f"{user_shell} -l -c '{command}'"
|
|
307
|
-
elif shell_basename == "bash":
|
|
308
|
-
shell_cmd = f"{user_shell} -l -c '{command}'"
|
|
309
|
-
elif shell_basename == "fish":
|
|
310
|
-
shell_cmd = f"{user_shell} -l -c '{command}'"
|
|
311
|
-
else:
|
|
312
|
-
# Default fallback
|
|
313
|
-
shell_cmd = f"{user_shell} -c '{command}'"
|
|
314
|
-
else:
|
|
315
|
-
self._log(
|
|
316
|
-
f"Using shell for command with shell operators: {command}"
|
|
317
|
-
)
|
|
281
|
+
# Format command using helper method
|
|
282
|
+
shell_cmd = self._format_win32_shell_command(
|
|
283
|
+
shell_basename, user_shell, command, use_login_shell
|
|
284
|
+
)
|
|
318
285
|
|
|
319
286
|
# Use shell for command execution
|
|
320
287
|
process = await asyncio.create_subprocess_shell(
|
|
321
288
|
shell_cmd,
|
|
322
289
|
stdout=asyncio.subprocess.PIPE,
|
|
323
290
|
stderr=asyncio.subprocess.PIPE,
|
|
324
|
-
cwd=
|
|
291
|
+
cwd=cwd,
|
|
325
292
|
env=command_env,
|
|
326
293
|
)
|
|
327
294
|
else:
|
|
328
|
-
#
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
295
|
+
# Unix systems - original logic
|
|
296
|
+
shell_operators = ["&&", "||", "|", ";", ">", "<", "$(", "`", "$"]
|
|
297
|
+
needs_shell = any(op in command for op in shell_operators)
|
|
298
|
+
|
|
299
|
+
if needs_shell or use_login_shell:
|
|
300
|
+
# Determine which shell to use
|
|
301
|
+
shell_cmd = command
|
|
302
|
+
|
|
303
|
+
if use_login_shell:
|
|
304
|
+
self._log(f"Using login shell: {user_shell} ({shell_basename})")
|
|
305
|
+
|
|
306
|
+
# Escape single quotes in command for shell -c wrapper
|
|
307
|
+
# The standard way to escape a single quote within a single-quoted string in POSIX shells is '\''
|
|
308
|
+
escaped_command = command.replace("'", "'\\''")
|
|
309
|
+
self._log(f"Original command: {command}")
|
|
310
|
+
self._log(f"Escaped command: {escaped_command}")
|
|
311
|
+
|
|
312
|
+
# Wrap command with appropriate shell invocation
|
|
313
|
+
if shell_basename == "zsh":
|
|
314
|
+
shell_cmd = f"{user_shell} -l -c '{escaped_command}'"
|
|
315
|
+
elif shell_basename == "bash":
|
|
316
|
+
shell_cmd = f"{user_shell} -l -c '{escaped_command}'"
|
|
317
|
+
elif shell_basename == "fish":
|
|
318
|
+
shell_cmd = f"{user_shell} -l -c '{escaped_command}'"
|
|
319
|
+
else:
|
|
320
|
+
# Default fallback
|
|
321
|
+
shell_cmd = f"{user_shell} -c '{escaped_command}'"
|
|
322
|
+
else:
|
|
323
|
+
self._log(
|
|
324
|
+
f"Using shell for command with shell operators: {command}"
|
|
325
|
+
)
|
|
326
|
+
# Escape single quotes in command for shell execution
|
|
327
|
+
escaped_command = command.replace("'", "'\\''")
|
|
328
|
+
self._log(f"Original command: {command}")
|
|
329
|
+
self._log(f"Escaped command: {escaped_command}")
|
|
330
|
+
shell_cmd = f"{user_shell} -c '{escaped_command}'"
|
|
331
|
+
|
|
332
|
+
# Use shell for command execution
|
|
333
|
+
process = await asyncio.create_subprocess_shell(
|
|
334
|
+
shell_cmd,
|
|
335
|
+
stdout=asyncio.subprocess.PIPE,
|
|
336
|
+
stderr=asyncio.subprocess.PIPE,
|
|
337
|
+
cwd=cwd,
|
|
338
|
+
env=command_env,
|
|
339
|
+
)
|
|
340
|
+
else:
|
|
341
|
+
# Split the command into arguments for regular commands
|
|
342
|
+
args: list[str] = shlex.split(command)
|
|
343
|
+
|
|
344
|
+
# Create and run the process without shell
|
|
345
|
+
process = await asyncio.create_subprocess_exec(
|
|
346
|
+
*args,
|
|
347
|
+
stdout=asyncio.subprocess.PIPE,
|
|
348
|
+
stderr=asyncio.subprocess.PIPE,
|
|
349
|
+
cwd=cwd,
|
|
350
|
+
env=command_env,
|
|
351
|
+
)
|
|
339
352
|
|
|
340
353
|
# Wait for the process to complete with timeout
|
|
341
354
|
try:
|
|
@@ -370,44 +383,38 @@ class CommandExecutor:
|
|
|
370
383
|
script: str,
|
|
371
384
|
interpreter: str = "bash",
|
|
372
385
|
cwd: str | None = None,
|
|
386
|
+
shell_type: str | None = None,
|
|
373
387
|
env: dict[str, str] | None = None,
|
|
374
388
|
timeout: float | None = 60.0,
|
|
375
389
|
use_login_shell: bool = True,
|
|
376
|
-
session_id: str | None = None,
|
|
377
390
|
) -> CommandResult:
|
|
378
391
|
"""Execute a script with the specified interpreter.
|
|
379
392
|
|
|
380
393
|
Args:
|
|
381
394
|
script: The script content to execute
|
|
382
395
|
interpreter: The interpreter to use (bash, python, etc.)
|
|
383
|
-
cwd: Optional working directory
|
|
396
|
+
cwd: Optional working directory
|
|
397
|
+
shell_type: Optional shell to use (e.g., "cmd", "powershell", "wsl", "bash")
|
|
384
398
|
env: Optional environment variables
|
|
385
399
|
timeout: Optional timeout in seconds
|
|
386
400
|
use_login_shell: Whether to use login shell (loads ~/.zshrc, ~/.bashrc, etc.)
|
|
387
|
-
session_id: Optional session ID for persistent working directory
|
|
388
401
|
|
|
389
402
|
Returns:
|
|
390
403
|
CommandResult containing execution results
|
|
391
404
|
"""
|
|
392
405
|
self._log(f"Executing script with interpreter: {interpreter}")
|
|
393
406
|
|
|
394
|
-
# Use session working directory if no cwd specified and session_id provided
|
|
395
|
-
effective_cwd = cwd
|
|
396
|
-
if session_id and not cwd:
|
|
397
|
-
effective_cwd = self.get_working_dir(session_id)
|
|
398
|
-
self._log(f"Using session working directory: {effective_cwd}")
|
|
399
|
-
|
|
400
407
|
# Check working directory permissions if specified
|
|
401
|
-
if
|
|
402
|
-
if not os.path.isdir(
|
|
408
|
+
if cwd:
|
|
409
|
+
if not os.path.isdir(cwd):
|
|
403
410
|
return CommandResult(
|
|
404
411
|
return_code=1,
|
|
405
|
-
error_message=f"Working directory does not exist: {
|
|
412
|
+
error_message=f"Working directory does not exist: {cwd}",
|
|
406
413
|
)
|
|
407
414
|
|
|
408
|
-
if not self.permission_manager.is_path_allowed(
|
|
415
|
+
if not self.permission_manager.is_path_allowed(cwd):
|
|
409
416
|
return CommandResult(
|
|
410
|
-
return_code=1, error_message=f"Working directory not allowed: {
|
|
417
|
+
return_code=1, error_message=f"Working directory not allowed: {cwd}"
|
|
411
418
|
)
|
|
412
419
|
|
|
413
420
|
# Check if we need special handling for this interpreter
|
|
@@ -415,11 +422,11 @@ class CommandExecutor:
|
|
|
415
422
|
if interpreter_name in self.special_interpreters:
|
|
416
423
|
self._log(f"Using special handler for interpreter: {interpreter_name}")
|
|
417
424
|
special_handler = self.special_interpreters[interpreter_name]
|
|
418
|
-
return await special_handler(interpreter, script,
|
|
425
|
+
return await special_handler(interpreter, script, cwd, env, timeout)
|
|
419
426
|
|
|
420
427
|
# Regular execution
|
|
421
428
|
return await self._execute_script_with_stdin(
|
|
422
|
-
interpreter, script,
|
|
429
|
+
interpreter, script, cwd, shell_type, env, timeout, use_login_shell
|
|
423
430
|
)
|
|
424
431
|
|
|
425
432
|
async def _execute_script_with_stdin(
|
|
@@ -427,6 +434,7 @@ class CommandExecutor:
|
|
|
427
434
|
interpreter: str,
|
|
428
435
|
script: str,
|
|
429
436
|
cwd: str | None = None,
|
|
437
|
+
shell_type: str | None = None,
|
|
430
438
|
env: dict[str, str] | None = None,
|
|
431
439
|
timeout: float | None = 60.0,
|
|
432
440
|
use_login_shell: bool = True,
|
|
@@ -437,6 +445,7 @@ class CommandExecutor:
|
|
|
437
445
|
interpreter: The interpreter command
|
|
438
446
|
script: The script content
|
|
439
447
|
cwd: Optional working directory
|
|
448
|
+
shell_type: Optional shell to use (e.g., "cmd", "powershell", "wsl", "bash")
|
|
440
449
|
env: Optional environment variables
|
|
441
450
|
timeout: Optional timeout in seconds
|
|
442
451
|
use_login_shell: Whether to use login shell (loads ~/.zshrc, ~/.bashrc, etc.)
|
|
@@ -450,38 +459,57 @@ class CommandExecutor:
|
|
|
450
459
|
command_env.update(env)
|
|
451
460
|
|
|
452
461
|
try:
|
|
453
|
-
#
|
|
454
|
-
|
|
455
|
-
# Try to find the best available shell
|
|
456
|
-
user_shell = self._get_preferred_shell()
|
|
457
|
-
shell_basename = os.path.basename(user_shell)
|
|
462
|
+
# Get shell information
|
|
463
|
+
shell_basename, user_shell = self._get_system_shell(shell_type)
|
|
458
464
|
|
|
459
|
-
|
|
465
|
+
if sys.platform == "win32":
|
|
466
|
+
# On Windows, always use shell
|
|
467
|
+
self._log(f"Using shell on Windows for interpreter: {user_shell}")
|
|
460
468
|
|
|
461
|
-
#
|
|
462
|
-
shell_cmd =
|
|
469
|
+
# Format command using helper method for the interpreter
|
|
470
|
+
shell_cmd = self._format_win32_shell_command(
|
|
471
|
+
shell_basename, user_shell, interpreter, use_login_shell
|
|
472
|
+
)
|
|
463
473
|
|
|
464
474
|
# Create and run the process with shell
|
|
465
475
|
process = await asyncio.create_subprocess_shell(
|
|
466
476
|
shell_cmd,
|
|
467
|
-
stdout=asyncio.subprocess.PIPE,
|
|
468
|
-
stderr=asyncio.subprocess.PIPE,
|
|
469
|
-
cwd=effective_cwd,
|
|
470
|
-
env=command_env,
|
|
471
|
-
)
|
|
472
|
-
else:
|
|
473
|
-
# Parse the interpreter command to get arguments
|
|
474
|
-
interpreter_parts = shlex.split(interpreter)
|
|
475
|
-
|
|
476
|
-
# Create and run the process normally
|
|
477
|
-
process = await asyncio.create_subprocess_exec(
|
|
478
|
-
*interpreter_parts,
|
|
479
477
|
stdin=asyncio.subprocess.PIPE,
|
|
480
478
|
stdout=asyncio.subprocess.PIPE,
|
|
481
479
|
stderr=asyncio.subprocess.PIPE,
|
|
482
|
-
cwd=
|
|
480
|
+
cwd=cwd,
|
|
483
481
|
env=command_env,
|
|
484
482
|
)
|
|
483
|
+
else:
|
|
484
|
+
# Unix systems - original logic
|
|
485
|
+
if use_login_shell:
|
|
486
|
+
self._log(f"Using login shell for interpreter: {user_shell}")
|
|
487
|
+
|
|
488
|
+
# Create command that pipes script to interpreter through login shell
|
|
489
|
+
shell_cmd = f"{user_shell} -l -c '{interpreter}'"
|
|
490
|
+
|
|
491
|
+
# Create and run the process with shell
|
|
492
|
+
process = await asyncio.create_subprocess_shell(
|
|
493
|
+
shell_cmd,
|
|
494
|
+
stdin=asyncio.subprocess.PIPE,
|
|
495
|
+
stdout=asyncio.subprocess.PIPE,
|
|
496
|
+
stderr=asyncio.subprocess.PIPE,
|
|
497
|
+
cwd=cwd,
|
|
498
|
+
env=command_env,
|
|
499
|
+
)
|
|
500
|
+
else:
|
|
501
|
+
# Parse the interpreter command to get arguments
|
|
502
|
+
interpreter_parts = shlex.split(interpreter)
|
|
503
|
+
|
|
504
|
+
# Create and run the process normally
|
|
505
|
+
process = await asyncio.create_subprocess_exec(
|
|
506
|
+
*interpreter_parts,
|
|
507
|
+
stdin=asyncio.subprocess.PIPE,
|
|
508
|
+
stdout=asyncio.subprocess.PIPE,
|
|
509
|
+
stderr=asyncio.subprocess.PIPE,
|
|
510
|
+
cwd=cwd,
|
|
511
|
+
env=command_env,
|
|
512
|
+
)
|
|
485
513
|
|
|
486
514
|
# Wait for the process to complete with timeout
|
|
487
515
|
try:
|
|
@@ -592,11 +620,11 @@ class CommandExecutor:
|
|
|
592
620
|
script: str,
|
|
593
621
|
language: str,
|
|
594
622
|
cwd: str | None = None,
|
|
623
|
+
shell_type: str | None = None,
|
|
595
624
|
env: dict[str, str] | None = None,
|
|
596
625
|
timeout: float | None = 60.0,
|
|
597
626
|
args: list[str] | None = None,
|
|
598
627
|
use_login_shell: bool = True,
|
|
599
|
-
session_id: str | None = None,
|
|
600
628
|
) -> CommandResult:
|
|
601
629
|
"""Execute a script by writing it to a temporary file and executing it.
|
|
602
630
|
|
|
@@ -606,60 +634,18 @@ class CommandExecutor:
|
|
|
606
634
|
Args:
|
|
607
635
|
script: The script content
|
|
608
636
|
language: The script language (determines file extension and interpreter)
|
|
609
|
-
cwd: Optional working directory
|
|
637
|
+
cwd: Optional working directory
|
|
638
|
+
shell_type: Optional shell to use (e.g., "cmd", "powershell", "wsl", "bash")
|
|
610
639
|
env: Optional environment variables
|
|
611
640
|
timeout: Optional timeout in seconds
|
|
612
641
|
args: Optional command-line arguments
|
|
613
642
|
use_login_shell: Whether to use login shell. default true (loads ~/.zshrc, ~/.bashrc, etc.)
|
|
614
|
-
session_id: Optional session ID for persistent working directory
|
|
615
643
|
|
|
616
644
|
Returns:
|
|
617
645
|
CommandResult containing execution results
|
|
618
646
|
"""
|
|
619
|
-
#
|
|
620
|
-
|
|
621
|
-
if session_id and not cwd:
|
|
622
|
-
effective_cwd = self.get_working_dir(session_id)
|
|
623
|
-
self._log(f"Using session working directory: {effective_cwd}")
|
|
624
|
-
# Language to interpreter mapping
|
|
625
|
-
language_map: dict[str, dict[str, str]] = {
|
|
626
|
-
"python": {
|
|
627
|
-
"command": "python",
|
|
628
|
-
"extension": ".py",
|
|
629
|
-
},
|
|
630
|
-
"javascript": {
|
|
631
|
-
"command": "node",
|
|
632
|
-
"extension": ".js",
|
|
633
|
-
},
|
|
634
|
-
"typescript": {
|
|
635
|
-
"command": "ts-node",
|
|
636
|
-
"extension": ".ts",
|
|
637
|
-
},
|
|
638
|
-
"bash": {
|
|
639
|
-
"command": "bash",
|
|
640
|
-
"extension": ".sh",
|
|
641
|
-
},
|
|
642
|
-
"fish": {
|
|
643
|
-
"command": "fish",
|
|
644
|
-
"extension": ".fish",
|
|
645
|
-
},
|
|
646
|
-
"ruby": {
|
|
647
|
-
"command": "ruby",
|
|
648
|
-
"extension": ".rb",
|
|
649
|
-
},
|
|
650
|
-
"php": {
|
|
651
|
-
"command": "php",
|
|
652
|
-
"extension": ".php",
|
|
653
|
-
},
|
|
654
|
-
"perl": {
|
|
655
|
-
"command": "perl",
|
|
656
|
-
"extension": ".pl",
|
|
657
|
-
},
|
|
658
|
-
"r": {
|
|
659
|
-
"command": "Rscript",
|
|
660
|
-
"extension": ".R",
|
|
661
|
-
},
|
|
662
|
-
}
|
|
647
|
+
# Get language info from the centralized language map
|
|
648
|
+
language_map = self._get_language_map()
|
|
663
649
|
|
|
664
650
|
# Check if the language is supported
|
|
665
651
|
if language not in language_map:
|
|
@@ -670,9 +656,13 @@ class CommandExecutor:
|
|
|
670
656
|
|
|
671
657
|
# Get language info
|
|
672
658
|
language_info = language_map[language]
|
|
673
|
-
command = language_info["command"]
|
|
674
659
|
extension = language_info["extension"]
|
|
675
660
|
|
|
661
|
+
# Get interpreter command with full path if possible
|
|
662
|
+
command, language_args = self._get_interpreter_path(language, shell_type)
|
|
663
|
+
|
|
664
|
+
self._log(f"Interpreter path: {command} :: {language_args}")
|
|
665
|
+
|
|
676
666
|
# Set up environment
|
|
677
667
|
command_env: dict[str, str] = os.environ.copy()
|
|
678
668
|
if env:
|
|
@@ -686,48 +676,97 @@ class CommandExecutor:
|
|
|
686
676
|
_ = temp.write(script) # Explicitly ignore the return value
|
|
687
677
|
|
|
688
678
|
try:
|
|
689
|
-
#
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
679
|
+
# Normalize path for the current OS
|
|
680
|
+
temp_path = os.path.normpath(temp_path)
|
|
681
|
+
original_temp_path = temp_path
|
|
682
|
+
|
|
683
|
+
if sys.platform == "win32":
|
|
684
|
+
# Windows always uses shell
|
|
685
|
+
shell_basename, user_shell = self._get_system_shell(shell_type)
|
|
686
|
+
|
|
687
|
+
# Convert Windows path to WSL path if using WSL
|
|
688
|
+
if shell_basename in ["wsl", "wsl.exe"]:
|
|
689
|
+
match = re.match(r"([a-zA-Z]):\\(.*)", temp_path)
|
|
690
|
+
if match:
|
|
691
|
+
drive, path = match.groups()
|
|
692
|
+
wsl_path = f"/mnt/{drive.lower()}/{path.replace('\\', '/')}"
|
|
693
|
+
else:
|
|
694
|
+
wsl_path = temp_path.replace("\\", "/")
|
|
695
|
+
self._log(f"WSL path conversion may be incomplete: {wsl_path}")
|
|
694
696
|
|
|
695
|
-
|
|
697
|
+
self._log(
|
|
698
|
+
f"Converted Windows path '{temp_path}' to WSL path '{wsl_path}'"
|
|
699
|
+
)
|
|
700
|
+
temp_path = wsl_path
|
|
696
701
|
|
|
697
702
|
# Build the command including args
|
|
698
703
|
cmd = f"{command} {temp_path}"
|
|
704
|
+
if language_args:
|
|
705
|
+
cmd = f"{command} {' '.join(language_args)} {temp_path}"
|
|
699
706
|
if args:
|
|
700
707
|
cmd += " " + " ".join(args)
|
|
701
708
|
|
|
702
|
-
#
|
|
703
|
-
shell_cmd =
|
|
709
|
+
# Format command using helper method
|
|
710
|
+
shell_cmd = self._format_win32_shell_command(
|
|
711
|
+
shell_basename, user_shell, cmd, use_login_shell
|
|
712
|
+
)
|
|
704
713
|
|
|
705
|
-
self._log(
|
|
714
|
+
self._log(
|
|
715
|
+
f"Executing script from file on Windows with shell: {shell_cmd}"
|
|
716
|
+
)
|
|
706
717
|
|
|
707
718
|
# Create and run the process with shell
|
|
708
719
|
process = await asyncio.create_subprocess_shell(
|
|
709
720
|
shell_cmd,
|
|
710
721
|
stdout=asyncio.subprocess.PIPE,
|
|
711
722
|
stderr=asyncio.subprocess.PIPE,
|
|
712
|
-
cwd=
|
|
723
|
+
cwd=cwd,
|
|
713
724
|
env=command_env,
|
|
714
725
|
)
|
|
715
726
|
else:
|
|
716
|
-
#
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
727
|
+
# Unix systems - original logic
|
|
728
|
+
if use_login_shell:
|
|
729
|
+
# Get the user's login shell
|
|
730
|
+
shell_basename, user_shell = self._get_system_shell(shell_type)
|
|
720
731
|
|
|
721
|
-
|
|
732
|
+
# Build the command including args
|
|
733
|
+
cmd = f"{command} {temp_path}"
|
|
734
|
+
if language_args:
|
|
735
|
+
cmd = f"{command} {' '.join(language_args)} {temp_path}"
|
|
736
|
+
if args:
|
|
737
|
+
cmd += " " + " ".join(args)
|
|
722
738
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
739
|
+
# Create command that runs script through login shell
|
|
740
|
+
shell_cmd = f"{user_shell} -l -c '{cmd}'"
|
|
741
|
+
|
|
742
|
+
self._log(
|
|
743
|
+
f"Executing script from file with login shell: {shell_cmd}"
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
# Create and run the process with shell
|
|
747
|
+
process = await asyncio.create_subprocess_shell(
|
|
748
|
+
shell_cmd,
|
|
749
|
+
stdout=asyncio.subprocess.PIPE,
|
|
750
|
+
stderr=asyncio.subprocess.PIPE,
|
|
751
|
+
cwd=cwd,
|
|
752
|
+
env=command_env,
|
|
753
|
+
)
|
|
754
|
+
else:
|
|
755
|
+
# Build command arguments
|
|
756
|
+
cmd_args = [command] + language_args + [temp_path]
|
|
757
|
+
if args:
|
|
758
|
+
cmd_args.extend(args)
|
|
759
|
+
|
|
760
|
+
self._log(f"Executing script from file with: {' '.join(cmd_args)}")
|
|
761
|
+
|
|
762
|
+
# Create and run the process normally
|
|
763
|
+
process = await asyncio.create_subprocess_exec(
|
|
764
|
+
*cmd_args,
|
|
765
|
+
stdout=asyncio.subprocess.PIPE,
|
|
766
|
+
stderr=asyncio.subprocess.PIPE,
|
|
767
|
+
cwd=cwd,
|
|
768
|
+
env=command_env,
|
|
769
|
+
)
|
|
731
770
|
|
|
732
771
|
# Wait for the process to complete with timeout
|
|
733
772
|
try:
|
|
@@ -759,127 +798,139 @@ class CommandExecutor:
|
|
|
759
798
|
finally:
|
|
760
799
|
# Clean up temporary file
|
|
761
800
|
try:
|
|
762
|
-
os.unlink(
|
|
801
|
+
os.unlink(original_temp_path)
|
|
763
802
|
except Exception as e:
|
|
764
803
|
self._log(f"Error cleaning up temporary file: {str(e)}")
|
|
765
804
|
|
|
766
|
-
def
|
|
767
|
-
"""Get
|
|
805
|
+
def _get_language_map(self) -> dict[str, dict[str, str | list[str]]]:
|
|
806
|
+
"""Get the mapping of languages to interpreter information.
|
|
807
|
+
|
|
808
|
+
This is a single source of truth for language mappings used by
|
|
809
|
+
both execute_script_from_file and get_available_languages.
|
|
768
810
|
|
|
769
811
|
Returns:
|
|
770
|
-
|
|
812
|
+
Dictionary mapping language names to interpreter information
|
|
771
813
|
"""
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
"
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
814
|
+
return {
|
|
815
|
+
"python": {
|
|
816
|
+
"command": "python",
|
|
817
|
+
"extension": ".py",
|
|
818
|
+
"alternatives": ["python3"], # Alternative command names to try
|
|
819
|
+
},
|
|
820
|
+
"javascript": {
|
|
821
|
+
"command": "node",
|
|
822
|
+
"extension": ".js",
|
|
823
|
+
"alternatives": ["nodejs"],
|
|
824
|
+
},
|
|
825
|
+
"typescript": {
|
|
826
|
+
"command": "ts-node",
|
|
827
|
+
"extension": ".ts",
|
|
828
|
+
},
|
|
829
|
+
"bash": {
|
|
830
|
+
"command": "bash",
|
|
831
|
+
"extension": ".sh",
|
|
832
|
+
},
|
|
833
|
+
"fish": {
|
|
834
|
+
"command": "fish",
|
|
835
|
+
"extension": ".fish",
|
|
836
|
+
},
|
|
837
|
+
"ruby": {
|
|
838
|
+
"command": "ruby",
|
|
839
|
+
"extension": ".rb",
|
|
840
|
+
},
|
|
841
|
+
"php": {
|
|
842
|
+
"command": "php",
|
|
843
|
+
"extension": ".php",
|
|
844
|
+
},
|
|
845
|
+
"perl": {
|
|
846
|
+
"command": "perl",
|
|
847
|
+
"extension": ".pl",
|
|
848
|
+
},
|
|
849
|
+
"r": {"command": "Rscript", "extension": ".R", "alternatives": ["R"]},
|
|
850
|
+
# Windows-specific languages
|
|
851
|
+
"batch": {
|
|
852
|
+
"command": "cmd.exe",
|
|
853
|
+
"extension": ".bat",
|
|
854
|
+
"args": ["/c"],
|
|
855
|
+
},
|
|
856
|
+
"powershell": {
|
|
857
|
+
"command": "powershell.exe",
|
|
858
|
+
"extension": ".ps1",
|
|
859
|
+
"args": ["-ExecutionPolicy", "Bypass", "-File"],
|
|
860
|
+
"alternatives": ["pwsh.exe", "pwsh"],
|
|
861
|
+
},
|
|
783
862
|
}
|
|
784
|
-
return list(language_map.keys())
|
|
785
863
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
864
|
+
def _get_interpreter_path(
|
|
865
|
+
self, language: str, shell_type: str | None = None
|
|
866
|
+
) -> tuple[str, list[str]]:
|
|
867
|
+
"""Get the full path to the interpreter for the given language.
|
|
868
|
+
|
|
869
|
+
Attempts to find the full path to the interpreter command, but only for
|
|
870
|
+
Windows shell types (cmd, powershell). For WSL, just returns the command name.
|
|
792
871
|
|
|
793
872
|
Args:
|
|
794
|
-
|
|
873
|
+
language: The language name (e.g., "python", "javascript")
|
|
874
|
+
shell_type: Optional shell type (e.g., "wsl", "cmd", "powershell")
|
|
875
|
+
|
|
876
|
+
Returns:
|
|
877
|
+
Tuple of (interpreter_command, args) where:
|
|
878
|
+
- interpreter_command is either the full path to the interpreter or the command name
|
|
879
|
+
- args is a list of additional arguments to pass to the interpreter
|
|
795
880
|
"""
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
)
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
else:
|
|
853
|
-
return result.format_output()
|
|
854
|
-
|
|
855
|
-
# Script tool for executing scripts in various languages
|
|
856
|
-
@mcp_server.tool()
|
|
857
|
-
async def script_tool(
|
|
858
|
-
language: str,
|
|
859
|
-
script: str,
|
|
860
|
-
cwd: str,
|
|
861
|
-
ctx: MCPContext,
|
|
862
|
-
args: list[str] | None = None,
|
|
863
|
-
use_login_shell: bool = True,
|
|
864
|
-
) -> str:
|
|
865
|
-
tool_ctx = create_tool_context(ctx)
|
|
866
|
-
tool_ctx.set_tool_info("script_tool")
|
|
867
|
-
|
|
868
|
-
# Use request_id as session_id for persistent working directory
|
|
869
|
-
session_id = ctx.request_id
|
|
870
|
-
|
|
871
|
-
# Execute the script
|
|
872
|
-
result = await self.execute_script_from_file(
|
|
873
|
-
script=script,
|
|
874
|
-
language=language,
|
|
875
|
-
cwd=cwd,
|
|
876
|
-
timeout=30.0,
|
|
877
|
-
args=args,
|
|
878
|
-
use_login_shell=use_login_shell,
|
|
879
|
-
session_id=session_id
|
|
880
|
-
)
|
|
881
|
-
|
|
882
|
-
if result.is_success:
|
|
883
|
-
return result.stdout
|
|
884
|
-
else:
|
|
885
|
-
return result.format_output()
|
|
881
|
+
language_map = self._get_language_map()
|
|
882
|
+
|
|
883
|
+
if language not in language_map:
|
|
884
|
+
# Return the language name itself as a fallback
|
|
885
|
+
return language, []
|
|
886
|
+
|
|
887
|
+
language_info = language_map[language]
|
|
888
|
+
command = language_info["command"]
|
|
889
|
+
args = language_info.get("args", [])
|
|
890
|
+
alternatives = language_info.get("alternatives", [])
|
|
891
|
+
|
|
892
|
+
# Special handling for WSL - use command name directly (not Windows paths)
|
|
893
|
+
if shell_type and shell_type.lower() in ["wsl", "wsl.exe"]:
|
|
894
|
+
# For Python specifically, use python3 in WSL environments
|
|
895
|
+
if language.lower() == "python":
|
|
896
|
+
return "python3", args
|
|
897
|
+
# For other languages, just use the command name
|
|
898
|
+
return command, args
|
|
899
|
+
|
|
900
|
+
# For Windows shell types, try to find the full path
|
|
901
|
+
if sys.platform == "win32" and (
|
|
902
|
+
not shell_type
|
|
903
|
+
or shell_type.lower() in ["cmd", "powershell", "cmd.exe", "powershell.exe"]
|
|
904
|
+
):
|
|
905
|
+
try:
|
|
906
|
+
# Try to find the full path to the command
|
|
907
|
+
full_path = shutil.which(command)
|
|
908
|
+
if full_path:
|
|
909
|
+
self._log(
|
|
910
|
+
f"Found full path for {language} interpreter: {full_path}"
|
|
911
|
+
)
|
|
912
|
+
return full_path, args
|
|
913
|
+
|
|
914
|
+
# If primary command not found, try alternatives
|
|
915
|
+
for alt_command in alternatives:
|
|
916
|
+
alt_path = shutil.which(alt_command)
|
|
917
|
+
if alt_path:
|
|
918
|
+
self._log(
|
|
919
|
+
f"Found alternative path for {language} interpreter: {alt_path}"
|
|
920
|
+
)
|
|
921
|
+
return alt_path, args
|
|
922
|
+
except Exception as e:
|
|
923
|
+
self._log(f"Error finding path for {language} interpreter: {str(e)}")
|
|
924
|
+
|
|
925
|
+
# If we can't find the full path or it's not appropriate, return the command name
|
|
926
|
+
self._log(f"Using command name for {language} interpreter: {command}")
|
|
927
|
+
return command, args
|
|
928
|
+
|
|
929
|
+
def get_available_languages(self) -> list[str]:
|
|
930
|
+
"""Get a list of available script languages.
|
|
931
|
+
|
|
932
|
+
Returns:
|
|
933
|
+
List of supported language names
|
|
934
|
+
"""
|
|
935
|
+
# Use the centralized language map
|
|
936
|
+
return list(self._get_language_map().keys())
|