/** * File Operation Tool * Handle file system operations safely */ const fs = require('fs').promises; const path = require('path'); const { BaseTool, ToolResult } = require('./tool-base.cjs'); class FileOperationTool extends BaseTool { constructor(config = {}) { super({ name: 'file', description: 'Perform file system operations (read, write, list, etc.)', parameters: [ { name: 'operation', type: 'string', required: true, description: 'Operation to perform: read, write, list, delete, move, copy, exists, stat' }, { name: 'path', type: 'string', required: false, description: 'File or directory path' }, { name: 'content', type: 'string', required: false, description: 'Content for write operations' }, { name: 'destination', type: 'string', required: false, description: 'Destination path for move/copy operations' }, { name: 'encoding', type: 'string', required: false, description: 'File encoding (default: utf8)' } ], ...config }); this.allowedPaths = config.allowedPaths || []; this.maxFileSize = config.maxFileSize || 1024 * 1024; // 1MB } async execute(params) { const { operation, path: filePath, content, destination, encoding = 'utf8' } = params; // Validate path const validation = this.validatePath(filePath); if (!validation.valid) { throw new Error(`Path validation failed: ${validation.reason}`); } try { switch (operation) { case 'read': return await this.readFile(filePath, encoding); case 'write': return await this.writeFile(filePath, content, encoding); case 'list': return await this.listFiles(filePath); case 'delete': return await this.deleteFile(filePath); case 'move': return await this.moveFile(filePath, destination); case 'copy': return await this.copyFile(filePath, destination); case 'exists': return await this.fileExists(filePath); case 'stat': return await this.getFileStats(filePath); default: throw new Error(`Unknown operation: ${operation}`); } } catch (error) { return ToolResult.failure( error, `File operation '${operation}' failed: ${error.message}`, { operation, path: filePath } ); } } /** * Validate file path against security rules */ validatePath(filePath) { if (!filePath) { return { valid: false, reason: 'Path is required' }; } // Resolve absolute path const resolvedPath = path.resolve(filePath); // Check against allowed paths if configured if (this.allowedPaths.length > 0) { const isAllowed = this.allowedPaths.some(allowedPath => { const resolvedAllowed = path.resolve(allowedPath); return resolvedPath.startsWith(resolvedAllowed); }); if (!isAllowed) { return { valid: false, reason: 'Path is outside allowed directories' }; } } // Prevent path traversal if (filePath.includes('..')) { return { valid: false, reason: 'Path traversal not allowed' }; } return { valid: true }; } async readFile(filePath, encoding) { const stats = await fs.stat(filePath); if (stats.size > this.maxFileSize) { throw new Error(`File too large (${stats.size} bytes, max ${this.maxFileSize})`); } if (!stats.isFile()) { throw new Error('Path is not a file'); } const content = await fs.readFile(filePath, encoding); return ToolResult.success( { content, size: stats.size }, content, { operation: 'read', path: filePath, size: stats.size } ); } async writeFile(filePath, content, encoding) { if (content === undefined || content === null) { throw new Error('Content is required for write operation'); } // Create parent directories if needed const dir = path.dirname(filePath); await fs.mkdir(dir, { recursive: true }); await fs.writeFile(filePath, content, encoding); const stats = await fs.stat(filePath); return ToolResult.success( { size: stats.size }, `Wrote ${stats.size} bytes to ${filePath}`, { operation: 'write', path: filePath, size: stats.size } ); } async listFiles(dirPath) { const stats = await fs.stat(dirPath); if (!stats.isDirectory()) { throw new Error('Path is not a directory'); } const entries = await fs.readdir(dirPath, { withFileTypes: true }); const files = entries.map(entry => ({ name: entry.name, type: entry.isDirectory() ? 'directory' : 'file', path: path.join(dirPath, entry.name) })); const output = files .map(f => `${f.type === 'directory' ? 'D' : 'F'} ${f.name}`) .join('\n'); return ToolResult.success( files, output || '[Empty directory]', { operation: 'list', path: dirPath, count: files.length } ); } async deleteFile(filePath) { const stats = await fs.stat(filePath); if (stats.isDirectory()) { await fs.rmdir(filePath, { recursive: true }); } else { await fs.unlink(filePath); } return ToolResult.success( { deleted: true }, `Deleted: ${filePath}`, { operation: 'delete', path: filePath } ); } async moveFile(source, destination) { if (!destination) { throw new Error('Destination is required for move operation'); } const destValidation = this.validatePath(destination); if (!destValidation.valid) { throw new Error(`Destination validation failed: ${destValidation.reason}`); } // Create parent directories const destDir = path.dirname(destination); await fs.mkdir(destDir, { recursive: true }); await fs.rename(source, destination); return ToolResult.success( { moved: true }, `Moved ${source} to ${destination}`, { operation: 'move', source, destination } ); } async copyFile(source, destination) { if (!destination) { throw new Error('Destination is required for copy operation'); } const destValidation = this.validatePath(destination); if (!destValidation.valid) { throw new Error(`Destination validation failed: ${destValidation.reason}`); } // Create parent directories const destDir = path.dirname(destination); await fs.mkdir(destDir, { recursive: true }); await fs.copyFile(source, destination); const stats = await fs.stat(destination); return ToolResult.success( { size: stats.size }, `Copied ${source} to ${destination}`, { operation: 'copy', source, destination, size: stats.size } ); } async fileExists(filePath) { try { await fs.access(filePath); return ToolResult.success( { exists: true }, `File exists: ${filePath}`, { operation: 'exists', path: filePath, exists: true } ); } catch { return ToolResult.success( { exists: false }, `File does not exist: ${filePath}`, { operation: 'exists', path: filePath, exists: false } ); } } async getFileStats(filePath) { const stats = await fs.stat(filePath); const info = { size: stats.size, created: stats.birthtime, modified: stats.mtime, accessed: stats.atime, isFile: stats.isFile(), isDirectory: stats.isDirectory(), permissions: stats.mode.toString(8) }; const output = ` Size: ${info.size} bytes Created: ${info.created} Modified: ${info.modified} Type: ${info.isFile ? 'File' : 'Directory'} Permissions: ${info.permissions} `.trim(); return ToolResult.success( info, output, { operation: 'stat', path: filePath } ); } } module.exports = { FileOperationTool };