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.

255 lines (241 loc) 7.49 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { SessionMonitor } from "../../core/monitoring/session-monitor"; import { FrameManager } from "../../core/frame/frame-manager"; import { DatabaseManager } from "../../core/storage/database-manager"; import * as fs from "fs/promises"; import * as path from "path"; import * as os from "os"; class ClaudeCodeLifecycleHooks { monitor; config; isActive = false; hookScripts = /* @__PURE__ */ new Map(); constructor(config) { this.config = { ...config, claudeHooksPath: config.claudeHooksPath || path.join(os.homedir(), ".claude", "hooks") }; } /** * Initialize hooks and start monitoring */ async initialize() { if (this.isActive) return; const dbPath = path.join( this.config.projectRoot, ".stackmemory", "db", "stackmemory.db" ); const dbManager = new DatabaseManager(dbPath); await dbManager.initialize(); const frameManager = new FrameManager(dbManager); this.monitor = new SessionMonitor( frameManager, dbManager, this.config.projectRoot, { contextWarningThreshold: 0.6, contextCriticalThreshold: 0.7, contextAutoSaveThreshold: 0.85, idleTimeoutMinutes: 5, autoSaveLedger: this.config.autoTriggers.onContextCritical, autoGenerateHandoff: this.config.autoTriggers.onSessionIdle, sessionEndHandoff: this.config.autoTriggers.onSessionEnd } ); this.registerEventHandlers(); await this.installClaudeHooks(); await this.monitor.start(); this.isActive = true; console.log("\u2705 Claude Code lifecycle hooks initialized"); } /** * Register event handlers for monitor events */ registerEventHandlers() { if (!this.monitor) return; this.monitor.on("context:warning", (data) => { console.log(`\u26A0\uFE0F Context at ${Math.round(data.percentage * 100)}%`); }); this.monitor.on("context:high", async (data) => { if (this.config.autoTriggers.onContextHigh) { console.log("\u{1F7E1} High context - preparing auto-save"); await this.executeHook("on-context-high", data); } }); this.monitor.on("context:ledger_saved", (data) => { console.log(`\u2705 Ledger saved (${data.compression}x compression)`); console.log("\u{1F4A1} You can now safely use /clear"); }); this.monitor.on("handoff:generated", (data) => { console.log(`\u{1F4CB} Handoff saved (trigger: ${data.trigger})`); }); } /** * Install hooks into Claude Code configuration */ async installClaudeHooks() { await fs.mkdir(this.config.claudeHooksPath, { recursive: true }); await this.installHook( "on-message-submit", ` #!/bin/bash # StackMemory Context Monitor Hook # Monitors token usage and triggers auto-save when needed # Get estimated token count from Claude (if available) TOKEN_COUNT=\${CLAUDE_TOKEN_COUNT:-0} MAX_TOKENS=\${CLAUDE_MAX_TOKENS:-100000} if [ "$TOKEN_COUNT" -gt 0 ]; then USAGE=$((TOKEN_COUNT * 100 / MAX_TOKENS)) if [ "$USAGE" -gt 85 ]; then echo "\u{1F534} Critical: Context at \${USAGE}% - Auto-saving..." stackmemory clear --save > /dev/null 2>&1 echo "\u2705 Ledger saved. Consider using /clear" elif [ "$USAGE" -gt 70 ]; then echo "\u26A0\uFE0F Warning: Context at \${USAGE}%" echo "\u{1F4A1} Run: stackmemory clear --save" fi fi # Update activity timestamp stackmemory monitor --activity 2>/dev/null || true ` ); await this.installHook( "on-session-end", ` #!/bin/bash # StackMemory Session End Hook # Generates handoff document when session ends echo "\u{1F4E6} Saving session state..." # Generate handoff stackmemory capture --no-commit > /dev/null 2>&1 && echo "\u2705 Handoff saved" # Save ledger if context is significant CONTEXT_STATUS=$(stackmemory clear --check 2>/dev/null | grep -o '[0-9]\\+%' | head -1 | tr -d '%') if [ "\${CONTEXT_STATUS:-0}" -gt 30 ]; then stackmemory clear --save > /dev/null 2>&1 && echo "\u2705 Continuity ledger saved" fi echo "\u{1F44B} Session state preserved for next time" ` ); await this.installHook( "on-command-clear", ` #!/bin/bash # StackMemory Clear Interceptor # Saves state before /clear command echo "\u{1F504} Preparing for /clear..." # Save continuity ledger stackmemory clear --save > /dev/null 2>&1 echo "\u2705 Continuity ledger saved" # Generate quick handoff stackmemory capture --no-commit > /dev/null 2>&1 echo "\u2705 Handoff document saved" echo "\u2705 Ready for /clear - context will be restored automatically" echo "\u{1F4A1} After /clear, run: stackmemory clear --restore" ` ); await this.installHook( "on-idle-5min", ` #!/bin/bash # StackMemory Idle Detector # Generates handoff after 5 minutes of inactivity echo "\u23F8\uFE0F Session idle - generating handoff..." stackmemory capture --no-commit > /dev/null 2>&1 echo "\u2705 Handoff saved. Ready to resume anytime." ` ); } /** * Install a specific hook script */ async installHook(name, script) { const hookPath = path.join(this.config.claudeHooksPath, name); this.hookScripts.set(name, script); await fs.writeFile(hookPath, script.trim(), { mode: 493 }); await fs.chmod(hookPath, 493); } /** * Execute a hook with context */ async executeHook(hookName, context) { const hookPath = path.join(this.config.claudeHooksPath, hookName); try { await fs.access(hookPath); const { exec } = await import("child_process"); const { promisify } = await import("util"); const execAsync = promisify(exec); const env = { ...process.env, STACKMEMORY_CONTEXT: JSON.stringify(context), STACKMEMORY_PROJECT: this.config.projectRoot }; const { stdout, stderr } = await execAsync(hookPath, { env }); if (stdout) console.log(stdout); if (stderr) console.error(stderr); } catch (error) { console.debug(`Hook ${hookName} not found or failed:`, error); } } /** * Stop monitoring and cleanup */ async stop() { if (this.monitor) { await this.monitor.stop(); } this.isActive = false; console.log("\u{1F6D1} Claude Code lifecycle hooks stopped"); } /** * Get current status */ getStatus() { return { isActive: this.isActive, monitorStatus: this.monitor?.getStatus(), config: this.config, installedHooks: Array.from(this.hookScripts.keys()) }; } } let globalInstance; async function initializeClaudeHooks(projectRoot) { if (!projectRoot) { projectRoot = process.cwd(); } if (!globalInstance) { globalInstance = new ClaudeCodeLifecycleHooks({ projectRoot, autoTriggers: { onContextHigh: true, onContextCritical: true, onSessionIdle: true, onSessionEnd: true, onClearCommand: true } }); await globalInstance.initialize(); } return globalInstance; } function getClaudeHooks() { return globalInstance; } async function stopClaudeHooks() { if (globalInstance) { await globalInstance.stop(); globalInstance = void 0; } } export { ClaudeCodeLifecycleHooks, getClaudeHooks, initializeClaudeHooks, stopClaudeHooks }; //# sourceMappingURL=lifecycle-hooks.js.map