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