Skip to content

Commit db00b0a

Browse files
authored
Merge pull request supermemoryai#395 from supermemoryai/mahesh/browser-extension
feat: browser extension
2 parents 12223f6 + a238aa0 commit db00b0a

36 files changed

Lines changed: 4727 additions & 199 deletions

apps/browser-extension/.gitignore

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
.output
12+
stats.html
13+
stats-*.json
14+
.wxt
15+
web-ext.config.ts
16+
17+
# Editor directories and files
18+
.vscode/*
19+
!.vscode/extensions.json
20+
.idea
21+
.DS_Store
22+
*.suo
23+
*.ntvs*
24+
*.njsproj
25+
*.sln
26+
*.sw?

apps/browser-extension/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
## supermemory Browser Extension
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import { getDefaultProject, saveMemory, searchMemories } from "../utils/api"
2+
import {
3+
CONTAINER_TAGS,
4+
CONTEXT_MENU_IDS,
5+
MESSAGE_TYPES,
6+
} from "../utils/constants"
7+
import { captureTwitterTokens } from "../utils/twitter-auth"
8+
import {
9+
type TwitterImportConfig,
10+
TwitterImporter,
11+
} from "../utils/twitter-import"
12+
import type {
13+
ExtensionMessage,
14+
MemoryData,
15+
MemoryPayload,
16+
} from "../utils/types"
17+
18+
export default defineBackground(() => {
19+
let twitterImporter: TwitterImporter | null = null
20+
21+
browser.runtime.onInstalled.addListener((details) => {
22+
browser.contextMenus.create({
23+
id: CONTEXT_MENU_IDS.SAVE_TO_SUPERMEMORY,
24+
title: "Save to supermemory",
25+
contexts: ["selection", "page", "link"],
26+
})
27+
28+
// Open welcome tab on first install
29+
if (details.reason === "install") {
30+
browser.tabs.create({
31+
url: browser.runtime.getURL("/welcome.html"),
32+
})
33+
}
34+
})
35+
36+
// Intercept Twitter requests to capture authentication headers.
37+
browser.webRequest.onBeforeSendHeaders.addListener(
38+
(details) => {
39+
captureTwitterTokens(details)
40+
return {}
41+
},
42+
{ urls: ["*://x.com/*", "*://twitter.com/*"] },
43+
["requestHeaders", "extraHeaders"],
44+
)
45+
46+
// Handle context menu clicks.
47+
browser.contextMenus.onClicked.addListener(async (info, tab) => {
48+
if (info.menuItemId === CONTEXT_MENU_IDS.SAVE_TO_SUPERMEMORY) {
49+
if (tab?.id) {
50+
try {
51+
await browser.tabs.sendMessage(tab.id, {
52+
action: MESSAGE_TYPES.SAVE_MEMORY,
53+
})
54+
} catch (error) {
55+
console.error("Failed to send message to content script:", error)
56+
}
57+
}
58+
}
59+
})
60+
61+
// Send message to current active tab.
62+
const sendMessageToCurrentTab = async (message: string) => {
63+
const tabs = await browser.tabs.query({
64+
active: true,
65+
currentWindow: true,
66+
})
67+
if (tabs.length > 0 && tabs[0].id) {
68+
await browser.tabs.sendMessage(tabs[0].id, {
69+
type: MESSAGE_TYPES.IMPORT_UPDATE,
70+
importedMessage: message,
71+
})
72+
}
73+
}
74+
75+
/**
76+
* Send import completion message
77+
*/
78+
const sendImportDoneMessage = async (totalImported: number) => {
79+
const tabs = await browser.tabs.query({
80+
active: true,
81+
currentWindow: true,
82+
})
83+
if (tabs.length > 0 && tabs[0].id) {
84+
await browser.tabs.sendMessage(tabs[0].id, {
85+
type: MESSAGE_TYPES.IMPORT_DONE,
86+
totalImported,
87+
})
88+
}
89+
}
90+
91+
/**
92+
* Save memory to supermemory API
93+
*/
94+
const saveMemoryToSupermemory = async (
95+
data: MemoryData,
96+
): Promise<{ success: boolean; data?: unknown; error?: string }> => {
97+
try {
98+
let containerTag: string = CONTAINER_TAGS.DEFAULT_PROJECT
99+
try {
100+
const defaultProject = await getDefaultProject()
101+
if (defaultProject?.containerTag) {
102+
containerTag = defaultProject.containerTag
103+
}
104+
} catch (error) {
105+
console.warn("Failed to get default project, using fallback:", error)
106+
}
107+
108+
const payload: MemoryPayload = {
109+
containerTags: [containerTag],
110+
content: `${data.highlightedText}\n\n${data.html}\n\n${data?.url}`,
111+
metadata: { sm_source: "consumer" },
112+
}
113+
114+
const responseData = await saveMemory(payload)
115+
return { success: true, data: responseData }
116+
} catch (error) {
117+
return {
118+
success: false,
119+
error: error instanceof Error ? error.message : "Unknown error",
120+
}
121+
}
122+
}
123+
124+
const getRelatedMemories = async (
125+
data: string,
126+
): Promise<{ success: boolean; data?: unknown; error?: string }> => {
127+
try {
128+
const responseData = await searchMemories(data)
129+
const response = responseData as {
130+
results?: Array<{ memory?: string }>
131+
}
132+
let memories = "";
133+
response.results?.forEach((result, index) => {
134+
memories += `[${index + 1}] ${result.memory} `
135+
})
136+
console.log("Memories:", memories)
137+
return { success: true, data: memories }
138+
} catch (error) {
139+
return {
140+
success: false,
141+
error: error instanceof Error ? error.message : "Unknown error",
142+
}
143+
}
144+
}
145+
146+
/**
147+
* Handle extension messages
148+
*/
149+
browser.runtime.onMessage.addListener(
150+
(message: ExtensionMessage, _sender, sendResponse) => {
151+
// Handle Twitter import request
152+
if (message.type === MESSAGE_TYPES.BATCH_IMPORT_ALL) {
153+
const importConfig: TwitterImportConfig = {
154+
onProgress: sendMessageToCurrentTab,
155+
onComplete: sendImportDoneMessage,
156+
onError: async (error: Error) => {
157+
await sendMessageToCurrentTab(`Error: ${error.message}`)
158+
},
159+
}
160+
161+
twitterImporter = new TwitterImporter(importConfig)
162+
twitterImporter.startImport().catch(console.error)
163+
sendResponse({ success: true })
164+
return true
165+
}
166+
167+
// Handle regular memory save request
168+
if (message.action === MESSAGE_TYPES.SAVE_MEMORY) {
169+
;(async () => {
170+
try {
171+
const result = await saveMemoryToSupermemory(
172+
message.data as MemoryData,
173+
)
174+
sendResponse(result)
175+
} catch (error) {
176+
sendResponse({
177+
success: false,
178+
error: error instanceof Error ? error.message : "Unknown error",
179+
})
180+
}
181+
})()
182+
return true
183+
}
184+
185+
if (message.action === MESSAGE_TYPES.GET_RELATED_MEMORIES) {
186+
;(async () => {
187+
try {
188+
const result = await getRelatedMemories(message.data as string)
189+
sendResponse(result)
190+
} catch (error) {
191+
sendResponse({
192+
success: false,
193+
error: error instanceof Error ? error.message : "Unknown error",
194+
})
195+
}
196+
})()
197+
return true
198+
}
199+
},
200+
)
201+
})

0 commit comments

Comments
 (0)