mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-02-16 03:13:08 +08:00
session-end.js: Extract meaningful summaries from CLAUDE_TRANSCRIPT_PATH instead of writing blank template files. Pulls user messages, tools used, and files modified from the session transcript JSONL. session-start.js: Output the latest session summary to stdout (via the output() helper) so it gets injected into Claude's conversation context, instead of only logging to stderr which just shows briefly in the terminal.
178 lines
4.9 KiB
JavaScript
178 lines
4.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 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();
|
|
|
|
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();
|
|
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);
|
|
});
|