skydeckai-code 0.1.38__tar.gz → 0.1.39__tar.gz
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.
- {skydeckai_code-0.1.38 → skydeckai_code-0.1.39}/PKG-INFO +1 -1
- {skydeckai_code-0.1.38 → skydeckai_code-0.1.39}/pyproject.toml +1 -1
- {skydeckai_code-0.1.38 → skydeckai_code-0.1.39}/src/aidd/tools/code_execution.py +135 -98
- {skydeckai_code-0.1.38 → skydeckai_code-0.1.39}/src/aidd/tools/code_tools.py +53 -7
- skydeckai_code-0.1.39/src/aidd/tools/directory_tools.py +434 -0
- {skydeckai_code-0.1.38 → skydeckai_code-0.1.39}/src/aidd/tools/system_tools.py +4 -1
- skydeckai_code-0.1.38/src/aidd/tools/directory_tools.py +0 -289
- {skydeckai_code-0.1.38 → skydeckai_code-0.1.39}/.claude/settings.local.json +0 -0
- {skydeckai_code-0.1.38 → skydeckai_code-0.1.39}/.gitignore +0 -0
- {skydeckai_code-0.1.38 → skydeckai_code-0.1.39}/Dockerfile +0 -0
- {skydeckai_code-0.1.38 → skydeckai_code-0.1.39}/LICENSE +0 -0
- {skydeckai_code-0.1.38 → skydeckai_code-0.1.39}/README.md +0 -0
- {skydeckai_code-0.1.38 → skydeckai_code-0.1.39}/screenshots/skydeck_ai_helper.png +0 -0
- {skydeckai_code-0.1.38 → skydeckai_code-0.1.39}/smithery.yaml +0 -0
- {skydeckai_code-0.1.38 → skydeckai_code-0.1.39}/src/__init__.py +0 -0
- {skydeckai_code-0.1.38 → skydeckai_code-0.1.39}/src/aidd/__init__.py +0 -0
- {skydeckai_code-0.1.38 → skydeckai_code-0.1.39}/src/aidd/cli.py +0 -0
- {skydeckai_code-0.1.38 → skydeckai_code-0.1.39}/src/aidd/server.py +0 -0
- {skydeckai_code-0.1.38 → skydeckai_code-0.1.39}/src/aidd/tools/__init__.py +0 -0
- {skydeckai_code-0.1.38 → skydeckai_code-0.1.39}/src/aidd/tools/base.py +0 -0
- {skydeckai_code-0.1.38 → skydeckai_code-0.1.39}/src/aidd/tools/code_analysis.py +0 -0
- {skydeckai_code-0.1.38 → skydeckai_code-0.1.39}/src/aidd/tools/file_tools.py +0 -0
- {skydeckai_code-0.1.38 → skydeckai_code-0.1.39}/src/aidd/tools/get_active_apps_tool.py +0 -0
- {skydeckai_code-0.1.38 → skydeckai_code-0.1.39}/src/aidd/tools/get_available_windows_tool.py +0 -0
- {skydeckai_code-0.1.38 → skydeckai_code-0.1.39}/src/aidd/tools/image_tools.py +0 -0
- {skydeckai_code-0.1.38 → skydeckai_code-0.1.39}/src/aidd/tools/other_tools.py +0 -0
- {skydeckai_code-0.1.38 → skydeckai_code-0.1.39}/src/aidd/tools/path_tools.py +0 -0
- {skydeckai_code-0.1.38 → skydeckai_code-0.1.39}/src/aidd/tools/screenshot_tool.py +0 -0
- {skydeckai_code-0.1.38 → skydeckai_code-0.1.39}/src/aidd/tools/state.py +0 -0
- {skydeckai_code-0.1.38 → skydeckai_code-0.1.39}/src/aidd/tools/web_tools.py +0 -0
- {skydeckai_code-0.1.38 → skydeckai_code-0.1.39}/uv.lock +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: skydeckai-code
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.39
|
4
4
|
Summary: This MCP server provides a comprehensive set of tools for AI-driven Development workflows including file operations, code analysis, multi-language execution, web content fetching with HTML-to-markdown conversion, multi-engine web search, code content searching, and system information retrieval.
|
5
5
|
Project-URL: Homepage, https://github.com/skydeckai/skydeckai-code
|
6
6
|
Project-URL: Repository, https://github.com/skydeckai/skydeckai-code
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "skydeckai-code"
|
3
|
-
version = "0.1.
|
3
|
+
version = "0.1.39"
|
4
4
|
description = "This MCP server provides a comprehensive set of tools for AI-driven Development workflows including file operations, code analysis, multi-language execution, web content fetching with HTML-to-markdown conversion, multi-engine web search, code content searching, and system information retrieval."
|
5
5
|
readme = "README.md"
|
6
6
|
requires-python = ">=3.11"
|
@@ -2,49 +2,28 @@ import os
|
|
2
2
|
import stat
|
3
3
|
import subprocess
|
4
4
|
from typing import Any, Dict, List
|
5
|
-
|
6
5
|
import mcp.types as types
|
6
|
+
from pathlib import Path
|
7
7
|
|
8
8
|
from .state import state
|
9
9
|
|
10
10
|
# Language configurations
|
11
11
|
LANGUAGE_CONFIGS = {
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
},
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
'file_extension': '.rb',
|
24
|
-
'command': ['ruby'],
|
25
|
-
'comment_prefix': '#'
|
12
|
+
"python": {"file_extension": ".py", "command": ["python3"], "comment_prefix": "#"},
|
13
|
+
"javascript": {"file_extension": ".js", "command": ["node"], "comment_prefix": "//"},
|
14
|
+
"ruby": {"file_extension": ".rb", "command": ["ruby"], "comment_prefix": "#"},
|
15
|
+
"php": {"file_extension": ".php", "command": ["php"], "comment_prefix": "//"},
|
16
|
+
"go": {"file_extension": ".go", "command": ["go", "run"], "comment_prefix": "//", "wrapper_start": "package main\nfunc main() {", "wrapper_end": "}"},
|
17
|
+
"rust": {
|
18
|
+
"file_extension": ".rs",
|
19
|
+
"command": ["rustc", "-o"], # Special handling needed
|
20
|
+
"comment_prefix": "//",
|
21
|
+
"wrapper_start": "fn main() {",
|
22
|
+
"wrapper_end": "}",
|
26
23
|
},
|
27
|
-
'php': {
|
28
|
-
'file_extension': '.php',
|
29
|
-
'command': ['php'],
|
30
|
-
'comment_prefix': '//'
|
31
|
-
},
|
32
|
-
'go': {
|
33
|
-
'file_extension': '.go',
|
34
|
-
'command': ['go', 'run'],
|
35
|
-
'comment_prefix': '//',
|
36
|
-
'wrapper_start': 'package main\nfunc main() {',
|
37
|
-
'wrapper_end': '}'
|
38
|
-
},
|
39
|
-
'rust': {
|
40
|
-
'file_extension': '.rs',
|
41
|
-
'command': ['rustc', '-o'], # Special handling needed
|
42
|
-
'comment_prefix': '//',
|
43
|
-
'wrapper_start': 'fn main() {',
|
44
|
-
'wrapper_end': '}'
|
45
|
-
}
|
46
24
|
}
|
47
25
|
|
26
|
+
|
48
27
|
def execute_code_tool() -> Dict[str, Any]:
|
49
28
|
return {
|
50
29
|
"name": "execute_code",
|
@@ -70,29 +49,31 @@ def execute_code_tool() -> Dict[str, Any]:
|
|
70
49
|
"language": {
|
71
50
|
"type": "string",
|
72
51
|
"enum": list(LANGUAGE_CONFIGS.keys()),
|
73
|
-
"description": "Programming language to use. Must be one of the supported languages: "
|
74
|
-
"
|
75
|
-
"
|
52
|
+
"description": "Programming language to use. Must be one of the supported languages: "
|
53
|
+
+ ", ".join(LANGUAGE_CONFIGS.keys())
|
54
|
+
+ ". "
|
55
|
+
+ "Each language requires the appropriate runtime to be installed on the user's machine. The code will be executed using: python3 for "
|
56
|
+
+ "Python, node for JavaScript, ruby for Ruby, php for PHP, go run for Go, and rustc for Rust.",
|
76
57
|
},
|
77
58
|
"code": {
|
78
59
|
"type": "string",
|
79
|
-
"description": "Code to execute on the user's local machine in the current working directory. The code will be saved to a "
|
80
|
-
"temporary file and executed within the allowed workspace. For Go and Rust, main function wrappers will be added automatically if "
|
81
|
-
"not present. For PHP, <?php will be prepended if not present."
|
60
|
+
"description": "Code to execute on the user's local machine in the current working directory. The code will be saved to a "
|
61
|
+
+ "temporary file and executed within the allowed workspace. For Go and Rust, main function wrappers will be added automatically if "
|
62
|
+
+ "not present. For PHP, <?php will be prepended if not present.",
|
82
63
|
},
|
83
64
|
"timeout": {
|
84
65
|
"type": "integer",
|
85
|
-
"description": "Maximum execution time in seconds. The execution will be terminated if it exceeds this time limit, returning a " +
|
86
|
-
"timeout message. Must be between 1 and 30 seconds.",
|
66
|
+
"description": "Maximum execution time in seconds. The execution will be terminated if it exceeds this time limit, returning a " + "timeout message. Must be between 1 and 30 seconds.",
|
87
67
|
"default": 5,
|
88
68
|
"minimum": 1,
|
89
|
-
"maximum": 30
|
90
|
-
}
|
69
|
+
"maximum": 30,
|
70
|
+
},
|
91
71
|
},
|
92
|
-
"required": ["language", "code"]
|
93
|
-
}
|
72
|
+
"required": ["language", "code"],
|
73
|
+
},
|
94
74
|
}
|
95
75
|
|
76
|
+
|
96
77
|
def execute_shell_script_tool() -> Dict[str, Any]:
|
97
78
|
return {
|
98
79
|
"name": "execute_shell_script",
|
@@ -122,47 +103,46 @@ def execute_shell_script_tool() -> Dict[str, Any]:
|
|
122
103
|
"script": {
|
123
104
|
"type": "string",
|
124
105
|
"description": "Shell script to execute on the user's local machine. Can include any valid shell commands or scripts that would "
|
125
|
-
"run in a standard shell environment. The script is executed using /bin/sh for maximum compatibility across systems."
|
106
|
+
"run in a standard shell environment. The script is executed using /bin/sh for maximum compatibility across systems.",
|
126
107
|
},
|
127
108
|
"timeout": {
|
128
109
|
"type": "integer",
|
129
|
-
"description": "Maximum execution time in seconds. The execution will be terminated if it exceeds this time limit. "
|
130
|
-
"Default is 300 seconds (5 minutes), with a maximum allowed value of 600 seconds (10 minutes).",
|
110
|
+
"description": "Maximum execution time in seconds. The execution will be terminated if it exceeds this time limit. Default is 300 seconds (5 minutes), with a maximum allowed value of 600 seconds (10 minutes).",
|
131
111
|
"default": 300,
|
132
|
-
"maximum": 600
|
133
|
-
}
|
112
|
+
"maximum": 600,
|
113
|
+
},
|
134
114
|
},
|
135
|
-
"required": ["script"]
|
136
|
-
}
|
115
|
+
"required": ["script"],
|
116
|
+
},
|
137
117
|
}
|
138
118
|
|
119
|
+
|
139
120
|
def is_command_available(command: str) -> bool:
|
140
121
|
"""Check if a command is available in the system."""
|
141
122
|
try:
|
142
|
-
subprocess.run([
|
143
|
-
stdout=subprocess.PIPE,
|
144
|
-
stderr=subprocess.PIPE,
|
145
|
-
check=True)
|
123
|
+
subprocess.run(["which", command], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
|
146
124
|
return True
|
147
125
|
except subprocess.CalledProcessError:
|
148
126
|
return False
|
149
127
|
|
128
|
+
|
150
129
|
def prepare_code(code: str, language: str) -> str:
|
151
130
|
"""Prepare code for execution based on language requirements."""
|
152
131
|
config = LANGUAGE_CONFIGS[language]
|
153
132
|
|
154
|
-
if language ==
|
155
|
-
if
|
133
|
+
if language == "go":
|
134
|
+
if "package main" not in code and "func main()" not in code:
|
156
135
|
return f"{config['wrapper_start']}\n{code}\n{config['wrapper_end']}"
|
157
|
-
elif language ==
|
158
|
-
if
|
136
|
+
elif language == "rust":
|
137
|
+
if "fn main()" not in code:
|
159
138
|
return f"{config['wrapper_start']}\n{code}\n{config['wrapper_end']}"
|
160
|
-
elif language ==
|
161
|
-
if
|
139
|
+
elif language == "php":
|
140
|
+
if "<?php" not in code:
|
162
141
|
return f"<?php\n{code}"
|
163
142
|
|
164
143
|
return code
|
165
144
|
|
145
|
+
|
166
146
|
async def execute_code_in_temp_file(language: str, code: str, timeout: int) -> tuple[str, str, int]:
|
167
147
|
"""Execute code in a temporary file and return stdout, stderr, and return code."""
|
168
148
|
config = LANGUAGE_CONFIGS[language]
|
@@ -173,27 +153,24 @@ async def execute_code_in_temp_file(language: str, code: str, timeout: int) -> t
|
|
173
153
|
os.chdir(state.allowed_directory)
|
174
154
|
|
175
155
|
# Write code to temp file
|
176
|
-
with open(temp_file,
|
156
|
+
with open(temp_file, "w") as f:
|
177
157
|
# Prepare and write code
|
178
158
|
prepared_code = prepare_code(code, language)
|
179
159
|
f.write(prepared_code)
|
180
160
|
f.flush()
|
181
161
|
|
182
162
|
# Prepare command
|
183
|
-
if language ==
|
163
|
+
if language == "rust":
|
184
164
|
# Special handling for Rust
|
185
|
-
output_path =
|
186
|
-
compile_cmd = [
|
165
|
+
output_path = "temp_script.exe"
|
166
|
+
compile_cmd = ["rustc", temp_file, "-o", output_path]
|
187
167
|
try:
|
188
|
-
subprocess.run(compile_cmd,
|
189
|
-
check=True,
|
190
|
-
capture_output=True,
|
191
|
-
timeout=timeout)
|
168
|
+
subprocess.run(compile_cmd, check=True, capture_output=True, timeout=timeout)
|
192
169
|
cmd = [output_path]
|
193
170
|
except subprocess.CalledProcessError as e:
|
194
|
-
return
|
171
|
+
return "", e.stderr.decode(), e.returncode
|
195
172
|
else:
|
196
|
-
cmd = config[
|
173
|
+
cmd = config["command"] + [temp_file]
|
197
174
|
|
198
175
|
# Execute code
|
199
176
|
try:
|
@@ -205,18 +182,19 @@ async def execute_code_in_temp_file(language: str, code: str, timeout: int) -> t
|
|
205
182
|
)
|
206
183
|
return result.stdout, result.stderr, result.returncode
|
207
184
|
except subprocess.TimeoutExpired:
|
208
|
-
return
|
185
|
+
return "", f"Execution timed out after {timeout} seconds", 124
|
209
186
|
|
210
187
|
finally:
|
211
188
|
# Cleanup
|
212
189
|
# Note: We stay in the allowed directory as all operations should happen there
|
213
190
|
try:
|
214
191
|
os.unlink(temp_file)
|
215
|
-
if language ==
|
192
|
+
if language == "rust" and os.path.exists(output_path):
|
216
193
|
os.unlink(output_path)
|
217
194
|
except Exception:
|
218
195
|
pass
|
219
196
|
|
197
|
+
|
220
198
|
async def handle_execute_code(arguments: dict) -> List[types.TextContent]:
|
221
199
|
"""Handle code execution in various programming languages."""
|
222
200
|
language = arguments.get("language")
|
@@ -230,12 +208,9 @@ async def handle_execute_code(arguments: dict) -> List[types.TextContent]:
|
|
230
208
|
raise ValueError(f"Unsupported language: {language}")
|
231
209
|
|
232
210
|
# Check if required command is available
|
233
|
-
command = LANGUAGE_CONFIGS[language][
|
211
|
+
command = LANGUAGE_CONFIGS[language]["command"][0]
|
234
212
|
if not is_command_available(command):
|
235
|
-
return [types.TextContent(
|
236
|
-
type="text",
|
237
|
-
text=f"Error: {command} is not installed on the system"
|
238
|
-
)]
|
213
|
+
return [types.TextContent(type="text", text=f"Error: {command} is not installed on the system")]
|
239
214
|
|
240
215
|
try:
|
241
216
|
stdout, stderr, returncode = await execute_code_in_temp_file(language, code, timeout)
|
@@ -250,16 +225,11 @@ async def handle_execute_code(arguments: dict) -> List[types.TextContent]:
|
|
250
225
|
if returncode != 0:
|
251
226
|
result.append(f"\nProcess exited with code {returncode}")
|
252
227
|
|
253
|
-
return [types.TextContent(
|
254
|
-
type="text",
|
255
|
-
text="\n\n".join(result)
|
256
|
-
)]
|
228
|
+
return [types.TextContent(type="text", text="\n\n".join(result))]
|
257
229
|
|
258
230
|
except Exception as e:
|
259
|
-
return [types.TextContent(
|
260
|
-
|
261
|
-
text=f"Error executing code:\n{str(e)}"
|
262
|
-
)]
|
231
|
+
return [types.TextContent(type="text", text=f"Error executing code:\n{str(e)}")]
|
232
|
+
|
263
233
|
|
264
234
|
async def execute_shell_script_in_temp_file(script: str, timeout: int) -> tuple[str, str, int]:
|
265
235
|
"""Execute a shell script in a temporary file and return stdout, stderr, and return code."""
|
@@ -270,7 +240,7 @@ async def execute_shell_script_in_temp_file(script: str, timeout: int) -> tuple[
|
|
270
240
|
os.chdir(state.allowed_directory)
|
271
241
|
|
272
242
|
# Write script to temp file
|
273
|
-
with open(temp_file,
|
243
|
+
with open(temp_file, "w") as f:
|
274
244
|
f.write("#!/bin/sh\n") # Use sh for maximum compatibility
|
275
245
|
f.write(script)
|
276
246
|
f.flush()
|
@@ -280,15 +250,19 @@ async def execute_shell_script_in_temp_file(script: str, timeout: int) -> tuple[
|
|
280
250
|
|
281
251
|
# Execute script
|
282
252
|
try:
|
253
|
+
path_env = get_comprehensive_shell_paths()
|
254
|
+
env = os.environ.copy()
|
255
|
+
env["PATH"] = path_env
|
283
256
|
result = subprocess.run(
|
284
257
|
["/bin/sh", temp_file], # Use sh explicitly for consistent behavior
|
285
258
|
capture_output=True,
|
286
259
|
timeout=timeout,
|
287
260
|
text=True,
|
261
|
+
env=env,
|
288
262
|
)
|
289
263
|
return result.stdout, result.stderr, result.returncode
|
290
264
|
except subprocess.TimeoutExpired:
|
291
|
-
return
|
265
|
+
return "", f"Execution timed out after {timeout} seconds", 124
|
292
266
|
|
293
267
|
finally:
|
294
268
|
# Cleanup
|
@@ -297,6 +271,7 @@ async def execute_shell_script_in_temp_file(script: str, timeout: int) -> tuple[
|
|
297
271
|
except Exception:
|
298
272
|
pass
|
299
273
|
|
274
|
+
|
300
275
|
async def handle_execute_shell_script(arguments: dict) -> List[types.TextContent]:
|
301
276
|
"""Handle shell script execution."""
|
302
277
|
script = arguments.get("script")
|
@@ -314,13 +289,75 @@ async def handle_execute_shell_script(arguments: dict) -> List[types.TextContent
|
|
314
289
|
if returncode != 0:
|
315
290
|
result.append(f"\nScript exited with code {returncode}")
|
316
291
|
|
317
|
-
return [types.TextContent(
|
318
|
-
type="text",
|
319
|
-
text="\n\n".join(result)
|
320
|
-
)]
|
292
|
+
return [types.TextContent(type="text", text="\n\n".join(result))]
|
321
293
|
|
322
294
|
except Exception as e:
|
323
|
-
return [types.TextContent(
|
324
|
-
|
325
|
-
|
326
|
-
|
295
|
+
return [types.TextContent(type="text", text=f"Error executing shell script:\n{str(e)}")]
|
296
|
+
|
297
|
+
|
298
|
+
def get_comprehensive_shell_paths():
|
299
|
+
"""
|
300
|
+
Get PATH from shells using login mode to capture initialization files
|
301
|
+
"""
|
302
|
+
shells_file = Path("/etc/shells")
|
303
|
+
|
304
|
+
if not shells_file.exists():
|
305
|
+
return os.environ.get("PATH", "")
|
306
|
+
|
307
|
+
# Read available shells
|
308
|
+
try:
|
309
|
+
with open(shells_file, "r") as f:
|
310
|
+
shells = [line.strip() for line in f if line.strip() and not line.startswith("#")]
|
311
|
+
except IOError:
|
312
|
+
return os.environ.get("PATH", "")
|
313
|
+
|
314
|
+
all_paths = []
|
315
|
+
|
316
|
+
# Get PATH from each shell as login shell
|
317
|
+
for shell in shells:
|
318
|
+
if not os.path.exists(shell):
|
319
|
+
continue
|
320
|
+
|
321
|
+
# Different approaches for different shells
|
322
|
+
commands_to_try = [
|
323
|
+
[shell, "--login", "-i", "-c", "echo $PATH"],
|
324
|
+
[shell, "-l", "-i", "-c", "echo $PATH"],
|
325
|
+
[shell, "--login", "-c", "echo $PATH"],
|
326
|
+
[shell, "-l", "-c", "echo $PATH"],
|
327
|
+
[shell, "-c", "echo $PATH"],
|
328
|
+
]
|
329
|
+
|
330
|
+
# Try each command until one works
|
331
|
+
for cmd in commands_to_try:
|
332
|
+
try:
|
333
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
|
334
|
+
if result.returncode == 0 and result.stdout.strip():
|
335
|
+
shell_path = result.stdout.strip()
|
336
|
+
if shell_path and shell_path != "$PATH":
|
337
|
+
all_paths.extend(shell_path.split(":"))
|
338
|
+
break
|
339
|
+
except (subprocess.SubprocessError, subprocess.TimeoutExpired):
|
340
|
+
continue
|
341
|
+
|
342
|
+
# Add current PATH
|
343
|
+
current_path = os.environ.get("PATH", "")
|
344
|
+
if current_path:
|
345
|
+
all_paths.extend(current_path.split(":"))
|
346
|
+
|
347
|
+
# Remove duplicates while preserving order
|
348
|
+
return deduplicate_paths(all_paths)
|
349
|
+
|
350
|
+
|
351
|
+
def deduplicate_paths(all_paths):
|
352
|
+
"""
|
353
|
+
Remove duplicates while preserving order
|
354
|
+
"""
|
355
|
+
seen = set()
|
356
|
+
merged_path = []
|
357
|
+
for path in all_paths:
|
358
|
+
path = path.strip()
|
359
|
+
if path and path not in seen and os.path.exists(path):
|
360
|
+
seen.add(path)
|
361
|
+
merged_path.append(path)
|
362
|
+
|
363
|
+
return ":".join(merged_path)
|
@@ -5,7 +5,8 @@ import subprocess
|
|
5
5
|
import json
|
6
6
|
from datetime import datetime
|
7
7
|
from typing import List, Dict, Any, Optional, Union, Tuple
|
8
|
-
|
8
|
+
import platform
|
9
|
+
import stat
|
9
10
|
from mcp.types import TextContent
|
10
11
|
from .state import state
|
11
12
|
|
@@ -65,7 +66,14 @@ def search_code_tool():
|
|
65
66
|
"Examples: '.' for current directory, 'src' to search only within src directory. "
|
66
67
|
"Default is the root of the allowed directory.",
|
67
68
|
"default": "."
|
68
|
-
}
|
69
|
+
},
|
70
|
+
"include_hidden": {
|
71
|
+
"type": "boolean",
|
72
|
+
"description": "Whether to include hidden files. When true, also include the hidden files like"
|
73
|
+
".env, .config on Unix/Posix/Linux, or files with hidden attribute on Windows"
|
74
|
+
"Default is false, which exclude the hidden files from search",
|
75
|
+
"default": False,
|
76
|
+
},
|
69
77
|
},
|
70
78
|
"required": ["patterns"]
|
71
79
|
}
|
@@ -80,6 +88,7 @@ async def handle_search_code(arguments: dict) -> List[TextContent]:
|
|
80
88
|
max_results = arguments.get("max_results", 100)
|
81
89
|
case_sensitive = arguments.get("case_sensitive", False)
|
82
90
|
path = arguments.get("path", ".")
|
91
|
+
include_hidden = arguments.get("include_hidden", False)
|
83
92
|
|
84
93
|
if not patterns:
|
85
94
|
raise ValueError("At least one pattern must be provided")
|
@@ -109,12 +118,12 @@ async def handle_search_code(arguments: dict) -> List[TextContent]:
|
|
109
118
|
# Use ripgrep if available for faster results
|
110
119
|
try:
|
111
120
|
result = await _search_with_ripgrep(
|
112
|
-
pattern, include, exclude, max_results, case_sensitive, full_path
|
121
|
+
pattern, include, exclude, max_results, case_sensitive, full_path, include_hidden
|
113
122
|
)
|
114
123
|
except (subprocess.SubprocessError, FileNotFoundError):
|
115
124
|
# Fallback to Python implementation if ripgrep not available
|
116
125
|
result = await _search_with_python(
|
117
|
-
pattern, include, exclude, max_results, case_sensitive, full_path
|
126
|
+
pattern, include, exclude, max_results, case_sensitive, full_path, include_hidden
|
118
127
|
)
|
119
128
|
|
120
129
|
# Add pattern header for multiple patterns
|
@@ -139,7 +148,8 @@ async def _search_with_ripgrep(
|
|
139
148
|
exclude: str,
|
140
149
|
max_results: int,
|
141
150
|
case_sensitive: bool,
|
142
|
-
full_path: str
|
151
|
+
full_path: str,
|
152
|
+
include_hidden: bool
|
143
153
|
) -> List[TextContent]:
|
144
154
|
"""Search using ripgrep for better performance."""
|
145
155
|
cmd = ["rg", "--line-number"]
|
@@ -161,6 +171,10 @@ async def _search_with_ripgrep(
|
|
161
171
|
# Add max results
|
162
172
|
cmd.extend(["--max-count", str(max_results)])
|
163
173
|
|
174
|
+
# Add hidden files
|
175
|
+
if include_hidden:
|
176
|
+
cmd.append("--hidden")
|
177
|
+
|
164
178
|
# Add pattern and path
|
165
179
|
cmd.extend([pattern, full_path])
|
166
180
|
|
@@ -247,7 +261,8 @@ async def _search_with_python(
|
|
247
261
|
exclude: str,
|
248
262
|
max_results: int,
|
249
263
|
case_sensitive: bool,
|
250
|
-
full_path: str
|
264
|
+
full_path: str,
|
265
|
+
include_hidden: bool
|
251
266
|
) -> List[TextContent]:
|
252
267
|
"""Fallback search implementation using Python's regex and file operations."""
|
253
268
|
# Compile the regex pattern
|
@@ -273,10 +288,17 @@ async def _search_with_python(
|
|
273
288
|
match_count = 0
|
274
289
|
|
275
290
|
# Walk the directory tree
|
276
|
-
for root,
|
291
|
+
for root, dirs, files in os.walk(full_path):
|
277
292
|
if match_count >= max_results:
|
278
293
|
break
|
279
294
|
|
295
|
+
if not include_hidden:
|
296
|
+
# Remove hidden directories from dirs list to prevent os.walk from entering them
|
297
|
+
dirs[:] = [d for d in dirs if not is_hidden(os.path.join(root, d))]
|
298
|
+
|
299
|
+
# Filter out hidden files
|
300
|
+
files = [f for f in files if not is_hidden(os.path.join(root, f))]
|
301
|
+
|
280
302
|
for filename in files:
|
281
303
|
if match_count >= max_results:
|
282
304
|
break
|
@@ -371,3 +393,27 @@ async def _search_with_python(
|
|
371
393
|
type="text",
|
372
394
|
text="\n".join(formatted_output)
|
373
395
|
)]
|
396
|
+
|
397
|
+
|
398
|
+
def is_hidden_windows(filepath: str) -> bool:
|
399
|
+
"""Check if file/folder is hidden on Windows"""
|
400
|
+
try:
|
401
|
+
attrs = os.stat(filepath).st_file_attributes
|
402
|
+
return attrs & stat.FILE_ATTRIBUTE_HIDDEN
|
403
|
+
except (AttributeError, OSError):
|
404
|
+
return False
|
405
|
+
|
406
|
+
|
407
|
+
def is_hidden_unix(name: str) -> bool:
|
408
|
+
"""Check if file/folder is hidden on Unix-like systems (Linux/macOS)"""
|
409
|
+
return name.startswith('.')
|
410
|
+
|
411
|
+
|
412
|
+
def is_hidden(filepath: str) -> bool:
|
413
|
+
"""Cross-platform hidden file/folder detection"""
|
414
|
+
name = os.path.basename(filepath)
|
415
|
+
|
416
|
+
if platform.system() == 'Windows':
|
417
|
+
return is_hidden_windows(filepath) or name.startswith('.')
|
418
|
+
else:
|
419
|
+
return is_hidden_unix(name)
|