mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-12 18:14:06 +00:00
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
353 lines
9.1 KiB
TypeScript
353 lines
9.1 KiB
TypeScript
import {
|
|
getDefaultProject,
|
|
saveMemory,
|
|
searchMemories,
|
|
fetchProjects,
|
|
} from "../utils/api"
|
|
import {
|
|
CONTAINER_TAGS,
|
|
MESSAGE_TYPES,
|
|
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"
|
|
import type {
|
|
ExtensionMessage,
|
|
MemoryData,
|
|
MemoryPayload,
|
|
} from "../utils/types"
|
|
|
|
const PLATFORM_LABELS: Record<string, string> = {
|
|
chatgpt: "ChatGPT",
|
|
claude: "Claude",
|
|
gemini: "Gemini",
|
|
t3: "T3 Chat",
|
|
twitter: "X / Twitter",
|
|
}
|
|
|
|
function normalizePlatform(value?: string): string | undefined {
|
|
if (!value) return undefined
|
|
return value
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, "_")
|
|
.replace(/^_+|_+$/g, "")
|
|
}
|
|
|
|
function inferPlatformFromActionSource(
|
|
actionSource: string,
|
|
): string | undefined {
|
|
const source = actionSource.toLowerCase()
|
|
if (source.includes("chatgpt")) return "chatgpt"
|
|
if (source.includes("claude")) return "claude"
|
|
if (source.includes("gemini")) return "gemini"
|
|
if (source.includes("t3")) return "t3"
|
|
if (source.includes("twitter") || source.includes("x_")) return "twitter"
|
|
return undefined
|
|
}
|
|
|
|
function inferPlatformFromUrl(url?: string): string | undefined {
|
|
if (!url) return undefined
|
|
try {
|
|
const hostname = new URL(url).hostname
|
|
if (hostname === "chatgpt.com" || hostname === "chat.openai.com") {
|
|
return "chatgpt"
|
|
}
|
|
if (hostname === "claude.ai") return "claude"
|
|
if (hostname === "gemini.google.com") return "gemini"
|
|
if (hostname === "t3.chat") return "t3"
|
|
if (hostname === "x.com" || hostname === "twitter.com") return "twitter"
|
|
} catch {
|
|
return undefined
|
|
}
|
|
}
|
|
|
|
export default defineBackground(() => {
|
|
let twitterImporter: TwitterImporter | null = null
|
|
|
|
browser.runtime.onInstalled.addListener(async (details) => {
|
|
if (details.reason === "install" || details.reason === "update") {
|
|
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 {}
|
|
},
|
|
{ urls: ["*://x.com/*", "*://twitter.com/*"] },
|
|
["requestHeaders", "extraHeaders"],
|
|
)
|
|
|
|
// Send message to current active tab.
|
|
const sendMessageToCurrentTab = async (message: string) => {
|
|
const tabs = await browser.tabs.query({
|
|
active: true,
|
|
currentWindow: true,
|
|
})
|
|
if (tabs.length > 0 && tabs[0].id) {
|
|
await browser.tabs.sendMessage(tabs[0].id, {
|
|
type: MESSAGE_TYPES.IMPORT_UPDATE,
|
|
importedMessage: message,
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send import completion message
|
|
*/
|
|
const sendImportDoneMessage = async (totalImported: number) => {
|
|
const tabs = await browser.tabs.query({
|
|
active: true,
|
|
currentWindow: true,
|
|
})
|
|
if (tabs.length > 0 && tabs[0].id) {
|
|
await browser.tabs.sendMessage(tabs[0].id, {
|
|
type: MESSAGE_TYPES.IMPORT_DONE,
|
|
totalImported,
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save memory to supermemory API
|
|
*/
|
|
const saveMemoryToSupermemory = async (
|
|
data: MemoryData,
|
|
actionSource: string,
|
|
): Promise<{ success: boolean; data?: unknown; error?: string }> => {
|
|
try {
|
|
let containerTag: string = CONTAINER_TAGS.DEFAULT_PROJECT
|
|
try {
|
|
const defaultProject = await getDefaultProject()
|
|
if (defaultProject?.containerTag) {
|
|
containerTag = defaultProject.containerTag
|
|
}
|
|
} catch (error) {
|
|
console.warn("Failed to get default project, using fallback:", error)
|
|
}
|
|
|
|
let content: string
|
|
if (data.content) {
|
|
content = data.content
|
|
} else if (data.highlightedText) {
|
|
content = `${data.highlightedText}\n\n${data?.url || ""}`
|
|
} else if (data.markdown) {
|
|
content = `${data.markdown}\n\n${data?.url || ""}`
|
|
} else if (data.html) {
|
|
content = `${data.html}\n\n${data?.url || ""}`
|
|
} else {
|
|
content = data?.url || ""
|
|
}
|
|
|
|
const platform =
|
|
normalizePlatform(data.sourcePlatform) ||
|
|
inferPlatformFromUrl(data.url) ||
|
|
inferPlatformFromActionSource(actionSource)
|
|
const platformLabel = platform
|
|
? data.sourcePlatformLabel || PLATFORM_LABELS[platform] || platform
|
|
: undefined
|
|
|
|
const metadata: MemoryPayload["metadata"] = {
|
|
sm_source: "consumer",
|
|
sm_origin: "browser_extension",
|
|
sm_origin_action: actionSource,
|
|
website_url: data.url,
|
|
}
|
|
|
|
if (platform) {
|
|
metadata.sm_origin_platform = platform
|
|
}
|
|
|
|
if (platformLabel) {
|
|
metadata.sm_origin_platform_label = platformLabel
|
|
}
|
|
|
|
if (data.sourceSurface) {
|
|
metadata.sm_origin_surface = data.sourceSurface
|
|
}
|
|
|
|
if (data.ogImage) {
|
|
metadata.website_og_image = data.ogImage
|
|
}
|
|
|
|
if (data.title) {
|
|
metadata.website_title = data.title
|
|
}
|
|
|
|
const payload: MemoryPayload = {
|
|
containerTags: [containerTag],
|
|
content,
|
|
metadata,
|
|
}
|
|
|
|
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 {
|
|
let containerTag: string = CONTAINER_TAGS.DEFAULT_PROJECT
|
|
try {
|
|
const defaultProject = await getDefaultProject()
|
|
if (defaultProject?.containerTag) {
|
|
containerTag = defaultProject.containerTag
|
|
}
|
|
} catch (error) {
|
|
console.warn("Failed to get default project, using fallback:", error)
|
|
}
|
|
|
|
const responseData = await searchMemories(data, containerTag)
|
|
const response = responseData as {
|
|
results?: Array<{ memory?: string }>
|
|
}
|
|
const memories: string[] = []
|
|
response.results?.forEach((result, index) => {
|
|
memories.push(`${index + 1}. ${result.memory} \n`)
|
|
})
|
|
await trackEvent(eventSource)
|
|
return { success: true, data: memories }
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle extension messages
|
|
*/
|
|
browser.runtime.onMessage.addListener(
|
|
(message: ExtensionMessage, _sender, sendResponse) => {
|
|
// Handle Twitter import request
|
|
if (message.type === MESSAGE_TYPES.BATCH_IMPORT_ALL) {
|
|
const importConfig: TwitterImportConfig = {
|
|
isFolderImport: message.isFolderImport,
|
|
bookmarkCollectionId: message.bookmarkCollectionId,
|
|
selectedProject: message.selectedProject,
|
|
onProgress: sendMessageToCurrentTab,
|
|
onComplete: sendImportDoneMessage,
|
|
onError: async (error: Error) => {
|
|
await sendMessageToCurrentTab(`Error: ${error.message}`)
|
|
},
|
|
}
|
|
|
|
twitterImporter = new TwitterImporter(importConfig)
|
|
twitterImporter.startImport().catch(console.error)
|
|
sendResponse({ success: true })
|
|
return true
|
|
}
|
|
|
|
// Handle regular memory save request
|
|
if (message.action === MESSAGE_TYPES.SAVE_MEMORY) {
|
|
;(async () => {
|
|
try {
|
|
const result = await saveMemoryToSupermemory(
|
|
message.data as MemoryData,
|
|
message.actionSource || "unknown",
|
|
)
|
|
sendResponse(result)
|
|
} catch (error) {
|
|
sendResponse({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
})
|
|
}
|
|
})()
|
|
return true
|
|
}
|
|
|
|
if (message.action === MESSAGE_TYPES.GET_RELATED_MEMORIES) {
|
|
;(async () => {
|
|
try {
|
|
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
|
|
}
|
|
|
|
if (message.action === MESSAGE_TYPES.CAPTURE_PROMPT) {
|
|
;(async () => {
|
|
try {
|
|
const messageData = message.data as {
|
|
prompt: string
|
|
platform: string
|
|
source: string
|
|
}
|
|
|
|
const memoryData: MemoryData = {
|
|
content: messageData.prompt,
|
|
url: messageData.source,
|
|
sourcePlatform: messageData.platform,
|
|
sourceSurface: "prompt_capture",
|
|
}
|
|
|
|
const result = await saveMemoryToSupermemory(
|
|
memoryData,
|
|
`prompt_capture_${messageData.platform}`,
|
|
)
|
|
sendResponse(result)
|
|
} catch (error) {
|
|
sendResponse({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
})
|
|
}
|
|
})()
|
|
return true
|
|
}
|
|
|
|
if (message.action === MESSAGE_TYPES.FETCH_PROJECTS) {
|
|
;(async () => {
|
|
try {
|
|
const projects = await fetchProjects()
|
|
sendResponse({ success: true, data: projects })
|
|
} catch (error) {
|
|
sendResponse({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
})
|
|
}
|
|
})()
|
|
return true
|
|
}
|
|
},
|
|
)
|
|
})
|