mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-02-15 10:53:18 +08:00
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)
183 lines
5.0 KiB
JavaScript
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);
|
|
});
|