269 lines
6.8 KiB
TypeScript
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();
|