diff options
| author | Evan Senter <[email protected]> | 2025-04-19 19:45:42 +0100 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-04-19 19:45:42 +0100 |
| commit | 3fce6cea27d3e6129d6c06e528b62e1b11bf7094 (patch) | |
| tree | 244b8e9ab94f902d65d4bda8739a6538e377ed17 /packages/cli/src/tools/read-file.tool.ts | |
| parent | 0c9e1ef61be7db53e6e73b7208b649cd8cbed6c3 (diff) | |
Starting to modularize into separate cli / server packages. (#55)
* Starting to move a lot of code into packages/server
* More of the massive refactor, builds and runs, some issues though.
* Fixing outstanding issue with double messages.
* Fixing a minor UI issue.
* Fixing the build post-merge.
* Running formatting.
* Addressing comments.
Diffstat (limited to 'packages/cli/src/tools/read-file.tool.ts')
| -rw-r--r-- | packages/cli/src/tools/read-file.tool.ts | 281 |
1 files changed, 29 insertions, 252 deletions
diff --git a/packages/cli/src/tools/read-file.tool.ts b/packages/cli/src/tools/read-file.tool.ts index 0ebe2dbc..bbf9eafb 100644 --- a/packages/cli/src/tools/read-file.tool.ts +++ b/packages/cli/src/tools/read-file.tool.ts @@ -4,288 +4,65 @@ * SPDX-License-Identifier: Apache-2.0 */ -import fs from 'fs'; -import path from 'path'; -import { SchemaValidator } from '../utils/schemaValidator.js'; -import { makeRelative, shortenPath } from '../utils/paths.js'; -import { BaseTool, ToolResult } from './tools.js'; +import { + ReadFileLogic, + ReadFileToolParams, + ToolResult, +} from '@gemini-code/server'; +import { BaseTool } from './tools.js'; +import { ToolCallConfirmationDetails } from '../ui/types.js'; /** - * Parameters for the ReadFile tool - */ -export interface ReadFileToolParams { - /** - * The absolute path to the file to read - */ - file_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 that reads files from the filesystem + * CLI wrapper for the ReadFile tool */ export class ReadFileTool extends BaseTool<ReadFileToolParams, ToolResult> { - static readonly Name: string = 'read_file'; - - // Maximum number of lines to read by default - private static readonly DEFAULT_MAX_LINES = 2000; - - // Maximum length of a line before truncating - private static readonly MAX_LINE_LENGTH = 2000; - - /** - * The root directory that this tool is grounded in. - * All file operations will be restricted to this directory. - */ - private rootDirectory: string; + static readonly Name: string = ReadFileLogic.Name; + private coreLogic: ReadFileLogic; /** - * Creates a new instance of the ReadFileTool - * @param rootDirectory Root directory to ground this tool in. All operations will be restricted to this directory. + * Creates a new instance of the ReadFileTool CLI wrapper + * @param rootDirectory Root directory to ground this tool in. */ constructor(rootDirectory: string) { + const coreLogicInstance = new ReadFileLogic(rootDirectory); super( ReadFileTool.Name, 'ReadFile', 'Reads and returns the content of a specified file from the local filesystem. Handles large files by allowing reading specific line ranges.', - { - properties: { - file_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: 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: Maximum number of lines to read. Use with 'offset' to paginate through large files. If omitted, reads the entire file (if feasible).", - type: 'number', - }, - }, - required: ['file_path'], - type: 'object', - }, + (coreLogicInstance.schema.parameters as Record<string, unknown>) ?? {}, ); - - // Set the root directory - this.rootDirectory = path.resolve(rootDirectory); + this.coreLogic = coreLogicInstance; } /** - * Checks if a path is within the root directory - * @param pathToCheck The path to check - * @returns True if the path is within the root directory, false otherwise + * Delegates validation to the core logic */ - private isWithinRoot(pathToCheck: string): boolean { - const normalizedPath = path.normalize(pathToCheck); - const normalizedRoot = path.normalize(this.rootDirectory); - - // Ensure the normalizedRoot ends with a path separator for proper path comparison - const rootWithSep = normalizedRoot.endsWith(path.sep) - ? normalizedRoot - : normalizedRoot + path.sep; - - return ( - normalizedPath === normalizedRoot || - normalizedPath.startsWith(rootWithSep) - ); - } - - /** - * Validates the parameters for the ReadFile tool - * @param params Parameters to validate - * @returns True if parameters are valid, false otherwise - */ - validateToolParams(params: ReadFileToolParams): string | null { - if ( - this.schema.parameters && - !SchemaValidator.validate( - this.schema.parameters as Record<string, unknown>, - params, - ) - ) { - return 'Parameters failed schema validation.'; - } - const filePath = params.file_path; - if (!path.isAbsolute(filePath)) { - return `File path must be absolute: ${filePath}`; - } - if (!this.isWithinRoot(filePath)) { - 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'; - } + validateToolParams(_params: ReadFileToolParams): string | null { + // Currently allowing any path. Add validation if needed. return null; } /** - * Determines if a file is likely binary based on content sampling - * @param filePath Path to the file - * @returns True if the file appears to be binary - */ - private isBinaryFile(filePath: string): boolean { - try { - // Read the first 4KB of the file - const fd = fs.openSync(filePath, 'r'); - const buffer = Buffer.alloc(4096); - const bytesRead = fs.readSync(fd, buffer, 0, 4096, 0); - fs.closeSync(fd); - - // Check for null bytes or high concentration of non-printable characters - let nonPrintableCount = 0; - for (let i = 0; i < bytesRead; i++) { - // Null byte is a strong indicator of binary data - if (buffer[i] === 0) { - return true; - } - - // Count non-printable characters - if (buffer[i] < 9 || (buffer[i] > 13 && buffer[i] < 32)) { - nonPrintableCount++; - } - } - - // If more than 30% are non-printable, likely binary - return nonPrintableCount / bytesRead > 0.3; - } catch { - return false; - } - } - - /** - * Detects the type of file based on extension and content - * @param filePath Path to the file - * @returns File type description + * Delegates getting description to the core logic */ - private detectFileType(filePath: string): string { - const ext = path.extname(filePath).toLowerCase(); - - // Common image formats - if ( - ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'].includes(ext) - ) { - return 'image'; - } - - // Other known binary formats - if (['.pdf', '.zip', '.tar', '.gz', '.exe', '.dll', '.so'].includes(ext)) { - return 'binary'; - } - - // Check content for binary indicators - if (this.isBinaryFile(filePath)) { - return 'binary'; - } - - return 'text'; + getDescription(_params: ReadFileToolParams): string { + return this.coreLogic.getDescription(_params); } /** - * Gets a description of the file reading operation - * @param params Parameters for the file reading - * @returns A string describing the file being read + * Define confirmation behavior here in the CLI wrapper if needed + * For ReadFile, we likely don't need confirmation. */ - getDescription(params: ReadFileToolParams): string { - const relativePath = makeRelative(params.file_path, this.rootDirectory); - return shortenPath(relativePath); + shouldConfirmExecute( + _params: ReadFileToolParams, + ): Promise<ToolCallConfirmationDetails | false> { + return Promise.resolve(false); } /** - * Reads a file and returns its contents with line numbers - * @param params Parameters for the file reading - * @returns Result with file contents + * Delegates execution to the core logic */ async execute(params: ReadFileToolParams): Promise<ToolResult> { - const validationError = this.validateToolParams(params); - if (validationError) { - return { - llmContent: `Error: Invalid parameters provided. Reason: ${validationError}`, - returnDisplay: '**Error:** Failed to execute tool.', - }; - } - - const filePath = params.file_path; - try { - if (!fs.existsSync(filePath)) { - return { - llmContent: `File not found: ${filePath}`, - returnDisplay: `File not found.`, - }; - } - - const stats = fs.statSync(filePath); - if (stats.isDirectory()) { - return { - llmContent: `Path is a directory, not a file: ${filePath}`, - returnDisplay: `File is directory.`, - }; - } - - const fileType = this.detectFileType(filePath); - if (fileType !== 'text') { - return { - llmContent: `Binary file: ${filePath} (${fileType})`, - returnDisplay: ``, - }; - } - - const content = fs.readFileSync(filePath, 'utf8'); - const lines = content.split('\n'); - - const startLine = params.offset || 0; - const endLine = params.limit - ? startLine + params.limit - : Math.min(startLine + ReadFileTool.DEFAULT_MAX_LINES, lines.length); - const selectedLines = lines.slice(startLine, endLine); - - let truncated = false; - const formattedLines = selectedLines.map((line) => { - let processedLine = line; - if (line.length > ReadFileTool.MAX_LINE_LENGTH) { - processedLine = - line.substring(0, ReadFileTool.MAX_LINE_LENGTH) + '... [truncated]'; - truncated = true; - } - - return processedLine; - }); - - const contentTruncated = endLine < lines.length || truncated; - - let llmContent = ''; - if (contentTruncated) { - llmContent += `[File truncated: showing lines ${startLine + 1}-${endLine} of ${lines.length} total lines. Use offset parameter to view more.]\n`; - } - llmContent += formattedLines.join('\n'); - - return { - llmContent, - returnDisplay: '', - }; - } catch (error) { - const errorMsg = `Error reading file: ${error instanceof Error ? error.message : String(error)}`; - - return { - llmContent: `Error reading file ${filePath}: ${errorMsg}`, - returnDisplay: `Failed to read file: ${errorMsg}`, - }; - } + return this.coreLogic.execute(params); } } |
