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 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==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();

})();