import * as plugins from './plugins.js'; import * as interfaces from './smartagent.interfaces.js'; import { BaseToolWrapper } from './smartagent.tools.base.js'; /** * Options for FilesystemTool */ export interface IFilesystemToolOptions { /** Base path to scope all operations to. If set, all paths must be within this directory. */ basePath?: string; } /** * Filesystem tool for file and directory operations * Wraps @push.rocks/smartfs */ export class FilesystemTool extends BaseToolWrapper { public name = 'filesystem'; public description = 'Read, write, list, and delete files and directories'; /** Base path to scope all operations to */ private basePath?: string; constructor(options?: IFilesystemToolOptions) { super(); if (options?.basePath) { this.basePath = plugins.path.resolve(options.basePath); } } /** * Validate that a path is within the allowed base path * @throws Error if path is outside allowed directory */ private validatePath(pathArg: string): string { const resolved = plugins.path.resolve(pathArg); if (this.basePath) { // Ensure the resolved path starts with the base path if (!resolved.startsWith(this.basePath + plugins.path.sep) && resolved !== this.basePath) { throw new Error(`Access denied: path "${pathArg}" is outside allowed directory "${this.basePath}"`); } } return resolved; } public actions: interfaces.IToolAction[] = [ { name: 'read', description: 'Read the contents of a file', parameters: { type: 'object', properties: { path: { type: 'string', description: 'Absolute path to the file' }, encoding: { type: 'string', enum: ['utf8', 'binary', 'base64'], default: 'utf8', description: 'File encoding', }, }, required: ['path'], }, }, { name: 'write', description: 'Write content to a file (creates or overwrites)', parameters: { type: 'object', properties: { path: { type: 'string', description: 'Absolute path to the file' }, content: { type: 'string', description: 'Content to write' }, encoding: { type: 'string', enum: ['utf8', 'binary', 'base64'], default: 'utf8', description: 'File encoding', }, }, required: ['path', 'content'], }, }, { name: 'append', description: 'Append content to a file', parameters: { type: 'object', properties: { path: { type: 'string', description: 'Absolute path to the file' }, content: { type: 'string', description: 'Content to append' }, }, required: ['path', 'content'], }, }, { name: 'list', description: 'List files and directories in a path', parameters: { type: 'object', properties: { path: { type: 'string', description: 'Directory path to list' }, recursive: { type: 'boolean', default: false, description: 'List recursively' }, filter: { type: 'string', description: 'Glob pattern to filter results (e.g., "*.ts")' }, }, required: ['path'], }, }, { name: 'delete', description: 'Delete a file or directory', parameters: { type: 'object', properties: { path: { type: 'string', description: 'Path to delete' }, recursive: { type: 'boolean', default: false, description: 'For directories, delete recursively', }, }, required: ['path'], }, }, { name: 'exists', description: 'Check if a file or directory exists', parameters: { type: 'object', properties: { path: { type: 'string', description: 'Path to check' }, }, required: ['path'], }, }, { name: 'stat', description: 'Get file or directory statistics (size, dates, etc.)', parameters: { type: 'object', properties: { path: { type: 'string', description: 'Path to get stats for' }, }, required: ['path'], }, }, { name: 'copy', description: 'Copy a file to a new location', parameters: { type: 'object', properties: { source: { type: 'string', description: 'Source file path' }, destination: { type: 'string', description: 'Destination file path' }, }, required: ['source', 'destination'], }, }, { name: 'move', description: 'Move a file to a new location', parameters: { type: 'object', properties: { source: { type: 'string', description: 'Source file path' }, destination: { type: 'string', description: 'Destination file path' }, }, required: ['source', 'destination'], }, }, { name: 'mkdir', description: 'Create a directory', parameters: { type: 'object', properties: { path: { type: 'string', description: 'Directory path to create' }, recursive: { type: 'boolean', default: true, description: 'Create parent directories if needed', }, }, required: ['path'], }, }, ]; private smartfs!: plugins.smartfs.SmartFs; public async initialize(): Promise { this.smartfs = new plugins.smartfs.SmartFs(new plugins.smartfs.SmartFsProviderNode()); this.isInitialized = true; } public async cleanup(): Promise { this.isInitialized = false; } public async execute( action: string, params: Record ): Promise { this.validateAction(action); this.ensureInitialized(); try { switch (action) { case 'read': { const validatedPath = this.validatePath(params.path as string); const encoding = (params.encoding as string) || 'utf8'; const content = await this.smartfs .file(validatedPath) .encoding(encoding as 'utf8' | 'binary' | 'base64') .read(); return { success: true, result: { path: params.path, content: content.toString(), encoding, }, }; } case 'write': { const validatedPath = this.validatePath(params.path as string); const encoding = (params.encoding as string) || 'utf8'; await this.smartfs .file(validatedPath) .encoding(encoding as 'utf8' | 'binary' | 'base64') .write(params.content as string); return { success: true, result: { path: params.path, written: true, bytesWritten: (params.content as string).length, }, }; } case 'append': { const validatedPath = this.validatePath(params.path as string); await this.smartfs.file(validatedPath).append(params.content as string); return { success: true, result: { path: params.path, appended: true, }, }; } case 'list': { const validatedPath = this.validatePath(params.path as string); let dir = this.smartfs.directory(validatedPath); if (params.recursive) { dir = dir.recursive(); } if (params.filter) { dir = dir.filter(params.filter as string); } const entries = await dir.list(); return { success: true, result: { path: params.path, entries, count: entries.length, }, }; } case 'delete': { const validatedPath = this.validatePath(params.path as string); // Check if it's a directory or file const exists = await this.smartfs.file(validatedPath).exists(); if (exists) { // Try to get stats to check if it's a directory try { const stats = await this.smartfs.file(validatedPath).stat(); if (stats.isDirectory && params.recursive) { await this.smartfs.directory(validatedPath).recursive().delete(); } else { await this.smartfs.file(validatedPath).delete(); } } catch { await this.smartfs.file(validatedPath).delete(); } } return { success: true, result: { path: params.path, deleted: true, }, }; } case 'exists': { const validatedPath = this.validatePath(params.path as string); const exists = await this.smartfs.file(validatedPath).exists(); return { success: true, result: { path: params.path, exists, }, }; } case 'stat': { const validatedPath = this.validatePath(params.path as string); const stats = await this.smartfs.file(validatedPath).stat(); return { success: true, result: { path: params.path, stats, }, }; } case 'copy': { const validatedSource = this.validatePath(params.source as string); const validatedDest = this.validatePath(params.destination as string); await this.smartfs.file(validatedSource).copy(validatedDest); return { success: true, result: { source: params.source, destination: params.destination, copied: true, }, }; } case 'move': { const validatedSource = this.validatePath(params.source as string); const validatedDest = this.validatePath(params.destination as string); await this.smartfs.file(validatedSource).move(validatedDest); return { success: true, result: { source: params.source, destination: params.destination, moved: true, }, }; } case 'mkdir': { const validatedPath = this.validatePath(params.path as string); let dir = this.smartfs.directory(validatedPath); if (params.recursive !== false) { dir = dir.recursive(); } await dir.create(); return { success: true, result: { path: params.path, created: true, }, }; } default: return { success: false, error: `Unknown action: ${action}`, }; } } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), }; } } public getCallSummary(action: string, params: Record): string { switch (action) { case 'read': return `Read file "${params.path}" with encoding ${params.encoding || 'utf8'}`; case 'write': { const content = params.content as string; const preview = content.length > 100 ? content.substring(0, 100) + '...' : content; return `Write ${content.length} bytes to "${params.path}". Content preview: "${preview}"`; } case 'append': { const content = params.content as string; const preview = content.length > 100 ? content.substring(0, 100) + '...' : content; return `Append ${content.length} bytes to "${params.path}". Content preview: "${preview}"`; } case 'list': return `List directory "${params.path}"${params.recursive ? ' recursively' : ''}${params.filter ? ` with filter "${params.filter}"` : ''}`; case 'delete': return `Delete "${params.path}"${params.recursive ? ' recursively' : ''}`; case 'exists': return `Check if "${params.path}" exists`; case 'stat': return `Get statistics for "${params.path}"`; case 'copy': return `Copy "${params.source}" to "${params.destination}"`; case 'move': return `Move "${params.source}" to "${params.destination}"`; case 'mkdir': return `Create directory "${params.path}"${params.recursive !== false ? ' (with parents)' : ''}`; default: return `Unknown action: ${action}`; } } }