#!/usr/bin/env node /** * Stop Hook (Session End) - Persist learnings when session ends * * Cross-platform (Windows, macOS, Linux) * * Runs when Claude session ends. Extracts a meaningful summary from * the session transcript (via CLAUDE_TRANSCRIPT_PATH) and saves it * to a session file for cross-session continuity. */ const path = require('path'); const fs = require('fs'); const { getSessionsDir, getDateString, getTimeString, getSessionIdShort, ensureDir, readFile, writeFile, replaceInFile, log } = require('../lib/utils'); /** * Extract a meaningful summary from the session transcript. * Reads the JSONL transcript and pulls out key information: * - User messages (tasks requested) * - Tools used * - Files modified */ function extractSessionSummary(transcriptPath) { const content = readFile(transcriptPath); if (!content) return null; const lines = content.split('\n').filter(Boolean); const userMessages = []; const toolsUsed = new Set(); const filesModified = new Set(); let parseErrors = 0; for (const line of lines) { try { const entry = JSON.parse(line); // Collect user messages (first 200 chars each) if (entry.type === 'user' || entry.role === 'user') { const text = typeof entry.content === 'string' ? entry.content : Array.isArray(entry.content) ? entry.content.map(c => c.text || '').join(' ') : ''; if (text.trim()) { userMessages.push(text.trim().slice(0, 200)); } } // Collect tool names and modified files if (entry.type === 'tool_use' || entry.tool_name) { const toolName = entry.tool_name || entry.name || ''; if (toolName) toolsUsed.add(toolName); const filePath = entry.tool_input?.file_path || entry.input?.file_path || ''; if (filePath && (toolName === 'Edit' || toolName === 'Write')) { filesModified.add(filePath); } } } catch { parseErrors++; } } if (parseErrors > 0) { log(`[SessionEnd] Skipped ${parseErrors}/${lines.length} unparseable transcript lines`); } if (userMessages.length === 0) return null; return { userMessages: userMessages.slice(-10), // Last 10 user messages toolsUsed: Array.from(toolsUsed).slice(0, 20), filesModified: Array.from(filesModified).slice(0, 30), totalMessages: userMessages.length }; } async function main() { const sessionsDir = getSessionsDir(); const today = getDateString(); const shortId = getSessionIdShort(); const sessionFile = path.join(sessionsDir, `${today}-${shortId}-session.tmp`); ensureDir(sessionsDir); const currentTime = getTimeString(); // Try to extract summary from transcript const transcriptPath = process.env.CLAUDE_TRANSCRIPT_PATH; let summary = null; if (transcriptPath) { if (fs.existsSync(transcriptPath)) { summary = extractSessionSummary(transcriptPath); } else { log(`[SessionEnd] Transcript not found: ${transcriptPath}`); } } if (fs.existsSync(sessionFile)) { // Update existing session file const updated = replaceInFile( sessionFile, /\*\*Last Updated:\*\*.*/, `**Last Updated:** ${currentTime}` ); if (!updated) { log(`[SessionEnd] Failed to update timestamp in ${sessionFile}`); } // If we have a new summary and the file still has the blank template, replace it if (summary) { const existing = readFile(sessionFile); if (existing && existing.includes('[Session context goes here]')) { const updatedContent = existing.replace( /## Current State\n\n\[Session context goes here\]\n\n### Completed\n- \[ \]\n\n### In Progress\n- \[ \]\n\n### Notes for Next Session\n-\n\n### Context to Load\n```\n\[relevant files\]\n```/, buildSummarySection(summary) ); writeFile(sessionFile, updatedContent); } } log(`[SessionEnd] Updated session file: ${sessionFile}`); } else { // Create new session file const summarySection = summary ? buildSummarySection(summary) : `## Current State\n\n[Session context goes here]\n\n### Completed\n- [ ]\n\n### In Progress\n- [ ]\n\n### Notes for Next Session\n-\n\n### Context to Load\n\`\`\`\n[relevant files]\n\`\`\``; const template = `# Session: ${today} **Date:** ${today} **Started:** ${currentTime} **Last Updated:** ${currentTime} --- ${summarySection} `; writeFile(sessionFile, template); log(`[SessionEnd] Created session file: ${sessionFile}`); } process.exit(0); } function buildSummarySection(summary) { let section = '## Session Summary\n\n'; // Tasks (from user messages) section += '### Tasks\n'; for (const msg of summary.userMessages) { section += `- ${msg}\n`; } section += '\n'; // Files modified if (summary.filesModified.length > 0) { section += '### Files Modified\n'; for (const f of summary.filesModified) { section += `- ${f}\n`; } section += '\n'; } // Tools used if (summary.toolsUsed.length > 0) { section += `### Tools Used\n${summary.toolsUsed.join(', ')}\n\n`; } section += `### Stats\n- Total user messages: ${summary.totalMessages}\n`; return section; } main().catch(err => { console.error('[SessionEnd] Error:', err.message); process.exit(0); });