UNPKG

@stackmemoryai/stackmemory

Version:

Lossless, project-scoped memory for AI coding tools. Durable context across sessions with 56 MCP tools, FTS5 search, conductor orchestrator, loop/watch monitoring, snapshot capture, pre-flight overlap checks, Claude/Codex/OpenCode wrappers, Linear sync, a

257 lines (256 loc) 9.13 kB
#!/usr/bin/env node import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import chalk from "chalk"; import Table from "cli-table3"; import { SessionManager } from "../../core/session/session-manager.js"; import Database from "better-sqlite3"; import { join } from "path"; import { existsSync, readFileSync } from "fs"; import { getModelTokenLimit } from "../../core/models/model-router.js"; const dashboardCommand = { command: "dashboard", describe: "Display monitoring dashboard in terminal", builder: (yargs) => { return yargs.option("watch", { alias: "w", type: "boolean", description: "Auto-refresh dashboard", default: false }).option("interval", { alias: "i", type: "number", description: "Refresh interval in seconds", default: 5 }); }, handler: async (argv) => { const projectRoot = process.cwd(); const dbPath = join(projectRoot, ".stackmemory", "context.db"); if (!existsSync(dbPath)) { console.log( '\u274C StackMemory not initialized. Run "stackmemory init" first.' ); return; } const displayDashboard = async () => { console.clear(); console.log( chalk.cyan.bold( "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550" ) ); console.log( chalk.cyan.bold( " \u{1F680} StackMemory Monitoring Dashboard " ) ); console.log( chalk.cyan.bold( "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550" ) ); console.log(); const sessionManager = new SessionManager({ enableMonitoring: false }); await sessionManager.initialize(); const db = new Database(dbPath); const sessions = await sessionManager.listSessions({ state: "active", limit: 5 }); const sessionsTable = new Table({ head: [ chalk.white("Session ID"), chalk.white("Status"), chalk.white("Branch"), chalk.white("Duration"), chalk.white("Last Active") ], style: { head: [], border: [] } }); sessions.forEach((session) => { const duration = Math.round( (Date.now() - session.startedAt) / 1e3 / 60 ); const lastActive = Math.round( (Date.now() - session.lastActiveAt) / 1e3 / 60 ); const status = session.state === "active" ? chalk.green("\u25CF Active") : session.state === "completed" ? chalk.gray("\u25CF Completed") : chalk.yellow("\u25CF Idle"); sessionsTable.push([ session.sessionId.substring(0, 8), status, session.branch || "main", `${duration}m`, `${lastActive}m ago` ]); }); console.log(chalk.yellow.bold("\u{1F4CA} Active Sessions")); console.log(sessionsTable.toString()); console.log(); const frameStats = db.prepare( ` SELECT COUNT(*) as total, SUM(CASE WHEN state = 'active' THEN 1 ELSE 0 END) as active, COUNT(DISTINCT run_id) as sessions FROM frames ` ).get(); const statsTable = new Table({ head: [chalk.white("Metric"), chalk.white("Value")], style: { head: [], border: [] } }); statsTable.push( ["Total Frames", frameStats?.total || 0], ["Active Frames", chalk.green(frameStats?.active || 0)], ["Total Sessions", frameStats?.sessions || 0] ); console.log(chalk.yellow.bold("\u{1F4C8} Frame Statistics")); console.log(statsTable.toString()); console.log(); const recentActivity = db.prepare( ` SELECT name, type, state, datetime(created_at, 'unixepoch') as created FROM frames ORDER BY created_at DESC LIMIT 5 ` ).all(); if (recentActivity.length > 0) { const activityTable = new Table({ head: [ chalk.white("Frame"), chalk.white("Type"), chalk.white("Status"), chalk.white("Created") ], style: { head: [], border: [] } }); recentActivity.forEach((frame) => { const status = frame.state === "active" ? chalk.green("Active") : chalk.gray("Closed"); activityTable.push([ frame.name.substring(0, 30), frame.type, status, frame.created ]); }); console.log(chalk.yellow.bold("\u{1F550} Recent Activity")); console.log(activityTable.toString()); console.log(); } const contextUsage = await estimateContextUsage(db); const usageBar = createProgressBar(contextUsage, 100); console.log(chalk.yellow.bold("\u{1F4BE} Context Usage")); console.log(`${usageBar} ${contextUsage}%`); console.log(); const conductorStatusPath = join( projectRoot, ".stackmemory", "conductor-status.json" ); if (existsSync(conductorStatusPath)) { try { const raw = readFileSync(conductorStatusPath, "utf-8"); const conductorStatus = JSON.parse(raw); const stale = Date.now() - conductorStatus.updatedAt > 12e4; const header = stale ? chalk.gray.bold("\u2699 Conductor (stale)") : chalk.yellow.bold("\u2699 Conductor"); console.log(header); if (conductorStatus.running.length > 0) { const conductorTable = new Table({ head: [ chalk.white("Issue"), chalk.white("Status"), chalk.white("Title"), chalk.white("Runtime") ], style: { head: [], border: [] }, colWidths: [12, 14, 36, 10] }); conductorStatus.running.forEach((r) => { const mins = Math.round(r.runtime / 6e4); const statusColor = r.status === "running" ? chalk.green : r.status === "completed" ? chalk.cyan : chalk.red; conductorTable.push([ r.identifier, statusColor(r.status), r.title.slice(0, 34), `${mins}m` ]); }); console.log(conductorTable.toString()); } else { console.log(chalk.gray(" No agents running")); } console.log( chalk.gray( ` Completed: ${conductorStatus.completed} Failed: ${conductorStatus.failed} Max: ${conductorStatus.maxConcurrent}` ) ); console.log(); } catch { } } db.close(); if (argv.watch) { console.log( chalk.gray( `Auto-refreshing every ${argv.interval} seconds. Press Ctrl+C to exit.` ) ); } else { console.log(chalk.gray("Run with --watch to auto-refresh")); } }; try { await displayDashboard(); if (argv.watch) { const interval = setInterval(async () => { await displayDashboard(); }, argv.interval * 1e3); process.on("SIGINT", () => { clearInterval(interval); console.clear(); console.log(chalk.green("\u2705 Dashboard closed")); process.exit(0); }); } } catch (error) { console.error(chalk.red("\u274C Dashboard error:"), error.message); process.exit(1); } } }; function createProgressBar(value, max) { const percentage = Math.min(100, Math.round(value / max * 100)); const filled = Math.round(percentage / 5); const empty = 20 - filled; let color = chalk.green; if (percentage > 80) color = chalk.red; else if (percentage > 60) color = chalk.yellow; return color("\u2588".repeat(filled)) + chalk.gray("\u2591".repeat(empty)); } async function estimateContextUsage(db) { const result = db.prepare( ` SELECT COUNT(*) as frame_count, SUM(LENGTH(inputs)) as input_size, SUM(LENGTH(outputs)) as output_size FROM frames WHERE state = 'active' ` ).get(); const totalBytes = (result?.input_size || 0) + (result?.output_size || 0); const estimatedTokens = totalBytes / 4; const maxTokens = getModelTokenLimit(process.env.ANTHROPIC_MODEL); return Math.round(estimatedTokens / maxTokens * 100); } export { dashboardCommand };