diff options
| author | Dhravya Shah <[email protected]> | 2025-09-24 23:26:38 -0700 |
|---|---|---|
| committer | Dhravya Shah <[email protected]> | 2025-09-24 23:26:38 -0700 |
| commit | 697315b9dc64b1c7e5534ea64dcd22b13d57de24 (patch) | |
| tree | ff296b7343e4ff86eaa8947ef8d0bc0c695b4d4f | |
| parent | bump version (diff) | |
| parent | fix: SSR issue while localStorage is being accessed (#437) (diff) | |
| download | supermemory-697315b9dc64b1c7e5534ea64dcd22b13d57de24.tar.xz supermemory-697315b9dc64b1c7e5534ea64dcd22b13d57de24.zip | |
Merge branch 'main' of https://github.com/supermemoryai/supermemory
| -rw-r--r-- | CONTRIBUTING.md (renamed from CONTRIBUTE.md) | 0 | ||||
| -rw-r--r-- | README.md | 2 | ||||
| -rw-r--r-- | apps/browser-extension/entrypoints/popup/App.tsx | 108 | ||||
| -rw-r--r-- | apps/browser-extension/utils/api.ts | 29 | ||||
| -rw-r--r-- | apps/browser-extension/utils/query-hooks.ts | 12 | ||||
| -rw-r--r-- | apps/browser-extension/utils/ui-components.ts | 52 | ||||
| -rw-r--r-- | apps/browser-extension/wxt.config.ts | 2 | ||||
| -rw-r--r-- | apps/web/components/connect-ai-modal.tsx | 226 | ||||
| -rw-r--r-- | apps/web/components/views/billing.tsx | 2 | ||||
| -rw-r--r-- | apps/web/components/views/profile.tsx | 2 | ||||
| -rw-r--r-- | packages/lib/constants.ts | 2 |
11 files changed, 364 insertions, 73 deletions
diff --git a/CONTRIBUTE.md b/CONTRIBUTING.md index b3ea376f..b3ea376f 100644 --- a/CONTRIBUTE.md +++ b/CONTRIBUTING.md @@ -56,7 +56,7 @@ We welcome contributions from developers of all skill levels! Whether you're fix 3. **Set up your environment** by copying `.env.example` to `.env.local` 4. **Start developing** with `bun run dev` -For detailed guidelines, development setup, coding standards, and the complete contribution workflow, please see our [**Contributing Guide**](CONTRIBUTE.md). +For detailed guidelines, development setup, coding standards, and the complete contribution workflow, please see our [**Contributing Guide**](CONTRIBUTING.md). ### Ways to Contribute diff --git a/apps/browser-extension/entrypoints/popup/App.tsx b/apps/browser-extension/entrypoints/popup/App.tsx index 3e4f15e2..c6e8468c 100644 --- a/apps/browser-extension/entrypoints/popup/App.tsx +++ b/apps/browser-extension/entrypoints/popup/App.tsx @@ -1,11 +1,13 @@ import { useQueryClient } from "@tanstack/react-query" import { useEffect, useState } from "react" import "./App.css" +import { validateAuthToken } from "../../utils/api" import { MESSAGE_TYPES, STORAGE_KEYS } from "../../utils/constants" import { useDefaultProject, useProjects, useSetDefaultProject, + useUserData, } from "../../utils/query-hooks" import type { Project } from "../../utils/types" @@ -20,6 +22,7 @@ function App() { "save", ) const [autoSearchEnabled, setAutoSearchEnabled] = useState<boolean>(false) + const [authInvalidated, setAuthInvalidated] = useState<boolean>(false) const queryClient = useQueryClient() const { data: projects = [], isLoading: loadingProjects } = useProjects({ @@ -28,8 +31,12 @@ function App() { const { data: defaultProject } = useDefaultProject({ enabled: userSignedIn, }) + const { data: userData, isLoading: loadingUserData } = useUserData({ + enabled: userSignedIn, + }) const setDefaultProjectMutation = useSetDefaultProject() + // biome-ignore lint/correctness/useExhaustiveDependencies: suppress dependency analysis useEffect(() => { const checkAuthStatus = async () => { try { @@ -37,8 +44,28 @@ function App() { STORAGE_KEYS.BEARER_TOKEN, STORAGE_KEYS.AUTO_SEARCH_ENABLED, ]) - const isSignedIn = !!result[STORAGE_KEYS.BEARER_TOKEN] - setUserSignedIn(isSignedIn) + const hasToken = !!result[STORAGE_KEYS.BEARER_TOKEN] + + if (hasToken) { + const isTokenValid = await validateAuthToken() + + if (isTokenValid) { + setUserSignedIn(true) + setAuthInvalidated(false) + } else { + await chrome.storage.local.remove([ + STORAGE_KEYS.BEARER_TOKEN, + STORAGE_KEYS.USER_DATA, + STORAGE_KEYS.DEFAULT_PROJECT, + ]) + queryClient.clear() + setUserSignedIn(false) + setAuthInvalidated(true) + } + } else { + setUserSignedIn(false) + setAuthInvalidated(false) + } const autoSearchSetting = result[STORAGE_KEYS.AUTO_SEARCH_ENABLED] ?? false @@ -46,6 +73,7 @@ function App() { } catch (error) { console.error("Error checking auth status:", error) setUserSignedIn(false) + setAuthInvalidated(false) } finally { setLoading(false) } @@ -397,6 +425,34 @@ function App() { </div> ) : ( <div className="flex flex-col gap-4 min-h-[200px]"> + {/* Account Section */} + <div> + <h3 className="text-base font-semibold text-black mb-3"> + Account + </h3> + <div className="p-3 bg-gray-50 rounded-lg border border-gray-200"> + {loadingUserData ? ( + <div className="text-sm text-gray-500"> + Loading account data... + </div> + ) : userData?.email ? ( + <div className="flex justify-between items-center"> + <span className="text-xs font-medium text-black"> + Email + </span> + <span className="text-sm text-gray-600"> + {userData.email} + </span> + </div> + ) : ( + <div className="text-sm text-gray-500"> + No email found + </div> + )} + </div> + </div> + + {/* Chat Integration Section */} <div className="mb-4"> <h3 className="text-base font-semibold text-black mb-3"> Chat Integration @@ -480,23 +536,37 @@ function App() { </div> ) : ( <div className="text-center py-2"> - <div className="mb-8"> - <h2 className="m-0 mb-4 text-sm font-normal text-black leading-tight"> - Login to unlock all chrome extension features - </h2> - - <ul className="list-none p-0 m-0 text-left"> - <li className="py-1.5 text-sm text-black relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-black before:font-bold"> - Save any page to your supermemory - </li> - <li className="py-1.5 text-sm text-black relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-black before:font-bold"> - Import all your Twitter / X Bookmarks - </li> - <li className="py-1.5 text-sm text-black relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-black before:font-bold"> - Import your ChatGPT Memories - </li> - </ul> - </div> + {authInvalidated ? ( + <div className="mb-8"> + <div className="p-3 mb-4 bg-red-50 border border-red-200 rounded-lg"> + <h2 className="m-0 mb-2 text-sm font-semibold text-red-800 leading-tight"> + Session Expired + </h2> + <p className="m-0 text-xs text-red-600 leading-tight"> + Logged out since authentication was invalidated. Please + login again. + </p> + </div> + </div> + ) : ( + <div className="mb-8"> + <h2 className="m-0 mb-4 text-sm font-normal text-black leading-tight"> + Login to unlock all chrome extension features + </h2> + + <ul className="list-none p-0 m-0 text-left"> + <li className="py-1.5 text-sm text-black relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-black before:font-bold"> + Save any page to your supermemory + </li> + <li className="py-1.5 text-sm text-black relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-black before:font-bold"> + Import all your Twitter / X Bookmarks + </li> + <li className="py-1.5 text-sm text-black relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-black before:font-bold"> + Import your ChatGPT Memories + </li> + </ul> + </div> + )} <div className="mt-8"> <p className="m-0 mb-4 text-sm text-gray-500"> diff --git a/apps/browser-extension/utils/api.ts b/apps/browser-extension/utils/api.ts index 2a4c838b..d98b39f5 100644 --- a/apps/browser-extension/utils/api.ts +++ b/apps/browser-extension/utils/api.ts @@ -100,6 +100,35 @@ export async function setDefaultProject(project: Project): Promise<void> { } /** + * Validate if current bearer token is still valid + */ +export async function validateAuthToken(): Promise<boolean> { + try { + await makeAuthenticatedRequest<ProjectsResponse>("/v3/projects") + return true + } catch (error) { + if (error instanceof AuthenticationError) { + return false + } + console.error("Failed to validate auth token:", error) + return true + } +} + +/** + * Get user data from storage + */ +export async function getUserData(): Promise<{ email?: string } | null> { + try { + const result = await chrome.storage.local.get([STORAGE_KEYS.USER_DATA]) + return result[STORAGE_KEYS.USER_DATA] || null + } catch (error) { + console.error("Failed to get user data:", error) + return null + } +} + +/** * Save memory to Supermemory API */ export async function saveMemory(payload: MemoryPayload): Promise<unknown> { diff --git a/apps/browser-extension/utils/query-hooks.ts b/apps/browser-extension/utils/query-hooks.ts index 721a68ad..5c7cfc21 100644 --- a/apps/browser-extension/utils/query-hooks.ts +++ b/apps/browser-extension/utils/query-hooks.ts @@ -5,6 +5,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { fetchProjects, getDefaultProject, + getUserData, saveMemory, searchMemories, setDefaultProject, @@ -15,6 +16,7 @@ import type { MemoryPayload } from "./types" export const queryKeys = { projects: ["projects"] as const, defaultProject: ["defaultProject"] as const, + userData: ["userData"] as const, } // Projects Query @@ -37,6 +39,16 @@ export function useDefaultProject(options?: { enabled?: boolean }) { }) } +// User Data Query +export function useUserData(options?: { enabled?: boolean }) { + return useQuery({ + queryKey: queryKeys.userData, + queryFn: getUserData, + staleTime: 5 * 60 * 1000, // 5 minutes + enabled: options?.enabled ?? true, + }) +} + // Set Default Project Mutation export function useSetDefaultProject() { const queryClient = useQueryClient() diff --git a/apps/browser-extension/utils/ui-components.ts b/apps/browser-extension/utils/ui-components.ts index 8a56ea5a..dabe4974 100644 --- a/apps/browser-extension/utils/ui-components.ts +++ b/apps/browser-extension/utils/ui-components.ts @@ -94,8 +94,8 @@ export function createToast(state: ToastState): HTMLElement { const icon = document.createElement("div") icon.style.cssText = "width: 20px; height: 20px; flex-shrink: 0;" - const text = document.createElement("span") - text.style.fontWeight = "500" + let textElement: HTMLElement = document.createElement("span") + textElement.style.fontWeight = "500" // Configure toast based on state switch (state) { @@ -113,17 +113,17 @@ export function createToast(state: ToastState): HTMLElement { </svg> ` icon.style.animation = "spin 1s linear infinite" - text.textContent = "Adding to Memory..." + textElement.textContent = "Adding to Memory..." break case "success": { const iconUrl = browser.runtime.getURL("/icon-16.png") icon.innerHTML = `<img src="${iconUrl}" width="20" height="20" alt="Success" style="border-radius: 2px;" />` - text.textContent = "Added to Memory" + textElement.textContent = "Added to Memory" break } - case "error": + case "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"/> @@ -131,12 +131,29 @@ export function createToast(state: ToastState): HTMLElement { <path d="M9 9L15 15" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> ` - text.textContent = "Failed to save memory / Make sure you are logged in" + const textContainer = document.createElement("div") + textContainer.style.cssText = + "display: flex; flex-direction: column; gap: 2px;" + + const mainText = document.createElement("span") + mainText.style.cssText = "font-weight: 500; line-height: 1.2;" + mainText.textContent = "Failed to save memory" + + const helperText = document.createElement("span") + helperText.style.cssText = + "font-size: 12px; color: #6b7280; font-weight: 400; line-height: 1.2;" + helperText.textContent = "Make sure you are logged in" + + textContainer.appendChild(mainText) + textContainer.appendChild(helperText) + + textElement = textContainer break + } } toast.appendChild(icon) - toast.appendChild(text) + toast.appendChild(textElement) return toast } @@ -433,8 +450,25 @@ export const DOMUtils = { </svg> ` icon.style.animation = "" - text.textContent = - "Failed to save memory / Make sure you are logged in" + + const textContainer = document.createElement("div") + textContainer.style.cssText = + "display: flex; flex-direction: column; gap: 2px;" + + const mainText = document.createElement("span") + mainText.style.cssText = "font-weight: 500; line-height: 1.2;" + mainText.textContent = "Failed to save memory" + + const helperText = document.createElement("span") + helperText.style.cssText = + "font-size: 12px; color: #6b7280; font-weight: 400; line-height: 1.2;" + helperText.textContent = "Make sure you are logged in" + + textContainer.appendChild(mainText) + textContainer.appendChild(helperText) + + text.innerHTML = "" + text.appendChild(textContainer) } // Auto-dismiss diff --git a/apps/browser-extension/wxt.config.ts b/apps/browser-extension/wxt.config.ts index 1bd7910d..b536b853 100644 --- a/apps/browser-extension/wxt.config.ts +++ b/apps/browser-extension/wxt.config.ts @@ -11,7 +11,7 @@ export default defineConfig({ manifest: { name: "supermemory", homepage_url: "https://supermemory.ai", - version: "6.0.002", + version: "6.0.003", permissions: ["contextMenus", "storage", "activeTab", "webRequest", "tabs"], host_permissions: [ "*://x.com/*", diff --git a/apps/web/components/connect-ai-modal.tsx b/apps/web/components/connect-ai-modal.tsx index 6500ba23..b9759d4e 100644 --- a/apps/web/components/connect-ai-modal.tsx +++ b/apps/web/components/connect-ai-modal.tsx @@ -27,6 +27,7 @@ import { useEffect, useState } from "react" import { toast } from "sonner" import { z } from "zod/v4" import { analytics } from "@/lib/analytics" +import { cn } from "@lib/utils" const clients = { cursor: "Cursor", @@ -79,7 +80,19 @@ export function ConnectAIModal({ const setIsOpen = onOpenChange || setInternalIsOpen const [isMigrateDialogOpen, setIsMigrateDialogOpen] = useState(false) const [selectedProject, setSelectedProject] = useState<string | null>("none") - const projectId = localStorage.getItem("selectedProject") ?? "default" + const [cursorInstallTab, setCursorInstallTab] = useState< + "oneClick" | "manual" + >("oneClick") + + const [projectId, setProjectId] = useState("default") + + useEffect(() => { + if (typeof window !== "undefined") { + const storedProjectId = + localStorage.getItem("selectedProject") ?? "default" + setProjectId(storedProjectId) + } + }, []) useEffect(() => { analytics.mcpViewOpened() @@ -164,6 +177,10 @@ export function ConnectAIModal({ return command } + function getCursorDeeplink() { + return "https://cursor.com/en/install-mcp?name=supermemory-mcp&config=eyJjb21tYW5kIjoibnB4IC15IG1jcC1yZW1vdGUgaHR0cHM6Ly9hcGkuc3VwZXJtZW1vcnkuYWkvbWNwIn0%3D" + } + const copyToClipboard = () => { const command = generateInstallCommand() navigator.clipboard.writeText(command) @@ -252,21 +269,142 @@ export function ConnectAIModal({ </div> </div> - {/* Step 2: Project Selection or MCP URL */} + {/* Step 2: One-click Install for Cursor, Project Selection for others, or MCP URL */} {selectedClient && ( <div className="space-y-4"> - <div className="flex items-center gap-3"> - <div className="w-8 h-8 rounded-full bg-white/10 text-white/60 flex items-center justify-center text-sm font-medium"> - 2 + <div className="flex justify-between"> + <div className="flex items-center gap-3"> + <div className="w-8 h-8 rounded-full bg-white/10 text-white/60 flex items-center justify-center text-sm font-medium"> + 2 + </div> + <h3 className="text-sm font-medium"> + {selectedClient === "cursor" + ? "Install Supermemory MCP" + : selectedClient === "mcp-url" + ? "MCP Server URL" + : "Select Target Project (Optional)"} + </h3> + </div> + + <div + className={cn( + "flex-col gap-2 hidden", + selectedClient === "cursor" && "flex", + )} + > + {/* Tabs */} + <div className="flex justify-end"> + <div className="flex bg-white/5 rounded-full p-1 border border-white/10"> + <button + className={`px-3 py-1.5 text-xs font-medium rounded-full transition-all ${ + cursorInstallTab === "oneClick" + ? "bg-white/10 text-white border border-white/20" + : "text-white/60 hover:text-white/80" + }`} + onClick={() => setCursorInstallTab("oneClick")} + type="button" + > + One Click Install + </button> + <button + className={`px-3 py-1.5 text-xs font-medium rounded-full transition-all ${ + cursorInstallTab === "manual" + ? "bg-white/10 text-white border border-white/20" + : "text-white/60 hover:text-white/80" + }`} + onClick={() => setCursorInstallTab("manual")} + type="button" + > + Manual Install + </button> + </div> + </div> </div> - <h3 className="text-sm font-medium"> - {selectedClient === "mcp-url" - ? "MCP Server URL" - : "Select Target Project (Optional)"} - </h3> </div> - {selectedClient === "mcp-url" ? ( + {selectedClient === "cursor" ? ( + <div className="space-y-4"> + {/* Tab Content */} + {cursorInstallTab === "oneClick" ? ( + <div className="space-y-4"> + <div className="flex flex-col items-center gap-4 p-6 border border-green-500/20 rounded-lg bg-green-500/5"> + <div className="text-center"> + <p className="text-sm text-white/80 mb-2"> + Click the button below to automatically install and + configure Supermemory in Cursor + </p> + <p className="text-xs text-white/50"> + This will install the MCP server without any + additional setup required + </p> + </div> + <a + href={getCursorDeeplink()} + onClick={() => { + analytics.mcpInstallCmdCopied() + toast.success("Opening Cursor installer...") + }} + > + <img + alt="Add Supermemory MCP server to Cursor" + className="hover:opacity-80 transition-opacity cursor-pointer" + height="40" + src="https://cursor.com/deeplink/mcp-install-dark.svg" + /> + </a> + </div> + <p className="text-xs text-white/40 text-center"> + Make sure you have Cursor installed on your system + </p> + </div> + ) : ( + <div className="space-y-4"> + <p className="text-sm text-white/70"> + Choose a project and follow the installation steps below + </p> + <div className="max-w-md"> + <Select + disabled={isLoadingProjects} + onValueChange={setSelectedProject} + value={selectedProject || "none"} + > + <SelectTrigger className="w-full"> + <SelectValue placeholder="Select project" /> + </SelectTrigger> + <SelectContent className="bg-black/90 backdrop-blur-xl border-white/10"> + <SelectItem + className="text-white hover:bg-white/10" + value="none" + > + Auto-select project + </SelectItem> + <SelectItem + className="text-white hover:bg-white/10" + value="sm_project_default" + > + Default Project + </SelectItem> + {projects + .filter( + (p: Project) => + p.containerTag !== "sm_project_default", + ) + .map((project: Project) => ( + <SelectItem + className="text-white hover:bg-white/10" + key={project.id} + value={project.containerTag} + > + {project.name} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + </div> + )} + </div> + ) : selectedClient === "mcp-url" ? ( <div className="space-y-2"> <div className="relative"> <Input @@ -336,39 +474,47 @@ export function ConnectAIModal({ </div> )} - {/* Step 3: Command Line */} - {selectedClient && selectedClient !== "mcp-url" && ( - <div className="space-y-4"> - <div className="flex items-center gap-3"> - <div className="w-8 h-8 rounded-full bg-white/10 text-white/60 flex items-center justify-center text-sm font-medium"> - 3 + {/* Step 3: Command Line - Show for manual installation or non-cursor clients */} + {selectedClient && + selectedClient !== "mcp-url" && + (selectedClient !== "cursor" || cursorInstallTab === "manual") && ( + <div className="space-y-4"> + <div className="flex items-center gap-3"> + <div className="w-8 h-8 rounded-full bg-white/10 text-white/60 flex items-center justify-center text-sm font-medium"> + 3 + </div> + <h3 className="text-sm font-medium"> + {selectedClient === "cursor" && + cursorInstallTab === "manual" + ? "Manual Installation Command" + : "Installation Command"} + </h3> </div> - <h3 className="text-sm font-medium">Installation Command</h3> - </div> - <div className="relative"> - <Input - className="font-mono text-xs w-full pr-10" - readOnly - value={generateInstallCommand()} - /> - <Button - className="absolute top-[-1px] right-0 cursor-pointer" - onClick={copyToClipboard} - variant="ghost" - > - <CopyIcon className="size-4" /> - </Button> - </div> + <div className="relative"> + <Input + className="font-mono text-xs w-full pr-10" + readOnly + value={generateInstallCommand()} + /> + <Button + className="absolute top-[-1px] right-0 cursor-pointer" + onClick={copyToClipboard} + variant="ghost" + > + <CopyIcon className="size-4" /> + </Button> + </div> - <p className="text-xs text-white/50"> - Copy and run this command in your terminal to install the MCP - server - </p> - </div> - )} + <p className="text-xs text-white/50"> + {selectedClient === "cursor" && cursorInstallTab === "manual" + ? "Copy and run this command in your terminal for manual installation (or switch to the one-click option above)" + : "Copy and run this command in your terminal to install the MCP server"} + </p> + </div> + )} - {/* Blurred Command Placeholder */} + {/* Blurred Command Placeholder - Only show when no client selected */} {!selectedClient && ( <div className="space-y-4"> <div className="flex items-center gap-3"> diff --git a/apps/web/components/views/billing.tsx b/apps/web/components/views/billing.tsx index 033a3915..8b79eb23 100644 --- a/apps/web/components/views/billing.tsx +++ b/apps/web/components/views/billing.tsx @@ -216,7 +216,7 @@ export function BillingView() { <ul className="space-y-2"> <li className="flex items-center gap-2 text-sm text-white/90"> <CheckCircle className="h-4 w-4 text-green-400" /> - 5000 memories + Unlimited memories </li> <li className="flex items-center gap-2 text-sm text-white/90"> <CheckCircle className="h-4 w-4 text-green-400" /> diff --git a/apps/web/components/views/profile.tsx b/apps/web/components/views/profile.tsx index 9fa086ec..3d49d395 100644 --- a/apps/web/components/views/profile.tsx +++ b/apps/web/components/views/profile.tsx @@ -301,7 +301,7 @@ export function ProfileView() { <ul className="space-y-2"> <li className="flex items-center gap-2 text-sm text-white/90"> <CheckCircle className="h-4 w-4 text-green-400" /> - 5000 memories + Unlimited memories </li> <li className="flex items-center gap-2 text-sm text-white/90"> <CheckCircle className="h-4 w-4 text-green-400" /> diff --git a/packages/lib/constants.ts b/packages/lib/constants.ts index fde5bce1..967beb19 100644 --- a/packages/lib/constants.ts +++ b/packages/lib/constants.ts @@ -3,7 +3,7 @@ const DEFAULT_PROJECT_ID = "sm_project_default" const SEARCH_MEMORY_SHORTCUT_URL = "https://www.icloud.com/shortcuts/f2b5c544372844a38ab4c6900e2a88de" const ADD_MEMORY_SHORTCUT_URL = - "https://www.icloud.com/shortcuts/ec33b029b2c7481d89eda7640dbb7688" + "https://www.icloud.com/shortcuts/0fd3e855be444845b457f94c78c2c8d9" export { BIG_DIMENSIONS_NEW, |