Files
everything-claude-code/scripts/hooks/session-end.js
Affaan Mustafa bc0520c6c1 fix: broken cross-references, version sync, and enhanced command validator
- Fix /build-and-fix → /build-fix in tdd.md, plan.md (+ cursor, zh-CN)
- Fix non-existent explorer agent → planner in orchestrate.md (+ cursor, zh-CN, zh-TW)
- Fix /python-test → /tdd in python-review.md (+ cursor, zh-CN)
- Sync package.json version from 1.0.0 to 1.4.1 to match plugin.json
- Enhance validate-commands.js with cross-reference checking:
  command refs, agent path refs, skill dir refs, workflow diagrams
- Strip fenced code blocks before scanning to avoid false positives
- Skip hypothetical "Creates:" lines in evolve.md examples
- Add 46 new tests (suggest-compact, session-manager, utils, hooks)
2026-02-12 16:19:04 -08:00

218 lines
5.9 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 stdin JSON 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 && 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
};
}
// Read hook input from stdin (Claude Code provides transcript_path via stdin JSON)
const MAX_STDIN = 1024 * 1024;
let stdinData = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => {
if (stdinData.length < MAX_STDIN) {
stdinData += chunk;
}
});
process.stdin.on('end', () => {
runMain();
});
function runMain() {
main().catch(err => {
console.error('[SessionEnd] Error:', err.message);
process.exit(0);
});
}
async function main() {
// Parse stdin JSON to get transcript_path
let transcriptPath = null;
try {
const input = JSON.parse(stdinData);
transcriptPath = input.transcript_path;
} catch {
// Fallback: try env var for backwards compatibility
transcriptPath = process.env.CLAUDE_TRANSCRIPT_PATH;
}
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
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]')) {
// Use a flexible regex that tolerates CRLF, extra whitespace, and minor template variations
const updatedContent = existing.replace(
/## Current State\s*\n\s*\[Session context goes here\][\s\S]*?### Context to Load\s*\n```\s*\n\[relevant files\]\s*\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 — escape backticks to prevent markdown breaks)
section += '### Tasks\n';
for (const msg of summary.userMessages) {
section += `- ${msg.replace(/`/g, '\\`')}\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;
}