238 lines
7.4 KiB
JavaScript
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`);
|
|
});
|