fixed bugs

This commit is contained in:
2025-10-26 18:29:28 -05:00
parent aa8b0bd3e5
commit 75326a0138
47 changed files with 2202 additions and 463 deletions
+1
View File
@@ -0,0 +1 @@
var background=(function(){"use strict";const t=globalThis.browser?.runtime?.id?globalThis.browser:globalThis.chrome;function i(r){return r==null||typeof r=="function"?{main:r}:r}const a=i(()=>{console.log("Claude Conversation Exporter background script loaded"),t.runtime.onInstalled.addListener(()=>{console.log("Claude Conversation Exporter installed"),(async()=>{try{const r=await t.tabs.query({url:"https://claude.ai/*"});for(const e of r)if(e.id)try{await t.scripting.executeScript({target:{tabId:e.id},files:["content-scripts/content.js"]})}catch(n){console.log("Could not inject into tab",e.id,n)}}catch(r){console.error("Error querying or injecting tabs:",r)}})()}),t.runtime.onMessage.addListener((r,e)=>r.action==="ensureContentScript"?(async()=>{try{const c=(await t.tabs.query({active:!0,currentWindow:!0}))[0];return c?.id?(await t.scripting.executeScript({target:{tabId:c.id},files:["content-scripts/content.js"]}),{success:!0}):{success:!1,error:"No active tab found"}}catch(n){return{success:!1,error:n instanceof Error?n.message:"Unknown error"}}})():!1)});function d(){}function o(r,...e){}const u={debug:(...r)=>o(console.debug,...r),log:(...r)=>o(console.log,...r),warn:(...r)=>o(console.warn,...r),error:(...r)=>o(console.error,...r)};let s;try{s=a.main(),s instanceof Promise&&console.warn("The background's main() function return a promise, but it must be synchronous")}catch(r){throw u.error("The background crashed on startup!"),r}return s})();
@@ -0,0 +1 @@
(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const r of document.querySelectorAll('link[rel="modulepreload"]'))c(r);new MutationObserver(r=>{for(const s of r)if(s.type==="childList")for(const o of s.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&c(o)}).observe(document,{childList:!0,subtree:!0});function t(r){const s={};return r.integrity&&(s.integrity=r.integrity),r.referrerPolicy&&(s.referrerPolicy=r.referrerPolicy),r.crossOrigin==="use-credentials"?s.credentials="include":r.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function c(r){if(r.ep)return;r.ep=!0;const s=t(r);fetch(r.href,s)}})();try{}catch(n){console.error("[wxt] Failed to initialize plugins",n)}const p=globalThis.browser?.runtime?.id?globalThis.browser:globalThis.chrome,u=p;async function f(){return(await u.storage.sync.get(["organizationId"])).organizationId}async function g(){const e=(await u.tabs.query({active:!0,currentWindow:!0}))[0];if(!e?.url)return null;const t=new URL(e.url);return/\/chat\/([a-f0-9-]+)/.exec(t.pathname)?.[1]??null}function l(n,e="info"){const t=document.getElementById("status");t&&(t.className=`status ${e}`,t.textContent=n,e==="success"&&setTimeout(()=>{t.textContent="",t.className=""},3e3))}function i(n){return document.getElementById(n)}async function w(){if(!await f()){const e=i("setupNotice");e&&(e.style.display="block");const t=i("exportCurrent"),c=i("exportAll");t&&(t.disabled=!0),c&&(c.disabled=!0)}}function h(){i("openOptions")?.addEventListener("click",e=>{e.preventDefault(),u.runtime.openOptionsPage()})}async function m(){const n=i("exportCurrent");if(n){n.disabled=!0,l("Fetching conversation...","info");try{const e=await f(),t=await g();if(!e)throw new Error("Organization ID not configured. Click the setup link above to configure it.");if(!t)throw new Error("Could not detect a conversation ID. Make sure you are on a Claude.ai conversation page.");const r=(await u.tabs.query({active:!0,currentWindow:!0}))[0];if(!r?.url?.includes("claude.ai"))throw new Error("Please navigate to a Claude.ai conversation page first.");if(!r.id)throw new Error("Could not identify active tab.");const s=i("format"),o=i("includeMetadata");try{const a=await u.tabs.sendMessage(r.id,{action:"exportConversation",conversationId:t,orgId:e,format:s?.value??"json",includeMetadata:o?.checked??!0});if(a?.success)l("Conversation exported successfully!","success");else{const d=a?.error??"Export failed";console.error("Export failed:",d,a?.details),l(d,"error")}}catch(a){const d=a instanceof Error?a.message:String(a);throw d.includes("connection")||d.includes("Receiving end")?new Error("Cannot connect to page. Please ensure you are on a Claude.ai conversation page (https://claude.ai/chat/...) and refresh if needed."):a}}catch(e){const t=e instanceof Error?e.message:"Unknown error occurred";l(t,"error")}finally{n.disabled=!1}}}function b(){u.tabs.create({url:u.runtime.getURL("browse.html")})}async function v(){const n=i("exportAll");if(n){n.disabled=!0,l("Fetching all conversations...","info");try{const e=await f();if(!e)throw new Error("Organization ID not configured. Click the setup link above to configure it.");const c=(await u.tabs.query({active:!0,currentWindow:!0}))[0];if(!c?.url?.includes("claude.ai"))throw new Error('Please navigate to a Claude.ai page first, or use "Browse All Conversations" instead.');if(!c.id)throw new Error("Could not identify active tab.");const r=i("format"),s=i("includeMetadata");try{const o=await u.tabs.sendMessage(c.id,{action:"exportAllConversations",orgId:e,format:r?.value??"json",includeMetadata:s?.checked??!0});if(o?.success)o.warnings?l(o.warnings,"info"):l(`Exported ${o.count??0} conversations!`,"success");else{const a=o?.error??"Export failed";console.error("Export failed:",a,o?.details),l(a,"error")}}catch(o){const a=o instanceof Error?o.message:String(o);throw a.includes("connection")||a.includes("Receiving end")?new Error('Cannot connect to page. This feature requires a Claude.ai page (https://claude.ai/chat/... or similar). Try "Browse All Conversations" instead - it works from any tab.'):o}}catch(e){const t=e instanceof Error?e.message:"Unknown error occurred";l(t,"error")}finally{n.disabled=!1}}}document.addEventListener("DOMContentLoaded",()=>{w(),h();const n=i("exportCurrent"),e=i("browseConversations"),t=i("exportAll");n?.addEventListener("click",()=>void m()),e?.addEventListener("click",b),t?.addEventListener("click",()=>void v())});
File diff suppressed because one or more lines are too long
+22
View File
@@ -0,0 +1,22 @@
/* Optional: Add a floating export button on Claude.ai pages */
.claude-exporter-button {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 9999;
background: #5436da;
color: white;
border: none;
border-radius: 50%;
width: 60px;
height: 60px;
font-size: 24px;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transition: all 0.3s;
}
.claude-exporter-button:hover {
background: #4329b8;
transform: scale(1.1);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

+1
View File
@@ -0,0 +1 @@
{"manifest_version":3,"name":"Claude Conversation Exporter","description":"Export your Claude.ai conversations with full message history","version":"1.0.0","permissions":["activeTab","storage"],"host_permissions":["https://claude.ai/*"],"background":{"service_worker":"background.js"},"action":{"default_title":"Claude Conversation Exporter","default_popup":"popup.html"},"content_scripts":[{"matches":["https://claude.ai/*"],"js":["content-scripts/content.js"]}]}
Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

+119
View File
@@ -0,0 +1,119 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Claude Conversation Exporter</title>
<style>
body {
width: 350px;
padding: 8px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
}
h2 {
margin-top: 0;
color: #333;
font-size: 18px;
}
button {
width: 100%;
padding: 10px;
margin: 5px 0;
border: none;
border-radius: 5px;
background: #5436da;
color: white;
font-size: 14px;
cursor: pointer;
transition: background 0.2s;
}
button:hover {
background: #4329b8;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
#browseConversations {
background: #28a745;
margin-top: 10px;
}
#browseConversations:hover {
background: #218838;
}
.status {
margin: 10px 0;
padding: 10px;
border-radius: 5px;
font-size: 13px;
}
.success {
background: #d4edda;
color: #155724;
}
.error {
background: #f8d7da;
color: #721c24;
}
.info {
background: #d1ecf1;
color: #0c5460;
}
.options {
margin: 15px 0;
}
label {
display: block;
margin: 8px 0;
font-size: 13px;
}
input[type='checkbox'] {
margin-right: 5px;
}
select {
width: 100%;
padding: 5px;
margin-top: 5px;
border: 1px solid #ddd;
border-radius: 3px;
}
</style>
<script type="module" crossorigin src="/chunks/popup-BzajcGsb.js"></script>
</head>
<body>
<div style="margin: -8px -8px 15px -8px">
<img src="/popup-header.png" alt="Claude Conversation Exporter" style="width: 100%; height: auto; display: block">
</div>
<div style="padding: 0 7px">
<div id="setupNotice" class="status info" style="display: none">
⚠️ Organization ID not configured.
<a href="#" id="openOptions">Click here to set it up</a>
</div>
<div class="options">
<label>
Export format:
<select id="format">
<option value="json">JSON (Full Data)</option>
<option value="markdown">Markdown</option>
<option value="text">Plain Text</option>
</select>
</label>
<label>
<input type="checkbox" id="includeMetadata" checked>
Include metadata (timestamps, model, etc.)
</label>
</div>
<button id="exportCurrent">Export Current Conversation</button>
<button id="exportAll">Export All (requires Claude.ai tab)</button>
<button id="browseConversations">Browse All Conversations</button>
<div id="status"></div>
</div>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
var background=(function(){"use strict";const t=globalThis.browser?.runtime?.id?globalThis.browser:globalThis.chrome;function i(r){return r==null||typeof r=="function"?{main:r}:r}const a=i(()=>{console.log("Claude Conversation Exporter background script loaded"),t.runtime.onInstalled.addListener(()=>{console.log("Claude Conversation Exporter installed"),(async()=>{try{const r=await t.tabs.query({url:"https://claude.ai/*"});for(const e of r)if(e.id)try{await t.scripting.executeScript({target:{tabId:e.id},files:["content-scripts/content.js"]})}catch(n){console.log("Could not inject into tab",e.id,n)}}catch(r){console.error("Error querying or injecting tabs:",r)}})()}),t.runtime.onMessage.addListener((r,e)=>r.action==="ensureContentScript"?(async()=>{try{const c=(await t.tabs.query({active:!0,currentWindow:!0}))[0];return c?.id?(await t.scripting.executeScript({target:{tabId:c.id},files:["content-scripts/content.js"]}),{success:!0}):{success:!1,error:"No active tab found"}}catch(n){return{success:!1,error:n instanceof Error?n.message:"Unknown error"}}})():!1)});function d(){}function o(r,...e){}const u={debug:(...r)=>o(console.debug,...r),log:(...r)=>o(console.log,...r),warn:(...r)=>o(console.warn,...r),error:(...r)=>o(console.error,...r)};let s;try{s=a.main(),s instanceof Promise&&console.warn("The background's main() function return a promise, but it must be synchronous")}catch(r){throw u.error("The background crashed on startup!"),r}return s})();
@@ -0,0 +1 @@
(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const r of document.querySelectorAll('link[rel="modulepreload"]'))c(r);new MutationObserver(r=>{for(const s of r)if(s.type==="childList")for(const o of s.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&c(o)}).observe(document,{childList:!0,subtree:!0});function t(r){const s={};return r.integrity&&(s.integrity=r.integrity),r.referrerPolicy&&(s.referrerPolicy=r.referrerPolicy),r.crossOrigin==="use-credentials"?s.credentials="include":r.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function c(r){if(r.ep)return;r.ep=!0;const s=t(r);fetch(r.href,s)}})();try{}catch(n){console.error("[wxt] Failed to initialize plugins",n)}const p=globalThis.browser?.runtime?.id?globalThis.browser:globalThis.chrome,u=p;async function f(){return(await u.storage.sync.get(["organizationId"])).organizationId}async function g(){const e=(await u.tabs.query({active:!0,currentWindow:!0}))[0];if(!e?.url)return null;const t=new URL(e.url);return/\/chat\/([a-f0-9-]+)/.exec(t.pathname)?.[1]??null}function l(n,e="info"){const t=document.getElementById("status");t&&(t.className=`status ${e}`,t.textContent=n,e==="success"&&setTimeout(()=>{t.textContent="",t.className=""},3e3))}function i(n){return document.getElementById(n)}async function w(){if(!await f()){const e=i("setupNotice");e&&(e.style.display="block");const t=i("exportCurrent"),c=i("exportAll");t&&(t.disabled=!0),c&&(c.disabled=!0)}}function h(){i("openOptions")?.addEventListener("click",e=>{e.preventDefault(),u.runtime.openOptionsPage()})}async function m(){const n=i("exportCurrent");if(n){n.disabled=!0,l("Fetching conversation...","info");try{const e=await f(),t=await g();if(!e)throw new Error("Organization ID not configured. Click the setup link above to configure it.");if(!t)throw new Error("Could not detect a conversation ID. Make sure you are on a Claude.ai conversation page.");const r=(await u.tabs.query({active:!0,currentWindow:!0}))[0];if(!r?.url?.includes("claude.ai"))throw new Error("Please navigate to a Claude.ai conversation page first.");if(!r.id)throw new Error("Could not identify active tab.");const s=i("format"),o=i("includeMetadata");try{const a=await u.tabs.sendMessage(r.id,{action:"exportConversation",conversationId:t,orgId:e,format:s?.value??"json",includeMetadata:o?.checked??!0});if(a?.success)l("Conversation exported successfully!","success");else{const d=a?.error??"Export failed";console.error("Export failed:",d,a?.details),l(d,"error")}}catch(a){const d=a instanceof Error?a.message:String(a);throw d.includes("connection")||d.includes("Receiving end")?new Error("Cannot connect to page. Please ensure you are on a Claude.ai conversation page (https://claude.ai/chat/...) and refresh if needed."):a}}catch(e){const t=e instanceof Error?e.message:"Unknown error occurred";l(t,"error")}finally{n.disabled=!1}}}function b(){u.tabs.create({url:u.runtime.getURL("browse.html")})}async function v(){const n=i("exportAll");if(n){n.disabled=!0,l("Fetching all conversations...","info");try{const e=await f();if(!e)throw new Error("Organization ID not configured. Click the setup link above to configure it.");const c=(await u.tabs.query({active:!0,currentWindow:!0}))[0];if(!c?.url?.includes("claude.ai"))throw new Error('Please navigate to a Claude.ai page first, or use "Browse All Conversations" instead.');if(!c.id)throw new Error("Could not identify active tab.");const r=i("format"),s=i("includeMetadata");try{const o=await u.tabs.sendMessage(c.id,{action:"exportAllConversations",orgId:e,format:r?.value??"json",includeMetadata:s?.checked??!0});if(o?.success)o.warnings?l(o.warnings,"info"):l(`Exported ${o.count??0} conversations!`,"success");else{const a=o?.error??"Export failed";console.error("Export failed:",a,o?.details),l(a,"error")}}catch(o){const a=o instanceof Error?o.message:String(o);throw a.includes("connection")||a.includes("Receiving end")?new Error('Cannot connect to page. This feature requires a Claude.ai page (https://claude.ai/chat/... or similar). Try "Browse All Conversations" instead - it works from any tab.'):o}}catch(e){const t=e instanceof Error?e.message:"Unknown error occurred";l(t,"error")}finally{n.disabled=!1}}}document.addEventListener("DOMContentLoaded",()=>{w(),h();const n=i("exportCurrent"),e=i("browseConversations"),t=i("exportAll");n?.addEventListener("click",()=>void m()),e?.addEventListener("click",b),t?.addEventListener("click",()=>void v())});
File diff suppressed because one or more lines are too long
+22
View File
@@ -0,0 +1,22 @@
/* Optional: Add a floating export button on Claude.ai pages */
.claude-exporter-button {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 9999;
background: #5436da;
color: white;
border: none;
border-radius: 50%;
width: 60px;
height: 60px;
font-size: 24px;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transition: all 0.3s;
}
.claude-exporter-button:hover {
background: #4329b8;
transform: scale(1.1);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

+1
View File
@@ -0,0 +1 @@
{"manifest_version":2,"name":"Claude Conversation Exporter","description":"Export your Claude.ai conversations with full message history","version":"1.0.0","permissions":["activeTab","storage","https://claude.ai/*"],"background":{"scripts":["background.js"]},"browser_action":{"default_title":"Claude Conversation Exporter","default_popup":"popup.html"},"content_scripts":[{"matches":["https://claude.ai/*"],"js":["content-scripts/content.js"]}]}
Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

+119
View File
@@ -0,0 +1,119 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Claude Conversation Exporter</title>
<style>
body {
width: 350px;
padding: 8px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
}
h2 {
margin-top: 0;
color: #333;
font-size: 18px;
}
button {
width: 100%;
padding: 10px;
margin: 5px 0;
border: none;
border-radius: 5px;
background: #5436da;
color: white;
font-size: 14px;
cursor: pointer;
transition: background 0.2s;
}
button:hover {
background: #4329b8;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
#browseConversations {
background: #28a745;
margin-top: 10px;
}
#browseConversations:hover {
background: #218838;
}
.status {
margin: 10px 0;
padding: 10px;
border-radius: 5px;
font-size: 13px;
}
.success {
background: #d4edda;
color: #155724;
}
.error {
background: #f8d7da;
color: #721c24;
}
.info {
background: #d1ecf1;
color: #0c5460;
}
.options {
margin: 15px 0;
}
label {
display: block;
margin: 8px 0;
font-size: 13px;
}
input[type='checkbox'] {
margin-right: 5px;
}
select {
width: 100%;
padding: 5px;
margin-top: 5px;
border: 1px solid #ddd;
border-radius: 3px;
}
</style>
<script type="module" crossorigin src="/chunks/popup-BzajcGsb.js"></script>
</head>
<body>
<div style="margin: -8px -8px 15px -8px">
<img src="/popup-header.png" alt="Claude Conversation Exporter" style="width: 100%; height: auto; display: block">
</div>
<div style="padding: 0 7px">
<div id="setupNotice" class="status info" style="display: none">
⚠️ Organization ID not configured.
<a href="#" id="openOptions">Click here to set it up</a>
</div>
<div class="options">
<label>
Export format:
<select id="format">
<option value="json">JSON (Full Data)</option>
<option value="markdown">Markdown</option>
<option value="text">Plain Text</option>
</select>
</label>
<label>
<input type="checkbox" id="includeMetadata" checked>
Include metadata (timestamps, model, etc.)
</label>
</div>
<button id="exportCurrent">Export Current Conversation</button>
<button id="exportAll">Export All (requires Claude.ai tab)</button>
<button id="browseConversations">Browse All Conversations</button>
<div id="status"></div>
</div>
</body>
</html>
+49
View File
@@ -0,0 +1,49 @@
{
"globals": {
"AutoMount": true,
"AutoMountOptions": true,
"Browser": true,
"ContentScriptAnchoredOptions": true,
"ContentScriptAppendMode": true,
"ContentScriptContext": true,
"ContentScriptInlinePositioningOptions": true,
"ContentScriptModalPositioningOptions": true,
"ContentScriptOverlayAlignment": true,
"ContentScriptOverlayPositioningOptions": true,
"ContentScriptPositioningOptions": true,
"ContentScriptUi": true,
"ContentScriptUiOptions": true,
"IframeContentScriptUi": true,
"IframeContentScriptUiOptions": true,
"InjectScriptOptions": true,
"IntegratedContentScriptUi": true,
"IntegratedContentScriptUiOptions": true,
"InvalidMatchPattern": true,
"MatchPattern": true,
"MigrationError": true,
"ScriptPublicPath": true,
"ShadowRootContentScriptUi": true,
"ShadowRootContentScriptUiOptions": true,
"StopAutoMount": true,
"StorageArea": true,
"StorageAreaChanges": true,
"StorageItemKey": true,
"WxtAppConfig": true,
"WxtStorage": true,
"WxtStorageItem": true,
"WxtWindowEventMap": true,
"browser": true,
"createIframeUi": true,
"createIntegratedUi": true,
"createShadowRootUi": true,
"defineAppConfig": true,
"defineBackground": true,
"defineContentScript": true,
"defineUnlistedScript": true,
"defineWxtPlugin": true,
"fakeBrowser": true,
"injectScript": true,
"storage": true,
"useAppConfig": true
}
}
+28
View File
@@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"noEmit": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"strict": true,
"skipLibCheck": true,
"paths": {
"@": [".."],
"@/*": ["../*"],
"~": [".."],
"~/*": ["../*"],
"@@": [".."],
"@@/*": ["../*"],
"~~": [".."],
"~~/*": ["../*"]
}
},
"include": [
"../**/*",
"./wxt.d.ts"
],
"exclude": ["../.output"]
}
-31
View File
@@ -1,31 +0,0 @@
#!/bin/bash
set -e
echo "Building Firefox extension..."
cd /home/jasonross/workspace/Claude-Conversation-Exporter-Firefox-TS
# Run TypeScript compiler
echo "Running TypeScript compiler..."
npx tsc --noEmit
# Run Vite build
echo "Running Vite build..."
npx vite build --mode firefox
# Check if content script was built correctly
echo ""
echo "Checking content script..."
CONTENT_SIZE=$(stat -f%z dist/firefox/src/content.js 2>/dev/null || stat -c%s dist/firefox/src/content.js 2>/dev/null)
echo "Content script size: $CONTENT_SIZE bytes"
if [ "$CONTENT_SIZE" -lt 100 ]; then
echo "❌ ERROR: Content script is too small! ($CONTENT_SIZE bytes)"
echo "Content of content.js:"
cat dist/firefox/src/content.js
else
echo "✅ Content script built successfully"
fi
echo ""
echo "Build complete!"
ls -lh dist/firefox/src/*.js
-40
View File
@@ -1,40 +0,0 @@
#!/bin/bash
# Build script for Chrome extension
# This script builds the content script separately as an IIFE to avoid
# the broken dynamic import loader that the vite-plugin-web-extension creates
set -e
echo "Building Chrome extension..."
# Step 1: Build content script separately as IIFE
echo "Step 1: Building content script..."
npx vite build --config vite.content-chrome.config.ts
# Step 2: Build main extension with vite-plugin-web-extension
echo "Step 2: Building main extension..."
BROWSER=chrome npx vite build --mode chrome
# Step 3: Copy the IIFE content script over the stub
echo "Step 3: Replacing content script stub with IIFE bundle..."
cp dist-temp-content-chrome/content.js dist/chrome/src/chrome/scripts/content.js
# Step 3.5: Fix serviceWorker.js import path (make it relative)
echo "Step 3.5: Fixing serviceWorker.js import path..."
sed -i 's|"/src/|"./src/|g' dist/chrome/serviceWorker.js
# Step 4: Copy icon files
echo "Step 4: Copying icon files..."
mkdir -p dist/chrome/src
cp src/icon16.png dist/chrome/src/
cp src/icon48.png dist/chrome/src/
cp src/icon128.png dist/chrome/src/
# Step 5: Copy popup header image
echo "Step 5: Copying popup header..."
mkdir -p dist/chrome/src/chrome/scripts
cp src/popup-header.png dist/chrome/src/chrome/scripts/
echo "Chrome build complete!"
echo "Load unpacked extension from: dist/chrome/"
-38
View File
@@ -1,38 +0,0 @@
#!/bin/bash
set -e
cd /home/jasonross/workspace/Claude-Conversation-Exporter-Firefox-TS
echo "Building content script separately..."
npx vite build --config vite.content-firefox.config.ts
echo "Building main extension..."
npx vite build --mode firefox
echo "Copying content script to dist..."
cp dist-temp-content-firefox/content.js dist/firefox/src/firefox/scripts/content.js
rm -rf dist-temp-content-firefox
echo "Copying icon files..."
mkdir -p dist/firefox/src
cp src/icon16.png dist/firefox/src/
cp src/icon48.png dist/firefox/src/
cp src/icon128.png dist/firefox/src/
echo "Copying popup header..."
mkdir -p dist/firefox/src/firefox/scripts
cp src/popup-header.png dist/firefox/src/firefox/scripts/
echo ""
echo "Checking content script..."
CONTENT_SIZE=$(stat -c%s dist/firefox/src/firefox/scripts/content.js 2>/dev/null || stat -f%z dist/firefox/src/firefox/scripts/content.js)
echo "Content script size: $CONTENT_SIZE bytes"
if [ "$CONTENT_SIZE" -gt 1000 ]; then
echo "✅ Content script built successfully!"
else
echo "❌ ERROR: Content script is too small"
fi
echo ""
echo "Build complete!"
-45
View File
@@ -1,45 +0,0 @@
#!/bin/bash
# Script to convert Firefox browser.* API calls to Chrome chrome.* API calls
# This creates Chrome-specific versions of the extension scripts
set -e
echo "Converting Firefox scripts to Chrome API..."
# Files to convert
FILES=("popup.ts" "options.ts" "browse.ts" "content.ts" "background.ts")
for file in "${FILES[@]}"; do
echo "Converting $file..."
# Copy file to Chrome directory
cp "src/$file" "src/chrome/scripts/$file"
# Convert browser.* calls to chrome.*
# This uses sed to replace browser API calls with chrome API calls
# and wraps Promise-based calls with callbacks
sed -i 's/browser\.storage\.sync\.get/chrome.storage.sync.get/g' "src/chrome/scripts/$file"
sed -i 's/browser\.storage\.sync\.set/chrome.storage.sync.set/g' "src/chrome/scripts/$file"
sed -i 's/browser\.tabs\.query/chrome.tabs.query/g' "src/chrome/scripts/$file"
sed -i 's/browser\.tabs\.sendMessage/chrome.tabs.sendMessage/g' "src/chrome/scripts/$file"
sed -i 's/browser\.tabs\.create/chrome.tabs.create/g' "src/chrome/scripts/$file"
sed -i 's/browser\.runtime\.sendMessage/chrome.runtime.sendMessage/g' "src/chrome/scripts/$file"
sed -i 's/browser\.runtime\.getURL/chrome.runtime.getURL/g' "src/chrome/scripts/$file"
sed -i 's/browser\.runtime\.onMessage/chrome.runtime.onMessage/g' "src/chrome/scripts/$file"
echo "✅ Converted $file"
done
# Copy Firefox scripts to firefox directory
echo "Copying Firefox scripts..."
for file in "${FILES[@]}"; do
cp "src/$file" "src/firefox/scripts/$file"
echo "✅ Copied $file to Firefox directory"
done
echo ""
echo "Conversion complete!"
echo "Chrome scripts: src/chrome/scripts/"
echo "Firefox scripts: src/firefox/scripts/"
+81
View File
@@ -0,0 +1,81 @@
/**
* Background script for Claude Conversation Exporter
* Handles extension lifecycle events and content script injection
*/
import { defineBackground as wxtDefineBackground } from 'wxt/utils/define-background';
import type { ExtensionMessage } from '../src/types';
export default wxtDefineBackground(() => {
console.log('Claude Conversation Exporter background script loaded');
/**
* Handle extension installation
*/
browser.runtime.onInstalled.addListener(() => {
console.log('Claude Conversation Exporter installed');
// Inject content script into already-open Claude.ai tabs
void (async () => {
try {
const tabs = await browser.tabs.query({ url: 'https://claude.ai/*' });
for (const tab of tabs) {
if (tab.id) {
try {
await browser.scripting.executeScript({
target: { tabId: tab.id },
files: ['content-scripts/content.js'],
});
} catch (err) {
console.log('Could not inject into tab', tab.id, err);
}
}
}
} catch (err) {
console.error('Error querying or injecting tabs:', err);
}
})();
});
/**
* Handle messages from popup or other extension components
*/
browser.runtime.onMessage.addListener(
(request: ExtensionMessage, _sender: browser.runtime.MessageSender) => {
if (request.action === 'ensureContentScript') {
return (async () => {
try {
const tabs = await browser.tabs.query({
active: true,
currentWindow: true,
});
const activeTab = tabs[0];
if (activeTab?.id) {
await browser.scripting.executeScript({
target: { tabId: activeTab.id },
files: ['content-scripts/content.js'],
});
return { success: true };
}
return {
success: false,
error: 'No active tab found',
};
} catch (err) {
return {
success: false,
error: err instanceof Error ? err.message : 'Unknown error',
};
}
})();
}
// Return false to indicate no response will be sent
return false;
}
);
});
+255
View File
@@ -0,0 +1,255 @@
/**
* Content script for Claude Conversation Exporter
* Runs on claude.ai pages to handle conversation exports
*/
import { defineContentScript as wxtDefineContentScript } from 'wxt/utils/define-content-script';
import type {
Conversation,
ConversationListItem,
ExtensionMessage,
ExportFormat,
} from '../src/types';
import {
convertToMarkdown,
convertToText,
downloadFile,
generateFilename,
inferModel,
} from '../src/utils';
export default wxtDefineContentScript({
matches: ['https://claude.ai/*'],
cssInjectionMode: 'ui',
main() {
console.log('Claude Conversation Exporter content script loaded');
/**
* Fetch a single conversation from Claude API
*/
async function fetchConversation(
orgId: string,
conversationId: string
): Promise<Conversation> {
const url = `https://claude.ai/api/organizations/${orgId}/chat_conversations/${conversationId}?tree=True&rendering_mode=messages&render_all_tools=true`;
const response = await fetch(url, {
credentials: 'include',
headers: {
Accept: 'application/json',
},
});
if (!response.ok) {
throw new Error(`Failed to fetch conversation: ${response.status}`);
}
return (await response.json()) as Conversation;
}
/**
* Fetch all conversations from Claude API
*/
async function fetchAllConversations(orgId: string): Promise<ConversationListItem[]> {
const url = `https://claude.ai/api/organizations/${orgId}/chat_conversations`;
const response = await fetch(url, {
credentials: 'include',
headers: {
Accept: 'application/json',
},
});
if (!response.ok) {
throw new Error(`Failed to fetch conversations: ${response.status}`);
}
return (await response.json()) as ConversationListItem[];
}
/**
* Export a single conversation
*/
async function exportConversation(
orgId: string,
conversationId: string,
format: ExportFormat,
includeMetadata: boolean
): Promise<void> {
console.log('Fetching conversation:', conversationId);
const data = await fetchConversation(orgId, conversationId);
console.log('Conversation data fetched successfully:', data);
// Infer model if null
const modelName = inferModel(data);
data.model = modelName;
let content: string;
let filename: string;
let type: string;
switch (format) {
case 'markdown':
content = convertToMarkdown(data, includeMetadata);
filename = generateFilename(data.name || 'untitled', 'markdown', data.uuid);
type = 'text/markdown';
break;
case 'text':
content = convertToText(data, includeMetadata);
filename = generateFilename(data.name || 'untitled', 'text', data.uuid);
type = 'text/plain';
break;
default:
content = JSON.stringify(data, null, 2);
filename = generateFilename(data.name || 'untitled', 'json', data.uuid);
type = 'application/json';
}
console.log('Downloading file:', filename);
downloadFile(content, filename, type);
}
/**
* Export all conversations
*/
async function exportAllConversations(
orgId: string,
format: ExportFormat,
includeMetadata: boolean
): Promise<{ count: number; warnings?: string }> {
const conversations = await fetchAllConversations(orgId);
console.log(`Fetched ${conversations.length} conversations`);
if (format === 'json') {
// For JSON, export as a single file with all conversations
const filename = `claude-all-conversations-${
new Date().toISOString().split('T')[0] ?? 'export'
}.json`;
console.log('Downloading all conversations as JSON:', filename);
downloadFile(
JSON.stringify(conversations, null, 2),
filename,
'application/json'
);
return { count: conversations.length };
}
// For other formats, create individual files
let count = 0;
const errors: string[] = [];
for (const conv of conversations) {
try {
console.log(
`Fetching full conversation ${count + 1}/${conversations.length}: ${conv.uuid}`
);
const fullConv = await fetchConversation(orgId, conv.uuid);
// Infer model if null
fullConv.model = inferModel(fullConv);
let content: string;
let filename: string;
let type: string;
if (format === 'markdown') {
content = convertToMarkdown(fullConv, includeMetadata);
filename = generateFilename(conv.name || 'untitled', 'markdown', conv.uuid);
type = 'text/markdown';
} else {
content = convertToText(fullConv, includeMetadata);
filename = generateFilename(conv.name || 'untitled', 'text', conv.uuid);
type = 'text/plain';
}
downloadFile(content, filename, type);
count++;
// Add a small delay to avoid overwhelming the API
await new Promise((resolve) => setTimeout(resolve, 500));
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
console.error(`Failed to export conversation ${conv.uuid}:`, errorMsg);
errors.push(`${conv.name || conv.uuid}: ${errorMsg}`);
}
}
if (errors.length > 0) {
console.warn('Some conversations failed to export:', errors);
return {
count,
warnings: `Exported ${count}/${conversations.length} conversations. Some failed: ${errors.join('; ')}`,
};
}
return { count };
}
/**
* Handle messages from popup or background script
*/
(async () => {
browser.runtime.onMessage.addListener(
(request: ExtensionMessage, _sender: browser.runtime.MessageSender) => {
if (request.action === 'exportConversation') {
console.log('Export conversation request received:', request);
return (async () => {
try {
await exportConversation(
request.orgId,
request.conversationId,
request.format,
request.includeMetadata
);
return { success: true };
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
const errorStack = error instanceof Error ? error.stack : undefined;
console.error('Export conversation error:', error);
return {
success: false,
error: errorMsg,
details: errorStack,
};
}
})();
}
if (request.action === 'exportAllConversations') {
console.log('Export all conversations request received:', request);
return (async () => {
try {
const result = await exportAllConversations(
request.orgId,
request.format,
request.includeMetadata
);
return {
success: true,
count: result.count,
warnings: result.warnings,
};
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
const errorStack = error instanceof Error ? error.stack : undefined;
console.error('Export all conversations error:', error);
return {
success: false,
error: errorMsg,
details: errorStack,
};
}
})();
}
// Return false to indicate no response will be sent
return false;
}
);
})();
},
} as const);
+123
View File
@@ -0,0 +1,123 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Claude Conversation Exporter</title>
<style>
body {
width: 350px;
padding: 8px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
}
h2 {
margin-top: 0;
color: #333;
font-size: 18px;
}
button {
width: 100%;
padding: 10px;
margin: 5px 0;
border: none;
border-radius: 5px;
background: #5436da;
color: white;
font-size: 14px;
cursor: pointer;
transition: background 0.2s;
}
button:hover {
background: #4329b8;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
#browseConversations {
background: #28a745;
margin-top: 10px;
}
#browseConversations:hover {
background: #218838;
}
.status {
margin: 10px 0;
padding: 10px;
border-radius: 5px;
font-size: 13px;
}
.success {
background: #d4edda;
color: #155724;
}
.error {
background: #f8d7da;
color: #721c24;
}
.info {
background: #d1ecf1;
color: #0c5460;
}
.options {
margin: 15px 0;
}
label {
display: block;
margin: 8px 0;
font-size: 13px;
}
input[type='checkbox'] {
margin-right: 5px;
}
select {
width: 100%;
padding: 5px;
margin-top: 5px;
border: 1px solid #ddd;
border-radius: 3px;
}
</style>
</head>
<body>
<div style="margin: -8px -8px 15px -8px">
<img
src="/popup-header.png"
alt="Claude Conversation Exporter"
style="width: 100%; height: auto; display: block"
/>
</div>
<div style="padding: 0 7px">
<div id="setupNotice" class="status info" style="display: none">
⚠️ Organization ID not configured.
<a href="#" id="openOptions">Click here to set it up</a>
</div>
<div class="options">
<label>
Export format:
<select id="format">
<option value="json">JSON (Full Data)</option>
<option value="markdown">Markdown</option>
<option value="text">Plain Text</option>
</select>
</label>
<label>
<input type="checkbox" id="includeMetadata" checked />
Include metadata (timestamps, model, etc.)
</label>
</div>
<button id="exportCurrent">Export Current Conversation</button>
<button id="exportAll">Export All (requires Claude.ai tab)</button>
<button id="browseConversations">Browse All Conversations</button>
<div id="status"></div>
</div>
<script type="module" src="./main.ts"></script>
</body>
</html>
+257
View File
@@ -0,0 +1,257 @@
/**
* Popup script for Claude Conversation Exporter
* Handles the extension popup UI and user interactions
*/
import type { ExtensionResponse } from '../../src/types';
/**
* Get organization ID from storage
*/
async function getOrgId(): Promise<string | undefined> {
const result = await browser.storage.sync.get(['organizationId']);
return result['organizationId'] as string | undefined;
}
/**
* Get current conversation ID from active tab URL
*/
async function getCurrentConversationId(): Promise<string | null> {
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
const tab = tabs[0];
if (!tab?.url) {
return null;
}
const url = new URL(tab.url);
const match = /\/chat\/([a-f0-9-]+)/.exec(url.pathname);
return match?.[1] ?? null;
}
/**
* Show status message to user
*/
function showStatus(message: string, type: 'info' | 'success' | 'error' = 'info'): void {
const statusEl = document.getElementById('status');
if (!statusEl) return;
statusEl.className = `status ${type}`;
statusEl.textContent = message;
if (type === 'success') {
setTimeout(() => {
statusEl.textContent = '';
statusEl.className = '';
}, 3000);
}
}
/**
* Get HTML element by ID with type checking
*/
function getElement<T extends HTMLElement>(id: string): T | null {
return document.getElementById(id) as T | null;
}
/**
* Check if org ID is configured on popup load
*/
async function initializePopup(): Promise<void> {
const orgId = await getOrgId();
if (!orgId) {
const setupNotice = getElement('setupNotice');
if (setupNotice) {
setupNotice.style.display = 'block';
}
const exportCurrentBtn = getElement<HTMLButtonElement>('exportCurrent');
const exportAllBtn = getElement<HTMLButtonElement>('exportAll');
if (exportCurrentBtn) exportCurrentBtn.disabled = true;
if (exportAllBtn) exportAllBtn.disabled = true;
}
}
/**
* Handle options link click
*/
function setupOptionsLink(): void {
const openOptionsBtn = getElement('openOptions');
openOptionsBtn?.addEventListener('click', (e) => {
e.preventDefault();
void browser.runtime.openOptionsPage();
});
}
/**
* Handle export current conversation
*/
async function handleExportCurrent(): Promise<void> {
const button = getElement<HTMLButtonElement>('exportCurrent');
if (!button) return;
button.disabled = true;
showStatus('Fetching conversation...', 'info');
try {
const orgId = await getOrgId();
const conversationId = await getCurrentConversationId();
if (!orgId) {
throw new Error(
'Organization ID not configured. Click the setup link above to configure it.'
);
}
if (!conversationId) {
throw new Error(
'Could not detect a conversation ID. Make sure you are on a Claude.ai conversation page.'
);
}
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
const tab = tabs[0];
// Check if we're on Claude.ai
if (!tab?.url?.includes('claude.ai')) {
throw new Error('Please navigate to a Claude.ai conversation page first.');
}
if (!tab.id) {
throw new Error('Could not identify active tab.');
}
const formatSelect = getElement<HTMLSelectElement>('format');
const metadataCheck = getElement<HTMLInputElement>('includeMetadata');
try {
const response = (await browser.tabs.sendMessage(tab.id, {
action: 'exportConversation',
conversationId,
orgId,
format: formatSelect?.value ?? 'json',
includeMetadata: metadataCheck?.checked ?? true,
})) as ExtensionResponse;
if (response?.success) {
showStatus('Conversation exported successfully!', 'success');
} else {
const errorMsg = response?.error ?? 'Export failed';
console.error('Export failed:', errorMsg, response?.details);
showStatus(errorMsg, 'error');
}
} catch (sendError) {
// Handle "Could not establish connection" error
const errorMsg = sendError instanceof Error ? sendError.message : String(sendError);
if (errorMsg.includes('connection') || errorMsg.includes('Receiving end')) {
throw new Error(
'Cannot connect to page. Please ensure you are on a Claude.ai conversation page (https://claude.ai/chat/...) and refresh if needed.'
);
}
throw sendError;
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error occurred';
showStatus(message, 'error');
} finally {
button.disabled = false;
}
}
/**
* Handle browse conversations button
*/
function handleBrowseConversations(): void {
void browser.tabs.create({ url: browser.runtime.getURL('browse.html') });
}
/**
* Handle export all conversations
*/
async function handleExportAll(): Promise<void> {
const button = getElement<HTMLButtonElement>('exportAll');
if (!button) return;
button.disabled = true;
showStatus('Fetching all conversations...', 'info');
try {
const orgId = await getOrgId();
if (!orgId) {
throw new Error(
'Organization ID not configured. Click the setup link above to configure it.'
);
}
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
const tab = tabs[0];
// Check if we're on Claude.ai
if (!tab?.url?.includes('claude.ai')) {
throw new Error(
'Please navigate to a Claude.ai page first, or use "Browse All Conversations" instead.'
);
}
if (!tab.id) {
throw new Error('Could not identify active tab.');
}
const formatSelect = getElement<HTMLSelectElement>('format');
const metadataCheck = getElement<HTMLInputElement>('includeMetadata');
try {
const response = (await browser.tabs.sendMessage(tab.id, {
action: 'exportAllConversations',
orgId,
format: formatSelect?.value ?? 'json',
includeMetadata: metadataCheck?.checked ?? true,
})) as ExtensionResponse;
if (response?.success) {
if (response.warnings) {
showStatus(response.warnings, 'info');
} else {
showStatus(`Exported ${response.count ?? 0} conversations!`, 'success');
}
} else {
const errorMsg = response?.error ?? 'Export failed';
console.error('Export failed:', errorMsg, response?.details);
showStatus(errorMsg, 'error');
}
} catch (sendError) {
// Handle "Could not establish connection" error
const errorMsg = sendError instanceof Error ? sendError.message : String(sendError);
if (errorMsg.includes('connection') || errorMsg.includes('Receiving end')) {
throw new Error(
'Cannot connect to page. This feature requires a Claude.ai page (https://claude.ai/chat/... or similar). Try "Browse All Conversations" instead - it works from any tab.'
);
}
throw sendError;
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error occurred';
showStatus(message, 'error');
} finally {
button.disabled = false;
}
}
/**
* Initialize popup when DOM is loaded
*/
document.addEventListener('DOMContentLoaded', () => {
void initializePopup();
setupOptionsLink();
const exportCurrentBtn = getElement('exportCurrent');
const browseBtn = getElement('browseConversations');
const exportAllBtn = getElement('exportAll');
exportCurrentBtn?.addEventListener('click', () => void handleExportCurrent());
browseBtn?.addEventListener('click', handleBrowseConversations);
exportAllBtn?.addEventListener('click', () => void handleExportAll());
});
+18 -19
View File
@@ -4,24 +4,25 @@
"description": "Export your Claude.ai conversations in various formats - Chrome and Firefox extension with TypeScript support",
"type": "module",
"scripts": {
"build": "tsc && vite build",
"build:firefox": "bash build-firefox.sh",
"build:chrome": "bash build-chrome.sh",
"dev": "vite build --watch",
"dev:firefox": "vite build --mode firefox --watch",
"dev:chrome": "vite build --mode chrome --watch",
"type-check": "tsc --noEmit",
"lint": "eslint src/**/*.ts",
"lint:fix": "eslint src/**/*.ts --fix",
"format": "prettier --check \"src/**/*.{ts,html,css,json}\"",
"format:fix": "prettier --write \"src/**/*.{ts,html,css,json}\"",
"dev": "wxt",
"dev:firefox": "wxt -b firefox",
"dev:chrome": "wxt -b chrome",
"build": "wxt build",
"build:firefox": "wxt build -b firefox",
"build:chrome": "wxt build -b chrome",
"zip": "wxt zip",
"zip:firefox": "wxt zip -b firefox",
"zip:chrome": "wxt zip -b chrome",
"type-check": "wxt type-check",
"lint": "eslint entrypoints/**/*.ts src/**/*.ts",
"lint:fix": "eslint entrypoints/**/*.ts src/**/*.ts --fix",
"format": "prettier --check \"**/*.{ts,html,css,json}\"",
"format:fix": "prettier --write \"**/*.{ts,html,css,json}\"",
"test": "vitest run",
"test:ui": "vitest run --ui",
"test:coverage": "vitest run --coverage",
"check": "pnpm type-check && pnpm lint && pnpm format && pnpm test:run",
"firefox:run": "web-ext run --source-dir=dist/firefox --start-url https://claude.ai",
"chrome:run": "echo 'Load dist/chrome in chrome://extensions with Developer Mode enabled'",
"clean": "rm -rf dist"
"check": "pnpm type-check && pnpm lint && pnpm format && pnpm test",
"clean": "rm -rf .wxt .output dist"
},
"keywords": [
"claude",
@@ -37,7 +38,6 @@
"packageManager": "pnpm@10.18.1",
"devDependencies": {
"@eslint/js": "^9.38.0",
"@samrum/vite-plugin-web-extension": "^5.1.1",
"@types/chrome": "^0.1.24",
"@types/firefox-webext-browser": "^143.0.0",
"@types/jszip": "^3.4.0",
@@ -53,10 +53,9 @@
"prettier": "^3.6.2",
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.1",
"vite": "^7.1.10",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.2.4",
"web-ext": "^9.0.0"
"web-ext": "^9.0.0",
"wxt": "^0.20.11"
},
"dependencies": {
"jszip": "^3.10.1"
+962 -129
View File
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
/* Optional: Add a floating export button on Claude.ai pages */
.claude-exporter-button {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 9999;
background: #5436da;
color: white;
border: none;
border-radius: 50%;
width: 60px;
height: 60px;
font-size: 24px;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transition: all 0.3s;
}
.claude-exporter-button:hover {
background: #4329b8;
transform: scale(1.1);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 627 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

+3 -1
View File
@@ -170,7 +170,9 @@ async function handleExportCurrent(): Promise<void> {
* Handle browse conversations button
*/
function handleBrowseConversations(): void {
void chrome.tabs.create({ url: chrome.runtime.getURL('src/chrome/scripts/browse.html') });
void chrome.tabs.create({
url: chrome.runtime.getURL('src/chrome/scripts/browse.html'),
});
}
/**
+3 -1
View File
@@ -168,7 +168,9 @@ async function handleExportCurrent(): Promise<void> {
* Handle browse conversations button
*/
function handleBrowseConversations(): void {
void browser.tabs.create({ url: browser.runtime.getURL('src/firefox/scripts/browse.html') });
void browser.tabs.create({
url: browser.runtime.getURL('src/firefox/scripts/browse.html'),
});
}
/**
+4 -16
View File
@@ -38,25 +38,13 @@
/* Path Mapping */
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
"@/*": ["entrypoints/*"],
"~/*": ["src/*"]
},
/* Browser Extension Types */
"types": ["firefox-webext-browser", "vitest/globals"]
},
"include": [
"src/**/*.ts",
"vite.config.ts",
"vite.content.config.ts",
"vite.content-chrome.config.ts",
"vite.content-firefox.config.ts",
"eslint.config.js"
],
"exclude": [
"node_modules",
"dist",
"src/**/*.js",
"src/chrome/scripts/**/*",
"src/firefox/scripts/**/*"
]
"include": ["entrypoints/**/*.ts", "src/**/*.ts", "wxt.config.ts", "*.config.ts", ".wxt/types/**/*.d.ts"],
"exclude": ["node_modules", "dist", ".wxt/**/*", "!.wxt/types/**/*.d.ts"]
}
+1
View File
@@ -0,0 +1 @@
{}
-74
View File
@@ -1,74 +0,0 @@
import { defineConfig } from 'vite';
import webExtension from '@samrum/vite-plugin-web-extension';
import tsconfigPaths from 'vite-tsconfig-paths';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
import { readFileSync } from 'fs';
const __dirname = dirname(fileURLToPath(import.meta.url));
// Determine target platform from mode or environment
function getTargetFromMode(mode: string): 'firefox' | 'chrome' {
return mode === 'firefox' ? 'firefox' : 'chrome';
}
export default defineConfig(({ mode }) => {
const target = getTargetFromMode(mode);
const manifestPath = resolve(__dirname, `src/${target}/manifest.json`);
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8')) as Record<
string,
unknown
>;
return {
plugins: [
tsconfigPaths(),
webExtension({
manifest,
useDynamicUrlWebAccessibleResources: false,
additionalInputs: {
html: [
`src/${target}/scripts/browse.html`,
`src/${target}/scripts/options.html`,
],
scripts: [`src/${target}/scripts/content.ts`],
},
}),
],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
build: {
outDir: `dist/${target}`,
emptyOutDir: true,
sourcemap: true,
rollupOptions: {
output: {
entryFileNames: '[name].js',
chunkFileNames: '[name].js',
assetFileNames: '[name].[ext]',
},
},
},
test: {
globals: true,
environment: 'happy-dom',
setupFiles: ['./src/tests/setup.ts'],
include: ['src/tests/**/*.test.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'dist/',
'src/tests/',
'**/*.d.ts',
'**/*.config.*',
'**/mockData',
],
},
},
};
});
-23
View File
@@ -1,23 +0,0 @@
import { defineConfig } from 'vite';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
export default defineConfig({
build: {
outDir: 'dist-temp-content-chrome',
emptyOutDir: true,
lib: {
entry: resolve(__dirname, 'src/chrome/scripts/content.ts'),
name: 'ContentScript',
formats: ['iife'],
fileName: () => 'content.js',
},
rollupOptions: {
output: {
extend: true,
},
},
},
});
-23
View File
@@ -1,23 +0,0 @@
import { defineConfig } from 'vite';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
export default defineConfig({
build: {
outDir: 'dist-temp-content-firefox',
emptyOutDir: true,
lib: {
entry: resolve(__dirname, 'src/firefox/scripts/content.ts'),
name: 'ContentScript',
formats: ['iife'],
fileName: () => 'content.js',
},
rollupOptions: {
output: {
extend: true,
},
},
},
});
-23
View File
@@ -1,23 +0,0 @@
import { defineConfig } from 'vite';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
export default defineConfig({
build: {
outDir: 'dist-temp-content',
emptyOutDir: true,
lib: {
entry: resolve(__dirname, 'src/content.ts'),
name: 'ContentScript',
formats: ['iife'],
fileName: () => 'content.js',
},
rollupOptions: {
output: {
extend: true,
},
},
},
});
+31
View File
@@ -0,0 +1,31 @@
/// <reference types="vitest" />
import { defineConfig } from 'vitest/config';
import { resolve } from 'path';
export default defineConfig({
resolve: {
alias: {
'~': resolve(__dirname, 'src'),
},
},
test: {
globals: true,
environment: 'happy-dom',
setupFiles: ['./src/tests/setup.ts'],
include: ['src/tests/**/*.test.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'dist/',
'.wxt/',
'.output/',
'src/tests/',
'**/*.d.ts',
'**/*.config.*',
'**/mockData',
],
},
},
});
+23
View File
@@ -0,0 +1,23 @@
import { defineConfig } from 'wxt';
import { resolve } from 'path';
// https://wxt.dev/api/config.html
export default defineConfig({
manifest: {
name: 'Claude Conversation Exporter',
description: 'Export your Claude.ai conversations with full message history',
permissions: ['activeTab', 'storage'],
host_permissions: ['https://claude.ai/*'],
},
imports: false,
vite: () => ({
resolve: {
alias: {
'~': resolve(__dirname, 'src'),
},
},
}),
runner: {
startUrls: ['https://claude.ai'],
},
});