extension: updated telemetry and batch upload (#415)

This commit is contained in:
MaheshtheDev 2025-09-08 05:19:10 +00:00
parent 99fd62d5f1
commit 3e5ea2fd9e
16 changed files with 706 additions and 652 deletions

View file

@ -0,0 +1,2 @@
# PostHog Configuration
WXT_POSTHOG_API_KEY=your_posthog_project_api_key_here

View file

@ -1,47 +1,52 @@
import { getDefaultProject, saveMemory, searchMemories } from "../utils/api";
import { getDefaultProject, saveMemory, searchMemories } from "../utils/api"
import {
CONTAINER_TAGS,
CONTEXT_MENU_IDS,
MESSAGE_TYPES,
} from "../utils/constants";
import { captureTwitterTokens } from "../utils/twitter-auth";
POSTHOG_EVENT_KEY,
} from "../utils/constants"
import { trackEvent } from "../utils/posthog"
import { captureTwitterTokens } from "../utils/twitter-auth"
import {
type TwitterImportConfig,
TwitterImporter,
} from "../utils/twitter-import";
} from "../utils/twitter-import"
import type {
ExtensionMessage,
MemoryData,
MemoryPayload,
} from "../utils/types";
} from "../utils/types"
export default defineBackground(() => {
let twitterImporter: TwitterImporter | null = null;
let twitterImporter: TwitterImporter | null = null
browser.runtime.onInstalled.addListener((details) => {
browser.runtime.onInstalled.addListener(async (details) => {
browser.contextMenus.create({
id: CONTEXT_MENU_IDS.SAVE_TO_SUPERMEMORY,
title: "Save to supermemory",
contexts: ["selection", "page", "link"],
});
})
// Open welcome tab on first install
if (details.reason === "install") {
await trackEvent("extension_installed", {
reason: details.reason,
version: browser.runtime.getManifest().version,
})
browser.tabs.create({
url: browser.runtime.getURL("/welcome.html"),
});
})
}
});
})
// Intercept Twitter requests to capture authentication headers.
browser.webRequest.onBeforeSendHeaders.addListener(
(details) => {
captureTwitterTokens(details);
return {};
captureTwitterTokens(details)
return {}
},
{ urls: ["*://x.com/*", "*://twitter.com/*"] },
["requestHeaders", "extraHeaders"],
);
)
// Handle context menu clicks.
browser.contextMenus.onClicked.addListener(async (info, tab) => {
@ -50,27 +55,28 @@ export default defineBackground(() => {
try {
await browser.tabs.sendMessage(tab.id, {
action: MESSAGE_TYPES.SAVE_MEMORY,
});
actionSource: "context_menu",
})
} catch (error) {
console.error("Failed to send message to content script:", error);
console.error("Failed to send message to content script:", error)
}
}
}
});
})
// 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
@ -79,61 +85,71 @@ export default defineBackground(() => {
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: MemoryData,
actionSource: string,
): Promise<{ success: boolean; data?: unknown; error?: string }> => {
try {
let containerTag: string = CONTAINER_TAGS.DEFAULT_PROJECT;
let containerTag: string = CONTAINER_TAGS.DEFAULT_PROJECT
try {
const defaultProject = await getDefaultProject();
const defaultProject = await getDefaultProject()
if (defaultProject?.containerTag) {
containerTag = defaultProject.containerTag;
containerTag = defaultProject.containerTag
}
} catch (error) {
console.warn("Failed to get default project, using fallback:", error);
console.warn("Failed to get default project, using fallback:", error)
}
const payload: MemoryPayload = {
containerTags: [containerTag],
content: `${data.highlightedText}\n\n${data.html}\n\n${data?.url}`,
metadata: { sm_source: "consumer" },
};
}
const responseData = await saveMemory(payload);
return { success: true, data: responseData };
const responseData = await saveMemory(payload)
await trackEvent(POSTHOG_EVENT_KEY.SAVE_MEMORY_ATTEMPTED, {
source: `${POSTHOG_EVENT_KEY.SOURCE}_${actionSource}`,
has_highlight: !!data.highlightedText,
url_domain: data.url ? new URL(data.url).hostname : undefined,
})
return { success: true, data: responseData }
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error",
};
}
}
};
}
const getRelatedMemories = async (
data: string,
eventSource: string,
): Promise<{ success: boolean; data?: unknown; error?: string }> => {
try {
const responseData = await searchMemories(data)
const response = responseData as {
results?: Array<{ memory?: string }>
}
let memories = "";
let memories = ""
response.results?.forEach((result, index) => {
memories += `[${index + 1}] ${result.memory} `
})
})
console.log("Memories:", memories)
await trackEvent(eventSource)
return { success: true, data: memories }
} catch (error) {
return {
@ -141,7 +157,7 @@ export default defineBackground(() => {
error: error instanceof Error ? error.message : "Unknown error",
}
}
};
}
/**
* Handle extension messages
@ -154,48 +170,52 @@ export default defineBackground(() => {
onProgress: sendMessageToCurrentTab,
onComplete: sendImportDoneMessage,
onError: async (error: Error) => {
await sendMessageToCurrentTab(`Error: ${error.message}`);
await sendMessageToCurrentTab(`Error: ${error.message}`)
},
};
}
twitterImporter = new TwitterImporter(importConfig);
twitterImporter.startImport().catch(console.error);
sendResponse({ success: true });
return true;
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 () => {
;(async () => {
try {
const result = await saveMemoryToSupermemory(
message.data as MemoryData,
);
sendResponse(result);
message.actionSource || "unknown",
)
sendResponse(result)
} catch (error) {
sendResponse({
success: false,
error: error instanceof Error ? error.message : "Unknown error",
});
})
}
})();
return true;
})()
return true
}
if (message.action === MESSAGE_TYPES.GET_RELATED_MEMORIES) {
(async () => {
;(async () => {
try {
const result = await getRelatedMemories(message.data as string);
sendResponse(result);
const result = await getRelatedMemories(
message.data as string,
message.actionSource || "unknown",
)
sendResponse(result)
} catch (error) {
sendResponse({
success: false,
error: error instanceof Error ? error.message : "Unknown error",
});
})
}
})();
return true;
})()
return true
}
},
);
});
)
})

View file

@ -2,94 +2,105 @@ import {
DOMAINS,
ELEMENT_IDS,
MESSAGE_TYPES,
POSTHOG_EVENT_KEY,
STORAGE_KEYS,
} from "../utils/constants";
} from "../utils/constants"
import { trackEvent } from "../utils/posthog"
import {
createChatGPTInputBarElement,
createClaudeInputBarElement,
createSaveTweetElement,
createT3InputBarElement,
createTwitterImportButton,
createTwitterImportUI,
DOMUtils,
} from "../utils/ui-components";
} from "../utils/ui-components"
export default defineContentScript({
matches: ["<all_urls>"],
main() {
let twitterImportUI: HTMLElement | null = null;
let isTwitterImportOpen = false;
browser.runtime.onMessage.addListener(async (message) => {
if (message.action === MESSAGE_TYPES.SHOW_TOAST) {
DOMUtils.showToast(message.state);
DOMUtils.showToast(message.state)
} else if (message.action === MESSAGE_TYPES.SAVE_MEMORY) {
await saveMemory();
await saveMemory()
} else if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) {
updateTwitterImportUI(message);
updateTwitterImportUI(message)
} else if (message.type === MESSAGE_TYPES.IMPORT_DONE) {
updateTwitterImportUI(message);
updateTwitterImportUI(message)
}
});
})
const observeForMemoriesDialog = () => {
const observer = new MutationObserver(() => {
if (DOMUtils.isOnDomain(DOMAINS.CHATGPT)) {
addSupermemoryButtonToMemoriesDialog();
addSaveChatGPTElementBeforeComposerBtn();
addSupermemoryButtonToMemoriesDialog()
addSaveChatGPTElementBeforeComposerBtn()
}
if (DOMUtils.isOnDomain(DOMAINS.CLAUDE)) {
addSupermemoryIconToClaudeInput();
addSupermemoryIconToClaudeInput()
}
if (DOMUtils.isOnDomain(DOMAINS.T3)) {
addSupermemoryIconToT3Input();
addSupermemoryIconToT3Input()
}
if (DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
addTwitterImportButton();
//addSaveTweetElement();
if (
DOMUtils.isOnDomain(DOMAINS.TWITTER) &&
window.location.pathname === "/i/bookmarks"
) {
addTwitterImportButton()
} else if (DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) {
DOMUtils.removeElement(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)
}
}
});
})
observer.observe(document.body, {
childList: true,
subtree: true,
});
};
})
}
if (DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
if (
DOMUtils.isOnDomain(DOMAINS.TWITTER) &&
window.location.pathname === "/i/bookmarks"
) {
setTimeout(() => {
addTwitterImportButton(); // Wait 2 seconds for page to load
addTwitterImportButton() // Wait 2 seconds for page to load
//addSaveTweetElement();
}, 2000);
}, 2000)
} else if (DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) {
DOMUtils.removeElement(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)
}
}
if (DOMUtils.isOnDomain(DOMAINS.CLAUDE)) {
setTimeout(() => {
addSupermemoryIconToClaudeInput(); // Wait 2 seconds for Claude page to load
}, 2000);
addSupermemoryIconToClaudeInput() // Wait 2 seconds for Claude page to load
}, 2000)
}
if (DOMUtils.isOnDomain(DOMAINS.T3)) {
setTimeout(() => {
addSupermemoryIconToT3Input(); // Wait 2 seconds for T3 page to load
}, 2000);
addSupermemoryIconToT3Input() // Wait 2 seconds for T3 page to load
}, 2000)
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", observeForMemoriesDialog);
document.addEventListener("DOMContentLoaded", observeForMemoriesDialog)
} else {
observeForMemoriesDialog();
observeForMemoriesDialog()
}
async function saveMemory() {
try {
DOMUtils.showToast("loading");
DOMUtils.showToast("loading")
const highlightedText = window.getSelection()?.toString() || "";
const highlightedText = window.getSelection()?.toString() || ""
const url = window.location.href;
const url = window.location.href
const html = document.documentElement.outerHTML;
const html = document.documentElement.outerHTML
const response = await browser.runtime.sendMessage({
action: MESSAGE_TYPES.SAVE_MEMORY,
@ -98,75 +109,77 @@ export default defineContentScript({
highlightedText,
url,
},
});
actionSource: "context_menu",
})
console.log("Response from enxtension:", response);
console.log("Response from enxtension:", response)
if (response.success) {
DOMUtils.showToast("success");
DOMUtils.showToast("success")
} else {
DOMUtils.showToast("error");
DOMUtils.showToast("error")
}
} catch (error) {
console.error("Error saving memory:", error);
DOMUtils.showToast("error");
console.error("Error saving memory:", error)
DOMUtils.showToast("error")
}
}
async function getRelatedMemories() {
async function getRelatedMemories(actionSource: string) {
try {
const userQuery =
document.getElementById("prompt-textarea")?.textContent || "";
document.getElementById("prompt-textarea")?.textContent || ""
const response = await browser.runtime.sendMessage({
action: MESSAGE_TYPES.GET_RELATED_MEMORIES,
data: userQuery,
});
actionSource: actionSource,
})
if (response.success && response.data) {
const promptElement = document.getElementById("prompt-textarea");
const promptElement = document.getElementById("prompt-textarea")
if (promptElement) {
const currentContent = promptElement.innerHTML;
promptElement.innerHTML = `${currentContent}<br>Supermemories: ${response.data}`;
const currentContent = promptElement.innerHTML
promptElement.innerHTML = `${currentContent}<br>Supermemories: ${response.data}`
}
}
} catch (error) {
console.error("Error getting related memories:", error);
console.error("Error getting related memories:", error)
}
}
function addSupermemoryButtonToMemoriesDialog() {
const dialogs = document.querySelectorAll('[role="dialog"]');
let memoriesDialog: HTMLElement | null = null;
const dialogs = document.querySelectorAll('[role="dialog"]')
let memoriesDialog: HTMLElement | null = null
for (const dialog of dialogs) {
const headerText = dialog.querySelector("h2");
const headerText = dialog.querySelector("h2")
if (headerText?.textContent?.includes("Saved memories")) {
memoriesDialog = dialog as HTMLElement;
break;
memoriesDialog = dialog as HTMLElement
break
}
}
if (!memoriesDialog) return;
if (!memoriesDialog) return
if (memoriesDialog.querySelector("#supermemory-save-button")) return;
if (memoriesDialog.querySelector("#supermemory-save-button")) return
const deleteAllContainer = memoriesDialog.querySelector(
".mt-5.flex.justify-end",
);
if (!deleteAllContainer) return;
)
if (!deleteAllContainer) return
const supermemoryButton = document.createElement("button");
supermemoryButton.id = "supermemory-save-button";
supermemoryButton.className = "btn relative btn-primary-outline mr-2";
const supermemoryButton = document.createElement("button")
supermemoryButton.id = "supermemory-save-button"
supermemoryButton.className = "btn relative btn-primary-outline mr-2"
const iconUrl = browser.runtime.getURL("/icon-16.png");
const iconUrl = browser.runtime.getURL("/icon-16.png")
supermemoryButton.innerHTML = `
<div class="flex items-center justify-center gap-2">
<img src="${iconUrl}" alt="supermemory" style="width: 16px; height: 16px; flex-shrink: 0; border-radius: 2px;" />
Save to supermemory
</div>
`;
`
supermemoryButton.style.cssText = `
background: #1C2026 !important;
@ -178,388 +191,352 @@ export default defineContentScript({
font-size: 14px !important;
margin-right: 8px !important;
cursor: pointer !important;
`;
`
supermemoryButton.addEventListener("mouseenter", () => {
supermemoryButton.style.backgroundColor = "#2B2E33";
});
supermemoryButton.style.backgroundColor = "#2B2E33"
})
supermemoryButton.addEventListener("mouseleave", () => {
supermemoryButton.style.backgroundColor = "#1C2026";
});
supermemoryButton.style.backgroundColor = "#1C2026"
})
supermemoryButton.addEventListener("click", async () => {
await saveMemoriesToSupermemory();
});
await saveMemoriesToSupermemory()
})
deleteAllContainer.insertBefore(
supermemoryButton,
deleteAllContainer.firstChild,
);
)
}
async function saveMemoriesToSupermemory() {
try {
DOMUtils.showToast("loading");
DOMUtils.showToast("loading")
const memoriesTable = document.querySelector(
'[role="dialog"] table tbody',
);
)
if (!memoriesTable) {
DOMUtils.showToast("error");
return;
DOMUtils.showToast("error")
return
}
const memoryRows = memoriesTable.querySelectorAll("tr");
const memories: string[] = [];
const memoryRows = memoriesTable.querySelectorAll("tr")
const memories: string[] = []
memoryRows.forEach((row) => {
const memoryCell = row.querySelector("td .py-2.whitespace-pre-wrap");
const memoryCell = row.querySelector("td .py-2.whitespace-pre-wrap")
if (memoryCell?.textContent) {
memories.push(memoryCell.textContent.trim());
memories.push(memoryCell.textContent.trim())
}
});
})
console.log("Memories:", memories);
console.log("Memories:", memories)
if (memories.length === 0) {
DOMUtils.showToast("error");
return;
DOMUtils.showToast("error")
return
}
const combinedContent = `ChatGPT Saved Memories:\n\n${memories.map((memory, index) => `${index + 1}. ${memory}`).join("\n\n")}`;
const combinedContent = `ChatGPT Saved Memories:\n\n${memories.map((memory, index) => `${index + 1}. ${memory}`).join("\n\n")}`
const response = await browser.runtime.sendMessage({
action: MESSAGE_TYPES.SAVE_MEMORY,
data: {
html: combinedContent,
},
});
actionSource: "chatgpt_memories_dialog",
})
console.log({ response });
console.log({ response })
if (response.success) {
DOMUtils.showToast("success");
DOMUtils.showToast("success")
} else {
DOMUtils.showToast("error");
DOMUtils.showToast("error")
}
} catch (error) {
console.error("Error saving memories to supermemory:", error);
DOMUtils.showToast("error");
console.error("Error saving memories to supermemory:", error)
DOMUtils.showToast("error")
}
}
function addTwitterImportButton() {
if (!DOMUtils.isOnDomain(DOMAINS.TWITTER)) {
return;
return
}
// Only show the import button on the bookmarks page
if (window.location.pathname !== "/i/bookmarks") {
return;
return
}
if (DOMUtils.elementExists(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)) {
return;
return
}
const button = createTwitterImportButton(() => {
showTwitterImportUI();
});
const button = createTwitterImportButton(async () => {
try {
await browser.runtime.sendMessage({
type: MESSAGE_TYPES.BATCH_IMPORT_ALL,
})
await trackEvent(POSTHOG_EVENT_KEY.TWITTER_IMPORT_STARTED, {
source: `${POSTHOG_EVENT_KEY.SOURCE}_content_script`,
})
} catch (error) {
console.error("Error starting import:", error)
}
})
document.body.appendChild(button);
document.body.appendChild(button)
}
function showTwitterImportUI() {
if (twitterImportUI) {
twitterImportUI.remove();
}
isTwitterImportOpen = true;
// Check if user is authenticated
browser.storage.local.get([STORAGE_KEYS.BEARER_TOKEN], (result) => {
const isAuthenticated = !!result[STORAGE_KEYS.BEARER_TOKEN];
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: {
type: string;
importedMessage?: string;
totalImported?: number;
type: string
importedMessage?: string
totalImported?: number
}) {
if (!isTwitterImportOpen || !twitterImportUI) return;
const importButton = document.getElementById(ELEMENT_IDS.TWITTER_IMPORT_BUTTON)
if (!importButton) return
const statusDiv = twitterImportUI.querySelector(
`#${ELEMENT_IDS.TWITTER_IMPORT_STATUS}`,
);
const button = twitterImportUI.querySelector(
`#${ELEMENT_IDS.TWITTER_IMPORT_BTN}`,
);
const iconUrl = browser.runtime.getURL("/icon-16.png")
if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) {
if (statusDiv) {
statusDiv.innerHTML = `
<div style="display: flex; align-items: center; gap: 8px; color: #92400e; background: #fef3c7; border: 1px solid #f59e0b; border-radius: 8px; padding: 8px 12px; font-size: 13px;">
<div style="width: 12px; height: 12px; border: 2px solid #f59e0b; border-top: 2px solid transparent; border-radius: 50%; animation: spin 1s linear infinite;"></div>
<span>${message.importedMessage}</span>
</div>
`;
}
if (button) {
(button as HTMLButtonElement).disabled = true;
(button as HTMLButtonElement).textContent = "Importing...";
}
importButton.innerHTML = `
<img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" />
<span style="font-weight: 500; font-size: 14px;">${message.importedMessage}</span>
`
importButton.style.cursor = "default"
}
if (message.type === MESSAGE_TYPES.IMPORT_DONE) {
if (statusDiv) {
statusDiv.innerHTML = `
<div style="display: flex; align-items: center; gap: 8px; color: #0369a1; background: #f0f9ff; border: 1px solid #0ea5e9; border-radius: 8px; padding: 8px 12px; font-size: 13px;">
<span style="color: #059669;"></span>
<span>Successfully imported ${message.totalImported} tweets!</span>
</div>
`;
}
importButton.innerHTML = `
<img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" />
<span style="font-weight: 500; font-size: 14px; color: #059669;"> Imported ${message.totalImported} tweets!</span>
`
setTimeout(() => {
hideTwitterImportUI();
}, 3000);
importButton.innerHTML = `
<img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" />
<span style="font-weight: 500; font-size: 14px;">Import Bookmarks</span>
`
importButton.style.cursor = "pointer"
}, 3000)
}
}
function addSaveChatGPTElementBeforeComposerBtn() {
if (!DOMUtils.isOnDomain(DOMAINS.CHATGPT)) {
return;
return
}
const composerButtons = document.querySelectorAll("button.composer-btn");
const composerButtons = document.querySelectorAll("button.composer-btn")
composerButtons.forEach((button) => {
if (button.hasAttribute("data-supermemory-icon-added-before")) {
return;
return
}
const parent = button.parentElement;
if (!parent) return;
const parent = button.parentElement
if (!parent) return
const parentSiblings = parent.parentElement?.children;
if (!parentSiblings) return;
const parentSiblings = parent.parentElement?.children
if (!parentSiblings) return
let hasSpeechButtonSibling = false;
let hasSpeechButtonSibling = false
for (const sibling of parentSiblings) {
if (
sibling.getAttribute("data-testid") ===
"composer-speech-button-container"
) {
hasSpeechButtonSibling = true;
break;
hasSpeechButtonSibling = true
break
}
}
if (!hasSpeechButtonSibling) return;
if (!hasSpeechButtonSibling) return
const grandParent = parent.parentElement;
if (!grandParent) return;
const grandParent = parent.parentElement
if (!grandParent) return
const existingIcon = grandParent.querySelector(
`#${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer`,
);
)
if (existingIcon) {
button.setAttribute("data-supermemory-icon-added-before", "true");
return;
button.setAttribute("data-supermemory-icon-added-before", "true")
return
}
const saveChatGPTElement = createChatGPTInputBarElement(async () => {
await getRelatedMemories();
});
await getRelatedMemories(
POSTHOG_EVENT_KEY.CHATGPT_CHAT_MEMORIES_SEARCHED,
)
})
saveChatGPTElement.id = `${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
saveChatGPTElement.id = `${ELEMENT_IDS.CHATGPT_INPUT_BAR_ELEMENT}-before-composer-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
button.setAttribute("data-supermemory-icon-added-before", "true");
button.setAttribute("data-supermemory-icon-added-before", "true")
grandParent.insertBefore(saveChatGPTElement, parent);
});
grandParent.insertBefore(saveChatGPTElement, parent)
})
}
function addSupermemoryIconToClaudeInput() {
if (!DOMUtils.isOnDomain(DOMAINS.CLAUDE)) {
return;
return
}
const targetContainers = document.querySelectorAll(
".relative.flex-1.flex.items-center.gap-2.shrink.min-w-0",
);
)
targetContainers.forEach((container) => {
if (container.hasAttribute("data-supermemory-icon-added")) {
return;
return
}
const existingIcon = container.querySelector(
`#${ELEMENT_IDS.CLAUDE_INPUT_BAR_ELEMENT}`,
);
)
if (existingIcon) {
container.setAttribute("data-supermemory-icon-added", "true");
return;
container.setAttribute("data-supermemory-icon-added", "true")
return
}
const supermemoryIcon = createClaudeInputBarElement(async () => {
await getRelatedMemoriesForClaude();
});
await getRelatedMemoriesForClaude()
})
supermemoryIcon.id = `${ELEMENT_IDS.CLAUDE_INPUT_BAR_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
supermemoryIcon.id = `${ELEMENT_IDS.CLAUDE_INPUT_BAR_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
container.setAttribute("data-supermemory-icon-added", "true");
container.setAttribute("data-supermemory-icon-added", "true")
container.insertBefore(supermemoryIcon, container.firstChild);
});
container.insertBefore(supermemoryIcon, container.firstChild)
})
}
async function getRelatedMemoriesForClaude() {
try {
let userQuery = "";
let userQuery = ""
const supermemoryContainer = document.querySelector(
'[data-supermemory-icon-added="true"]',
);
)
if (supermemoryContainer?.parentElement?.previousElementSibling) {
const pTag =
supermemoryContainer.parentElement.previousElementSibling.querySelector(
"p",
);
userQuery = pTag?.innerText || pTag?.textContent || "";
)
userQuery = pTag?.innerText || pTag?.textContent || ""
}
if (!userQuery.trim()) {
const textareaElement = document.querySelector(
'div[contenteditable="true"]',
) as HTMLElement;
) as HTMLElement
userQuery =
textareaElement?.innerText || textareaElement?.textContent || "";
textareaElement?.innerText || textareaElement?.textContent || ""
}
if (!userQuery.trim()) {
const inputElements = document.querySelectorAll(
'div[contenteditable="true"], textarea, input[type="text"]',
);
)
for (const element of inputElements) {
const text =
(element as HTMLElement).innerText ||
(element as HTMLInputElement).value;
(element as HTMLInputElement).value
if (text?.trim()) {
userQuery = text.trim();
break;
userQuery = text.trim()
break
}
}
}
console.log("Claude query extracted:", userQuery);
console.log("Claude query extracted:", userQuery)
if (!userQuery.trim()) {
console.log("No query text found");
DOMUtils.showToast("error");
return;
console.log("No query text found")
DOMUtils.showToast("error")
return
}
const response = await browser.runtime.sendMessage({
action: MESSAGE_TYPES.GET_RELATED_MEMORIES,
data: userQuery,
});
actionSource: POSTHOG_EVENT_KEY.CLAUDE_CHAT_MEMORIES_SEARCHED,
})
console.log("Claude memories response:", response);
console.log("Claude memories response:", response)
if (response.success && response.data) {
const textareaElement = document.querySelector(
'div[contenteditable="true"]',
) as HTMLElement;
) as HTMLElement
if (textareaElement) {
const currentContent = textareaElement.innerHTML;
textareaElement.innerHTML = `${currentContent}<br>Supermemories: ${response.data}`;
const currentContent = textareaElement.innerHTML
textareaElement.innerHTML = `${currentContent}<br>Supermemories: ${response.data}`
textareaElement.dispatchEvent(
new Event("input", { bubbles: true }),
);
textareaElement.dispatchEvent(new Event("input", { bubbles: true }))
} else {
console.log("Could not find Claude input area");
console.log("Could not find Claude input area")
}
} else {
console.log(
"Failed to get memories:",
response.error || "Unknown error",
);
)
}
} catch (error) {
console.error("Error getting related memories for Claude:", error);
console.error("Error getting related memories for Claude:", error)
}
}
function addSupermemoryIconToT3Input() {
if (!DOMUtils.isOnDomain(DOMAINS.T3)) {
return;
return
}
const targetContainers = document.querySelectorAll(
".flex.min-w-0.items-center.gap-2.overflow-hidden",
);
)
targetContainers.forEach((container) => {
if (container.hasAttribute("data-supermemory-icon-added")) {
return;
return
}
const existingIcon = container.querySelector(
`#${ELEMENT_IDS.T3_INPUT_BAR_ELEMENT}`,
);
)
if (existingIcon) {
container.setAttribute("data-supermemory-icon-added", "true");
return;
container.setAttribute("data-supermemory-icon-added", "true")
return
}
const supermemoryIcon = createT3InputBarElement(async () => {
await getRelatedMemoriesForT3();
});
await getRelatedMemoriesForT3()
})
supermemoryIcon.id = `${ELEMENT_IDS.T3_INPUT_BAR_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
supermemoryIcon.id = `${ELEMENT_IDS.T3_INPUT_BAR_ELEMENT}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
container.setAttribute("data-supermemory-icon-added", "true");
container.setAttribute("data-supermemory-icon-added", "true")
container.insertBefore(supermemoryIcon, container.firstChild);
});
container.insertBefore(supermemoryIcon, container.firstChild)
})
}
async function getRelatedMemoriesForT3() {
try {
let userQuery = "";
let userQuery = ""
const supermemoryContainer = document.querySelector(
'[data-supermemory-icon-added="true"]',
);
)
if (
supermemoryContainer?.parentElement?.parentElement
?.previousElementSibling
@ -567,48 +544,49 @@ export default defineContentScript({
const textareaElement =
supermemoryContainer.parentElement.parentElement.previousElementSibling.querySelector(
"textarea",
);
userQuery = textareaElement?.value || "";
)
userQuery = textareaElement?.value || ""
}
if (!userQuery.trim()) {
const textareaElement = document.querySelector(
'div[contenteditable="true"]',
) as HTMLElement;
) as HTMLElement
userQuery =
textareaElement?.innerText || textareaElement?.textContent || "";
textareaElement?.innerText || textareaElement?.textContent || ""
}
if (!userQuery.trim()) {
const textareas = document.querySelectorAll("textarea");
const textareas = document.querySelectorAll("textarea")
for (const textarea of textareas) {
const text = (textarea as HTMLTextAreaElement).value;
const text = (textarea as HTMLTextAreaElement).value
if (text?.trim()) {
userQuery = text.trim();
break;
userQuery = text.trim()
break
}
}
}
console.log("T3 query extracted:", userQuery);
console.log("T3 query extracted:", userQuery)
if (!userQuery.trim()) {
console.log("No query text found");
return;
console.log("No query text found")
return
}
const response = await browser.runtime.sendMessage({
action: MESSAGE_TYPES.GET_RELATED_MEMORIES,
data: userQuery,
});
actionSource: POSTHOG_EVENT_KEY.T3_CHAT_MEMORIES_SEARCHED,
})
console.log("T3 memories response:", response);
console.log("T3 memories response:", response)
if (response.success && response.data) {
let textareaElement = null;
let textareaElement = null
const supermemoryContainer = document.querySelector(
'[data-supermemory-icon-added="true"]',
);
)
if (
supermemoryContainer?.parentElement?.parentElement
?.previousElementSibling
@ -616,75 +594,41 @@ export default defineContentScript({
textareaElement =
supermemoryContainer.parentElement.parentElement.previousElementSibling.querySelector(
"textarea",
);
)
}
if (!textareaElement) {
textareaElement = document.querySelector(
'div[contenteditable="true"]',
) as HTMLElement;
) as HTMLElement
}
if (textareaElement) {
if (textareaElement.tagName === "TEXTAREA") {
const currentContent = (textareaElement as HTMLTextAreaElement)
.value;
(textareaElement as HTMLTextAreaElement).value =
`${currentContent}\n\nSupermemories: ${response.data}`;
.value
;(textareaElement as HTMLTextAreaElement).value =
`${currentContent}\n\nSupermemories: ${response.data}`
} else {
const currentContent = textareaElement.innerHTML;
textareaElement.innerHTML = `${currentContent}<br>Supermemories: ${response.data}`;
const currentContent = textareaElement.innerHTML
textareaElement.innerHTML = `${currentContent}<br>Supermemories: ${response.data}`
}
textareaElement.dispatchEvent(
new Event("input", { bubbles: true }),
);
textareaElement.dispatchEvent(new Event("input", { bubbles: true }))
} else {
console.log("Could not find T3 input area");
console.log("Could not find T3 input area")
}
} else {
console.log(
"Failed to get memories:",
response.error || "Unknown error",
);
)
}
} catch (error) {
console.error("Error getting related memories for T3:", error);
console.error("Error getting related memories for T3:", error)
}
}
// 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?.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 (
@ -692,18 +636,18 @@ export default defineContentScript({
event.shiftKey &&
event.key === "m"
) {
event.preventDefault();
await saveMemory();
event.preventDefault()
await saveMemory()
}
});
})
window.addEventListener("message", (event) => {
if (event.source !== window) {
return;
return
}
const bearerToken = event.data.token;
if (bearerToken) {
const bearerToken = event.data.token
const userData = event.data.userData
if (bearerToken && userData) {
if (
!(
window.location.hostname === "localhost" ||
@ -712,18 +656,19 @@ export default defineContentScript({
)
) {
console.log(
"Bearer token is only allowed to be used on localhost or supermemory.ai",
);
return;
"Bearer token and user data is only allowed to be used on localhost or supermemory.ai",
)
return
}
chrome.storage.local.set(
{
[STORAGE_KEYS.BEARER_TOKEN]: bearerToken,
[STORAGE_KEYS.USER_DATA]: userData,
},
() => {},
);
)
}
});
})
},
});
})

View file

@ -1,118 +1,152 @@
import { useQueryClient } from "@tanstack/react-query";
import { useEffect, useState } from "react";
import "./App.css";
import { STORAGE_KEYS } from "../../utils/constants";
import { useQueryClient } from "@tanstack/react-query"
import { useEffect, useState } from "react"
import "./App.css"
import { MESSAGE_TYPES, STORAGE_KEYS } from "../../utils/constants"
import {
useDefaultProject,
useProjects,
useSetDefaultProject,
} from "../../utils/query-hooks";
import type { Project } from "../../utils/types";
} from "../../utils/query-hooks"
import type { Project } from "../../utils/types"
function App() {
const [userSignedIn, setUserSignedIn] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(true);
const [showProjectSelector, setShowProjectSelector] =
useState<boolean>(false);
const [currentUrl, setCurrentUrl] = useState<string>("");
const [currentTitle, setCurrentTitle] = useState<string>("");
const [saving, setSaving] = useState<boolean>(false);
const [activeTab, setActiveTab] = useState<"save" | "imports">("save");
const [userSignedIn, setUserSignedIn] = useState<boolean>(false)
const [loading, setLoading] = useState<boolean>(true)
const [showProjectSelector, setShowProjectSelector] = useState<boolean>(false)
const [currentUrl, setCurrentUrl] = useState<string>("")
const [currentTitle, setCurrentTitle] = useState<string>("")
const [saving, setSaving] = useState<boolean>(false)
const [activeTab, setActiveTab] = useState<"save" | "imports">("save")
const queryClient = useQueryClient();
const queryClient = useQueryClient()
const { data: projects = [], isLoading: loadingProjects } = useProjects({
enabled: userSignedIn,
});
})
const { data: defaultProject } = useDefaultProject({
enabled: userSignedIn,
});
const setDefaultProjectMutation = useSetDefaultProject();
})
const setDefaultProjectMutation = useSetDefaultProject()
useEffect(() => {
const checkAuthStatus = async () => {
try {
const result = await chrome.storage.local.get([
STORAGE_KEYS.BEARER_TOKEN,
]);
const isSignedIn = !!result[STORAGE_KEYS.BEARER_TOKEN];
setUserSignedIn(isSignedIn);
])
const isSignedIn = !!result[STORAGE_KEYS.BEARER_TOKEN]
setUserSignedIn(isSignedIn)
} catch (error) {
console.error("Error checking auth status:", error);
setUserSignedIn(false);
console.error("Error checking auth status:", error)
setUserSignedIn(false)
} finally {
setLoading(false);
setLoading(false)
}
};
}
const getCurrentTab = async () => {
try {
const tabs = await chrome.tabs.query({
active: true,
currentWindow: true,
});
})
if (tabs.length > 0 && tabs[0].url && tabs[0].title) {
setCurrentUrl(tabs[0].url);
setCurrentTitle(tabs[0].title);
setCurrentUrl(tabs[0].url)
setCurrentTitle(tabs[0].title)
}
} catch (error) {
console.error("Error getting current tab:", error);
console.error("Error getting current tab:", error)
}
};
}
checkAuthStatus();
getCurrentTab();
}, []);
checkAuthStatus()
getCurrentTab()
}, [])
const handleProjectSelect = (project: Project) => {
setDefaultProjectMutation.mutate(project, {
onSuccess: () => {
setShowProjectSelector(false);
setShowProjectSelector(false)
},
onError: (error) => {
console.error("Error setting default project:", error);
console.error("Error setting default project:", error)
},
});
};
})
}
const handleShowProjectSelector = () => {
setShowProjectSelector(true);
};
setShowProjectSelector(true)
}
useEffect(() => {
if (!defaultProject && projects.length > 0) {
const firstProject = projects[0];
setDefaultProjectMutation.mutate(firstProject);
const firstProject = projects[0]
setDefaultProjectMutation.mutate(firstProject)
}
}, [defaultProject, projects, setDefaultProjectMutation]);
}, [defaultProject, projects, setDefaultProjectMutation])
const handleSaveCurrentPage = async () => {
setSaving(true);
setSaving(true)
try {
const tabs = await chrome.tabs.query({
active: true,
currentWindow: true,
});
})
if (tabs.length > 0 && tabs[0].id) {
await chrome.tabs.sendMessage(tabs[0].id, {
action: "saveMemory",
});
const response = await chrome.tabs.sendMessage(tabs[0].id, {
action: MESSAGE_TYPES.SAVE_MEMORY,
actionSource: "popup",
})
if (response?.success) {
await chrome.tabs.sendMessage(tabs[0].id, {
action: MESSAGE_TYPES.SHOW_TOAST,
state: "success",
})
} else {
await chrome.tabs.sendMessage(tabs[0].id, {
action: MESSAGE_TYPES.SHOW_TOAST,
state: "error",
})
}
window.close()
}
} catch (error) {
console.error("Failed to save current page:", error);
console.error("Failed to save current page:", error)
try {
const tabs = await chrome.tabs.query({
active: true,
currentWindow: true,
})
if (tabs.length > 0 && tabs[0].id) {
await chrome.tabs.sendMessage(tabs[0].id, {
action: MESSAGE_TYPES.SHOW_TOAST,
state: "error",
})
}
} catch (toastError) {
console.error("Failed to show error toast:", toastError)
}
window.close()
} finally {
setSaving(false);
setSaving(false)
}
};
}
const handleSignOut = async () => {
try {
await chrome.storage.local.remove([STORAGE_KEYS.BEARER_TOKEN]);
setUserSignedIn(false);
queryClient.clear();
await chrome.storage.local.remove([STORAGE_KEYS.BEARER_TOKEN])
await chrome.storage.local.remove([STORAGE_KEYS.USER_DATA])
await chrome.storage.local.remove([STORAGE_KEYS.DEFAULT_PROJECT])
setUserSignedIn(false)
queryClient.clear()
} catch (error) {
console.error("Error signing out:", error);
console.error("Error signing out:", error)
}
};
}
if (loading) {
return (
@ -131,7 +165,7 @@ function App() {
<div>Loading...</div>
</div>
</div>
);
)
}
return (
@ -267,11 +301,11 @@ function App() {
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<button
className="w-full py-3 px-3 bg-white text-black border border-gray-200 rounded-md text-sm font-medium cursor-pointer flex items-center justify-center transition-colors duration-200 hover:bg-gray-50"
className="w-full py-3 px-3 bg-white text-black border border-gray-200 rounded-md text-sm font-medium cursor-pointer flex items-center justify-start transition-colors duration-200 hover:bg-gray-50"
onClick={() => {
chrome.tabs.create({
url: "https://chatgpt.com/#settings/Personalization",
});
})
}}
type="button"
>
@ -286,17 +320,33 @@ function App() {
<title>OpenAI</title>
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
</svg>
Import ChatGPT Memories
<div className="text-left">
<p>Import ChatGPT Memories</p>
<p className="m-0 text-[10px] text-gray-500 leading-tight">
open 'manage', save your memories to supermemory
</p>
</div>
</button>
</div>
<div className="flex flex-col gap-2">
<button
className="w-full py-3 px-3 bg-white text-black border border-gray-200 rounded-md text-sm font-medium cursor-pointer flex items-center justify-center transition-colors duration-200 outline-none appearance-none hover:bg-gray-50 focus:outline-none"
onClick={() => {
chrome.tabs.create({
url: "https://x.com/i/bookmarks",
});
onClick={async () => {
const [activeTab] = await chrome.tabs.query({
active: true,
currentWindow: true,
})
const targetUrl = "https://x.com/i/bookmarks"
if (activeTab?.url === targetUrl) {
return
}
await chrome.tabs.create({
url: targetUrl,
})
}}
type="button"
>
@ -310,11 +360,13 @@ function App() {
<title>X Twitter Logo</title>
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
Import X Bookmarks
<div className="text-left">
<p>Import X/Twitter Bookmarks</p>
<p className="m-0 text-[10px] text-gray-500 leading-tight">
Click on supermemory on top right to import bookmarks
</p>
</div>
</button>
<p className="m-0 text-xs text-gray-500 leading-tight pl-1">
Click on supermemory on top right to import bookmarks
</p>
</div>
</div>
</div>
@ -393,7 +445,7 @@ function App() {
<button
className="bg-transparent border-none text-blue-500 cursor-pointer underline text-sm p-0 hover:text-blue-700"
onClick={() => {
window.open("mailto:dhravya@supermemory.com", "_blank");
window.open("mailto:dhravya@supermemory.com", "_blank")
}}
type="button"
>
@ -408,7 +460,7 @@ function App() {
url: import.meta.env.PROD
? "https://app.supermemory.ai/login"
: "http://localhost:3000/login",
});
})
}}
type="button"
>
@ -419,7 +471,7 @@ function App() {
)}
</div>
</div>
);
)
}
export default App;
export default App

View file

@ -15,7 +15,6 @@
-webkit-text-size-adjust: 100%;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;

View file

@ -77,7 +77,7 @@ function Welcome() {
url: import.meta.env.PROD
? "https://app.supermemory.ai/login"
: "http://localhost:3000/login",
});
})
}}
type="button"
>
@ -101,7 +101,7 @@ function Welcome() {
</div>
</div>
</div>
);
)
}
export default Welcome;
export default Welcome

View file

@ -17,6 +17,7 @@
"dependencies": {
"@tailwindcss/vite": "^4.1.12",
"@tanstack/react-query": "^5.85.5",
"posthog-js": "^1.261.7",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"tailwindcss": "^4.1.12"

View file

@ -1,27 +1,27 @@
/**
* API service for supermemory browser extension
*/
import { API_ENDPOINTS, STORAGE_KEYS } from "./constants";
import { API_ENDPOINTS, STORAGE_KEYS } from "./constants"
import {
AuthenticationError,
type MemoryPayload,
type Project,
type ProjectsResponse,
SupermemoryAPIError,
} from "./types";
} from "./types"
/**
* Get bearer token from storage
*/
async function getBearerToken(): Promise<string> {
const result = await chrome.storage.local.get([STORAGE_KEYS.BEARER_TOKEN]);
const token = result[STORAGE_KEYS.BEARER_TOKEN];
const result = await chrome.storage.local.get([STORAGE_KEYS.BEARER_TOKEN])
const token = result[STORAGE_KEYS.BEARER_TOKEN]
if (!token) {
throw new AuthenticationError("Bearer token not found");
throw new AuthenticationError("Bearer token not found")
}
return token;
return token
}
/**
@ -31,7 +31,7 @@ async function makeAuthenticatedRequest<T>(
endpoint: string,
options: RequestInit = {},
): Promise<T> {
const token = await getBearerToken();
const token = await getBearerToken()
const response = await fetch(`${API_ENDPOINTS.SUPERMEMORY_API}${endpoint}`, {
...options,
@ -41,19 +41,19 @@ async function makeAuthenticatedRequest<T>(
"Content-Type": "application/json",
...options.headers,
},
});
})
if (!response.ok) {
if (response.status === 401) {
throw new AuthenticationError("Invalid or expired token");
throw new AuthenticationError("Invalid or expired token")
}
throw new SupermemoryAPIError(
`API request failed: ${response.statusText}`,
response.status,
);
)
}
return response.json();
return response.json()
}
/**
@ -62,11 +62,11 @@ async function makeAuthenticatedRequest<T>(
export async function fetchProjects(): Promise<Project[]> {
try {
const response =
await makeAuthenticatedRequest<ProjectsResponse>("/v3/projects");
return response.projects;
await makeAuthenticatedRequest<ProjectsResponse>("/v3/projects")
return response.projects
} catch (error) {
console.error("Failed to fetch projects:", error);
throw error;
console.error("Failed to fetch projects:", error)
throw error
}
}
@ -77,11 +77,11 @@ export async function getDefaultProject(): Promise<Project | null> {
try {
const result = await chrome.storage.local.get([
STORAGE_KEYS.DEFAULT_PROJECT,
]);
return result[STORAGE_KEYS.DEFAULT_PROJECT] || null;
])
return result[STORAGE_KEYS.DEFAULT_PROJECT] || null
} catch (error) {
console.error("Failed to get default project:", error);
return null;
console.error("Failed to get default project:", error)
return null
}
}
@ -92,10 +92,10 @@ export async function setDefaultProject(project: Project): Promise<void> {
try {
await chrome.storage.local.set({
[STORAGE_KEYS.DEFAULT_PROJECT]: project,
});
})
} catch (error) {
console.error("Failed to set default project:", error);
throw error;
console.error("Failed to set default project:", error)
throw error
}
}
@ -107,11 +107,11 @@ export async function saveMemory(payload: MemoryPayload): Promise<unknown> {
const response = await makeAuthenticatedRequest<unknown>("/v3/memories", {
method: "POST",
body: JSON.stringify(payload),
});
return response;
})
return response
} catch (error) {
console.error("Failed to save memory:", error);
throw error;
console.error("Failed to save memory:", error)
throw error
}
}
@ -123,34 +123,40 @@ export async function searchMemories(query: string): Promise<unknown> {
const response = await makeAuthenticatedRequest<unknown>("/v4/search", {
method: "POST",
body: JSON.stringify({ q: query, include: { relatedMemories: true } }),
});
return response;
})
return response
} catch (error) {
console.error("Failed to search memories:", error);
throw error;
console.error("Failed to search memories:", error)
throw error
}
}
/**
* Save tweet to Supermemory API (specific for Twitter imports)
*/
export async function saveTweet(
content: string,
metadata: { sm_source: string; [key: string]: unknown },
containerTag = "sm_project_twitter_bookmarks",
): Promise<void> {
export async function saveAllTweets(
documents: MemoryPayload[],
): Promise<unknown> {
try {
const payload: MemoryPayload = {
containerTags: [containerTag],
content,
metadata,
};
await saveMemory(payload);
const response = await makeAuthenticatedRequest<unknown>(
"/v3/memories/batch",
{
method: "POST",
body: JSON.stringify({
documents,
metadata: {
sm_source: "consumer",
sm_internal_group_id: "twitter_bookmarks",
},
}),
},
)
return response
} catch (error) {
if (error instanceof SupermemoryAPIError && error.statusCode === 409) {
// Skip if already exists (409 Conflict)
return;
return
}
throw error;
throw error
}
}

View file

@ -15,6 +15,7 @@ export const API_ENDPOINTS = {
*/
export const STORAGE_KEYS = {
BEARER_TOKEN: "bearer-token",
USER_DATA: "user-data",
TOKENS_LOGGED: "tokens-logged",
TWITTER_COOKIE: "twitter-cookie",
TWITTER_CSRF: "twitter-csrf",
@ -27,10 +28,6 @@ export const STORAGE_KEYS = {
*/
export const ELEMENT_IDS = {
TWITTER_IMPORT_BUTTON: "sm-twitter-import-button",
TWITTER_IMPORT_STATUS: "sm-twitter-import-status",
TWITTER_CLOSE_BTN: "sm-twitter-close-btn",
TWITTER_IMPORT_BTN: "sm-twitter-import-btn",
TWITTER_SIGNIN_BTN: "sm-twitter-signin-btn",
SUPERMEMORY_TOAST: "sm-toast",
SUPERMEMORY_SAVE_BUTTON: "sm-save-button",
SAVE_TWEET_ELEMENT: "sm-save-tweet-element",
@ -83,3 +80,13 @@ export const MESSAGE_TYPES = {
export const CONTEXT_MENU_IDS = {
SAVE_TO_SUPERMEMORY: "sm-save-to-supermemory",
} as const
export const POSTHOG_EVENT_KEY = {
TWITTER_IMPORT_STARTED: "twitter_import_started",
SAVE_MEMORY_ATTEMPTED: "save_memory_attempted",
SAVE_MEMORY_ATTEMPT_FAILED: "save_memory_attempt_failed",
SOURCE: "extension",
T3_CHAT_MEMORIES_SEARCHED: "t3_chat_memories_searched",
CLAUDE_CHAT_MEMORIES_SEARCHED: "claude_chat_memories_searched",
CHATGPT_CHAT_MEMORIES_SEARCHED: "chatgpt_chat_memories_searched",
} as const

View file

@ -0,0 +1,74 @@
import { PostHog } from "posthog-js/dist/module.no-external"
import { STORAGE_KEYS } from "./constants"
export async function identifyUser(posthog: PostHog): Promise<void> {
const stored = await chrome.storage.local.get([STORAGE_KEYS.USER_DATA])
const userData = stored[STORAGE_KEYS.USER_DATA]
if (userData?.userId) {
posthog.identify(userData.userId, {
email: userData.email,
name: userData.name,
userId: userData.userId,
})
}
}
let posthogInstance: PostHog | null = null
let initializationPromise: Promise<PostHog> | null = null
export const POSTHOG_CONFIG = {
api_host: "https://api.supermemory.ai/orange",
person_profiles: "identified_only",
disable_external_dependency_loading: true,
persistence: "localStorage",
capture_pageview: false,
autocapture: false,
} as const
export async function getPostHogInstance(): Promise<PostHog> {
if (posthogInstance) {
return posthogInstance
}
if (initializationPromise) {
return initializationPromise
}
initializationPromise = initializePostHog()
return initializationPromise
}
async function initializePostHog(): Promise<PostHog> {
try {
const posthog = new PostHog()
if (!import.meta.env.WXT_POSTHOG_API_KEY) {
console.error("PostHog API key not configured")
throw new Error("PostHog API key not configured")
}
posthog.init(import.meta.env.WXT_POSTHOG_API_KEY || "", POSTHOG_CONFIG)
await identifyUser(posthog)
posthogInstance = posthog
return posthog
} catch (error) {
console.error("Failed to initialize PostHog:", error)
initializationPromise = null
throw error
}
}
export async function trackEvent(
eventName: string,
properties?: Record<string, unknown>,
): Promise<void> {
try {
const posthog = await getPostHogInstance()
posthog.capture(eventName, properties)
} catch (error) {
console.error(`Failed to track event ${eventName}:`, error)
}
}

View file

@ -3,16 +3,14 @@
* Handles the import process for Twitter bookmarks
*/
import { saveTweet } from "./api"
import { saveAllTweets } from "./api"
import { createTwitterAPIHeaders, getTwitterTokens } from "./twitter-auth"
import {
BOOKMARKS_URL,
buildRequestVariables,
extractNextCursor,
getAllTweets,
type Tweet,
type TwitterAPIResponse,
tweetToMarkdown,
} from "./twitter-utils"
export type ImportProgressCallback = (message: string) => Promise<void>
@ -47,31 +45,6 @@ class RateLimiter {
}
}
/**
* 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<void> {
const metadata = {
sm_source: "consumer",
tweet_id: tweet.id_str,
author: tweet.user.screen_name,
created_at: tweet.created_at,
likes: tweet.favorite_count,
retweets: tweet.retweet_count || 0,
}
try {
await saveTweet(tweetMd, metadata)
} catch (error) {
throw new Error(
`Failed to save tweet: ${error instanceof Error ? error.message : "Unknown error"}`,
)
}
}
/**
* Main class for handling Twitter bookmarks import
*/
@ -91,9 +64,10 @@ export class TwitterImporter {
}
this.importInProgress = true
const uniqueGroupId = crypto.randomUUID()
try {
await this.batchImportAll("", 0)
await this.batchImportAll("", 0, uniqueGroupId)
this.rateLimiter.reset()
} catch (error) {
await this.config.onError(error as Error)
@ -107,7 +81,7 @@ export class TwitterImporter {
* @param cursor - Pagination cursor for Twitter API
* @param totalImported - Number of tweets imported so far
*/
private async batchImportAll(cursor = "", totalImported = 0): Promise<void> {
private async batchImportAll(cursor = "", totalImported = 0, uniqueGroupId = "twitter_bookmarks"): Promise<void> {
try {
// Use a local variable to track imported count
let importedCount = totalImported
@ -130,9 +104,6 @@ export class TwitterImporter {
? `${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,
@ -145,7 +116,7 @@ export class TwitterImporter {
if (response.status === 429) {
await this.rateLimiter.handleRateLimit(this.config.onProgress)
return this.batchImportAll(cursor, totalImported)
return this.batchImportAll(cursor, totalImported, uniqueGroupId)
}
throw new Error(
`Failed to fetch data: ${response.status} - ${errorText}`,
@ -155,21 +126,45 @@ export class TwitterImporter {
const data: TwitterAPIResponse = await response.json()
const tweets = getAllTweets(data)
console.log("Tweets:", tweets)
const documents: MemoryPayload[] = []
// Process each tweet
// Convert tweets to MemoryPayload
for (const tweet of tweets) {
try {
const tweetMd = tweetToMarkdown(tweet)
await importTweet(tweetMd, tweet)
const metadata = {
sm_source: "consumer",
tweet_id: tweet.id_str,
author: tweet.user.screen_name,
created_at: tweet.created_at,
likes: tweet.favorite_count,
retweets: tweet.retweet_count || 0,
sm_internal_group_id: uniqueGroupId,
}
documents.push({
containerTags: ["sm_project_twitter_bookmarks"],
content: `https://x.com/${tweet.user.screen_name}/status/${tweet.id_str}`,
metadata,
customId: tweet.id_str,
})
importedCount++
await this.config.onProgress(`Imported ${importedCount} tweets`)
await this.config.onProgress(`Imported ${importedCount} tweets, so far...`)
} catch (error) {
console.error("Error importing tweet:", error)
// Continue with next tweet
}
}
try {
if (documents.length > 0) {
await saveAllTweets(documents)
}
console.log("Tweets saved")
console.log("Documents:", documents)
} catch (error) {
console.error("Error saving tweets batch:", error)
await this.config.onError(error as Error)
return
}
// Handle pagination
const instructions =
data.data?.bookmark_timeline_v2?.timeline?.instructions
@ -180,7 +175,7 @@ export class TwitterImporter {
if (nextCursor && tweets.length > 0) {
await new Promise((resolve) => setTimeout(resolve, 1000)) // Rate limiting
await this.batchImportAll(nextCursor, importedCount)
await this.batchImportAll(nextCursor, importedCount, uniqueGroupId)
} else {
await this.config.onComplete(importedCount)
}

View file

@ -17,6 +17,7 @@ export interface ExtensionMessage {
state?: ToastState
importedMessage?: string
totalImported?: number
actionSource?: string
}
/**
@ -32,12 +33,13 @@ export interface MemoryData {
* Supermemory API payload for storing memories
*/
export interface MemoryPayload {
containerTags: string[]
containerTags?: string[]
content: string
metadata: {
sm_source: string
[key: string]: unknown
}
customId?: string
}
/**

View file

@ -3,7 +3,7 @@
* Reusable UI components for the browser extension
*/
import { API_ENDPOINTS, ELEMENT_IDS, UI_CONFIG } from "./constants"
import { ELEMENT_IDS, UI_CONFIG } from "./constants"
import type { ToastState } from "./types"
/**
@ -158,7 +158,7 @@ export function createTwitterImportButton(onClick: () => void): HTMLElement {
color: black;
border: none;
border-radius: 50px;
padding: 12px 16px;
padding: 10px 12px;
cursor: pointer;
display: flex;
align-items: center;
@ -169,15 +169,16 @@ export function createTwitterImportButton(onClick: () => void): HTMLElement {
const iconUrl = browser.runtime.getURL("/icon-16.png")
button.innerHTML = `
<img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" />
<span style="font-weight: 500; font-size: 12px;">Import Bookmarks</span>
`
button.addEventListener("mouseenter", () => {
button.style.transform = "scale(1.05)"
button.style.opacity = "0.8"
button.style.boxShadow = "0 4px 12px rgba(29, 155, 240, 0.4)"
})
button.addEventListener("mouseleave", () => {
button.style.transform = "scale(1)"
button.style.opacity = "1"
button.style.boxShadow = "0 2px 8px rgba(29, 155, 240, 0.3)"
})
@ -186,103 +187,6 @@ export function createTwitterImportButton(onClick: () => void): HTMLElement {
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: 'Space Grotesk', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
`
container.innerHTML = `
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px;">
<div style="display: flex; align-items: center; gap: 8px;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="#1d9bf0">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
</svg>
<h3 style="margin: 0; font-size: 16px; font-weight: 600; color: #0f1419;">
Import Twitter Bookmarks
</h3>
</div>
<button id="${ELEMENT_IDS.TWITTER_CLOSE_BTN}" style="background: none; border: none; cursor: pointer; padding: 4px; border-radius: 4px; color: #536471;">
</button>
</div>
${
isAuthenticated
? `
<div>
<p style="color: #536471; font-size: 14px; margin: 0 0 12px 0; line-height: 1.4;">
This will import all your Twitter bookmarks to Supermemory
</p>
<button id="${ELEMENT_IDS.TWITTER_IMPORT_BTN}" style="width: 100%; background: #1d9bf0; color: white; border: none; border-radius: 20px; padding: 12px 16px; cursor: pointer; font-size: 14px; font-weight: 500; margin-bottom: 12px;">
Import All Bookmarks
</button>
<div id="${ELEMENT_IDS.TWITTER_IMPORT_STATUS}"></div>
</div>
`
: `
<div style="text-align: center;">
<p style="color: #536471; font-size: 14px; margin: 0 0 12px 0;">
Please sign in to supermemory first
</p>
<button id="${ELEMENT_IDS.TWITTER_SIGNIN_BTN}" style="background: #1d9bf0; color: white; border: none; border-radius: 20px; padding: 8px 16px; cursor: pointer; font-size: 14px; font-weight: 500;">
Sign In
</button>
</div>
`
}
<style>
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
`
// 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
@ -510,7 +414,48 @@ export const DOMUtils = {
state: ToastState,
duration: number = UI_CONFIG.TOAST_DURATION,
): HTMLElement {
// Remove all existing toasts more aggressively
const existingToast = document.getElementById(ELEMENT_IDS.SUPERMEMORY_TOAST)
if ((state === "success" || state === "error") && existingToast) {
const icon = existingToast.querySelector("div")
const text = existingToast.querySelector("span")
if (icon && text) {
// Update based on new state
if (state === "success") {
const iconUrl = browser.runtime.getURL("/icon-16.png")
icon.innerHTML = `<img src="${iconUrl}" width="20" height="20" alt="Success" style="border-radius: 2px;" />`
icon.style.animation = ""
text.textContent = "Added to Memory"
} else if (state === "error") {
icon.innerHTML = `
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" fill="#ef4444"/>
<path d="M15 9L9 15" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 9L15 15" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
`
icon.style.animation = ""
text.textContent =
"Failed to save memory / Make sure you are logged in"
}
// Auto-dismiss
setTimeout(() => {
if (document.body.contains(existingToast)) {
existingToast.style.animation = "fadeOut 0.3s ease-out"
setTimeout(() => {
if (document.body.contains(existingToast)) {
existingToast.remove()
}
}, 300)
}
}, duration)
return existingToast
}
}
const existingToasts = document.querySelectorAll(
`#${ELEMENT_IDS.SUPERMEMORY_TOAST}`,
)

View file

@ -1,5 +1,5 @@
import tailwindcss from "@tailwindcss/vite";
import { defineConfig, type WxtViteConfig } from "wxt";
import tailwindcss from "@tailwindcss/vite"
import { defineConfig, type WxtViteConfig } from "wxt"
// See https://wxt.dev/api/config.html
export default defineConfig({
@ -12,13 +12,7 @@ export default defineConfig({
name: "supermemory",
homepage_url: "https://supermemory.ai",
version: "6.0.000",
permissions: [
"contextMenus",
"storage",
"activeTab",
"webRequest",
"tabs",
],
permissions: ["contextMenus", "storage", "activeTab", "webRequest", "tabs"],
host_permissions: [
"*://x.com/*",
"*://twitter.com/*",
@ -26,6 +20,7 @@ export default defineConfig({
"*://api.supermemory.ai/*",
"*://chatgpt.com/*",
"*://chat.openai.com/*",
"https://*.posthog.com/*",
],
web_accessible_resources: [
{
@ -37,4 +32,4 @@ export default defineConfig({
webExt: {
disabled: true,
},
});
})

View file

@ -19,7 +19,6 @@ import {
} from "lucide-react"
import { AnimatePresence, motion } from "motion/react"
import Link from "next/link"
import { useRouter } from "next/navigation"
import { useCallback, useEffect, useMemo, useState } from "react"
import type { z } from "zod"
import { ConnectAIModal } from "@/components/connect-ai-modal"
@ -741,21 +740,30 @@ const MemoryGraphPage = () => {
// Wrapper component to handle auth and waitlist checks
export default function Page() {
const router = useRouter()
const { user } = useAuth()
const { user, session } = useAuth()
useEffect(() => {
// save the token for chrome extension
const url = new URL(window.location.href)
const rawToken = url.searchParams.get("token")
const authenticateChromeExtension = url.searchParams.get(
"extension-auth-success",
)
if (rawToken) {
const encodedToken = encodeURIComponent(rawToken)
window.postMessage({ token: encodedToken }, "*")
url.searchParams.delete("token")
window.history.replaceState({}, "", url.toString())
if (authenticateChromeExtension) {
const sessionToken = session?.token
const userData = {
email: user?.email,
name: user?.name,
userId: user?.id,
}
if (sessionToken && userData?.email) {
const encodedToken = encodeURIComponent(sessionToken)
window.postMessage({ token: encodedToken, userData }, "*")
url.searchParams.delete("extension-auth-success")
window.history.replaceState({}, "", url.toString())
}
}
}, [])
}, [user, session])
// Show loading state while checking authentication and waitlist status
if (!user) {

View file

@ -54,6 +54,7 @@
"dependencies": {
"@tailwindcss/vite": "^4.1.12",
"@tanstack/react-query": "^5.85.5",
"posthog-js": "^1.261.7",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"tailwindcss": "^4.1.12",
@ -4810,6 +4811,8 @@
"strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
"supermemory-browser-extension/posthog-js": ["posthog-js@1.261.7", "", { "dependencies": { "@posthog/core": "1.0.2", "core-js": "^3.38.1", "fflate": "^0.4.8", "preact": "^10.19.3", "web-vitals": "^4.2.4" }, "peerDependencies": { "@rrweb/types": "2.0.0-alpha.17", "rrweb-snapshot": "2.0.0-alpha.17" }, "optionalPeers": ["@rrweb/types", "rrweb-snapshot"] }, "sha512-Fjpbz6VfIMsEbKIN/UyTWhU1DGgVIngqoRjPGRolemIMOVzTfI77OZq8WwiBhMug+rU+wNhGCQhC41qRlR5CxA=="],
"supermemory-browser-extension/typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
"tempy/is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="],