ls.ts 2.7 KB

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