ls.ts 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. import z from "zod"
  2. import { Tool } from "./tool"
  3. import * as path from "path"
  4. import DESCRIPTION from "./ls.txt"
  5. import { Instance } from "../project/instance"
  6. import { Ripgrep } from "../file/ripgrep"
  7. export const IGNORE_PATTERNS = [
  8. "node_modules/",
  9. "__pycache__/",
  10. ".git/",
  11. "dist/",
  12. "build/",
  13. "target/",
  14. "vendor/",
  15. "bin/",
  16. "obj/",
  17. ".idea/",
  18. ".vscode/",
  19. ".zig-cache/",
  20. "zig-out",
  21. ".coverage",
  22. "coverage/",
  23. "vendor/",
  24. "tmp/",
  25. "temp/",
  26. ".cache/",
  27. "cache/",
  28. "logs/",
  29. ".venv/",
  30. "venv/",
  31. "env/",
  32. ]
  33. const LIMIT = 100
  34. export const ListTool = Tool.define("list", {
  35. description: DESCRIPTION,
  36. parameters: z.object({
  37. path: z.string().describe("The absolute path to the directory to list (must be absolute, not relative)").optional(),
  38. ignore: z.array(z.string()).describe("List of glob patterns to ignore").optional(),
  39. }),
  40. async execute(params) {
  41. const searchPath = path.resolve(Instance.directory, params.path || ".")
  42. const ignoreGlobs = IGNORE_PATTERNS.map((p) => `!${p}*`).concat(params.ignore?.map((p) => `!${p}`) || [])
  43. const files = []
  44. for await (const file of Ripgrep.files({ cwd: searchPath, glob: ignoreGlobs })) {
  45. files.push(file)
  46. if (files.length >= LIMIT) break
  47. }
  48. // Build directory structure
  49. const dirs = new Set<string>()
  50. const filesByDir = new Map<string, string[]>()
  51. for (const file of files) {
  52. const dir = path.dirname(file)
  53. const parts = dir === "." ? [] : dir.split("/")
  54. // Add all parent directories
  55. for (let i = 0; i <= parts.length; i++) {
  56. const dirPath = i === 0 ? "." : parts.slice(0, i).join("/")
  57. dirs.add(dirPath)
  58. }
  59. // Add file to its directory
  60. if (!filesByDir.has(dir)) filesByDir.set(dir, [])
  61. filesByDir.get(dir)!.push(path.basename(file))
  62. }
  63. function renderDir(dirPath: string, depth: number): string {
  64. const indent = " ".repeat(depth)
  65. let output = ""
  66. if (depth > 0) {
  67. output += `${indent}${path.basename(dirPath)}/\n`
  68. }
  69. const childIndent = " ".repeat(depth + 1)
  70. const children = Array.from(dirs)
  71. .filter((d) => path.dirname(d) === dirPath && d !== dirPath)
  72. .sort()
  73. // Render subdirectories first
  74. for (const child of children) {
  75. output += renderDir(child, depth + 1)
  76. }
  77. // Render files
  78. const files = filesByDir.get(dirPath) || []
  79. for (const file of files.sort()) {
  80. output += `${childIndent}${file}\n`
  81. }
  82. return output
  83. }
  84. const output = `${searchPath}/\n` + renderDir(".", 0)
  85. return {
  86. title: path.relative(Instance.worktree, searchPath),
  87. metadata: {
  88. count: files.length,
  89. truncated: files.length >= LIMIT,
  90. },
  91. output,
  92. }
  93. },
  94. })