Claude API Exporter 0.12

Export Claude conversations using API

当前为 2025-07-10 提交的版本,查看 最新版本

// ==UserScript==
// @name         Claude API Exporter 0.12
// @namespace    http://tampermonkey.net/
// @version      0.12
// @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
     * @param {string|Date} [dateInput] - Optional date input, defaults to current time
     * @returns {string} Formatted timestamp
     */
    function generateTimestamp(dateInput) {
        const date = dateInput ? 
            (typeof dateInput === 'string' ? new Date(dateInput) : dateInput) : 
            new Date();
        
        // Check if date is valid, fallback to current time
        if (isNaN(date.getTime())) {
            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');
        }
        
        return date.getFullYear() +
               String(date.getMonth() + 1).padStart(2, '0') +
               String(date.getDate()).padStart(2, '0') +
               String(date.getHours()).padStart(2, '0') +
               String(date.getMinutes()).padStart(2, '0') +
               String(date.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({
                            type: input.type,
                            title: input.title || `Artifact ${artifactId}`,
                            command: input.command,
                            content: input.content || '',
                            new_str: input.new_str || '',
                            old_str: input.old_str || '',
                            language: input.language || '',
                            timestamp_created_at: message.created_at,
                            timestamp_updated_at: message.updated_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({
                        messageUuid: message.uuid,
                        messageText: message.text,
                        version: versionNumber,
                        content_start_timestamp: content.start_timestamp,
                        content_stop_timestamp: content.stop_timestamp,
                        content_type: content.type,
                        type: input.type,
                        title: input.title || `Artifact ${artifactId}`,
                        command: input.command,
                        old_str: input.old_str || '',
                        new_str: input.new_str || '',
                        content: input.content || '',
                        uuid: input.version_uuid,
                        language: input.language || '',
                        messageIndex: branchStartIndex + relativeIndex,
                        stop_reason: message.stop_reason,
                        timestamp_created_at: message.created_at,
                        timestamp_updated_at: message.updated_at,
                        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';
                        break;
                    case 'rewrite':
                        currentContent = version.content;
                        currentTitle = version.title;
                        // Keep type and language from create
                        changeDescription = isMainBranch ? 'Rewritten' : 'Rewritten';
                        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.content_stop_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.content_stop_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.startsWith('//')) {
            metadata += '\n// ---\n';
        } else if (start.startsWith('-- ')) {
            metadata += '\n-- ---\n';
        } else if (start.startsWith('<!--')) {
            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 conversationId = conversationData.uuid;
            const safeTitle = sanitizeFileName(conversationData.name);

            // Use updated_at for conversation timestamp
            const conversationTimestamp = generateTimestamp(conversationData.updated_at);

            // Export main conversation
            const conversationMarkdown = generateConversationMarkdown(conversationData);
            const conversationFilename = `${conversationTimestamp}_${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);
                        
                        // Use content_stop_timestamp for artifact timestamp
                        const artifactTimestamp = generateTimestamp(version.content_stop_timestamp);

                        const filename = `${artifactTimestamp}_${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 conversationId = conversationData.uuid;
            const safeTitle = sanitizeFileName(conversationData.name);

            // Use updated_at for conversation timestamp
            const conversationTimestamp = generateTimestamp(conversationData.updated_at);

            // Export only main conversation
            const conversationMarkdown = generateConversationMarkdown(conversationData);
            const conversationFilename = `${conversationTimestamp}_${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();
    }
})();