// ==UserScript==
// @name Claude API Exporter 1.1
// @namespace http://tampermonkey.net/
// @version 1.1
// @description Export Claude conversations using API
// @author MRL
// @match https://claude.ai/*
// @grant GM_registerMenuCommand
// @grant GM_download
// @grant GM_setValue
// @grant GM_getValue
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// =============================================
// CONSTANTS AND DEFAULT SETTINGS
// =============================================
/**
* Default settings configuration
*/
const defaultSettings = {
conversationTemplate: '{timestamp}_{conversationId}__{title}.md',
artifactTemplate: '{timestamp}_{conversationId}_{artifactId}_branch{branch}{main_suffix}_v{version}_{title}{extension}',
dateFormat: 'YYYYMMDDHHMMSS', // YYYYMMDDHHMMSS, YYYY-MM-DD_HH-MM-SS, ISO
artifactExportMode: 'files', // Flexible artifact export settings: 'embed', 'files', 'both'
includeArtifactsInConversationOnly: false, // For "Export Conversation Only" button
includeArtifactMetadata: true, // Include metadata comments in artifact files
// Content formatting settings
excludeAttachments: false, // Exclude attachments from conversation export
removeDoubleNewlinesFromConversation: false, // Remove \n\n from conversation content
removeDoubleNewlinesFromMarkdown: false // Remove \n\n from markdown artifact content
};
/**
* Available variables for filename templates
*/
const availableVariables = {
// Common variables
'{timestamp}': 'Current timestamp (format depends on dateFormat setting)',
'{conversationId}': 'Unique conversation identifier',
'{title}': 'Sanitized conversation or artifact title',
// Artifact-specific variables
'{artifactId}': 'Unique artifact identifier',
'{version}': 'Artifact version number',
'{branch}': 'Branch number (e.g., 1, 2, 3)',
'{main_suffix}': 'Suffix "_main" for main branch, empty for others',
'{extension}': 'File extension based on artifact type',
// Date/time variables
'{date}': 'Current date in YYYY-MM-DD format',
'{time}': 'Current time in HH-MM-SS format',
'{year}': 'Current year (YYYY)',
'{month}': 'Current month (MM)',
'{day}': 'Current day (DD)',
'{hour}': 'Current hour (HH)',
'{minute}': 'Current minute (MM)',
'{second}': 'Current second (SS)'
};
// =============================================
// SETTINGS MANAGEMENT
// =============================================
/**
* Load settings from storage
*/
function loadSettings() {
const settings = {};
for (const [key, defaultValue] of Object.entries(defaultSettings)) {
settings[key] = GM_getValue(key, defaultValue);
}
return settings;
}
/**
* Save settings to storage
*/
function saveSettings(settings) {
for (const [key, value] of Object.entries(settings)) {
GM_setValue(key, value);
}
}
/**
* Apply variables to template string
*/
function applyTemplate(template, variables) {
let result = template;
for (const [placeholder, value] of Object.entries(variables)) {
// Replace all occurrences of the placeholder
result = result.replace(new RegExp(placeholder.replace(/[{}]/g, '\\$&'), 'g'), value || '');
}
return result;
}
// =============================================
// SETTINGS UI
// =============================================
/**
* Creates and shows the settings interface
*/
function showSettingsUI() {
// Remove existing settings UI if present
document.getElementById('claude-exporter-settings')?.remove();
const currentSettings = loadSettings();
// Create modal overlay
const overlay = document.createElement('div');
overlay.id = 'claude-exporter-settings';
overlay.innerHTML = `
<div class="claude-settings-overlay">
<div class="claude-settings-modal">
<div class="claude-settings-header">
<h2>🔧 Claude Exporter Settings</h2>
<button class="claude-settings-close" type="button">×</button>
</div>
<!-- Tab Navigation -->
<div class="claude-tabs-nav">
<button class="claude-tab-btn active" data-tab="general">⚙️ General Settings</button>
<button class="claude-tab-btn" data-tab="filenames">📁 Filename Templates</button>
</div>
<div class="claude-settings-content">
<!-- Tab 1: General Settings -->
<div class="claude-tab-content active" id="tab-general">
<div class="claude-settings-section">
<h3>📦 Artifact Export Settings</h3>
<div class="claude-setting-group">
<label for="artifactExportMode">Default artifact export mode:</label>
<select id="artifactExportMode">
${['embed', 'files', 'both'].map(mode =>
`<option value="${mode}" ${currentSettings.artifactExportMode === mode ? 'selected' : ''}>
${mode === 'embed' ? '📄 Embed Only - Include artifacts in conversation file only' :
mode === 'files' ? '📁 Files Only - Export artifacts as separate files only' :
'📄📁 Both - Include in conversation and export as separate files'}
</option>`).join('')}
</select>
<p class="claude-settings-help">This setting affects the "Export Conversation + Final Artifacts" and "Export Conversation + All Artifacts" buttons.</p>
</div>
<div class="claude-setting-group">
<div class="claude-checkbox-group">
<input type="checkbox" id="includeArtifactsInConversationOnly" ${currentSettings.includeArtifactsInConversationOnly ? 'checked' : ''}>
<label for="includeArtifactsInConversationOnly">Include all artifacts in "Export Conversation Only"</label>
</div>
<p class="claude-settings-help">When enabled, the "Export Conversation Only" button will embed all artifact versions in the conversation file (but never export separate artifact files).</p>
</div>
<div class="claude-export-behavior-info">
<h4>📋 Export Behavior Summary</h4>
<div class="claude-behavior-grid">
${['conversationOnlyBehavior', 'finalArtifactsBehavior', 'allArtifactsBehavior'].map(id =>
`<div class="claude-behavior-item"><strong>${id.replace(/([A-Z])/g, ' $1').replace('Behavior', '').replace(/([a-z])([A-Z])/g, '$1 $2')}:</strong><span id="${id}"></span></div>`).join('')}
</div>
</div>
</div>
<div class="claude-settings-section">
<h3>📝 Content Formatting</h3>
<div class="claude-setting-group">
<div class="claude-checkbox-group">
<input type="checkbox" id="excludeAttachments" ${currentSettings.excludeAttachments ? 'checked' : ''}>
<label for="excludeAttachments">Exclude attachments content from conversation export</label>
</div>
<p class="claude-settings-help">When enabled, the extracted content of attachments will not be included in the exported conversation markdown.</p>
</div>
${['removeDoubleNewlinesFromConversation', 'removeDoubleNewlinesFromMarkdown'].map(setting => `
<div class="claude-setting-group">
<div class="claude-checkbox-group">
<input type="checkbox" id="${setting}" ${currentSettings[setting] ? 'checked' : ''}>
<label for="${setting}">Remove double newlines (\\n\\n) from ${setting.includes('Conversation') ? 'conversation content' : 'markdown artifact content'}</label>
</div>
<p class="claude-settings-help">When enabled, replaces multiple consecutive newlines with single newlines in ${setting.includes('Conversation') ? 'conversation text content only. Does not affect markdown structure or metadata.' : 'markdown artifact content only. Does not affect artifact metadata or other file types.'}</p>
</div>
`).join('')}
</div>
<div class="claude-settings-section">
<h3>📝 Artifact File Settings</h3>
<div class="claude-setting-group">
<div class="claude-checkbox-group">
<input type="checkbox" id="includeArtifactMetadata" ${currentSettings.includeArtifactMetadata ? 'checked' : ''}>
<label for="includeArtifactMetadata">Include metadata comments in artifact files</label>
</div>
<p class="claude-settings-help">When enabled, artifact files will include metadata comments at the top (ID, branch, version, etc.). When disabled, files will contain only the pure artifact content.</p>
</div>
</div>
</div>
<!-- Tab 2: Filename Templates -->
<div class="claude-tab-content" id="tab-filenames">
<div class="claude-settings-section">
<h3>📁 Filename Templates</h3>
<p class="claude-settings-description">Configure how exported files are named using variables. <a href="#claude-variables-panel" class="claude-variables-toggle">Show available variables</a></p>
<div class="claude-variables-panel" style="display: none;">
<h4>Available Variables:</h4>
<div class="claude-variables-grid">
${Object.entries(availableVariables).map(([variable, description]) =>
`<div class="claude-variable-item"><code class="claude-variable-name">${variable}</code><span class="claude-variable-desc">${description}</span></div>`).join('')}
</div>
</div>
${['conversation', 'artifact'].map(type => `
<div class="claude-setting-group">
<label for="${type}Template">${type.charAt(0).toUpperCase() + type.slice(1)} Files:</label>
<input type="text" id="${type}Template" value="${currentSettings[type + 'Template']}" placeholder="${defaultSettings[type + 'Template']}">
<div class="claude-preview" id="${type}Preview"></div>
</div>
`).join('')}
</div>
<div class="claude-settings-section">
<h3>📅 Date Format</h3>
<div class="claude-setting-group">
<label for="dateFormat">Timestamp Format:</label>
<select id="dateFormat">
${[
['YYYYMMDDHHMMSS', 'Compact (YYYYMMDDHHMMSS) - e.g., 20250710143022'],
['YYYY-MM-DD_HH-MM-SS', 'Readable (YYYY-MM-DD_HH-MM-SS) - e.g., 2025-07-10_14-30-22'],
['ISO', 'ISO (YYYY-MM-DDTHH-MM-SS) - e.g., 2025-07-10T14-30-22']
].map(([value, text]) =>
`<option value="${value}" ${currentSettings.dateFormat === value ? 'selected' : ''}>${text}</option>`).join('')}
</select>
</div>
</div>
</div>
</div>
<div class="claude-settings-footer">
<button class="claude-btn claude-btn-secondary" type="button" id="resetDefaults">🔄 Reset to Defaults</button>
<div class="claude-btn-group">
<button class="claude-btn claude-btn-secondary" type="button" id="cancelSettings">Cancel</button>
<button class="claude-btn claude-btn-primary" type="button" id="saveSettings">💾 Save Settings</button>
</div>
</div>
</div>
</div>
`;
// Add CSS styles (compressed)
const styles = `<style id="claude-exporter-styles">
.claude-settings-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.75);display:flex;align-items:center;justify-content:center;z-index:99999;font-family:system-ui,-apple-system,sans-serif}
.claude-settings-modal{background:#fff;border-radius:12px;box-shadow:0 20px 60px rgba(0,0,0,0.3);width:90%;max-width:800px;max-height:90vh;overflow:hidden;display:flex;flex-direction:column}
.claude-settings-header{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:white;padding:20px 24px;display:flex;align-items:center;justify-content:space-between}
.claude-settings-header h2{margin:0;font-size:20px;font-weight:600}
.claude-settings-close{background:none;border:none;color:white;font-size:24px;cursor:pointer;padding:0;width:30px;height:30px;border-radius:50%;display:flex;align-items:center;justify-content:center;transition:background-color 0.2s}
.claude-settings-close:hover{background:rgba(255,255,255,0.2)}
.claude-tabs-nav{display:flex;background:#f8fafc;border-bottom:1px solid #e2e8f0}
.claude-tab-btn{background:none;border:none;padding:16px 20px;font-size:14px;font-weight:500;color:#64748b;cursor:pointer;border-bottom:3px solid transparent;transition:all 0.2s;flex:1;text-align:center}
.claude-tab-btn:hover{background:rgba(102,126,234,0.1);color:#475569}
.claude-tab-btn.active{color:#667eea;background:white;border-bottom-color:#667eea}
.claude-tab-content{display:none}.claude-tab-content.active{display:block}
.claude-settings-content{flex:1;overflow-y:auto;padding:24px}
.claude-settings-section{margin-bottom:32px}.claude-settings-section:last-child{margin-bottom:0}
.claude-settings-section h3{margin:0 0 12px 0;font-size:18px;font-weight:600;color:#2d3748}
.claude-settings-description{margin:0 0 20px 0;color:#718096;font-size:14px;line-height:1.5}
.claude-variables-toggle{color:#667eea;text-decoration:none;font-weight:500}.claude-variables-toggle:hover{text-decoration:underline}
.claude-variables-panel{background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:16px;margin:16px 0}
.claude-variables-panel h4{margin:0 0 12px 0;font-size:14px;font-weight:600;color:#2d3748}
.claude-variables-grid{display:grid;gap:8px}
.claude-variable-item{display:grid;grid-template-columns:auto 1fr;gap:12px;align-items:start}
.claude-variable-name{background:#e2e8f0;color:#2d3748;padding:2px 6px;border-radius:4px;font-family:'Monaco','Menlo',monospace;font-size:13px;white-space:nowrap}
.claude-variable-desc{font-size:13px;color:#718096;line-height:1.4}
.claude-setting-group{margin-bottom:20px}
.claude-setting-group label{display:block;margin-bottom:6px;font-weight:500;color:#2d3748;font-size:14px}
.claude-setting-group input,.claude-setting-group select{width:100%;padding:10px 12px;border:2px solid #e2e8f0;border-radius:6px;font-size:14px;transition:border-color 0.2s,box-shadow 0.2s;box-sizing:border-box}
.claude-setting-group input[type="text"]{font-family:'Monaco','Menlo',monospace}
.claude-checkbox-group{display:flex;align-items:center;gap:10px;margin-bottom:8px}
.claude-checkbox-group input[type="checkbox"]{width:auto;margin:0;transform:scale(1.2)}
.claude-checkbox-group label{margin:0;cursor:pointer;font-weight:500}
.claude-settings-help{margin:8px 0 0 0;font-size:13px;color:#718096;line-height:1.4}
.claude-setting-group input:focus,.claude-setting-group select:focus{outline:none;border-color:#667eea;box-shadow:0 0 0 3px rgba(102,126,234,0.1)}
.claude-preview{margin-top:8px;padding:8px 12px;background:#f0f9ff;border:1px solid #bae6fd;border-radius:6px;font-family:'Monaco','Menlo',monospace;font-size:13px;color:#0369a1;word-break:break-all}
.claude-export-behavior-info{background:#f0f9ff;border:1px solid #bae6fd;border-radius:8px;padding:16px;margin-top:20px}
.claude-export-behavior-info h4{margin:0 0 12px 0;font-size:14px;font-weight:600;color:#0369a1}
.claude-behavior-grid{display:grid;gap:8px}
.claude-behavior-item{display:grid;grid-template-columns:auto 1fr;gap:12px;align-items:start}
.claude-behavior-item strong{font-size:13px;color:#0369a1;white-space:nowrap}
.claude-behavior-item span{font-size:13px;color:#374151;line-height:1.4}
.claude-settings-footer{background:#f8fafc;padding:20px 24px;border-top:1px solid #e2e8f0;display:flex;align-items:center;justify-content:space-between}
.claude-btn{padding:10px 20px;border:none;border-radius:6px;font-size:14px;font-weight:500;cursor:pointer;transition:all 0.2s;text-decoration:none;display:inline-flex;align-items:center;gap:6px}
.claude-btn-primary{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:white}
.claude-btn-primary:hover{transform:translateY(-1px);box-shadow:0 4px 12px rgba(102,126,234,0.4)}
.claude-btn-secondary{background:#e2e8f0;color:#2d3748}.claude-btn-secondary:hover{background:#cbd5e0}
.claude-btn-group{display:flex;gap:12px}
@media (max-width:600px){.claude-settings-modal{width:95%;margin:20px}.claude-settings-footer{flex-direction:column;gap:12px}.claude-btn-group{width:100%}.claude-btn{flex:1;justify-content:center}.claude-tabs-nav{flex-direction:column}.claude-tab-btn{flex:none}}
</style>`;
// Add styles to head and modal to body
document.head.insertAdjacentHTML('beforeend', styles);
document.body.appendChild(overlay);
// Initialize functionality
initTabs();
initEventListeners();
updatePreviews();
}
// Tab switching functionality
function initTabs() {
document.querySelectorAll('.claude-tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
const targetTab = btn.dataset.tab;
document.querySelectorAll('.claude-tab-btn, .claude-tab-content').forEach(el => el.classList.remove('active'));
btn.classList.add('active');
document.getElementById(`tab-${targetTab}`).classList.add('active');
});
});
}
// Initialize all event listeners
function initEventListeners() {
const formElements = ['conversationTemplate', 'artifactTemplate', 'dateFormat', 'artifactExportMode',
'includeArtifactsInConversationOnly', 'includeArtifactMetadata',
'excludeAttachments', 'removeDoubleNewlinesFromConversation', 'removeDoubleNewlinesFromMarkdown'];
formElements.forEach(id => {
const element = document.getElementById(id);
if (element) {
element.addEventListener(element.type === 'checkbox' ? 'change' : 'input', updatePreviews);
}
});
// Variables panel toggle
document.querySelector('.claude-variables-toggle').addEventListener('click', (e) => {
e.preventDefault();
const panel = document.querySelector('.claude-variables-panel');
const isVisible = panel.style.display !== 'none';
panel.style.display = isVisible ? 'none' : 'block';
e.target.textContent = isVisible ? 'Show available variables' : 'Hide available variables';
});
// Reset defaults
document.getElementById('resetDefaults').addEventListener('click', () => {
if (confirm('Reset all settings to defaults?')) {
Object.entries(defaultSettings).forEach(([key, value]) => {
const element = document.getElementById(key);
if (element) {
element.type === 'checkbox' ? element.checked = value : element.value = value;
}
});
updatePreviews();
}
});
// Close modal functionality
const closeModal = () => {
document.getElementById('claude-exporter-settings')?.remove();
document.getElementById('claude-exporter-styles')?.remove();
};
document.getElementById('cancelSettings').addEventListener('click', closeModal);
document.querySelector('.claude-settings-close').addEventListener('click', closeModal);
// Close on overlay click
document.querySelector('.claude-settings-overlay').addEventListener('click', (e) => {
if (e.target.classList.contains('claude-settings-overlay')) closeModal();
});
// Save settings
document.getElementById('saveSettings').addEventListener('click', () => {
const newSettings = {};
Object.keys(defaultSettings).forEach(key => {
const element = document.getElementById(key);
if (element) {
newSettings[key] = element.type === 'checkbox' ? element.checked : element.value;
}
});
saveSettings(newSettings);
showNotification('Settings saved successfully!', 'success');
closeModal();
});
}
// Update previews function
function updatePreviews() {
const conversationTemplate = document.getElementById('conversationTemplate').value;
const artifactTemplate = document.getElementById('artifactTemplate').value;
const dateFormat = document.getElementById('dateFormat').value;
const artifactExportMode = document.getElementById('artifactExportMode').value;
const includeArtifactsInConversationOnly = document.getElementById('includeArtifactsInConversationOnly').checked;
// Sample data for preview
const sampleVariables = createTemplateVariables({
'{timestamp}': formatTimestamp(new Date(), dateFormat),
'{conversationId}': '12dasdh1-fa1j-f213-da13-dfa3124123ff',
'{title}': 'Title',
'{artifactId}': 'artifact2',
'{version}': '3',
'{branch}': '2',
'{main_suffix}': '',
'{extension}': '.js'
});
document.getElementById('conversationPreview').textContent = 'Preview: ' + applyTemplate(conversationTemplate, sampleVariables);
document.getElementById('artifactPreview').textContent = 'Preview: ' + applyTemplate(artifactTemplate, sampleVariables);
// Behavior descriptions
const behaviors = {
conversationOnlyBehavior: includeArtifactsInConversationOnly ? 'Embeds all artifacts in conversation file' : 'Pure conversation without artifacts',
finalArtifactsBehavior: {
'embed': 'Embeds final artifacts in conversation file only',
'files': 'Pure conversation + final artifacts as separate files',
'both': 'Embeds final artifacts in conversation + exports as separate files'
}[artifactExportMode],
allArtifactsBehavior: {
'embed': 'Embeds all artifacts in conversation file only',
'files': 'Pure conversation + all artifacts as separate files',
'both': 'Embeds all artifacts in conversation + exports as separate files'
}[artifactExportMode]
};
Object.entries(behaviors).forEach(([id, text]) => {
document.getElementById(id).textContent = text;
});
}
// =============================================
// SETTINGS UTILITY FUNCTIONS
// =============================================
/**
* Formats timestamp according to the selected format
*/
function formatTimestamp(dateInput, format) {
const d = typeof dateInput === 'string' ? new Date(dateInput) : dateInput;
if (isNaN(d.getTime())) {
return formatTimestamp(new Date(), format);
}
const components = {
year: d.getFullYear(),
month: String(d.getMonth() + 1).padStart(2, '0'),
day: String(d.getDate()).padStart(2, '0'),
hour: String(d.getHours()).padStart(2, '0'),
minute: String(d.getMinutes()).padStart(2, '0'),
second: String(d.getSeconds()).padStart(2, '0')
};
switch (format) {
case 'YYYY-MM-DD_HH-MM-SS':
return `${components.year}-${components.month}-${components.day}_${components.hour}-${components.minute}-${components.second}`;
case 'ISO':
return `${components.year}-${components.month}-${components.day}T${components.hour}-${components.minute}-${components.second}`;
case 'YYYYMMDDHHMMSS':
default:
return `${components.year}${components.month}${components.day}${components.hour}${components.minute}${components.second}`;
}
}
/**
* Generates timestamp using current settings
*/
function generateTimestamp(dateInput) {
const settings = loadSettings();
const date = dateInput ?
(typeof dateInput === 'string' ? new Date(dateInput) : dateInput) :
new Date();
return formatTimestamp(date, settings.dateFormat);
}
/**
* Creates template variables object for filenames
*/
function createTemplateVariables(baseData) {
const now = new Date();
const base = {
'{date}': now.toISOString().split('T')[0],
'{time}': now.toTimeString().split(' ')[0].replace(/:/g, '-'),
'{year}': now.getFullYear().toString(),
'{month}': String(now.getMonth() + 1).padStart(2, '0'),
'{day}': String(now.getDate()).padStart(2, '0'),
'{hour}': String(now.getHours()).padStart(2, '0'),
'{minute}': String(now.getMinutes()).padStart(2, '0'),
'{second}': String(now.getSeconds()).padStart(2, '0')
};
return { ...base, ...baseData };
}
/**
* Generates conversation filename using template
*/
function generateConversationFilename(conversationData) {
const settings = loadSettings();
const variables = createTemplateVariables({
'{timestamp}': generateTimestamp(conversationData.updated_at),
'{conversationId}': conversationData.uuid,
'{title}': sanitizeFileName(conversationData.name)
});
return applyTemplate(settings.conversationTemplate, variables);
}
/**
* Generates artifact filename using template
*/
function generateArtifactFilename(version, conversationData, branchLabel, isMain, artifactId) {
const settings = loadSettings();
const variables = createTemplateVariables({
'{timestamp}': generateTimestamp(version.content_stop_timestamp),
'{conversationId}': conversationData.uuid,
'{artifactId}': artifactId,
'{version}': version.version.toString(),
'{branch}': branchLabel,
'{main_suffix}': isMain ? '_main' : '',
'{title}': sanitizeFileName(version.title),
'{extension}': getFileExtension(version.finalType, version.finalLanguage)
});
return applyTemplate(settings.artifactTemplate, variables);
}
/**
* Removes double newlines from text content while preserving markdown structure
* @param {string} content - Text content to process
* @param {boolean} removeDoubleNewlines - Whether to remove \n\n
* @returns {string} Processed content
*/
function processTextContent(content, removeDoubleNewlines) {
return processContent(content, removeDoubleNewlines);
}
/**
* Processes artifact content based on type and settings
* @param {string} content - Artifact content
* @param {string} type - Artifact type
* @param {boolean} removeDoubleNewlines - Whether to remove \n\n
* @returns {string} Processed content
*/
function processArtifactContent(content, type, removeDoubleNewlines) {
return processContent(content, removeDoubleNewlines, type);
}
/**
* Unified content processing function
*/
function processContent(content, removeDoubleNewlines, type = null) {
if (!removeDoubleNewlines || !content) {
return content;
}
// Only apply to markdown artifacts or general content
if (!type || type === 'text/markdown') {
return content.replace(/\n\n+/g, '\n');
}
return content;
}
// =============================================
// UTILITY FUNCTIONS
// =============================================
/**
* Sanitizes filename by removing invalid characters and limiting length
*/
function sanitizeFileName(name) {
return name.replace(/[\\/:*?"<>|]/g, '_')
.replace(/\s+/g, '_')
.replace(/__+/g, '_')
.replace(/^_+|_+$/g, '')
.slice(0, 100);
}
/**
* Formats date string to localized format
*/
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
*/
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');
const colors = {
error: '#f44336',
success: '#4CAF50',
info: '#2196F3'
};
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);
background-color: ${colors[type] || colors.info};
`;
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' || 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() : '';
// Define comment patterns
const commentPatterns = {
// Languages with // comments
slash: ['javascript', 'typescript', 'java', 'csharp', 'c#', 'cpp', 'c++', 'c', 'go', 'rust', 'swift', 'kotlin', 'dart', 'php', 'scala', 'jsx', 'tsx'],
// Languages with # comments
hash: ['python', 'ruby', 'perl', 'bash', 'shell', 'sh', 'zsh', 'fish', 'yaml', 'yml', 'r', 'julia', 'toml', 'ini', 'powershell'],
// Languages with -- comments
dash: ['sql', 'mysql', 'postgresql', 'sqlite', 'plsql', 'haskell', 'lua']
};
if (commentPatterns.slash.includes(normalizedLanguage)) {
return { start: '// ', end: '' };
} else if (commentPatterns.hash.includes(normalizedLanguage)) {
return { start: '# ', end: '' };
} else if (commentPatterns.dash.includes(normalizedLanguage)) {
return { start: '-- ', end: '' };
}
return { start: '# ', end: '' }; // Default to hash comments
}
/**
* Gets language identifier for markdown syntax highlighting
*/
function getLanguageForHighlighting(type, language) {
const typeMap = {
'text/html': 'html',
'text/markdown': 'markdown',
'image/svg+xml': 'xml',
'application/vnd.ant.mermaid': 'mermaid',
'application/vnd.ant.react': 'jsx'
};
if (typeMap[type]) return typeMap[type];
if (type === 'application/vnd.ant.code' && language) {
const normalizedLanguage = language.toLowerCase().trim();
// Map some languages to their markdown equivalents
const languageMap = {
'c++': 'cpp', 'c#': 'csharp', 'f#': 'fsharp',
'objective-c': 'objc', 'shell': 'bash', 'sh': 'bash'
};
return languageMap[normalizedLanguage] || normalizedLanguage;
}
return '';
}
// =============================================
// 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`
};
}
/**
* Creates change description for artifact commands
*/
function createChangeDescription(version, oldContent, currentContent) {
switch (version.command) {
case 'create':
return 'Created';
case 'rewrite':
return 'Rewritten';
case 'update':
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') + '...' : '';
let changeDescription = `Updated: "${oldPreview}" → "${newPreview}"`;
// Add information about character count changes
const lengthDiff = currentContent.length - oldContent.length;
if (lengthDiff > 0) {
changeDescription += ` (+${lengthDiff} chars)`;
} else if (lengthDiff < 0) {
changeDescription += ` (${lengthDiff} chars)`;
}
return changeDescription;
default:
return `Unknown command: ${version.command}`;
}
}
/**
* 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 updateInfo = '';
let versionStartContent = currentContent;
const oldContent = currentContent;
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;
updateInfo = updateResult.info;
if (!updateResult.success) {
updateInfo = `[WARNING: ${updateResult.info}]`;
}
break;
default:
console.warn(`Unknown command: ${version.command}`);
break;
}
const changeDescription = createChangeDescription(version, oldContent, currentContent);
processedVersions.push({
...version,
fullContent: currentContent,
changeDescription: updateInfo ? changeDescription + ` ${updateInfo}` : 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
// =============================================
/**
* Finds the artifact content for a specific artifact ID and message
*/
function findArtifactContent(artifactId, messageUuid, branchArtifacts, includeMode = 'final') {
let allVersionsOfArtifact = [];
let messageVersion = null;
// Collect all versions of this artifact from all branches
for (const [branchId, artifactsMap] of branchArtifacts) {
if (artifactsMap.has(artifactId)) {
const versions = artifactsMap.get(artifactId);
allVersionsOfArtifact = allVersionsOfArtifact.concat(versions);
// Also find if current message has any version of this artifact
const msgVersion = versions.find(v => v.messageUuid === messageUuid);
if (msgVersion) {
messageVersion = msgVersion;
}
}
}
if (allVersionsOfArtifact.length === 0) {
return null;
}
if (includeMode === 'all') {
// Show the version that was created in this specific message
return messageVersion;
} else if (includeMode === 'final') {
// Sort all versions by creation time to find the truly latest one
allVersionsOfArtifact.sort((a, b) => {
const timeA = new Date(a.content_stop_timestamp || a.timestamp_created_at);
const timeB = new Date(b.content_stop_timestamp || b.timestamp_created_at);
return timeA - timeB;
});
const globalLatestVersion = allVersionsOfArtifact[allVersionsOfArtifact.length - 1];
// Show artifact ONLY if this message contains the globally final version
if (globalLatestVersion.messageUuid === messageUuid) {
return globalLatestVersion;
}
return null;
}
return null;
}
/**
* Generates markdown content for the entire conversation
* @param {Object} conversationData - Complete conversation data from API
* @returns {string} Formatted markdown content
*/
function generateConversationMarkdown(conversationData, includeArtifacts = 'none', branchArtifacts = null, branchInfo = null) {
const settings = loadSettings();
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') {
const processedText = processTextContent(content.text, settings.removeDoubleNewlinesFromConversation);
markdown += processedText + '\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) {
// 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 ? branchInfo.find(b => b.branchId === artifactVersion.branchId) : null;
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`;
}
}
}
// Include artifact content based on mode
if (includeArtifacts !== 'none' && branchArtifacts) {
const artifactContent = findArtifactContent(input.id, message.uuid, branchArtifacts, includeArtifacts);
if (artifactContent) {
markdown += `\n### Artifact Content\n\n`;
// Determine the language for syntax highlighting
const language = getLanguageForHighlighting(artifactContent.finalType, artifactContent.finalLanguage);
// Process artifact content based on settings
let processedArtifactContent = artifactContent.fullContent;
if (artifactContent.finalType === 'text/markdown' && settings.removeDoubleNewlinesFromMarkdown) {
processedArtifactContent = processArtifactContent(artifactContent.fullContent, artifactContent.finalType, true);
}
markdown += '```' + language + '\n';
markdown += processedArtifactContent + '\n';
markdown += '```\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`;
const processedThinking = processTextContent(content.thinking, settings.removeDoubleNewlinesFromConversation);
markdown += processedThinking + '\n\n';
markdown += `</details>\n\n`;
} else {
markdown += `*[Claude thinking...]*\n\n`;
}
}
});
// Process files if present (files or files_v2)
const attachedFiles = message.files_v2 || message.files || [];
if (attachedFiles.length > 0) {
attachedFiles.forEach(file => {
markdown += `**File:** ${file.file_name}\n`;
markdown += `*ID:* \`${file.file_uuid}\`\n`;
if (file.file_kind === 'image') {
markdown += `*Preview:* https://claude.ai${file.preview_url}\n`;
}
markdown += `\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 (!settings.excludeAttachments && attachment.extracted_content) {
markdown += `<details>\n\n`;
markdown += '```\n';
const processedAttachment = processTextContent(attachment.extracted_content, settings.removeDoubleNewlinesFromConversation);
markdown += processedAttachment + '\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) {
const settings = loadSettings();
// Return empty string if metadata is disabled
if (!settings.includeArtifactMetadata) {
return '';
}
const metadataInfo = [
`Artifact ID: ${artifactId}`,
`Branch: ${branchLabel}${isMain ? ' (main)' : ''} (${version.branchId.substring(0, 8)}...)`,
`Version: ${version.version}`,
`Command: ${version.command}`,
`UUID: ${version.uuid}`,
`Created: ${formatDate(version.content_stop_timestamp)}`
];
if (version.changeDescription) {
metadataInfo.push(`Change: ${version.changeDescription}`);
}
if (version.updateInfo) {
metadataInfo.push(`Update Info: ${version.updateInfo}`);
}
if (!isMain && version.inheritedContent && version.command === 'update') {
metadataInfo.push(`Started from inherited content: ${version.inheritedContent.length} chars`);
}
// Special formatting for markdown files
if (version.finalType === 'text/markdown') {
let metadata = metadataInfo.map(info => `*${info}*`).join('\n') + '\n\n---\n';
return metadata;
}
// For all other file types, use comments
const commentStyle = getCommentStyle(version.finalType, version.finalLanguage);
const { start, end } = commentStyle;
let metadata = metadataInfo.map(info => `${start}${info}${end}`).join('\n') + '\n';
// Add separator based on language
const separators = {
'// ': '\n// ---\n',
'-- ': '\n-- ---\n',
'<!-- ': '\n<!-- --- -->\n'
};
metadata += separators[start] || '\n# ---\n';
return metadata;
}
/**
* Helper to count artifacts across branches
*/
function countArtifacts(branchArtifacts) {
return Array.from(branchArtifacts.values())
.reduce((total, artifactsMap) => total + artifactsMap.size, 0);
}
/**
* 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 settings = loadSettings();
// Extract and process artifacts from all branches
const { branchArtifacts, branchInfo } = extractAllArtifacts(conversationData);
let conversationMarkdown;
let shouldExportSeparateFiles = false;
// Determine behavior based on setting
switch (settings.artifactExportMode) {
case 'embed':
// Only embed in conversation file
const includeMode = finalVersionsOnly ? 'final' : 'all';
conversationMarkdown = generateConversationMarkdown(conversationData, includeMode, branchArtifacts, branchInfo);
shouldExportSeparateFiles = false;
break;
case 'files':
// Only separate files, no embedding
conversationMarkdown = generateConversationMarkdown(conversationData, 'none', branchArtifacts, branchInfo);
shouldExportSeparateFiles = true;
break;
case 'both':
// Both embed and separate files
const includeModeBoth = finalVersionsOnly ? 'final' : 'all';
conversationMarkdown = generateConversationMarkdown(conversationData, includeModeBoth, branchArtifacts, branchInfo);
shouldExportSeparateFiles = true;
break;
}
// Always download conversation file
const conversationFilename = generateConversationFilename(conversationData);
downloadFile(conversationFilename, conversationMarkdown);
// Export separate artifact files if needed
if (shouldExportSeparateFiles && branchArtifacts.size > 0) {
let totalExported = 0;
// Export artifacts for each branch
branchArtifacts.forEach((artifactsMap, branchId) => {
const branchData = branchInfo.find(b => b.branchId === branchId);
const branchLabel = branchData ? branchData.branchIndex.toString() : '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 filename = generateArtifactFilename(version, conversationData, branchLabel, isMain, artifactId);
// Format metadata as comments
const metadata = formatArtifactMetadata(version, artifactId, branchLabel, isMain);
// Process artifact content based on settings
let processedContent = version.fullContent;
if (version.finalType === 'text/markdown' && settings.removeDoubleNewlinesFromMarkdown) {
processedContent = processArtifactContent(version.fullContent, version.finalType, true);
}
// Combine metadata and content
const content = metadata ? metadata + '\n' + processedContent : processedContent;
downloadFile(filename, content);
totalExported++;
});
});
});
const mode = finalVersionsOnly ? 'final versions' : 'all versions';
const modeText = settings.artifactExportMode === 'both' ? 'embedded + separate files' : 'separate files';
showNotification(`Export completed! Downloaded conversation + ${totalExported} artifacts as ${modeText} (${mode})`, 'success');
} else {
// No separate files exported
const artifactCount = countArtifacts(branchArtifacts);
if (artifactCount > 0) {
const mode = finalVersionsOnly ? 'final versions' : 'all versions';
showNotification(`Export completed! Downloaded conversation with ${artifactCount} embedded artifacts (${mode})`, 'success');
} else {
showNotification('Export completed! No artifacts found in conversation', 'info');
}
}
} catch (error) {
console.error('Export failed:', error);
showNotification(`Export failed: ${error.message}`, 'error');
}
}
/**
* Exports only the conversation, with optional artifact inclusion based on settings
*/
async function exportConversationOnly() {
try {
showNotification('Fetching conversation data...', 'info');
const conversationData = await getConversationData();
const settings = loadSettings();
// Extract artifacts to get metadata for conversation display
const { branchArtifacts, branchInfo } = extractAllArtifacts(conversationData);
const includeArtifacts = settings.includeArtifactsInConversationOnly ? 'all' : 'none';
const conversationMarkdown = generateConversationMarkdown(conversationData, includeArtifacts, branchArtifacts, branchInfo);
const conversationFilename = generateConversationFilename(conversationData);
downloadFile(conversationFilename, conversationMarkdown);
if (settings.includeArtifactsInConversationOnly && branchArtifacts.size > 0) {
const artifactCount = countArtifacts(branchArtifacts);
showNotification(`Conversation exported with ${artifactCount} embedded artifacts!`, 'success');
} else {
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('⚙️ Settings', showSettingsUI);
GM_registerMenuCommand('📄 Export Conversation Only', exportConversationOnly);
GM_registerMenuCommand('📁 Export Conversation + Final Artifacts', () => exportConversation(true));
GM_registerMenuCommand('📁 Export Conversation + All Artifacts', () => exportConversation(false));
}
// Start when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();