| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290 |
- 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[];
- }
- export const ls = Tool.define({
- name: "ls",
- description: DESCRIPTION,
- 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(),
- }),
- 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 entries = await fs.promises
- .readdir(dir, { withFileTypes: true })
- .catch(() => []);
- for (const entry of entries) {
- const fullPath = path.join(dir, entry.name);
- if (shouldSkip(fullPath, ignorePatterns)) {
- 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;
- }
- }
- }
- }
- await walk(initialPath);
- return { files: results, truncated };
- }
- function shouldSkip(filePath: string, ignorePatterns: string[]): boolean {
- const base = path.basename(filePath);
- 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;
- }
- } else {
- if (base === ignored) {
- return true;
- }
- }
- }
- for (const pattern of ignorePatterns) {
- const glob = new Bun.Glob(pattern);
- if (glob.match(base)) {
- return true;
- }
- }
- 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 = "";
- 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 (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: [],
- };
- pathMap[currentPath] = newNode;
- if (i > 0 && parentPath !== "") {
- if (pathMap[parentPath]) {
- pathMap[parentPath].children?.push(newNode);
- }
- } else {
- root.push(newNode);
- }
- parentPath = currentPath;
- }
- }
- 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`;
- if (node.type === "directory" && node.children && node.children.length > 0) {
- for (const child of node.children) {
- result = printNode(child, level + 1, result);
- }
- }
- return result;
- }
|