| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent e347152 commit 6b4d617
5 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -32,6 +32,9 @@ description: Use this when you are working on file operations like reading, writ | |||
| 32 | 32 | - Decode tool stderr with `Bun.readableStreamToText`. | |
| 33 | 33 | - For large writes, use `Bun.write(Bun.file(path), text)`. | |
| 34 | 34 | ||
| 35 | + NOTE: Bun.file(...).exists() will return `false` if the value is a directory. | ||
| 36 | + Use Filesystem.exists(...) instead if path can be file or directory | ||
| 37 | + | ||
| 35 | 38 | ## Quick checklist | |
| 36 | 39 | ||
| 37 | 40 | - Use Bun APIs first. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2,9 +2,9 @@ Performs exact string replacements in files. | |||
| 2 | 2 | ||
| 3 | 3 | Usage: | |
| 4 | 4 | - 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. | |
| 5 | - - 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. | ||
| 5 | + - 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. | ||
| 6 | 6 | - ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required. | |
| 7 | 7 | - Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked. | |
| 8 | 8 | - The edit will FAIL if `oldString` is not found in the file with an error "oldString not found in content". | |
| 9 | - - The edit will FAIL if `oldString` is found multiple times in the file with an error "oldString found multiple times and requires more code context to uniquely identify the intended match". Either provide a larger string with more surrounding context to make it unique or use `replaceAll` to change every instance of `oldString`. | ||
| 9 | + - The edit will FAIL if `oldString` is found multiple times in the file with an error "Found multiple matches for oldString. Provide more surrounding lines in oldString to identify the correct match." Either provide a larger string with more surrounding context to make it unique or use `replaceAll` to change every instance of `oldString`. | ||
| 10 | 10 | - Use `replaceAll` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -17,9 +17,9 @@ const MAX_BYTES = 50 * 1024 | |||
| 17 | 17 | export const ReadTool = Tool.define("read", { | |
| 18 | 18 | description: DESCRIPTION, | |
| 19 | 19 | parameters: z.object({ | |
| 20 | - filePath: z.string().describe("The path to the file to read"), | ||
| 21 | - offset: z.coerce.number().describe("The line number to start reading from (0-based)").optional(), | ||
| 22 | - limit: z.coerce.number().describe("The number of lines to read (defaults to 2000)").optional(), | ||
| 20 | + filePath: z.string().describe("The absolute path to the file or directory to read"), | ||
| 21 | + offset: z.coerce.number().describe("The 0-based line offset to start reading from").optional(), | ||
| 22 | + limit: z.coerce.number().describe("The maximum number of lines to read (defaults to 2000)").optional(), | ||
| 23 | 23 | }), | |
| 24 | 24 | async execute(params, ctx) { | |
| 25 | 25 | let filepath = params.filePath | |
@@ -28,8 +28,12 @@ export const ReadTool = Tool.define("read", { | |||
| 28 | 28 | } | |
| 29 | 29 | const title = path.relative(Instance.worktree, filepath) | |
| 30 | 30 | ||
| 31 | + const file = Bun.file(filepath) | ||
| 32 | + const stat = await file.stat().catch(() => undefined) | ||
| 33 | + | ||
| 31 | 34 | await assertExternalDirectory(ctx, filepath, { | |
| 32 | 35 | bypass: Boolean(ctx.extra?.["bypassCwdCheck"]), | |
| 36 | + kind: stat?.isDirectory() ? "directory" : "file", | ||
| 33 | 37 | }) | |
| 34 | 38 | ||
| 35 | 39 | await ctx.ask({ | |
@@ -39,8 +43,7 @@ export const ReadTool = Tool.define("read", { | |||
| 39 | 43 | metadata: {}, | |
| 40 | 44 | }) | |
| 41 | 45 | ||
| 42 | - const file = Bun.file(filepath) | ||
| 43 | - if (!(await file.exists())) { | ||
| 46 | + if (!stat) { | ||
| 44 | 47 | const dir = path.dirname(filepath) | |
| 45 | 48 | const base = path.basename(filepath) | |
| 46 | 49 | ||
@@ -60,6 +63,47 @@ export const ReadTool = Tool.define("read", { | |||
| 60 | 63 | throw new Error(`File not found: ${filepath}`) | |
| 61 | 64 | } | |
| 62 | 65 | ||
| 66 | + if (stat.isDirectory()) { | ||
| 67 | + const dirents = await fs.promises.readdir(filepath, { withFileTypes: true }) | ||
| 68 | + const entries = await Promise.all( | ||
| 69 | + dirents.map(async (dirent) => { | ||
| 70 | + if (dirent.isDirectory()) return dirent.name + "/" | ||
| 71 | + if (dirent.isSymbolicLink()) { | ||
| 72 | + const target = await fs.promises.stat(path.join(filepath, dirent.name)).catch(() => undefined) | ||
| 73 | + if (target?.isDirectory()) return dirent.name + "/" | ||
| 74 | + } | ||
| 75 | + return dirent.name | ||
| 76 | + }), | ||
| 77 | + ) | ||
| 78 | + entries.sort((a, b) => a.localeCompare(b)) | ||
| 79 | + | ||
| 80 | + const limit = params.limit ?? DEFAULT_READ_LIMIT | ||
| 81 | + const offset = params.offset || 0 | ||
| 82 | + const sliced = entries.slice(offset, offset + limit) | ||
| 83 | + const truncated = offset + sliced.length < entries.length | ||
| 84 | + | ||
| 85 | + const output = [ | ||
| 86 | + `<path>${filepath}</path>`, | ||
| 87 | + `<type>directory</type>`, | ||
| 88 | + `<entries>`, | ||
| 89 | + sliced.join("\n"), | ||
| 90 | + truncated | ||
| 91 | + ? `\n(Showing ${sliced.length} of ${entries.length} entries. Use 'offset' parameter to read beyond entry ${offset + sliced.length})` | ||
| 92 | + : `\n(${entries.length} entries)`, | ||
| 93 | + `</entries>`, | ||
| 94 | + ].join("\n") | ||
| 95 | + | ||
| 96 | + return { | ||
| 97 | + title, | ||
| 98 | + output, | ||
| 99 | + metadata: { | ||
| 100 | + preview: sliced.slice(0, 20).join("\n"), | ||
| 101 | + truncated, | ||
| 102 | + loaded: [] as string[], | ||
| 103 | + }, | ||
| 104 | + } | ||
| 105 | + } | ||
| 106 | + | ||
| 63 | 107 | const instructions = await InstructionPrompt.resolve(ctx.messages, filepath, ctx.messageID) | |
| 64 | 108 | ||
| 65 | 109 | // Exclude SVG (XML-based) and vnd.fastbidsheet (.fbs extension, commonly FlatBuffers schema files) | |
@@ -75,7 +119,7 @@ export const ReadTool = Tool.define("read", { | |||
| 75 | 119 | metadata: { | |
| 76 | 120 | preview: msg, | |
| 77 | 121 | truncated: false, | |
| 78 | - ...(instructions.length > 0 && { loaded: instructions.map((i) => i.filepath) }), | ||
| 122 | + loaded: instructions.map((i) => i.filepath), | ||
| 79 | 123 | }, | |
| 80 | 124 | attachments: [ | |
| 81 | 125 | { | |
@@ -112,11 +156,11 @@ export const ReadTool = Tool.define("read", { | |||
| 112 | 156 | } | |
| 113 | 157 | ||
| 114 | 158 | const content = raw.map((line, index) => { | |
| 115 | - return `${(index + offset + 1).toString().padStart(5, "0")}| ${line}` | ||
| 159 | + return `${index + offset + 1}: ${line}` | ||
| 116 | 160 | }) | |
| 117 | 161 | const preview = raw.slice(0, 20).join("\n") | |
| 118 | 162 | ||
| 119 | - let output = "<file>\n" | ||
| 163 | + let output = [`<path>${filepath}</path>`, `<type>file</type>`, "<content>"].join("\n") | ||
| 120 | 164 | output += content.join("\n") | |
| 121 | 165 | ||
| 122 | 166 | const totalLines = lines.length | |
@@ -131,7 +175,7 @@ export const ReadTool = Tool.define("read", { | |||
| 131 | 175 | } else { | |
| 132 | 176 | output += `\n\n(End of file - total ${totalLines} lines)` | |
| 133 | 177 | } | |
| 134 | - output += "\n</file>" | ||
| 178 | + output += "\n</content>" | ||
| 135 | 179 | ||
| 136 | 180 | // just warms the lsp client | |
| 137 | 181 | LSP.touchFile(filepath, false) | |
@@ -147,7 +191,7 @@ export const ReadTool = Tool.define("read", { | |||
| 147 | 191 | metadata: { | |
| 148 | 192 | preview, | |
| 149 | 193 | truncated, | |
| 150 | - ...(instructions.length > 0 && { loaded: instructions.map((i) => i.filepath) }), | ||
| 194 | + loaded: instructions.map((i) => i.filepath), | ||
| 151 | 195 | }, | |
| 152 | 196 | } | |
| 153 | 197 | }, | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,12 +1,13 @@ | |||
| 1 | - Reads a file from the local filesystem. You can access any file directly by using this tool. | ||
| 2 | - 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. | ||
| 1 | + Read a file or directory from the local filesystem. If the path does not exist, an error is returned. | ||
| 3 | 2 | ||
| 4 | 3 | Usage: | |
| 5 | - - The filePath parameter must be an absolute path, not a relative path | ||
| 6 | - - By default, it reads up to 2000 lines starting from the beginning of the file | ||
| 7 | - - 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 | ||
| 8 | - - Any lines longer than 2000 characters will be truncated | ||
| 9 | - - Results are returned using cat -n format, with line numbers starting at 1 | ||
| 10 | - - 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. | ||
| 11 | - - If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents. | ||
| 12 | - - You can read image files using this tool. | ||
| 4 | + - The filePath parameter should be an absolute path. | ||
| 5 | + - By default, this tool returns up to 2000 lines from the start of the file. | ||
| 6 | + - To read later sections, call this tool again with a larger offset. | ||
| 7 | + - Use the grep tool to find specific content in large files or files with long lines. | ||
| 8 | + - If you are unsure of the correct file path, use the glob tool to look up filenames by glob pattern. | ||
| 9 | + - Contents are returned with each line prefixed by its line number as `<line>: <content>`. 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. | ||
| 10 | + - Any line longer than 2000 characters is truncated. | ||
| 11 | + - Call this tool in parallel when you know there are multiple files you want to read. | ||
| 12 | + - Avoid tiny repeated slices (30 line chunks). If you need more context, read a larger window. | ||
| 13 | + - This tool can read image files and PDFs and return them as file attachments. | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -78,6 +78,32 @@ describe("tool.read external_directory permission", () => { | |||
| 78 | 78 | }) | |
| 79 | 79 | }) | |
| 80 | 80 | ||
| 81 | + test("asks for directory-scoped external_directory permission when reading external directory", async () => { | ||
| 82 | + await using outerTmp = await tmpdir({ | ||
| 83 | + init: async (dir) => { | ||
| 84 | + await Bun.write(path.join(dir, "external", "a.txt"), "a") | ||
| 85 | + }, | ||
| 86 | + }) | ||
| 87 | + await using tmp = await tmpdir({ git: true }) | ||
| 88 | + await Instance.provide({ | ||
| 89 | + directory: tmp.path, | ||
| 90 | + fn: async () => { | ||
| 91 | + const read = await ReadTool.init() | ||
| 92 | + const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = [] | ||
| 93 | + const testCtx = { | ||
| 94 | + ...ctx, | ||
| 95 | + ask: async (req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) => { | ||
| 96 | + requests.push(req) | ||
| 97 | + }, | ||
| 98 | + } | ||
| 99 | + await read.execute({ filePath: path.join(outerTmp.path, "external") }, testCtx) | ||
| 100 | + const extDirReq = requests.find((r) => r.permission === "external_directory") | ||
| 101 | + expect(extDirReq).toBeDefined() | ||
| 102 | + expect(extDirReq!.patterns).toContain(path.join(outerTmp.path, "external", "*")) | ||
| 103 | + }, | ||
| 104 | + }) | ||
| 105 | + }) | ||
| 106 | + | ||
| 81 | 107 | test("asks for external_directory permission when reading relative path outside project", async () => { | |
| 82 | 108 | await using tmp = await tmpdir({ git: true }) | |
| 83 | 109 | await Instance.provide({ | |
@@ -249,6 +275,25 @@ describe("tool.read truncation", () => { | |||
| 249 | 275 | }) | |
| 250 | 276 | }) | |
| 251 | 277 | ||
| 278 | + test("does not mark final directory page as truncated", async () => { | ||
| 279 | + await using tmp = await tmpdir({ | ||
| 280 | + init: async (dir) => { | ||
| 281 | + await Promise.all( | ||
| 282 | + Array.from({ length: 10 }, (_, i) => Bun.write(path.join(dir, "dir", `file-${i}.txt`), `line${i}`)), | ||
| 283 | + ) | ||
| 284 | + }, | ||
| 285 | + }) | ||
| 286 | + await Instance.provide({ | ||
| 287 | + directory: tmp.path, | ||
| 288 | + fn: async () => { | ||
| 289 | + const read = await ReadTool.init() | ||
| 290 | + const result = await read.execute({ filePath: path.join(tmp.path, "dir"), offset: 5, limit: 5 }, ctx) | ||
| 291 | + expect(result.metadata.truncated).toBe(false) | ||
| 292 | + expect(result.output).not.toContain("Showing 5 of 10 entries") | ||
| 293 | + }, | ||
| 294 | + }) | ||
| 295 | + }) | ||
| 296 | + | ||
| 252 | 297 | test("truncates long lines", async () => { | |
| 253 | 298 | await using tmp = await tmpdir({ | |
| 254 | 299 | init: async (dir) => { | |
| Back | FazBrowse Home | New Git URL |
0 commit comments