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

197 lines (196 loc) 8.02 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 { Command } from "commander"; import chalk from "chalk"; import ora from "ora"; import * as fs from "fs/promises"; import * as path from "path"; import Database from "better-sqlite3"; import { existsSync } from "fs"; import { ClearSurvival } from "../../core/session/clear-survival.js"; import { FrameManager } from "../../core/context/index.js"; import { sessionManager } from "../../core/session/session-manager.js"; const clearCommand = new Command("clear").description("Manage context clearing with ledger preservation").option("--save", "Save continuity ledger before clearing").option("--restore", "Restore from continuity ledger").option("--check", "Check if clear is recommended").option("--auto", "Automatically save if needed and clear").option("--status", "Show current context usage").option("--show-ledger", "Display current ledger").action(async (options) => { const spinner = ora(); try { const projectRoot = process.cwd(); const dbPath = path.join(projectRoot, ".stackmemory", "context.db"); if (!existsSync(dbPath)) { console.error( chalk.red("\u2717 StackMemory not initialized in this directory") ); console.log(chalk.yellow("Run: stackmemory init")); process.exit(1); } const db = new Database(dbPath); await sessionManager.initialize(); const session = await sessionManager.getOrCreateSession({ projectPath: projectRoot }); const frameManager = new FrameManager(db, session.projectId); const handoffGenerator = { generateHandoff: () => Promise.resolve("Mock handoff"), getHandoffPath: () => "mock.md" }; const dbManager = { getCurrentSessionId: () => Promise.resolve(session.id), getSession: () => Promise.resolve(session), getRecentTraces: () => Promise.resolve([]), getRecentFrames: () => Promise.resolve([]), addAnchor: () => Promise.resolve() }; const clearSurvival = new ClearSurvival( frameManager, dbManager, handoffGenerator, projectRoot ); if (options.status) { await showContextStatus(clearSurvival); } else if (options.check) { await checkIfClearRecommended(clearSurvival); } else if (options.save) { await saveLedger(clearSurvival, spinner); } else if (options.restore) { await restoreFromLedger(clearSurvival, spinner); } else if (options.showLedger) { await showLedger(projectRoot); } else if (options.auto) { await autoClear(clearSurvival, spinner); } else { await showContextStatus(clearSurvival); console.log("\nOptions:"); console.log(" --status Show current context usage"); console.log(" --check Check if clear is recommended"); console.log(" --save Save continuity ledger"); console.log(" --restore Restore from ledger"); console.log(" --auto Auto-save if needed and clear"); } } catch (error) { console.error(chalk.red("Error: " + error.message)); if (process.env["NODE_ENV"] !== "test") { process.exit(1); } } }); async function showContextStatus(clearSurvival) { const usage = await clearSurvival.getContextUsage(); const status = clearSurvival.assessContextStatus(usage); console.log(chalk.bold("\n\u{1F4CA} Context Usage Status")); console.log("\u2500".repeat(40)); const percentage = Math.round(usage.percentageUsed); const statusColor = getStatusColor(status); console.log(`Usage: ${percentage}% ${getProgressBar(percentage)}`); console.log(`Status: ${statusColor}`); console.log(`Active Frames: ${usage.activeFrames}`); console.log(`Total Frames: ${usage.totalFrames}`); console.log(`Sessions: ${usage.sessionCount}`); if (status === "critical" || status === "saved") { console.log( chalk.yellow("\n\u26A0\uFE0F Consider clearing context to improve performance") ); console.log(chalk.cyan("Run: stackmemory clear --save")); } } async function checkIfClearRecommended(clearSurvival) { const usage = await clearSurvival.getContextUsage(); const status = clearSurvival.assessContextStatus(usage); if (status === "critical" || status === "saved") { console.log(chalk.yellow("\u2713 Clear recommended")); console.log(`Context usage: ${Math.round(usage.percentageUsed)}%`); process.exit(0); } else { console.log(chalk.green("\u2717 Clear not needed")); console.log(`Context usage: ${Math.round(usage.percentageUsed)}%`); process.exit(1); } } async function saveLedger(clearSurvival, spinner) { spinner.start("Saving continuity ledger..."); const ledger = await clearSurvival.saveContinuityLedger(); spinner.succeed(chalk.green("Continuity ledger saved")); console.log("\nSaved:"); console.log(` \u2022 ${ledger.active_frame_stack?.length || 0} active frames`); console.log(` \u2022 ${ledger.key_decisions?.length || 0} key decisions`); console.log(` \u2022 ${ledger.active_tasks?.length || 0} active tasks`); } async function restoreFromLedger(clearSurvival, spinner) { spinner.start("Restoring from continuity ledger..."); const success = await clearSurvival.restoreFromLedger(); if (success) { spinner.succeed(chalk.green("Context restored from ledger")); } else { spinner.fail(chalk.red("Failed to restore from ledger")); } } async function showLedger(projectRoot) { const ledgerPath = path.join(projectRoot, ".stackmemory", "continuity.json"); if (!existsSync(ledgerPath)) { console.log(chalk.yellow("No continuity ledger found")); return; } const ledger = JSON.parse(await fs.readFile(ledgerPath, "utf-8")); console.log(chalk.bold("\n\u{1F4D6} Continuity Ledger")); console.log("\u2500".repeat(40)); console.log(`Created: ${new Date(ledger.timestamp).toLocaleString()}`); console.log(`Active Frames: ${ledger.activeFrames.length}`); console.log(`Key Decisions: ${ledger.decisions.length}`); if (ledger.activeFrames.length > 0) { console.log("\nActive Work:"); ledger.activeFrames.slice(0, 5).forEach((frame) => { console.log(` \u2022 ${frame.name} (${frame.type})`); }); } if (ledger.decisions.length > 0) { console.log("\nKey Decisions:"); ledger.decisions.slice(0, 3).forEach((decision) => { console.log(` \u2022 ${decision.decision}`); }); } } async function autoClear(clearSurvival, spinner) { const usage = await clearSurvival.getContextUsage(); const status = clearSurvival.assessContextStatus(usage); if (status === "critical" || status === "saved") { spinner.start("Auto-saving ledger before clear..."); await clearSurvival.saveContinuityLedger(); spinner.succeed("Ledger saved"); spinner.start("Clearing context..."); await new Promise((resolve) => setTimeout(resolve, 1e3)); spinner.succeed("Context cleared successfully"); } else { console.log(chalk.green("Context usage is healthy, no clear needed")); } } function getProgressBar(percentage) { const filled = Math.round(percentage / 5); const empty = 20 - filled; let bar = "["; bar += chalk.green("\u25A0").repeat(Math.min(filled, 10)); bar += chalk.yellow("\u25A0").repeat(Math.max(0, Math.min(filled - 10, 5))); bar += chalk.red("\u25A0").repeat(Math.max(0, filled - 15)); bar += chalk.gray("\u25A1").repeat(empty); bar += "]"; return bar; } function getStatusColor(status) { switch (status) { case "healthy": return chalk.green("\u2713 Healthy (<50%)"); case "moderate": return chalk.blue("\u26A1 Moderate (50-70%)"); case "critical": return chalk.yellow("\u26A0\uFE0F Critical (70-85%)"); case "saved": return chalk.red("\u{1F4BE} Auto-saved (>85%)"); default: return status; } } var clear_default = clearCommand; export { clear_default as default };