import fs from 'fs/promises' import path from 'path' import * as z from 'zod' import { DEFAULT_READ_LIMIT, isBinaryFile, MAX_LINE_LENGTH, validatePath } from '../types' // Schema definition export const ReadToolSchema = z.object({ file_path: z.string().describe('The path to the file to read'), offset: z.number().optional().describe('The line number to start reading from (1-based)'), limit: z.number().optional().describe('The number of lines to read (defaults to 2000)') }) // Tool definition with detailed description export const readToolDefinition = { name: 'read', description: `Reads a file from the local filesystem. - Assumes this tool can read all files on the machine - The file_path parameter must be an absolute path, not a relative path - By default, reads up to 2000 lines starting from the beginning - You can optionally specify a line offset and limit for long files - Any lines longer than 2000 characters will be truncated - Results are returned with line numbers starting at 1 - Binary files are detected and rejected with an error - Empty files return a warning`, inputSchema: z.toJSONSchema(ReadToolSchema) } // Handler implementation export async function handleReadTool(args: unknown, baseDir: string) { const parsed = ReadToolSchema.safeParse(args) if (!parsed.success) { throw new Error(`Invalid arguments for read: ${parsed.error}`) } const filePath = parsed.data.file_path const validPath = await validatePath(filePath, baseDir) // Check if file exists try { const stats = await fs.stat(validPath) if (!stats.isFile()) { throw new Error(`Path is not a file: ${filePath}`) } } catch (error: any) { if (error.code === 'ENOENT') { throw new Error(`File not found: ${filePath}`) } throw error } // Check if file is binary if (await isBinaryFile(validPath)) { throw new Error(`Cannot read binary file: ${filePath}`) } // Read file content const content = await fs.readFile(validPath, 'utf-8') const lines = content.split('\n') // Apply offset and limit const offset = (parsed.data.offset || 1) - 1 // Convert to 0-based const limit = parsed.data.limit || DEFAULT_READ_LIMIT if (offset < 0 || offset >= lines.length) { throw new Error(`Invalid offset: ${offset + 1}. File has ${lines.length} lines.`) } const selectedLines = lines.slice(offset, offset + limit) // Format output with line numbers and truncate long lines const output: string[] = [] const relativePath = path.relative(baseDir, validPath) output.push(`File: ${relativePath}`) if (offset > 0 || limit < lines.length) { output.push(`Lines ${offset + 1} to ${Math.min(offset + limit, lines.length)} of ${lines.length}`) } output.push('') selectedLines.forEach((line, index) => { const lineNumber = offset + index + 1 const truncatedLine = line.length > MAX_LINE_LENGTH ? line.substring(0, MAX_LINE_LENGTH) + '...' : line output.push(`${lineNumber.toString().padStart(6)}\t${truncatedLine}`) }) if (offset + limit < lines.length) { output.push('') output.push(`(${lines.length - (offset + limit)} more lines not shown)`) } return { content: [ { type: 'text', text: output.join('\n') } ] } }