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.

460 lines (459 loc) 15.6 kB
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