const express = require('express'); const fs = require('fs'); const path = require('path'); const readline = require('readline'); const app = express(); const PORT = 8890; const SESSIONS_DIR = '/Users/jakeshore/.clawdbot/agents/main/sessions'; const WORKSPACE = '/Users/jakeshore/.clawdbot/workspace'; const MEMORY_DIR = path.join(WORKSPACE, 'memory'); // Serve static files app.use(express.static(path.join(__dirname, 'public'))); // Parse JSONL file (last N lines for efficiency) async function parseSessionFile(filePath, maxLines = 50) { const messages = []; try { const content = fs.readFileSync(filePath, 'utf8'); const lines = content.trim().split('\n').slice(-maxLines); for (const line of lines) { try { const parsed = JSON.parse(line); messages.push(parsed); } catch (e) { /* skip malformed */ } } } catch (e) { /* file not readable */ } return messages; } // Get all sessions with metadata function getSessionFiles() { try { const files = fs.readdirSync(SESSIONS_DIR) .filter(f => f.endsWith('.jsonl')) .map(f => { const fullPath = path.join(SESSIONS_DIR, f); const stat = fs.statSync(fullPath); return { id: f.replace('.jsonl', ''), file: f, path: fullPath, size: stat.size, modified: stat.mtime, created: stat.ctime, }; }) .sort((a, b) => b.modified - a.modified); return files; } catch (e) { return []; } } // Extract activity from messages function extractActivity(messages, sessionId) { const activities = []; for (const msg of messages) { const ts = msg.timestamp || msg.ts || null; if (msg.role === 'user') { const content = typeof msg.content === 'string' ? msg.content : Array.isArray(msg.content) ? msg.content.map(c => c.text || '').join(' ') : ''; if (content && !content.startsWith('HEARTBEAT')) { activities.push({ type: 'user_message', session: sessionId, content: content.substring(0, 200), timestamp: ts, }); } } if (msg.role === 'assistant') { const content = typeof msg.content === 'string' ? msg.content : Array.isArray(msg.content) ? msg.content.map(c => c.text || '').join(' ') : ''; // Check for tool use if (Array.isArray(msg.content)) { for (const block of msg.content) { if (block.type === 'tool_use') { activities.push({ type: 'tool_call', session: sessionId, tool: block.name, content: block.name + (block.input?.command ? ': ' + block.input.command.substring(0, 80) : block.input?.action ? ': ' + block.input.action : block.input?.query ? ': ' + block.input.query.substring(0, 80) : ''), timestamp: ts, }); } } } if (content && content !== 'NO_REPLY' && content !== 'HEARTBEAT_OK') { activities.push({ type: 'buba_reply', session: sessionId, content: content.substring(0, 200), timestamp: ts, }); } } } return activities; } // API: Dashboard state app.get('/api/state', async (req, res) => { try { const sessions = getSessionFiles(); const now = Date.now(); const oneHourAgo = now - (60 * 60 * 1000); const oneDayAgo = now - (24 * 60 * 60 * 1000); // Categorize sessions const activeSessions = sessions.filter(s => s.modified.getTime() > oneHourAgo); const recentSessions = sessions.filter(s => s.modified.getTime() > oneDayAgo); const subagentSessions = sessions.filter(s => s.id.includes('subagent')); const activeSubagents = subagentSessions.filter(s => s.modified.getTime() > oneHourAgo); // Parse main session for recent activity const mainSession = sessions.find(s => !s.id.includes('subagent')); let recentActivity = []; // Get activity from recent sessions const sessionsToScan = sessions.slice(0, 10); for (const session of sessionsToScan) { const messages = await parseSessionFile(session.path, 20); const activity = extractActivity(messages, session.id); recentActivity.push(...activity); } // Sort by recency and limit recentActivity.sort((a, b) => { const ta = a.timestamp ? new Date(a.timestamp).getTime() : 0; const tb = b.timestamp ? new Date(b.timestamp).getTime() : 0; return tb - ta; }); recentActivity = recentActivity.slice(0, 50); // Read working state let workingState = ''; try { workingState = fs.readFileSync(path.join(MEMORY_DIR, 'working-state.md'), 'utf8'); } catch (e) { } // Read today's log let todayLog = ''; const today = new Date().toISOString().split('T')[0]; try { todayLog = fs.readFileSync(path.join(MEMORY_DIR, `${today}.md`), 'utf8'); } catch (e) { } // Sub-agent details const subagentDetails = []; for (const sa of subagentSessions.slice(0, 20)) { const messages = await parseSessionFile(sa.path, 5); const firstMsg = messages.find(m => m.role === 'user'); const lastMsg = [...messages].reverse().find(m => m.role === 'assistant'); const isActive = sa.modified.getTime() > oneHourAgo; let task = ''; if (firstMsg) { const content = typeof firstMsg.content === 'string' ? firstMsg.content : Array.isArray(firstMsg.content) ? firstMsg.content.map(c => c.text || '').join(' ') : ''; task = content.substring(0, 150); } subagentDetails.push({ id: sa.id.split(':').pop().substring(0, 8), fullId: sa.id, task: task, active: isActive, lastModified: sa.modified, size: sa.size, }); } // Stats const totalSessions = sessions.length; const totalSubagents = subagentSessions.length; const todaysSessions = sessions.filter(s => { const d = s.created.toISOString().split('T')[0]; return d === today; }).length; // Tools used today const toolCounts = {}; for (const act of recentActivity) { if (act.type === 'tool_call') { const tool = act.tool || 'unknown'; toolCounts[tool] = (toolCounts[tool] || 0) + 1; } } res.json({ timestamp: new Date().toISOString(), buba: { status: activeSessions.length > 0 ? 'active' : 'idle', currentTask: workingState.split('\n').slice(0, 5).join('\n'), uptime: '24/7', }, stats: { totalSessions, totalSubagents, activeSessions: activeSessions.length, activeSubagents: activeSubagents.length, todaysSessions, toolsUsedRecently: toolCounts, }, subagents: subagentDetails, activity: recentActivity, workingState: workingState.substring(0, 2000), todayLog: todayLog.substring(0, 2000), }); } catch (e) { res.status(500).json({ error: e.message }); } }); // API: Session detail app.get('/api/session/:id', async (req, res) => { try { const sessionFile = path.join(SESSIONS_DIR, req.params.id + '.jsonl'); const messages = await parseSessionFile(sessionFile, 100); res.json({ messages }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.listen(PORT, '0.0.0.0', () => { console.log(`Buba Dashboard running at http://localhost:${PORT}`); console.log(`Network: http://192.168.0.25:8890`); });