From 3e5ea2fd9ed210644ae29b013b579703e30986de Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Mon, 8 Sep 2025 05:19:10 +0000 Subject: extension: updated telemetry and batch upload (#415) --- apps/browser-extension/.env.example | 2 + apps/browser-extension/entrypoints/background.ts | 130 +++-- apps/browser-extension/entrypoints/content.ts | 565 ++++++++++----------- apps/browser-extension/entrypoints/popup/App.tsx | 194 ++++--- apps/browser-extension/entrypoints/popup/style.css | 1 - .../entrypoints/welcome/Welcome.tsx | 6 +- apps/browser-extension/package.json | 1 + apps/browser-extension/utils/api.ts | 92 ++-- apps/browser-extension/utils/constants.ts | 15 +- apps/browser-extension/utils/posthog.ts | 74 +++ apps/browser-extension/utils/twitter-import.ts | 77 ++- apps/browser-extension/utils/types.ts | 4 +- apps/browser-extension/utils/ui-components.ts | 149 ++---- apps/browser-extension/wxt.config.ts | 15 +- apps/web/app/page.tsx | 30 +- bun.lock | 3 + 16 files changed, 706 insertions(+), 652 deletions(-) create mode 100644 apps/browser-extension/.env.example create mode 100644 apps/browser-extension/utils/posthog.ts diff --git a/apps/browser-extension/.env.example b/apps/browser-extension/.env.example new file mode 100644 index 00000000..aca1ac3d --- /dev/null +++ b/apps/browser-extension/.env.example @@ -0,0 +1,2 @@ +# PostHog Configuration +WXT_POSTHOG_API_KEY=your_posthog_project_api_key_here \ No newline at end of file diff --git a/apps/browser-extension/entrypoints/background.ts b/apps/browser-extension/entrypoints/background.ts index 1f4ae24e..05fef55d 100644 --- a/apps/browser-extension/entrypoints/background.ts +++ b/apps/browser-extension/entrypoints/background.ts @@ -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) + + 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, + }) - const responseData = await saveMemory(payload); - return { success: true, data: responseData }; + 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 } }, - ); -}); + ) +}) diff --git a/apps/browser-extension/entrypoints/content.ts b/apps/browser-extension/entrypoints/content.ts index 6c8a96c5..e755efa6 100644 --- a/apps/browser-extension/entrypoints/content.ts +++ b/apps/browser-extension/entrypoints/content.ts @@ -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: [""], 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}
Supermemories: ${response.data}`; + const currentContent = promptElement.innerHTML + promptElement.innerHTML = `${currentContent}
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 = `
supermemory Save to supermemory
- `; + ` 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; - } - - const button = createTwitterImportButton(() => { - showTwitterImportUI(); - }); - - document.body.appendChild(button); - } - - function showTwitterImportUI() { - if (twitterImportUI) { - twitterImportUI.remove(); + return } - 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, - ); + 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(twitterImportUI); - }); + document.body.appendChild(button) } - 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 = ` -
-
- ${message.importedMessage} -
- `; - } - if (button) { - (button as HTMLButtonElement).disabled = true; - (button as HTMLButtonElement).textContent = "Importing..."; - } + importButton.innerHTML = ` + Save to Memory + ${message.importedMessage} + ` + importButton.style.cursor = "default" } if (message.type === MESSAGE_TYPES.IMPORT_DONE) { - if (statusDiv) { - statusDiv.innerHTML = ` -
- - Successfully imported ${message.totalImported} tweets! -
- `; - } - + importButton.innerHTML = ` + Save to Memory + ✓ Imported ${message.totalImported} tweets! + ` + setTimeout(() => { - hideTwitterImportUI(); - }, 3000); + importButton.innerHTML = ` + Save to Memory + Import Bookmarks + ` + 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}
Supermemories: ${response.data}`; + const currentContent = textareaElement.innerHTML + textareaElement.innerHTML = `${currentContent}
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}
Supermemories: ${response.data}`; + const currentContent = textareaElement.innerHTML + textareaElement.innerHTML = `${currentContent}
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, }, () => {}, - ); + ) } - }); + }) }, -}); +}) diff --git a/apps/browser-extension/entrypoints/popup/App.tsx b/apps/browser-extension/entrypoints/popup/App.tsx index 3f960b34..ddb498c6 100644 --- a/apps/browser-extension/entrypoints/popup/App.tsx +++ b/apps/browser-extension/entrypoints/popup/App.tsx @@ -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(false); - const [loading, setLoading] = useState(true); - const [showProjectSelector, setShowProjectSelector] = - useState(false); - const [currentUrl, setCurrentUrl] = useState(""); - const [currentTitle, setCurrentTitle] = useState(""); - const [saving, setSaving] = useState(false); - const [activeTab, setActiveTab] = useState<"save" | "imports">("save"); + const [userSignedIn, setUserSignedIn] = useState(false) + const [loading, setLoading] = useState(true) + const [showProjectSelector, setShowProjectSelector] = useState(false) + const [currentUrl, setCurrentUrl] = useState("") + const [currentTitle, setCurrentTitle] = useState("") + const [saving, setSaving] = useState(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() {
Loading...
- ); + ) } return ( @@ -267,11 +301,11 @@ function App() {
-

- Click on supermemory on top right to import bookmarks -

@@ -393,7 +445,7 @@ function App() { - - - ${ - isAuthenticated - ? ` -
-

- This will import all your Twitter bookmarks to Supermemory -

- - - -
-
- ` - : ` -
-

- 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 @@ -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 = `Success` + icon.style.animation = "" + text.textContent = "Added to Memory" + } else if (state === "error") { + icon.innerHTML = ` + + + + + + ` + 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}`, ) diff --git a/apps/browser-extension/wxt.config.ts b/apps/browser-extension/wxt.config.ts index 0ef00bd2..d655949a 100644 --- a/apps/browser-extension/wxt.config.ts +++ b/apps/browser-extension/wxt.config.ts @@ -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, }, -}); +}) diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 6bf8cf6f..f66327e1 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -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 (authenticateChromeExtension) { + const sessionToken = session?.token + const userData = { + email: user?.email, + name: user?.name, + userId: user?.id, + } - if (rawToken) { - const encodedToken = encodeURIComponent(rawToken) - window.postMessage({ token: encodedToken }, "*") - url.searchParams.delete("token") - window.history.replaceState({}, "", url.toString()) + 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) { diff --git a/bun.lock b/bun.lock index 00818471..7ece9abc 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], -- cgit v1.2.3