// ==UserScript==
// @name Claude Exporter 0.5
// @namespace http://tampermonkey.net/
// @version 0.5
// @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
function generateTimestamp() {
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');
}
function sanitizeFileName(name) {
return name.replace(/[\\/:*?"<>|]/g, '_')
.replace(/\s+/g, '_')
.replace(/__+/g, '_')
.replace(/^_+|_+$/g, '')
.slice(0, 100);
}
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);
}
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
function getConversationId() {
const match = window.location.pathname.match(/\/chat\/([^/?]+)/);
return match ? match[1] : null;
}
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');
}
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();
}
// Text processing functions
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
function extractArtifacts(conversationData) {
const artifacts = new Map(); // Map<artifactId, Array<{version, command, uuid, content, title, old_str, new_str}>>
conversationData.chat_messages.forEach(message => {
message.content.forEach(content => {
if (content.type === 'tool_use' && content.name === 'artifacts' && content.input) {
const input = content.input;
const artifactId = input.id;
if (!artifacts.has(artifactId)) {
artifacts.set(artifactId, []);
}
const versions = artifacts.get(artifactId);
versions.push({
version: versions.length + 1,
command: input.command,
uuid: input.version_uuid,
content: input.content || '',
old_str: input.old_str || '',
new_str: input.new_str || '',
title: input.title || `Artifact ${artifactId}`,
timestamp: message.created_at
});
}
});
});
return artifacts;
}
function applyUpdate(previousContent, oldStr, newStr) {
if (!previousContent || !oldStr) {
console.warn('Cannot apply update: missing previousContent or oldStr');
return previousContent || '';
}
// Apply the string replacement
const updatedContent = previousContent.replace(oldStr, newStr);
if (updatedContent === previousContent) {
console.warn('Update did not change content - old string not found');
console.warn('Looking for:', oldStr.substring(0, 100) + '...');
console.warn('In content length:', previousContent.length);
const lines = previousContent.split('\n');
const oldLines = oldStr.split('\n');
if (oldLines.length > 0) {
const firstOldLine = oldLines[0].trim();
const foundLine = lines.find(line => line.includes(firstOldLine));
if (foundLine) {
console.warn('Found similar line:', foundLine);
}
}
}
return updatedContent;
}
function buildArtifactVersions(artifacts) {
const processedArtifacts = new Map();
artifacts.forEach((versions, artifactId) => {
const processedVersions = [];
let currentContent = '';
versions.forEach((version, index) => {
let changeDescription = '';
switch (version.command) {
case 'create':
currentContent = version.content;
changeDescription = 'Created';
break;
case 'rewrite':
currentContent = version.content;
changeDescription = 'Rewritten';
break;
case 'update':
const oldContent = currentContent;
currentContent = applyUpdate(currentContent, version.old_str, version.new_str);
const oldPreview = version.old_str ? version.old_str.substring(0, 100) + '...' : '';
const newPreview = version.new_str ? version.new_str.substring(0, 100) + '...' : '';
changeDescription = `Updated: "${oldPreview}" → "${newPreview}"`;
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)`;
}
break;
default:
console.warn(`Unknown command: ${version.command}`);
break;
}
processedVersions.push({
...version,
fullContent: currentContent,
changeDescription: changeDescription
});
});
processedArtifacts.set(artifactId, processedVersions);
});
return processedArtifacts;
}
// Export functions
function generateConversationMarkdown(conversationData) {
let markdown = '';
// Header
markdown += `# ${conversationData.name}\n\n`;
markdown += `*Exported on: ${new Date().toLocaleString()}*\n`;
markdown += `*Conversation ID: ${conversationData.uuid}*\n`;
markdown += `*Model: ${conversationData.model}*\n\n`;
// Messages
conversationData.chat_messages.forEach(message => {
const role = message.sender === 'human' ? 'Human' : 'Claude';
markdown += `## ${role}\n\n`;
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;
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`;
}
}
});
});
return markdown;
}
async function exportConversation(finalVersionsOnly = false) {
try {
showNotification('Fetching conversation data...', 'info');
const conversationData = await getConversationData();
const timestamp = generateTimestamp();
const conversationId = conversationData.uuid;
const safeTitle = sanitizeFileName(conversationData.name);
// Export main conversation
const conversationMarkdown = generateConversationMarkdown(conversationData);
const conversationFilename = `${timestamp}_${conversationId}__${safeTitle}.md`;
downloadFile(conversationFilename, conversationMarkdown);
// Extract and process artifacts
const rawArtifacts = extractArtifacts(conversationData);
const processedArtifacts = buildArtifactVersions(rawArtifacts);
if (processedArtifacts.size === 0) {
showNotification('No artifacts found in conversation', 'info');
return;
}
let totalExported = 0;
// Export artifacts
processedArtifacts.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 filename = `${timestamp}_${conversationId}_${artifactId}_v${version.version}_${safeArtifactTitle}.md`;
let content = `# ${version.title}\n\n`;
content += `*Artifact ID:* \`${artifactId}\`\n`;
content += `*Version:* ${version.version}\n`;
content += `*Command:* \`${version.command}\`\n`;
content += `*UUID:* \`${version.uuid}\`\n`;
content += `*Created:* ${version.timestamp}\n`;
if (version.changeDescription) {
content += `*Change:* ${version.changeDescription}\n`;
}
content += '\n---\n\n';
content += version.fullContent;
downloadFile(filename, content);
totalExported++;
});
});
const mode = finalVersionsOnly ? 'final versions' : 'all versions';
showNotification(`Export completed! Downloaded conversation + ${totalExported} artifacts (${mode})`, 'success');
} catch (error) {
console.error('Export failed:', error);
showNotification(`Export failed: ${error.message}`, 'error');
}
}
// Initialize
function init() {
console.log('[Claude API Exporter] Initializing...');
// Register menu commands
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();
}
})();