'use strict'; const fs = require('fs').promises; const path = require('path'); module.exports = function (RED) { function AgentFileNode(config) { RED.nodes.createNode(this, config); const node = this; this.configNode = RED.nodes.getNode(config.agentConfig); this.operation = config.operation || 'read'; this.filePath = config.filePath || ''; this.encoding = config.encoding || 'utf8'; this.createDirs = config.createDirs !== false; if (!this.configNode) { node.error('No agent config node selected'); return; } node.on('input', async function (msg) { const cfg = node.configNode; const workspaceDir = cfg.workspaceDir || '/data/agent-workspace'; const operation = msg.operation || node.operation; const filePath = msg.filePath || node.filePath; const encoding = msg.encoding || node.encoding; if (!filePath) { node.error('No file path specified', msg); return; } const fullPath = path.isAbsolute(filePath) ? filePath : path.join(workspaceDir, filePath); const resolvedWorkspace = path.resolve(workspaceDir); const resolvedPath = path.resolve(fullPath); if (!resolvedPath.startsWith(resolvedWorkspace)) { node.error('Path traversal attempt blocked', msg); return; } node.status({ fill: 'blue', shape: 'dot', text: operation }); try { let result; switch (operation) { case 'read': result = await fs.readFile(resolvedPath, encoding); break; case 'write': const content = msg.payload; if (node.createDirs) { await fs.mkdir(path.dirname(resolvedPath), { recursive: true }); } await fs.writeFile(resolvedPath, content, encoding); result = { written: true, path: filePath, bytes: Buffer.byteLength(content, encoding) }; break; case 'append': const appendContent = msg.payload; if (node.createDirs) { await fs.mkdir(path.dirname(resolvedPath), { recursive: true }); } await fs.appendFile(resolvedPath, appendContent, encoding); result = { appended: true, path: filePath, bytes: Buffer.byteLength(appendContent, encoding) }; break; case 'delete': await fs.unlink(resolvedPath); result = { deleted: true, path: filePath }; break; case 'exists': try { await fs.access(resolvedPath); result = { exists: true, path: filePath }; } catch { result = { exists: false, path: filePath }; } break; case 'list': const entries = await fs.readdir(resolvedPath, { withFileTypes: true }); result = entries.map(e => ({ name: e.name, path: path.join(filePath, e.name), isDirectory: e.isDirectory(), isFile: e.isFile() })); break; case 'stat': const stats = await fs.stat(resolvedPath); result = { path: filePath, size: stats.size, isDirectory: stats.isDirectory(), isFile: stats.isFile(), created: stats.birthtime, modified: stats.mtime }; break; case 'mkdir': await fs.mkdir(resolvedPath, { recursive: true }); result = { created: true, path: filePath }; break; default: throw new Error(`Unknown operation: ${operation}`); } node.status({ fill: 'green', shape: 'dot', text: 'Done' }); msg.payload = result; msg.filePath = filePath; msg.operation = operation; node.send(msg); } catch (err) { node.status({ fill: 'red', shape: 'ring', text: 'Error' }); msg.error = err.message; msg.filePath = filePath; msg.operation = operation; node.error(`File ${operation} failed: ${err.message}`, msg); } }); } RED.nodes.registerType('agent-file', AgentFileNode); };