|
|
@@ -2,289 +2,95 @@ import { z } from "zod";
|
|
|
import { Tool } from "./tool";
|
|
|
import { App } from "../app/app";
|
|
|
import * as path from "path";
|
|
|
-import * as fs from "fs";
|
|
|
|
|
|
-const DESCRIPTION = `Directory listing tool that shows files and subdirectories in a tree structure, helping you explore and understand the project organization.
|
|
|
-
|
|
|
-WHEN TO USE THIS TOOL:
|
|
|
-- Use when you need to explore the structure of a directory
|
|
|
-- Helpful for understanding the organization of a project
|
|
|
-- Good first step when getting familiar with a new codebase
|
|
|
-
|
|
|
-HOW TO USE:
|
|
|
-- Provide a path to list (defaults to current working directory)
|
|
|
-- Optionally specify glob patterns to ignore
|
|
|
-- Results are displayed in a tree structure
|
|
|
-
|
|
|
-FEATURES:
|
|
|
-- Displays a hierarchical view of files and directories
|
|
|
-- Automatically skips hidden files/directories (starting with '.')
|
|
|
-- Skips common system directories like __pycache__
|
|
|
-- Can filter out files matching specific patterns
|
|
|
-
|
|
|
-LIMITATIONS:
|
|
|
-- Results are limited to 1000 files
|
|
|
-- Very large directories will be truncated
|
|
|
-- Does not show file sizes or permissions
|
|
|
-- Cannot recursively list all directories in a large project
|
|
|
-
|
|
|
-TIPS:
|
|
|
-- Use Glob tool for finding files by name patterns instead of browsing
|
|
|
-- Use Grep tool for searching file contents
|
|
|
-- Combine with other tools for more effective exploration`;
|
|
|
-
|
|
|
-const MAX_LS_FILES = 1000;
|
|
|
-
|
|
|
-interface TreeNode {
|
|
|
- name: string;
|
|
|
- path: string;
|
|
|
- type: "file" | "directory";
|
|
|
- children?: TreeNode[];
|
|
|
-}
|
|
|
+const IGNORE_PATTERNS = [
|
|
|
+ "node_modules/",
|
|
|
+ "__pycache__/",
|
|
|
+ ".git/",
|
|
|
+ "dist/",
|
|
|
+ "build/",
|
|
|
+ "target/",
|
|
|
+ "vendor/",
|
|
|
+ "bin/",
|
|
|
+ "obj/",
|
|
|
+ ".idea/",
|
|
|
+ ".vscode/",
|
|
|
+];
|
|
|
|
|
|
export const ls = Tool.define({
|
|
|
- name: "ls",
|
|
|
- description: DESCRIPTION,
|
|
|
+ name: "opencode.ls",
|
|
|
+ description: "List directory contents",
|
|
|
parameters: z.object({
|
|
|
- path: z
|
|
|
- .string()
|
|
|
- .describe(
|
|
|
- "The path to the directory to list (defaults to current working directory)",
|
|
|
- )
|
|
|
- .optional(),
|
|
|
- ignore: z
|
|
|
- .array(z.string())
|
|
|
- .describe("List of glob patterns to ignore")
|
|
|
- .optional(),
|
|
|
+ path: z.string().optional(),
|
|
|
+ ignore: z.array(z.string()).optional(),
|
|
|
}),
|
|
|
async execute(params) {
|
|
|
const app = await App.use();
|
|
|
- let searchPath = params.path || app.root;
|
|
|
-
|
|
|
- if (!path.isAbsolute(searchPath)) {
|
|
|
- searchPath = path.join(app.root, searchPath);
|
|
|
- }
|
|
|
-
|
|
|
- const stat = await fs.promises.stat(searchPath).catch(() => null);
|
|
|
- if (!stat) {
|
|
|
- return {
|
|
|
- metadata: {},
|
|
|
- output: `Path does not exist: ${searchPath}`,
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- const { files, truncated } = await listDirectory(
|
|
|
- searchPath,
|
|
|
- params.ignore || [],
|
|
|
- MAX_LS_FILES,
|
|
|
- );
|
|
|
- const tree = createFileTree(files);
|
|
|
- let output = printTree(tree, searchPath);
|
|
|
-
|
|
|
- if (truncated) {
|
|
|
- output = `There are more than ${MAX_LS_FILES} files in the directory. Use a more specific path or use the Glob tool to find specific files. The first ${MAX_LS_FILES} files and directories are included below:\n\n${output}`;
|
|
|
- }
|
|
|
-
|
|
|
- return {
|
|
|
- metadata: {
|
|
|
- count: files.length,
|
|
|
- truncated,
|
|
|
- },
|
|
|
- output,
|
|
|
- };
|
|
|
- },
|
|
|
-});
|
|
|
-
|
|
|
-async function listDirectory(
|
|
|
- initialPath: string,
|
|
|
- ignorePatterns: string[],
|
|
|
- limit: number,
|
|
|
-): Promise<{ files: string[]; truncated: boolean }> {
|
|
|
- const results: string[] = [];
|
|
|
- let truncated = false;
|
|
|
-
|
|
|
- async function walk(dir: string): Promise<void> {
|
|
|
- if (results.length >= limit) {
|
|
|
- truncated = true;
|
|
|
- return;
|
|
|
- }
|
|
|
+ const searchPath = path.resolve(app.root, params.path || ".");
|
|
|
|
|
|
- const entries = await fs.promises
|
|
|
- .readdir(dir, { withFileTypes: true })
|
|
|
- .catch(() => []);
|
|
|
+ const glob = new Bun.Glob("**/*");
|
|
|
+ const files = [];
|
|
|
|
|
|
- for (const entry of entries) {
|
|
|
- const fullPath = path.join(dir, entry.name);
|
|
|
-
|
|
|
- if (shouldSkip(fullPath, ignorePatterns)) {
|
|
|
+ for await (const file of glob.scan({ cwd: searchPath })) {
|
|
|
+ if (file.startsWith(".") || IGNORE_PATTERNS.some((p) => file.includes(p)))
|
|
|
continue;
|
|
|
- }
|
|
|
-
|
|
|
- if (entry.isDirectory()) {
|
|
|
- if (fullPath !== initialPath) {
|
|
|
- results.push(fullPath + path.sep);
|
|
|
- }
|
|
|
-
|
|
|
- if (results.length >= limit) {
|
|
|
- truncated = true;
|
|
|
- return;
|
|
|
- }
|
|
|
- await walk(fullPath);
|
|
|
- } else if (entry.isFile()) {
|
|
|
- if (fullPath !== initialPath) {
|
|
|
- results.push(fullPath);
|
|
|
- }
|
|
|
-
|
|
|
- if (results.length >= limit) {
|
|
|
- truncated = true;
|
|
|
- return;
|
|
|
- }
|
|
|
- }
|
|
|
+ if (params.ignore?.some((pattern) => new Bun.Glob(pattern).match(file)))
|
|
|
+ continue;
|
|
|
+ files.push(file);
|
|
|
+ if (files.length >= 1000) break;
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- await walk(initialPath);
|
|
|
- return { files: results, truncated };
|
|
|
-}
|
|
|
+ // Build directory structure
|
|
|
+ const dirs = new Set<string>();
|
|
|
+ const filesByDir = new Map<string, string[]>();
|
|
|
|
|
|
-function shouldSkip(filePath: string, ignorePatterns: string[]): boolean {
|
|
|
- const base = path.basename(filePath);
|
|
|
+ for (const file of files) {
|
|
|
+ const dir = path.dirname(file);
|
|
|
+ const parts = dir === "." ? [] : dir.split("/");
|
|
|
|
|
|
- if (base !== "." && base.startsWith(".")) {
|
|
|
- return true;
|
|
|
- }
|
|
|
-
|
|
|
- const commonIgnored = [
|
|
|
- "__pycache__",
|
|
|
- "node_modules",
|
|
|
- "dist",
|
|
|
- "build",
|
|
|
- "target",
|
|
|
- "vendor",
|
|
|
- "bin",
|
|
|
- "obj",
|
|
|
- ".git",
|
|
|
- ".idea",
|
|
|
- ".vscode",
|
|
|
- ".DS_Store",
|
|
|
- "*.pyc",
|
|
|
- "*.pyo",
|
|
|
- "*.pyd",
|
|
|
- "*.so",
|
|
|
- "*.dll",
|
|
|
- "*.exe",
|
|
|
- ];
|
|
|
-
|
|
|
- if (filePath.includes(path.join("__pycache__", ""))) {
|
|
|
- return true;
|
|
|
- }
|
|
|
-
|
|
|
- for (const ignored of commonIgnored) {
|
|
|
- if (ignored.endsWith("/")) {
|
|
|
- if (filePath.includes(path.join(ignored.slice(0, -1), ""))) {
|
|
|
- return true;
|
|
|
- }
|
|
|
- } else if (ignored.startsWith("*.")) {
|
|
|
- if (base.endsWith(ignored.slice(1))) {
|
|
|
- return true;
|
|
|
+ // Add all parent directories
|
|
|
+ for (let i = 0; i <= parts.length; i++) {
|
|
|
+ const dirPath = i === 0 ? "." : parts.slice(0, i).join("/");
|
|
|
+ dirs.add(dirPath);
|
|
|
}
|
|
|
- } else {
|
|
|
- if (base === ignored) {
|
|
|
- return true;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
|
|
|
- for (const pattern of ignorePatterns) {
|
|
|
- const glob = new Bun.Glob(pattern);
|
|
|
- if (glob.match(base)) {
|
|
|
- return true;
|
|
|
+ // Add file to its directory
|
|
|
+ if (!filesByDir.has(dir)) filesByDir.set(dir, []);
|
|
|
+ filesByDir.get(dir)!.push(path.basename(file));
|
|
|
}
|
|
|
- }
|
|
|
-
|
|
|
- return false;
|
|
|
-}
|
|
|
-
|
|
|
-function createFileTree(sortedPaths: string[]): TreeNode[] {
|
|
|
- const root: TreeNode[] = [];
|
|
|
- const pathMap: Record<string, TreeNode> = {};
|
|
|
|
|
|
- for (const filePath of sortedPaths) {
|
|
|
- const parts = filePath.split(path.sep).filter((part) => part !== "");
|
|
|
- let currentPath = "";
|
|
|
- let parentPath = "";
|
|
|
+ function renderDir(dirPath: string, depth: number): string {
|
|
|
+ const indent = " ".repeat(depth);
|
|
|
+ let output = "";
|
|
|
|
|
|
- if (parts.length === 0) {
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- for (let i = 0; i < parts.length; i++) {
|
|
|
- const part = parts[i];
|
|
|
-
|
|
|
- if (currentPath === "") {
|
|
|
- currentPath = part;
|
|
|
- } else {
|
|
|
- currentPath = path.join(currentPath, part);
|
|
|
+ if (depth > 0) {
|
|
|
+ output += `${indent}${path.basename(dirPath)}/\n`;
|
|
|
}
|
|
|
|
|
|
- if (pathMap[currentPath]) {
|
|
|
- parentPath = currentPath;
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- const isLastPart = i === parts.length - 1;
|
|
|
- const isDir = !isLastPart || filePath.endsWith(path.sep);
|
|
|
- const nodeType = isDir ? "directory" : "file";
|
|
|
-
|
|
|
- const newNode: TreeNode = {
|
|
|
- name: part,
|
|
|
- path: currentPath,
|
|
|
- type: nodeType,
|
|
|
- children: [],
|
|
|
- };
|
|
|
+ const childIndent = " ".repeat(depth + 1);
|
|
|
+ const children = Array.from(dirs)
|
|
|
+ .filter((d) => path.dirname(d) === dirPath && d !== dirPath)
|
|
|
+ .sort();
|
|
|
|
|
|
- pathMap[currentPath] = newNode;
|
|
|
+ // Render subdirectories first
|
|
|
+ for (const child of children) {
|
|
|
+ output += renderDir(child, depth + 1);
|
|
|
+ }
|
|
|
|
|
|
- if (i > 0 && parentPath !== "") {
|
|
|
- if (pathMap[parentPath]) {
|
|
|
- pathMap[parentPath].children?.push(newNode);
|
|
|
- }
|
|
|
- } else {
|
|
|
- root.push(newNode);
|
|
|
+ // Render files
|
|
|
+ const files = filesByDir.get(dirPath) || [];
|
|
|
+ for (const file of files.sort()) {
|
|
|
+ output += `${childIndent}${file}\n`;
|
|
|
}
|
|
|
|
|
|
- parentPath = currentPath;
|
|
|
+ return output;
|
|
|
}
|
|
|
- }
|
|
|
-
|
|
|
- return root;
|
|
|
-}
|
|
|
-
|
|
|
-function printTree(tree: TreeNode[], rootPath: string): string {
|
|
|
- let result = `- ${rootPath}${path.sep}\n`;
|
|
|
-
|
|
|
- for (const node of tree) {
|
|
|
- result = printNode(node, 1, result);
|
|
|
- }
|
|
|
-
|
|
|
- return result;
|
|
|
-}
|
|
|
-
|
|
|
-function printNode(node: TreeNode, level: number, result: string): string {
|
|
|
- const indent = " ".repeat(level);
|
|
|
-
|
|
|
- let nodeName = node.name;
|
|
|
- if (node.type === "directory") {
|
|
|
- nodeName += path.sep;
|
|
|
- }
|
|
|
|
|
|
- result += `${indent}- ${nodeName}\n`;
|
|
|
+ const output = `${searchPath}/\n` + renderDir(".", 0);
|
|
|
|
|
|
- if (node.type === "directory" && node.children && node.children.length > 0) {
|
|
|
- for (const child of node.children) {
|
|
|
- result = printNode(child, level + 1, result);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return result;
|
|
|
-}
|
|
|
+ return {
|
|
|
+ metadata: { count: files.length, truncated: files.length >= 1000 },
|
|
|
+ output,
|
|
|
+ };
|
|
|
+ },
|
|
|
+});
|