import express from 'express'; import fs from 'node:fs'; import path from 'node:path'; import { execSync } from 'node:child_process'; import yaml from 'js-yaml'; const app = express(); const PORT = 3233; const AGENTS_DIR = '/agents'; const HERDCTL_CONTAINER = 'herdctl'; app.use(express.json({ limit: '1mb' })); app.use(express.static('public')); // List all agents app.get('/api/agents', (req, res) => { try { const files = fs.readdirSync(AGENTS_DIR).filter(f => f.endsWith('.yaml') || f.endsWith('.yml')); const agents = files.map(file => { const content = fs.readFileSync(path.join(AGENTS_DIR, file), 'utf8'); let parsed = {}; try { parsed = yaml.load(content); } catch(e) {} return { filename: file, name: parsed.name || file.replace(/\.ya?ml$/, ''), description: parsed.description || '', schedules: parsed.schedules ? Object.keys(parsed.schedules) : [] }; }); res.json(agents); } catch (err) { res.status(500).json({ error: err.message }); } }); // Get agent YAML content app.get('/api/agents/:filename', (req, res) => { try { const filename = path.basename(req.params.filename); if (!filename.endsWith('.yaml') && !filename.endsWith('.yml')) return res.status(400).json({ error: 'Invalid file' }); const filepath = path.join(AGENTS_DIR, filename); if (!fs.existsSync(filepath)) return res.status(404).json({ error: 'Not found' }); res.json({ filename, content: fs.readFileSync(filepath, 'utf8') }); } catch (err) { res.status(500).json({ error: err.message }); } }); // Save agent YAML and restart herdctl app.put('/api/agents/:filename', (req, res) => { try { const filename = path.basename(req.params.filename); if (!filename.endsWith('.yaml') && !filename.endsWith('.yml')) return res.status(400).json({ error: 'Invalid file' }); const { content } = req.body; if (!content) return res.status(400).json({ error: 'No content' }); try { yaml.load(content); } catch(e) { return res.status(400).json({ error: `Invalid YAML: ${e.message}` }); } const filepath = path.join(AGENTS_DIR, filename); if (fs.existsSync(filepath)) fs.copyFileSync(filepath, filepath + '.bak'); fs.writeFileSync(filepath, content, 'utf8'); try { execSync(`docker restart ${HERDCTL_CONTAINER}`, { timeout: 15000 }); res.json({ success: true, message: 'Saved and herdctl restarted.' }); } catch (e) { res.json({ success: true, message: 'Saved. Restart failed: ' + e.message }); } } catch (err) { res.status(500).json({ error: err.message }); } }); // Create new agent app.post('/api/agents', (req, res) => { try { const { filename, content } = req.body; if (!filename || !content) return res.status(400).json({ error: 'filename and content required' }); const safe = path.basename(filename); if (!safe.endsWith('.yaml') && !safe.endsWith('.yml')) return res.status(400).json({ error: 'Filename must end in .yaml or .yml' }); try { yaml.load(content); } catch(e) { return res.status(400).json({ error: `Invalid YAML: ${e.message}` }); } const filepath = path.join(AGENTS_DIR, safe); if (fs.existsSync(filepath)) return res.status(409).json({ error: 'File already exists' }); fs.writeFileSync(filepath, content, 'utf8'); res.json({ success: true, filename: safe }); } catch (err) { res.status(500).json({ error: err.message }); } }); // Trigger agent immediately via herdctl API app.post('/api/agents/:filename/trigger', (req, res) => { try { const filename = path.basename(req.params.filename); const content = fs.readFileSync(path.join(AGENTS_DIR, filename), 'utf8'); const parsed = yaml.load(content); const agentName = parsed.name || filename.replace(/\.ya?ml$/, ''); execSync(`curl -s -X POST http://herdctl:3232/api/agents/${agentName}/trigger`, { timeout: 10000 }); res.json({ success: true, message: `Triggered ${agentName}` }); } catch (err) { res.status(500).json({ error: err.message }); } }); // Proxy herdctl agent status app.get('/api/status', (req, res) => { try { const result = execSync('curl -s http://herdctl:3232/api/agents', { timeout: 5000 }).toString(); res.json(JSON.parse(result)); } catch (err) { res.status(500).json({ error: err.message }); } }); app.listen(PORT, '0.0.0.0', () => { console.log(`herdctl-editor running on port ${PORT}`); });