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