gadget/docs/archive/tools/search/list.ts

269 lines
6.8 KiB
TypeScript

// src/tools/search/list.ts
// Copyright (C) 2025 DTP Technologies, LLC
// All Rights Reserved
import fs from "node:fs/promises";
import path from "node:path";
import type { ToolDefinition } from "../../lib/ai-client.js";
import {
DtpTool,
type ToolArguments,
type ToolContext,
} from "../../lib/tool.js";
import { ChatSessionMode } from "../../models/chat-session.js";
import HostMonitorService from "../../services/host-monitor.js";
const MAX_ENTRIES = 1000;
class ListTool extends DtpTool {
get name(): string {
return "ListTool";
}
get slug(): string {
return "list";
}
get metadata() {
return {
name: this.definition.function.name || "list",
category: "search",
tags: ["search", "find", "directory", "ls", "list"],
modes: [
ChatSessionMode.Plan,
ChatSessionMode.Build,
ChatSessionMode.Test,
ChatSessionMode.Ship,
ChatSessionMode.Develop,
],
};
}
public definition: ToolDefinition = {
type: "function",
function: {
name: "list",
description:
"List directory contents with optional filtering. Can show file types, sizes, and modification dates. Supports filtering by glob pattern.",
parameters: {
type: "object",
properties: {
path: {
type: "string",
description:
"Directory path to list. Defaults to current working directory.",
},
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(
_context: ToolContext,
args: ToolArguments,
): Promise<string> {
const targetPath = (args.path as string | undefined) || process.cwd();
const patternStr = args.pattern as string | undefined;
let pattern: RegExp | undefined;
if (patternStr) {
try {
const globPattern = patternStr
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
.replace(/\*/g, ".*")
.replace(/\?/g, ".");
pattern = new RegExp("^" + globPattern + "$");
} catch {
// Invalid regex, ignore
}
}
const recursive = (args.recursive as boolean) || false;
const showHidden = (args.showHidden as boolean) || false;
const maxDepth = (args.maxDepth as number) || 3;
let resolvedPath: string;
try {
resolvedPath = path.resolve(targetPath);
} catch {
return this.error("INVALID_PARAMETER", "Invalid path.", {
parameter: "path",
});
}
try {
const stat = await fs.stat(resolvedPath);
if (!stat.isDirectory()) {
return this.error(
"INVALID_PARAMETER",
`"${targetPath}" is not a directory.`,
{
parameter: "path",
recoveryHint: "Provide a directory path to list.",
},
);
}
const entries = await this.listDirectory(
resolvedPath,
pattern || undefined,
recursive,
showHidden,
0,
maxDepth,
);
const limited = entries.slice(0, MAX_ENTRIES);
let output = "";
if (entries.length === 0) {
output = `No entries found in "${targetPath}"`;
} else {
const truncated = entries.length > MAX_ENTRIES;
output = `Contents of "${targetPath}" (${
truncated ? `showing first ${MAX_ENTRIES} of ` : ""
}${entries.length} entries):\n\n`;
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]
: "-";
output += `${typeIndicator} ${size.padStart(10)} ${modified} ${entry.name}\n`;
}
if (truncated) {
output += `\n... and ${entries.length - MAX_ENTRIES} more entries`;
}
}
const byteCount = Buffer.byteLength(output, "utf-8");
HostMonitorService.toolCall(byteCount);
return this.success(
{
entries: limited,
total: entries.length,
truncated: entries.length > MAX_ENTRIES,
},
output,
);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
return this.error(
"OPERATION_FAILED",
`Failed to list directory: ${errorMessage}`,
);
}
}
private async listDirectory(
dir: string,
pattern: RegExp | undefined,
recursive: boolean,
showHidden: boolean,
depth: number,
maxDepth: number,
): 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);
let relativePath = fullPath;
try {
relativePath = path.relative(process.cwd(), fullPath);
} catch {
// ignore
}
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,
);
results.push(...subResults);
}
}
return results;
}
}
export default new ListTool();