skydeckai-code 0.1.23__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.
- aidd/__init__.py +11 -0
- aidd/cli.py +141 -0
- aidd/server.py +54 -0
- aidd/tools/__init__.py +155 -0
- aidd/tools/base.py +18 -0
- aidd/tools/code_analysis.py +703 -0
- aidd/tools/code_execution.py +321 -0
- aidd/tools/directory_tools.py +289 -0
- aidd/tools/file_tools.py +784 -0
- aidd/tools/get_active_apps_tool.py +455 -0
- aidd/tools/get_available_windows_tool.py +395 -0
- aidd/tools/git_tools.py +687 -0
- aidd/tools/image_tools.py +127 -0
- aidd/tools/path_tools.py +86 -0
- aidd/tools/screenshot_tool.py +1029 -0
- aidd/tools/state.py +47 -0
- aidd/tools/system_tools.py +190 -0
- skydeckai_code-0.1.23.dist-info/METADATA +628 -0
- skydeckai_code-0.1.23.dist-info/RECORD +22 -0
- skydeckai_code-0.1.23.dist-info/WHEEL +4 -0
- skydeckai_code-0.1.23.dist-info/entry_points.txt +3 -0
- skydeckai_code-0.1.23.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,321 @@
|
|
1
|
+
import os
|
2
|
+
import stat
|
3
|
+
import subprocess
|
4
|
+
from typing import Any, Dict, List
|
5
|
+
|
6
|
+
import mcp.types as types
|
7
|
+
|
8
|
+
from .state import state
|
9
|
+
|
10
|
+
# Language configurations
|
11
|
+
LANGUAGE_CONFIGS = {
|
12
|
+
'python': {
|
13
|
+
'file_extension': '.py',
|
14
|
+
'command': ['python3'],
|
15
|
+
'comment_prefix': '#'
|
16
|
+
},
|
17
|
+
'javascript': {
|
18
|
+
'file_extension': '.js',
|
19
|
+
'command': ['node'],
|
20
|
+
'comment_prefix': '//'
|
21
|
+
},
|
22
|
+
'ruby': {
|
23
|
+
'file_extension': '.rb',
|
24
|
+
'command': ['ruby'],
|
25
|
+
'comment_prefix': '#'
|
26
|
+
},
|
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
|
+
}
|
47
|
+
|
48
|
+
def execute_code_tool() -> Dict[str, Any]:
|
49
|
+
return {
|
50
|
+
"name": "execute_code",
|
51
|
+
"description": (
|
52
|
+
"Execute arbitrary code in various programming languages on the user's local machine within the current working directory. "
|
53
|
+
"WHEN TO USE: When you need to run small code snippets to test functionality, compute values, process data, or "
|
54
|
+
"demonstrate how code works. Useful for quick prototyping, data transformations, or explaining programming concepts with running "
|
55
|
+
"examples. "
|
56
|
+
"WHEN NOT TO USE: When you need to modify files (use write_file or edit_file instead), when running potentially harmful "
|
57
|
+
"operations, or when you need to install external dependencies. "
|
58
|
+
"RETURNS: Text output including stdout, stderr, and exit code of the "
|
59
|
+
"execution. The output sections are clearly labeled with '=== stdout ===' and '=== stderr ==='. "
|
60
|
+
"Supported languages: " + ", ".join(LANGUAGE_CONFIGS.keys()) + ". "
|
61
|
+
"Always review the code carefully before execution to prevent unintended consequences. "
|
62
|
+
"Examples: "
|
63
|
+
"- Python: code='print(sum(range(10)))'. "
|
64
|
+
"- JavaScript: code='console.log(Array.from({length: 5}, (_, i) => i*2))'. "
|
65
|
+
"- Ruby: code='puts (1..5).reduce(:+)'. "
|
66
|
+
),
|
67
|
+
"inputSchema": {
|
68
|
+
"type": "object",
|
69
|
+
"properties": {
|
70
|
+
"language": {
|
71
|
+
"type": "string",
|
72
|
+
"enum": list(LANGUAGE_CONFIGS.keys()),
|
73
|
+
"description": "Programming language to use. Must be one of the supported languages: " + ", ".join(LANGUAGE_CONFIGS.keys()) + ". " +
|
74
|
+
"Each language requires the appropriate runtime to be installed on the user's machine. The code will be executed using: python3 for " +
|
75
|
+
"Python, node for JavaScript, ruby for Ruby, php for PHP, go run for Go, and rustc for Rust."
|
76
|
+
},
|
77
|
+
"code": {
|
78
|
+
"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."
|
82
|
+
},
|
83
|
+
"timeout": {
|
84
|
+
"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.",
|
87
|
+
"default": 5,
|
88
|
+
"minimum": 1,
|
89
|
+
"maximum": 30
|
90
|
+
}
|
91
|
+
},
|
92
|
+
"required": ["language", "code"]
|
93
|
+
}
|
94
|
+
}
|
95
|
+
|
96
|
+
def execute_shell_script_tool() -> Dict[str, Any]:
|
97
|
+
return {
|
98
|
+
"name": "execute_shell_script",
|
99
|
+
"description": (
|
100
|
+
"Execute a shell script (bash/sh) on the user's local machine within the current working directory. "
|
101
|
+
"WHEN TO USE: When you need to automate system tasks, run shell commands, interact with the operating system, or perform operations "
|
102
|
+
"that are best expressed as shell commands. Useful for file system operations, system configuration, or running system utilities. "
|
103
|
+
"WHEN NOT TO USE: When you need more structured programming (use execute_code instead), when you need to execute potentially "
|
104
|
+
"dangerous system operations, or when you want to run commands outside the allowed directory. "
|
105
|
+
"RETURNS: Text output including stdout, stderr, and exit code of the execution. The output sections are clearly labeled with "
|
106
|
+
"'=== stdout ===' and '=== stderr ==='. "
|
107
|
+
"This tool can execute shell commands and scripts for system automation and management tasks. "
|
108
|
+
"It is designed to perform tasks on the user's local environment, such as opening applications, installing packages and more. "
|
109
|
+
"Always review the script carefully before execution to prevent unintended consequences. "
|
110
|
+
"Examples: "
|
111
|
+
"- script='echo \"Current directory:\" && pwd'. "
|
112
|
+
"- script='for i in {1..5}; do echo $i; done'. "
|
113
|
+
),
|
114
|
+
"inputSchema": {
|
115
|
+
"type": "object",
|
116
|
+
"properties": {
|
117
|
+
"script": {
|
118
|
+
"type": "string",
|
119
|
+
"description": "Shell script to execute on the user's local machine. Can include any valid shell commands or scripts that would "
|
120
|
+
"run in a standard shell environment. The script is executed using /bin/sh for maximum compatibility across systems."
|
121
|
+
},
|
122
|
+
"timeout": {
|
123
|
+
"type": "integer",
|
124
|
+
"description": "Maximum execution time in seconds. The execution will be terminated if it exceeds this time limit. "
|
125
|
+
"Default is 300 seconds (5 minutes), with a maximum allowed value of 600 seconds (10 minutes).",
|
126
|
+
"default": 300,
|
127
|
+
"maximum": 600
|
128
|
+
}
|
129
|
+
},
|
130
|
+
"required": ["script"]
|
131
|
+
}
|
132
|
+
}
|
133
|
+
|
134
|
+
def is_command_available(command: str) -> bool:
|
135
|
+
"""Check if a command is available in the system."""
|
136
|
+
try:
|
137
|
+
subprocess.run(['which', command],
|
138
|
+
stdout=subprocess.PIPE,
|
139
|
+
stderr=subprocess.PIPE,
|
140
|
+
check=True)
|
141
|
+
return True
|
142
|
+
except subprocess.CalledProcessError:
|
143
|
+
return False
|
144
|
+
|
145
|
+
def prepare_code(code: str, language: str) -> str:
|
146
|
+
"""Prepare code for execution based on language requirements."""
|
147
|
+
config = LANGUAGE_CONFIGS[language]
|
148
|
+
|
149
|
+
if language == 'go':
|
150
|
+
if 'package main' not in code and 'func main()' not in code:
|
151
|
+
return f"{config['wrapper_start']}\n{code}\n{config['wrapper_end']}"
|
152
|
+
elif language == 'rust':
|
153
|
+
if 'fn main()' not in code:
|
154
|
+
return f"{config['wrapper_start']}\n{code}\n{config['wrapper_end']}"
|
155
|
+
elif language == 'php':
|
156
|
+
if '<?php' not in code:
|
157
|
+
return f"<?php\n{code}"
|
158
|
+
|
159
|
+
return code
|
160
|
+
|
161
|
+
async def execute_code_in_temp_file(language: str, code: str, timeout: int) -> tuple[str, str, int]:
|
162
|
+
"""Execute code in a temporary file and return stdout, stderr, and return code."""
|
163
|
+
config = LANGUAGE_CONFIGS[language]
|
164
|
+
temp_file = f"temp_script{config['file_extension']}"
|
165
|
+
|
166
|
+
try:
|
167
|
+
# Change to allowed directory first
|
168
|
+
os.chdir(state.allowed_directory)
|
169
|
+
|
170
|
+
# Write code to temp file
|
171
|
+
with open(temp_file, 'w') as f:
|
172
|
+
# Prepare and write code
|
173
|
+
prepared_code = prepare_code(code, language)
|
174
|
+
f.write(prepared_code)
|
175
|
+
f.flush()
|
176
|
+
|
177
|
+
# Prepare command
|
178
|
+
if language == 'rust':
|
179
|
+
# Special handling for Rust
|
180
|
+
output_path = 'temp_script.exe'
|
181
|
+
compile_cmd = ['rustc', temp_file, '-o', output_path]
|
182
|
+
try:
|
183
|
+
subprocess.run(compile_cmd,
|
184
|
+
check=True,
|
185
|
+
capture_output=True,
|
186
|
+
timeout=timeout)
|
187
|
+
cmd = [output_path]
|
188
|
+
except subprocess.CalledProcessError as e:
|
189
|
+
return '', e.stderr.decode(), e.returncode
|
190
|
+
else:
|
191
|
+
cmd = config['command'] + [temp_file]
|
192
|
+
|
193
|
+
# Execute code
|
194
|
+
try:
|
195
|
+
result = subprocess.run(
|
196
|
+
cmd,
|
197
|
+
capture_output=True,
|
198
|
+
timeout=timeout,
|
199
|
+
text=True,
|
200
|
+
)
|
201
|
+
return result.stdout, result.stderr, result.returncode
|
202
|
+
except subprocess.TimeoutExpired:
|
203
|
+
return '', f'Execution timed out after {timeout} seconds', 124
|
204
|
+
|
205
|
+
finally:
|
206
|
+
# Cleanup
|
207
|
+
# Note: We stay in the allowed directory as all operations should happen there
|
208
|
+
try:
|
209
|
+
os.unlink(temp_file)
|
210
|
+
if language == 'rust' and os.path.exists(output_path):
|
211
|
+
os.unlink(output_path)
|
212
|
+
except Exception:
|
213
|
+
pass
|
214
|
+
|
215
|
+
async def handle_execute_code(arguments: dict) -> List[types.TextContent]:
|
216
|
+
"""Handle code execution in various programming languages."""
|
217
|
+
language = arguments.get("language")
|
218
|
+
code = arguments.get("code")
|
219
|
+
timeout = arguments.get("timeout", 5)
|
220
|
+
|
221
|
+
if not language or not code:
|
222
|
+
raise ValueError("Both language and code must be provided")
|
223
|
+
|
224
|
+
if language not in LANGUAGE_CONFIGS:
|
225
|
+
raise ValueError(f"Unsupported language: {language}")
|
226
|
+
|
227
|
+
# Check if required command is available
|
228
|
+
command = LANGUAGE_CONFIGS[language]['command'][0]
|
229
|
+
if not is_command_available(command):
|
230
|
+
return [types.TextContent(
|
231
|
+
type="text",
|
232
|
+
text=f"Error: {command} is not installed on the system"
|
233
|
+
)]
|
234
|
+
|
235
|
+
try:
|
236
|
+
stdout, stderr, returncode = await execute_code_in_temp_file(language, code, timeout)
|
237
|
+
|
238
|
+
result = []
|
239
|
+
if stdout:
|
240
|
+
result.append(f"=== stdout ===\n{stdout.rstrip()}")
|
241
|
+
if stderr:
|
242
|
+
result.append(f"=== stderr ===\n{stderr.rstrip()}")
|
243
|
+
if not stdout and not stderr:
|
244
|
+
result.append("Code executed successfully with no output")
|
245
|
+
if returncode != 0:
|
246
|
+
result.append(f"\nProcess exited with code {returncode}")
|
247
|
+
|
248
|
+
return [types.TextContent(
|
249
|
+
type="text",
|
250
|
+
text="\n\n".join(result)
|
251
|
+
)]
|
252
|
+
|
253
|
+
except Exception as e:
|
254
|
+
return [types.TextContent(
|
255
|
+
type="text",
|
256
|
+
text=f"Error executing code:\n{str(e)}"
|
257
|
+
)]
|
258
|
+
|
259
|
+
async def execute_shell_script_in_temp_file(script: str, timeout: int) -> tuple[str, str, int]:
|
260
|
+
"""Execute a shell script in a temporary file and return stdout, stderr, and return code."""
|
261
|
+
temp_file = "temp_script.sh"
|
262
|
+
|
263
|
+
try:
|
264
|
+
# Change to allowed directory first
|
265
|
+
os.chdir(state.allowed_directory)
|
266
|
+
|
267
|
+
# Write script to temp file
|
268
|
+
with open(temp_file, 'w') as f:
|
269
|
+
f.write("#!/bin/sh\n") # Use sh for maximum compatibility
|
270
|
+
f.write(script)
|
271
|
+
f.flush()
|
272
|
+
|
273
|
+
# Make the script executable
|
274
|
+
os.chmod(temp_file, os.stat(temp_file).st_mode | stat.S_IEXEC)
|
275
|
+
|
276
|
+
# Execute script
|
277
|
+
try:
|
278
|
+
result = subprocess.run(
|
279
|
+
["/bin/sh", temp_file], # Use sh explicitly for consistent behavior
|
280
|
+
capture_output=True,
|
281
|
+
timeout=timeout,
|
282
|
+
text=True,
|
283
|
+
)
|
284
|
+
return result.stdout, result.stderr, result.returncode
|
285
|
+
except subprocess.TimeoutExpired:
|
286
|
+
return '', f'Execution timed out after {timeout} seconds', 124
|
287
|
+
|
288
|
+
finally:
|
289
|
+
# Cleanup
|
290
|
+
try:
|
291
|
+
os.unlink(temp_file)
|
292
|
+
except Exception:
|
293
|
+
pass
|
294
|
+
|
295
|
+
async def handle_execute_shell_script(arguments: dict) -> List[types.TextContent]:
|
296
|
+
"""Handle shell script execution."""
|
297
|
+
script = arguments.get("script")
|
298
|
+
timeout = min(arguments.get("timeout", 300), 600) # Default 5 minutes, cap at 10 minutes
|
299
|
+
|
300
|
+
try:
|
301
|
+
stdout, stderr, returncode = await execute_shell_script_in_temp_file(script, timeout)
|
302
|
+
result = []
|
303
|
+
if stdout:
|
304
|
+
result.append(f"=== stdout ===\n{stdout.rstrip()}")
|
305
|
+
if stderr:
|
306
|
+
result.append(f"=== stderr ===\n{stderr.rstrip()}")
|
307
|
+
if not stdout and not stderr:
|
308
|
+
result.append("Script executed successfully with no output")
|
309
|
+
if returncode != 0:
|
310
|
+
result.append(f"\nScript exited with code {returncode}")
|
311
|
+
|
312
|
+
return [types.TextContent(
|
313
|
+
type="text",
|
314
|
+
text="\n\n".join(result)
|
315
|
+
)]
|
316
|
+
|
317
|
+
except Exception as e:
|
318
|
+
return [types.TextContent(
|
319
|
+
type="text",
|
320
|
+
text=f"Error executing shell script:\n{str(e)}"
|
321
|
+
)]
|
@@ -0,0 +1,289 @@
|
|
1
|
+
import json
|
2
|
+
import os
|
3
|
+
import subprocess
|
4
|
+
from datetime import datetime
|
5
|
+
|
6
|
+
from mcp.types import TextContent
|
7
|
+
|
8
|
+
from .state import state
|
9
|
+
|
10
|
+
|
11
|
+
def list_directory_tool():
|
12
|
+
return {
|
13
|
+
"name": "list_directory",
|
14
|
+
"description": "Get a detailed listing of files and directories in the specified path, including type, size, and modification "
|
15
|
+
"date. WHEN TO USE: When you need to explore the contents of a directory, understand what files are available, check file sizes or "
|
16
|
+
"modification dates, or locate specific files by name. WHEN NOT TO USE: When you need to read the contents of files (use read_file "
|
17
|
+
"instead), when you need a recursive listing of all subdirectories (use directory_tree instead), or when searching for files by name pattern "
|
18
|
+
"(use search_files instead). RETURNS: Text with each line containing file type ([DIR]/[FILE]), name, size (in B/KB/MB), and "
|
19
|
+
"modification date. Only works within the allowed directory. Example: Enter 'src' to list contents of the src directory, or '.' for "
|
20
|
+
"current directory.",
|
21
|
+
"inputSchema": {
|
22
|
+
"type": "object",
|
23
|
+
"properties": {
|
24
|
+
"path": {
|
25
|
+
"type": "string",
|
26
|
+
"description": "Path of the directory to list. Examples: '.' for current directory, 'src' for src directory, 'docs/images' for a nested directory. The path must be within the allowed workspace.",
|
27
|
+
}
|
28
|
+
},
|
29
|
+
"required": ["path"]
|
30
|
+
},
|
31
|
+
}
|
32
|
+
|
33
|
+
async def handle_list_directory(arguments: dict):
|
34
|
+
from mcp.types import TextContent
|
35
|
+
|
36
|
+
path = arguments.get("path", ".")
|
37
|
+
|
38
|
+
# Determine full path based on whether input is absolute or relative
|
39
|
+
if os.path.isabs(path):
|
40
|
+
full_path = os.path.abspath(path) # Just normalize the absolute path
|
41
|
+
else:
|
42
|
+
# For relative paths, join with allowed_directory
|
43
|
+
full_path = os.path.abspath(os.path.join(state.allowed_directory, path))
|
44
|
+
|
45
|
+
if not full_path.startswith(state.allowed_directory):
|
46
|
+
raise ValueError(f"Access denied: Path ({full_path}) must be within allowed directory ({state.allowed_directory})")
|
47
|
+
if not os.path.exists(full_path):
|
48
|
+
raise ValueError(f"Path does not exist: {full_path}")
|
49
|
+
if not os.path.isdir(full_path):
|
50
|
+
raise ValueError(f"Path is not a directory: {full_path}")
|
51
|
+
|
52
|
+
# List directory contents
|
53
|
+
entries = []
|
54
|
+
try:
|
55
|
+
with os.scandir(full_path) as it:
|
56
|
+
for entry in it:
|
57
|
+
try:
|
58
|
+
stat = entry.stat()
|
59
|
+
# Format size to be human readable
|
60
|
+
size = stat.st_size
|
61
|
+
if size >= 1024 * 1024: # MB
|
62
|
+
size_str = f"{size / (1024 * 1024):.1f}MB"
|
63
|
+
elif size >= 1024: # KB
|
64
|
+
size_str = f"{size / 1024:.1f}KB"
|
65
|
+
else: # bytes
|
66
|
+
size_str = f"{size}B"
|
67
|
+
|
68
|
+
entry_type = "[DIR]" if entry.is_dir() else "[FILE]"
|
69
|
+
mod_time = datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M:%S")
|
70
|
+
entries.append(f"{entry_type} {entry.name:<30} {size_str:>8} {mod_time}")
|
71
|
+
except (OSError, PermissionError):
|
72
|
+
continue
|
73
|
+
|
74
|
+
entries.sort() # Sort entries alphabetically
|
75
|
+
return [TextContent(type="text", text="\n".join(entries))]
|
76
|
+
|
77
|
+
except PermissionError:
|
78
|
+
raise ValueError(f"Permission denied accessing: {full_path}")
|
79
|
+
|
80
|
+
def create_directory_tool():
|
81
|
+
return {
|
82
|
+
"name": "create_directory",
|
83
|
+
"description": "Create a new directory or ensure a directory exists. "
|
84
|
+
"Can create multiple nested directories in one operation. "
|
85
|
+
"WHEN TO USE: When you need to set up project structure, organize files, create output directories before saving files, or establish a directory hierarchy. "
|
86
|
+
"WHEN NOT TO USE: When you only want to check if a directory exists (use get_file_info instead), or when trying to create directories outside the allowed workspace. "
|
87
|
+
"RETURNS: Text message confirming either that the directory was successfully created or that it already exists. "
|
88
|
+
"The operation succeeds silently if the directory already exists. "
|
89
|
+
"Only works within the allowed directory. "
|
90
|
+
"Example: Enter 'src/components' to create nested directories.",
|
91
|
+
"inputSchema": {
|
92
|
+
"type": "object",
|
93
|
+
"properties": {
|
94
|
+
"path": {
|
95
|
+
"type": "string",
|
96
|
+
"description": "Path of the directory to create. Can include nested directories which will all be created. Examples: 'logs' for a simple directory, 'src/components/buttons' for nested directories. Both absolute and relative paths are supported, but must be within the allowed workspace."
|
97
|
+
}
|
98
|
+
},
|
99
|
+
"required": ["path"]
|
100
|
+
},
|
101
|
+
}
|
102
|
+
|
103
|
+
async def handle_create_directory(arguments: dict):
|
104
|
+
"""Handle creating a new directory."""
|
105
|
+
from mcp.types import TextContent
|
106
|
+
|
107
|
+
path = arguments.get("path")
|
108
|
+
if not path:
|
109
|
+
raise ValueError("path must be provided")
|
110
|
+
|
111
|
+
# Determine full path based on whether input is absolute or relative
|
112
|
+
if os.path.isabs(path):
|
113
|
+
full_path = os.path.abspath(path) # Just normalize the absolute path
|
114
|
+
else:
|
115
|
+
# For relative paths, join with allowed_directory
|
116
|
+
full_path = os.path.abspath(os.path.join(state.allowed_directory, path))
|
117
|
+
|
118
|
+
# Security check: ensure path is within allowed directory
|
119
|
+
if not full_path.startswith(state.allowed_directory):
|
120
|
+
raise ValueError(
|
121
|
+
f"Access denied: Path ({full_path}) must be within allowed directory ({state.allowed_directory})"
|
122
|
+
)
|
123
|
+
|
124
|
+
already_exists = os.path.exists(full_path)
|
125
|
+
|
126
|
+
try:
|
127
|
+
# Create directory and any necessary parent directories
|
128
|
+
os.makedirs(full_path, exist_ok=True)
|
129
|
+
|
130
|
+
if already_exists:
|
131
|
+
return [TextContent(type="text", text=f"Directory already exists: {path}")]
|
132
|
+
return [TextContent(
|
133
|
+
type="text",
|
134
|
+
text=f"Successfully created directory: {path}"
|
135
|
+
)]
|
136
|
+
except PermissionError:
|
137
|
+
raise ValueError(f"Permission denied creating directory: {path}")
|
138
|
+
except Exception as e:
|
139
|
+
raise ValueError(f"Error creating directory: {str(e)}")
|
140
|
+
|
141
|
+
def directory_tree_tool():
|
142
|
+
return {
|
143
|
+
"name": "directory_tree",
|
144
|
+
"description": "Get a recursive tree view of files and directories in the specified path as a JSON structure. "
|
145
|
+
"WHEN TO USE: When you need to understand the complete structure of a directory tree, visualize the hierarchy of files and directories, or get a comprehensive overview of a project's organization. "
|
146
|
+
"Particularly useful for large projects where you need to see nested relationships. "
|
147
|
+
"WHEN NOT TO USE: When you only need a flat list of files in a single directory (use directory_listing instead), or when you're only interested in specific file types (use search_files instead). "
|
148
|
+
"RETURNS: JSON structure where each entry includes 'name', 'type' (file/directory), and 'children' for directories. "
|
149
|
+
"Files have no children array, while directories always have a children array (which may be empty). "
|
150
|
+
"The output is formatted with 2-space indentation for readability. For Git repositories, shows tracked files only. "
|
151
|
+
"Only works within the allowed directory. "
|
152
|
+
"Example: Enter '.' for current directory, or 'src' for a specific directory.",
|
153
|
+
"inputSchema": {
|
154
|
+
"type": "object",
|
155
|
+
"properties": {
|
156
|
+
"path": {
|
157
|
+
"type": "string",
|
158
|
+
"description": "Root directory to analyze. This is the starting point for the recursive tree generation. Examples: '.' for current directory, 'src' for the src directory. Both absolute and relative paths are supported, but must be within the allowed workspace."
|
159
|
+
}
|
160
|
+
},
|
161
|
+
"required": ["path"]
|
162
|
+
},
|
163
|
+
}
|
164
|
+
|
165
|
+
async def build_directory_tree(dir_path: str) -> dict:
|
166
|
+
"""Build directory tree as a JSON structure."""
|
167
|
+
try:
|
168
|
+
entries = list(os.scandir(dir_path))
|
169
|
+
# Sort entries by name
|
170
|
+
entries.sort(key=lambda e: e.name.lower())
|
171
|
+
|
172
|
+
result = {
|
173
|
+
"name": os.path.basename(dir_path) or dir_path,
|
174
|
+
"type": "directory",
|
175
|
+
"children": []
|
176
|
+
}
|
177
|
+
|
178
|
+
for entry in entries:
|
179
|
+
if entry.is_dir():
|
180
|
+
# Recursively process subdirectories
|
181
|
+
child_tree = await build_directory_tree(entry.path)
|
182
|
+
result["children"].append(child_tree)
|
183
|
+
else:
|
184
|
+
result["children"].append({
|
185
|
+
"name": entry.name,
|
186
|
+
"type": "file"
|
187
|
+
})
|
188
|
+
|
189
|
+
return result
|
190
|
+
except PermissionError:
|
191
|
+
raise ValueError(f"Access denied: {dir_path}")
|
192
|
+
except Exception as e:
|
193
|
+
raise ValueError(f"Error processing directory {dir_path}: {str(e)}")
|
194
|
+
|
195
|
+
async def handle_directory_tree(arguments: dict):
|
196
|
+
"""Handle building a directory tree."""
|
197
|
+
path = arguments.get("path", ".")
|
198
|
+
|
199
|
+
# Validate and get full path
|
200
|
+
full_path = os.path.abspath(os.path.join(state.allowed_directory, path))
|
201
|
+
if not os.path.abspath(full_path).startswith(state.allowed_directory):
|
202
|
+
raise ValueError(f"Access denied: Path ({full_path}) must be within allowed directory ({state.allowed_directory})")
|
203
|
+
if not os.path.exists(full_path):
|
204
|
+
raise ValueError(f"Path does not exist: {full_path}")
|
205
|
+
if not os.path.isdir(full_path):
|
206
|
+
raise ValueError(f"Path is not a directory: {full_path}")
|
207
|
+
|
208
|
+
# Try git ls-files first
|
209
|
+
try:
|
210
|
+
# Get list of all files tracked by git
|
211
|
+
result = subprocess.run(
|
212
|
+
['git', 'ls-files'],
|
213
|
+
cwd=full_path,
|
214
|
+
capture_output=True,
|
215
|
+
text=True,
|
216
|
+
check=True,
|
217
|
+
)
|
218
|
+
|
219
|
+
# If git command was successful
|
220
|
+
files = [f for f in result.stdout.split('\n') if f.strip()]
|
221
|
+
files.sort()
|
222
|
+
|
223
|
+
# Build tree from git files
|
224
|
+
directory_map = {}
|
225
|
+
root_name = os.path.basename(full_path) or full_path
|
226
|
+
|
227
|
+
# First pass: collect all directories and files
|
228
|
+
for file in files:
|
229
|
+
parts = file.split(os.sep)
|
230
|
+
# Add all intermediate directories
|
231
|
+
for i in range(len(parts)):
|
232
|
+
parent = os.sep.join(parts[:i])
|
233
|
+
os.sep.join(parts[:i+1])
|
234
|
+
if i < len(parts) - 1: # It's a directory
|
235
|
+
directory_map.setdefault(parent, {"dirs": set(), "files": set()})["dirs"].add(parts[i])
|
236
|
+
else: # It's a file
|
237
|
+
directory_map.setdefault(parent, {"dirs": set(), "files": set()})["files"].add(parts[i])
|
238
|
+
|
239
|
+
async def build_git_tree(current_path: str) -> dict:
|
240
|
+
dir_name = current_path.split(os.sep)[-1] if current_path else ''
|
241
|
+
result = {
|
242
|
+
"name": dir_name or root_name,
|
243
|
+
"type": "directory",
|
244
|
+
"children": [],
|
245
|
+
}
|
246
|
+
|
247
|
+
if current_path not in directory_map:
|
248
|
+
return result
|
249
|
+
|
250
|
+
entry = directory_map[current_path]
|
251
|
+
|
252
|
+
# Add directories first
|
253
|
+
for dir_name in sorted(entry["dirs"]):
|
254
|
+
child_path = os.path.join(current_path, dir_name) if current_path else dir_name
|
255
|
+
child_tree = await build_git_tree(child_path)
|
256
|
+
result["children"].append(child_tree)
|
257
|
+
|
258
|
+
# Then add files
|
259
|
+
for file_name in sorted(entry["files"]):
|
260
|
+
result["children"].append({
|
261
|
+
"name": file_name,
|
262
|
+
"type": "file",
|
263
|
+
})
|
264
|
+
|
265
|
+
return result
|
266
|
+
|
267
|
+
# Build the tree structure starting from root
|
268
|
+
tree = await build_git_tree('')
|
269
|
+
return [TextContent(type="text", text=json.dumps(tree, indent=2))]
|
270
|
+
|
271
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
272
|
+
# Git not available or not a git repository, use fallback implementation
|
273
|
+
pass
|
274
|
+
except Exception as e:
|
275
|
+
# Log the error but continue with fallback
|
276
|
+
print(f"Error using git ls-files: {e}")
|
277
|
+
pass
|
278
|
+
|
279
|
+
# Fallback to regular directory traversal
|
280
|
+
try:
|
281
|
+
# Build the directory tree structure
|
282
|
+
tree = await build_directory_tree(full_path)
|
283
|
+
|
284
|
+
# Convert to JSON with pretty printing
|
285
|
+
json_tree = json.dumps(tree, indent=2)
|
286
|
+
|
287
|
+
return [TextContent(type="text", text=json_tree)]
|
288
|
+
except Exception as e:
|
289
|
+
raise ValueError(f"Error building directory tree: {str(e)}")
|