/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import path from 'path'; import { SchemaValidator } from '../utils/schemaValidator.js'; import { makeRelative, shortenPath } from '../utils/paths.js'; import { BaseTool, ToolResult } from './tools.js'; import { isWithinRoot, processSingleFileContent } from '../utils/fileUtils.js'; /** * Parameters for the ReadFile tool */ export interface ReadFileToolParams { /** * The absolute path to the file to read */ path: string; /** * The line number to start reading from (optional) */ offset?: number; /** * The number of lines to read (optional) */ limit?: number; } /** * Implementation of the ReadFile tool logic */ export class ReadFileTool extends BaseTool { static readonly Name: string = 'read_file'; constructor(private rootDirectory: string) { super( ReadFileTool.Name, 'ReadFile', 'Reads and returns the content of a specified file from the local filesystem. Handles text, images (PNG, JPG, GIF, WEBP, SVG, BMP), and PDF files. For text files, it can read specific line ranges.', { properties: { path: { description: "The absolute path to the file to read (e.g., '/home/user/project/file.txt'). Relative paths are not supported.", type: 'string', }, offset: { description: "Optional: For text files, the 0-based line number to start reading from. Requires 'limit' to be set. Use for paginating through large files.", type: 'number', }, limit: { description: "Optional: For text files, maximum number of lines to read. Use with 'offset' to paginate through large files. If omitted, reads the entire file (if feasible, up to a default limit).", type: 'number', }, }, required: ['path'], type: 'object', }, ); this.rootDirectory = path.resolve(rootDirectory); } validateToolParams(params: ReadFileToolParams): string | null { if ( this.schema.parameters && !SchemaValidator.validate( this.schema.parameters as Record, params, ) ) { return 'Parameters failed schema validation.'; } const filePath = params.path; if (!path.isAbsolute(filePath)) { return `File path must be absolute: ${filePath}`; } if (!isWithinRoot(filePath, this.rootDirectory)) { return `File path must be within the root directory (${this.rootDirectory}): ${filePath}`; } if (params.offset !== undefined && params.offset < 0) { return 'Offset must be a non-negative number'; } if (params.limit !== undefined && params.limit <= 0) { return 'Limit must be a positive number'; } return null; } getDescription(params: ReadFileToolParams): string { if ( !params || typeof params.path !== 'string' || params.path.trim() === '' ) { return `Path unavailable`; } const relativePath = makeRelative(params.path, this.rootDirectory); return shortenPath(relativePath); } async execute( params: ReadFileToolParams, _signal: AbortSignal, ): Promise { const validationError = this.validateToolParams(params); if (validationError) { return { llmContent: `Error: Invalid parameters provided. Reason: ${validationError}`, returnDisplay: validationError, }; } const result = await processSingleFileContent( params.path, this.rootDirectory, params.offset, params.limit, ); if (result.error) { return { llmContent: result.error, // The detailed error for LLM returnDisplay: result.returnDisplay, // User-friendly error }; } return { llmContent: result.llmContent, returnDisplay: result.returnDisplay, }; } }