mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 21:53:28 +08:00
fix(sessions): make session hooks actually persist and load context (#187)
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.
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user