Greasy Fork

Greasy Fork is available in English.

Claude Project Files Extractor

Download/extract all files from a Claude project as a single ZIP

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

// ==UserScript==
// @name         Claude Project Files Extractor
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  Download/extract all files from a Claude project as a single ZIP
// @author       sharmanhall
// @match        https://claude.ai/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // Load JSZip from CDN first
    function loadJSZip() {
        return new Promise((resolve, reject) => {
            if (typeof JSZip !== 'undefined') {
                console.log('JSZip already available');
                resolve();
                return;
            }
            
            console.log('Loading JSZip from CDN...');
            const script = document.createElement('script');
            script.src = 'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js';
            script.onload = () => {
                console.log('JSZip script loaded');
                // Wait a bit for it to be available
                setTimeout(() => {
                    if (typeof JSZip !== 'undefined') {
                        console.log('JSZip is now available');
                        resolve();
                    } else {
                        reject(new Error('JSZip loaded but not available'));
                    }
                }, 500);
            };
            script.onerror = () => {
                console.error('Failed to load JSZip');
                reject(new Error('Failed to load JSZip'));
            };
            document.head.appendChild(script);
        });
    }

    // Helper function to wait for modal to close
    async function waitForModalClose(timeout = 3000) {
        const startTime = Date.now();
        while (Date.now() - startTime < timeout) {
            const modal = document.querySelector('[role="dialog"]');
            if (!modal) return true;
            await new Promise(resolve => setTimeout(resolve, 100));
        }
        return false;
    }

    // Function to close modal
    async function closeModal() {
        // Try multiple close methods
        const closeButtons = document.querySelectorAll('button[aria-label*="close"], button[aria-label*="Close"], [data-testid*="close"]');
        for (const btn of closeButtons) {
            btn.click();
            await new Promise(resolve => setTimeout(resolve, 200));
        }
        
        // Press Escape
        document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
        document.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape', bubbles: true }));
        
        // Wait for modal to close
        await waitForModalClose();
    }

    // Extract project knowledge files
    async function extractProjectFiles() {
        const files = [];
        
        console.log('🔍 Looking for project knowledge files...');
        
        // Find file cards in the sidebar
        const fileCards = document.querySelectorAll('div[class*="cursor-pointer"], button[class*="cursor-pointer"], [role="button"]');
        
        for (const card of fileCards) {
            try {
                const text = card.textContent;
                
                // Look for cards with file indicators
                if (text.includes('PDF') || text.includes('TEXT') || text.includes('lines')) {
                    console.log(`📄 Found file card: ${text.substring(0, 50)}...`);
                    
                    // Extract file info using better regex
                    let title = 'Unknown File';
                    let fileType = 'txt';
                    
                    // Try to parse the file card text
                    const cleanText = text.replace(/\s+/g, ' ').trim();
                    
                    // Look for patterns like "filename.pdf214 linespdf" 
                    const match = cleanText.match(/^(.+?)(\d+\s*lines\s*(pdf|text))/i);
                    if (match) {
                        title = match[1].trim();
                        fileType = match[3] === 'pdf' ? 'pdf.txt' : 'txt';
                    }
                    
                    // Special handling for project instructions/prompts (based on title only for now)
                    if (title.includes('Project Assistant') || 
                        title.includes('When responding to requests') ||
                        title.includes('specialized project assistant') ||
                        title.includes('You are a specialized') ||
                        title.toLowerCase().includes('prompt')) {
                        title = 'Project Instructions';
                        fileType = 'txt';
                    }
                    
                    // Clean up long titles generically
                    if (title.length > 60) {
                        // Extract meaningful parts from long titles
                        const words = title.split(' ');
                        if (words.length > 8) {
                            // Take first few meaningful words and last word if it looks like a file type
                            const start = words.slice(0, 4).join(' ');
                            const end = words[words.length - 1];
                            if (end.includes('.') || end.toLowerCase().includes('pdf') || end.toLowerCase().includes('doc')) {
                                title = `${start}...${end}`;
                            } else {
                                title = `${start}...`;
                            }
                        }
                    }
                    
                    console.log(`⚡ Clicking: ${title}`);
                    card.click();
                    
                    // Wait for modal to load
                    await new Promise(resolve => setTimeout(resolve, 2000));
                    
                    // Extract content from modal
                    let content = '';
                    const modal = document.querySelector('[role="dialog"]');
                    if (modal) {
                        // Get all text but filter out UI elements
                        const allText = modal.textContent;
                        const lines = allText.split('\n')
                            .map(line => line.trim())
                            .filter(line => line.length > 3)
                            .filter(line => !line.match(/^(Close|Download|Export|PDF|TEXT|\d+\s*lines|Select file)$/i));
                        
                        if (lines.length > 10) {
                            content = lines.join('\n');
                        }
                    }
                    
                    // Special handling for project instructions/prompts (check content after extraction)
                    if (content.includes('Project Assistant Prompt') || 
                        content.includes('specialized project assistant') ||
                        content.includes('Your Mission') ||
                        content.includes('You are a specialized') ||
                        (content.includes('prompt') && content.includes('assistant'))) {
                        title = 'Project Instructions';
                        fileType = 'txt';
                    }
                    
                    console.log(`✅ Extracted ${content.length} characters from: ${title}`);
                    
                    if (content.length > 50) {
                        files.push({
                            filename: `${title.replace(/[^a-zA-Z0-9\s\-_\.]/g, '_')}.${fileType}`,
                            content: content
                        });
                    }
                    
                    // Close modal
                    await closeModal();
                    await new Promise(resolve => setTimeout(resolve, 1000));
                }
            } catch (error) {
                console.error('Error processing file:', error);
            }
        }
        
        return files;
    }

    // Create and download ZIP
    async function createZIP(files, projectName) {
        try {
            console.log('📦 Creating ZIP with JSZip...');
            
            if (typeof JSZip === 'undefined') {
                throw new Error('JSZip not available');
            }
            
            const zip = new JSZip();
            
            // Add each file to ZIP
            files.forEach(file => {
                console.log(`📁 Adding to ZIP: ${file.filename}`);
                zip.file(file.filename, file.content);
            });
            
            // Add metadata
            const metadata = {
                exportDate: new Date().toISOString(),
                projectTitle: projectName,
                url: window.location.href,
                fileCount: files.length
            };
            
            zip.file('_metadata.json', JSON.stringify(metadata, null, 2));
            
            console.log('🔄 Generating ZIP blob...');
            
            // Generate ZIP
            const zipBlob = await zip.generateAsync({
                type: "blob",
                compression: "DEFLATE",
                compressionOptions: { level: 6 }
            });
            
            console.log(`✅ ZIP created! Size: ${zipBlob.size} bytes`);
            
            // Download ZIP
            const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 16);
            const filename = `${projectName.replace(/[^a-zA-Z0-9]/g, '_')}_export_${timestamp}.zip`;
            
            const url = URL.createObjectURL(zipBlob);
            const link = document.createElement('a');
            link.href = url;
            link.download = filename;
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
            URL.revokeObjectURL(url);
            
            return true;
            
        } catch (error) {
            console.error('❌ ZIP creation failed:', error);
            return false;
        }
    }

    // Download individual files as fallback
    function downloadIndividualFiles(files, projectName) {
        console.log('📥 Falling back to individual downloads...');
        
        files.forEach((file, index) => {
            setTimeout(() => {
                const blob = new Blob([file.content], { type: 'text/plain' });
                const url = URL.createObjectURL(blob);
                const link = document.createElement('a');
                link.href = url;
                link.download = file.filename;
                document.body.appendChild(link);
                link.click();
                document.body.removeChild(link);
                URL.revokeObjectURL(url);
            }, index * 300); // Stagger downloads
        });
    }

    // Get project title
    function getProjectTitle() {
        const titleSelectors = ['h1', '[data-testid*="title"]', '.text-xl'];
        for (const selector of titleSelectors) {
            const element = document.querySelector(selector);
            if (element && element.textContent.trim()) {
                return element.textContent.trim();
            }
        }
        return 'Claude_Project';
    }

    // Main export function
    async function exportProject() {
        const button = document.querySelector('#claude-export-btn');
        
        try {
            // Update button status
            const updateStatus = (msg) => {
                if (button) button.textContent = `🔄 ${msg}`;
                console.log(msg);
            };
            
            updateStatus('Loading ZIP library...');
            await loadJSZip();
            
            updateStatus('Finding files...');
            const files = await extractProjectFiles();
            
            if (files.length === 0) {
                updateStatus('❌ No files found');
                setTimeout(() => {
                    if (button) button.textContent = '📁 Export Project Files';
                }, 2000);
                return;
            }
            
            const projectName = getProjectTitle();
            updateStatus(`Creating ZIP (${files.length} files)...`);
            
            const zipSuccess = await createZIP(files, projectName);
            
            if (zipSuccess) {
                updateStatus(`✅ ZIP exported! (${files.length} files)`);
                setTimeout(() => {
                    if (button) button.textContent = '📁 Export Project Files';
                }, 3000);
            } else {
                updateStatus('ZIP failed - downloading individual files...');
                downloadIndividualFiles(files, projectName);
                setTimeout(() => {
                    if (button) button.textContent = '📁 Export Project Files';
                }, 3000);
            }
            
        } catch (error) {
            console.error('💥 Export failed:', error);
            if (button) button.textContent = '❌ Export Failed';
            setTimeout(() => {
                if (button) button.textContent = '📁 Export Project Files';
            }, 3000);
        }
    }

    // Add export button
    function addExportButton() {
        const existingButton = document.querySelector('#claude-export-btn');
        if (existingButton) existingButton.remove();

        const button = document.createElement('button');
        button.id = 'claude-export-btn';
        button.textContent = '📁 Export Project Files';
        button.style.cssText = `
            position: fixed;
            bottom: 20px;
            right: 20px;
            padding: 12px 20px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border: none;
            border-radius: 8px;
            cursor: pointer;
            z-index: 10000;
            font-size: 14px;
            font-weight: 600;
            box-shadow: 0 4px 15px rgba(0,0,0,0.2);
            transition: all 0.3s ease;
        `;
        
        button.addEventListener('click', exportProject);
        document.body.appendChild(button);
    }

    // Initialize
    function init() {
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', addExportButton);
        } else {
            addExportButton();
        }
        
        // Re-add button on navigation
        let currentUrl = location.href;
        const observer = new MutationObserver(() => {
            if (location.href !== currentUrl) {
                currentUrl = location.href;
                setTimeout(addExportButton, 1000);
            }
        });
        
        observer.observe(document.body, { childList: true, subtree: true });
    }

    init();

})();