diff --git a/scripts/hooks/session-end.js b/scripts/hooks/session-end.js index b429db37..6c1fbed9 100644 --- a/scripts/hooks/session-end.js +++ b/scripts/hooks/session-end.js @@ -4,8 +4,9 @@ * * Cross-platform (Windows, macOS, Linux) * - * Runs when Claude session ends. Creates/updates session log file - * with timestamp for continuity tracking. + * 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'); @@ -16,35 +17,114 @@ const { 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(); + + 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 { + // Skip unparseable 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(); - // Include session ID in filename for unique per-session tracking const sessionFile = path.join(sessionsDir, `${today}-${shortId}-session.tmp`); ensureDir(sessionsDir); const currentTime = getTimeString(); - // If session file exists for today, update the end time + // Try to extract summary from transcript + const transcriptPath = process.env.CLAUDE_TRANSCRIPT_PATH; + let summary = null; + + if (transcriptPath && fs.existsSync(transcriptPath)) { + summary = extractSessionSummary(transcriptPath); + } + if (fs.existsSync(sessionFile)) { - const success = replaceInFile( + // Update existing session file + replaceInFile( sessionFile, /\*\*Last Updated:\*\*.*/, `**Last Updated:** ${currentTime}` ); - if (success) { - log(`[SessionEnd] Updated session file: ${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 with template + // 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} @@ -52,23 +132,7 @@ async function main() { --- -## Current State - -[Session context goes here] - -### Completed -- [ ] - -### In Progress -- [ ] - -### Notes for Next Session -- - -### Context to Load -\`\`\` -[relevant files] -\`\`\` +${summarySection} `; writeFile(sessionFile, template); @@ -78,6 +142,35 @@ async function main() { 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); diff --git a/scripts/hooks/session-start.js b/scripts/hooks/session-start.js index 893bb036..ae3418d9 100644 --- a/scripts/hooks/session-start.js +++ b/scripts/hooks/session-start.js @@ -4,16 +4,20 @@ * * Cross-platform (Windows, macOS, Linux) * - * Runs when a new Claude session starts. Checks for recent session - * files and notifies Claude of available context to load. + * Runs when a new Claude session starts. Loads the most recent session + * summary into Claude's context via stdout, and reports available + * sessions and learned skills. */ +const fs = require('fs'); const { getSessionsDir, getLearnedSkillsDir, findFiles, ensureDir, - log + readFile, + log, + output } = require('../lib/utils'); const { getPackageManager, getSelectionPrompt } = require('../lib/package-manager'); const { listAliases } = require('../lib/session-aliases'); @@ -27,13 +31,19 @@ async function main() { ensureDir(learnedDir); // Check for recent session files (last 7 days) - // Match both old format (YYYY-MM-DD-session.tmp) and new format (YYYY-MM-DD-shortid-session.tmp) const recentSessions = findFiles(sessionsDir, '*-session.tmp', { maxAge: 7 }); if (recentSessions.length > 0) { const latest = recentSessions[0]; log(`[SessionStart] Found ${recentSessions.length} recent session(s)`); log(`[SessionStart] Latest: ${latest.path}`); + + // Read and inject the latest session content into Claude's context + const content = readFile(latest.path); + if (content && !content.includes('[Session context goes here]')) { + // Only inject if the session has actual content (not the blank template) + output(`Previous session summary:\n${content}`); + } } // Check for learned skills