This commit is contained in:
Dhravya Shah 2025-09-24 23:26:38 -07:00
commit 697315b9dc
11 changed files with 364 additions and 73 deletions

View file

@ -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

View file

@ -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>
{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>
<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">

View file

@ -99,6 +99,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
*/

View file

@ -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()

View file

@ -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

View file

@ -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/*",

View file

@ -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 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">
{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>
)}
<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>
)}
{/* 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">

View file

@ -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" />

View file

@ -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" />

View file

@ -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,