@stackmemoryai/stackmemory
Version:
Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.
509 lines (444 loc) • 12.7 kB
JavaScript
/**
* Real-time Swarm Monitoring Utility
* Provides live monitoring and metrics for running swarms
*/
import * as fs from 'fs/promises';
import * as path from 'path';
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import blessed from 'blessed';
class SwarmMonitor {
constructor() {
this.swarmDir = '.swarm';
this.statusDir = path.join(this.swarmDir, 'status');
this.logsDir = path.join(this.swarmDir, 'logs');
this.metricsHistory = [];
this.maxHistorySize = 100;
this.updateInterval = 2000; // 2 seconds
this.wsPort = 3456;
}
async startMonitoring() {
console.log('🔍 Starting Swarm Monitor...');
// Check if we should use terminal UI or web interface
const mode = process.argv[2] || 'terminal';
if (mode === 'web') {
await this.startWebMonitor();
} else {
await this.startTerminalMonitor();
}
}
async startTerminalMonitor() {
// Create blessed screen
const screen = blessed.screen({
smartCSR: true,
title: 'Ralph Swarm Monitor'
});
// Create layout boxes
const header = blessed.box({
parent: screen,
top: 0,
left: 0,
width: '100%',
height: 3,
content: ' RALPH SWARM MONITOR ',
align: 'center',
style: {
fg: 'white',
bg: 'blue',
bold: true
}
});
const swarmList = blessed.list({
parent: screen,
top: 3,
left: 0,
width: '50%',
height: '40%',
label: ' Active Swarms ',
border: {
type: 'line'
},
style: {
fg: 'white',
border: {
fg: 'cyan'
},
selected: {
bg: 'blue'
}
},
mouse: true,
keys: true,
vi: true
});
const metricsBox = blessed.box({
parent: screen,
top: 3,
left: '50%',
width: '50%',
height: '40%',
label: ' Performance Metrics ',
border: {
type: 'line'
},
style: {
fg: 'white',
border: {
fg: 'yellow'
}
}
});
const logBox = blessed.log({
parent: screen,
bottom: 3,
left: 0,
width: '100%',
height: '55%',
label: ' Live Logs ',
border: {
type: 'line'
},
style: {
fg: 'white',
border: {
fg: 'green'
}
},
scrollable: true,
alwaysScroll: true,
mouse: true
});
const statusBar = blessed.box({
parent: screen,
bottom: 0,
left: 0,
width: '100%',
height: 3,
style: {
fg: 'white',
bg: 'black'
}
});
// Handle exit
screen.key(['q', 'C-c'], () => {
return process.exit(0);
});
// Update function
const updateDisplay = async () => {
try {
// Get swarm status
const swarms = await this.getActiveSwarms();
// Update swarm list
const swarmItems = swarms.map(s => {
const status = s.status === 'running' ? '🟢' : '🔴';
return `${status} ${s.id} - ${s.project}`;
});
swarmList.setItems(swarmItems);
// Update metrics
const metrics = await this.collectMetrics(swarms);
const metricsContent = this.formatMetrics(metrics);
metricsBox.setContent(metricsContent);
// Update status bar
const now = new Date().toLocaleTimeString();
statusBar.setContent(
` Active: ${swarms.filter(s => s.status === 'running').length} | ` +
`Total: ${swarms.length} | ` +
`Updated: ${now} | ` +
`Press 'q' to quit`
);
screen.render();
} catch (error) {
logBox.log(`Error: ${error.message}`);
}
};
// Handle swarm selection
swarmList.on('select', async (item, index) => {
const swarms = await this.getActiveSwarms();
if (swarms[index]) {
await this.tailSwarmLogs(swarms[index], logBox);
}
});
// Start update loop
await updateDisplay();
setInterval(updateDisplay, this.updateInterval);
screen.render();
}
async startWebMonitor() {
// Create HTTP server for web interface
const server = createServer((req, res) => {
if (req.url === '/') {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(this.getWebInterface());
} else if (req.url === '/api/swarms') {
this.handleApiRequest(res);
} else {
res.writeHead(404);
res.end('Not Found');
}
});
// Create WebSocket server for real-time updates
const wss = new WebSocketServer({ server });
wss.on('connection', (ws) => {
console.log('Client connected to monitor');
// Send initial data
this.sendSwarmUpdate(ws);
// Set up periodic updates
const updateInterval = setInterval(() => {
this.sendSwarmUpdate(ws);
}, this.updateInterval);
ws.on('close', () => {
clearInterval(updateInterval);
console.log('Client disconnected from monitor');
});
});
server.listen(this.wsPort, () => {
console.log(`📡 Web monitor running at http://localhost:${this.wsPort}`);
console.log('Open in browser to view real-time swarm status');
});
}
async getActiveSwarms() {
const swarms = [];
try {
const files = await fs.readdir(this.statusDir);
for (const file of files) {
if (file.endsWith('.json')) {
const filePath = path.join(this.statusDir, file);
const content = await fs.readFile(filePath, 'utf-8');
const swarm = JSON.parse(content);
// Check if process is still running
if (swarm.pid) {
try {
process.kill(swarm.pid, 0);
swarm.status = 'running';
} catch {
swarm.status = 'stopped';
}
}
swarms.push(swarm);
}
}
} catch (error) {
// Directory might not exist
}
return swarms;
}
async collectMetrics(swarms) {
const metrics = {
timestamp: Date.now(),
activeSwarms: swarms.filter(s => s.status === 'running').length,
totalSwarms: swarms.length,
agentCount: 0,
taskCompletion: 0,
memoryUsage: process.memoryUsage(),
cpuUsage: process.cpuUsage()
};
// Count total agents
for (const swarm of swarms) {
if (swarm.agents) {
metrics.agentCount += swarm.agents.split(',').length;
}
}
// Calculate average task completion (mock for now)
metrics.taskCompletion = Math.round(Math.random() * 100);
// Store in history
this.metricsHistory.push(metrics);
if (this.metricsHistory.length > this.maxHistorySize) {
this.metricsHistory.shift();
}
return metrics;
}
formatMetrics(metrics) {
const memoryMB = (metrics.memoryUsage.heapUsed / 1024 / 1024).toFixed(2);
return `
Active Swarms: ${metrics.activeSwarms}/${metrics.totalSwarms}
Total Agents: ${metrics.agentCount}
Task Completion: ${metrics.taskCompletion}%
Memory Usage: ${memoryMB} MB
CPU Time: ${metrics.cpuUsage.user / 1000}ms
Performance Trend:
${this.getPerformanceTrend()}
`;
}
getPerformanceTrend() {
if (this.metricsHistory.length < 2) return 'Collecting data...';
const recent = this.metricsHistory.slice(-10);
let trend = '';
for (let i = 0; i < recent.length; i++) {
const value = recent[i].taskCompletion;
if (value > 80) trend += '█';
else if (value > 60) trend += '▓';
else if (value > 40) trend += '▒';
else if (value > 20) trend += '░';
else trend += ' ';
}
return trend;
}
async tailSwarmLogs(swarm, logBox) {
const logFile = swarm.logFile || path.join(this.logsDir, `${swarm.id}.log`);
try {
const content = await fs.readFile(logFile, 'utf-8');
const lines = content.split('\n');
const recentLines = lines.slice(-20);
logBox.log(`\n=== Logs for ${swarm.id} ===`);
recentLines.forEach(line => {
if (line.trim()) {
logBox.log(line);
}
});
} catch (error) {
logBox.log(`Could not read logs for ${swarm.id}: ${error.message}`);
}
}
async sendSwarmUpdate(ws) {
try {
const swarms = await this.getActiveSwarms();
const metrics = await this.collectMetrics(swarms);
ws.send(JSON.stringify({
type: 'update',
swarms,
metrics,
history: this.metricsHistory.slice(-20)
}));
} catch (error) {
console.error('Failed to send update:', error);
}
}
async handleApiRequest(res) {
try {
const swarms = await this.getActiveSwarms();
const metrics = await this.collectMetrics(swarms);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ swarms, metrics }));
} catch (error) {
res.writeHead(500);
res.end(JSON.stringify({ error: error.message }));
}
}
getWebInterface() {
return `
<!DOCTYPE html>
<html>
<head>
<title>Ralph Swarm Monitor</title>
<style>
body {
font-family: 'Monaco', 'Menlo', monospace;
background: #1e1e1e;
color: #d4d4d4;
margin: 0;
padding: 20px;
}
h1 {
color: #569cd6;
text-align: center;
}
.container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-top: 20px;
}
.panel {
background: #2d2d30;
border: 1px solid #3e3e42;
border-radius: 5px;
padding: 15px;
}
.panel h2 {
color: #4ec9b0;
margin-top: 0;
}
.swarm-item {
padding: 8px;
margin: 5px 0;
background: #1e1e1e;
border-radius: 3px;
}
.status-running {
border-left: 3px solid #4ec9b0;
}
.status-stopped {
border-left: 3px solid #f44747;
}
.metrics {
font-size: 14px;
}
.metric-label {
color: #9cdcfe;
}
.metric-value {
color: #d4d4d4;
font-weight: bold;
}
#logs {
background: #1e1e1e;
padding: 10px;
height: 200px;
overflow-y: auto;
font-size: 12px;
white-space: pre-wrap;
}
</style>
</head>
<body>
<h1>🚀 Ralph Swarm Monitor</h1>
<div class="container">
<div class="panel">
<h2>Active Swarms</h2>
<div id="swarms"></div>
</div>
<div class="panel">
<h2>Performance Metrics</h2>
<div id="metrics" class="metrics"></div>
</div>
</div>
<div class="panel" style="margin-top: 20px;">
<h2>Live Logs</h2>
<div id="logs"></div>
</div>
<script>
const ws = new WebSocket('ws://localhost:${this.wsPort}');
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
updateDisplay(data);
};
function updateDisplay(data) {
// Update swarms
const swarmsDiv = document.getElementById('swarms');
swarmsDiv.innerHTML = data.swarms.map(s => \`
<div class="swarm-item status-\${s.status}">
<strong>\${s.id}</strong><br>
Project: \${s.project}<br>
Agents: \${s.agents}<br>
Status: \${s.status}
</div>
\`).join('');
// Update metrics
const metricsDiv = document.getElementById('metrics');
metricsDiv.innerHTML = \`
<p><span class="metric-label">Active Swarms:</span> <span class="metric-value">\${data.metrics.activeSwarms}/\${data.metrics.totalSwarms}</span></p>
<p><span class="metric-label">Total Agents:</span> <span class="metric-value">\${data.metrics.agentCount}</span></p>
<p><span class="metric-label">Task Completion:</span> <span class="metric-value">\${data.metrics.taskCompletion}%</span></p>
<p><span class="metric-label">Memory Usage:</span> <span class="metric-value">\${(data.metrics.memoryUsage.heapUsed / 1024 / 1024).toFixed(2)} MB</span></p>
\`;
// Update logs (mock for now)
const logsDiv = document.getElementById('logs');
if (Math.random() > 0.7) {
const logEntry = new Date().toLocaleTimeString() + ' - Swarm activity detected\\n';
logsDiv.textContent += logEntry;
logsDiv.scrollTop = logsDiv.scrollHeight;
}
}
</script>
</body>
</html>
`;
}
}
// Run monitor if executed directly
if (import.meta.url === `file://${process.argv[1]}`) {
const monitor = new SwarmMonitor();
monitor.startMonitoring().catch(console.error);
}
export { SwarmMonitor };