Skip to content

Instantly share code, notes, and snippets.

@lumpsoid
Last active April 3, 2026 08:50
Show Gist options
  • Select an option

  • Save lumpsoid/66e835287f7dd5ccd27e61b0068c5f6b to your computer and use it in GitHub Desktop.

Select an option

Save lumpsoid/66e835287f7dd5ccd27e61b0068c5f6b to your computer and use it in GitHub Desktop.
claude-code-web-backup-chat.js
(async function backup(){const sanitizeFilename=s=>s.replace(/[/\\?%*:|"<>]/g,'-');const sleep=ms=>new Promise(r=>setTimeout(r,ms));const BINARY_EXTENSIONS=new Set(['png','jpg','jpeg','gif','webp','svg','ico','pdf','zip','tar','gz','wasm','ttf','woff','woff2']);function getOrgUuid(){const orgUuid=document.cookie.match(/lastActiveOrg=([^;]+)/)?.[1];if(!orgUuid){throw new Error('No orgUuid found in cookies')}return orgUuid}function getConversationFromReactFiber(){const msgEl=document.querySelector('[data-testid="user-message"]');if(!msgEl){throw new Error('No user-message element found in DOM')}const fiberKey=Object.keys(msgEl).find(k=>k.startsWith('__reactFiber'));let fiber=msgEl[fiberKey];for(let depth=0;fiber&&depth<100;fiber=fiber.return,depth+=1){if(fiber.memoizedProps?.conversation?.chat_messages){return fiber.memoizedProps.conversation}}throw new Error('conversation not found in React fiber tree')}async function fetchFileAsEmbeddable(url,filename){const ext=filename.split('.').pop().toLowerCase();const isBinary=BINARY_EXTENSIONS.has(ext);const resp=await fetch(url,{credentials:'include'});if(!resp.ok){throw new Error(`HTTP ${resp.status }`)}const mime_type=resp.headers.get('content-type')||null;if(isBinary){const buffer=await resp.arrayBuffer();const content=btoa(String.fromCharCode(...new Uint8Array(buffer)));return{ext,encoding:'base64',mime_type,content}}else{const content=await resp.text();return{ext,encoding:'utf8',mime_type,content}}}async function collectArtifactFiles(messages,orgUuid,convUuid){const files={};for(const msg of messages){for(const block of(msg.content||[])){if(block.type!=='tool_result'){continue}for(const item of(block.content||[])){if(item.type!=='local_resource'||!item.file_path){continue}const fname=item.file_path.split('/').pop();const url=`/api/organizations/${ orgUuid }/conversations/${ convUuid }/wiggle/download-file`+`?path=${encodeURIComponent(item.file_path)}`;try{files[fname]={source:'artifact',file_path:item.file_path,...(await fetchFileAsEmbeddable(url,fname))};console.log(`✅ artifact: ${ fname }`)}catch(err){files[fname]={source:'artifact',file_path:item.file_path,error:String(err)};console.warn(`⚠️ artifact failed: ${ fname }`,err)}await sleep(100)}}}return files}async function collectHumanUploadedFiles(messages){const files={};for(const msg of messages){for(const f of(msg.files||[])){const fileUuid=f.file_uuid||f.uuid;if(!fileUuid){continue}const fname=f.file_name||`file-${ fileUuid }`;const url=f.preview_url;try{files[fname]={source:'human_upload',file_uuid:fileUuid,file_name:fname,actual_encoding:'webp',...(await fetchFileAsEmbeddable(url,fname))};console.log(`✅ human file: ${ fname }`)}catch(err){files[fname]={source:'human_upload',file_uuid:fileUuid,file_name:fname,error:String(err)};console.warn(`⚠️ human file failed: ${ fname }`,err)}await sleep(100)}}return files}function downloadAsJson(data,title){const timestamp=new Date().toISOString().replace(/[:.]/g,'-');const filename=sanitizeFilename(`${ title }-${ timestamp }.json`);const blob=new Blob([JSON.stringify(data,null,2)],{type:'application/json'});const a=Object.assign(document.createElement('a'),{href:URL.createObjectURL(blob),download:filename});a.click();URL.revokeObjectURL(a.href);return filename}async function backupClaudeChat(){const orgUuid=getOrgUuid();const conv=getConversationFromReactFiber();const[artifactFiles,humanFiles]=await Promise.all([collectArtifactFiles(conv.chat_messages,orgUuid,conv.uuid),collectHumanUploadedFiles(conv.chat_messages)]);const files={...artifactFiles,...humanFiles};const backup={...conv,files};const filename=downloadAsJson(backup,conv.name||'claude-chat');console.log(`✅ Saved: ${ filename }`);console.log(` messages: ${conv.chat_messages.length }, files embedded: ${Object.keys(files).length }`)}backupClaudeChat()})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment