mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-02-16 03:13:08 +08:00
* fix: Windows compatibility for hook scripts
- post-edit-format.js: add `shell: process.platform === 'win32'` to
execFileSync options so npx.cmd is resolved via cmd.exe on Windows
- post-edit-typecheck.js: same fix for tsc invocation via npx
- hooks.json: skip tmux-dependent hooks on Windows where tmux is
unavailable (dev-server blocker and long-running command reminder)
On Windows, execFileSync('npx', ...) without shell:true fails with
ENOENT because Node.js cannot directly execute .cmd files. These
hooks silently fail on all Windows installations.
The tmux hooks unconditionally block dev server commands (exit 2) or
warn about tmux on Windows where tmux is not available.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: parse Claude Code JSONL transcript format correctly
The session-end hook expected user messages at entry.content, but
Claude Code's actual JSONL format nests them at entry.message.content.
This caused all session files to be blank templates (0 user messages
despite 136+ actual entries).
- Check entry.message?.content in addition to entry.content
- Extract tool_use blocks from assistant message.content arrays
Verified with Claude Code v2.1.41 JSONL transcripts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: ddungan <sckim@mococo.co.kr>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
235 lines
6.7 KiB
JavaScript
235 lines
6.7 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' || entry.message?.role === 'user') {
|
|
// Support both direct content and nested message.content (Claude Code JSONL format)
|
|
const rawContent = entry.message?.content ?? entry.content;
|
|
const text = typeof rawContent === 'string'
|
|
? rawContent
|
|
: Array.isArray(rawContent)
|
|
? rawContent.map(c => (c && c.text) || '').join(' ')
|
|
: '';
|
|
if (text.trim()) {
|
|
userMessages.push(text.trim().slice(0, 200));
|
|
}
|
|
}
|
|
|
|
// Collect tool names and modified files (direct tool_use entries)
|
|
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);
|
|
}
|
|
}
|
|
|
|
// Extract tool uses from assistant message content blocks (Claude Code JSONL format)
|
|
if (entry.type === 'assistant' && Array.isArray(entry.message?.content)) {
|
|
for (const block of entry.message.content) {
|
|
if (block.type === 'tool_use') {
|
|
const toolName = block.name || '';
|
|
if (toolName) toolsUsed.add(toolName);
|
|
|
|
const filePath = block.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;
|
|
}
|
|
|