ls.ts 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. import { z } from "zod";
  2. import { Tool } from "./tool";
  3. import { App } from "../app/app";
  4. import * as path from "path";
  5. import * as fs from "fs";
  6. const DESCRIPTION = `Directory listing tool that shows files and subdirectories in a tree structure, helping you explore and understand the project organization.
  7. WHEN TO USE THIS TOOL:
  8. - Use when you need to explore the structure of a directory
  9. - Helpful for understanding the organization of a project
  10. - Good first step when getting familiar with a new codebase
  11. HOW TO USE:
  12. - Provide a path to list (defaults to current working directory)
  13. - Optionally specify glob patterns to ignore
  14. - Results are displayed in a tree structure
  15. FEATURES:
  16. - Displays a hierarchical view of files and directories
  17. - Automatically skips hidden files/directories (starting with '.')
  18. - Skips common system directories like __pycache__
  19. - Can filter out files matching specific patterns
  20. LIMITATIONS:
  21. - Results are limited to 1000 files
  22. - Very large directories will be truncated
  23. - Does not show file sizes or permissions
  24. - Cannot recursively list all directories in a large project
  25. TIPS:
  26. - Use Glob tool for finding files by name patterns instead of browsing
  27. - Use Grep tool for searching file contents
  28. - Combine with other tools for more effective exploration`;
  29. const MAX_LS_FILES = 1000;
  30. interface TreeNode {
  31. name: string;
  32. path: string;
  33. type: "file" | "directory";
  34. children?: TreeNode[];
  35. }
  36. export const ls = Tool.define({
  37. name: "ls",
  38. description: DESCRIPTION,
  39. parameters: z.object({
  40. path: z
  41. .string()
  42. .describe(
  43. "The path to the directory to list (defaults to current working directory)",
  44. )
  45. .optional(),
  46. ignore: z
  47. .array(z.string())
  48. .describe("List of glob patterns to ignore")
  49. .optional(),
  50. }),
  51. async execute(params) {
  52. const app = await App.use();
  53. let searchPath = params.path || app.root;
  54. if (!path.isAbsolute(searchPath)) {
  55. searchPath = path.join(app.root, searchPath);
  56. }
  57. const stat = await fs.promises.stat(searchPath).catch(() => null);
  58. if (!stat) {
  59. return {
  60. metadata: {},
  61. output: `Path does not exist: ${searchPath}`,
  62. };
  63. }
  64. const { files, truncated } = await listDirectory(
  65. searchPath,
  66. params.ignore || [],
  67. MAX_LS_FILES,
  68. );
  69. const tree = createFileTree(files);
  70. let output = printTree(tree, searchPath);
  71. if (truncated) {
  72. 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}`;
  73. }
  74. return {
  75. metadata: {
  76. count: files.length,
  77. truncated,
  78. },
  79. output,
  80. };
  81. },
  82. });
  83. async function listDirectory(
  84. initialPath: string,
  85. ignorePatterns: string[],
  86. limit: number,
  87. ): Promise<{ files: string[]; truncated: boolean }> {
  88. const results: string[] = [];
  89. let truncated = false;
  90. async function walk(dir: string): Promise<void> {
  91. if (results.length >= limit) {
  92. truncated = true;
  93. return;
  94. }
  95. const entries = await fs.promises
  96. .readdir(dir, { withFileTypes: true })
  97. .catch(() => []);
  98. for (const entry of entries) {
  99. const fullPath = path.join(dir, entry.name);
  100. if (shouldSkip(fullPath, ignorePatterns)) {
  101. continue;
  102. }
  103. if (entry.isDirectory()) {
  104. if (fullPath !== initialPath) {
  105. results.push(fullPath + path.sep);
  106. }
  107. if (results.length >= limit) {
  108. truncated = true;
  109. return;
  110. }
  111. await walk(fullPath);
  112. } else if (entry.isFile()) {
  113. if (fullPath !== initialPath) {
  114. results.push(fullPath);
  115. }
  116. if (results.length >= limit) {
  117. truncated = true;
  118. return;
  119. }
  120. }
  121. }
  122. }
  123. await walk(initialPath);
  124. return { files: results, truncated };
  125. }
  126. function shouldSkip(filePath: string, ignorePatterns: string[]): boolean {
  127. const base = path.basename(filePath);
  128. if (base !== "." && base.startsWith(".")) {
  129. return true;
  130. }
  131. const commonIgnored = [
  132. "__pycache__",
  133. "node_modules",
  134. "dist",
  135. "build",
  136. "target",
  137. "vendor",
  138. "bin",
  139. "obj",
  140. ".git",
  141. ".idea",
  142. ".vscode",
  143. ".DS_Store",
  144. "*.pyc",
  145. "*.pyo",
  146. "*.pyd",
  147. "*.so",
  148. "*.dll",
  149. "*.exe",
  150. ];
  151. if (filePath.includes(path.join("__pycache__", ""))) {
  152. return true;
  153. }
  154. for (const ignored of commonIgnored) {
  155. if (ignored.endsWith("/")) {
  156. if (filePath.includes(path.join(ignored.slice(0, -1), ""))) {
  157. return true;
  158. }
  159. } else if (ignored.startsWith("*.")) {
  160. if (base.endsWith(ignored.slice(1))) {
  161. return true;
  162. }
  163. } else {
  164. if (base === ignored) {
  165. return true;
  166. }
  167. }
  168. }
  169. for (const pattern of ignorePatterns) {
  170. const glob = new Bun.Glob(pattern);
  171. if (glob.match(base)) {
  172. return true;
  173. }
  174. }
  175. return false;
  176. }
  177. function createFileTree(sortedPaths: string[]): TreeNode[] {
  178. const root: TreeNode[] = [];
  179. const pathMap: Record<string, TreeNode> = {};
  180. for (const filePath of sortedPaths) {
  181. const parts = filePath.split(path.sep).filter((part) => part !== "");
  182. let currentPath = "";
  183. let parentPath = "";
  184. if (parts.length === 0) {
  185. continue;
  186. }
  187. for (let i = 0; i < parts.length; i++) {
  188. const part = parts[i];
  189. if (currentPath === "") {
  190. currentPath = part;
  191. } else {
  192. currentPath = path.join(currentPath, part);
  193. }
  194. if (pathMap[currentPath]) {
  195. parentPath = currentPath;
  196. continue;
  197. }
  198. const isLastPart = i === parts.length - 1;
  199. const isDir = !isLastPart || filePath.endsWith(path.sep);
  200. const nodeType = isDir ? "directory" : "file";
  201. const newNode: TreeNode = {
  202. name: part,
  203. path: currentPath,
  204. type: nodeType,
  205. children: [],
  206. };
  207. pathMap[currentPath] = newNode;
  208. if (i > 0 && parentPath !== "") {
  209. if (pathMap[parentPath]) {
  210. pathMap[parentPath].children?.push(newNode);
  211. }
  212. } else {
  213. root.push(newNode);
  214. }
  215. parentPath = currentPath;
  216. }
  217. }
  218. return root;
  219. }
  220. function printTree(tree: TreeNode[], rootPath: string): string {
  221. let result = `- ${rootPath}${path.sep}\n`;
  222. for (const node of tree) {
  223. result = printNode(node, 1, result);
  224. }
  225. return result;
  226. }
  227. function printNode(node: TreeNode, level: number, result: string): string {
  228. const indent = " ".repeat(level);
  229. let nodeName = node.name;
  230. if (node.type === "directory") {
  231. nodeName += path.sep;
  232. }
  233. result += `${indent}- ${nodeName}\n`;
  234. if (node.type === "directory" && node.children && node.children.length > 0) {
  235. for (const child of node.children) {
  236. result = printNode(child, level + 1, result);
  237. }
  238. }
  239. return result;
  240. }