165 lines
5.4 KiB
JavaScript
Executable File
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();
|