@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.
460 lines (459 loc) • 15.6 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 { EventEmitter } from "events";
import * as fs from "fs/promises";
import * as path from "path";
import { execSync } from "child_process";
class EnhancedPreClearHooks extends EventEmitter {
frameManager;
dbManager;
clearSurvival;
handoffGenerator;
projectRoot;
constructor(frameManager, dbManager, clearSurvival, handoffGenerator, projectRoot) {
super();
this.frameManager = frameManager;
this.dbManager = dbManager;
this.clearSurvival = clearSurvival;
this.handoffGenerator = handoffGenerator;
this.projectRoot = projectRoot;
}
/**
* Comprehensive pre-clear context capture
*/
async capturePreClearContext(trigger) {
console.log("\u{1F50D} Capturing comprehensive session context...");
const context = {
sessionId: await this.dbManager.getCurrentSessionId(),
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
trigger,
contextUsage: await this.analyzeContextUsage(),
workingState: await this.captureWorkingState(),
conversationState: await this.captureConversationState(),
codeContext: await this.captureCodeContext(),
cognitiveState: await this.captureCognitiveState(),
environment: await this.captureEnvironment()
};
await this.saveEnhancedContext(context);
this.emit("context:captured", context);
console.log("\u2705 Comprehensive context captured");
return context;
}
/**
* Analyze current context usage with detailed breakdown
*/
async analyzeContextUsage() {
const sessionId = await this.dbManager.getCurrentSessionId();
const frames = await this.dbManager.getRecentFrames(sessionId, 1e3);
const traces = await this.dbManager.getRecentTraces(sessionId, 1e3);
const frameTokens = frames.length * 200;
const traceTokens = traces.length * 100;
const conversationTokens = await this.estimateConversationTokens();
const codeBlockTokens = await this.estimateCodeBlockTokens();
const estimatedTokens = frameTokens + traceTokens + conversationTokens + codeBlockTokens;
const maxTokens = 1e5;
return {
estimatedTokens,
maxTokens,
percentage: estimatedTokens / maxTokens,
components: {
frames: frameTokens,
traces: traceTokens,
conversations: conversationTokens,
codeBlocks: codeBlockTokens
}
};
}
/**
* Capture current working state
*/
async captureWorkingState() {
const activeFrame = await this.getCurrentActiveFrame();
const recentTraces = await this.dbManager.getRecentTraces(
await this.dbManager.getCurrentSessionId(),
50
);
const activeFiles = this.extractActiveFiles(recentTraces);
const recentCommands = recentTraces.filter((t) => t.type === "bash" || t.type === "command").map((t) => t.content.command).slice(0, 10);
const pendingActions = this.extractPendingActions(activeFrame);
const blockers = this.extractBlockers(recentTraces);
return {
currentTask: activeFrame?.description || "No active task",
activeFiles,
recentCommands,
pendingActions,
blockers
};
}
/**
* Capture conversation state and recent context
*/
async captureConversationState() {
const sessionId = await this.dbManager.getCurrentSessionId();
const recentTraces = await this.dbManager.getRecentTraces(sessionId, 100);
const userMessages = recentTraces.filter((t) => t.type === "user_message" || t.type === "input").slice(0, 5);
const assistantMessages = recentTraces.filter((t) => t.type === "assistant_message" || t.type === "response").slice(0, 5);
const conversationTopic = this.inferConversationTopic(recentTraces);
const recentContext = this.buildRecentContextSummary(recentTraces);
return {
lastUserMessage: userMessages[0]?.content.message || "No recent user message",
lastAssistantMessage: assistantMessages[0]?.content.message || "No recent assistant message",
conversationTopic,
messageCount: userMessages.length + assistantMessages.length,
recentContext
};
}
/**
* Capture comprehensive code context
*/
async captureCodeContext() {
const gitStatus = await this.captureGitStatus();
const modifiedFiles = await this.captureModifiedFiles();
const testResults = await this.captureTestResults();
const buildStatus = await this.captureBuildStatus();
const dependencies = await this.captureDependencies();
return {
modifiedFiles,
gitStatus,
testResults,
buildStatus,
dependencies
};
}
/**
* Capture cognitive state and mental model
*/
async captureCognitiveState() {
const sessionId = await this.dbManager.getCurrentSessionId();
const recentTraces = await this.dbManager.getRecentTraces(sessionId, 100);
const currentFocus = await this.extractCurrentFocus();
const mentalModel = this.extractMentalModel(recentTraces);
const assumptions = this.extractAssumptions(recentTraces);
const hypotheses = this.extractHypotheses(recentTraces);
const explorationPaths = this.extractExplorationPaths(recentTraces);
return {
currentFocus,
mentalModel,
assumptions,
hypotheses,
explorationPaths
};
}
/**
* Capture environment snapshot
*/
async captureEnvironment() {
const gitBranch = await this.getCurrentGitBranch();
const packageJson = await this.getPackageJson();
const environmentVars = this.getRelevantEnvVars();
return {
workingDirectory: this.projectRoot,
gitBranch,
nodeVersion: process.version,
packageJson,
environmentVars
};
}
/**
* Save enhanced context to multiple locations for reliability
*/
async saveEnhancedContext(context) {
const timestamp = context.timestamp.replace(/[:.]/g, "-");
const primaryPath = path.join(
this.projectRoot,
".stackmemory",
"pre-clear",
`context-${timestamp}.json`
);
const backupPath = path.join(
this.projectRoot,
".stackmemory",
"pre-clear",
"latest-context.json"
);
const markdownPath = path.join(
this.projectRoot,
".stackmemory",
"pre-clear",
`context-${timestamp}.md`
);
await fs.mkdir(path.dirname(primaryPath), { recursive: true });
await fs.writeFile(primaryPath, JSON.stringify(context, null, 2), "utf-8");
await fs.writeFile(backupPath, JSON.stringify(context, null, 2), "utf-8");
const markdown = this.generateMarkdownSummary(context);
await fs.writeFile(markdownPath, markdown, "utf-8");
console.log(
`\u{1F4C1} Context saved to ${path.relative(this.projectRoot, primaryPath)}`
);
}
/**
* Generate human-readable markdown summary
*/
generateMarkdownSummary(context) {
const lines = [
`# Pre-Clear Context Snapshot`,
`**Timestamp**: ${new Date(context.timestamp).toLocaleString()}`,
`**Trigger**: ${context.trigger}`,
`**Session ID**: ${context.sessionId}`,
"",
`## \u{1F4CA} Context Usage`,
`- **Total Tokens**: ${context.contextUsage.estimatedTokens.toLocaleString()} / ${context.contextUsage.maxTokens.toLocaleString()} (${Math.round(context.contextUsage.percentage * 100)}%)`,
`- **Frames**: ${context.contextUsage.components.frames} tokens`,
`- **Traces**: ${context.contextUsage.components.traces} tokens`,
`- **Conversations**: ${context.contextUsage.components.conversations} tokens`,
`- **Code Blocks**: ${context.contextUsage.components.codeBlocks} tokens`,
"",
`## \u{1F3AF} Current Work State`,
`**Task**: ${context.workingState.currentTask}`,
`**Active Files** (${context.workingState.activeFiles.length}):`,
...context.workingState.activeFiles.slice(0, 10).map((f) => `- ${f}`),
"",
`**Recent Commands**:`,
...context.workingState.recentCommands.slice(0, 5).map((c) => `- \`${c}\``),
"",
`## \u{1F4AC} Conversation State`,
`**Topic**: ${context.conversationState.conversationTopic}`,
`**Messages**: ${context.conversationState.messageCount}`,
`**Last User**: ${context.conversationState.lastUserMessage.substring(0, 100)}...`,
"",
`## \u{1F4DD} Code Context`,
`**Git Branch**: ${context.codeContext.gitStatus.branch}`,
`**Modified Files**: ${context.codeContext.modifiedFiles.length}`,
`**Staged**: ${context.codeContext.gitStatus.staged.length}`,
`**Unstaged**: ${context.codeContext.gitStatus.unstaged.length}`,
"",
`## \u{1F9E0} Cognitive State`,
`**Current Focus**: ${context.cognitiveState.currentFocus}`,
`**Mental Model**:`,
...context.cognitiveState.mentalModel.slice(0, 5).map((m) => `- ${m}`),
"",
`## \u{1F30D} Environment`,
`**Directory**: ${context.environment.workingDirectory}`,
`**Node Version**: ${context.environment.nodeVersion}`,
`**Git Branch**: ${context.environment.gitBranch}`,
""
];
return lines.filter((l) => l !== void 0).join("\n");
}
// Helper methods (simplified implementations)
async estimateConversationTokens() {
return 15e3;
}
async estimateCodeBlockTokens() {
return 8e3;
}
async getCurrentActiveFrame() {
const stack = await this.frameManager.getStack();
return stack.frames.find((f) => f.status === "open");
}
extractActiveFiles(traces) {
const files = /* @__PURE__ */ new Set();
traces.forEach((trace) => {
if (trace.content?.file_path) files.add(trace.content.file_path);
if (trace.content?.path) files.add(trace.content.path);
});
return Array.from(files).slice(0, 20);
}
extractPendingActions(frame) {
if (!frame?.metadata?.pendingActions) return [];
return frame.metadata.pendingActions;
}
extractBlockers(traces) {
return traces.filter((t) => t.type === "error" && !t.metadata?.resolved).map((t) => t.content.error || "Unknown error").slice(0, 5);
}
inferConversationTopic(traces) {
return "Code implementation and debugging";
}
buildRecentContextSummary(traces) {
return traces.slice(0, 10).map(
(t) => `${t.type}: ${t.content.summary || t.content.description || "No description"}`
).filter((s) => s.length > 10);
}
async captureGitStatus() {
try {
const branch = execSync("git branch --show-current", {
encoding: "utf-8",
cwd: this.projectRoot
}).trim();
const staged = execSync("git diff --cached --name-only", {
encoding: "utf-8",
cwd: this.projectRoot
}).trim().split("\n").filter(Boolean);
const unstaged = execSync("git diff --name-only", {
encoding: "utf-8",
cwd: this.projectRoot
}).trim().split("\n").filter(Boolean);
const untracked = execSync("git ls-files --others --exclude-standard", {
encoding: "utf-8",
cwd: this.projectRoot
}).trim().split("\n").filter(Boolean);
return {
branch,
ahead: 0,
// Would implement git status parsing
behind: 0,
staged,
unstaged,
untracked,
lastCommit: {
hash: "abc123",
// Would get from git log
message: "Recent commit",
timestamp: (/* @__PURE__ */ new Date()).toISOString()
}
};
} catch (error) {
return {
branch: "unknown",
ahead: 0,
behind: 0,
staged: [],
unstaged: [],
untracked: [],
lastCommit: { hash: "", message: "", timestamp: "" }
};
}
}
async captureModifiedFiles() {
try {
const output = execSync("git diff --name-status", {
encoding: "utf-8",
cwd: this.projectRoot
});
return output.trim().split("\n").filter(Boolean).map((line) => {
const [status, path2] = line.split(" ");
return {
path: path2,
lastModified: (/* @__PURE__ */ new Date()).toISOString(),
changeType: status === "A" ? "created" : status === "D" ? "deleted" : "modified",
lineChanges: { added: 0, removed: 0 },
// Would get from git diff --stat
purpose: "Code changes",
relatedFiles: []
};
});
} catch (error) {
return [];
}
}
async captureTestResults() {
return void 0;
}
async captureBuildStatus() {
return void 0;
}
async captureDependencies() {
try {
const packageJsonPath = path.join(this.projectRoot, "package.json");
const content = await fs.readFile(packageJsonPath, "utf-8");
const packageJson = JSON.parse(content);
const deps = [];
Object.entries(packageJson.dependencies || {}).forEach(
([name, version]) => {
deps.push({
name,
version,
type: "dependency",
critical: ["react", "express", "next"].includes(name)
});
}
);
return deps;
} catch (error) {
return [];
}
}
async extractCurrentFocus() {
const activeFrame = await this.getCurrentActiveFrame();
return activeFrame?.description || "No current focus";
}
extractMentalModel(traces) {
return [
"Component architecture",
"Data flow patterns",
"Error handling strategy"
];
}
extractAssumptions(traces) {
return [
"User input is validated",
"Database is available",
"Network is stable"
];
}
extractHypotheses(traces) {
return [
"Bug is in validation logic",
"Performance issue is database-related"
];
}
extractExplorationPaths(traces) {
return [
"Try different algorithm",
"Refactor data structure",
"Add caching layer"
];
}
async getCurrentGitBranch() {
try {
return execSync("git branch --show-current", {
encoding: "utf-8",
cwd: this.projectRoot
}).trim();
} catch (error) {
return "unknown";
}
}
async getPackageJson() {
try {
const content = await fs.readFile(
path.join(this.projectRoot, "package.json"),
"utf-8"
);
return JSON.parse(content);
} catch (error) {
return null;
}
}
getRelevantEnvVars() {
const relevantVars = ["NODE_ENV", "DEBUG", "PORT", "DATABASE_URL"];
const result = {};
relevantVars.forEach((varName) => {
if (process.env[varName]) {
result[varName] = process.env[varName];
}
});
return result;
}
/**
* Restore context after /clear
*/
async restoreFromEnhancedContext() {
const latestPath = path.join(
this.projectRoot,
".stackmemory",
"pre-clear",
"latest-context.json"
);
try {
const content = await fs.readFile(latestPath, "utf-8");
const context = JSON.parse(content);
console.log("\u{1F4DA} Restoring enhanced context...");
console.log(` Session: ${context.sessionId}`);
console.log(` Task: ${context.workingState.currentTask}`);
console.log(` Files: ${context.workingState.activeFiles.length}`);
console.log(` Focus: ${context.cognitiveState.currentFocus}`);
await this.clearSurvival.restoreFromLedger();
return true;
} catch (error) {
console.error("Failed to restore enhanced context:", error);
return false;
}
}
}
export {
EnhancedPreClearHooks
};
//# sourceMappingURL=enhanced-pre-clear-hooks.js.map