"use client" import { useAuth } from "@lib/auth-context" import { useSession } from "@lib/auth" import { cn } from "@lib/utils" import { dmSans125ClassName } from "@/lib/fonts" import { ArrowRight, XCircle } from "lucide-react" import Image from "next/image" import { useRouter, useSearchParams } from "next/navigation" import { Suspense, useEffect, useMemo, useState } from "react" import { PENDING_CONNECT_URL_KEY } from "@/lib/constants" const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" function isValidLocalhostCallback(callback: string): boolean { try { const url = new URL(callback) const isLocalhost = url.hostname === "localhost" || url.hostname === "127.0.0.1" const isHttp = url.protocol === "http:" const isCallbackPath = url.pathname === "/callback" return isLocalhost && isHttp && isCallbackPath } catch { return false } } interface PluginInfo { name: string description: string features: string[] icon: string } const PLUGIN_INFO: Record = { claude_code: { name: "Claude Code", description: "Persistent memory for Claude Code. Remembers your coding context, patterns, and decisions across sessions.", features: [ "Auto-recalls relevant context at session start", "Captures important observations from tool usage", "Builds persistent user profile from interactions", ], icon: "/images/plugins/claude-code.svg", }, opencode: { name: "OpenCode", description: "Memory layer for OpenCode. Enhances your coding assistant with long-term memory capabilities.", features: [ "Semantic search across previous sessions", "Auto-capture of coding decisions", "Context injection before each prompt", ], icon: "/images/plugins/opencode.svg", }, openclaw: { name: "OpenClaw", description: "Multi-platform memory for OpenClaw. Works across Telegram, WhatsApp, Discord, Slack and more.", features: [ "Cross-channel memory persistence", "Automatic conversation capture", "User profile building across platforms", ], icon: "/images/plugins/openclaw.svg", }, hermes: { name: "Hermes", description: "Memory layer for Hermes agent", features: [ "Semantic search across previous sessions", "Auto-capture of conversation context", "Builds persistent user profile from interactions", ], icon: "/images/plugins/hermes.svg", }, cursor: { name: "Cursor", description: "Memory layer for Cursor. Enhances your AI coding assistant with persistent context.", features: [ "Remembers coding patterns across sessions", "Auto-capture of project decisions", "Context-aware suggestions", ], icon: "/images/plugins/cursor.png", }, codex: { name: "OpenAI Codex", description: "Persistent memory for OpenAI Codex CLI. Remembers your coding context, patterns, and decisions across sessions.", features: [ "Auto-recalls relevant context before each prompt", "Captures coding decisions and patterns automatically", "Builds persistent user profile across projects", ], icon: "/images/plugins/codex.png", }, } const MULTI_PLUGIN_FEATURES = [ "Share one persistent memory layer across selected coding agents.", "Recall project context, coding decisions, and prior sessions.", "Connect every selected plugin with one approval.", ] function isKnownPlugin(value: string): boolean { return Object.hasOwn(PLUGIN_INFO, value) } function getPluginName(client: string): string { return PLUGIN_INFO[client]?.name ?? "External Tool" } function formatPluginNames(clients: string[]): string { const names = clients.map((id) => getPluginName(id)) if (names.length === 0) return "External Tool" if (names.length === 1) return names[0] ?? "External Tool" if (names.length === 2) { return `${names[0] ?? "External Tool"} and ${names[1] ?? "External Tool"}` } return `${names.slice(0, -1).join(", ")}, and ${names.at(-1) ?? "External Tool"}` } function encodeBase64UrlJson(value: Record): string { return btoa(JSON.stringify(value)) .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=+$/g, "") } function PluginLogoStack({ clients }: { clients: string[] }) { if (clients.length === 0) { return (
) } return (
{clients.map((id, index) => { const plugin = PLUGIN_INFO[id] return (
{plugin ? ( {plugin.name} ) : ( )}
) })}
) } type Status = "loading" | "creating" | "success" | "error" const pageWrapperClass = "flex items-center justify-center min-h-screen bg-background p-4" const cardClass = cn( "bg-[#14161A] rounded-[14px] p-6 w-full max-w-[400px]", "shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]", ) function AuthConnectContent() { const params = useSearchParams() const router = useRouter() const { data: session, isPending } = useSession() const { org, organizations, isRestoring } = useAuth() const [status, setStatus] = useState("loading") const [error, setError] = useState(null) const callback = params.get("callback") const client = params.get("client") const clientsParam = params.get("clients") const hasClientList = params.has("clients") const rawRequestedClients = useMemo( () => (clientsParam !== null ? clientsParam.split(",") : client ? [client] : []) .map((value) => value.trim()) .filter(Boolean), [client, clientsParam], ) const requestedClients = useMemo( () => Array.from(new Set(rawRequestedClients.filter(isKnownPlugin))), [rawRequestedClients], ) const invalidClients = useMemo( () => rawRequestedClients.filter((value) => !isKnownPlugin(value)), [rawRequestedClients], ) const validClient = requestedClients[0] ?? null const displayName = formatPluginNames(requestedClients) const pluginInfo = requestedClients.length === 1 && validClient ? PLUGIN_INFO[validClient] : null // Redirect new users (logged in but no organization) to onboarding. // Store the current connect URL so onboarding can redirect back here. const shouldRedirectToOnboarding = !isPending && !isRestoring && !!session && Array.isArray(organizations) && organizations.length === 0 useEffect(() => { if (isPending || isRestoring) return if (!session) return if (organizations === null) return // orgs query still pending if (organizations.length > 0) return // has orgs, nothing to do try { sessionStorage.setItem(PENDING_CONNECT_URL_KEY, window.location.href) } catch (e) { console.warn("Failed to access sessionStorage for pending connect URL", e) } router.replace("/onboarding") }, [isPending, isRestoring, session, organizations, router]) async function handleConnect() { if (!callback) { setStatus("error") setError("Missing callback parameter.") return } if (!isValidLocalhostCallback(callback)) { setStatus("error") setError("Invalid callback URL.") return } if (invalidClients.length > 0) { setStatus("error") setError(`Unsupported plugin requested: ${invalidClients.join(", ")}.`) return } if (requestedClients.length === 0) { setStatus("error") setError("Invalid or missing client.") return } if (!session || !org) { setStatus("error") setError( "Your account is not fully set up yet. Please complete onboarding first.", ) return } try { setStatus("creating") const fetchParams = new URLSearchParams({ callback }) fetchParams.set("client", requestedClients[0] ?? "") const res = await fetch(`${API_URL}/v3/auth/key?${fetchParams}`, { credentials: "include", }) if (!res.ok) { const errorData = (await res.json().catch(() => ({}))) as { message?: string } throw new Error(errorData.message || "Failed to get API key") } const data = (await res.json()) as { key: string } setStatus("success") const redirectUrl = new URL(callback) if (hasClientList) { redirectUrl.searchParams.set( "keys", encodeBase64UrlJson( Object.fromEntries( requestedClients.map((requestedClient) => [ requestedClient, data.key, ]), ), ), ) } else { redirectUrl.searchParams.set("apikey", data.key) } redirectUrl.searchParams.set("api_url", API_URL) window.location.href = redirectUrl.toString() } catch (err) { console.error("Failed to get API key:", err) setStatus("error") setError(err instanceof Error ? err.message : "Failed to get API key") } } // Show a spinner while session/org data is loading or while we're about // to redirect to onboarding (prevents a brief flash of the connect card). const isAuthLoading = isPending || isRestoring || organizations === null useEffect(() => { if (status !== "loading") return if (rawRequestedClients.length === 0) { setStatus("error") setError("Invalid or missing client.") return } if (invalidClients.length > 0) { setStatus("error") setError(`Unsupported plugin requested: ${invalidClients.join(", ")}.`) } }, [invalidClients, rawRequestedClients.length, status]) if (isAuthLoading || shouldRedirectToOnboarding) { return (
) } if (status === "loading") { return (

Connect {displayName}

{pluginInfo?.description ?? (requestedClients.length > 1 ? "Use one Supermemory account across these plugins." : `Use your Supermemory account with ${displayName}.`)}

    {(pluginInfo?.features ?? MULTI_PLUGIN_FEATURES).map( (feature) => (
  • {feature}
  • ), )}
) } if (status === "error") { return (

Connection failed

{error}

Go to app
) } return (

{status === "creating" && `Connecting ${displayName}…`} {status === "success" && `Success! Redirecting back to ${displayName}…`}

) } export default function AuthConnectPage() { return (
} >
) }