'use strict'; const { spawn } = require('child_process'); const path = require('path'); module.exports = function (RED) { function AgentExecNode(config) { RED.nodes.createNode(this, config); const node = this; this.configNode = RED.nodes.getNode(config.agentConfig); this.command = config.command || 'run'; this.args = config.args || ''; this.workingDir = config.workingDir || ''; this.timeout = parseInt(config.timeout) || 120000; this.outputFormat = config.outputFormat || 'text'; if (!this.configNode) { node.error('No agent config node selected'); return; } node.on('input', async function (msg) { const cfg = node.configNode; const startTime = Date.now(); const cliCmd = cfg.cliPath || 'agent'; const workspaceDir = msg.workingDir || node.workingDir || cfg.workspaceDir || '/data/agent-workspace'; const command = msg.command || node.command; const args = msg.args ? (Array.isArray(msg.args) ? msg.args : msg.args.split(' ').filter(Boolean)) : (node.args ? node.args.split(' ').filter(Boolean) : []); const timeout = msg.timeout || node.timeout; const fullArgs = [command, ...args]; if (command === 'run' && msg.prompt) { fullArgs.push('--prompt', msg.prompt); } if (cfg.defaultArgs) { fullArgs.unshift(...cfg.defaultArgs.split(' ').filter(Boolean)); } node.status({ fill: 'blue', shape: 'dot', text: `Running: ${command}` }); try { const result = await runAgent(cliCmd, fullArgs, { cwd: workspaceDir, timeout, env: { ...process.env, AGENT_WORKSPACE: workspaceDir, AGENT_API_ENDPOINT: cfg.apiEndpoint, AGENT_API_KEY: cfg.apiKey || '' } }); const duration = Date.now() - startTime; node.status({ fill: 'green', shape: 'dot', text: `Done (${duration}ms)` }); msg.payload = node.outputFormat === 'json' ? parseOutput(result.stdout) : result.stdout; msg.stderr = result.stderr; msg.exitCode = result.exitCode; msg.duration = duration; msg.command = `${cliCmd} ${fullArgs.join(' ')}`; msg.workingDir = workspaceDir; node.send(msg); } catch (err) { const duration = Date.now() - startTime; node.status({ fill: 'red', shape: 'ring', text: `Error (${duration}ms)` }); msg.error = err.message; msg.stderr = err.stderr || ''; msg.exitCode = err.exitCode || -1; msg.duration = duration; node.error(`agent ${command} failed: ${err.message}`, msg); } }); } function runAgent(cmd, args, options) { return new Promise((resolve, reject) => { let stdout = ''; let stderr = ''; let exitCode = 0; const child = spawn(cmd, args, { cwd: options.cwd, env: options.env, timeout: options.timeout, windowsHide: true }); child.stdout.on('data', (data) => { stdout += data.toString(); }); child.stderr.on('data', (data) => { stderr += data.toString(); }); child.on('close', (code) => { exitCode = code || 0; if (exitCode === 0) { resolve({ stdout, stderr, exitCode }); } else { const err = new Error(`Process exited with code ${exitCode}`); err.exitCode = exitCode; err.stderr = stderr; reject(err); } }); child.on('error', (err) => { err.exitCode = -1; reject(err); }); child.on('timeout', () => { child.kill('SIGTERM'); const err = new Error(`Process timed out after ${options.timeout}ms`); err.exitCode = -2; err.stderr = stderr; reject(err); }); }); } function parseOutput(text) { try { const trimmed = text.trim(); if (trimmed.startsWith('{') || trimmed.startsWith('[')) { return JSON.parse(trimmed); } return text; } catch { return text; } } RED.nodes.registerType('agent-exec', AgentExecNode); };