@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
JavaScript
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
};