ls.ts 3.1 KB

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