| #!/usr/bin/env node |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| const path = require('path'); |
| const fs = require('fs'); |
| const { |
| getSessionsDir, |
| getDateString, |
| getTimeString, |
| getSessionIdShort, |
| ensureDir, |
| readFile, |
| writeFile, |
| replaceInFile, |
| log |
| } = require('../lib/utils'); |
|
|
| |
| |
| |
| |
| |
| |
| |
| 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); |
|
|
| |
| if (entry.type === 'user' || entry.role === 'user' || entry.message?.role === 'user') { |
| |
| 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)); |
| } |
| } |
|
|
| |
| 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); |
| } |
| } |
|
|
| |
| 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), |
| toolsUsed: Array.from(toolsUsed).slice(0, 20), |
| filesModified: Array.from(filesModified).slice(0, 30), |
| totalMessages: userMessages.length |
| }; |
| } |
|
|
| |
| const MAX_STDIN = 1024 * 1024; |
| let stdinData = ''; |
| process.stdin.setEncoding('utf8'); |
|
|
| process.stdin.on('data', chunk => { |
| if (stdinData.length < MAX_STDIN) { |
| const remaining = MAX_STDIN - stdinData.length; |
| stdinData += chunk.substring(0, remaining); |
| } |
| }); |
|
|
| process.stdin.on('end', () => { |
| runMain(); |
| }); |
|
|
| function runMain() { |
| main().catch(err => { |
| console.error('[SessionEnd] Error:', err.message); |
| process.exit(0); |
| }); |
| } |
|
|
| async function main() { |
| |
| let transcriptPath = null; |
| try { |
| const input = JSON.parse(stdinData); |
| transcriptPath = input.transcript_path; |
| } catch { |
| |
| 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(); |
|
|
| |
| let summary = null; |
|
|
| if (transcriptPath) { |
| if (fs.existsSync(transcriptPath)) { |
| summary = extractSessionSummary(transcriptPath); |
| } else { |
| log(`[SessionEnd] Transcript not found: ${transcriptPath}`); |
| } |
| } |
|
|
| if (fs.existsSync(sessionFile)) { |
| |
| const updated = replaceInFile( |
| sessionFile, |
| /\*\*Last Updated:\*\*.*/, |
| `**Last Updated:** ${currentTime}` |
| ); |
| if (!updated) { |
| log(`[SessionEnd] Failed to update timestamp in ${sessionFile}`); |
| } |
|
|
| |
| if (summary) { |
| const existing = readFile(sessionFile); |
| if (existing && existing.includes('[Session context goes here]')) { |
| |
| 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 { |
| |
| 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}`); |
| } |
|
|
| |
| const activeTicketFile = path.join(process.cwd(), '.ai', 'tickets', 'active.md'); |
| if (fs.existsSync(activeTicketFile)) { |
| const activeContent = readFile(activeTicketFile); |
| const ticketIdMatch = activeContent && activeContent.match(/^(GH-\d+|\d+)$/m); |
| if (ticketIdMatch) { |
| const rawId = ticketIdMatch[1]; |
| const ticketId = rawId.startsWith('GH-') ? rawId : `GH-${rawId}`; |
| const contextFile = path.join(process.cwd(), '.ai', 'tickets', ticketId, 'context.md'); |
| if (fs.existsSync(contextFile)) { |
| replaceInFile(contextFile, /\*\*Last Updated:\*\*.*/, `**Last Updated:** ${currentTime}`); |
| replaceInFile(contextFile, /\*\*Last Agent:\*\*.*/, `**Last Agent:** claude-code`); |
| log(`[SessionEnd] Updated ticket context metadata: ${ticketId}`); |
| } |
| } |
| } |
|
|
| process.exit(0); |
| } |
|
|
| function buildSummarySection(summary) { |
| let section = '## Session Summary\n\n'; |
|
|
| |
| section += '### Tasks\n'; |
| for (const msg of summary.userMessages) { |
| section += `- ${msg.replace(/\n/g, ' ').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; |
| } |
| |
| |