Claude API Exporter 0.13

Export Claude conversations using API

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Claude API Exporter 0.13
// @namespace    http://tampermonkey.net/
// @version      0.13
// @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
     * @param {Map} branchArtifacts - Artifacts data from all branches
     * @param {Array} branchInfo - Branch information array
     * @returns {string} Formatted markdown content
     */
    function generateConversationMarkdown(conversationData, branchArtifacts = null, branchInfo = null) {
        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`;
                    
                    // Add version, branch and timestamp info if available
                    if (branchArtifacts && branchInfo) {
                        // Find the specific version for this operation (by timestamp if available)
                        let artifactVersion = null;
                        
                        for (const [branchId, artifactsMap] of branchArtifacts) {
                            if (artifactsMap.has(input.id)) {
                                const versions = artifactsMap.get(input.id);
                                
                                if (content.stop_timestamp) {
                                    // Try to find by exact timestamp
                                    artifactVersion = versions.find(v => 
                                        v.messageUuid === message.uuid && 
                                        v.content_stop_timestamp === content.stop_timestamp
                                    );
                                }
                                
                                // Fallback: find any version in this message
                                if (!artifactVersion) {
                                    artifactVersion = versions.find(v => v.messageUuid === message.uuid);
                                }
                                
                                if (artifactVersion) break;
                            }
                        }
                        
                        if (artifactVersion) {
                            // Find branch info for proper branch label  
                            const branchData = branchInfo.find(b => b.branchId === artifactVersion.branchId);
                            let branchLabel;
                            
                            if (branchData) {
                                if (branchData.isMainBranch) {
                                    branchLabel = `branch${branchData.branchIndex} (main) (${artifactVersion.branchId.substring(0, 8)}...)`;
                                } else {
                                    branchLabel = `branch${branchData.branchIndex} (${artifactVersion.branchId.substring(0, 8)}...)`;
                                }
                            } else {
                                branchLabel = `unknown (${artifactVersion.branchId.substring(0, 8)}...)`;
                            }
                            
                            markdown += `*Version:* ${artifactVersion.version}\n`;
                            markdown += `*Branch:* ${branchLabel}\n`;
                            markdown += `*Created:* ${formatDate(artifactVersion.content_stop_timestamp || artifactVersion.timestamp_created_at)}\n`;
                            
                            // Show change description if available
                            if (artifactVersion.changeDescription) {
                                markdown += `*Change:* ${artifactVersion.changeDescription}\n`;
                            }
                        }
                    }
                    
                    markdown += `\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);

            // Extract and process artifacts from all branches
            const { branchArtifacts, branchInfo } = extractAllArtifacts(conversationData);

            // Export main conversation
            const conversationMarkdown = generateConversationMarkdown(conversationData, branchArtifacts, branchInfo);
            const conversationFilename = `${conversationTimestamp}_${conversationId}__${safeTitle}.md`;
            downloadFile(conversationFilename, conversationMarkdown);

            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);

            // Extract artifacts to get metadata for conversation display
            const { branchArtifacts, branchInfo } = extractAllArtifacts(conversationData);

            // Export only main conversation
            const conversationMarkdown = generateConversationMarkdown(conversationData, branchArtifacts, branchInfo);
            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();
    }
})();