Files
everything-claude-code/scripts/hooks/session-end.js
Affaan Mustafa 18c5a76a96 fix: improve error handling, fix bugs, and optimize core libraries
utils.js:
- Fix countInFile: enforce global flag on regex to prevent silent
  under-counting (match() without /g returns only first match)
- Add 5s timeout to readStdinJson to prevent hooks hanging forever
- Handle EEXIST race condition in ensureDir
- Pre-compile regex patterns in getGitModifiedFiles to avoid N*M
  compilations and catch invalid patterns before filtering
- Add JSDoc documentation to all improved functions

session-manager.js:
- Fix getSessionById triple file read: pass pre-read content to
  getSessionStats instead of re-reading from disk
- Allow getSessionStats to accept content string directly

session-aliases.js:
- Wrap temp file cleanup in try/catch to prevent cascading errors

check-console-log.js:
- Refactor to use shared utils (isGitRepo, getGitModifiedFiles, log)
  instead of raw execSync calls
- Add exclusion patterns for test files, config files, and scripts/
  where console.log is intentional

session-end.js:
- Log count of skipped unparseable transcript lines for diagnostics

suggest-compact.js:
- Guard against NaN from corrupted counter files

package-manager.js:
- Remove dead fallbackOrder parameter (unused after #162 fix)
2026-02-12 07:06:53 -08:00

183 lines
5.0 KiB
JavaScript

#!/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 && fs.existsSync(transcriptPath)) {
summary = extractSessionSummary(transcriptPath);
}
if (fs.existsSync(sessionFile)) {
// Update existing session file
replaceInFile(
sessionFile,
/\*\*Last Updated:\*\*.*/,
`**Last Updated:** ${currentTime}`
);
// 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);
});