192 lines
6.2 KiB
TypeScript
192 lines
6.2 KiB
TypeScript
// src/system/list.ts
|
|
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
|
|
// Licensed under the Apache License, Version 2.0
|
|
|
|
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
|
|
import type { IAiLogger, IToolArguments, IToolDefinition } from "@gadget/ai";
|
|
import { GadgetTool } from "../tool.ts";
|
|
import { getProjectRoot, resolveProjectPath, toolError } from "./common.ts";
|
|
|
|
const MAX_ENTRIES = 1000;
|
|
|
|
export class ListTool extends GadgetTool {
|
|
get name(): string {
|
|
return "list";
|
|
}
|
|
|
|
get category(): string {
|
|
return "system";
|
|
}
|
|
|
|
public definition: IToolDefinition = {
|
|
type: "function",
|
|
function: {
|
|
name: this.name,
|
|
description: "List project directory contents with optional filtering. Shows file types, sizes, and modification dates. Supports filtering by glob pattern.",
|
|
parameters: {
|
|
type: "object",
|
|
properties: {
|
|
path: {
|
|
type: "string",
|
|
description: "Directory path to list, relative to the project root. Defaults to project root.",
|
|
},
|
|
pattern: {
|
|
type: "string",
|
|
description: "Optional glob pattern to filter results (e.g., '*.ts', 'src/**/*').",
|
|
},
|
|
recursive: {
|
|
type: "boolean",
|
|
description: "List subdirectories recursively (default: false).",
|
|
},
|
|
showHidden: {
|
|
type: "boolean",
|
|
description: "Show hidden files (files starting with dot) (default: false).",
|
|
},
|
|
maxDepth: {
|
|
type: "number",
|
|
description: "Maximum directory depth for recursive listing (default: 3).",
|
|
},
|
|
},
|
|
required: [],
|
|
},
|
|
},
|
|
};
|
|
|
|
public async execute(args: IToolArguments, logger: IAiLogger): Promise<string> {
|
|
const projectRoot = getProjectRoot(this.toolbox);
|
|
if (!projectRoot) {
|
|
return toolError({
|
|
code: "OPERATION_NOT_ALLOWED",
|
|
message: "No active project workspace is configured for file tools.",
|
|
recoveryHint: "Run this tool during an active Agent work order.",
|
|
});
|
|
}
|
|
|
|
let targetPath: string;
|
|
if (args.path !== undefined && typeof args.path === "string") {
|
|
const resolved = resolveProjectPath(this.toolbox, args.path);
|
|
if (typeof resolved === "string") return resolved;
|
|
targetPath = resolved.absolutePath;
|
|
} else {
|
|
targetPath = path.resolve(projectRoot);
|
|
}
|
|
|
|
const patternStr = typeof args.pattern === "string" ? args.pattern : undefined;
|
|
let pattern: RegExp | undefined;
|
|
if (patternStr) {
|
|
try {
|
|
const globPattern = patternStr
|
|
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
.replace(/\*/g, ".*")
|
|
.replace(/\?/g, ".");
|
|
pattern = new RegExp("^" + globPattern + "$");
|
|
} catch {
|
|
// invalid glob pattern — skip filtering
|
|
}
|
|
}
|
|
|
|
const recursive = args.recursive === true;
|
|
const showHidden = args.showHidden === true;
|
|
const maxDepth = typeof args.maxDepth === "number" ? args.maxDepth : 3;
|
|
const displayPath = path.relative(projectRoot, targetPath) || ".";
|
|
|
|
try {
|
|
const stat = await fs.stat(targetPath);
|
|
if (!stat.isDirectory()) {
|
|
return toolError({
|
|
code: "INVALID_PARAMETER",
|
|
message: `"${displayPath}" is not a directory.`,
|
|
parameter: "path",
|
|
recoveryHint: "Provide a directory path to list.",
|
|
});
|
|
}
|
|
|
|
const entries = await this.listDirectory(targetPath, pattern, recursive, showHidden, 0, maxDepth, projectRoot);
|
|
const limited = entries.slice(0, MAX_ENTRIES);
|
|
|
|
if (entries.length === 0) {
|
|
return `No entries found in "${displayPath}"`;
|
|
}
|
|
|
|
const truncated = entries.length > MAX_ENTRIES;
|
|
const lines: string[] = [
|
|
`Contents of "${displayPath}" (${truncated ? `showing first ${MAX_ENTRIES} of ` : ""}${entries.length} entries):`,
|
|
"",
|
|
];
|
|
|
|
for (const entry of limited) {
|
|
const typeIndicator = entry.isDirectory ? "d" : entry.isSymlink ? "l" : "-";
|
|
const size = entry.isDirectory ? "-" : entry.size.toString();
|
|
const modified = entry.modified ? new Date(entry.modified).toISOString().split("T")[0] : "-";
|
|
lines.push(`${typeIndicator} ${size.padStart(10)} ${modified} ${entry.name}`);
|
|
}
|
|
|
|
if (truncated) {
|
|
lines.push("", `... and ${entries.length - MAX_ENTRIES} more entries`);
|
|
}
|
|
|
|
return lines.join("\n");
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
logger.error("failed to list directory", { path: displayPath, error: errorMessage });
|
|
return toolError({
|
|
code: "OPERATION_FAILED",
|
|
message: `Failed to list directory: ${errorMessage}`,
|
|
});
|
|
}
|
|
}
|
|
|
|
private async listDirectory(
|
|
dir: string,
|
|
pattern: RegExp | undefined,
|
|
recursive: boolean,
|
|
showHidden: boolean,
|
|
depth: number,
|
|
maxDepth: number,
|
|
projectRoot: string,
|
|
): Promise<Array<{ name: string; isDirectory: boolean; isSymlink: boolean; size: number; modified: Date | null }>> {
|
|
const results: Array<{ name: string; isDirectory: boolean; isSymlink: boolean; size: number; modified: Date | null }> = [];
|
|
|
|
let entries;
|
|
try {
|
|
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
} catch {
|
|
return results;
|
|
}
|
|
|
|
for (const entry of entries) {
|
|
if (!showHidden && entry.name.startsWith(".")) continue;
|
|
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
|
|
const fullPath = path.join(dir, entry.name);
|
|
const relativePath = path.relative(projectRoot, fullPath);
|
|
|
|
if (pattern && !pattern.test(relativePath) && !pattern.test(entry.name)) continue;
|
|
|
|
let stat;
|
|
try {
|
|
stat = await fs.stat(fullPath);
|
|
} catch {
|
|
continue;
|
|
}
|
|
|
|
results.push({
|
|
name: relativePath,
|
|
isDirectory: entry.isDirectory(),
|
|
isSymlink: entry.isSymbolicLink(),
|
|
size: stat.size,
|
|
modified: stat.mtime,
|
|
});
|
|
|
|
if (recursive && entry.isDirectory() && depth < maxDepth) {
|
|
const subResults = await this.listDirectory(fullPath, pattern, recursive, showHidden, depth + 1, maxDepth, projectRoot);
|
|
results.push(...subResults);
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
}
|