read.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. import { z } from "zod"
  2. import * as fs from "fs"
  3. import * as path from "path"
  4. import { Tool } from "./tool"
  5. import { LSP } from "../lsp"
  6. import { FileTime } from "../file/time"
  7. import DESCRIPTION from "./read.txt"
  8. import { App } from "../app/app"
  9. import { Filesystem } from "../util/filesystem"
  10. const DEFAULT_READ_LIMIT = 2000
  11. const MAX_LINE_LENGTH = 2000
  12. export const ReadTool = Tool.define("read", {
  13. description: DESCRIPTION,
  14. parameters: z.object({
  15. filePath: z.string().describe("The path to the file to read"),
  16. offset: z.coerce.number().describe("The line number to start reading from (0-based)").optional(),
  17. limit: z.coerce.number().describe("The number of lines to read (defaults to 2000)").optional(),
  18. }),
  19. async execute(params, ctx) {
  20. let filepath = params.filePath
  21. if (!path.isAbsolute(filepath)) {
  22. filepath = path.join(process.cwd(), filepath)
  23. }
  24. const app = App.info()
  25. if (!Filesystem.contains(app.path.cwd, filepath)) {
  26. throw new Error(`File ${filepath} is not in the current working directory`)
  27. }
  28. const file = Bun.file(filepath)
  29. if (!(await file.exists())) {
  30. const dir = path.dirname(filepath)
  31. const base = path.basename(filepath)
  32. const dirEntries = fs.readdirSync(dir)
  33. const suggestions = dirEntries
  34. .filter(
  35. (entry) =>
  36. entry.toLowerCase().includes(base.toLowerCase()) || base.toLowerCase().includes(entry.toLowerCase()),
  37. )
  38. .map((entry) => path.join(dir, entry))
  39. .slice(0, 3)
  40. if (suggestions.length > 0) {
  41. throw new Error(`File not found: ${filepath}\n\nDid you mean one of these?\n${suggestions.join("\n")}`)
  42. }
  43. throw new Error(`File not found: ${filepath}`)
  44. }
  45. const limit = params.limit ?? DEFAULT_READ_LIMIT
  46. const offset = params.offset || 0
  47. const isImage = isImageFile(filepath)
  48. if (isImage) throw new Error(`This is an image file of type: ${isImage}\nUse a different tool to process images`)
  49. const isBinary = await isBinaryFile(file)
  50. if (isBinary) throw new Error(`Cannot read binary file: ${filepath}`)
  51. const lines = await file.text().then((text) => text.split("\n"))
  52. const raw = lines.slice(offset, offset + limit).map((line) => {
  53. return line.length > MAX_LINE_LENGTH ? line.substring(0, MAX_LINE_LENGTH) + "..." : line
  54. })
  55. const content = raw.map((line, index) => {
  56. return `${(index + offset + 1).toString().padStart(5, "0")}| ${line}`
  57. })
  58. const preview = raw.slice(0, 20).join("\n")
  59. let output = "<file>\n"
  60. output += content.join("\n")
  61. if (lines.length > offset + content.length) {
  62. output += `\n\n(File has more lines. Use 'offset' parameter to read beyond line ${offset + content.length})`
  63. }
  64. output += "\n</file>"
  65. // just warms the lsp client
  66. LSP.touchFile(filepath, false)
  67. FileTime.read(ctx.sessionID, filepath)
  68. return {
  69. title: path.relative(App.info().path.root, filepath),
  70. output,
  71. metadata: {
  72. preview,
  73. },
  74. }
  75. },
  76. })
  77. function isImageFile(filePath: string): string | false {
  78. const ext = path.extname(filePath).toLowerCase()
  79. switch (ext) {
  80. case ".jpg":
  81. case ".jpeg":
  82. return "JPEG"
  83. case ".png":
  84. return "PNG"
  85. case ".gif":
  86. return "GIF"
  87. case ".bmp":
  88. return "BMP"
  89. case ".svg":
  90. return "SVG"
  91. case ".webp":
  92. return "WebP"
  93. default:
  94. return false
  95. }
  96. }
  97. async function isBinaryFile(file: Bun.BunFile): Promise<boolean> {
  98. const buffer = await file.arrayBuffer()
  99. const bytes = new Uint8Array(buffer.slice(0, 512)) // Check first 512 bytes
  100. for (let i = 0; i < bytes.length; i++) {
  101. if (bytes[i] === 0) return true // Null byte indicates binary
  102. }
  103. return false
  104. }