您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Export Claude conversations using API
当前为
// ==UserScript== // @name Claude API Exporter 0.11++ // @namespace http://tampermonkey.net/ // @version 0.11++ // @description Export Claude conversations using API // @author MRL // @match https://claude.ai/chat/* // @grant GM_registerMenuCommand // @grant GM_download // @license MIT // ==/UserScript== (function() { 'use strict'; // ============================================= // UTILITY FUNCTIONS // ============================================= /** * Generates timestamp in format YYYYMMDDHHMMSS for file naming * @returns {string} Formatted timestamp */ function generateTimestamp() { const now = new Date(); return now.getFullYear() + String(now.getMonth() + 1).padStart(2, '0') + String(now.getDate()).padStart(2, '0') + String(now.getHours()).padStart(2, '0') + String(now.getMinutes()).padStart(2, '0') + String(now.getSeconds()).padStart(2, '0'); } /** * Sanitizes filename by removing invalid characters and limiting length * @param {string} name - Original filename * @returns {string} Sanitized filename safe for file system */ function sanitizeFileName(name) { return name.replace(/[\\/:*?"<>|]/g, '_') .replace(/\s+/g, '_') .replace(/__+/g, '_') .replace(/^_+|_+$/g, '') .slice(0, 100); } /** * Formats date string to localized format * @param {string|Date} dateInput - ISO date string or Date object * @returns {string} Formatted date string */ function formatDate(dateInput) { if (!dateInput) return ''; const date = typeof dateInput === 'string' ? new Date(dateInput) : dateInput; return date.toLocaleString(); } /** * Downloads content as a file using browser's download functionality * @param {string} filename - Name of the file to download * @param {string} content - Content to save in the file */ function downloadFile(filename, content) { const blob = new Blob([content], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; link.click(); setTimeout(() => { URL.revokeObjectURL(url); }, 100); } /** * Shows temporary notification to the user * @param {string} message - Message to display * @param {string} type - Type of notification (info, success, error) */ function showNotification(message, type = "info") { const notification = document.createElement('div'); notification.style.cssText = ` position: fixed; top: 20px; right: 20px; padding: 15px 20px; border-radius: 5px; color: white; font-family: system-ui, -apple-system, sans-serif; font-size: 14px; z-index: 10000; max-width: 400px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); `; if (type === "error") { notification.style.backgroundColor = '#f44336'; } else if (type === "success") { notification.style.backgroundColor = '#4CAF50'; } else { notification.style.backgroundColor = '#2196F3'; } notification.textContent = message; document.body.appendChild(notification); setTimeout(() => { if (notification.parentNode) { notification.parentNode.removeChild(notification); } }, 5000); } // ============================================= // API FUNCTIONS // ============================================= /** * Extracts conversation ID from current URL * @returns {string|null} Conversation ID or null if not found */ function getConversationId() { const match = window.location.pathname.match(/\/chat\/([^/?]+)/); return match ? match[1] : null; } /** * Gets organization ID from browser cookies * @returns {string} Organization ID * @throws {Error} If organization ID not found */ function getOrgId() { const cookies = document.cookie.split(';'); for (const cookie of cookies) { const [name, value] = cookie.trim().split('='); if (name === 'lastActiveOrg') { return value; } } throw new Error('Could not find organization ID'); } /** * Fetches conversation data from Claude API * @returns {Promise<Object>} Complete conversation data including messages and metadata */ async function getConversationData() { const conversationId = getConversationId(); if (!conversationId) { throw new Error('Not in a conversation'); } const orgId = getOrgId(); const response = await fetch(`/api/organizations/${orgId}/chat_conversations/${conversationId}?tree=true&rendering_mode=messages&render_all_tools=true`); if (!response.ok) { throw new Error(`API request failed: ${response.status}`); } return await response.json(); } // ============================================= // FILE EXTENSION FUNCTIONS // ============================================= /** * Gets appropriate file extension based on artifact type and language * @param {string} type - Artifact MIME type * @param {string} language - Programming language (for code artifacts) * @returns {string} File extension including the dot */ function getFileExtension(type, language) { switch (type) { case 'application/vnd.ant.code': return getCodeExtension(language); case 'text/html': return '.html'; case 'text/markdown': return '.md'; case 'image/svg+xml': return '.svg'; case 'application/vnd.ant.mermaid': return '.mmd'; case 'application/vnd.ant.react': return '.jsx'; case undefined: default: return '.txt'; } } /** * Maps programming language names to file extensions * @param {string} language - Programming language name * @returns {string} File extension including the dot */ function getCodeExtension(language) { const extensionMap = { // Web languages 'javascript': '.js', 'typescript': '.ts', 'html': '.html', 'css': '.css', 'scss': '.scss', 'sass': '.sass', 'less': '.less', 'jsx': '.jsx', 'tsx': '.tsx', 'vue': '.vue', // Languages 'python': '.py', 'java': '.java', 'csharp': '.cs', 'c#': '.cs', 'cpp': '.cpp', 'c++': '.cpp', 'c': '.c', 'go': '.go', 'rust': '.rs', 'swift': '.swift', 'kotlin': '.kt', 'dart': '.dart', 'php': '.php', 'ruby': '.rb', 'perl': '.pl', 'lua': '.lua', // Functional languages 'haskell': '.hs', 'clojure': '.clj', 'erlang': '.erl', 'elixir': '.ex', 'fsharp': '.fs', 'f#': '.fs', 'ocaml': '.ml', 'scala': '.scala', 'lisp': '.lisp', // Data and config 'json': '.json', 'yaml': '.yaml', 'yml': '.yml', 'xml': '.xml', 'toml': '.toml', 'ini': '.ini', 'csv': '.csv', // Query languages 'sql': '.sql', 'mysql': '.sql', 'postgresql': '.sql', 'sqlite': '.sql', 'plsql': '.sql', // Shell and scripting 'bash': '.sh', 'shell': '.sh', 'sh': '.sh', 'zsh': '.zsh', 'fish': '.fish', 'powershell': '.ps1', 'batch': '.bat', 'cmd': '.cmd', // Scientific and specialized 'r': '.r', 'matlab': '.m', 'julia': '.jl', 'fortran': '.f90', 'cobol': '.cob', 'assembly': '.asm', 'vhdl': '.vhd', 'verilog': '.v', // Build and config files 'dockerfile': '.dockerfile', 'makefile': '.mk', 'cmake': '.cmake', 'gradle': '.gradle', 'maven': '.xml', // Markup and documentation 'markdown': '.md', 'latex': '.tex', 'restructuredtext': '.rst', 'asciidoc': '.adoc', // Other 'regex': '.regex', 'text': '.txt', 'plain': '.txt' }; const normalizedLanguage = language ? language.toLowerCase().trim() : ''; return extensionMap[normalizedLanguage] || '.txt'; } /** * Gets comment style for a programming language * @param {string} type - Artifact MIME type * @param {string} language - Programming language * @returns {Object} Comment style object with start and end strings */ function getCommentStyle(type, language) { if (type === 'text/html') { return { start: '<!-- ', end: ' -->' }; } if (type === 'image/svg+xml') { return { start: '<!-- ', end: ' -->' }; } if (type !== 'application/vnd.ant.code') { return { start: '# ', end: '' }; // Default to hash comments } const normalizedLanguage = language ? language.toLowerCase().trim() : ''; // Languages with // comments const slashCommentLanguages = [ 'javascript', 'typescript', 'java', 'csharp', 'c#', 'cpp', 'c++', 'c', 'go', 'rust', 'swift', 'kotlin', 'dart', 'php', 'scala', 'jsx', 'tsx' ]; // Languages with # comments const hashCommentLanguages = [ 'python', 'ruby', 'perl', 'bash', 'shell', 'sh', 'zsh', 'fish', 'yaml', 'yml', 'r', 'julia', 'toml', 'ini', 'powershell' ]; // Languages with -- comments const dashCommentLanguages = [ 'sql', 'mysql', 'postgresql', 'sqlite', 'plsql', 'haskell', 'lua' ]; if (slashCommentLanguages.includes(normalizedLanguage)) { return { start: '// ', end: '' }; } else if (hashCommentLanguages.includes(normalizedLanguage)) { return { start: '# ', end: '' }; } else if (dashCommentLanguages.includes(normalizedLanguage)) { return { start: '-- ', end: '' }; } // Default to hash comments return { start: '# ', end: '' }; } // ============================================= // BRANCH HANDLING FUNCTIONS // ============================================= /** * Builds conversation tree structure to understand message branches * @param {Array} messages - Array of chat messages * @returns {Object} Tree structure with branch information */ function buildConversationTree(messages) { const messageMap = new Map(); const rootMessages = []; // First pass: create message map messages.forEach(message => { messageMap.set(message.uuid, { ...message, children: [], branchId: null, branchIndex: null }); }); // Second pass: build parent-child relationships messages.forEach(message => { const messageNode = messageMap.get(message.uuid); if (message.parent_message_uuid && messageMap.has(message.parent_message_uuid)) { const parent = messageMap.get(message.parent_message_uuid); parent.children.push(messageNode); } else { rootMessages.push(messageNode); } }); return { messageMap, rootMessages }; } /** * Gets all branch information including branch points * @param {Object} tree - Tree structure from buildConversationTree * @returns {Array} Array of branch information */ function getAllBranchInfo(tree) { const branches = []; let branchCounter = 0; function traverseBranches(node, currentPath = [], branchStartIndex = 0) { const newPath = [...currentPath, node]; if (node.children.length === 0) { // Leaf node - this is a complete branch branchCounter++; branches.push({ branchId: node.uuid, branchIndex: branchCounter, fullPath: newPath, branchStartIndex: branchStartIndex, // Index in fullPath where this branch starts isMainBranch: branchStartIndex === 0 }); } else if (node.children.length === 1) { // Single child - continue same branch traverseBranches(node.children[0], newPath, branchStartIndex); } else { // Multiple children - branch point node.children.forEach((child, childIndex) => { // For first child, continue current branch // For other children, start new branches from this point const newBranchStart = childIndex === 0 ? branchStartIndex : newPath.length; traverseBranches(child, newPath, newBranchStart); }); } } tree.rootMessages.forEach(root => { traverseBranches(root, [], 0); }); return branches; } // ============================================= // TEXT PROCESSING FUNCTIONS // ============================================= /** * Recursively extracts text content from nested content structures * @param {Object} content - Content object to process * @returns {Promise<Array<string>>} Array of text pieces found */ async function getTextFromContent(content) { let textPieces = []; if (content.text) { textPieces.push(content.text); } if (content.input) { textPieces.push(JSON.stringify(content.input)); } if (content.content) { if (Array.isArray(content.content)) { for (const nestedContent of content.content) { textPieces = textPieces.concat(await getTextFromContent(nestedContent)); } } else if (typeof content.content === 'object') { textPieces = textPieces.concat(await getTextFromContent(content.content)); } } return textPieces; } // ============================================= // ARTIFACT PROCESSING FUNCTIONS // ============================================= /** * Extracts artifacts from messages, respecting branch boundaries * @param {Array} branchPath - Full path from root to leaf * @param {number} branchStartIndex - Index where this branch starts (for split branches) * @param {string} branchId - Unique identifier for this branch * @param {boolean} isMainBranch - Whether this is the main branch * @returns {Object} {ownArtifacts: Map, inheritedStates: Map} */ function extractArtifacts(branchPath, branchStartIndex, branchId, isMainBranch) { const ownArtifacts = new Map(); // Artifacts created/modified in this branch const inheritedStates = new Map(); // Final states of artifacts from parent branch // For non-main branches, first collect inherited states from parent path if (!isMainBranch && branchStartIndex > 0) { const parentPath = branchPath.slice(0, branchStartIndex); const parentArtifacts = new Map(); // Extract artifacts from parent path parentPath.forEach((message, messageIndex) => { message.content.forEach(content => { if (content.type === 'tool_use' && content.name === 'artifacts' && content.input) { const input = content.input; const artifactId = input.id; if (!parentArtifacts.has(artifactId)) { parentArtifacts.set(artifactId, []); } const versions = parentArtifacts.get(artifactId); versions.push({ command: input.command, content: input.content || '', old_str: input.old_str || '', new_str: input.new_str || '', title: input.title || `Artifact ${artifactId}`, type: input.type, language: input.language || '', timestamp: message.created_at }); } }); }); // Build final states from parent artifacts parentArtifacts.forEach((versions, artifactId) => { let currentContent = ''; let currentTitle = `Artifact ${artifactId}`; let currentType = undefined; let currentLanguage = ''; let versionCount = 0; versions.forEach(version => { versionCount++; switch (version.command) { case 'create': currentContent = version.content; currentTitle = version.title; currentType = version.type; currentLanguage = version.language; break; case 'rewrite': currentContent = version.content; currentTitle = version.title; // Keep type and language from create break; case 'update': const updateResult = applyUpdate(currentContent, version.old_str, version.new_str); currentContent = updateResult.content; break; } }); inheritedStates.set(artifactId, { content: currentContent, title: currentTitle, type: currentType, language: currentLanguage, versionCount: versionCount }); }); } // Now extract artifacts from this branch only (starting from branchStartIndex) const branchMessages = branchPath.slice(branchStartIndex); branchMessages.forEach((message, relativeIndex) => { message.content.forEach(content => { if (content.type === 'tool_use' && content.name === 'artifacts' && content.input) { const input = content.input; const artifactId = input.id; if (!ownArtifacts.has(artifactId)) { ownArtifacts.set(artifactId, []); } const versions = ownArtifacts.get(artifactId); // Calculate version number based on inherited versions let versionNumber; if (isMainBranch) { // Main branch: start from 1 versionNumber = versions.length + 1; } else { // Split branch: continue from parent version count const inheritedCount = inheritedStates.has(artifactId) ? inheritedStates.get(artifactId).versionCount : 0; versionNumber = inheritedCount + versions.length + 1; } versions.push({ version: versionNumber, command: input.command, uuid: input.version_uuid, content: input.content || '', old_str: input.old_str || '', new_str: input.new_str || '', title: input.title || `Artifact ${artifactId}`, type: input.type, language: input.language || '', timestamp: message.created_at, messageUuid: message.uuid, messageIndex: branchStartIndex + relativeIndex, branchId: branchId }); } }); }); return { ownArtifacts, inheritedStates }; } /** * Applies update command to previous content by replacing old_str with new_str * @param {string} previousContent - Content before update * @param {string} oldStr - String to be replaced * @param {string} newStr - String to replace with * @returns {Object} {success: boolean, content: string, info: string} */ function applyUpdate(previousContent, oldStr, newStr) { if (!previousContent || !oldStr) { return { success: false, content: previousContent || '', info: 'Cannot apply update: missing previousContent or oldStr' }; } // Apply the string replacement const updatedContent = previousContent.replace(oldStr, newStr); if (updatedContent === previousContent) { // Try to find similar strings for debugging const lines = previousContent.split('\n'); const oldLines = oldStr.split('\n'); let debugInfo = 'Update did not change content - old string not found'; if (oldLines.length > 0) { const firstOldLine = oldLines[0].trim(); const foundLine = lines.find(line => line.includes(firstOldLine)); if (foundLine) { debugInfo += ` | Found similar line: "${foundLine.trim()}"`; } } return { success: false, content: previousContent, info: debugInfo }; } return { success: true, content: updatedContent, info: `Successfully applied update` }; } /** * Builds complete artifact versions for a specific branch * @param {Map} ownArtifacts - Artifacts created/modified in this branch * @param {Map} inheritedStates - Final states from parent branch * @param {string} branchId - Branch identifier * @param {boolean} isMainBranch - Whether this is the main branch * @returns {Map} Map of artifact ID to processed versions with full content */ function buildArtifactVersions(ownArtifacts, inheritedStates, branchId, isMainBranch) { const processedArtifacts = new Map(); ownArtifacts.forEach((versions, artifactId) => { const processedVersions = []; // Start with inherited content if this is a branch let currentContent = ''; let currentTitle = `Artifact ${artifactId}`; let currentType = undefined; let currentLanguage = ''; if (!isMainBranch && inheritedStates.has(artifactId)) { const inherited = inheritedStates.get(artifactId); currentContent = inherited.content; currentTitle = inherited.title; currentType = inherited.type; currentLanguage = inherited.language; } versions.forEach((version, index) => { let changeDescription = ''; let updateInfo = ''; let versionStartContent = currentContent; switch (version.command) { case 'create': currentContent = version.content; currentTitle = version.title; currentType = version.type; currentLanguage = version.language; changeDescription = isMainBranch ? 'Created' : 'Created (overriding inherited)'; break; case 'rewrite': currentContent = version.content; currentTitle = version.title; // Keep type and language from create changeDescription = isMainBranch ? 'Rewritten' : 'Rewritten (from inherited)'; break; case 'update': const oldContent = currentContent; const updateResult = applyUpdate(currentContent, version.old_str, version.new_str); currentContent = updateResult.content; updateInfo = updateResult.info; // Create more informative change description const oldPreview = version.old_str ? version.old_str.substring(0, 50).replace(/\n/g, '\\n') + '...' : ''; const newPreview = version.new_str ? version.new_str.substring(0, 50).replace(/\n/g, '\\n') + '...' : ''; changeDescription = `Updated: "${oldPreview}" → "${newPreview}"`; // Add information about character count changes const oldLength = oldContent.length; const newLength = currentContent.length; const lengthDiff = newLength - oldLength; if (lengthDiff > 0) { changeDescription += ` (+${lengthDiff} chars)`; } else if (lengthDiff < 0) { changeDescription += ` (${lengthDiff} chars)`; } if (!updateResult.success) { changeDescription += ` [WARNING: ${updateResult.info}]`; } break; default: console.warn(`Unknown command: ${version.command}`); break; } processedVersions.push({ ...version, fullContent: currentContent, changeDescription: changeDescription, updateInfo: updateInfo, branchId: branchId, isMainBranch: isMainBranch, inheritedContent: versionStartContent, finalType: currentType, finalLanguage: currentLanguage }); }); processedArtifacts.set(artifactId, processedVersions); }); return processedArtifacts; } /** * Extracts and processes all artifacts from all branches with proper inheritance * @param {Object} conversationData - Complete conversation data * @returns {Object} {branchArtifacts: Map, branchInfo: Array} */ function extractAllArtifacts(conversationData) { // Build conversation tree const tree = buildConversationTree(conversationData.chat_messages); const branches = getAllBranchInfo(tree); console.log(`Found ${branches.length} conversation branches`); const branchArtifacts = new Map(); // branchId -> Map<artifactId, versions> const branchInfo = []; branches.forEach((branch) => { const { ownArtifacts, inheritedStates } = extractArtifacts( branch.fullPath, branch.branchStartIndex, branch.branchId, branch.isMainBranch ); if (ownArtifacts.size > 0) { // Process artifacts for this branch const processedArtifacts = buildArtifactVersions( ownArtifacts, inheritedStates, branch.branchId, branch.isMainBranch ); branchArtifacts.set(branch.branchId, processedArtifacts); const leafMessage = branch.fullPath[branch.fullPath.length - 1]; branchInfo.push({ branchId: branch.branchId, branchIndex: branch.branchIndex, messageCount: branch.fullPath.length, branchMessageCount: branch.fullPath.length - branch.branchStartIndex, artifactCount: ownArtifacts.size, inheritedArtifactCount: inheritedStates.size, lastMessageTime: leafMessage.created_at, lastMessageUuid: leafMessage.uuid, isMainBranch: branch.isMainBranch, branchStartIndex: branch.branchStartIndex }); } }); return { branchArtifacts, branchInfo }; } // ============================================= // VERSION TRACKING FUNCTIONS // ============================================= /** * Builds version information for messages with alternatives (same parent) * @param {Array} messages - Array of chat messages * @returns {Map} Map of message UUID to version info {version, total} */ function buildVersionInfo(messages) { const versionInfo = new Map(); // Group messages by parent_message_uuid const parentGroups = new Map(); messages.forEach(message => { if (message.parent_message_uuid) { if (!parentGroups.has(message.parent_message_uuid)) { parentGroups.set(message.parent_message_uuid, []); } parentGroups.get(message.parent_message_uuid).push(message); } }); // Process groups with more than one message (alternatives) parentGroups.forEach((siblings, parentUuid) => { if (siblings.length > 1) { // Sort by created_at to determine version numbers siblings.sort((a, b) => new Date(a.created_at) - new Date(b.created_at)); siblings.forEach((message, index) => { versionInfo.set(message.uuid, { version: index + 1, total: siblings.length }); }); } }); return versionInfo; } // ============================================= // EXPORT FUNCTIONS // ============================================= /** * Generates markdown content for the entire conversation * @param {Object} conversationData - Complete conversation data from API * @returns {string} Formatted markdown content */ function generateConversationMarkdown(conversationData) { let markdown = ''; // Header with conversation metadata markdown += `# ${conversationData.name}\n`; // Project info (if available) if (conversationData.project) { markdown += `*Project:* [${conversationData.project.name}] (https://claude.ai/project/${conversationData.project.uuid})\n`; } markdown += `*URL:* https://claude.ai/chat/${conversationData.uuid}\n`; // Format dates properly markdown += `*Created:* ${formatDate(conversationData.created_at)}\n`; markdown += `*Updated:* ${formatDate(conversationData.updated_at)}\n`; markdown += `*Exported:* ${formatDate(new Date())}\n`; if (conversationData.model) { markdown += `*Model:* \`${conversationData.model}\`\n`; } markdown += `\n`; // Build version info for messages with alternatives const versionInfo = buildVersionInfo(conversationData.chat_messages); // Process each message conversationData.chat_messages.forEach(message => { const role = message.sender === 'human' ? 'Human' : 'Claude'; markdown += `__________\n\n`; markdown += `## ${role}\n`; markdown += `*UUID:* \`${message.uuid}\`\n`; markdown += `*Created:* ${formatDate(message.created_at)}\n`; // Add version info if this message has alternatives if (versionInfo.has(message.uuid)) { const info = versionInfo.get(message.uuid); markdown += `*Version:* ${info.version} of ${info.total}\n`; } markdown += `\n`; // Process message content message.content.forEach(content => { if (content.type === 'text') { markdown += content.text + '\n\n'; } else if (content.type === 'tool_use' && content.name === 'artifacts') { const input = content.input; if (input.title) { markdown += `**Artifact Created:** ${input.title}\n`; } markdown += `*ID:* \`${input.id}\`\n`; markdown += `*Command:* \`${input.command}\`\n\n`; } else if (content.type === 'thinking') { if (content.thinking) { markdown += `*[Claude thinking...]*\n\n`; markdown += `<details>\n<summary>Thinking process</summary>\n\n`; markdown += content.thinking + '\n\n'; markdown += `</details>\n\n`; } else { markdown += `*[Claude thinking...]*\n\n`; } } }); // Process attachments if present if (message.attachments && message.attachments.length > 0) { message.attachments.forEach(attachment => { markdown += `**Attachment:** ${attachment.file_name}\n`; markdown += `*ID:* \`${attachment.id}\`\n\n`; if (attachment.extracted_content) { markdown += `<details>\n<summary>File content</summary>\n\n`; markdown += '```\n'; markdown += attachment.extracted_content + '\n'; markdown += '```\n\n'; markdown += `</details>\n\n`; } }); } }); return markdown; } /** * Formats artifact metadata as comments in the appropriate style * @param {Object} version - Version object with metadata * @param {string} artifactId - Artifact ID * @param {string} branchLabel - Branch label * @param {boolean} isMain - Whether this is the main branch * @returns {string} Formatted metadata comments */ function formatArtifactMetadata(version, artifactId, branchLabel, isMain) { // Special formatting for markdown files if (version.finalType === 'text/markdown') { let metadata = ''; metadata += `*Artifact ID:* \`${artifactId}\`\n`; metadata += `*Branch:* ${branchLabel}${isMain ? ' (main)' : ''} (${version.branchId.substring(0, 8)}...)\n`; metadata += `*Version:* ${version.version}\n`; metadata += `*Command:* \`${version.command}\`\n`; metadata += `*UUID:* \`${version.uuid}\`\n`; metadata += `*Created:* ${formatDate(version.timestamp)}\n`; if (version.changeDescription) { metadata += `*Change:* ${version.changeDescription}\n`; } if (version.updateInfo) { metadata += `*Update Info:* ${version.updateInfo}\n`; } if (!isMain && version.inheritedContent && version.command === 'update') { metadata += `*Started from inherited content:* ${version.inheritedContent.length} chars\n`; } metadata += '\n---\n'; return metadata; } // For all other file types, use comments const commentStyle = getCommentStyle(version.finalType, version.finalLanguage); const { start, end } = commentStyle; let metadata = ''; metadata += `${start}Artifact ID: ${artifactId}${end}\n`; metadata += `${start}Branch: ${branchLabel}${isMain ? ' (main)' : ''} (${version.branchId.substring(0, 8)}...)${end}\n`; metadata += `${start}Version: ${version.version}${end}\n`; metadata += `${start}Command: ${version.command}${end}\n`; metadata += `${start}UUID: ${version.uuid}${end}\n`; metadata += `${start}Created: ${formatDate(version.timestamp)}${end}\n`; if (version.changeDescription) { metadata += `${start}Change: ${version.changeDescription}${end}\n`; } if (version.updateInfo) { metadata += `${start}Update Info: ${version.updateInfo}${end}\n`; } if (!isMain && version.inheritedContent && version.command === 'update') { metadata += `${start}Started from inherited content: ${version.inheritedContent.length} chars${end}\n`; } // Add separator based on language if (start.includes('//')) { metadata += '\n// ---\n'; } else if (start.includes('--')) { metadata += '\n-- ---\n'; } else if (start.includes('<!--')) { metadata += '\n<!-- --- -->\n'; } else { metadata += '\n# ---\n'; } return metadata; } /** * Exports conversation with artifacts (all versions or final versions only) * @param {boolean} finalVersionsOnly - If true, exports only final artifact versions */ async function exportConversation(finalVersionsOnly = false) { try { showNotification('Fetching conversation data...', 'info'); const conversationData = await getConversationData(); const timestamp = generateTimestamp(); const conversationId = conversationData.uuid; const safeTitle = sanitizeFileName(conversationData.name); // Export main conversation const conversationMarkdown = generateConversationMarkdown(conversationData); const conversationFilename = `${timestamp}_${conversationId}__${safeTitle}.md`; downloadFile(conversationFilename, conversationMarkdown); // Extract and process artifacts from all branches const { branchArtifacts, branchInfo } = extractAllArtifacts(conversationData); if (branchArtifacts.size === 0) { showNotification('No artifacts found in conversation', 'info'); return; } let totalExported = 0; // Export artifacts for each branch branchArtifacts.forEach((artifactsMap, branchId) => { const branchData = branchInfo.find(b => b.branchId === branchId); const branchLabel = branchData ? `branch${branchData.branchIndex}` : 'unknown'; const isMain = branchData ? branchData.isMainBranch : false; artifactsMap.forEach((versions, artifactId) => { const versionsToExport = finalVersionsOnly ? [versions[versions.length - 1]] : // Only last version versions; // All versions versionsToExport.forEach(version => { const safeArtifactTitle = sanitizeFileName(version.title); const mainSuffix = isMain ? '_main' : ''; const fileExtension = getFileExtension(version.finalType, version.finalLanguage); const filename = `${timestamp}_${conversationId}_${artifactId}_${branchLabel}${mainSuffix}_v${version.version}_${safeArtifactTitle}${fileExtension}`; // Format metadata as comments const metadata = formatArtifactMetadata(version, artifactId, branchLabel, isMain); // Combine metadata and content const content = metadata + '\n' + version.fullContent; downloadFile(filename, content); totalExported++; }); }); }); const mode = finalVersionsOnly ? 'final versions' : 'all versions'; const branchCount = branchArtifacts.size; const mainBranches = branchInfo.filter(b => b.isMainBranch).length; const splitBranches = branchCount - mainBranches; showNotification(`Export completed! Downloaded conversation + ${totalExported} artifacts from ${branchCount} branches (${mainBranches} main, ${splitBranches} split) (${mode})`, 'success'); } catch (error) { console.error('Export failed:', error); showNotification(`Export failed: ${error.message}`, 'error'); } } /** * Exports only the conversation without any artifacts */ async function exportConversationOnly() { try { showNotification('Fetching conversation data...', 'info'); const conversationData = await getConversationData(); const timestamp = generateTimestamp(); const conversationId = conversationData.uuid; const safeTitle = sanitizeFileName(conversationData.name); // Export only main conversation const conversationMarkdown = generateConversationMarkdown(conversationData); const conversationFilename = `${timestamp}_${conversationId}__${safeTitle}.md`; downloadFile(conversationFilename, conversationMarkdown); showNotification('Conversation exported successfully!', 'success'); } catch (error) { console.error('Export failed:', error); showNotification(`Export failed: ${error.message}`, 'error'); } } // ============================================= // INITIALIZATION // ============================================= /** * Initializes the script and registers menu commands */ function init() { console.log('[Claude API Exporter] Initializing...'); // Register menu commands GM_registerMenuCommand('Export Conversation Only', exportConversationOnly); GM_registerMenuCommand('Export Conversation + Final Artifact Versions', () => exportConversation(true)); GM_registerMenuCommand('Export Conversation + All Artifact Versions', () => exportConversation(false)); } // Start when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();