Files
gh-shakes-tzd-contextune/hooks/context_restorer.js
2025-11-30 08:56:10 +08:00

165 lines
5.4 KiB
JavaScript
Executable File

#!/usr/bin/env node
/**
* Context Restorer (SessionStart Hook)
*
* Automatically injects preserved context from scratch_pad.md into new session.
* Complements PreCompact hook (context_preserver.py) for DRY workflow:
*
* Session 1: Work → /compact → PreCompact writes scratch_pad.md
* Session 2: SessionStart injects scratch_pad.md → Claude has context (no Read needed!)
*
* DRY Benefit: No redundant file reading - context injected once at session start.
*
* Context Cost: Variable (size of scratch_pad.md content, typically 2-5K tokens)
*/
const fs = require('fs');
const path = require('path');
/**
* Find project root by walking up from current directory
* @returns {string|null} Project root path or null if not found
*/
function findProjectRoot() {
let currentDir = process.cwd();
const root = path.parse(currentDir).root;
while (currentDir !== root) {
// Check for common project indicators
if (
fs.existsSync(path.join(currentDir, '.git')) ||
fs.existsSync(path.join(currentDir, 'pyproject.toml')) ||
fs.existsSync(path.join(currentDir, 'package.json')) ||
fs.existsSync(path.join(currentDir, 'Cargo.toml'))
) {
return currentDir;
}
currentDir = path.dirname(currentDir);
}
return null;
}
/**
* Read and format scratch_pad.md for injection
* @param {string} scratchPadPath Path to scratch_pad.md
* @returns {string|null} Formatted context or null if not found
*/
function readScratchPad(scratchPadPath) {
if (!fs.existsSync(scratchPadPath)) {
return null;
}
try {
const content = fs.readFileSync(scratchPadPath, 'utf8');
// Don't inject if file is empty or very small
if (content.trim().length < 100) {
return null;
}
// Format for injection
const formatted = [
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
'📋 WORKING CONTEXT RESTORED FROM PREVIOUS SESSION',
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
'',
content,
'',
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
'✅ You can continue from where you left off.',
' Context preserved automatically by PreCompact hook.',
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
''
].join('\n');
return formatted;
} catch (err) {
console.error(`DEBUG: Failed to read scratch_pad.md: ${err.message}`);
return null;
}
}
/**
* Delete scratch_pad.md after successful injection
* @param {string} scratchPadPath Path to scratch_pad.md
*/
function cleanupScratchPad(scratchPadPath) {
try {
if (fs.existsSync(scratchPadPath)) {
fs.unlinkSync(scratchPadPath);
console.error('DEBUG: ✅ Cleaned up scratch_pad.md (context injected)');
}
} catch (err) {
console.error(`DEBUG: Failed to cleanup scratch_pad.md: ${err.message}`);
// Non-fatal
}
}
/**
* Main hook execution
*/
function main() {
try {
// Read SessionStart event (contains source: startup|resume|clear|compact)
const event = JSON.parse(fs.readFileSync(0, 'utf-8'));
const source = event.source || 'unknown';
console.error(`DEBUG: SessionStart triggered (source: ${source})`);
// Find project root
const projectRoot = findProjectRoot();
if (!projectRoot) {
console.error('DEBUG: Project root not found, skipping context restoration');
process.exit(0);
}
console.error(`DEBUG: Project root: ${projectRoot}`);
// Check for scratch_pad.md
const scratchPadPath = path.join(projectRoot, 'scratch_pad.md');
const scratchPadContent = readScratchPad(scratchPadPath);
if (!scratchPadContent) {
console.error('DEBUG: No scratch_pad.md found or content too small');
// No context to restore, continue normally
const output = { continue: true };
console.log(JSON.stringify(output));
process.exit(0);
}
// Calculate token estimate (rough: 4 chars per token)
const estimatedTokens = Math.ceil(scratchPadContent.length / 4);
console.error(`DEBUG: Restoring context (~${estimatedTokens.toLocaleString()} tokens)`);
// Inject context via additionalContext
const output = {
continue: true,
hookSpecificOutput: {
hookEventName: 'SessionStart',
additionalContext: scratchPadContent
},
feedback: `📋 Working context restored from previous session (~${estimatedTokens.toLocaleString()} tokens)`,
suppressOutput: false // Show in transcript for transparency
};
console.log(JSON.stringify(output));
// Cleanup scratch_pad.md after successful injection
// (prevents re-injection in future sessions)
cleanupScratchPad(scratchPadPath);
process.exit(0);
} catch (err) {
console.error('Context restoration error:', err.message);
// Fail gracefully - don't block session
const output = { continue: true };
console.log(JSON.stringify(output));
process.exit(0);
}
}
main();