Claude API Exporter (Minimal)

Simple export of Claude conversations and artifacts

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Claude API Exporter (Minimal)
// @namespace    http://tampermonkey.net/
// @version      6.2.0
// @description  Simple export of Claude conversations and artifacts
// @author       MRL (Minimized)
// @match        https://claude.ai/*
// @grant        GM_registerMenuCommand
// @icon         https://www.google.com/s2/favicons?sz=64&domain=claude.ai
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // =============================================
    // UTILITIES
    // =============================================
    const sanitize = (name) => (name || 'Untitled').replace(/[\\/:*?"<>|]/g, '_').replace(/\s+/g, '_').slice(0, 100);
    const formatDate = (d) => d ? new Date(d).toLocaleString() : '';

    const timestamp = (d) => {
        const dt = new Date(d || Date.now());
        const p = (n) => String(n).padStart(2, '0');
        return `${dt.getFullYear()}-${p(dt.getMonth()+1)}-${p(dt.getDate())}_${p(dt.getHours())}-${p(dt.getMinutes())}-${p(dt.getSeconds())}`;
    };

    const download = (name, content) => {
        const url = URL.createObjectURL(new Blob([content], { type: 'text/plain' }));
        Object.assign(document.createElement('a'), { href: url, download: name }).click();
        setTimeout(() => URL.revokeObjectURL(url), 100);
    };

    const notify = (msg, type = 'info') => {
        document.querySelectorAll('.claude-notif').forEach(n => n.remove());
        const el = document.createElement('div');
        el.className = 'claude-notif';
        el.style.cssText = `position:fixed;top:20px;right:20px;padding:15px 20px;border-radius:8px;color:#fff;
            font-family:system-ui;font-size:14px;z-index:10000;box-shadow:0 4px 12px rgba(0,0,0,0.3);
            background:${type === 'error' ? '#e53935' : type === 'success' ? '#43a047' : '#1e88e5'}`;
        el.textContent = msg;
        document.body.appendChild(el);
        setTimeout(() => el.remove(), 4000);
    };

    // File extensions
    const EXT = {
        'application/vnd.ant.code': (lang) => ({
            javascript: '.js', typescript: '.ts', python: '.py', html: '.html', css: '.css',
            java: '.java', cpp: '.cpp', c: '.c', go: '.go', rust: '.rs', php: '.php',
            ruby: '.rb', json: '.json', yaml: '.yaml', sql: '.sql', bash: '.sh', markdown: '.md',
            jsx: '.jsx', tsx: '.tsx', swift: '.swift', kotlin: '.kt'
        }[(lang || '').toLowerCase()] || '.txt'),
        'text/html': '.html',
        'text/markdown': '.md',
        'image/svg+xml': '.svg',
        'application/vnd.ant.mermaid': '.mmd',
        'application/vnd.ant.react': '.jsx'
    };

    const getExt = (type, lang) => typeof EXT[type] === 'function' ? EXT[type](lang) : EXT[type] || '.txt';

    // =============================================
    // API
    // =============================================
    const getOrgId = () => {
        const c = document.cookie.split(';').find(c => c.trim().startsWith('lastActiveOrg='));
        if (!c) throw new Error('Organization ID not found');
        return c.trim().split('=')[1];
    };

    const getConvId = () => (window.location.pathname.match(/\/chat\/([^/?]+)/) || [])[1];

    const fetchConv = async (id) => {
        const cid = id || getConvId();
        if (!cid) return null;
        const r = await fetch(`/api/organizations/${getOrgId()}/chat_conversations/${cid}?tree=true&rendering_mode=messages&render_all_tools=true`);
        if (!r.ok) throw new Error(`API error: ${r.status}`);
        return r.json();
    };

    // =============================================
    // ARTIFACT EXTRACTION
    // =============================================
    const extractArtifacts = (conv) => {
        const artifacts = new Map();
        const msgs = [...conv.chat_messages].sort((a, b) => a.index - b.index);

        msgs.forEach(msg => {
            msg.content.forEach(c => {
                if (c.type === 'tool_use' && c.name === 'artifacts' && c.input) {
                    const inp = c.input;
                    if (!artifacts.has(inp.id)) {
                        artifacts.set(inp.id, { versions: [], content: '', title: inp.title || inp.id, type: inp.type, lang: inp.language });
                    }
                    const art = artifacts.get(inp.id);

                    if (inp.command === 'create') {
                        art.content = inp.content || '';
                        art.title = inp.title || art.title;
                        art.type = inp.type;
                        art.lang = inp.language;
                    } else if (inp.command === 'rewrite') {
                        art.content = inp.content || '';
                        art.title = inp.title || art.title;
                    } else if (inp.command === 'update') {
                        art.content = art.content.replace(inp.old_str || '', inp.new_str || '');
                    }

                    art.versions.push({
                        version: art.versions.length + 1,
                        command: inp.command,
                        title: inp.title || art.title,
                        content: art.content,
                        type: art.type,
                        lang: art.lang,
                        timestamp: c.stop_timestamp || msg.created_at,
                        msgUuid: msg.uuid
                    });
                }
            });
        });

        return artifacts;
    };

    // =============================================
    // MARKDOWN GENERATION
    // =============================================
    const generateMarkdown = (conv, artifacts = null, embedArtifacts = false) => {
        const lines = [];

        // Header
        if (conv.project) lines.push(`*Project:* [${conv.project.name}](https://claude.ai/project/${conv.project.uuid})  `);
        lines.push(`*URL:* https://claude.ai/chat/${conv.uuid}  `);
        lines.push(`*Created:* ${formatDate(conv.created_at)}  `);
        lines.push(`*Updated:* ${formatDate(conv.updated_at)}`);
        if (conv.model) lines.push(`*Model:* \`${conv.model}\``);
        lines.push(`\n# ${conv.name || 'Untitled'}\n`);

        const msgs = [...conv.chat_messages].sort((a, b) => a.index - b.index);

        msgs.forEach(msg => {
            const role = msg.sender === 'human' ? 'Human' : 'Claude';
            lines.push('__________\n');
            lines.push(`## ${msg.index} - ${role}  `);
            lines.push(`*Created:* ${formatDate(msg.created_at)}  `);
            lines.push(`*UUID:* \`${msg.uuid}\`\n`);

            msg.content.forEach(c => {
                if (c.type === 'text') {
                    lines.push(c.text + '\n');
                } else if (c.type === 'tool_use' && c.name === 'artifacts' && c.input) {
                    const inp = c.input;
                    lines.push(`**Artifact:** ${inp.title || inp.id}  `);
                    lines.push(`*ID:* \`${inp.id}\` | *Command:* \`${inp.command}\`\n`);

                    if (embedArtifacts && artifacts?.has(inp.id)) {
                        const art = artifacts.get(inp.id);
                        const ver = art.versions.find(v => v.msgUuid === msg.uuid);
                        if (ver) {
                            const lang = ver.type === 'text/markdown' ? 'markdown' :
                                        ver.type === 'text/html' ? 'html' :
                                        ver.type === 'application/vnd.ant.react' ? 'jsx' :
                                        (ver.lang || '').toLowerCase() || '';
                            lines.push('```' + lang);
                            lines.push(ver.content);
                            lines.push('```\n');
                        }
                    }
                } else if (c.type === 'tool_use' && c.name === 'web_search' && c.input) {
                    lines.push(`**🔍 Web Search:** \`${c.input.query}\`\n`);
                } else if (c.type === 'thinking' && c.thinking) {
                    const sum = c.summaries?.length ? c.summaries[c.summaries.length - 1]?.summary : 'Thinking...';
                    lines.push(`*[Claude thinking...]*\n<details>\n<summary>${sum}</summary>\n${c.thinking}\n</details>\n`);
                }
            });

            (msg.files_v2 || msg.files || []).forEach(f => {
                lines.push(`**File:** ${f.file_name} | *ID:* \`${f.file_uuid}\`\n`);
            });
        });

        return lines.join('\n');
    };

    // =============================================
    // EXPORT FUNCTIONS
    // =============================================
    const genFilename = (conv) => `[${timestamp(conv.created_at)}] ${sanitize(conv.name)}.md`;

    const exportConversation = async () => {
        try {
            notify('Exporting...', 'info');
            const conv = await fetchConv();
            if (!conv) { notify('No conversation found', 'error'); return; }

            download(genFilename(conv), generateMarkdown(conv));
            notify('✅ Conversation exported!', 'success');
        } catch (e) {
            console.error('[Exporter]', e);
            notify(`Export failed: ${e.message}`, 'error');
        }
    };

    const exportWithArtifacts = async (embedOnly = false) => {
        try {
            notify('Exporting...', 'info');
            const conv = await fetchConv();
            if (!conv) { notify('No conversation found', 'error'); return; }

            const artifacts = extractArtifacts(conv);
            download(genFilename(conv), generateMarkdown(conv, artifacts, true));

            if (!embedOnly) {
                let count = 0;
                artifacts.forEach((art) => {
                    if (art.versions.length) {
                        const final = art.versions[art.versions.length - 1];
                        const ext = getExt(final.type, final.lang);
                        const fn = `[${timestamp(final.timestamp)}] ${sanitize(conv.name)} - ${sanitize(final.title)}${ext}`;
                        download(fn, final.content);
                        count++;
                    }
                });
                notify(`✅ Exported + ${count} artifact files!`, 'success');
            } else {
                notify(`✅ Exported with ${artifacts.size} artifacts inline!`, 'success');
            }
        } catch (e) {
            console.error('[Exporter]', e);
            notify(`Export failed: ${e.message}`, 'error');
        }
    };

    // =============================================
    // INIT - v6.2: Removed Raw JSON option
    // =============================================
    const init = () => {
        console.log('[Claude Exporter v6.2] Ready');
        GM_registerMenuCommand('📄 Conversation Only', exportConversation);
        GM_registerMenuCommand('📝 Conversation + Code Inline', () => exportWithArtifacts(true));
        GM_registerMenuCommand('📁 Conversation + Separate Files', () => exportWithArtifacts(false));
    };

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
})();