import { promises as fs } from "node:fs"; import path from "node:path"; // Utility to normalize and validate paths within chroot 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 chroot boundary: ${inputPath}`); } return normalized; }; // 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: ${currentDir} (${err?.message || String(err)})`); } 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 within a chroot directory with customizable options", parameters: { type: "object", properties: { path: { type: "string", description: "Directory or file path to list relative to chroot. Use '/' for the chroot root. Defaults to chroot root if not specified.", }, depth: { type: "integer", description: "Maximum subdirectory levels to traverse. Use -1 for unlimited depth. Defaults to 1.", minimum: -1, }, includeHidden: { type: "boolean", description: "Whether to include hidden files and directories (starting with '.'). Defaults to false.", default: false, } }, required: ["path", "depth", "includeHidden"], additionalProperties: false, }, strict: true, }; 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: "Chroot 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 prepare chroot path: ${chrootPath} (${err?.message || String(err)})` }; } 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: ${resolvedBase} (${err?.message || String(err)})` }; } const cwd = path.relative(chrootResolved, stat.isFile() ? path.dirname(resolvedBase) : resolvedBase) || "."; // 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: ${resolvedBase}` }; } // 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: ${err?.message || String(err)}` }; } }