diff --git a/apps/web/app/(app)/connect/page.tsx b/apps/web/app/(app)/connect/page.tsx
new file mode 100644
index 00000000..ff969250
--- /dev/null
+++ b/apps/web/app/(app)/connect/page.tsx
@@ -0,0 +1,250 @@
+"use client"
+
+import { dmSans125ClassName } from "@/lib/fonts"
+import { OAUTH_PLUGINS } from "@/lib/oauth-plugins"
+import { cn } from "@lib/utils"
+import { Building2, ExternalLink, LoaderIcon, Plug, Trash2 } from "lucide-react"
+import Image from "next/image"
+import { useCallback, useEffect, useState } from "react"
+
+const API_URL =
+ process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"
+
+interface Connection {
+ clientId: string
+ name: string
+ icon: string | null
+ isFirstParty: boolean
+ workspaceId: string | null
+ workspaceName: string | null
+ scopes: string[]
+ connectedAt: string | null
+ lastUsedAt: string | null
+}
+
+function relativeTime(iso: string | null): string | null {
+ if (!iso) return null
+ const then = new Date(iso).getTime()
+ if (Number.isNaN(then)) return null
+ const diff = Date.now() - then
+ const mins = Math.round(diff / 60000)
+ if (mins < 1) return "just now"
+ if (mins < 60) return `${mins}m ago`
+ const hrs = Math.round(mins / 60)
+ if (hrs < 24) return `${hrs}h ago`
+ const days = Math.round(hrs / 24)
+ if (days < 30) return `${days}d ago`
+ const months = Math.round(days / 30)
+ if (months < 12) return `${months}mo ago`
+ return `${Math.round(months / 12)}y ago`
+}
+
+const cardClass = cn(
+ "rounded-[14px] bg-[#14161A] p-5",
+ "shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]",
+)
+
+function PluginIcon({ src, alt }: { src: string | null; alt: string }) {
+ const [failed, setFailed] = useState(false)
+ if (!src || failed) {
+ return (
+
+ )
+ }
+ return (
+
+ setFailed(true)}
+ src={src}
+ width={20}
+ />
+
+ )
+}
+
+export default function ConnectPage() {
+ const [connections, setConnections] = useState(null)
+ const [error, setError] = useState(null)
+ const [revoking, setRevoking] = useState(null)
+
+ const load = useCallback(async () => {
+ try {
+ const res = await fetch(`${API_URL}/v3/oauth/grants`, {
+ credentials: "include",
+ })
+ if (!res.ok) throw new Error(`Failed to load connections (${res.status})`)
+ const data = (await res.json()) as { grants: Connection[] }
+ setConnections(data.grants)
+ setError(null)
+ } catch (err) {
+ console.error("Failed to load connections:", err)
+ setError(
+ err instanceof Error ? err.message : "Failed to load connections",
+ )
+ setConnections([])
+ }
+ }, [])
+
+ useEffect(() => {
+ load()
+ }, [load])
+
+ async function revoke(clientId: string) {
+ setRevoking(clientId)
+ try {
+ const res = await fetch(
+ `${API_URL}/v3/oauth/grants/${encodeURIComponent(clientId)}`,
+ { method: "DELETE", credentials: "include" },
+ )
+ if (!res.ok && res.status !== 204)
+ throw new Error(`Failed to revoke (${res.status})`)
+ setConnections((prev) =>
+ prev ? prev.filter((c) => c.clientId !== clientId) : prev,
+ )
+ } catch (err) {
+ console.error("Failed to revoke connection:", err)
+ setError(err instanceof Error ? err.message : "Failed to revoke")
+ } finally {
+ setRevoking(null)
+ }
+ }
+
+ const connectedClientIds = new Set(connections?.map((c) => c.clientId) ?? [])
+
+ return (
+
+
Connections
+
+ Apps and plugins you've connected to your Supermemory account.
+
+
+
+
+ Connected apps
+
+
+ {connections === null ? (
+
+
+ Loading…
+
+ ) : connections.length === 0 ? (
+
+
No apps connected yet
+
+ Connect a plugin below — anything you authorize will show up here.
+
+
+ ) : (
+
+ {connections.map((c) => {
+ const connectedRel = relativeTime(c.connectedAt)
+ const usedRel = relativeTime(c.lastUsedAt)
+ return (
+
+
+
+
+
+ {c.name}
+
+ {!c.isFirstParty && (
+
+ external
+
+ )}
+
+
+ {c.workspaceName && (
+
+
+ {c.workspaceName}
+
+ )}
+ {connectedRel && Connected {connectedRel}}
+ {usedRel && Last used {usedRel}}
+
+
+
+
+ )
+ })}
+
+ )}
+
+ {error && {error}
}
+
+
+
+
+ Available plugins
+
+
+ {OAUTH_PLUGINS.map((p) => {
+ const isConnected =
+ p.oauthClientId != null && connectedClientIds.has(p.oauthClientId)
+ return (
+
+
+
+
+ {p.name}
+
+ {isConnected && (
+
+ Connected
+
+ )}
+
+
+ {p.description}
+
+
+ Setup guide
+
+
+
+ )
+ })}
+
+
+
+ )
+}
diff --git a/apps/web/app/(auth)/login/page.tsx b/apps/web/app/(auth)/login/page.tsx
index dbdacdfc..e994d133 100644
--- a/apps/web/app/(auth)/login/page.tsx
+++ b/apps/web/app/(auth)/login/page.tsx
@@ -33,7 +33,7 @@ function buildMcpAuthorizeResumeUrl(
const p = new URLSearchParams(sp.toString())
p.delete("redirect")
p.delete("error")
- return `${backend}/api/auth/mcp/authorize?${p.toString()}`
+ return `${backend}/api/auth/oauth2/authorize?${p.toString()}`
}
function LoginHeadline({ className }: { className?: string }) {
diff --git a/apps/web/app/oauth/consent/ConsentCard.tsx b/apps/web/app/oauth/consent/ConsentCard.tsx
new file mode 100644
index 00000000..4ce7d1d5
--- /dev/null
+++ b/apps/web/app/oauth/consent/ConsentCard.tsx
@@ -0,0 +1,703 @@
+"use client"
+
+import { dmSans125ClassName } from "@/lib/fonts"
+import { cn } from "@lib/utils"
+import { ClaudeDesktopIcon, MCPIcon } from "@ui/assets/icons"
+import { Logo, LogoFull } from "@ui/assets/Logo"
+import { Popover, PopoverAnchor, PopoverContent } from "@ui/components/popover"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@ui/components/select"
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@ui/components/tooltip"
+import { ArrowLeft, BadgeCheck, Check, LoaderIcon, X } from "lucide-react"
+import { AnimatePresence, motion } from "motion/react"
+import {
+ type ComponentType,
+ type ReactNode,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from "react"
+
+type IconComponent = ComponentType<{ className?: string }>
+
+// Mirror of the OAuth plugins in mono's packages/lib/plugins.ts. An entry here
+// makes a client "verified" — we show its bundled icon + real name. Unknown DCR
+// clients still connect; they just render as a generic MCP client.
+export const OAUTH_PLUGINS: Record<
+ string,
+ { name: string; icon?: IconComponent }
+> = {
+ "supermemory-claude-code": { name: "Claude Code", icon: ClaudeDesktopIcon },
+ "supermemory-opencode": { name: "OpenCode" },
+ "supermemory-openclaw": { name: "OpenClaw" },
+ "supermemory-codex": { name: "OpenAI Codex" },
+}
+
+const GRADIENT_BG =
+ "linear-gradient(182.37deg, #0ff0d2 -91.53%, #5bd3fb -67.8%, #1e0ff0 95.17%)"
+const GRADIENT_SHADOW =
+ "1px 1px 2px 0px #1A88FF inset, 0 2px 10px 0 rgba(5, 1, 0, 0.20)"
+
+const EXPIRY_OPTIONS = [
+ { label: "1 year", value: "365" },
+ { label: "6 months", value: "180" },
+ { label: "90 days", value: "90" },
+ { label: "30 days", value: "30" },
+ { label: "7 days", value: "7" },
+ { label: "Never", value: "0" },
+]
+
+export function shortClientId(id: string): string {
+ return id.length > 12 ? `${id.slice(0, 4)}…${id.slice(-4)}` : id
+}
+
+export type ConsentOrg = {
+ id: string
+ name: string
+}
+export type ConsentPermission = "read" | "write"
+export type ConsentScopeType = "full" | "scoped"
+export type ConsentScope = {
+ permission: ConsentPermission
+ scopeType: ConsentScopeType
+ tags: string[]
+ expiresDays: number
+}
+
+function SectionLabel({ children }: { children: ReactNode }) {
+ return (
+
+ {children}
+
+ )
+}
+
+function ConnectingDots() {
+ return (
+
+ {["a", "b", "c"].map((k, i) => (
+
+ ))}
+
+ )
+}
+
+function ConnectingHeader({
+ clientIcon: ClientIcon,
+}: {
+ clientIcon: IconComponent
+}) {
+ return (
+
+ )
+}
+
+function SpacesPicker({
+ options,
+ selected,
+ setSelected,
+ loading,
+ disabled,
+}: {
+ options: string[]
+ selected: string[]
+ setSelected: (next: string[]) => void
+ loading: boolean
+ disabled?: boolean
+}) {
+ const [query, setQuery] = useState("")
+ const [open, setOpen] = useState(false)
+ const fieldRef = useRef(null)
+ const filtered = useMemo(() => {
+ const q = query.trim().toLowerCase()
+ return options
+ .filter(
+ (o) => !selected.includes(o) && (!q || o.toLowerCase().includes(q)),
+ )
+ .slice(0, 50)
+ }, [options, selected, query])
+
+ const add = (t: string) => {
+ if (!selected.includes(t)) setSelected([...selected, t])
+ setQuery("")
+ }
+ const remove = (t: string) => setSelected(selected.filter((x) => x !== t))
+
+ return (
+
+
+
+ {selected.length > 0 && (
+
+ {selected.map((t) => (
+
+ {t}
+
+
+ ))}
+
+ )}
+
setQuery(e.target.value)}
+ onFocus={() => setOpen(true)}
+ placeholder={loading ? "Loading spaces…" : "Search spaces to add…"}
+ value={query}
+ />
+
+
+ {
+ if (fieldRef.current?.contains(e.target as Node)) e.preventDefault()
+ }}
+ onInteractOutside={(e) => {
+ if (fieldRef.current?.contains(e.target as Node)) e.preventDefault()
+ }}
+ onOpenAutoFocus={(e) => e.preventDefault()}
+ sideOffset={6}
+ >
+ {filtered.length === 0 ? (
+
+ {loading
+ ? "Loading spaces…"
+ : query.trim()
+ ? `No spaces match “${query.trim()}”.`
+ : "No spaces available."}
+
+ ) : (
+ filtered.map((t) => (
+
+ ))
+ )}
+
+
+ )
+}
+
+export function CardShell({ children }: { children: ReactNode }) {
+ return (
+
+ )
+}
+
+export function FullScreenMessage({
+ title,
+ subtitle,
+}: {
+ title: string
+ subtitle: string
+}) {
+ return (
+
+
+
+
+ {title}
+
+
{subtitle}
+
+
+ )
+}
+
+export interface ConsentCardProps {
+ appLabel: string
+ verified: boolean
+ clientId: string
+ userEmail?: string
+ orgs: ConsentOrg[]
+ availableTags: string[]
+ tagsLoading: boolean
+ submitting: "approve" | "deny" | null
+ error: string | null
+ onEnterOrg: (orgId: string) => Promise
+ onScopedOpen: () => void
+ onSubmit: (accept: boolean, scope: ConsentScope) => void
+ onSignOut: () => void
+}
+
+export function ConsentCard({
+ appLabel,
+ verified,
+ clientId,
+ userEmail,
+ orgs,
+ availableTags,
+ tagsLoading,
+ submitting,
+ error,
+ onEnterOrg,
+ onScopedOpen,
+ onSubmit,
+ onSignOut,
+}: ConsentCardProps) {
+ const [step, setStep] = useState<1 | 2>(1)
+ const [selectedOrgId, setSelectedOrgId] = useState(null)
+ const [switchingOrgId, setSwitchingOrgId] = useState(null)
+ const [autoTried, setAutoTried] = useState(false)
+ const [permission, setPermission] = useState("write")
+ const [scopeType, setScopeType] = useState("full")
+ const [tags, setTags] = useState([])
+ const [expiresDays, setExpiresDays] = useState("365")
+
+ const multiOrg = orgs.length > 1
+ const busy = submitting !== null || switchingOrgId !== null
+ const selectedOrg = orgs.find((o) => o.id === selectedOrgId) ?? null
+ const ClientIcon = (clientId && OAUTH_PLUGINS[clientId]?.icon) || MCPIcon
+ const title = verified ? `Authorize ${appLabel}` : "Authorize MCP"
+ const connectLabel = verified ? appLabel : "this MCP client"
+
+ const enterOrg = useCallback(
+ async (orgId: string) => {
+ if (!orgId) return
+ setSelectedOrgId(orgId)
+ setSwitchingOrgId(orgId)
+ try {
+ await onEnterOrg(orgId)
+ } catch {
+ setSwitchingOrgId(null)
+ return
+ }
+ setSwitchingOrgId(null)
+ setTags([])
+ setStep(2)
+ },
+ [onEnterOrg],
+ )
+
+ useEffect(() => {
+ if (autoTried || step !== 1 || orgs.length !== 1) return
+ setAutoTried(true)
+ void enterOrg(orgs[0]?.id ?? "")
+ }, [orgs, step, autoTried, enterOrg])
+
+ useEffect(() => {
+ if (step === 2 && scopeType === "scoped") onScopedOpen()
+ }, [step, scopeType, onScopedOpen])
+
+ const listRef = useRef(null)
+ const [canScrollUp, setCanScrollUp] = useState(false)
+ const [canScrollDown, setCanScrollDown] = useState(false)
+ const measureFades = useCallback((el: HTMLDivElement | null) => {
+ if (!el) return
+ setCanScrollUp(el.scrollTop > 8)
+ setCanScrollDown(el.scrollTop + el.clientHeight < el.scrollHeight - 8)
+ }, [])
+ useEffect(() => {
+ if (step !== 1 || orgs.length === 0) {
+ setCanScrollUp(false)
+ setCanScrollDown(false)
+ return
+ }
+ measureFades(listRef.current)
+ }, [orgs, step, measureFades])
+
+ const submit = (accept: boolean) =>
+ onSubmit(accept, {
+ permission,
+ scopeType,
+ tags,
+ expiresDays: Number(expiresDays),
+ })
+
+ return (
+
+
+
+
+ {step === 1 ? (
+
+
+
+
+ Select an organization
+
+
+ Choose which organization to connect {connectLabel} to.
+
+
+
+
+
measureFades(e.currentTarget)}
+ ref={listRef}
+ >
+ {orgs.length === 0 ? (
+
+ No organizations found on your account.
+
+ ) : (
+ orgs.map((o) => {
+ const switching = switchingOrgId === o.id
+ return (
+
+ )
+ })
+ )}
+
+
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+ {userEmail && (
+
+ Signed in as {userEmail}
+
+ )}
+
+
+
+ ) : (
+
+
+
+
+
+ {title}
+
+ {verified && (
+
+
+
+
+
+
+
+ Verified app
+
+
+ )}
+
+
+ {verified ? appLabel : "An MCP client"} wants to connect to
+ your supermemory.
+
+
+
+
+
+
+
+
Connecting to
+
+ {selectedOrg?.name ?? "Workspace"}
+
+
+ {multiOrg && (
+
+ )}
+
+
+
+
+
+ Permission
+
+
+
+
+
Access
+
+
+ {scopeType === "scoped" && (
+
+
+
+
+ {tags.length > 0
+ ? `${tags.length} space${tags.length === 1 ? "" : "s"} selected`
+ : "Type to search and add spaces."}
+
+
+
+ )}
+
+
+
+
+ Expires
+
+
+
+ {error &&
{error}
}
+
+
+
+
+
+
+
+
+
+ {clientId && !verified && (
+
+ App ID · {shortClientId(clientId)}
+
+ )}
+
+ )}
+
+
+
+ )
+}
+
+function Choice({
+ options,
+ value,
+ onChange,
+ disabled,
+}: {
+ options: { value: T; label: string }[]
+ value: T
+ onChange: (v: T) => void
+ disabled?: boolean
+}) {
+ return (
+
+ {options.map((o) => {
+ const active = o.value === value
+ return (
+
+ )
+ })}
+
+ )
+}
diff --git a/apps/web/app/oauth/consent/page.tsx b/apps/web/app/oauth/consent/page.tsx
new file mode 100644
index 00000000..209a69f4
--- /dev/null
+++ b/apps/web/app/oauth/consent/page.tsx
@@ -0,0 +1,217 @@
+"use client"
+
+import { authClient, useSession } from "@lib/auth"
+import { useSearchParams } from "next/navigation"
+import { Suspense, useCallback, useMemo, useState } from "react"
+import {
+ CardShell,
+ ConsentCard,
+ type ConsentScope,
+ FullScreenMessage,
+ OAUTH_PLUGINS,
+} from "./ConsentCard"
+
+const API_URL =
+ process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"
+
+function OAuthConsentContent() {
+ const params = useSearchParams()
+ const { data: session } = useSession()
+ const { data: organizations } = authClient.useListOrganizations()
+
+ const [submitting, setSubmitting] = useState<"approve" | "deny" | null>(null)
+ const [done, setDone] = useState<"approved" | "denied" | null>(null)
+ const [error, setError] = useState(null)
+ const [availableTags, setAvailableTags] = useState([])
+ const [tagsLoading, setTagsLoading] = useState(false)
+
+ const orgs = useMemo(
+ () => (organizations ?? []).map((o) => ({ id: o.id, name: o.name })),
+ [organizations],
+ )
+ const activeOrgId = session?.session.activeOrganizationId ?? null
+ const clientId = params.get("client_id") ?? ""
+ const plugin = clientId ? (OAUTH_PLUGINS[clientId] ?? null) : null
+ const appLabel = plugin?.name ?? "An application"
+
+ // A valid consent page is reached only via /oauth2/authorize, which appends a
+ // signed (`sig`) + short-lived (`exp`) query. Without that it can't succeed.
+ const expSeconds = Number(params.get("exp"))
+ const requestExpired = expSeconds > 0 && expSeconds * 1000 < Date.now()
+ const invalidRequest = !params.get("sig") || requestExpired
+
+ const onEnterOrg = useCallback(
+ async (orgId: string) => {
+ setError(null)
+ if (orgId !== activeOrgId) {
+ try {
+ await authClient.organization.setActive({ organizationId: orgId })
+ } catch (err) {
+ setError("Couldn't switch to that organization. Try again.")
+ throw err
+ }
+ }
+ setAvailableTags([])
+ },
+ [activeOrgId],
+ )
+
+ const onScopedOpen = useCallback(() => {
+ if (tagsLoading || availableTags.length > 0) return
+ setTagsLoading(true)
+ fetch(`${API_URL}/v3/container-tags/list`, { credentials: "include" })
+ .then((r) => (r.ok ? r.json() : null))
+ .then((d) => {
+ const list = (d?.containerTags ?? d?.tags ?? d ?? []) as unknown[]
+ const names = (Array.isArray(list) ? list : [])
+ .map((t) =>
+ typeof t === "string"
+ ? t
+ : ((t as { containerTag?: string; tag?: string })?.containerTag ??
+ (t as { tag?: string })?.tag ??
+ null),
+ )
+ .filter((t): t is string => typeof t === "string" && t.length > 0)
+ setAvailableTags(Array.from(new Set(names)))
+ })
+ .catch(() => {})
+ .finally(() => setTagsLoading(false))
+ }, [tagsLoading, availableTags.length])
+
+ const onSubmit = useCallback(
+ async (accept: boolean, scope: ConsentScope) => {
+ // Send the raw, unmodified query string — better-auth re-verifies its HMAC,
+ // so it must be byte-for-byte what we were redirected with.
+ const oauthQuery = window.location.search.replace(/^\?/, "")
+ if (!oauthQuery) {
+ setError(
+ "Missing authorization request. Start the flow again from your app.",
+ )
+ return
+ }
+ setSubmitting(accept ? "approve" : "deny")
+ setError(null)
+ try {
+ if (accept && clientId) {
+ await fetch(`${API_URL}/v3/mcp/connect-scope`, {
+ method: "POST",
+ credentials: "include",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ clientId,
+ permission: scope.permission,
+ containerTags: scope.scopeType === "scoped" ? scope.tags : [],
+ expiresDays: scope.expiresDays,
+ }),
+ }).catch(() => {})
+ }
+ const res = await fetch(`${API_URL}/api/auth/oauth2/consent`, {
+ method: "POST",
+ credentials: "include",
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ body: JSON.stringify({ accept, oauth_query: oauthQuery }),
+ })
+ const data = (await res.json().catch(() => ({}))) as {
+ url?: string
+ redirectURI?: string
+ redirect_uri?: string
+ message?: string
+ error?: string
+ error_description?: string
+ }
+ if (!res.ok) {
+ if (
+ data.error === "invalid_signature" ||
+ data.error === "invalid_request"
+ ) {
+ throw new Error(
+ "This authorization request has expired. Start the connection again from your app.",
+ )
+ }
+ throw new Error(
+ data.error_description ||
+ data.message ||
+ data.error ||
+ "Authorization failed.",
+ )
+ }
+ // Show the final state regardless: many clients use a loopback or custom
+ // scheme redirect_uri that hands off without replacing this tab.
+ setDone(accept ? "approved" : "denied")
+ const redirectUrl = data.url ?? data.redirectURI ?? data.redirect_uri
+ if (redirectUrl) window.location.href = redirectUrl
+ } catch (err) {
+ console.error("OAuth consent failed:", err)
+ setError(err instanceof Error ? err.message : "Authorization failed.")
+ setSubmitting(null)
+ }
+ },
+ [clientId],
+ )
+
+ const onSignOut = useCallback(async () => {
+ try {
+ await authClient.signOut()
+ } catch {}
+ window.location.href = "/login"
+ }, [])
+
+ if (invalidRequest && !done) {
+ return (
+
+ )
+ }
+
+ if (done) {
+ return (
+
+ )
+ }
+
+ return (
+
+ )
+}
+
+export default function OAuthConsentPage() {
+ return (
+
+
+
+ }
+ >
+
+
+ )
+}
diff --git a/apps/web/lib/oauth-plugins.ts b/apps/web/lib/oauth-plugins.ts
new file mode 100644
index 00000000..2cc4e1da
--- /dev/null
+++ b/apps/web/lib/oauth-plugins.ts
@@ -0,0 +1,57 @@
+// OAuth-connectable plugins, mirroring the `authMethod: "oauth"` entries in mono's packages/lib/plugins.ts.
+// `oauthClientId` is the stable first-party client id (omitted for Cursor — it self-registers via DCR).
+export interface OAuthPluginInfo {
+ id: string
+ oauthClientId?: string
+ name: string
+ description: string
+ icon: string
+ docsUrl: string
+}
+
+export const OAUTH_PLUGINS: OAuthPluginInfo[] = [
+ {
+ id: "claude_code",
+ oauthClientId: "supermemory-claude-code",
+ name: "Claude Code",
+ description:
+ "Persistent memory for Claude Code — recalls your coding context, patterns and decisions across sessions.",
+ icon: "/images/plugins/claude-code.svg",
+ docsUrl: "https://supermemory.ai/docs/integrations/claude-code",
+ },
+ {
+ id: "opencode",
+ oauthClientId: "supermemory-opencode",
+ name: "OpenCode",
+ description:
+ "Memory layer for OpenCode — semantic search across sessions and automatic context injection.",
+ icon: "/images/plugins/opencode.svg",
+ docsUrl: "https://supermemory.ai/docs/integrations/opencode",
+ },
+ {
+ id: "openclaw",
+ oauthClientId: "supermemory-openclaw",
+ name: "OpenClaw",
+ description:
+ "Multi-platform memory for OpenClaw — persistence across Telegram, WhatsApp, Discord, Slack and more.",
+ icon: "/images/plugins/openclaw.svg",
+ docsUrl: "https://supermemory.ai/docs/integrations/openclaw",
+ },
+ {
+ id: "codex",
+ oauthClientId: "supermemory-codex",
+ name: "OpenAI Codex",
+ description:
+ "Persistent memory for the OpenAI Codex CLI — recalls coding context and decisions across projects.",
+ icon: "/images/plugins/codex.png",
+ docsUrl: "https://supermemory.ai/docs/integrations/codex",
+ },
+ {
+ id: "cursor",
+ name: "Cursor",
+ description:
+ "Persistent AI memory for Cursor via the Supermemory MCP server. Connect from Cursor's MCP setup.",
+ icon: "/images/plugins/cursor.png",
+ docsUrl: "https://supermemory.ai/docs/supermemory-mcp/setup",
+ },
+]