diff --git a/.opencode/skill/bun-file-io/SKILL.md b/.opencode/skill/bun-file-io/SKILL.md index ea39507d26..f78de33094 100644 --- a/.opencode/skill/bun-file-io/SKILL.md +++ b/.opencode/skill/bun-file-io/SKILL.md @@ -32,6 +32,9 @@ description: Use this when you are working on file operations like reading, writ - Decode tool stderr with `Bun.readableStreamToText`. - For large writes, use `Bun.write(Bun.file(path), text)`. +NOTE: Bun.file(...).exists() will return `false` if the value is a directory. +Use Filesystem.exists(...) instead if path can be file or directory + ## Quick checklist - Use Bun APIs first. diff --git a/packages/opencode/src/tool/edit.txt b/packages/opencode/src/tool/edit.txt index 863efb8409..baad14bf18 100644 --- a/packages/opencode/src/tool/edit.txt +++ b/packages/opencode/src/tool/edit.txt @@ -2,7 +2,7 @@ Performs exact string replacements in files. Usage: - You must use your `Read` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file. -- When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: spaces + line number + tab. Everything after that tab is the actual file content to match. Never include any part of the line number prefix in the oldString or newString. +- When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: line number + colon + space (e.g., `1: `). Everything after that space is the actual file content to match. Never include any part of the line number prefix in the oldString or newString. - ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required. - Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked. - The edit will FAIL if `oldString` is not found in the file with an error "oldString not found in content". diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts index f230cdf44c..cc2f5eb58d 100644 --- a/packages/opencode/src/tool/read.ts +++ b/packages/opencode/src/tool/read.ts @@ -17,9 +17,9 @@ const MAX_BYTES = 50 * 1024 export const ReadTool = Tool.define("read", { description: DESCRIPTION, parameters: z.object({ - filePath: z.string().describe("The path to the file to read"), - offset: z.coerce.number().describe("The line number to start reading from (0-based)").optional(), - limit: z.coerce.number().describe("The number of lines to read (defaults to 2000)").optional(), + filePath: z.string().describe("The absolute path to the file to read (directories are not yet supported)"), + offset: z.coerce.number().describe("The 0-based line offset to start reading from").optional(), + limit: z.coerce.number().describe("The maximum number of lines to read (defaults to 2000)").optional(), }), async execute(params, ctx) { let filepath = params.filePath @@ -40,7 +40,9 @@ export const ReadTool = Tool.define("read", { }) const file = Bun.file(filepath) - if (!(await file.exists())) { + const stat = await file.stat().catch(() => undefined) + + if (!stat) { const dir = path.dirname(filepath) const base = path.basename(filepath) @@ -60,6 +62,45 @@ export const ReadTool = Tool.define("read", { throw new Error(`File not found: ${filepath}`) } + if (stat.isDirectory()) { + const dirents = await fs.promises.readdir(filepath, { withFileTypes: true }) + const entries = await Promise.all( + dirents.map(async (dirent) => { + if (dirent.isDirectory()) return dirent.name + "/" + if (dirent.isSymbolicLink()) { + const target = await fs.promises.stat(path.join(filepath, dirent.name)).catch(() => undefined) + if (target?.isDirectory()) return dirent.name + "/" + } + return dirent.name + }), + ) + entries.sort((a, b) => a.localeCompare(b)) + + const limit = params.limit ?? DEFAULT_READ_LIMIT + const offset = params.offset || 0 + const sliced = entries.slice(offset, offset + limit) + const truncated = sliced.length < entries.length + + const output = [ + `${filepath}`, + `directory`, + ``, + sliced.join("\n"), + truncated + ? `\n(Showing ${sliced.length} of ${entries.length} entries. Use 'offset' parameter to read beyond entry ${offset + sliced.length})` + : `\n(${entries.length} entries)`, + ``, + ].join("\n") + + return { + title, + output, + metadata: { + truncated, + }, + } + } + const instructions = await InstructionPrompt.resolve(ctx.messages, filepath, ctx.messageID) // Exclude SVG (XML-based) and vnd.fastbidsheet (.fbs extension, commonly FlatBuffers schema files) @@ -112,11 +153,11 @@ export const ReadTool = Tool.define("read", { } const content = raw.map((line, index) => { - return `${(index + offset + 1).toString().padStart(5, "0")}| ${line}` + return `${index + offset + 1}: ${line}` }) const preview = raw.slice(0, 20).join("\n") - let output = "\n" + let output = [`${filepath}`, `file`, ""].join("\n") output += content.join("\n") const totalLines = lines.length @@ -131,7 +172,7 @@ export const ReadTool = Tool.define("read", { } else { output += `\n\n(End of file - total ${totalLines} lines)` } - output += "\n" + output += "\n" // just warms the lsp client LSP.touchFile(filepath, false) diff --git a/packages/opencode/src/tool/read.txt b/packages/opencode/src/tool/read.txt index b5bffee263..dfb3ecf1cc 100644 --- a/packages/opencode/src/tool/read.txt +++ b/packages/opencode/src/tool/read.txt @@ -1,12 +1,13 @@ -Reads a file from the local filesystem. You can access any file directly by using this tool. -Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned. +Read a file from the local filesystem. If the file does not exist, an error is returned. Usage: -- The filePath parameter must be an absolute path, not a relative path -- By default, it reads up to 2000 lines starting from the beginning of the file -- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters -- Any lines longer than 2000 characters will be truncated -- Results are returned using cat -n format, with line numbers starting at 1 -- You have the capability to call multiple tools in a single response. It is always better to speculatively read multiple files as a batch that are potentially useful. -- If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents. -- You can read image files using this tool. +- The filePath parameter should be an absolute path. +- By default, this tool returns up to 2000 lines from the start of the file. +- To read later sections, call this tool again with a larger offset. +- Use the grep tool to find specific content in large files or files with long lines. +- If you are unsure of the correct file path, use the glob tool to look up filenames by glob pattern. +- Contents are returned with each line prefixed by its line number as `: `. For example, if a file has contents "foo\n", you will receive "1: foo\n". For directories, entries are returned one per line (without line numbers) with a trailing `/` for subdirectories. +- Any line longer than 2000 characters is truncated. +- Call this tool in parallel when you know there are multiple files you want to read. +- Avoid tiny repeated slices (30 line chunks). If you need more context, read a larger window. +- This tool can read image files and PDFs and return them as file attachments.