diff --git a/cecli/tools/ls.py b/cecli/tools/ls.py index 9a9ed276340..443f74acd67 100644 --- a/cecli/tools/ls.py +++ b/cecli/tools/ls.py @@ -11,67 +11,78 @@ class Tool(BaseTool): SCHEMA = { "type": "function", "function": { - "name": "Ls", - "description": "List files in a directory.", + "name": "ls", + "description": "List files in a directory. Paths are relative to the project root.", "parameters": { "type": "object", "properties": { - "directory": { + "path": { "type": "string", - "description": "The directory to list.", - }, + "description": ( + "The path of the directory to list, relative to the project root. " + "Defaults to the project root." + ), + "default": ".", + } }, - "required": ["directory"], + "required": [], }, }, } @classmethod - def execute(cls, coder, dir_path=None, directory=None, **kwargs): - # Handle both positional and keyword arguments for backward compatibility - if dir_path is None and directory is not None: - dir_path = directory - elif dir_path is None: - return "Error: Missing directory parameter" + def execute(cls, coder, path=None, directory=None, **kwargs): """ List files in directory and optionally add some to context. This provides information about the structure of the codebase, similar to how a developer would explore directories. """ + # Handle both positional and keyword arguments for backward compatibility + dir_path = path or directory or "." + try: - # Make the path relative to root if it's absolute - if dir_path.startswith("/"): - rel_dir = os.path.relpath(dir_path, coder.root) - else: - rel_dir = dir_path + # Create an absolute path from the provided relative path + abs_path = os.path.abspath(os.path.join(coder.root, dir_path)) - # Get absolute path - abs_dir = coder.abs_root_path(rel_dir) + # Security check: ensure the resolved path is within the project root + if not abs_path.startswith(os.path.abspath(coder.root)): + coder.io.tool_error( + f"Error: Path '{dir_path}' attempts to access files outside the project root." + ) + return "Error: Path is outside the project root." # Check if path exists - if not os.path.exists(abs_dir): - coder.io.tool_output(f"⚠️ Directory '{dir_path}' not found") + if not os.path.exists(abs_path): + coder.io.tool_output(f"⚠️ Path '{dir_path}' not found") return "Directory not found" # Get directory contents contents = [] - try: - with os.scandir(abs_dir) as entries: - for entry in entries: - if entry.is_file() and not entry.name.startswith("."): - rel_path = os.path.join(rel_dir, entry.name) - contents.append(rel_path) - except NotADirectoryError: - # If it's a file, just return the file - contents = [rel_dir] + if os.path.isdir(abs_path): + # It's a directory, list its contents + try: + with os.scandir(abs_path) as entries: + for entry in entries: + if entry.is_file() and not entry.name.startswith("."): + rel_path = os.path.relpath(entry.path, coder.root) + contents.append(rel_path) + except OSError as e: + coder.io.tool_error(f"Error listing directory '{dir_path}': {e}") + return f"Error: {e}" + elif os.path.isfile(abs_path): + # It's a file, just return its relative path + contents.append(os.path.relpath(abs_path, coder.root)) if contents: coder.io.tool_output(f"📋 Listed {len(contents)} file(s) in '{dir_path}'") - if len(contents) > 10: - return f"Found {len(contents)} files: {', '.join(contents[:10])}..." + sorted_contents = sorted(contents) + if len(sorted_contents) > 10: + return ( + f"Found {len(sorted_contents)} files: {', '.join(sorted_contents[:10])}..." + ) else: - return f"Found {len(contents)} files: {', '.join(contents)}" + return f"Found {len(sorted_contents)} files: {', '.join(sorted_contents)}" else: coder.io.tool_output(f"📋 No files found in '{dir_path}'") return "No files found in directory"