2026-01-23 15:08:07 +08:00
#!/usr/bin/env node
/**
* Stop Hook (Session End) - Persist learnings when session ends
*
* Cross-platform (Windows, macOS, Linux)
*
2026-02-11 23:56:41 -08:00
* 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.
2026-01-23 15:08:07 +08:00
*/
const path = require ( 'path' );
const fs = require ( 'fs' );
const {
getSessionsDir ,
getDateString ,
getTimeString ,
2026-01-25 18:21:27 -08:00
getSessionIdShort ,
2026-01-23 15:08:07 +08:00
ensureDir ,
2026-02-11 23:56:41 -08:00
readFile ,
2026-01-23 15:08:07 +08:00
writeFile ,
replaceInFile ,
log
} = require ( '../lib/utils' );
2026-02-11 23:56:41 -08:00
/**
* 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 ();
2026-02-12 07:06:53 -08:00
let parseErrors = 0 ;
2026-02-11 23:56:41 -08:00
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 {
2026-02-12 07:06:53 -08:00
parseErrors ++ ;
2026-02-11 23:56:41 -08:00
}
}
2026-02-12 07:06:53 -08:00
if ( parseErrors > 0 ) {
log ( `[SessionEnd] Skipped ${ parseErrors } / ${ lines . length } unparseable transcript lines` );
}
2026-02-11 23:56:41 -08:00
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
};
}
2026-01-23 15:08:07 +08:00
async function main () {
const sessionsDir = getSessionsDir ();
const today = getDateString ();
2026-01-25 18:21:27 -08:00
const shortId = getSessionIdShort ();
const sessionFile = path . join ( sessionsDir , ` ${ today } - ${ shortId } -session.tmp` );
2026-01-23 15:08:07 +08:00
ensureDir ( sessionsDir );
const currentTime = getTimeString ();
2026-02-11 23:56:41 -08:00
// Try to extract summary from transcript
const transcriptPath = process . env . CLAUDE_TRANSCRIPT_PATH ;
let summary = null ;
2026-02-12 13:40:14 -08:00
if ( transcriptPath ) {
if ( fs . existsSync ( transcriptPath )) {
summary = extractSessionSummary ( transcriptPath );
} else {
log ( `[SessionEnd] Transcript not found: ${ transcriptPath } ` );
}
2026-02-11 23:56:41 -08:00
}
2026-01-23 15:08:07 +08:00
if ( fs . existsSync ( sessionFile )) {
2026-02-11 23:56:41 -08:00
// Update existing session file
2026-02-12 13:40:14 -08:00
const updated = replaceInFile (
2026-01-23 15:08:07 +08:00
sessionFile ,
/\*\*Last Updated:\*\*.*/ ,
`**Last Updated:** ${ currentTime } `
);
2026-02-12 13:40:14 -08:00
if ( ! updated ) {
log ( `[SessionEnd] Failed to update timestamp in ${ sessionFile } ` );
}
2026-01-23 15:08:07 +08:00
2026-02-11 23:56:41 -08:00
// 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 );
}
2026-01-23 15:08:07 +08:00
}
2026-02-11 23:56:41 -08:00
log ( `[SessionEnd] Updated session file: ${ sessionFile } ` );
2026-01-23 15:08:07 +08:00
} else {
2026-02-11 23:56:41 -08:00
// 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\`\`\`` ;
2026-01-23 15:08:07 +08:00
const template = `# Session: ${ today }
**Date:** ${ today }
**Started:** ${ currentTime }
**Last Updated:** ${ currentTime }
---
2026-02-11 23:56:41 -08:00
${ summarySection }
2026-01-23 15:08:07 +08:00
` ;
writeFile ( sessionFile , template );
log ( `[SessionEnd] Created session file: ${ sessionFile } ` );
}
process . exit ( 0 );
}
2026-02-11 23:56:41 -08:00
function buildSummarySection ( summary ) {
let section = '## Session Summary\n\n' ;
2026-02-12 14:14:21 -08:00
// Tasks (from user messages — escape backticks to prevent markdown breaks)
2026-02-11 23:56:41 -08:00
section += '### Tasks\n' ;
for ( const msg of summary . userMessages ) {
2026-02-12 14:14:21 -08:00
section += `- ${ msg . replace ( /`/g , '\\`' ) } \n` ;
2026-02-11 23:56:41 -08:00
}
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 ;
}
2026-01-23 15:08:07 +08:00
main (). catch ( err => {
console . error ( '[SessionEnd] Error:' , err . message );
process . exit ( 0 );
});