Files
toolLooper/tools/list_files.js

186 lines
5.8 KiB
JavaScript

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';
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)}` };
}
}