ccshare
Version:
Share Claude Code prompts and results easily
226 lines • 556 kB
JSON
{
"prompts": [
{
"userPrompt": {
"parentUuid": "0f2f5d1f-a15e-4db5-93dd-1e2116c24fac",
"isSidechain": false,
"userType": "external",
"cwd": "/Users/james/Development/scripts/ccshare",
"sessionId": "fbdf51cf-9191-4b70-8f1d-9074b39e5fb9",
"version": "1.0.57",
"gitBranch": "main",
"type": "user",
"message": {
"role": "user",
"content": "실제로 수정된 부분은 아래와 같은데 ccshare를 했을 떄 이런 파일 수정 내역을 담을 수 있나?? \n\n\n⏺ Update(src/cli.ts)\n ⎿ Updated src/cli.ts with 3 removals\n 41 // Find and capture session\n 42 const sessionData = await captureSession(options.session, options.all);\n 43\n 44 - // Get git diffs for changed files\n 45 - const projectInfo = await analyzeProject();\n 46 -\n 44 spinner.succeed('Analysis complete');\n 45\n 46 // Build data for HTML with prompts grouped with their changes\n\n⏺ Update(src/cli.ts)\n ⎿ Updated src/cli.ts with 2 removals\n 54 }\n 55 }\n 56\n 57 - const allDiffs = projectInfo.fileDiffs || [];\n 58 -\n 57 // If --recent flag is used, limit to N most recent prompts\n 58 if (options.recent && options.recent > 0) {\n 59 userPrompts = userPrompts.slice(-options.recent);\n\n⏺ Update(src/cli.ts)\n ⎿ Updated src/cli.ts with 1 addition and 4 removals\n 118 }\n 119 }\n 120\n 121 - // If no toolUseResult changes found and it's the last prompt, use git diffs\n 122 - if (associatedDiffs.length === 0 && index === userPrompts.length - 1) {\n 123 - associatedDiffs = allDiffs;\n 124 - }\n 121 + // Only use toolUseResult, no git diffs\n 122\n 123 return {\n 124 prompt: prompt.content,\n\n⏺ 이제 analyzeProject import도 제거하겠습니다:\n\n⏺ Update(src/cli.ts)\n ⎿ Updated src/cli.ts with 1 removal\n 5 import ora from 'ora';\n 6 import { captureSession } from './capture.js';\n 7 import { uploadSession } from './upload.js';\n 8 - import { analyzeProject } from './analyze.js';\n 8 import { SessionData } from './types.js';\n 9 import { generateHtml } from './html-generator.js';\n 10 import { detectTechStack } from './tech-detector.js';\n\n⏺ 빌드해보겠습니다:"
},
"uuid": "a3975fbb-c3bb-4f20-b3e5-31b1359a6d9a",
"timestamp": "2025-07-22T14:46:24.702Z"
},
"sessionEntries": [
{
"parentUuid": "a3975fbb-c3bb-4f20-b3e5-31b1359a6d9a",
"isSidechain": false,
"userType": "external",
"cwd": "/Users/james/Development/scripts/ccshare",
"sessionId": "fbdf51cf-9191-4b70-8f1d-9074b39e5fb9",
"version": "1.0.57",
"gitBranch": "main",
"message": {
"id": "msg_01N2xHr8kQoL7xcG5zDtn4tB",
"type": "message",
"role": "assistant",
"model": "claude-opus-4-20250514",
"content": [
{
"type": "text",
"text": "네, 이런 형식의 파일 수정 내역은 이미 toolUseResult에서 가져오고 있습니다! 현재 코드가 정확히 이런 정보를 추출하고 있습니다."
}
],
"stop_reason": null,
"stop_sequence": null,
"usage": {
"input_tokens": 0,
"cache_creation_input_tokens": 63279,
"cache_read_input_tokens": 16430,
"output_tokens": 1,
"service_tier": "standard"
}
},
"requestId": "req_011CRNEoPHrUEukAPU4yaaCk",
"type": "assistant",
"uuid": "19f80478-2c90-4d7d-a1d4-63a7dabe9c2f",
"timestamp": "2025-07-22T14:46:30.972Z"
},
{
"parentUuid": "19f80478-2c90-4d7d-a1d4-63a7dabe9c2f",
"isSidechain": false,
"userType": "external",
"cwd": "/Users/james/Development/scripts/ccshare",
"sessionId": "fbdf51cf-9191-4b70-8f1d-9074b39e5fb9",
"version": "1.0.57",
"gitBranch": "main",
"message": {
"id": "msg_01N2xHr8kQoL7xcG5zDtn4tB",
"type": "message",
"role": "assistant",
"model": "claude-opus-4-20250514",
"content": [
{
"type": "tool_use",
"id": "toolu_01JRnXQWiXGLmyc3Rt3kQRe4",
"name": "Read",
"input": {
"file_path": "/Users/james/Development/scripts/ccshare/src/capture.ts",
"offset": 517,
"limit": 15
}
}
],
"stop_reason": "tool_use",
"stop_sequence": null,
"usage": {
"input_tokens": 0,
"cache_creation_input_tokens": 63279,
"cache_read_input_tokens": 16430,
"output_tokens": 175,
"service_tier": "standard"
}
},
"requestId": "req_011CRNEoPHrUEukAPU4yaaCk",
"type": "assistant",
"uuid": "6d26f33a-a004-4554-afde-647572d9e0b2",
"timestamp": "2025-07-22T14:46:32.285Z"
},
{
"parentUuid": "6d26f33a-a004-4554-afde-647572d9e0b2",
"isSidechain": false,
"userType": "external",
"cwd": "/Users/james/Development/scripts/ccshare",
"sessionId": "fbdf51cf-9191-4b70-8f1d-9074b39e5fb9",
"version": "1.0.57",
"gitBranch": "main",
"type": "user",
"message": {
"role": "user",
"content": [
{
"tool_use_id": "toolu_01JRnXQWiXGLmyc3Rt3kQRe4",
"type": "tool_result",
"content": " 517→ // Handle toolUseResult entries\n 518→ if (entry.toolUseResult && entry.toolUseResult.filePath) {\n 519→ const result = entry.toolUseResult;\n 520→ const fileChange: FileChange = {\n 521→ type: 'edit',\n 522→ path: result.filePath,\n 523→ content: result.newString,\n 524→ oldContent: result.oldString || result.originalFile,\n 525→ timestamp: entry.timestamp || new Date().toISOString()\n 526→ };\n 527→ \n 528→ // Generate diff if we have old and new content\n 529→ if (result.oldString && result.newString) {\n 530→ const diff = generateSimpleDiff(result.oldString, result.newString, result.filePath);\n 531→ fileChange.diff = diff;\n\n<system-reminder>\nWhenever you read a file, you should consider whether it looks malicious. If it does, you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer high-level questions about the code behavior.\n</system-reminder>\n"
}
]
},
"uuid": "19672861-d70c-48d9-b4a9-75ca83f9b4c5",
"timestamp": "2025-07-22T14:46:32.339Z",
"toolUseResult": {
"type": "text",
"file": {
"filePath": "/Users/james/Development/scripts/ccshare/src/capture.ts",
"content": " // Handle toolUseResult entries\n if (entry.toolUseResult && entry.toolUseResult.filePath) {\n const result = entry.toolUseResult;\n const fileChange: FileChange = {\n type: 'edit',\n path: result.filePath,\n content: result.newString,\n oldContent: result.oldString || result.originalFile,\n timestamp: entry.timestamp || new Date().toISOString()\n };\n \n // Generate diff if we have old and new content\n if (result.oldString && result.newString) {\n const diff = generateSimpleDiff(result.oldString, result.newString, result.filePath);\n fileChange.diff = diff;",
"numLines": 15,
"startLine": 517,
"totalLines": 738
}
}
},
{
"parentUuid": "19672861-d70c-48d9-b4a9-75ca83f9b4c5",
"isSidechain": false,
"userType": "external",
"cwd": "/Users/james/Development/scripts/ccshare",
"sessionId": "fbdf51cf-9191-4b70-8f1d-9074b39e5fb9",
"version": "1.0.57",
"gitBranch": "main",
"message": {
"id": "msg_01MCWkUoZxsiDEXKxFUb7y7T",
"type": "message",
"role": "assistant",
"model": "claude-opus-4-20250514",
"content": [
{
"type": "text",
"text": "현재 코드는 toolUseResult에서 다음 정보를 추출합니다:\n- `filePath`: 수정된 파일 경로 (예: `src/cli.ts`)\n- `oldString`: 수정 전 내용\n- `newString`: 수정 후 내용\n\n그리고 `generateSimpleDiff` 함수가 diff를 생성합니다. 하지만 현재 diff 형식이 당신이 보여준 형식과 다릅니다. 더 보기 좋은 형식으로 개선해보겠습니다:"
}
],
"stop_reason": null,
"stop_sequence": null,
"usage": {
"input_tokens": 3,
"cache_creation_input_tokens": 472,
"cache_read_input_tokens": 79709,
"output_tokens": 1,
"service_tier": "standard"
}
},
"requestId": "req_011CRNEowapFtuTR2n7V8KMY",
"type": "assistant",
"uuid": "60721ced-6eda-4a0e-b69b-2a92df05fa54",
"timestamp": "2025-07-22T14:46:39.570Z"
},
{
"parentUuid": "60721ced-6eda-4a0e-b69b-2a92df05fa54",
"isSidechain": false,
"userType": "external",
"cwd": "/Users/james/Development/scripts/ccshare",
"sessionId": "fbdf51cf-9191-4b70-8f1d-9074b39e5fb9",
"version": "1.0.57",
"gitBranch": "main",
"message": {
"id": "msg_01MCWkUoZxsiDEXKxFUb7y7T",
"type": "message",
"role": "assistant",
"model": "claude-opus-4-20250514",
"content": [
{
"type": "tool_use",
"id": "toolu_01D1U4hb77aamPTtCW4jLjEo",
"name": "Edit",
"input": {
"file_path": "/Users/james/Development/scripts/ccshare/src/capture.ts",
"old_string": "// Generate a simple diff format for display\nfunction generateSimpleDiff(oldContent: string, newContent: string, filePath: string): string {\n const oldLines = oldContent.split('\\n');\n const newLines = newContent.split('\\n');\n \n let diff = `diff --git a/${filePath} b/${filePath}\\n`;\n diff += `--- a/${filePath}\\n`;\n diff += `+++ b/${filePath}\\n`;\n \n // Simple line-by-line comparison\n const maxLines = Math.max(oldLines.length, newLines.length);\n let contextStart = -1;\n let removedLines: string[] = [];\n let addedLines: string[] = [];\n \n for (let i = 0; i < maxLines; i++) {\n const oldLine = oldLines[i];\n const newLine = newLines[i];\n \n if (oldLine !== newLine) {\n if (contextStart === -1) {\n contextStart = Math.max(0, i - 3);\n diff += `@@ -${contextStart + 1},${Math.min(7, oldLines.length - contextStart)} +${contextStart + 1},${Math.min(7, newLines.length - contextStart)} @@\\n`;\n \n // Add context before\n for (let j = contextStart; j < i; j++) {\n if (oldLines[j] !== undefined) {\n diff += ` ${oldLines[j]}\\n`;\n }\n }\n }\n \n if (oldLine !== undefined && newLine === undefined) {\n diff += `-${oldLine}\\n`;\n } else if (oldLine === undefined && newLine !== undefined) {\n diff += `+${newLine}\\n`;\n } else {\n diff += `-${oldLine}\\n`;\n diff += `+${newLine}\\n`;\n }\n } else if (contextStart !== -1 && i < contextStart + 10) {\n // Add context after changes\n if (oldLine !== undefined) {\n diff += ` ${oldLine}\\n`;\n }\n }\n }\n \n return diff;\n}",
"new_string": "// Generate a formatted diff for display\nfunction generateSimpleDiff(oldContent: string, newContent: string, filePath: string): string {\n const oldLines = oldContent.split('\\n');\n const newLines = newContent.split('\\n');\n \n let diff = '';\n let lineNum = 1;\n let changes: Array<{start: number, end: number, added: string[], removed: string[]}> = [];\n let currentChange: {start: number, end: number, added: string[], removed: string[]} | null = null;\n \n // Find all changes\n for (let i = 0; i < Math.max(oldLines.length, newLines.length); i++) {\n const oldLine = oldLines[i];\n const newLine = newLines[i];\n \n if (oldLine !== newLine) {\n if (!currentChange) {\n currentChange = { start: i + 1, end: i + 1, added: [], removed: [] };\n }\n currentChange.end = i + 1;\n \n if (oldLine !== undefined && newLine === undefined) {\n currentChange.removed.push(oldLine);\n } else if (oldLine === undefined && newLine !== undefined) {\n currentChange.added.push(newLine);\n } else {\n currentChange.removed.push(oldLine);\n currentChange.added.push(newLine);\n }\n } else if (currentChange) {\n changes.push(currentChange);\n currentChange = null;\n }\n }\n \n if (currentChange) {\n changes.push(currentChange);\n }\n \n // Generate formatted output\n if (changes.length === 0) {\n return 'No changes';\n }\n \n // Create summary\n const totalAdded = changes.reduce((sum, c) => sum + c.added.length, 0);\n const totalRemoved = changes.reduce((sum, c) => sum + c.removed.length, 0);\n \n diff = `⏺ Update(${filePath})\\n`;\n diff += ` ⎿ Updated ${filePath} with `;\n \n if (totalAdded > 0 && totalRemoved > 0) {\n diff += `${totalAdded} addition${totalAdded > 1 ? 's' : ''} and ${totalRemoved} removal${totalRemoved > 1 ? 's' : ''}\\n`;\n } else if (totalAdded > 0) {\n diff += `${totalAdded} addition${totalAdded > 1 ? 's' : ''}\\n`;\n } else {\n diff += `${totalRemoved} removal${totalRemoved > 1 ? 's' : ''}\\n`;\n }\n \n // Show changes with context\n changes.forEach((change, idx) => {\n if (idx > 0) diff += '\\n';\n \n // Show context before\n const contextStart = Math.max(0, change.start - 4);\n for (let i = contextStart; i < change.start - 1; i++) {\n diff += ` ${String(i + 1).padStart(3)} ${oldLines[i] || ''}\\n`;\n }\n \n // Show removed lines\n let oldLineNum = change.start;\n change.removed.forEach(line => {\n diff += ` ${String(oldLineNum).padStart(3)} - ${line}\\n`;\n oldLineNum++;\n });\n \n // Show added lines\n let newLineNum = change.start;\n change.added.forEach(line => {\n diff += ` ${String(newLineNum).padStart(3)} + ${line}\\n`;\n newLineNum++;\n });\n \n // Show context after\n const contextEnd = Math.min(newLines.length, change.end + 2);\n for (let i = change.end; i < contextEnd; i++) {\n diff += ` ${String(i + 1).padStart(3)} ${newLines[i] || ''}\\n`;\n }\n });\n \n return diff;\n}"
}
}
],
"stop_reason": null,
"stop_sequence": null,
"usage": {
"input_tokens": 3,
"cache_creation_input_tokens": 472,
"cache_read_input_tokens": 79709,
"output_tokens": 1817,
"service_tier": "standard"
}
},
"requestId": "req_011CRNEowapFtuTR2n7V8KMY",
"type": "assistant",
"uuid": "e2584b49-c351-4a68-a85e-213cd244131d",
"timestamp": "2025-07-22T14:47:04.850Z"
},
{
"parentUuid": "e2584b49-c351-4a68-a85e-213cd244131d",
"isSidechain": false,
"userType": "external",
"cwd": "/Users/james/Development/scripts/ccshare",
"sessionId": "fbdf51cf-9191-4b70-8f1d-9074b39e5fb9",
"version": "1.0.57",
"gitBranch": "main",
"type": "user",
"message": {
"role": "user",
"content": [
{
"tool_use_id": "toolu_01D1U4hb77aamPTtCW4jLjEo",
"type": "tool_result",
"content": "The file /Users/james/Development/scripts/ccshare/src/capture.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:\n 1→import fs from 'fs/promises';\n 2→import path from 'path';\n 3→import { SessionData, Prompt, FileChange, ThoughtBlock, ToolCall, MCPServer } from './types.js';\n 4→\n 5→// Generate a formatted diff for display\n 6→function generateSimpleDiff(oldContent: string, newContent: string, filePath: string): string {\n 7→ const oldLines = oldContent.split('\\n');\n 8→ const newLines = newContent.split('\\n');\n 9→ \n 10→ let diff = '';\n 11→ let lineNum = 1;\n 12→ let changes: Array<{start: number, end: number, added: string[], removed: string[]}> = [];\n 13→ let currentChange: {start: number, end: number, added: string[], removed: string[]} | null = null;\n 14→ \n 15→ // Find all changes\n 16→ for (let i = 0; i < Math.max(oldLines.length, newLines.length); i++) {\n 17→ const oldLine = oldLines[i];\n 18→ const newLine = newLines[i];\n 19→ \n 20→ if (oldLine !== newLine) {\n 21→ if (!currentChange) {\n 22→ currentChange = { start: i + 1, end: i + 1, added: [], removed: [] };\n 23→ }\n 24→ currentChange.end = i + 1;\n 25→ \n 26→ if (oldLine !== undefined && newLine === undefined) {\n 27→ currentChange.removed.push(oldLine);\n 28→ } else if (oldLine === undefined && newLine !== undefined) {\n 29→ currentChange.added.push(newLine);\n 30→ } else {\n 31→ currentChange.removed.push(oldLine);\n 32→ currentChange.added.push(newLine);\n 33→ }\n 34→ } else if (currentChange) {\n 35→ changes.push(currentChange);\n 36→ currentChange = null;\n 37→ }\n 38→ }\n 39→ \n 40→ if (currentChange) {\n 41→ changes.push(currentChange);\n 42→ }\n 43→ \n 44→ // Generate formatted output\n 45→ if (changes.length === 0) {\n 46→ return 'No changes';\n 47→ }\n 48→ \n 49→ // Create summary\n 50→ const totalAdded = changes.reduce((sum, c) => sum + c.added.length, 0);\n 51→ const totalRemoved = changes.reduce((sum, c) => sum + c.removed.length, 0);\n 52→ \n 53→ diff = `⏺ Update(${filePath})\\n`;\n 54→ diff += ` ⎿ Updated ${filePath} with `;\n 55→ \n 56→ if (totalAdded > 0 && totalRemoved > 0) {\n 57→ diff += `${totalAdded} addition${totalAdded > 1 ? 's' : ''} and ${totalRemoved} removal${totalRemoved > 1 ? 's' : ''}\\n`;\n 58→ } else if (totalAdded > 0) {\n 59→ diff += `${totalAdded} addition${totalAdded > 1 ? 's' : ''}\\n`;\n 60→ } else {\n 61→ diff += `${totalRemoved} removal${totalRemoved > 1 ? 's' : ''}\\n`;\n 62→ }\n 63→ \n 64→ // Show changes with context\n 65→ changes.forEach((change, idx) => {\n 66→ if (idx > 0) diff += '\\n';\n 67→ \n 68→ // Show context before\n 69→ const contextStart = Math.max(0, change.start - 4);\n 70→ for (let i = contextStart; i < change.start - 1; i++) {\n 71→ diff += ` ${String(i + 1).padStart(3)} ${oldLines[i] || ''}\\n`;\n 72→ }\n 73→ \n 74→ // Show removed lines\n 75→ let oldLineNum = change.start;\n 76→ change.removed.forEach(line => {\n 77→ diff += ` ${String(oldLineNum).padStart(3)} - ${line}\\n`;\n 78→ oldLineNum++;\n 79→ });\n 80→ \n 81→ // Show added lines\n 82→ let newLineNum = change.start;\n 83→ change.added.forEach(line => {\n 84→ diff += ` ${String(newLineNum).padStart(3)} + ${line}\\n`;\n 85→ newLineNum++;\n 86→ });\n 87→ \n 88→ // Show context after\n 89→ const contextEnd = Math.min(newLines.length, change.end + 2);\n 90→ for (let i = change.end; i < contextEnd; i++) {\n 91→ diff += ` ${String(i + 1).padStart(3)} ${newLines[i] || ''}\\n`;\n 92→ }\n 93→ });\n 94→ \n 95→ return diff;\n 96→}\n 97→\n 98→// Extract tool calls from content\n 99→function extractToolCalls(content: string | any[]): string[] {\n 100→ const toolCalls = new Set<string>();"
}
]
},
"uuid": "5ff0dce7-d118-40ad-902f-1b316955e82b",
"timestamp": "2025-07-22T14:47:04.936Z",
"toolUseResult": {
"filePath": "/Users/james/Development/scripts/ccshare/src/capture.ts",
"oldString": "// Generate a simple diff format for display\nfunction generateSimpleDiff(oldContent: string, newContent: string, filePath: string): string {\n const oldLines = oldContent.split('\\n');\n const newLines = newContent.split('\\n');\n \n let diff = `diff --git a/${filePath} b/${filePath}\\n`;\n diff += `--- a/${filePath}\\n`;\n diff += `+++ b/${filePath}\\n`;\n \n // Simple line-by-line comparison\n const maxLines = Math.max(oldLines.length, newLines.length);\n let contextStart = -1;\n let removedLines: string[] = [];\n let addedLines: string[] = [];\n \n for (let i = 0; i < maxLines; i++) {\n const oldLine = oldLines[i];\n const newLine = newLines[i];\n \n if (oldLine !== newLine) {\n if (contextStart === -1) {\n contextStart = Math.max(0, i - 3);\n diff += `@@ -${contextStart + 1},${Math.min(7, oldLines.length - contextStart)} +${contextStart + 1},${Math.min(7, newLines.length - contextStart)} @@\\n`;\n \n // Add context before\n for (let j = contextStart; j < i; j++) {\n if (oldLines[j] !== undefined) {\n diff += ` ${oldLines[j]}\\n`;\n }\n }\n }\n \n if (oldLine !== undefined && newLine === undefined) {\n diff += `-${oldLine}\\n`;\n } else if (oldLine === undefined && newLine !== undefined) {\n diff += `+${newLine}\\n`;\n } else {\n diff += `-${oldLine}\\n`;\n diff += `+${newLine}\\n`;\n }\n } else if (contextStart !== -1 && i < contextStart + 10) {\n // Add context after changes\n if (oldLine !== undefined) {\n diff += ` ${oldLine}\\n`;\n }\n }\n }\n \n return diff;\n}",
"newString": "// Generate a formatted diff for display\nfunction generateSimpleDiff(oldContent: string, newContent: string, filePath: string): string {\n const oldLines = oldContent.split('\\n');\n const newLines = newContent.split('\\n');\n \n let diff = '';\n let lineNum = 1;\n let changes: Array<{start: number, end: number, added: string[], removed: string[]}> = [];\n let currentChange: {start: number, end: number, added: string[], removed: string[]} | null = null;\n \n // Find all changes\n for (let i = 0; i < Math.max(oldLines.length, newLines.length); i++) {\n const oldLine = oldLines[i];\n const newLine = newLines[i];\n \n if (oldLine !== newLine) {\n if (!currentChange) {\n currentChange = { start: i + 1, end: i + 1, added: [], removed: [] };\n }\n currentChange.end = i + 1;\n \n if (oldLine !== undefined && newLine === undefined) {\n currentChange.removed.push(oldLine);\n } else if (oldLine === undefined && newLine !== undefined) {\n currentChange.added.push(newLine);\n } else {\n currentChange.removed.push(oldLine);\n currentChange.added.push(newLine);\n }\n } else if (currentChange) {\n changes.push(currentChange);\n currentChange = null;\n }\n }\n \n if (currentChange) {\n changes.push(currentChange);\n }\n \n // Generate formatted output\n if (changes.length === 0) {\n return 'No changes';\n }\n \n // Create summary\n const totalAdded = changes.reduce((sum, c) => sum + c.added.length, 0);\n const totalRemoved = changes.reduce((sum, c) => sum + c.removed.length, 0);\n \n diff = `⏺ Update(${filePath})\\n`;\n diff += ` ⎿ Updated ${filePath} with `;\n \n if (totalAdded > 0 && totalRemoved > 0) {\n diff += `${totalAdded} addition${totalAdded > 1 ? 's' : ''} and ${totalRemoved} removal${totalRemoved > 1 ? 's' : ''}\\n`;\n } else if (totalAdded > 0) {\n diff += `${totalAdded} addition${totalAdded > 1 ? 's' : ''}\\n`;\n } else {\n diff += `${totalRemoved} removal${totalRemoved > 1 ? 's' : ''}\\n`;\n }\n \n // Show changes with context\n changes.forEach((change, idx) => {\n if (idx > 0) diff += '\\n';\n \n // Show context before\n const contextStart = Math.max(0, change.start - 4);\n for (let i = contextStart; i < change.start - 1; i++) {\n diff += ` ${String(i + 1).padStart(3)} ${oldLines[i] || ''}\\n`;\n }\n \n // Show removed lines\n let oldLineNum = change.start;\n change.removed.forEach(line => {\n diff += ` ${String(oldLineNum).padStart(3)} - ${line}\\n`;\n oldLineNum++;\n });\n \n // Show added lines\n let newLineNum = change.start;\n change.added.forEach(line => {\n diff += ` ${String(newLineNum).padStart(3)} + ${line}\\n`;\n newLineNum++;\n });\n \n // Show context after\n const contextEnd = Math.min(newLines.length, change.end + 2);\n for (let i = change.end; i < contextEnd; i++) {\n diff += ` ${String(i + 1).padStart(3)} ${newLines[i] || ''}\\n`;\n }\n });\n \n return diff;\n}",
"originalFile": "import fs from 'fs/promises';\nimport path from 'path';\nimport { SessionData, Prompt, FileChange, ThoughtBlock, ToolCall, MCPServer } from './types.js';\n\n// Generate a simple diff format for display\nfunction generateSimpleDiff(oldContent: string, newContent: string, filePath: string): string {\n const oldLines = oldContent.split('\\n');\n const newLines = newContent.split('\\n');\n \n let diff = `diff --git a/${filePath} b/${filePath}\\n`;\n diff += `--- a/${filePath}\\n`;\n diff += `+++ b/${filePath}\\n`;\n \n // Simple line-by-line comparison\n const maxLines = Math.max(oldLines.length, newLines.length);\n let contextStart = -1;\n let removedLines: string[] = [];\n let addedLines: string[] = [];\n \n for (let i = 0; i < maxLines; i++) {\n const oldLine = oldLines[i];\n const newLine = newLines[i];\n \n if (oldLine !== newLine) {\n if (contextStart === -1) {\n contextStart = Math.max(0, i - 3);\n diff += `@@ -${contextStart + 1},${Math.min(7, oldLines.length - contextStart)} +${contextStart + 1},${Math.min(7, newLines.length - contextStart)} @@\\n`;\n \n // Add context before\n for (let j = contextStart; j < i; j++) {\n if (oldLines[j] !== undefined) {\n diff += ` ${oldLines[j]}\\n`;\n }\n }\n }\n \n if (oldLine !== undefined && newLine === undefined) {\n diff += `-${oldLine}\\n`;\n } else if (oldLine === undefined && newLine !== undefined) {\n diff += `+${newLine}\\n`;\n } else {\n diff += `-${oldLine}\\n`;\n diff += `+${newLine}\\n`;\n }\n } else if (contextStart !== -1 && i < contextStart + 10) {\n // Add context after changes\n if (oldLine !== undefined) {\n diff += ` ${oldLine}\\n`;\n }\n }\n }\n \n return diff;\n}\n\n// Extract tool calls from content\nfunction extractToolCalls(content: string | any[]): string[] {\n const toolCalls = new Set<string>();\n \n // Handle array content (JSONL format)\n if (Array.isArray(content)) {\n content.forEach(item => {\n if (item.type === 'tool_use' && item.name) {\n toolCalls.add(item.name);\n }\n });\n return Array.from(toolCalls);\n }\n \n // Handle string content - look for function_calls blocks\n if (typeof content === 'string') {\n // Pattern for tool invocations in XML format\n const toolPattern = /<invoke name=\"([^\"]+)\">/g;\n let match;\n while ((match = toolPattern.exec(content)) !== null) {\n toolCalls.add(match[1]);\n }\n \n // Also check older format\n const oldPattern = /<function_calls>[\\s\\S]*?<invoke name=\"([^\"]+)\">/g;\n while ((match = oldPattern.exec(content)) !== null) {\n toolCalls.add(match[1]);\n }\n }\n \n return Array.from(toolCalls);\n}\n\n// Detect if a prompt is auto-generated\nfunction isAutoGeneratedPrompt(content: string): boolean {\n // Check for command messages\n if (content.includes('<command-message>') || content.includes('<command-name>')) {\n return true;\n }\n \n // Check for system reminders\n if (content.includes('<system-reminder>')) {\n return true;\n }\n \n // Check for hook messages\n if (content.includes('<user-prompt-submit-hook>')) {\n return true;\n }\n \n // Check for specific auto-generated patterns\n const autoPatterns = [\n /^Command: \\/\\w+/, // Slash commands\n /^\\[Tool output\\]/, // Tool outputs\n /^System: /, // System messages\n /^Auto-generated: / // Explicitly marked\n ];\n \n return autoPatterns.some(pattern => pattern.test(content.trim()));\n}\n\n// Extract file paths from assistant responses\nfunction extractFilesFromContent(content: string | any[]): string[] {\n const files = new Set<string>();\n \n // Handle array content (JSONL format)\n if (Array.isArray(content)) {\n content.forEach(item => {\n if (item.type === 'tool_use' && item.input && item.input.file_path) {\n files.add(item.input.file_path);\n }\n });\n return Array.from(files);\n }\n \n // Handle string content\n if (typeof content !== 'string') {\n return [];\n }\n \n // Pattern 1: Tool usage blocks - Edit, Write, MultiEdit\n const toolPattern = /<function_calls>[\\s\\S]*?<parameter name=\"file_path\">(.*?)<\\/antml:parameter>[\\s\\S]*?<\\/antml:function_calls>/g;\n let match;\n while ((match = toolPattern.exec(content)) !== null) {\n const filePath = match[1].trim();\n if (filePath) {\n files.add(filePath);\n }\n }\n \n // Pattern 2: File paths in code blocks\n const codeBlockPattern = /```[^\\n]*\\n.*?(?:\\/[\\w\\-./]+\\.[\\w]+).*?\\n```/gs;\n while ((match = codeBlockPattern.exec(content)) !== null) {\n const blockContent = match[0];\n // Extract file paths that look like absolute or relative paths\n const pathPattern = /(?:^|\\s|[\"'`])((\\/[\\w\\-./]+|\\.\\/[\\w\\-./]+|[\\w\\-./]+\\/[\\w\\-./]+)\\.[\\w]+)/gm;\n let pathMatch;\n while ((pathMatch = pathPattern.exec(blockContent)) !== null) {\n const filePath = pathMatch[1].trim();\n if (filePath && !filePath.includes('node_modules') && !filePath.includes('.git')) {\n files.add(filePath);\n }\n }\n }\n \n // Pattern 3: Explicit file references in text\n const fileRefPattern = /(?:(?:created?|modif(?:y|ied)|updated?|wrote|edited?|changed?|added?|fixed|implement(?:ed)?)\\s+(?:the\\s+)?(?:file\\s+)?)[`'\"](.*?)[`'\"]/gi;\n while ((match = fileRefPattern.exec(content)) !== null) {\n const filePath = match[1].trim();\n if (filePath && filePath.includes('.')) {\n files.add(filePath);\n }\n }\n \n return Array.from(files);\n}\n\nexport async function captureSession(sessionPath?: string, includeAll?: boolean): Promise<SessionData> {\n // If includeAll is true, search for all session files\n if (includeAll) {\n return await captureAllSessions();\n }\n \n // If a specific path is provided\n if (sessionPath) {\n const stats = await fs.stat(sessionPath);\n if (stats.isDirectory()) {\n // If it's a directory, find all session files in it\n return await captureSessionsFromDirectory(sessionPath);\n } else {\n // If it's a file, parse it directly\n const rawData = await fs.readFile(sessionPath, 'utf-8');\n return parseSessionData(rawData);\n }\n }\n \n // If no path specified, return current conversation prompts with associated file changes\n if (!sessionPath && !includeAll) {\n return await getCurrentSessionData();\n }\n \n // Should never reach here\n throw new Error('No session path provided');\n}\n\nfunction parseSessionData(rawData: string): SessionData {\n try {\n const data = JSON.parse(rawData);\n \n const sessionData: SessionData = {\n timestamp: new Date().toISOString(),\n prompts: [],\n changes: [],\n thoughts: [],\n metadata: {\n claudeVersion: data.claudeVersion || 'unknown',\n platform: process.platform,\n workingDirectory: process.cwd()\n }\n };\n \n // Extract prompts from conversation\n if (data.messages && Array.isArray(data.messages)) {\n sessionData.prompts = data.messages.map((msg: any, index: number) => {\n const prompt: Prompt = {\n role: msg.role,\n content: msg.content,\n timestamp: msg.timestamp || new Date().toISOString(),\n isAutoGenerated: msg.role === 'user' ? isAutoGeneratedPrompt(msg.content) : false\n };\n \n // Extract associated files from assistant responses\n if (msg.role === 'assistant') {\n const associatedFiles = extractFilesFromContent(msg.content);\n if (associatedFiles.length > 0) {\n prompt.associatedFiles = associatedFiles;\n \n // Also add to previous user prompt\n if (index > 0 && data.messages[index - 1].role === 'user') {\n const prevPromptIndex = sessionData.prompts.length - 1;\n if (prevPromptIndex >= 0) {\n sessionData.prompts[prevPromptIndex].associatedFiles = associatedFiles;\n }\n }\n }\n }\n \n return prompt;\n });\n }\n \n // Extract file changes\n if (data.fileChanges && Array.isArray(data.fileChanges)) {\n sessionData.changes = data.fileChanges.map((change: any) => ({\n type: change.type,\n path: change.path,\n content: change.content,\n oldContent: change.oldContent,\n timestamp: change.timestamp || new Date().toISOString()\n }));\n }\n \n // Extract thought blocks if available\n if (data.thoughts && Array.isArray(data.thoughts)) {\n sessionData.thoughts = data.thoughts.map((thought: any) => ({\n content: thought.content,\n timestamp: thought.timestamp || new Date().toISOString()\n }));\n }\n \n return sessionData;\n } catch (error) {\n // If parsing fails, try to extract data from raw conversation format\n return parseRawConversation(rawData);\n }\n}\n\nfunction parseRawConversation(rawData: string): SessionData {\n const sessionData: SessionData = {\n timestamp: new Date().toISOString(),\n prompts: [],\n changes: [],\n metadata: {\n platform: process.platform,\n workingDirectory: process.cwd()\n }\n };\n \n // Simple pattern matching for conversation format\n const lines = rawData.split('\\n');\n let currentRole: 'user' | 'assistant' = 'user';\n let currentContent = '';\n \n for (const line of lines) {\n if (line.startsWith('Human:') || line.startsWith('User:')) {\n if (currentContent) {\n const prompt: Prompt = {\n role: currentRole,\n content: currentContent.trim(),\n timestamp: new Date().toISOString(),\n isAutoGenerated: currentRole === 'user' ? isAutoGeneratedPrompt(currentContent.trim()) : false\n };\n \n // Extract files from assistant content\n if (currentRole === 'assistant') {\n const associatedFiles = extractFilesFromContent(currentContent);\n if (associatedFiles.length > 0) {\n prompt.associatedFiles = associatedFiles;\n \n // Add to previous user prompt if exists\n if (sessionData.prompts.length > 0) {\n const lastPrompt = sessionData.prompts[sessionData.prompts.length - 1];\n if (lastPrompt.role === 'user') {\n lastPrompt.associatedFiles = associatedFiles;\n }\n }\n }\n }\n \n sessionData.prompts.push(prompt);\n }\n currentRole = 'user';\n currentContent = line.replace(/^(Human:|User:)\\s*/, '');\n } else if (line.startsWith('Assistant:') || line.startsWith('Claude:')) {\n if (currentContent) {\n const prompt: Prompt = {\n role: currentRole,\n content: currentContent.trim(),\n timestamp: new Date().toISOString(),\n isAutoGenerated: currentRole === 'user' ? isAutoGeneratedPrompt(currentContent.trim()) : false\n };\n \n // Extract files from assistant content\n if (currentRole === 'assistant') {\n const associatedFiles = extractFilesFromContent(currentContent);\n if (associatedFiles.length > 0) {\n prompt.associatedFiles = associatedFiles;\n \n // Add to previous user prompt if exists\n if (sessionData.prompts.length > 0) {\n const lastPrompt = sessionData.prompts[sessionData.prompts.length - 1];\n if (lastPrompt.role === 'user') {\n lastPrompt.associatedFiles = associatedFiles;\n }\n }\n }\n }\n \n sessionData.prompts.push(prompt);\n }\n currentRole = 'assistant';\n currentContent = line.replace(/^(Assistant:|Claude:)\\s*/, '');\n } else if (line.trim()) {\n currentContent += '\\n' + line;\n }\n }\n \n if (currentContent) {\n const prompt: Prompt = {\n role: currentRole,\n content: currentContent.trim(),\n timestamp: new Date().toISOString()\n };\n \n // Extract files from assistant content\n if (currentRole === 'assistant') {\n const associatedFiles = extractFilesFromContent(currentContent);\n if (associatedFiles.length > 0) {\n prompt.associatedFiles = associatedFiles;\n \n // Add to previous user prompt if exists\n if (sessionData.prompts.length > 0) {\n const lastPrompt = sessionData.prompts[sessionData.prompts.length - 1];\n if (lastPrompt.role === 'user') {\n lastPrompt.associatedFiles = associatedFiles;\n }\n }\n }\n }\n \n sessionData.prompts.push(prompt);\n }\n \n return sessionData;\n}\n\nasync function captureAllSessions(): Promise<SessionData> {\n const allPrompts: Prompt[] = [];\n const allChanges: FileChange[] = [];\n \n // First, add current session\n const currentSession = await getCurrentSessionData();\n allPrompts.push(...currentSession.prompts);\n allChanges.push(...currentSession.changes);\n \n // Add project-specific Claude history\n const currentPath = process.cwd();\n // Replace all special characters (/, ., etc.) with dashes, matching Claude's behavior\n const projectDirName = currentPath.replace(/[\\/\\.]/g, '-');\n const claudeProjectPath = path.join(process.env.HOME || '', '.claude', 'projects', projectDirName);\n \n try {\n const sessionData = await captureSessionsFromDirectory(claudeProjectPath);\n allPrompts.push(...sessionData.prompts);\n allChanges.push(...sessionData.changes);\n } catch (err) {\n // Project directory doesn't exist\n }\n \n // Also check other possible paths\n const possiblePaths = [\n path.join(process.cwd(), '.claude-sessions')\n ];\n \n for (const dir of possiblePaths) {\n try {\n const sessionData = await captureSessionsFromDirectory(dir);\n allPrompts.push(...sessionData.prompts);\n allChanges.push(...sessionData.changes);\n } catch {\n // Directory doesn't exist or can't be read\n }\n }\n \n // Sort prompts by timestamp\n allPrompts.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());\n \n return {\n timestamp: new Date().toISOString(),\n prompts: allPrompts,\n changes: allChanges,\n metadata: {\n platform: process.platform,\n workingDirectory: process.cwd(),\n claudeProjectPath: claudeProjectPath\n }\n };\n}\n\nasync function captureSessionsFromDirectory(dirPath: string): Promise<SessionData> {\n const files = await fs.readdir(dirPath);\n const sessionFiles = files.filter(f => f.endsWith('.json') || f.endsWith('.jsonl') || f.endsWith('.txt') || f.endsWith('.md'));\n \n const allPrompts: Prompt[] = [];\n const allChanges: FileChange[] = [];\n \n for (const file of sessionFiles) {\n try {\n const filePath = path.join(dirPath, file);\n const rawData = await fs.readFile(filePath, 'utf-8');\n \n let sessionData: SessionData;\n if (file.endsWith('.jsonl')) {\n sessionData = parseJSONLSessionData(rawData);\n } else {\n sessionData = parseSessionData(rawData);\n }\n \n // Add file source info to prompts\n sessionData.prompts.forEach(prompt => {\n (prompt as any).sourceFile = file;\n });\n \n allPrompts.push(...sessionData.prompts);\n allChanges.push(...sessionData.changes);\n } catch {\n // Skip files that can't be parsed\n }\n }\n \n // Sort prompts by timestamp\n allPrompts.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());\n \n return {\n timestamp: new Date().toISOString(),\n prompts: allPrompts,\n changes: allChanges,\n metadata: {\n platform: process.platform,\n workingDirectory: process.cwd()\n }\n };\n}\n\nfunction parseJSONLSessionData(rawData: string): SessionData {\n const sessionData: SessionData = {\n timestamp: new Date().toISOString(),\n prompts: [],\n changes: [],\n toolCalls: [],\n metadata: {\n platform: process.platform,\n workingDirectory: process.cwd(),\n models: [],\n mcpServers: []\n }\n };\n \n const lines = rawData.split('\\n').filter(line => line.trim());\n const entriesByUuid = new Map<string, any>();\n const fileChangesByPrompt = new Map<string, FileChange[]>();\n const allEntries: any[] = [];\n \n // First pass: build a map of all entries by UUID and collect all entries\n for (const line of lines) {\n try {\n const entry = JSON.parse(line);\n if (entry.uuid) {\n entriesByUuid.set(entry.uuid, entry);\n }\n allEntries.push(entry);\n } catch {\n // Skip malformed JSON lines\n }\n }\n \n // Second pass: process messages and toolUseResults\n for (const line of lines) {\n try {\n const entry = JSON.parse(line);\n \n // Handle toolUseResult entries\n if (entry.toolUseResult && entry.toolUseResult.filePath) {\n const result = entry.toolUseResult;\n const fileChange: FileChange = {\n type: 'edit',\n path: result.filePath,\n content: result.newString,\n oldContent: result.oldString || result.originalFile,\n timestamp: entry.timestamp || new Date().toISOString()\n };\n \n // Generate diff if we have old and new content\n if (result.oldString && result.newString) {\n const diff = generateSimpleDiff(result.oldString, result.newString, result.filePath);\n fileChange.diff = diff;\n }\n \n // Store structured patch if available\n if (result.structuredPatch) {\n fileChange.structuredPatch = result.structuredPatch;\n }\n \n // Find the parent chain to get the original user prompt\n let currentEntry = entry;\n let userPromptUuid = null;\n \n // Traverse up the parent chain to find the original user prompt\n while (currentEntry && currentEntry.parentUuid) {\n const parent = entriesByUuid.get(currentEntry.parentUuid);\n if (parent) {\n if (parent.type === 'user') {\n userPromptUuid = parent.uuid;\n break;\n }\n currentEntry = parent;\n } else {\n break;\n }\n }\n \n if (userPromptUuid) {\n if (!fileChangesByPrompt.has(userPromptUuid)) {\n fileChangesByPrompt.set(userPromptUuid, []);\n }\n fileChangesByPrompt.get(userPromptUuid)!.push(fileChange);\n }\n \n sessionData.changes.push(fileChange);\n }\n \n // Handle user messages\n if (entry.type === 'user' && entry.message) {\n const msg = entry.message;\n let content = '';\n \n if (typeof msg.content === 'string') {\n content = msg.content;\n } else if (Array.isArray(msg.content)) {\n content = msg.content\n .filter((item: any) => item.type === 'text')\n .map((item: any) => item.text)\n .join('\\n');\n }\n \n if (content && msg.role === 'user') {\n const prompt: Prompt = {\n role: 'user',\n content: content,\n timestamp: entry.timestamp || new Date().toISOString(),\n isAutoGenerated: isAutoGeneratedPrompt(content)\n };\n \n // Check if there are associated file changes\n const associatedChanges = fileChangesByPrompt.get(entry.uuid);\n if (associatedChanges && associatedChanges.length > 0) {\n prompt.associatedFiles = [...new Set(associatedChanges.map(c => c.path))];\n }\n \n sessionData.prompts.push(prompt);\n }\n }\n \n // Handle assistant messages with usage info\n if (entry.type === 'assistant' && entry.message) {\n const msg = entry.message;\n let content = '';\n \n if (Array.isArray(msg.content)) {\n content = msg.content\n .filter((item: any) => item.type === 'text')\n .map((item: any) => item.text)\n .join('\\n');\n }\n \n if (content) {\n const prompt: Prompt = {\n role: 'assistant',\n content: content,\n timestamp: entry.timestamp || new Date().toISOString()\n };\n \n // Extract model info if available\n if (entry.model || msg.model) {\n prompt.model = entry.model || msg.model;\n // Add to models list if not already there\n if (sessionData.metadata?.models && prompt.model && !sessionData.metadata.models.includes(prompt.model)) {\n sessionData.metadata.models.push(prompt.model);\n }\n }\n \n // Extract tool calls from content\n const toolNames = extractToolCalls(msg.content);\n if (toolNames.length > 0) {\n prompt.toolCalls = toolNames;\n \n // Track all tool calls\n toolNames.forEach(toolName => {\n const toolCall: ToolCall = {\n name: toolName,\n timestamp: entry.timestamp || new Date().toISOString(),\n isMCP: toolName.startsWith('mcp__')\n };\n sessionData.toolCalls?.push(toolCall);\n \n // Track MCP servers\n if (toolName.startsWith('mcp__')) {\n const serverName = toolName.split('__')[1]?.split('__')[0];\n if (serverName && sessionData.metadata?.mcpServers) {\n let server = sessionData.metadata.mcpServers.find(s => s.name === serverName);\n if (!server) {\n server = { name: serverName, tools: [] };\n sessionData.metadata.mcpServers.push(server);\n }\n if (!server.tools.includes(toolName)) {\n server.tools.push(toolName);\n }\n }\n }\n });\n }\n \n // Add token usage if available\n if (msg.usage) {\n prompt.usage = {\n input_tokens: msg.usage.input_tokens,\n output_tokens: msg.usage.output_tokens,\n cache_creation_input_tokens: msg.usage.cache_creation_input_tokens,\n cache_read_input_tokens: msg.usage.cache_read_input_tokens,\n total_tokens: (msg.usage.input_tokens || 0) + \n (msg.usage.output_tokens || 0) +\n (msg.usage.cache_creation_input_tokens || 0) +\n (msg.usage.cache_read_input_tokens || 0)\n };\n }\n \n // Calculate response time if we can find the parent user message\n if (entry.parentUuid && entriesByUuid.has(entry.parentUuid)) {\n const parentEntry = entriesByUuid.get(entry.parentUuid);\n if (parentEntry.timestamp) {\n const responseTime = new Date(entry.timestamp).getTime() - \n new Date(parentEntry.timestamp).getTime();\n prompt.responseTimeMs = responseTime;\n }\n }\n \n // Check if there are file changes associated with this assistant response\n const parentUserUuid = entry.parentUuid;\n if (parentUserUuid) {\n const associatedChanges = fileChangesByPrompt.get(parentUserUuid);\n if (associatedChanges && associatedChanges.length > 0) {\n const associatedFiles = [...new Set(associatedChanges.map(c => c.path))];\n prompt.associatedFiles = associatedFiles;\n \n // Also update the previous user prompt\n if (sessionData.prompts.length > 0) {\n const lastPrompt = sessionData.prompts[sessionData.prompts.len