import { promises as fs } from "node:fs"; import path from "node:path"; // Utility to normalize and validate paths within a contained root directory const normalizePath = (inputPath, chrootDir) => { // Resolve chroot directory const chrootResolved = path.resolve(chrootDir); // Handle root path ("/") as the chroot directory itself if (inputPath === "/" || inputPath === "") { return chrootResolved; } // Resolve input path relative to chroot directory const resolved = path.join(chrootResolved, inputPath); const normalized = path.normalize(resolved); // Ensure the path is within chrootDir if (!normalized.startsWith(chrootResolved)) { throw new Error(`Path escapes root boundary: ${inputPath}`); } return normalized; }; // Convert an absolute path under the contained root to a user-display path (root-relative) const toDisplayPath = (absPath, chrootDir) => { const rel = path.relative(path.resolve(chrootDir), absPath); if (!rel || rel === "") return "/"; return `/${rel}`; }; // Main recursive directory listing function async function listEntriesRecursive(startDir, chrootDir, maxDepth, includeHidden = false) { const results = []; const seen = new Set(); // Track paths to prevent symlink cycles async function walk(currentDir, currentDepth) { if (currentDepth > maxDepth) return; let dirents; try { dirents = await fs.readdir(currentDir, { withFileTypes: true }); } catch (err) { throw new Error(`Failed to read directory: ${toDisplayPath(currentDir, chrootDir)}`); } for (const dirent of dirents) { // Skip hidden files/directories unless includeHidden is true if (!includeHidden && dirent.name.startsWith(".")) continue; const fullPath = path.join(currentDir, dirent.name); // Ensure fullPath is within chroot if (!fullPath.startsWith(path.resolve(chrootDir))) { continue; // Skip paths outside chroot } const relPath = path.relative(startDir, fullPath) || "."; // Prevent symlink cycles if (dirent.isSymbolicLink()) { const realPath = await fs.realpath(fullPath).catch(() => null); // Ensure realPath is within chroot if (realPath && !realPath.startsWith(path.resolve(chrootDir))) { continue; // Skip symlinks pointing outside chroot } if (realPath && seen.has(realPath)) continue; if (realPath) seen.add(realPath); results.push([relPath, "l", null]); continue; // Don't recurse into symlinks } if (dirent.isDirectory()) { seen.add(fullPath); results.push([relPath, "d", null]); if (currentDepth < maxDepth) { await walk(fullPath, currentDepth + 1); } } else if (dirent.isFile()) { let size = null; try { const stat = await fs.stat(fullPath); size = stat.size; } catch { // Size remains null if stat fails } results.push([relPath, "f", size]); } else { results.push([relPath, "o", null]); } } } await walk(startDir, 0); return results; } export default { type: "function", name: "list_files", description: "List files and directories recursively from the root with customizable options", parameters: { type: "object", properties: { path: { type: ["string", "null"], description: "Directory or file path relative to the root. Use '/' for the root. Defaults to root if not specified.", }, depth: { type: ["integer", "null"], description: "Maximum subdirectory levels to traverse. Use -1 for unlimited depth. Defaults to 1.", minimum: -1, }, includeHidden: { type: ["boolean", "null"], description: "Whether to include hidden files and directories (starting with '.'). Defaults to false.", default: false, } }, required: [], additionalProperties: false, }, strict: false, }; export async function run(args) { const inputPath = args?.path || ""; const depth = Number.isInteger(args?.depth) ? args.depth : 1; const includeHidden = args?.includeHidden ?? false; const chrootPath = '/workspaces/aiTools/root'; if (!chrootPath) { return { err: "Root path is required" }; } if (depth < -1) { return { err: `Depth must be >= -1, received ${args?.depth}` }; } let chrootResolved; try { chrootResolved = path.resolve(chrootPath); // Auto-create the chroot base directory if it does not exist await fs.mkdir(chrootResolved, { recursive: true }); } catch (err) { return { err: "Failed to initialize root directory" }; } let resolvedBase; try { resolvedBase = normalizePath(inputPath, chrootResolved); } catch (err) { return { err: err.message }; } let stat; try { stat = await fs.lstat(resolvedBase); } catch (err) { return { err: `Path does not exist${inputPath ? `: ${inputPath}` : ""}` }; } const cwd = toDisplayPath( stat.isFile() ? path.dirname(resolvedBase) : resolvedBase, chrootResolved ); // Handle single file case if (stat.isFile()) { const fileName = path.basename(resolvedBase); if (!includeHidden && fileName.startsWith(".")) { return { cwd, files: [] }; } // Return structured object for easier machine parsing return { cwd, files: [{ path: fileName, type: 'f', size: stat.size }] }; } // Handle non-directory case if (!stat.isDirectory()) { return { err: `Not a file or directory${inputPath ? `: ${inputPath}` : ""}` }; } // Handle directory case try { const files = await listEntriesRecursive(resolvedBase, chrootResolved, depth === -1 ? Infinity : depth, includeHidden); // Map to structured objects and sort by path for consistent output const mapped = files .map(([p, t, s]) => ({ path: p, type: t, size: s })) .sort((a, b) => a.path.localeCompare(b.path)); return { cwd, files: mapped, }; } catch (err) { return { err: "Failed to list files" }; } }