238 lines
7.4 KiB
JavaScript

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`);
});