UNPKG

ccshare

Version:

Share Claude Code prompts and results easily

1,092 lines 54.5 kB
import fs from 'fs/promises'; import path from 'path'; import { appendFileSync, existsSync } from 'fs'; import { execSync } from 'child_process'; import { detectTechStack } from './tech-detector.js'; // Debug logging to file function debugLog(message) { if (process.env.DEBUG_PARENT_CHAIN) { appendFileSync('parent-chain-debug.log', `${new Date().toISOString()} - ${message}\n`); } } // Get additional metadata async function getAdditionalMetadata() { const metadata = {}; // Git information removed - using session data only // Get Node.js version metadata.nodeVersion = process.version; // Get npm version try { metadata.npmVersion = execSync('npm --version', { encoding: 'utf8' }).trim(); } catch { // npm not available } // Get OS information metadata.osInfo = { platform: process.platform, arch: process.arch, release: process.release.name, version: process.version }; // Get Claude settings try { const settingsPath = path.join(process.env.HOME || '', '.claude', 'settings.json'); if (existsSync(settingsPath)) { const settings = JSON.parse(await fs.readFile(settingsPath, 'utf-8')); metadata.claudeSettings = { permissions: settings.permissions?.allow || [], model: settings.model }; } } catch { // Settings file not available } // Get CLAUDE.md content if it exists try { const claudeMdPath = path.join(process.cwd(), 'CLAUDE.md'); metadata.claudeMd = await fs.readFile(claudeMdPath, 'utf-8'); } catch { // CLAUDE.md doesn't exist } metadata.workingDirectory = process.cwd(); return metadata; } // Calculate session statistics function calculateSessionStats(sessionData) { const stats = {}; // Calculate total tokens used let totalTokens = 0; let totalResponseTime = 0; let responseCount = 0; sessionData.prompts.forEach(prompt => { if (prompt.usage?.total_tokens) { totalTokens += prompt.usage.total_tokens; } if (prompt.responseTimeMs) { totalResponseTime += prompt.responseTimeMs; responseCount++; } }); stats.totalTokensUsed = totalTokens > 0 ? totalTokens : undefined; stats.averageResponseTime = responseCount > 0 ? Math.round(totalResponseTime / responseCount) : undefined; stats.totalToolCalls = sessionData.toolCalls?.length || 0; // Count errors from tool executions let errorCount = 0; sessionData.toolExecutions?.forEach(exec => { if (exec.status === 'error') errorCount++; }); stats.errorCount = errorCount > 0 ? errorCount : undefined; return stats; } // Generate a formatted diff for display function generateSimpleDiff(oldContent, newContent, filePath) { const oldLines = oldContent.split('\n'); const newLines = newContent.split('\n'); let diff = ''; let lineNum = 1; let changes = []; let currentChange = null; // Find all changes for (let i = 0; i < Math.max(oldLines.length, newLines.length); i++) { const oldLine = oldLines[i]; const newLine = newLines[i]; if (oldLine !== newLine) { if (!currentChange) { currentChange = { start: i + 1, end: i + 1, added: [], removed: [] }; } currentChange.end = i + 1; if (oldLine !== undefined && newLine === undefined) { currentChange.removed.push(oldLine); } else if (oldLine === undefined && newLine !== undefined) { currentChange.added.push(newLine); } else { currentChange.removed.push(oldLine); currentChange.added.push(newLine); } } else if (currentChange) { changes.push(currentChange); currentChange = null; } } if (currentChange) { changes.push(currentChange); } // Generate formatted output if (changes.length === 0) { return 'No changes'; } // Create summary const totalAdded = changes.reduce((sum, c) => sum + c.added.length, 0); const totalRemoved = changes.reduce((sum, c) => sum + c.removed.length, 0); diff = `⏺ Update(${filePath})\n`; diff += ` ⎿ Updated ${filePath} with `; if (totalAdded > 0 && totalRemoved > 0) { diff += `${totalAdded} addition${totalAdded > 1 ? 's' : ''} and ${totalRemoved} removal${totalRemoved > 1 ? 's' : ''}\n`; } else if (totalAdded > 0) { diff += `${totalAdded} addition${totalAdded > 1 ? 's' : ''}\n`; } else { diff += `${totalRemoved} removal${totalRemoved > 1 ? 's' : ''}\n`; } // Show changes with context changes.forEach((change, idx) => { if (idx > 0) diff += '\n'; // Show context before const contextStart = Math.max(0, change.start - 4); for (let i = contextStart; i < change.start - 1; i++) { diff += ` ${String(i + 1).padStart(3)} ${oldLines[i] || ''}\n`; } // Show removed lines let oldLineNum = change.start; change.removed.forEach(line => { diff += ` ${String(oldLineNum).padStart(3)} - ${line}\n`; oldLineNum++; }); // Show added lines let newLineNum = change.start; change.added.forEach(line => { diff += ` ${String(newLineNum).padStart(3)} + ${line}\n`; newLineNum++; }); // Show context after const contextEnd = Math.min(newLines.length, change.end + 2); for (let i = change.end; i < contextEnd; i++) { diff += ` ${String(i + 1).padStart(3)} ${newLines[i] || ''}\n`; } }); return diff; } // Extract tool calls from content function extractToolCalls(content) { const toolCalls = new Set(); // Handle array content (JSONL format) if (Array.isArray(content)) { content.forEach(item => { if (item.type === 'tool_use' && item.name) { toolCalls.add(item.name); } }); return Array.from(toolCalls); } // Handle string content - look for function_calls blocks if (typeof content === 'string') { // Pattern for tool invocations in XML format const toolPattern = /<invoke name="([^"]+)">/g; let match; while ((match = toolPattern.exec(content)) !== null) { toolCalls.add(match[1]); } // Also check older format const oldPattern = /<function_calls>[\s\S]*?<invoke name="([^"]+)">/g; while ((match = oldPattern.exec(content)) !== null) { toolCalls.add(match[1]); } } return Array.from(toolCalls); } // Detect if a prompt is auto-generated function extractAssistantActions(content, timestamp) { const actions = []; // Simply capture the entire assistant response as one action // This includes any completion summaries, explanations, etc. if (content && content.trim()) { // Remove tool_use patterns that are already tracked separately const cleanContent = content .split('\n') .filter(line => !line.trim().startsWith('⏺ ') || line.includes('완료')) .join('\n') .trim(); if (cleanContent) { actions.push({ type: 'explanation', description: cleanContent, timestamp }); } } return actions; } function isAutoGeneratedPrompt(content) { // Check for command messages if (content.includes('<command-message>') || content.includes('<command-name>')) { return true; } // Check for system reminders if (content.includes('<system-reminder>')) { return true; } // Check for hook messages if (content.includes('<user-prompt-submit-hook>')) { return true; } // Check for local command stdout if (content.includes('<local-command-stdout>')) { return true; } // Check for "Caveat:" messages generated by local commands if (content.startsWith('Caveat: The messages below were generated by the user while running local commands')) { return true; } // Check for specific auto-generated patterns const autoPatterns = [ /^Command: \/\w+/, // Slash commands /^\[Tool output\]/, // Tool outputs /^System: /, // System messages /^Auto-generated: / // Explicitly marked ]; return autoPatterns.some(pattern => pattern.test(content.trim())); } // Extract file paths from assistant responses function extractFilesFromContent(content) { const files = new Set(); // Handle array content (JSONL format) if (Array.isArray(content)) { content.forEach(item => { if (item.type === 'tool_use' && item.input && item.input.file_path) { files.add(item.input.file_path); } }); return Array.from(files); } // Handle string content if (typeof content !== 'string') { return []; } // Pattern 1: Tool usage blocks - Edit, Write, MultiEdit const toolPattern = /<function_calls>[\s\S]*?<parameter name="file_path">(.*?)<\/antml:parameter>[\s\S]*?<\/antml:function_calls>/g; let match; while ((match = toolPattern.exec(content)) !== null) { const filePath = match[1].trim(); if (filePath) { files.add(filePath); } } // Pattern 2: File paths in code blocks const codeBlockPattern = /```[^\n]*\n.*?(?:\/[\w\-./]+\.[\w]+).*?\n```/gs; while ((match = codeBlockPattern.exec(content)) !== null) { const blockContent = match[0]; // Extract file paths that look like absolute or relative paths const pathPattern = /(?:^|\s|["'`])((\/[\w\-./]+|\.\/[\w\-./]+|[\w\-./]+\/[\w\-./]+)\.[\w]+)/gm; let pathMatch; while ((pathMatch = pathPattern.exec(blockContent)) !== null) { const filePath = pathMatch[1].trim(); if (filePath && !filePath.includes('node_modules') && !filePath.includes('.git')) { files.add(filePath); } } } // Pattern 3: Explicit file references in text const fileRefPattern = /(?:(?:created?|modif(?:y|ied)|updated?|wrote|edited?|changed?|added?|fixed|implement(?:ed)?)\s+(?:the\s+)?(?:file\s+)?)[`'"](.*?)[`'"]/gi; while ((match = fileRefPattern.exec(content)) !== null) { const filePath = match[1].trim(); if (filePath && filePath.includes('.')) { files.add(filePath); } } return Array.from(files); } export async function captureRawSession(sessionPath, limit = 20) { const rawData = { prompts: [], metadata: {} }; // Get current session JSONL file const currentPath = process.cwd(); const projectDirName = currentPath.replace(/[^a-zA-Z0-9]/g, '-'); const claudeProjectPath = path.join(process.env.HOME || '', '.claude', 'projects', projectDirName); try { const files = await fs.readdir(claudeProjectPath); const jsonlFiles = files.filter(file => file.endsWith('.jsonl')); if (jsonlFiles.length === 0) { return rawData; } // Find the most recently modified JSONL file (current session) const fileStats = await Promise.all(jsonlFiles.map(async (file) => { const filePath = path.join(claudeProjectPath, file); const stat = await fs.stat(filePath); return { file, mtime: stat.mtime.getTime() }; })); // Sort by modification time (most recent first) fileStats.sort((a, b) => b.mtime - a.mtime); const latestFile = path.join(claudeProjectPath, fileStats[0].file); const content = await fs.readFile(latestFile, 'utf-8'); const lines = content.trim().split('\n'); const entries = []; // Parse all entries for (const line of lines) { try { const entry = JSON.parse(line); entries.push(entry); } catch { // Skip malformed lines } } // Find user prompts (type: "user" with message.role: "user" and content is text) const userPromptIndices = []; for (let i = 0; i < entries.length; i++) { const entry = entries[i]; if (entry.type === 'user' && entry.message?.role === 'user' && typeof entry.message?.content === 'string') { // Check if it's an auto-generated prompt const content = entry.message.content; if (!isAutoGeneratedPrompt(content)) { userPromptIndices.push(i); } } } // Get the last N user prompts const selectedIndices = userPromptIndices.slice(-limit); // Extract session data for each selected prompt for (let i = 0; i < selectedIndices.length; i++) { const startIdx = selectedIndices[i]; const endIdx = i < selectedIndices.length - 1 ? selectedIndices[i + 1] : entries.length; rawData.prompts.push({ userPrompt: entries[startIdx], sessionEntries: entries.slice(startIdx + 1, endIdx) }); } // Add metadata rawData.metadata = await getAdditionalMetadata(); // Add tech stack information try { const techStack = await detectTechStack(process.cwd()); rawData.metadata.techStack = techStack; } catch (error) { console.error('Error detecting tech stack:', error); } return rawData; } catch (error) { console.error('Error reading session:', error); return rawData; } } export async function captureSession(sessionPath, includeAll) { // If includeAll is true, search for all session files if (includeAll) { return await captureAllSessions(); } // If a specific path is provided if (sessionPath) { const stats = await fs.stat(sessionPath); if (stats.isDirectory()) { // If it's a directory, find all session files in it return await captureSessionsFromDirectory(sessionPath); } else { // If it's a file, parse it directly const rawData = await fs.readFile(sessionPath, 'utf-8'); return parseSessionData(rawData); } } // If no path specified, return current conversation prompts with associated file changes if (!sessionPath && !includeAll) { return await getCurrentSessionData(); } // Should never reach here throw new Error('No session path provided'); } export function parseSessionData(rawData) { try { const data = JSON.parse(rawData); const sessionData = { timestamp: new Date().toISOString(), prompts: [], changes: [], thoughts: [], assistantActions: [], // Initialize assistant actions array toolExecutions: [], // Initialize tool executions array metadata: { claudeVersion: data.claudeVersion || 'unknown', platform: process.platform, workingDirectory: process.cwd() } }; // Extract prompts from conversation if (data.messages && Array.isArray(data.messages)) { sessionData.prompts = data.messages.map((msg, index) => { const prompt = { role: msg.role, content: msg.content, timestamp: msg.timestamp || new Date().toISOString(), isAutoGenerated: msg.role === 'user' ? isAutoGeneratedPrompt(msg.content) : false }; // Extract associated files from assistant responses if (msg.role === 'assistant') { const associatedFiles = extractFilesFromContent(msg.content); if (associatedFiles.length > 0) { prompt.associatedFiles = associatedFiles; // Also add to previous user prompt if (index > 0 && data.messages[index - 1].role === 'user') { const prevPromptIndex = sessionData.prompts.length - 1; if (prevPromptIndex >= 0) { sessionData.prompts[prevPromptIndex].associatedFiles = associatedFiles; } } } // Extract assistant actions const actions = extractAssistantActions(msg.content, msg.timestamp || new Date().toISOString()); if (actions.length > 0 && sessionData.assistantActions) { sessionData.assistantActions.push(...actions); } } return prompt; }); } // Extract file changes if (data.fileChanges && Array.isArray(data.fileChanges)) { sessionData.changes = data.fileChanges.map((change) => { const fileChange = { type: change.type, path: change.path, content: change.content, oldContent: change.oldContent, timestamp: change.timestamp || new Date().toISOString() }; // Generate diff if we have old and new content if (change.oldContent && change.content && change.type === 'edit') { fileChange.diff = generateSimpleDiff(change.oldContent, change.content, change.path); } return fileChange; }); } // Extract thought blocks if available if (data.thoughts && Array.isArray(data.thoughts)) { sessionData.thoughts = data.thoughts.map((thought) => ({ content: thought.content, timestamp: thought.timestamp || new Date().toISOString() })); } return sessionData; } catch (error) { // If parsing fails, try to extract data from raw conversation format return parseRawConversation(rawData); } } function parseRawConversation(rawData) { const sessionData = { timestamp: new Date().toISOString(), prompts: [], changes: [], metadata: { platform: process.platform, workingDirectory: process.cwd() } }; // Simple pattern matching for conversation format const lines = rawData.split('\n'); let currentRole = 'user'; let currentContent = ''; for (const line of lines) { if (line.startsWith('Human:') || line.startsWith('User:')) { if (currentContent) { const prompt = { role: currentRole, content: currentContent.trim(), timestamp: new Date().toISOString(), isAutoGenerated: currentRole === 'user' ? isAutoGeneratedPrompt(currentContent.trim()) : false }; // Extract files from assistant content if (currentRole === 'assistant') { const associatedFiles = extractFilesFromContent(currentContent); if (associatedFiles.length > 0) { prompt.associatedFiles = associatedFiles; // Add to previous user prompt if exists if (sessionData.prompts.length > 0) { const lastPrompt = sessionData.prompts[sessionData.prompts.length - 1]; if (lastPrompt.role === 'user') { lastPrompt.associatedFiles = associatedFiles; } } } } sessionData.prompts.push(prompt); } currentRole = 'user'; currentContent = line.replace(/^(Human:|User:)\s*/, ''); } else if (line.startsWith('Assistant:') || line.startsWith('Claude:')) { if (currentContent) { const prompt = { role: currentRole, content: currentContent.trim(), timestamp: new Date().toISOString(), isAutoGenerated: currentRole === 'user' ? isAutoGeneratedPrompt(currentContent.trim()) : false }; // Extract files from assistant content if (currentRole === 'assistant') { const associatedFiles = extractFilesFromContent(currentContent); if (associatedFiles.length > 0) { prompt.associatedFiles = associatedFiles; // Add to previous user prompt if exists if (sessionData.prompts.length > 0) { const lastPrompt = sessionData.prompts[sessionData.prompts.length - 1]; if (lastPrompt.role === 'user') { lastPrompt.associatedFiles = associatedFiles; } } } } sessionData.prompts.push(prompt); } currentRole = 'assistant'; currentContent = line.replace(/^(Assistant:|Claude:)\s*/, ''); } else if (line.trim()) { currentContent += '\n' + line; } } if (currentContent) { const prompt = { role: currentRole, content: currentContent.trim(), timestamp: new Date().toISOString() }; // Extract files from assistant content if (currentRole === 'assistant') { const associatedFiles = extractFilesFromContent(currentContent); if (associatedFiles.length > 0) { prompt.associatedFiles = associatedFiles; // Add to previous user prompt if exists if (sessionData.prompts.length > 0) { const lastPrompt = sessionData.prompts[sessionData.prompts.length - 1]; if (lastPrompt.role === 'user') { lastPrompt.associatedFiles = associatedFiles; } } } } sessionData.prompts.push(prompt); } return sessionData; } async function captureAllSessions() { const allPrompts = []; const allChanges = []; const allAssistantActions = []; const allToolExecutions = []; const allToolCalls = []; // First, add current session const currentSession = await getCurrentSessionData(); allPrompts.push(...currentSession.prompts); allChanges.push(...currentSession.changes); if (currentSession.assistantActions) allAssistantActions.push(...currentSession.assistantActions); if (currentSession.toolExecutions) allToolExecutions.push(...currentSession.toolExecutions); if (currentSession.toolCalls) allToolCalls.push(...currentSession.toolCalls); // Add project-specific Claude history const currentPath = process.cwd(); // Replace all non-alphanumeric characters with dashes, matching Claude's behavior // This includes /, ., _, Korean characters, etc. const projectDirName = currentPath.replace(/[^a-zA-Z0-9]/g, '-'); const claudeProjectPath = path.join(process.env.HOME || '', '.claude', 'projects', projectDirName); try { const sessionData = await captureSessionsFromDirectory(claudeProjectPath); allPrompts.push(...sessionData.prompts); allChanges.push(...sessionData.changes); if (sessionData.assistantActions) allAssistantActions.push(...sessionData.assistantActions); if (sessionData.toolExecutions) allToolExecutions.push(...sessionData.toolExecutions); if (sessionData.toolCalls) allToolCalls.push(...sessionData.toolCalls); } catch (err) { // Project directory doesn't exist } // Also check other possible paths const possiblePaths = [ path.join(process.cwd(), '.claude-sessions') ]; for (const dir of possiblePaths) { try { const sessionData = await captureSessionsFromDirectory(dir); allPrompts.push(...sessionData.prompts); allChanges.push(...sessionData.changes); if (sessionData.assistantActions) allAssistantActions.push(...sessionData.assistantActions); if (sessionData.toolExecutions) allToolExecutions.push(...sessionData.toolExecutions); if (sessionData.toolCalls) allToolCalls.push(...sessionData.toolCalls); } catch { // Directory doesn't exist or can't be read } } // Sort prompts by timestamp allPrompts.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()); // Create session data const sessionData = { timestamp: new Date().toISOString(), prompts: allPrompts, changes: allChanges, assistantActions: allAssistantActions, toolExecutions: allToolExecutions, toolCalls: allToolCalls, metadata: { platform: process.platform, workingDirectory: process.cwd(), claudeProjectPath: claudeProjectPath } }; // Add additional metadata const additionalMetadata = await getAdditionalMetadata(); sessionData.metadata = { ...sessionData.metadata, ...additionalMetadata }; // Calculate session statistics sessionData.metadata.sessionStats = calculateSessionStats(sessionData); return sessionData; } async function captureSessionsFromDirectory(dirPath) { const files = await fs.readdir(dirPath); const sessionFiles = files.filter(f => f.endsWith('.json') || f.endsWith('.jsonl') || f.endsWith('.txt') || f.endsWith('.md')); const allPrompts = []; const allChanges = []; const allAssistantActions = []; const allToolExecutions = []; const allToolCalls = []; for (const file of sessionFiles) { try { const filePath = path.join(dirPath, file); const rawData = await fs.readFile(filePath, 'utf-8'); let sessionData; if (file.endsWith('.jsonl')) { sessionData = parseJSONLSessionData(rawData); } else { sessionData = parseSessionData(rawData); } // Add file source info to prompts sessionData.prompts.forEach(prompt => { prompt.sourceFile = file; }); allPrompts.push(...sessionData.prompts); allChanges.push(...sessionData.changes); if (sessionData.assistantActions) allAssistantActions.push(...sessionData.assistantActions); if (sessionData.toolExecutions) allToolExecutions.push(...sessionData.toolExecutions); if (sessionData.toolCalls) allToolCalls.push(...sessionData.toolCalls); } catch { // Skip files that can't be parsed } } // Sort prompts by timestamp allPrompts.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()); // Create session data const sessionData = { timestamp: new Date().toISOString(), prompts: allPrompts, changes: allChanges, assistantActions: allAssistantActions, toolExecutions: allToolExecutions, toolCalls: allToolCalls, metadata: { platform: process.platform, workingDirectory: process.cwd() } }; // Add additional metadata const additionalMetadata = await getAdditionalMetadata(); sessionData.metadata = { ...sessionData.metadata, ...additionalMetadata }; // Calculate session statistics sessionData.metadata.sessionStats = calculateSessionStats(sessionData); return sessionData; } export function parseJSONLSessionData(rawData) { const sessionData = { timestamp: new Date().toISOString(), prompts: [], changes: [], toolCalls: [], assistantActions: [], toolExecutions: [], metadata: { platform: process.platform, workingDirectory: process.cwd(), models: [], mcpServers: [] } }; const lines = rawData.split('\n').filter(line => line.trim()); const entriesByUuid = new Map(); const fileChangesByPrompt = new Map(); const allEntries = []; // First pass: build a map of all entries by UUID and collect all entries for (const line of lines) { try { const entry = JSON.parse(line); if (entry.uuid) { entriesByUuid.set(entry.uuid, entry); } allEntries.push(entry); } catch { // Skip malformed JSON lines } } // Second pass: process messages and toolUseResults for (const line of lines) { try { const entry = JSON.parse(line); // Handle toolUseResult entries if (entry.toolUseResult) { const result = entry.toolUseResult; // Handle new format (MultiEdit/Edit) if (result.filePath && result.edits && Array.isArray(result.edits)) { // Process each edit as a separate file change for (const edit of result.edits) { const fileChange = { type: 'edit', path: result.filePath, content: edit.new_string, oldContent: edit.old_string, timestamp: entry.timestamp || new Date().toISOString() }; // Generate diff if we have old and new content if (edit.old_string && edit.new_string) { const diff = generateSimpleDiff(edit.old_string, edit.new_string, result.filePath); fileChange.diff = diff; } // Find the real user prompt (not tool_result) let currentEntry = entry; let userPromptUuid = null; const visited = new Set(); // Debug logging debugLog(`\n[DEBUG] Searching for user prompt for file: ${result.filePath}`); debugLog(`[DEBUG] Starting from UUID: ${entry.uuid}`); // Traverse up the parent chain to find the original user prompt let depth = 0; const maxDepth = 20; // Prevent infinite loops while (currentEntry && currentEntry.parentUuid && !visited.has(currentEntry.uuid) && depth < maxDepth) { visited.add(currentEntry.uuid); depth++; const parent = entriesByUuid.get(currentEntry.parentUuid); if (parent) { debugLog(`[DEBUG] Depth ${depth}, Parent type: ${parent.type}, UUID: ${parent.uuid}`); if (parent.type === 'user' && parent.message) { // Check if it's a tool_result const isToolResult = parent.message.content?.[0]?.type === 'tool_result'; if (isToolResult) { // Skip tool_result messages and continue traversing debugLog(`[DEBUG] Skipping tool_result message`); } else { // Check if it's a real user message let content = ''; if (typeof parent.message.content === 'string') { content = parent.message.content; } else if (Array.isArray(parent.message.content)) { const textItem = parent.message.content.find((item) => item.type === 'text'); if (textItem && textItem.text) { content = textItem.text; } } debugLog(`[DEBUG] User message content: "${content.substring(0, 100)}..."`); // Exclude system messages and file change outputs if (content && !content.includes('<function_calls>') && !content.includes('Todos have been modified') && !content.includes('<system-reminder>') && !content.includes('Tool ran without output') && !content.includes('⏺ Update(') && !content.includes('⏺ Read(') && !content.includes('This session is being continued from')) { userPromptUuid = parent.uuid; debugLog(`[DEBUG] Found real user prompt! UUID: ${userPromptUuid}`); break; } else { debugLog(`[DEBUG] Skipping system/tool message`); } } } currentEntry = parent; } else { break; } } if (depth >= maxDepth) { debugLog(`[DEBUG] WARNING: Max depth ${maxDepth} reached without finding user prompt`); } if (userPromptUuid) { if (!fileChangesByPrompt.has(userPromptUuid)) { fileChangesByPrompt.set(userPromptUuid, []); } fileChangesByPrompt.get(userPromptUuid).push(fileChange); } sessionData.changes.push(fileChange); } } // Handle old format (single edit with oldString/newString) else if (result.filePath && (result.oldString || result.newString)) { const fileChange = { type: 'edit', path: result.filePath, content: result.newString, oldContent: result.oldString || result.originalFile, timestamp: entry.timestamp || new Date().toISOString() }; // Generate diff if we have old and new content if (result.oldString && result.newString) { const diff = generateSimpleDiff(result.oldString, result.newString, result.filePath); fileChange.diff = diff; } // Store structured patch if available if (result.structuredPatch) { fileChange.structuredPatch = result.structuredPatch; } // Find the real user prompt (not tool_result) let currentEntry = entry; let userPromptUuid = null; const visited = new Set(); let depth = 0; const maxDepth = 20; // Prevent infinite loops // Traverse up the parent chain to find the original user prompt while (currentEntry && currentEntry.parentUuid && !visited.has(currentEntry.uuid) && depth < maxDepth) { visited.add(currentEntry.uuid); depth++; const parent = entriesByUuid.get(currentEntry.parentUuid); if (parent) { if (parent.type === 'user' && parent.message) { // Check if it's a tool_result const isToolResult = parent.message.content?.[0]?.type === 'tool_result'; if (isToolResult) { // Skip tool_result messages and continue traversing debugLog(`[DEBUG] Skipping tool_result message`); } else { // Check if it's a real user message let content = ''; if (typeof parent.message.content === 'string') { content = parent.message.content; } else if (Array.isArray(parent.message.content)) { const textItem = parent.message.content.find((item) => item.type === 'text'); if (textItem && textItem.text) { content = textItem.text; } } // Exclude system messages and file change outputs if (content && !content.includes('<function_calls>') && !content.includes('Todos have been modified') && !content.includes('<system-reminder>') && !content.includes('Tool ran without output') && !content.includes('⏺ Update(') && !content.includes('⏺ Read(') && !content.includes('This session is being continued from')) { userPromptUuid = parent.uuid; break; } } } currentEntry = parent; } else { break; } } if (depth >= maxDepth) { debugLog(`[DEBUG] WARNING: Max depth ${maxDepth} reached without finding user prompt`); } if (userPromptUuid) { if (!fileChangesByPrompt.has(userPromptUuid)) { fileChangesByPrompt.set(userPromptUuid, []); } fileChangesByPrompt.get(userPromptUuid).push(fileChange); } sessionData.changes.push(fileChange); } } // Handle user messages if (entry.type === 'user' && entry.message) { const msg = entry.message; let content = ''; if (typeof msg.content === 'string') { content = msg.content; } else if (Array.isArray(msg.content)) { // Handle both text and tool_result content const contentParts = []; let hasToolResult = false; msg.content.forEach((item) => { if (item.type === 'text') { contentParts.push(item.text); } else if (item.type === 'tool_result') { hasToolResult = true; // Add tool result to assistant actions if (sessionData.assistantActions && item.content) { const toolResultAction = { type: 'command_execution', description: `Tool result: ${item.content.substring(0, 200)}${item.content.length > 200 ? '...' : ''}`, timestamp: entry.timestamp || new Date().toISOString() }; sessionData.assistantActions.push(toolResultAction); } } }); // Only process as user message if it's not just a tool result if (!hasToolResult || contentParts.length > 0) { content = contentParts.join('\n'); } } if (content && msg.role === 'user') { const prompt = { role: 'user', content: content, timestamp: entry.timestamp || new Date().toISOString(), isAutoGenerated: isAutoGeneratedPrompt(content), uuid: entry.uuid }; // Check if there are associated file changes const associatedChanges = fileChangesByPrompt.get(entry.uuid); if (associatedChanges && associatedChanges.length > 0) { prompt.associatedFiles = [...new Set(associatedChanges.map(c => c.path))]; } sessionData.prompts.push(prompt); } } // Handle assistant messages with usage info if (entry.type === 'assistant' && entry.message) { const msg = entry.message; let content = ''; if (Array.isArray(msg.content)) { // Include both text and tool_use content const contentParts = []; msg.content.forEach((item) => { if (item.type === 'text') { contentParts.push(item.text); } else if (item.type === 'tool_use') { // Format tool use as markdown contentParts.push(`\n⏺ ${item.name}`); // Track tool execution if (sessionData.toolExecutions) { const toolExecution = { tool: item.name, timestamp: entry.timestamp || new Date().toISOString(), parameters: item.input, promptId: entry.parentUuid }; sessionData.toolExecutions.push(toolExecution); debugLog(`[DEBUG] Added tool execution: ${item.name}`); } if (item.input) { // Show tool parameters if (item.name === 'Bash' && item.input.command) { contentParts.push(` ⎿ ${item.input.command}`); } else if (item.name === 'Edit' && item.input.file_path) { contentParts.push(` ⎿ ${item.input.file_path}`); } else if (item.name === 'TodoWrite') { contentParts.push(` ⎿ Update Todos`); } else if (item.name === 'Read' && item.input.file_path) { contentParts.push(` ⎿ ${item.input.file_path}`); } } } }); content = contentParts.join('\n'); } if (content) { const prompt = { role: 'assistant', content: content, timestamp: entry.timestamp || new Date().toISOString() }; // Extract actions from assistant response const actions = extractAssistantActions(content, entry.timestamp || new Date().toISOString()); debugLog(`[DEBUG] Extracted ${actions.length} actions from assistant response`); if (actions.length > 0 && sessionData.assistantActions) { // Link actions to the previous user prompt const lastUserPrompt = sessionData.prompts.filter(p => p.role === 'user').pop(); if (lastUserPrompt) { actions.forEach(action => { action.promptId = entry.parentUuid; // Link to parent prompt }); } sessionData.assistantActions.push(...actions); debugLog(`[DEBUG] Total assistant actions: ${sessionData.assistantActions.length}`); } // Extract model info if available if (entry.model || msg.model) { prompt.model = entry.model || msg.model; // Add to models list if not already there if (sessionData.metadata?.models && prompt.model && !sessionData.metadata.models.includes(prompt.model)) { sessionData.metadata.models.push(prompt.model); } } // Extract tool calls from content const toolNames = extractToolCalls(msg.content); if (toolNames.length > 0) { prompt.toolCalls = toolNames; // Track all tool calls toolNames.forEach(toolName => { const toolCall = { name: toolName, timestamp: entry.timestamp || new Date().toISOString(), isMCP: toolName.startsWith('mcp__') }; sessionData.toolCalls?.push(toolCall); // Track MCP servers if (toolName.startsWith('mcp__')) { const serverName = toolName.split('__')[1]?.split('__')[0]; if (serverName && sessionData.metadata?.mcpServers) { let server = sessionData.metadata.mcpServers.find(s => s.name === serverName); if (!server) { server = { name: serverName, tools: [] }; sessionData.metadata.mcpServers.push(server); } if (!server.tools.includes(toolName)) { server.tools.push(toolName); } } } }); } // Add token usage if available if (msg.usage) { prompt.usage = { input_tokens: msg.usage.input_tokens, output_tokens: msg.usage.output_tokens, cache_creation_input_tokens: msg.usage.cache_creation_input_tokens, cache_read_input_tokens: msg.usage.cache_read_input_tokens, total_tokens: (msg.usage.input_tokens || 0) + (msg.usage.output_tokens || 0) + (msg.usage.cache_creation_input_tokens || 0) + (msg.usage.cache_read_input_tokens || 0) }; } // Calculate response time if we can find the parent user message if (entry.parentUuid && entriesByUuid.has(entry.parentUuid)) { const parentEntry = entriesByUuid.get(entry.parentUuid); if (parentEntry.timestamp) { const responseTime = new Date(entry.timestamp).getTime() - new Date(parentEntry.timestamp).getTime(); prompt.responseTimeMs = responseTime; } } // Che