diff --git a/hooks/hooks.json b/hooks/hooks.json index bbd7329f..bfaf2dcd 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -7,7 +7,7 @@ "hooks": [ { "type": "command", - "command": "node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const i=JSON.parse(d);const cmd=i.tool_input?.command||'';if(/(npm run dev\\b|pnpm( run)? dev\\b|yarn dev\\b|bun run dev\\b)/.test(cmd)){console.error('[Hook] BLOCKED: Dev server must run in tmux for log access');console.error('[Hook] Use: tmux new-session -d -s dev \\\"npm run dev\\\"');console.error('[Hook] Then: tmux attach -t dev');process.exit(2)}}catch{}console.log(d)})\"" + "command": "node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const i=JSON.parse(d);const cmd=i.tool_input?.command||'';if(process.platform!=='win32'&&/(npm run dev\\b|pnpm( run)? dev\\b|yarn dev\\b|bun run dev\\b)/.test(cmd)){console.error('[Hook] BLOCKED: Dev server must run in tmux for log access');console.error('[Hook] Use: tmux new-session -d -s dev \\\"npm run dev\\\"');console.error('[Hook] Then: tmux attach -t dev');process.exit(2)}}catch{}console.log(d)})\"" } ], "description": "Block dev servers outside tmux - ensures you can access logs" @@ -17,7 +17,7 @@ "hooks": [ { "type": "command", - "command": "node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const i=JSON.parse(d);const cmd=i.tool_input?.command||'';if(!process.env.TMUX&&/(npm (install|test)|pnpm (install|test)|yarn (install|test)?|bun (install|test)|cargo build|make\\b|docker\\b|pytest|vitest|playwright)/.test(cmd)){console.error('[Hook] Consider running in tmux for session persistence');console.error('[Hook] tmux new -s dev | tmux attach -t dev')}}catch{}console.log(d)})\"" + "command": "node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const i=JSON.parse(d);const cmd=i.tool_input?.command||'';if(process.platform!=='win32'&&!process.env.TMUX&&/(npm (install|test)|pnpm (install|test)|yarn (install|test)?|bun (install|test)|cargo build|make\\b|docker\\b|pytest|vitest|playwright)/.test(cmd)){console.error('[Hook] Consider running in tmux for session persistence');console.error('[Hook] tmux new -s dev | tmux attach -t dev')}}catch{}console.log(d)})\"" } ], "description": "Reminder to use tmux for long-running commands" diff --git a/scripts/hooks/post-edit-format.js b/scripts/hooks/post-edit-format.js index 14f882f9..c57ef948 100644 --- a/scripts/hooks/post-edit-format.js +++ b/scripts/hooks/post-edit-format.js @@ -29,7 +29,8 @@ process.stdin.on('end', () => { try { execFileSync('npx', ['prettier', '--write', filePath], { stdio: ['pipe', 'pipe', 'pipe'], - timeout: 15000 + timeout: 15000, + shell: process.platform === 'win32' }); } catch { // Prettier not installed, file missing, or failed — non-blocking diff --git a/scripts/hooks/post-edit-typecheck.js b/scripts/hooks/post-edit-typecheck.js index 94e969d1..5116dd99 100644 --- a/scripts/hooks/post-edit-typecheck.js +++ b/scripts/hooks/post-edit-typecheck.js @@ -9,60 +9,70 @@ * and reports only errors related to the edited file. */ -const { execFileSync } = require('child_process'); -const fs = require('fs'); -const path = require('path'); +const { execFileSync } = require("child_process"); +const fs = require("fs"); +const path = require("path"); const MAX_STDIN = 1024 * 1024; // 1MB limit -let data = ''; -process.stdin.setEncoding('utf8'); +let data = ""; +process.stdin.setEncoding("utf8"); -process.stdin.on('data', chunk => { +process.stdin.on("data", (chunk) => { if (data.length < MAX_STDIN) { data += chunk; } }); -process.stdin.on('end', () => { +process.stdin.on("end", () => { try { const input = JSON.parse(data); const filePath = input.tool_input?.file_path; if (filePath && /\.(ts|tsx)$/.test(filePath)) { const resolvedPath = path.resolve(filePath); - if (!fs.existsSync(resolvedPath)) { console.log(data); return; } + if (!fs.existsSync(resolvedPath)) { + console.log(data); + return; + } // Find nearest tsconfig.json by walking up (max 20 levels to prevent infinite loop) let dir = path.dirname(resolvedPath); const root = path.parse(dir).root; let depth = 0; while (dir !== root && depth < 20) { - if (fs.existsSync(path.join(dir, 'tsconfig.json'))) { + if (fs.existsSync(path.join(dir, "tsconfig.json"))) { break; } dir = path.dirname(dir); depth++; } - if (fs.existsSync(path.join(dir, 'tsconfig.json'))) { + if (fs.existsSync(path.join(dir, "tsconfig.json"))) { try { - execFileSync('npx', ['tsc', '--noEmit', '--pretty', 'false'], { + execFileSync("npx", ["tsc", "--noEmit", "--pretty", "false"], { cwd: dir, - encoding: 'utf8', - stdio: ['pipe', 'pipe', 'pipe'], - timeout: 30000 + encoding: "utf8", + stdio: ["pipe", "pipe", "pipe"], + timeout: 30000, + shell: process.platform === "win32", }); } catch (err) { // tsc exits non-zero when there are errors — filter to edited file - const output = (err.stdout || '') + (err.stderr || ''); + const output = (err.stdout || "") + (err.stderr || ""); const relevantLines = output - .split('\n') - .filter(line => line.includes(filePath) || line.includes(path.basename(filePath))) + .split("\n") + .filter( + (line) => + line.includes(filePath) || + line.includes(path.basename(filePath)), + ) .slice(0, 10); if (relevantLines.length > 0) { - console.error('[Hook] TypeScript errors in ' + path.basename(filePath) + ':'); - relevantLines.forEach(line => console.error(line)); + console.error( + "[Hook] TypeScript errors in " + path.basename(filePath) + ":", + ); + relevantLines.forEach((line) => console.error(line)); } } } diff --git a/scripts/hooks/session-end.js b/scripts/hooks/session-end.js index e4afe24b..d6ce61ac 100644 --- a/scripts/hooks/session-end.js +++ b/scripts/hooks/session-end.js @@ -45,18 +45,20 @@ function extractSessionSummary(transcriptPath) { 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 && c.text) || '').join(' ') + 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 + // 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); @@ -66,6 +68,21 @@ function extractSessionSummary(transcriptPath) { 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++; }