fixed bugs
@@ -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())});
|
||||
@@ -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);
|
||||
}
|
||||
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 627 B |
|
After Width: | Height: | Size: 3.0 KiB |
@@ -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"]}]}
|
||||
|
After Width: | Height: | Size: 111 KiB |
@@ -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>
|
||||
@@ -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())});
|
||||
@@ -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);
|
||||
}
|
||||
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 627 B |
|
After Width: | Height: | Size: 3.0 KiB |
@@ -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"]}]}
|
||||
|
After Width: | Height: | Size: 111 KiB |
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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
|
||||
@@ -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/"
|
||||
@@ -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!"
|
||||
@@ -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/"
|
||||
@@ -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;
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
@@ -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());
|
||||
});
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 627 B |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 111 KiB |
@@ -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'),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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'),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -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',
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -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',
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -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'],
|
||||
},
|
||||
});
|
||||