Greasy Fork is available in English.
Download/extract all files from a Claude project as a single ZIP
当前为
// ==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();
})();