cloudaxe-opencode/packages/opencode/src/tool/read.ts

125 lines
3.4 KiB
TypeScript
Raw Normal View History

2025-05-31 18:41:00 +00:00
import { z } from "zod"
import * as fs from "fs"
import * as path from "path"
import { Tool } from "./tool"
import { LSP } from "../lsp"
2025-06-27 15:29:20 +00:00
import { FileTime } from "../file/time"
2025-06-04 17:12:13 +00:00
import DESCRIPTION from "./read.txt"
2025-06-11 16:58:06 +00:00
import { App } from "../app/app"
2025-05-19 23:29:38 +00:00
2025-05-31 18:41:00 +00:00
const MAX_READ_SIZE = 250 * 1024
const DEFAULT_READ_LIMIT = 2000
const MAX_LINE_LENGTH = 2000
2025-05-19 23:29:38 +00:00
2025-06-04 17:12:13 +00:00
export const ReadTool = Tool.define({
id: "read",
2025-05-19 23:29:38 +00:00
description: DESCRIPTION,
parameters: z.object({
filePath: z.string().describe("The path to the file to read"),
2025-05-19 23:29:38 +00:00
offset: z
.number()
.describe("The line number to start reading from (0-based)")
2025-06-17 15:27:07 +00:00
.optional(),
2025-05-19 23:29:38 +00:00
limit: z
.number()
.describe("The number of lines to read (defaults to 2000)")
2025-06-17 15:27:07 +00:00
.optional(),
2025-05-19 23:29:38 +00:00
}),
2025-06-03 00:24:32 +00:00
async execute(params, ctx) {
2025-05-31 18:41:00 +00:00
let filePath = params.filePath
2025-05-19 23:29:38 +00:00
if (!path.isAbsolute(filePath)) {
2025-05-31 18:41:00 +00:00
filePath = path.join(process.cwd(), filePath)
2025-05-19 23:29:38 +00:00
}
2025-05-31 18:41:00 +00:00
const file = Bun.file(filePath)
2025-05-19 23:29:38 +00:00
if (!(await file.exists())) {
2025-05-31 18:41:00 +00:00
const dir = path.dirname(filePath)
const base = path.basename(filePath)
2025-05-19 23:29:38 +00:00
2025-05-31 18:41:00 +00:00
const dirEntries = fs.readdirSync(dir)
2025-05-19 23:29:38 +00:00
const suggestions = dirEntries
.filter(
(entry) =>
entry.toLowerCase().includes(base.toLowerCase()) ||
base.toLowerCase().includes(entry.toLowerCase()),
)
.map((entry) => path.join(dir, entry))
2025-05-31 18:41:00 +00:00
.slice(0, 3)
2025-05-19 23:29:38 +00:00
if (suggestions.length > 0) {
throw new Error(
`File not found: ${filePath}\n\nDid you mean one of these?\n${suggestions.join("\n")}`,
2025-05-31 18:41:00 +00:00
)
2025-05-19 23:29:38 +00:00
}
2025-05-31 18:41:00 +00:00
throw new Error(`File not found: ${filePath}`)
2025-05-19 23:29:38 +00:00
}
2025-05-31 18:41:00 +00:00
const stats = await file.stat()
2025-05-19 23:29:38 +00:00
2025-05-21 14:30:39 +00:00
if (stats.size > MAX_READ_SIZE)
2025-05-19 23:29:38 +00:00
throw new Error(
`File is too large (${stats.size} bytes). Maximum size is ${MAX_READ_SIZE} bytes`,
2025-05-31 18:41:00 +00:00
)
const limit = params.limit ?? DEFAULT_READ_LIMIT
const offset = params.offset || 0
const isImage = isImageFile(filePath)
2025-05-21 14:30:39 +00:00
if (isImage)
2025-05-19 23:29:38 +00:00
throw new Error(
`This is an image file of type: ${isImage}\nUse a different tool to process images`,
2025-05-31 18:41:00 +00:00
)
const lines = await file.text().then((text) => text.split("\n"))
2025-05-30 17:29:27 +00:00
const raw = lines.slice(offset, offset + limit).map((line) => {
return line.length > MAX_LINE_LENGTH
? line.substring(0, MAX_LINE_LENGTH) + "..."
2025-05-31 18:41:00 +00:00
: line
})
2025-05-30 17:29:27 +00:00
const content = raw.map((line, index) => {
2025-05-31 18:41:00 +00:00
return `${(index + offset + 1).toString().padStart(5, "0")}| ${line}`
})
const preview = raw.slice(0, 20).join("\n")
2025-05-19 23:29:38 +00:00
2025-05-31 18:41:00 +00:00
let output = "<file>\n"
output += content.join("\n")
2025-05-19 23:29:38 +00:00
if (lines.length > offset + content.length) {
output += `\n\n(File has more lines. Use 'offset' parameter to read beyond line ${
offset + content.length
2025-05-31 18:41:00 +00:00
})`
2025-05-19 23:29:38 +00:00
}
2025-05-31 18:41:00 +00:00
output += "\n</file>"
2025-05-19 23:29:38 +00:00
2025-05-26 21:15:41 +00:00
// just warms the lsp client
2025-07-01 02:46:42 +00:00
await LSP.touchFile(filePath, false)
2025-06-27 15:29:20 +00:00
FileTime.read(ctx.sessionID, filePath)
2025-05-21 02:00:00 +00:00
2025-05-20 15:11:06 +00:00
return {
2025-05-21 02:00:00 +00:00
output,
2025-05-30 17:29:27 +00:00
metadata: {
preview,
2025-06-11 16:58:06 +00:00
title: path.relative(App.info().path.root, filePath),
2025-05-30 17:29:27 +00:00
},
2025-05-31 18:41:00 +00:00
}
2025-05-19 23:29:38 +00:00
},
2025-05-31 18:41:00 +00:00
})
2025-05-19 23:29:38 +00:00
function isImageFile(filePath: string): string | false {
2025-05-31 18:41:00 +00:00
const ext = path.extname(filePath).toLowerCase()
2025-05-19 23:29:38 +00:00
switch (ext) {
case ".jpg":
case ".jpeg":
2025-05-31 18:41:00 +00:00
return "JPEG"
2025-05-19 23:29:38 +00:00
case ".png":
2025-05-31 18:41:00 +00:00
return "PNG"
2025-05-19 23:29:38 +00:00
case ".gif":
2025-05-31 18:41:00 +00:00
return "GIF"
2025-05-19 23:29:38 +00:00
case ".bmp":
2025-05-31 18:41:00 +00:00
return "BMP"
2025-05-19 23:29:38 +00:00
case ".svg":
2025-05-31 18:41:00 +00:00
return "SVG"
2025-05-19 23:29:38 +00:00
case ".webp":
2025-05-31 18:41:00 +00:00
return "WebP"
2025-05-19 23:29:38 +00:00
default:
2025-05-31 18:41:00 +00:00
return false
2025-05-19 23:29:38 +00:00
}
}