"use client" import { cn } from "@lib/utils" import { dmSans125ClassName } from "@/lib/fonts" import { authClient } from "@lib/auth" import { useAuth } from "@lib/auth-context" import { hasActivePlan } from "@lib/queries" import { useCustomer } from "autumn-js/react" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { ArrowRight, BookOpen, Check, CheckCircle, Copy, ExternalLink, Loader, Trash2, Zap, } from "lucide-react" import Image from "next/image" import { useMemo, useState } from "react" import { toast } from "sonner" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogPortal, } from "@ui/components/dialog" /** Match `FREE_TIER_PLUGIN_IDS` in mono `packages/lib/plugins.ts`. */ function isFreeTierPlugin(pluginId: string): boolean { return pluginId === "hermes" } interface PluginInfo { id: string name: string description: string features: string[] icon: string docsUrl?: string repoUrl?: string } const PLUGIN_CATALOG: Record = { claude_code: { id: "claude_code", name: "Claude Code", description: "Claude Code remembers your conventions, past decisions, and project context across every session — no re-explaining yourself.", features: [ "Picks up where you left off at session start", "Captures decisions and patterns from tool usage", "Builds a persistent profile of how you work", ], icon: "/images/plugins/claude-code.svg", docsUrl: "https://docs.supermemory.ai/integrations/claude-code", repoUrl: "https://github.com/supermemoryai/claude-supermemory", }, opencode: { id: "opencode", name: "OpenCode", description: "Gives OpenCode persistent memory — your patterns, preferences, and decisions carry forward automatically, session to session.", features: [ "Semantic search across previous sessions", "Auto-capture of coding decisions", "Context injected before each prompt", ], icon: "/images/plugins/opencode.svg", docsUrl: "https://docs.supermemory.ai/integrations/opencode", }, openclaw: { id: "openclaw", name: "OpenClaw", description: "Persists memory across Telegram, WhatsApp, Discord, and Slack. OpenClaw knows who users are and what they talked about before.", features: [ "Cross-channel memory that follows the user", "Automatic conversation capture", "User profiles built across every platform", ], icon: "/images/plugins/openclaw.svg", docsUrl: "https://docs.supermemory.ai/integrations/openclaw", repoUrl: "https://github.com/supermemoryai/openclaw-supermemory", }, hermes: { id: "hermes", name: "Hermes", description: "Hermes never forgets. Conversations, user profiles, and context persist so every session feels like a continuation, not a cold start.", features: [ "Semantic search across previous sessions", "Auto-capture of conversation context", "Persistent user profile built over time", ], icon: "/images/plugins/hermes.svg", docsUrl: "https://docs.supermemory.ai/integrations/hermes", repoUrl: "https://github.com/NousResearch/hermes-agent", }, } interface ConnectedPlugin { id: string keyId: string pluginId: string createdAt: string lastUsed?: string | null keyStart?: string | null } function ProUpgradeNudge({ onUpgrade }: { onUpgrade: () => void }) { return (

Unlock Claude Code, OpenCode, OpenClaw and more with{" "} Pro

) } function ConnectedPluginRow({ plugin, info, onRevoke, }: { plugin: ConnectedPlugin info: PluginInfo | undefined onRevoke: (keyId: string) => void }) { return (
{info && (
{info.name}
)}

{info?.name || plugin.pluginId}

Connected {plugin.keyStart && ( {plugin.keyStart}... )}
) } function PluginCard({ plugin, pluginId, isConnected, isCurrentlyConnecting, connectingPlugin, needsProUpgrade, onConnect, onUpgrade, }: { plugin: PluginInfo pluginId: string isConnected: boolean isCurrentlyConnecting: boolean connectingPlugin: string | null needsProUpgrade: boolean onConnect: (id: string) => void onUpgrade: () => void }) { return (
{plugin.name}
{plugin.name} {isConnected && ( Connected )} {needsProUpgrade && ( PRO )}

{plugin.description}

    {plugin.features.map((feature) => (
  • {feature}
  • ))}
{isConnected ? ( ) : needsProUpgrade ? ( ) : ( )}
{plugin.docsUrl && ( Docs )} {plugin.repoUrl && ( GitHub )}
) } export function PluginsDetail() { const { org } = useAuth() const autumn = useCustomer() const queryClient = useQueryClient() const [connectingPlugin, setConnectingPlugin] = useState(null) const [newKey, setNewKey] = useState<{ open: boolean; key: string }>({ open: false, key: "", }) const [keyCopied, setKeyCopied] = useState(false) const hasProProduct = hasActivePlan(autumn.data?.subscriptions, "api_pro") const { data: pluginsData } = useQuery({ queryFn: async () => { const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" const res = await fetch(`${API_URL}/v3/auth/plugins`, { credentials: "include", }) if (!res.ok) throw new Error("Failed to fetch plugins") return (await res.json()) as { plugins: string[] } }, queryKey: ["plugins"], }) const { data: apiKeys = [], refetch: refetchKeys } = useQuery({ enabled: !!org?.id, queryFn: async () => { if (!org?.id) return [] const data = await authClient.apiKey.list({ fetchOptions: { query: { metadata: { organizationId: org.id } } }, }) return data.filter((key) => key.metadata?.organizationId === org.id) }, queryKey: ["api-keys", org?.id], }) const connectedPlugins = useMemo(() => { const plugins: ConnectedPlugin[] = [] for (const key of apiKeys) { if (!key.metadata) continue try { const metadata = typeof key.metadata === "string" ? (JSON.parse(key.metadata) as { sm_type?: string sm_client?: string }) : (key.metadata as { sm_type?: string; sm_client?: string }) if (metadata.sm_type === "plugin_auth" && metadata.sm_client) { plugins.push({ id: key.id, keyId: key.id, pluginId: metadata.sm_client, createdAt: key.createdAt.toISOString(), lastUsed: key.lastRequest?.toISOString() ?? null, keyStart: key.start ?? null, }) } } catch {} } return plugins }, [apiKeys]) const connectedPluginIds = useMemo( () => connectedPlugins.map((p) => p.pluginId), [connectedPlugins], ) const createPluginKeyMutation = useMutation({ mutationFn: async (pluginId: string) => { const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" const params = new URLSearchParams({ client: pluginId }) const res = await fetch(`${API_URL}/v3/auth/key?${params}`, { credentials: "include", }) if (!res.ok) { if (res.status === 403) { throw new Error( "This plugin requires a Pro plan. Hermes is available on the Free plan.", ) } const errorData = (await res.json().catch(() => ({}))) as { message?: string } throw new Error(errorData.message || "Failed to create plugin key") } return (await res.json()) as { key: string } }, onMutate: (pluginId) => setConnectingPlugin(pluginId), onError: (err) => { toast.error("Failed to connect plugin", { description: err instanceof Error ? err.message : "Unknown error", }) }, onSettled: () => { setConnectingPlugin(null) queryClient.invalidateQueries({ queryKey: ["api-keys", org?.id] }) }, onSuccess: (data) => { setNewKey({ open: true, key: data.key }) toast.success("Plugin connected!") }, }) const handleRevoke = async (keyId: string) => { try { await authClient.apiKey.delete({ keyId }) toast.success("Plugin disconnected") refetchKeys() } catch { toast.error("Failed to disconnect plugin") } } const handleUpgrade = async () => { try { await autumn.attach({ planId: "api_pro", successUrl: "https://app.supermemory.ai/?view=integrations", }) window.location.reload() } catch (error) { console.error(error) } } const handleCopyKey = async () => { try { await navigator.clipboard.writeText(newKey.key) setKeyCopied(true) setTimeout(() => setKeyCopied(false), 2000) toast.success("API key copied!") } catch { toast.error("Failed to copy") } } const isLoading = autumn.isLoading const availablePlugins = pluginsData?.plugins ?? Object.keys(PLUGIN_CATALOG) const allCatalogPluginIds = useMemo( () => availablePlugins.filter((id) => PLUGIN_CATALOG[id]), [availablePlugins], ) return ( <>
{!hasProProduct && !isLoading && ( )} {connectedPlugins.length > 0 && (
Connected {connectedPlugins.map((plugin) => ( ))}
)}
{connectedPlugins.length > 0 ? "Add more plugins" : "Available plugins"}
{allCatalogPluginIds.map((pluginId) => { const plugin = PLUGIN_CATALOG[pluginId] if (!plugin) return null const isConnected = connectedPluginIds.includes(pluginId) const isCurrentlyConnecting = connectingPlugin === pluginId const needsProUpgrade = !isLoading && !hasProProduct && !isFreeTierPlugin(pluginId) return ( createPluginKeyMutation.mutate(id)} onUpgrade={handleUpgrade} /> ) })}
setNewKey({ open, key: open ? newKey.key : "" }) } > Plugin Connected

Save your API key now — you won't be able to see it again.

) }