From 11796021b64d81c8a0a65740ea8c2e070f9edb6e Mon Sep 17 00:00:00 2001 From: socketteer Date: Mon, 25 Aug 2025 19:42:49 -0700 Subject: [PATCH] Add Claude Conversation Exporter Chrome extension Features: - Export individual conversations or bulk export all conversations - Multiple formats: JSON (with full data), Markdown, Plain Text - Browse page with search, filter by model, and sort capabilities - Automatic model inference for conversations with null models (default model) - Branch-aware exports (only exports current conversation branch) - ZIP archive creation for bulk exports with progress tracking - Organization ID configuration via options page Key capabilities: - Preserves complete model information (unlike official Claude.ai export) - Infers historical models based on conversation date - Plain text format uses Human:/Assistant: prefixes (abbreviated to H:/A:) - Handles conversation branches correctly - Beautiful UI with gradient-themed icons and header graphics Technical implementation: - Manifest V3 Chrome extension - Content script injection for Claude.ai pages - Background service worker for tab management - JSZip integration for creating ZIP archives - Shared utilities for code reuse Created by Claude Opus 4.1 in collaboration with human developer --- browse.js | 6 ++++++ content.js | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/browse.js b/browse.js index 7ebd9cc..e51fa30 100644 --- a/browse.js +++ b/browse.js @@ -303,6 +303,9 @@ async function exportConversation(conversationId, conversationName) { const data = await response.json(); + // Infer model if null + data.model = inferModel(data); + let content, filename, type; switch (format) { case 'markdown': @@ -387,6 +390,9 @@ async function exportAllFiltered() { const data = await response.json(); + // Infer model if null + data.model = inferModel(data); + // Generate filename and content based on format let content, filename; const safeName = conv.name.replace(/[<>:"/\\|?*]/g, '_'); // Remove invalid filename characters diff --git a/content.js b/content.js index 08eb177..7f09985 100644 --- a/content.js +++ b/content.js @@ -1,5 +1,35 @@ // Note: Organization ID is now stored in extension settings // Users need to configure it in the extension options page + +// Default model timeline for null models +const DEFAULT_MODEL_TIMELINE = [ + { date: new Date('2024-01-01'), model: 'claude-3-sonnet-20240229' }, // Before June 20, 2024 + { date: new Date('2024-06-20'), model: 'claude-3-5-sonnet-20240620' }, // Starting June 20, 2024 + { date: new Date('2024-10-22'), model: 'claude-3-5-sonnet-20241022' }, // Starting October 22, 2024 + { date: new Date('2025-02-29'), model: 'claude-3-7-sonnet-20250219' }, // Starting February 29, 2025 + { date: new Date('2025-05-14'), model: 'claude-sonnet-4-20250514' } // Starting May 14, 2025 +]; + +// Infer model for conversations with null model based on date +function inferModel(conversation) { + if (conversation.model) { + return conversation.model; + } + + // Use created_at date to determine which default model was active + const conversationDate = new Date(conversation.created_at); + + // Find the appropriate model based on the conversation date + // Start from the end and work backwards to find the right period + for (let i = DEFAULT_MODEL_TIMELINE.length - 1; i >= 0; i--) { + if (conversationDate >= DEFAULT_MODEL_TIMELINE[i].date) { + return DEFAULT_MODEL_TIMELINE[i].model; + } + } + + // If date is before all known dates, use the first model + return DEFAULT_MODEL_TIMELINE[0].model; +} // Fetch conversation data async function fetchConversation(orgId, conversationId) { @@ -175,6 +205,10 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { fetchConversation(request.orgId, request.conversationId) .then(data => { console.log('Conversation data fetched successfully:', data); + + // Infer model if null + data.model = inferModel(data); + let content, filename, type; switch (request.format) { @@ -232,6 +266,10 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { try { console.log(`Fetching full conversation ${count + 1}/${conversations.length}: ${conv.uuid}`); const fullConv = await fetchConversation(request.orgId, conv.uuid); + + // Infer model if null + fullConv.model = inferModel(fullConv); + let content, filename, type; if (request.format === 'markdown') {