diff --git a/apps/browser-extension/entrypoints/background.ts b/apps/browser-extension/entrypoints/background.ts index d8b26fdb..cd64c8e2 100644 --- a/apps/browser-extension/entrypoints/background.ts +++ b/apps/browser-extension/entrypoints/background.ts @@ -1,84 +1,145 @@ +import { TwitterImporter, type TwitterImportConfig } from '../utils/twitter-import'; +import { captureTwitterTokens } from '../utils/twitter-auth'; +import { CONTEXT_MENU_IDS, MESSAGE_TYPES, API_ENDPOINTS, CONTAINER_TAGS } from '../utils/constants'; +import type { ExtensionMessage, MemoryPayload } from '../utils/types'; + export default defineBackground(() => { + let twitterImporter: TwitterImporter | null = null; + browser.runtime.onInstalled.addListener(() => { browser.contextMenus.create({ - id: 'save-to-supermemory', + id: CONTEXT_MENU_IDS.SAVE_TO_SUPERMEMORY, title: 'Save to Supermemory', contexts: ['selection', 'page', 'link'], }); }); + + // Intercept Twitter requests to capture authentication headers. + browser.webRequest.onBeforeSendHeaders.addListener( + (details) => { + captureTwitterTokens(details); + return {}; + }, + { urls: ['*://x.com/*', '*://twitter.com/*'] }, + ['requestHeaders', 'extraHeaders'] + ); + + // Handle context menu clicks. browser.contextMenus.onClicked.addListener(async (info, tab) => { - if (info.menuItemId === 'save-to-supermemory') { + if (info.menuItemId === CONTEXT_MENU_IDS.SAVE_TO_SUPERMEMORY) { if (tab?.id) { try { await browser.tabs.sendMessage(tab.id, { - action: 'saveMemory', + action: MESSAGE_TYPES.SAVE_MEMORY, }); } catch (error) { console.error('Failed to send message to content script:', error); - console.log('Content script may not be injected on this page'); } } } }); - browser.runtime.onMessage.addListener((message, sender, sendResponse) => { - if (message.action === 'saveMemory') { + // Send message to current active tab. + const sendMessageToCurrentTab = async (message: string) => { + const tabs = await browser.tabs.query({ active: true, currentWindow: true }); + if (tabs.length > 0 && tabs[0].id) { + await browser.tabs.sendMessage(tabs[0].id, { + type: MESSAGE_TYPES.IMPORT_UPDATE, + importedMessage: message, + }); + } + }; + + /** + * Send import completion message + */ + const sendImportDoneMessage = async (totalImported: number) => { + const tabs = await browser.tabs.query({ active: true, currentWindow: true }); + if (tabs.length > 0 && tabs[0].id) { + await browser.tabs.sendMessage(tabs[0].id, { + type: MESSAGE_TYPES.IMPORT_DONE, + totalImported, + }); + } + }; + + /** + * Save memory to Supermemory API + */ + const saveMemoryToSupermemory = async (data: any): Promise<{ success: boolean; data?: any; error?: string }> => { + try { + const result = await browser.storage.local.get(['bearerToken']); + const bearerToken = result.bearerToken; + + if (!bearerToken) { + return { success: false, error: 'No authentication token found' }; + } + + const payload: MemoryPayload = { + containerTags: [CONTAINER_TAGS.DEFAULT_PROJECT], + content: data.highlightedText + '\n\n' + data.html + '\n\n' + data?.url, + metadata: { sm_source: 'consumer' }, + }; + + const response = await fetch(`${API_ENDPOINTS.SUPERMEMORY_API}/v3/memories`, { + method: 'POST', + credentials: 'omit', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${bearerToken}`, + }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + return { success: false, error: `API call failed: ${response.status}` }; + } + + const responseData = await response.json(); + return { success: true, data: responseData }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } + }; + + /** + * Handle extension messages + */ + browser.runtime.onMessage.addListener((message: ExtensionMessage, _sender, sendResponse) => { + // Handle Twitter import request + if (message.type === MESSAGE_TYPES.BATCH_IMPORT_ALL) { + const importConfig: TwitterImportConfig = { + onProgress: sendMessageToCurrentTab, + onComplete: sendImportDoneMessage, + onError: async (error: Error) => { + await sendMessageToCurrentTab(`Error: ${error.message}`); + }, + }; + + twitterImporter = new TwitterImporter(importConfig); + twitterImporter.startImport().catch(console.error); + sendResponse({ success: true }); + return true; + } + + // Handle regular memory save request + if (message.action === MESSAGE_TYPES.SAVE_MEMORY) { (async () => { try { - const result = await browser.storage.local.get(['bearerToken']); - const bearerToken = result.bearerToken; - //const backendURL = 'http://localhost:8787'; - const backendURL = 'https://api.supermemory.ai'; - - if (!bearerToken) { - console.error('No bearer token found'); - sendResponse({ success: false, error: 'No authentication token found' }); - return; - } - - const response = await fetch(`${backendURL}/v3/memories`, { - method: 'POST', - credentials: 'omit', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${bearerToken}`, - }, - body: JSON.stringify({ - containerTags: ['sm_project_default'], - content: - message.data?.highlightedText + - '\n\n' + - message.data.html + - '\n\n' + - message.data?.url, - metadata: { sm_source: 'consumer' }, - }), - }); - - if (!response.ok) { - const errorData = await response.text(); - console.error('API call failed:', response.status, errorData); - sendResponse({ - success: false, - error: `API call failed: ${response.status}`, - }); - return; - } - - const data = await response.json(); - console.log('Memory saved successfully:', data); - sendResponse({ success: true, data }); + const result = await saveMemoryToSupermemory(message.data); + sendResponse(result); } catch (error) { - console.error('Error saving memory:', error); sendResponse({ success: false, error: error instanceof Error ? error.message : 'Unknown error', }); } })(); - return true; } }); -}); +}); \ No newline at end of file diff --git a/apps/browser-extension/entrypoints/content.ts b/apps/browser-extension/entrypoints/content.ts index 84127916..e9a31697 100644 --- a/apps/browser-extension/entrypoints/content.ts +++ b/apps/browser-extension/entrypoints/content.ts @@ -1,21 +1,33 @@ +import { createTwitterImportButton, createTwitterImportUI, createSaveTweetElement, DOMUtils } from '../utils/ui-components'; +import { DOMAINS, ELEMENT_IDS, MESSAGE_TYPES } from '../utils/constants'; + export default defineContentScript({ matches: [''], main() { - let currentToast: HTMLElement | null = null; + let twitterImportUI: HTMLElement | null = null; + let isTwitterImportOpen = false; browser.runtime.onMessage.addListener(async (message) => { - if (message.action === 'showToast') { - showToast(message.state); - } else if (message.action === 'saveMemory') { + if (message.action === MESSAGE_TYPES.SHOW_TOAST) { + DOMUtils.showToast(message.state); + } else if (message.action === MESSAGE_TYPES.SAVE_MEMORY) { await saveMemory(); + } else if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) { + updateTwitterImportUI(message); + } else if (message.type === MESSAGE_TYPES.IMPORT_DONE) { + updateTwitterImportUI(message); } }); const observeForMemoriesDialog = () => { const observer = new MutationObserver(() => { - if (window.location.hostname === 'chatgpt.com' || window.location.hostname === 'chat.openai.com') { + if (DOMUtils.isOnDomain(DOMAINS.CHATGPT)) { addSupermemoryButtonToMemoriesDialog(); } + if (DOMUtils.isOnDomain(DOMAINS.TWITTER)) { + addTwitterImportButton(); + //addSaveTweetElement(); + } }); observer.observe(document.body, { @@ -26,8 +38,19 @@ export default defineContentScript({ if (window.location.hostname === 'chatgpt.com' || window.location.hostname === 'chat.openai.com') { addSupermemoryButtonToMemoriesDialog(); } + if (window.location.hostname === 'x.com' || window.location.hostname === 'twitter.com') { + addTwitterImportButton(); + //addSaveTweetElement(); + } }; + if (window.location.hostname === 'x.com' || window.location.hostname === 'twitter.com') { + setTimeout(() => { + addTwitterImportButton(); // Wait 2 seconds for page to load + //addSaveTweetElement(); + }, 2000); + } + if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', observeForMemoriesDialog); } else { @@ -36,7 +59,7 @@ export default defineContentScript({ async function saveMemory() { try { - showToast('loading'); + DOMUtils.showToast('loading'); const highlightedText = window.getSelection()?.toString() || ''; @@ -45,7 +68,7 @@ export default defineContentScript({ const html = document.documentElement.outerHTML; const response = await browser.runtime.sendMessage({ - action: 'saveMemory', + action: MESSAGE_TYPES.SAVE_MEMORY, data: { html, highlightedText, @@ -55,13 +78,13 @@ export default defineContentScript({ console.log('Response from enxtension:', response); if (response.success) { - showToast('success'); + DOMUtils.showToast('success'); } else { - showToast('error'); + DOMUtils.showToast('error'); } } catch (error) { console.error('Error saving memory:', error); - showToast('error'); + DOMUtils.showToast('error'); } } @@ -126,11 +149,11 @@ export default defineContentScript({ async function saveMemoriesToSupermemory() { try { - showToast('loading'); + DOMUtils.showToast('loading'); const memoriesTable = document.querySelector('[role="dialog"] table tbody'); if (!memoriesTable) { - showToast('error'); + DOMUtils.showToast('error'); return; } @@ -147,11 +170,10 @@ export default defineContentScript({ console.log('Memories:', memories); if (memories.length === 0) { - showToast('error'); + DOMUtils.showToast('error'); return; } - const url = window.location.href; const combinedContent = `ChatGPT Saved Memories:\n\n${memories.map((memory, index) => `${index + 1}. ${memory}`).join('\n\n')}`; const response = await browser.runtime.sendMessage({ @@ -162,148 +184,136 @@ export default defineContentScript({ }); if (response.success) { - showToast('success'); + DOMUtils.showToast('success'); } else { - showToast('error'); + DOMUtils.showToast('error'); } } catch (error) { console.error('Error saving memories to Supermemory:', error); - showToast('error'); + DOMUtils.showToast('error'); } } - function showToast(state: 'loading' | 'success' | 'error') { - if (currentToast) { - currentToast.remove(); + + function addTwitterImportButton() { + if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) { + return; } - const toast = document.createElement('div'); - toast.id = 'supermemory-toast'; - - toast.style.cssText = ` - position: fixed; - top: 20px; - right: 20px; - z-index: 2147483647; - background: #ffffff; - border-radius: 9999px; - padding: 12px 16px; - display: flex; - align-items: center; - gap: 12px; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - font-size: 14px; - color: #374151; - min-width: 200px; - max-width: 300px; - animation: slideIn 0.3s ease-out; - `; - - if (!document.getElementById('supermemory-toast-styles')) { - const style = document.createElement('style'); - style.id = 'supermemory-toast-styles'; - style.textContent = ` - @keyframes slideIn { - from { - transform: translateX(100%); - opacity: 0; - } - to { - transform: translateX(0); - opacity: 1; - } - } - @keyframes fadeOut { - from { - transform: translateX(0); - opacity: 1; - } - to { - transform: translateX(100%); - opacity: 0; - } - } - `; - document.head.appendChild(style); + if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) { + return; } - const icon = document.createElement('div'); - icon.style.cssText = ` - width: 20px; - height: 20px; - flex-shrink: 0; - `; + const button = createTwitterImportButton(() => { + showTwitterImportUI(); + }); + + document.body.appendChild(button); + } - const text = document.createElement('span'); - text.style.cssText = ` - font-weight: 500; - `; + function showTwitterImportUI() { + if (twitterImportUI) { + twitterImportUI.remove(); + } - if (state === 'loading') { - icon.innerHTML = ` - - - - - - - - - - - `; - icon.style.animation = 'spin 1s linear infinite'; - if (!document.getElementById('supermemory-spinner-styles')) { - const spinStyle = document.createElement('style'); - spinStyle.id = 'supermemory-spinner-styles'; - spinStyle.textContent = ` - @keyframes spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } + isTwitterImportOpen = true; + + // Check if user is authenticated + browser.storage.local.get(['bearerToken'], ({ bearerToken }) => { + const isAuthenticated = !!bearerToken; + + twitterImportUI = createTwitterImportUI( + hideTwitterImportUI, + async () => { + try { + await browser.runtime.sendMessage({ type: MESSAGE_TYPES.BATCH_IMPORT_ALL }); + } catch (error) { + console.error('Error starting import:', error); } + }, + isAuthenticated + ); + + document.body.appendChild(twitterImportUI); + }); + } + + function hideTwitterImportUI() { + if (twitterImportUI) { + twitterImportUI.remove(); + twitterImportUI = null; + } + isTwitterImportOpen = false; + } + + function updateTwitterImportUI(message: any) { + if (!isTwitterImportOpen || !twitterImportUI) return; + + const statusDiv = twitterImportUI.querySelector('#twitter-import-status'); + const button = twitterImportUI.querySelector('#twitter-import-button'); + + if (message.type === 'import-update') { + if (statusDiv) { + statusDiv.innerHTML = ` +
+
+ ${message.importedMessage} +
`; - document.head.appendChild(spinStyle); } - text.textContent = 'Adding to Memory...'; - } else if (state === 'success') { - const iconUrl = browser.runtime.getURL('/icon-16.png'); - icon.innerHTML = ` - Success - `; - text.textContent = 'Added to Memory'; - } else if (state === 'error') { - icon.innerHTML = ` - - - - - - `; - text.textContent = 'Failed to save memory / Make sure you are logged in'; + if (button) { + (button as HTMLButtonElement).disabled = true; + (button as HTMLButtonElement).textContent = 'Importing...'; + } } - - toast.appendChild(icon); - toast.appendChild(text); - document.body.appendChild(toast); - currentToast = toast; - - if (state === 'success' || state === 'error') { + + if (message.type === 'import-done') { + if (statusDiv) { + statusDiv.innerHTML = ` +
+ + Successfully imported ${message.totalImported} tweets! +
+ `; + } + setTimeout(() => { - if (currentToast === toast) { - toast.style.animation = 'fadeOut 0.3s ease-out'; - setTimeout(() => { - if (toast.parentNode) { - toast.remove(); - } - if (currentToast === toast) { - currentToast = null; - } - }, 300); - } + hideTwitterImportUI(); }, 3000); } } + // TODO: Add Tweet Capture Functionality + function addSaveTweetElement() { + if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) { + return; + } + + const targetDivs = document.querySelectorAll('div.css-175oi2r.r-18u37iz.r-1h0z5md.r-1wron08'); + + targetDivs.forEach((targetDiv) => { + if (targetDiv.hasAttribute('data-supermemory-icon-added')) { + return; + } + + const previousElement = targetDiv.previousElementSibling; + if (previousElement && previousElement.id && previousElement.id.startsWith(ELEMENT_IDS.SAVE_TWEET_ELEMENT)) { + targetDiv.setAttribute('data-supermemory-icon-added', 'true'); + return; + } + + const saveTweetElement = createSaveTweetElement(async () => { + await saveMemory(); + }); + + saveTweetElement.id = `${ELEMENT_IDS.SAVE_TWEET_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; + + targetDiv.setAttribute('data-supermemory-icon-added', 'true'); + + targetDiv.parentNode?.insertBefore(saveTweetElement, targetDiv); + }); + } + document.addEventListener('keydown', async (event) => { if ( (event.ctrlKey || event.metaKey) && diff --git a/apps/browser-extension/package.json b/apps/browser-extension/package.json index 839ab818..8d9cc233 100644 --- a/apps/browser-extension/package.json +++ b/apps/browser-extension/package.json @@ -1,5 +1,5 @@ { - "name": "supermemory-browser-extension", + "name": "Supermemory", "description": "Supermemory Browser Extension", "private": true, "version": "0.0.1", diff --git a/apps/browser-extension/public/dark-mode-icon.png b/apps/browser-extension/public/dark-mode-icon.png new file mode 100644 index 00000000..6c9dd28a Binary files /dev/null and b/apps/browser-extension/public/dark-mode-icon.png differ diff --git a/apps/browser-extension/public/light-mode-icon.png b/apps/browser-extension/public/light-mode-icon.png new file mode 100644 index 00000000..66df9ad7 Binary files /dev/null and b/apps/browser-extension/public/light-mode-icon.png differ diff --git a/apps/browser-extension/utils/constants.ts b/apps/browser-extension/utils/constants.ts new file mode 100644 index 00000000..dad6530c --- /dev/null +++ b/apps/browser-extension/utils/constants.ts @@ -0,0 +1,80 @@ +/** + * API Endpoints + */ +export const API_ENDPOINTS = { + SUPERMEMORY_API: import.meta.env.PROD ? 'https://api.supermemory.ai' : 'http://localhost:8787', + SUPERMEMORY_WEB: import.meta.env.PROD ? 'https://app.supermemory.ai' : 'http://localhost:3000', +} as const; + +/** + * Storage Keys + */ +export const STORAGE_KEYS = { + BEARER_TOKEN: 'bearerToken', + TWITTER_AUTH: 'twitterAuth', + TOKENS_LOGGED: 'tokens_logged', + TWITTER_COOKIE: 'cookie', + TWITTER_CSRF: 'csrf', + TWITTER_AUTH_TOKEN: 'auth', +} as const; + +/** + * DOM Element IDs + */ +export const ELEMENT_IDS = { + TWITTER_IMPORT_BUTTON: 'supermemory-twitter-import-button', + TWITTER_IMPORT_STATUS: 'twitter-import-status', + TWITTER_CLOSE_BTN: 'twitter-close-btn', + TWITTER_IMPORT_BTN: 'twitter-import-button', + TWITTER_SIGNIN_BTN: 'twitter-signin-btn', + SUPERMEMORY_TOAST: 'supermemory-toast', + SUPERMEMORY_SAVE_BUTTON: 'supermemory-save-button', + SAVE_TWEET_ELEMENT: 'supermemory-save-tweet-element', +} as const; + +/** + * UI Configuration + */ +export const UI_CONFIG = { + BUTTON_SHOW_DELAY: 2000, // milliseconds + TOAST_DURATION: 3000, // milliseconds + RATE_LIMIT_BASE_WAIT: 60000, // 1 minute + PAGINATION_DELAY: 1000, // 1 second between requests +} as const; + +/** + * Supported Domains + */ +export const DOMAINS = { + TWITTER: ['x.com', 'twitter.com'], + CHATGPT: ['chatgpt.com', 'chat.openai.com'], + SUPERMEMORY: ['localhost', 'supermemory.ai', 'app.supermemory.ai'], +} as const; + +/** + * Container Tags + */ +export const CONTAINER_TAGS = { + TWITTER_BOOKMARKS: 'sm_project_twitter_bookmarks', + DEFAULT_PROJECT: 'sm_project_default', +} as const; + +/** + * Message Types for extension communication + */ +export const MESSAGE_TYPES = { + SAVE_MEMORY: 'saveMemory', + SHOW_TOAST: 'showToast', + BATCH_IMPORT_ALL: 'batchImportAll', + IMPORT_UPDATE: 'import-update', + IMPORT_DONE: 'import-done', +} as const; + +export const CONTEXT_MENU_IDS = { + SAVE_TO_SUPERMEMORY: 'save-to-supermemory', +} as const; + +export const CSS_CLASSES = { + TOAST_STYLES_ID: 'supermemory-toast-styles', + SPINNER_STYLES_ID: 'supermemory-spinner-styles', +} as const; \ No newline at end of file diff --git a/apps/browser-extension/utils/twitter-auth.ts b/apps/browser-extension/utils/twitter-auth.ts new file mode 100644 index 00000000..4fa99542 --- /dev/null +++ b/apps/browser-extension/utils/twitter-auth.ts @@ -0,0 +1,85 @@ +/** + * Twitter Authentication Module + * Handles token capture and storage for Twitter API access + */ + +export interface TwitterAuthTokens { + cookie: string; + csrf: string; + auth: string; +} + +/** + * Captures Twitter authentication tokens from web request headers + * @param details - Web request details containing headers + * @returns True if tokens were captured, false otherwise + */ +export function captureTwitterTokens(details: any): boolean { + if (!(details.url.includes('x.com') || details.url.includes('twitter.com'))) { + return false; + } + + const authHeader = details.requestHeaders?.find( + (header: any) => header.name.toLowerCase() === 'authorization' + ); + const cookieHeader = details.requestHeaders?.find( + (header: any) => header.name.toLowerCase() === 'cookie' + ); + const csrfHeader = details.requestHeaders?.find( + (header: any) => header.name.toLowerCase() === 'x-csrf-token' + ); + + if (authHeader?.value && cookieHeader?.value && csrfHeader?.value) { + browser.storage.session.get(['tokens_logged'], (result) => { + if (!result.tokens_logged) { + console.log('Twitter auth tokens captured successfully'); + browser.storage.session.set({ tokens_logged: true }); + } + }); + + browser.storage.session.set({ + cookie: cookieHeader.value, + csrf: csrfHeader.value, + auth: authHeader.value + }); + + return true; + } + + return false; +} + +/** + * Retrieves stored Twitter authentication tokens + * @returns Promise resolving to tokens or null if not available + */ +export async function getTwitterTokens(): Promise { + const result = await browser.storage.session.get(['cookie', 'csrf', 'auth']); + + if (!result.cookie || !result.csrf || !result.auth) { + return null; + } + + return { + cookie: result.cookie, + csrf: result.csrf, + auth: result.auth + }; +} + +/** + * Creates HTTP headers for Twitter API requests using stored tokens + * @param tokens - Twitter authentication tokens + * @returns Headers object ready for fetch requests + */ +export function createTwitterAPIHeaders(tokens: TwitterAuthTokens): Headers { + const headers = new Headers(); + headers.append('Cookie', tokens.cookie); + headers.append('X-Csrf-Token', tokens.csrf); + headers.append('Authorization', tokens.auth); + headers.append('Content-Type', 'application/json'); + headers.append('User-Agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'); + headers.append('Accept', '*/*'); + headers.append('Accept-Language', 'en-US,en;q=0.9'); + return headers; +} \ No newline at end of file diff --git a/apps/browser-extension/utils/twitter-import.ts b/apps/browser-extension/utils/twitter-import.ts new file mode 100644 index 00000000..6349a3f8 --- /dev/null +++ b/apps/browser-extension/utils/twitter-import.ts @@ -0,0 +1,209 @@ +/** + * Twitter Bookmarks Import Module + * Handles the import process for Twitter bookmarks + */ + +import { + BOOKMARKS_URL, + getAllTweets, + extractNextCursor, + tweetToMarkdown, + buildRequestVariables, + type TwitterAPIResponse, + type Tweet +} from './twitter-utils'; +import { getTwitterTokens, createTwitterAPIHeaders, type TwitterAuthTokens } from './twitter-auth'; + +export type ImportProgressCallback = (message: string) => Promise; + +export type ImportCompleteCallback = (totalImported: number) => Promise; + +export interface TwitterImportConfig { + onProgress: ImportProgressCallback; + onComplete: ImportCompleteCallback; + onError: (error: Error) => Promise; +} + +/** + * Rate limiting configuration + */ +class RateLimiter { + private waitTime = 60000; // Start with 1 minute + + async handleRateLimit(onProgress: ImportProgressCallback): Promise { + const waitTimeInSeconds = this.waitTime / 1000; + + await onProgress( + `Rate limit reached. Waiting for ${waitTimeInSeconds} seconds before retrying...` + ); + + await new Promise(resolve => setTimeout(resolve, this.waitTime)); + this.waitTime *= 2; // Exponential backoff + } + + reset(): void { + this.waitTime = 60000; + } +} + +/** + * Imports a single tweet to Supermemory + * @param tweetMd - Tweet content in markdown format + * @param tweet - Original tweet object with metadata + * @returns Promise that resolves when tweet is imported + */ +async function importTweet(tweetMd: string, tweet: Tweet): Promise { + return new Promise((resolve, reject) => { + browser.storage.local.get(['bearerToken'], ({ bearerToken }) => { + if (!bearerToken) { + reject(new Error('No bearer token found')); + return; + } + + const backendURL = 'https://api.supermemory.ai'; + + fetch(`${backendURL}/v3/memories`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${bearerToken}`, + }, + body: JSON.stringify({ + containerTags: ['sm_project_twitter_bookmarks'], + content: tweetMd, + metadata: { + sm_source: 'twitter_bookmarks', + tweet_id: tweet.id_str, + author: tweet.user.screen_name, + created_at: tweet.created_at, + likes: tweet.favorite_count, + retweets: tweet.retweet_count || 0, + }, + }), + }) + .then(async (response) => { + if (!response.ok) { + if (response.status === 409) { + resolve(); // Skip if already exists + } else { + reject(new Error(`Failed to save tweet: ${response.status}`)); + } + } else { + resolve(); + } + }) + .catch(reject); + }); + }); +} + +/** + * Main class for handling Twitter bookmarks import + */ +export class TwitterImporter { + private importInProgress = false; + private rateLimiter = new RateLimiter(); + + constructor(private config: TwitterImportConfig) {} + + /** + * Starts the import process for all Twitter bookmarks + * @returns Promise that resolves when import is complete + */ + async startImport(): Promise { + if (this.importInProgress) { + throw new Error('Import already in progress'); + } + + this.importInProgress = true; + + try { + await this.batchImportAll('', 0); + this.rateLimiter.reset(); + } catch (error) { + await this.config.onError(error as Error); + } finally { + this.importInProgress = false; + } + } + + /** + * Recursive function to import all bookmarks with pagination + * @param cursor - Pagination cursor for Twitter API + * @param totalImported - Number of tweets imported so far + */ + private async batchImportAll(cursor = '', totalImported = 0): Promise { + try { + // Get authentication tokens + const tokens = await getTwitterTokens(); + if (!tokens) { + await this.config.onProgress('Please visit Twitter/X first to capture authentication tokens'); + return; + } + + // Create headers for API request + const headers = createTwitterAPIHeaders(tokens); + + // Build API request with pagination + const variables = buildRequestVariables(cursor); + const urlWithCursor = cursor + ? `${BOOKMARKS_URL}&variables=${encodeURIComponent(JSON.stringify(variables))}` + : BOOKMARKS_URL; + + console.log('Making Twitter API request to:', urlWithCursor); + console.log('Request headers:', Object.fromEntries(headers.entries())); + + const response = await fetch(urlWithCursor, { + method: 'GET', + headers, + redirect: 'follow', + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error(`Twitter API Error ${response.status}:`, errorText); + + if (response.status === 429) { + await this.rateLimiter.handleRateLimit(this.config.onProgress); + return this.batchImportAll(cursor, totalImported); + } + throw new Error(`Failed to fetch data: ${response.status} - ${errorText}`); + } + + const data: TwitterAPIResponse = await response.json(); + const tweets = getAllTweets(data); + + console.log('Tweets:', tweets); + + // Process each tweet + for (const tweet of tweets) { + try { + const tweetMd = tweetToMarkdown(tweet); + await importTweet(tweetMd, tweet); + totalImported++; + await this.config.onProgress(`Imported ${totalImported} tweets`); + } catch (error) { + console.error('Error importing tweet:', error); + // Continue with next tweet + } + } + + // Handle pagination + const instructions = data.data?.bookmark_timeline_v2?.timeline?.instructions; + const nextCursor = extractNextCursor(instructions || []); + + console.log("Next cursor:", nextCursor); + console.log("Tweets length:", tweets.length); + + if (nextCursor && tweets.length > 0) { + await new Promise(resolve => setTimeout(resolve, 1000)); // Rate limiting + await this.batchImportAll(nextCursor, totalImported); + } else { + await this.config.onComplete(totalImported); + } + } catch (error) { + console.error('Batch import error:', error); + await this.config.onError(error as Error); + } + } +} \ No newline at end of file diff --git a/apps/browser-extension/utils/twitter-utils.ts b/apps/browser-extension/utils/twitter-utils.ts new file mode 100644 index 00000000..13c5e45d --- /dev/null +++ b/apps/browser-extension/utils/twitter-utils.ts @@ -0,0 +1,289 @@ +// Twitter API data structures and transformation utilities + +export interface Tweet { + __typename?: string; + lang?: string; + favorite_count: number; + created_at: string; + display_text_range?: [number, number]; + entities: { + hashtags: Array<{ + indices: [number, number]; + text: string; + }>; + urls?: Array<{ + display_url: string; + expanded_url: string; + indices: [number, number]; + url: string; + }>; + user_mentions: Array<{ + id_str: string; + indices: [number, number]; + name: string; + screen_name: string; + }>; + symbols: Array; + }; + id_str: string; + text: string; + user: { + id_str: string; + name: string; + profile_image_url_https: string; + screen_name: string; + verified: boolean; + is_blue_verified?: boolean; + }; + conversation_count: number; + photos?: Array<{ + url: string; + width: number; + height: number; + }>; + videos?: Array<{ + url: string; + thumbnail_url: string; + duration: number; + }>; + retweet_count?: number; + quote_count?: number; + reply_count?: number; +} + +export interface TwitterAPIResponse { + data: { + bookmark_timeline_v2: { + timeline: { + instructions: Array<{ + type: string; + entries?: Array<{ + entryId: string; + sortIndex: string; + content: any; + }>; + }>; + }; + }; + }; +} + +// Twitter API features configuration +export const TWITTER_API_FEATURES = { + graphql_timeline_v2_bookmark_timeline: true, + responsive_web_graphql_exclude_directive_enabled: true, + responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, + responsive_web_graphql_timeline_navigation_enabled: true, + responsive_web_enhance_cards_enabled: false, + rweb_tipjar_consumption_enabled: true, + responsive_web_twitter_article_notes_tab_enabled: true, + creator_subscriptions_tweet_preview_api_enabled: true, + freedom_of_speech_not_reach_fetch_enabled: true, + standardized_nudges_misinfo: true, + tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true, + longform_notetweets_rich_text_read_enabled: true, + longform_notetweets_inline_media_enabled: true, + responsive_web_media_download_video_enabled: false, + responsive_web_text_conversations_enabled: false, + // Missing features that the API is complaining about + creator_subscriptions_quote_tweet_preview_enabled: true, + view_counts_everywhere_api_enabled: true, + c9s_tweet_anatomy_moderator_badge_enabled: true, + graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, + tweetypie_unmention_optimization_enabled: true, + responsive_web_twitter_article_tweet_consumption_enabled: true, + tweet_awards_web_tipping_enabled: true, + communities_web_enable_tweet_community_results_fetch: true, + responsive_web_edit_tweet_api_enabled: true, + longform_notetweets_consumption_enabled: true, + articles_preview_enabled: true, + rweb_video_timestamps_enabled: true, + verified_phone_label_enabled: true +}; + +export const BOOKMARKS_URL = `https://x.com/i/api/graphql/xLjCVTqYWz8CGSprLU349w/Bookmarks?features=${encodeURIComponent(JSON.stringify(TWITTER_API_FEATURES))}`; + +/** + * Transform raw Twitter API response data into standardized Tweet format + */ +export function transformTweetData(input: any): Tweet | null { + try { + const tweet = input.content?.itemContent?.tweet_results?.result; + + if (!tweet || !tweet.legacy) { + return null; + } + + // Handle media entities + const media = tweet.legacy.entities?.media || []; + const photos = media + .filter((m: any) => m.type === 'photo') + .map((m: any) => ({ + url: m.media_url_https, + width: m.sizes?.large?.w || 0, + height: m.sizes?.large?.h || 0, + })); + + const videos = media + .filter((m: any) => m.type === 'video') + .map((m: any) => ({ + url: m.video_info?.variants?.[0]?.url || '', + thumbnail_url: m.media_url_https, + duration: m.video_info?.duration_millis || 0, + })); + + const transformed: Tweet = { + __typename: tweet.__typename, + lang: tweet.legacy?.lang, + favorite_count: tweet.legacy.favorite_count || 0, + created_at: new Date(tweet.legacy.created_at).toISOString(), + display_text_range: tweet.legacy.display_text_range, + entities: { + hashtags: tweet.legacy.entities?.hashtags || [], + urls: tweet.legacy.entities?.urls || [], + user_mentions: tweet.legacy.entities?.user_mentions || [], + symbols: tweet.legacy.entities?.symbols || [], + }, + id_str: tweet.legacy.id_str, + text: tweet.legacy.full_text, + user: { + id_str: tweet.core?.user_results?.result?.legacy?.id_str || '', + name: tweet.core?.user_results?.result?.legacy?.name || 'Unknown', + profile_image_url_https: tweet.core?.user_results?.result?.legacy?.profile_image_url_https || '', + screen_name: tweet.core?.user_results?.result?.legacy?.screen_name || 'unknown', + verified: tweet.core?.user_results?.result?.legacy?.verified || false, + is_blue_verified: tweet.core?.user_results?.result?.is_blue_verified || false, + }, + conversation_count: tweet.legacy.reply_count || 0, + retweet_count: tweet.legacy.retweet_count || 0, + quote_count: tweet.legacy.quote_count || 0, + reply_count: tweet.legacy.reply_count || 0, + }; + + if (photos.length > 0) { + transformed.photos = photos; + } + + if (videos.length > 0) { + transformed.videos = videos; + } + + return transformed; + } catch (error) { + console.error('Error transforming tweet data:', error); + return null; + } +} + +/** + * Extract all tweets from Twitter API response + */ +export function getAllTweets(data: TwitterAPIResponse): Tweet[] { + const tweets: Tweet[] = []; + + try { + const instructions = data.data?.bookmark_timeline_v2?.timeline?.instructions || []; + + for (const instruction of instructions) { + if (instruction.type === 'TimelineAddEntries' && instruction.entries) { + for (const entry of instruction.entries) { + if (entry.entryId.startsWith('tweet-')) { + const tweet = transformTweetData(entry); + if (tweet) { + tweets.push(tweet); + } + } + } + } + } + } catch (error) { + console.error('Error extracting tweets:', error); + } + + return tweets; +} + +/** + * Extract pagination cursor from Twitter API response + */ +export function extractNextCursor(instructions: any[]): string | null { + try { + for (const instruction of instructions) { + if (instruction.type === 'TimelineAddEntries' && instruction.entries) { + for (const entry of instruction.entries) { + if (entry.entryId.startsWith('cursor-bottom-')) { + return entry.content?.value || null; + } + } + } + } + } catch (error) { + console.error('Error extracting cursor:', error); + } + + return null; +} + +/** + * Convert Tweet object to markdown format for storage + */ +export function tweetToMarkdown(tweet: Tweet): string { + const username = tweet.user?.screen_name || 'unknown'; + const displayName = tweet.user?.name || 'Unknown User'; + const date = new Date(tweet.created_at).toLocaleDateString(); + const time = new Date(tweet.created_at).toLocaleTimeString(); + + let markdown = `# Tweet by @${username} (${displayName})\n\n`; + markdown += `**Date:** ${date} ${time}\n`; + markdown += `**Likes:** ${tweet.favorite_count} | **Retweets:** ${tweet.retweet_count || 0} | **Replies:** ${tweet.reply_count || 0}\n\n`; + + // Add tweet text + markdown += `${tweet.text}\n\n`; + + // Add media if present + if (tweet.photos && tweet.photos.length > 0) { + markdown += `**Images:**\n`; + tweet.photos.forEach((photo, index) => { + markdown += `![Image ${index + 1}](${photo.url})\n`; + }); + markdown += '\n'; + } + + if (tweet.videos && tweet.videos.length > 0) { + markdown += `**Videos:**\n`; + tweet.videos.forEach((video, index) => { + markdown += `[Video ${index + 1}](${video.url})\n`; + }); + markdown += '\n'; + } + + // Add hashtags and mentions + if (tweet.entities.hashtags.length > 0) { + markdown += `**Hashtags:** ${tweet.entities.hashtags.map(h => `#${h.text}`).join(', ')}\n`; + } + + if (tweet.entities.user_mentions.length > 0) { + markdown += `**Mentions:** ${tweet.entities.user_mentions.map(m => `@${m.screen_name}`).join(', ')}\n`; + } + + // Add raw data for reference + markdown += `\n---\n
\nRaw Tweet Data\n\n\`\`\`json\n${JSON.stringify(tweet, null, 2)}\n\`\`\`\n
`; + + return markdown; +} + +/** + * Build Twitter API request variables for pagination + */ +export function buildRequestVariables(cursor?: string, count: number = 100) { + const variables = { + count, + includePromotedContent: false, + }; + + if (cursor) { + (variables as any).cursor = cursor; + } + + return variables; +} \ No newline at end of file diff --git a/apps/browser-extension/utils/types.ts b/apps/browser-extension/utils/types.ts new file mode 100644 index 00000000..0f390da5 --- /dev/null +++ b/apps/browser-extension/utils/types.ts @@ -0,0 +1,131 @@ +/** + * Type definitions for the browser extension + */ + +/** + * Toast states for UI feedback + */ +export type ToastState = 'loading' | 'success' | 'error'; + +/** + * Message types for extension communication + */ +export interface ExtensionMessage { + action?: string; + type?: string; + data?: any; + state?: ToastState; + importedMessage?: string; + totalImported?: number; +} + +/** + * Memory data structure for saving content + */ +export interface MemoryData { + html: string; + highlightedText?: string; + url?: string; +} + +/** + * Supermemory API payload for storing memories + */ +export interface MemoryPayload { + containerTags: string[]; + content: string; + metadata: { + sm_source: string; + [key: string]: any; + }; +} + +/** + * Twitter-specific memory metadata + */ +export interface TwitterMemoryMetadata { + sm_source: 'twitter_bookmarks'; + tweet_id: string; + author: string; + created_at: string; + likes: number; + retweets: number; +} + +/** + * Storage data structure for Chrome storage + */ +export interface StorageData { + bearerToken?: string; + twitterAuth?: { + cookie: string; + csrf: string; + auth: string; + }; + tokens_logged?: boolean; + cookie?: string; + csrf?: string; + auth?: string; +} + +/** + * Context menu click info + */ +export interface ContextMenuClickInfo { + menuItemId: string | number; + editable?: boolean; + frameId?: number; + frameUrl?: string; + linkUrl?: string; + mediaType?: string; + pageUrl?: string; + parentMenuItemId?: string | number; + selectionText?: string; + srcUrl?: string; + targetElementId?: number; + wasChecked?: boolean; +} + +/** + * API Response types + */ +export interface APIResponse { + success: boolean; + data?: T; + error?: string; +} + +/** + * Error types for better error handling + */ +export class ExtensionError extends Error { + constructor( + message: string, + public code?: string, + public statusCode?: number + ) { + super(message); + this.name = 'ExtensionError'; + } +} + +export class TwitterAPIError extends ExtensionError { + constructor(message: string, statusCode?: number) { + super(message, 'TWITTER_API_ERROR', statusCode); + this.name = 'TwitterAPIError'; + } +} + +export class SupermemoryAPIError extends ExtensionError { + constructor(message: string, statusCode?: number) { + super(message, 'SUPERMEMORY_API_ERROR', statusCode); + this.name = 'SupermemoryAPIError'; + } +} + +export class AuthenticationError extends ExtensionError { + constructor(message: string = 'Authentication required') { + super(message, 'AUTH_ERROR'); + this.name = 'AuthenticationError'; + } +} \ No newline at end of file diff --git a/apps/browser-extension/utils/ui-components.ts b/apps/browser-extension/utils/ui-components.ts new file mode 100644 index 00000000..062b809b --- /dev/null +++ b/apps/browser-extension/utils/ui-components.ts @@ -0,0 +1,351 @@ +/** + * UI Components Module + * Reusable UI components for the browser extension + */ + +import { ELEMENT_IDS, UI_CONFIG, API_ENDPOINTS } from './constants'; +import type { ToastState } from './types'; + +/** + * Creates a toast notification element + * @param state - The state of the toast (loading, success, error) + * @returns HTMLElement - The toast element + */ +export function createToast(state: ToastState): HTMLElement { + const toast = document.createElement('div'); + toast.id = ELEMENT_IDS.SUPERMEMORY_TOAST; + + toast.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + z-index: 2147483647; + background: #ffffff; + border-radius: 9999px; + padding: 12px 16px; + display: flex; + align-items: center; + gap: 12px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 14px; + color: #374151; + min-width: 200px; + max-width: 300px; + animation: slideIn 0.3s ease-out; + `; + + // Add keyframe animations if not already present + if (!document.getElementById('supermemory-toast-styles')) { + const style = document.createElement('style'); + style.id = 'supermemory-toast-styles'; + style.textContent = ` + @keyframes slideIn { + from { transform: translateX(100%); opacity: 0; } + to { transform: translateX(0); opacity: 1; } + } + @keyframes fadeOut { + from { transform: translateX(0); opacity: 1; } + to { transform: translateX(100%); opacity: 0; } + } + @keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } + } + `; + document.head.appendChild(style); + } + + const icon = document.createElement('div'); + icon.style.cssText = 'width: 20px; height: 20px; flex-shrink: 0;'; + + const text = document.createElement('span'); + text.style.fontWeight = '500'; + + // Configure toast based on state + switch (state) { + case 'loading': + icon.innerHTML = ` + + + + + + + + + + + `; + icon.style.animation = 'spin 1s linear infinite'; + text.textContent = 'Adding to Memory...'; + break; + + case 'success': + const iconUrl = browser.runtime.getURL('/icon-16.png'); + icon.innerHTML = `Success`; + text.textContent = 'Added to Memory'; + break; + + case 'error': + icon.innerHTML = ` + + + + + + `; + text.textContent = 'Failed to save memory / Make sure you are logged in'; + break; + } + + toast.appendChild(icon); + toast.appendChild(text); + + return toast; +} + +/** + * Creates the Twitter import button + * @param onClick - Click handler for the button + * @returns HTMLElement - The button element + */ +export function createTwitterImportButton(onClick: () => void): HTMLElement { + const button = document.createElement('div'); + button.id = ELEMENT_IDS.TWITTER_IMPORT_BUTTON; + button.style.cssText = ` + position: fixed; + bottom: 20px; + right: 20px; + z-index: 2147483646; + background: #ffffff; + color: black; + border: none; + border-radius: 50px; + padding: 12px 16px; + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; + transition: all 0.2s ease; + `; + + const iconUrl = browser.runtime.getURL('/light-mode-icon.png'); + button.innerHTML = ` + Save to Memory + `; + + button.addEventListener('mouseenter', () => { + button.style.transform = 'scale(1.05)'; + button.style.boxShadow = '0 4px 12px rgba(29, 155, 240, 0.4)'; + }); + + button.addEventListener('mouseleave', () => { + button.style.transform = 'scale(1)'; + button.style.boxShadow = '0 2px 8px rgba(29, 155, 240, 0.3)'; + }); + + button.addEventListener('click', onClick); + + return button; +} + +/** + * Creates the Twitter import UI dialog + * @param onClose - Close handler + * @param onImport - Import handler + * @param isAuthenticated - Whether user is authenticated + * @returns HTMLElement - The dialog element + */ +export function createTwitterImportUI( + onClose: () => void, + onImport: () => void, + isAuthenticated: boolean +): HTMLElement { + const container = document.createElement('div'); + container.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + z-index: 2147483647; + background: #ffffff; + border-radius: 12px; + padding: 16px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + min-width: 280px; + max-width: 400px; + border: 1px solid #e1e5e9; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + `; + + container.innerHTML = ` +
+
+ + + +

+ Import Twitter Bookmarks +

+
+ +
+ + ${isAuthenticated ? ` +
+

+ This will import all your Twitter bookmarks to Supermemory. Make sure you're logged into Twitter/X first. +

+ + + +
+
+ ` : ` +
+

+ Please sign in to Supermemory first +

+ +
+ `} + + + `; + + // Add event listeners + const closeBtn = container.querySelector(`#${ELEMENT_IDS.TWITTER_CLOSE_BTN}`); + closeBtn?.addEventListener('click', onClose); + + const importBtn = container.querySelector(`#${ELEMENT_IDS.TWITTER_IMPORT_BTN}`); + importBtn?.addEventListener('click', onImport); + + const signinBtn = container.querySelector(`#${ELEMENT_IDS.TWITTER_SIGNIN_BTN}`); + signinBtn?.addEventListener('click', () => { + browser.tabs.create({ url: `${API_ENDPOINTS.SUPERMEMORY_WEB}/login` }); + }); + + return container; +} + +/** + * Creates a save tweet element button for Twitter/X + * @param onClick - Click handler for the button + * @returns HTMLElement - The save button element + */ +export function createSaveTweetElement(onClick: () => void): HTMLElement { + const iconButton = document.createElement('div'); + iconButton.style.cssText = ` + display: inline-flex; + align-items: flex-end; + opacity: 0.7; + justify-content: center; + width: 20px; + height: 20px; + border-radius: 50%; + cursor: pointer; + margin-right: 10px; + margin-bottom: 2px; + z-index: 1000; + `; + + // Check body background color to determine which icon to use + const bodyStyle = window.getComputedStyle(document.body); + const backgroundColor = bodyStyle.backgroundColor; + const isLightMode = backgroundColor === 'rgb(255, 255, 255)'; + + const iconFileName = isLightMode ? '/light-mode-icon.png' : '/dark-mode-icon.png'; + const iconUrl = browser.runtime.getURL(iconFileName); + iconButton.innerHTML = ` + Save to Memory + `; + + iconButton.addEventListener('mouseenter', () => { + iconButton.style.opacity = '1'; + }); + + iconButton.addEventListener('mouseleave', () => { + iconButton.style.opacity = '0.7'; + }); + + iconButton.addEventListener('click', (event) => { + event.stopPropagation(); + event.preventDefault(); + onClick(); + }); + + return iconButton; +} + +/** + * Utility functions for DOM manipulation + */ +export const DOMUtils = { + /** + * Check if current page is on specified domains + * @param domains - Array of domain names to check + * @returns boolean + */ + isOnDomain(domains: readonly string[]): boolean { + return domains.includes(window.location.hostname); + }, + + /** + * Check if element exists in DOM + * @param id - Element ID to check + * @returns boolean + */ + elementExists(id: string): boolean { + return !!document.getElementById(id); + }, + + /** + * Remove element from DOM if it exists + * @param id - Element ID to remove + */ + removeElement(id: string): void { + const element = document.getElementById(id); + element?.remove(); + }, + + /** + * Show toast notification with auto-dismiss + * @param state - Toast state + * @param duration - Duration to show toast (default from config) + * @returns The toast element + */ + showToast(state: ToastState, duration: number = UI_CONFIG.TOAST_DURATION): HTMLElement { + // Remove all existing toasts more aggressively + const existingToasts = document.querySelectorAll(`#${ELEMENT_IDS.SUPERMEMORY_TOAST}`); + existingToasts.forEach(toast => toast.remove()); + + const toast = createToast(state); + document.body.appendChild(toast); + + // Auto-dismiss for success and error states + if (state === 'success' || state === 'error') { + setTimeout(() => { + if (document.body.contains(toast)) { + toast.style.animation = 'fadeOut 0.3s ease-out'; + setTimeout(() => { + if (document.body.contains(toast)) { + toast.remove(); + } + }, 300); + } + }, duration); + } + + return toast; + } +}; \ No newline at end of file diff --git a/apps/browser-extension/wxt.config.ts b/apps/browser-extension/wxt.config.ts index b4370f3c..77d1b7c7 100644 --- a/apps/browser-extension/wxt.config.ts +++ b/apps/browser-extension/wxt.config.ts @@ -4,15 +4,29 @@ import { defineConfig } from 'wxt'; export default defineConfig({ modules: ['@wxt-dev/module-react'], manifest: { - permissions: ['contextMenus', 'storage', 'scripting', 'activeTab'], + permissions: [ + 'contextMenus', + 'storage', + 'scripting', + 'activeTab', + 'webRequest', + 'tabs' + ], + host_permissions: [ + '*://x.com/*', + '*://twitter.com/*', + '*://supermemory.ai/*', + '*://api.supermemory.ai/*' + ], web_accessible_resources: [ { - resources: ['icon-16.png'], + resources: ['icon-16.png', 'light-mode-icon.png', 'dark-mode-icon.png'], matches: [''] } ], }, webExt: { + disabled: true, chromiumArgs: ['--user-data-dir=./.wxt/chrome-data'], }, }); diff --git a/package.json b/package.json index a7cc83e7..656dfcc5 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "supermemory", + "name": "supermemory-app", "private": true, "scripts": { "build": "turbo run build",