UNPKG

@stackmemoryai/stackmemory

Version:

Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.

344 lines (343 loc) 12.5 kB
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 Database from "better-sqlite3"; import { join } from "path"; import { existsSync } from "fs"; import { FrameManager } from "../../core/context/index.js"; import { createContextRehydrateCommand } from "./context-rehydrate.js"; function createContextCommands() { const context = new Command("context").alias("ctx").description("Manage context stack"); context.command("show").alias("status").description("Show current context stack").option("-v, --verbose", "Show detailed information").action(async (options) => { 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 db = new Database(dbPath); try { let projectId = "default"; try { const projectRow = db.prepare( ` SELECT value FROM metadata WHERE key = 'project_id' ` ).get(); if (projectRow?.value) projectId = projectRow.value; } catch { } const frameManager = new FrameManager(db, projectId, { skipContextBridge: true }); const depth = frameManager.getStackDepth(); const activePath = frameManager.getActiveFramePath(); console.log(` \u{1F4DA} Context Stack `); console.log(`Project: ${projectId}`); console.log(`Depth: ${depth}`); console.log(`Active frames: ${activePath.length} `); if (activePath.length === 0) { console.log("No active context frames.\n"); console.log('Use "stackmemory context push" to create one.'); } else { const typeIcon = { session: "\u{1F537}", task: "\u{1F4CB}", command: "\u26A1", file: "\u{1F4C4}", decision: "\u{1F4A1}" }; console.log("Stack (bottom to top):"); activePath.forEach((frame, i) => { const icon = typeIcon[frame.type] || "\u{1F4E6}"; const indent = " ".repeat(i); console.log( `${indent}${i === activePath.length - 1 ? "\u2514\u2500" : "\u251C\u2500"} ${icon} ${frame.name || frame.frame_id.slice(0, 10)}` ); if (options.verbose) { console.log(`${indent} ID: ${frame.frame_id}`); console.log(`${indent} Type: ${frame.type}`); console.log( `${indent} Created: ${new Date(frame.created_at * 1e3).toLocaleString()}` ); } }); } console.log(""); } catch (error) { console.error("\u274C Failed to show context:", error.message); } finally { db.close(); } }); context.command("push <name>").description("Push a new context frame onto the stack").option( "-t, --type <type>", "Frame type (session, task, command, file, decision)", "task" ).option("-m, --metadata <json>", "Additional metadata as JSON").action(async (name, options) => { 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 db = new Database(dbPath); try { let projectId = "default"; try { const projectRow = db.prepare(`SELECT value FROM metadata WHERE key = 'project_id'`).get(); if (projectRow?.value) projectId = projectRow.value; } catch { } const frameManager = new FrameManager(db, projectId, { skipContextBridge: true }); const activePath = frameManager.getActiveFramePath(); const parentId = activePath.length > 0 ? activePath[activePath.length - 1].frame_id : void 0; let inputs = {}; if (options.metadata) { try { inputs = JSON.parse(options.metadata); } catch { console.log("\u26A0\uFE0F Invalid metadata JSON, ignoring"); } } const frameId = frameManager.createFrame({ type: options.type, name, inputs, parentFrameId: parentId }); console.log(`\u2705 Pushed context frame: ${name}`); console.log(` ID: ${frameId.slice(0, 10)}`); console.log(` Type: ${options.type}`); console.log(` Depth: ${frameManager.getStackDepth()}`); } catch (error) { console.error("\u274C Failed to push context:", error.message); } finally { db.close(); } }); context.command("pop").description("Pop the top context frame from the stack").option("-a, --all", "Pop all frames (clear stack)").action(async (options) => { 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 db = new Database(dbPath); try { let projectId = "default"; try { const projectRow = db.prepare(`SELECT value FROM metadata WHERE key = 'project_id'`).get(); if (projectRow?.value) projectId = projectRow.value; } catch { } const frameManager = new FrameManager(db, projectId, { skipContextBridge: true }); const activePath = frameManager.getActiveFramePath(); if (activePath.length === 0) { console.log("\u{1F4DA} Stack is already empty."); return; } if (options.all) { for (let i = activePath.length - 1; i >= 0; i--) { frameManager.closeFrame(activePath[i].frame_id); } console.log(`\u2705 Cleared all ${activePath.length} context frames.`); } else { const topFrame = activePath[activePath.length - 1]; frameManager.closeFrame(topFrame.frame_id); console.log( `\u2705 Popped: ${topFrame.name || topFrame.frame_id.slice(0, 10)}` ); console.log(` Depth: ${frameManager.getStackDepth()}`); } } catch (error) { console.error("\u274C Failed to pop context:", error.message); } finally { db.close(); } }); context.command("add <type> <message>").description( "Add an event to current context (types: observation, decision, error)" ).action(async (type, message) => { 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 db = new Database(dbPath); try { let projectId = "default"; try { const projectRow = db.prepare(`SELECT value FROM metadata WHERE key = 'project_id'`).get(); if (projectRow?.value) projectId = projectRow.value; } catch { } const frameManager = new FrameManager(db, projectId, { skipContextBridge: true }); const activePath = frameManager.getActiveFramePath(); if (activePath.length === 0) { console.log("\u26A0\uFE0F No active context frame. Creating one..."); frameManager.createFrame({ type: "task", name: "cli-session", inputs: {} }); } const currentFrame = frameManager.getActiveFramePath().slice(-1)[0]; const validTypes = [ "observation", "decision", "error", "action", "result" ]; if (!validTypes.includes(type)) { console.log(`\u26A0\uFE0F Unknown event type "${type}". Using "observation".`); type = "observation"; } frameManager.addEvent( type, { message, content: message }, currentFrame.frame_id ); console.log( `\u2705 Added ${type}: ${message.slice(0, 50)}${message.length > 50 ? "..." : ""}` ); } catch (error) { console.error("\u274C Failed to add event:", error.message); } finally { db.close(); } }); context.command("worktree [action]").description("Manage Claude worktree contexts").option("-i, --instance <id>", "Instance ID").option("-b, --branch <name>", "Branch name").option("-l, --list", "List worktree contexts").action(async (action, options) => { 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 db = new Database(dbPath); try { let projectId = "default"; try { const projectRow = db.prepare(`SELECT value FROM metadata WHERE key = 'project_id'`).get(); if (projectRow?.value) projectId = projectRow.value; } catch { } const frameManager = new FrameManager(db, projectId, { skipContextBridge: true }); if (options.list || action === "list") { const worktreeFrames = db.prepare( ` SELECT * FROM frames WHERE project_id = ? AND type = 'session' AND inputs LIKE '%worktree%' ORDER BY created_at DESC LIMIT 10 ` ).all(projectId); console.log("\n\u{1F333} Worktree Contexts\n"); if (worktreeFrames.length === 0) { console.log("No worktree contexts found."); } else { worktreeFrames.forEach((frame) => { const inputs = JSON.parse(frame.inputs || "{}"); const instanceId = inputs.instanceId || "unknown"; const branch = inputs.branch || "unknown"; const created = new Date( frame.created_at * 1e3 ).toLocaleString(); console.log(`\u{1F4CD} ${frame.name || frame.frame_id.slice(0, 10)}`); console.log(` Instance: ${instanceId}`); console.log(` Branch: ${branch}`); console.log(` Created: ${created}`); console.log(""); }); } } else if (action === "save") { const instanceId = options.instance || process.env["CLAUDE_INSTANCE_ID"]; const branch = options.branch || "unknown"; if (!instanceId) { console.log("\u26A0\uFE0F No instance ID provided or detected."); return; } const frameId = frameManager.createFrame({ type: "task", name: `worktree-${branch}`, inputs: { worktree: true, instanceId, branch, path: process.cwd() } }); console.log(`\u2705 Saved worktree context for ${branch}`); console.log(` Instance: ${instanceId}`); console.log(` Frame ID: ${frameId.slice(0, 10)}`); } else if (action === "load") { const instanceId = options.instance || process.env["CLAUDE_INSTANCE_ID"]; if (!instanceId) { console.log("\u26A0\uFE0F No instance ID provided."); return; } const worktreeFrame = db.prepare( ` SELECT * FROM frames WHERE project_id = ? AND type = 'session' AND inputs LIKE ? ORDER BY created_at DESC LIMIT 1 ` ).get(projectId, `%"instanceId":"${instanceId}"%`); if (worktreeFrame) { const inputs = JSON.parse(worktreeFrame.inputs || "{}"); console.log(`\u2705 Loaded worktree context`); console.log(` Branch: ${inputs.branch}`); console.log(` Instance: ${inputs.instanceId}`); console.log(` Path: ${inputs.path}`); } else { console.log("\u26A0\uFE0F No worktree context found for this instance."); } } else { console.log("Usage: stackmemory context worktree [save|load|list]"); } } catch (error) { console.error( "\u274C Failed to manage worktree context:", error.message ); } finally { db.close(); } }); context.addCommand(createContextRehydrateCommand()); return context; } export { createContextCommands }; //# sourceMappingURL=context.js.map