"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, Brain, Check, CheckCircle, Copy, ExternalLink, Key, 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" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@ui/components/tabs" /** 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: "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", docsUrl: "https://docs.supermemory.ai/integrations/claude-code", repoUrl: "https://github.com/supermemoryai/claude-supermemory", }, opencode: { id: "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", docsUrl: "https://docs.supermemory.ai/integrations/opencode", }, openclaw: { id: "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", docsUrl: "https://docs.supermemory.ai/integrations/openclaw", repoUrl: "https://github.com/supermemoryai/openclaw-supermemory", }, hermes: { id: "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", 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 ProUpgradeBanner({ onUpgrade }: { onUpgrade: () => void }) { return (

Unlock Pro plugins

Connect Claude Code, OpenCode, OpenClaw, Cursor, and more with a Pro plan.

{[ { icon: Brain, title: "Context Retention", desc: "AI remembers your preferences across sessions", }, { icon: Zap, title: "Instant Recall", desc: "Past decisions surface automatically when relevant", }, { icon: Key, title: "Secure & Private", desc: "Your data stays yours with encrypted storage", }, ].map(({ icon: Icon, title, desc }) => (

{title}

{desc}

))}
{Object.values(PLUGIN_CATALOG) .filter((p) => !isFreeTierPlugin(p.id)) .map((plugin) => (
{plugin.name}
))} Claude Code, OpenCode, OpenClaw & more
) } 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 )}

{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.customer?.products, "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 freeConnected = useMemo( () => connectedPlugins.filter((p) => isFreeTierPlugin(p.pluginId)), [connectedPlugins], ) const proConnected = useMemo( () => connectedPlugins.filter((p) => !isFreeTierPlugin(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({ productId: "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 freePluginIds = useMemo(() => { const ids = new Set( availablePlugins.filter( (id) => PLUGIN_CATALOG[id] && isFreeTierPlugin(id), ), ) if (PLUGIN_CATALOG.hermes) ids.add("hermes") return [...ids] }, [availablePlugins]) const proPluginIds = useMemo( () => availablePlugins.filter( (id) => PLUGIN_CATALOG[id] && !isFreeTierPlugin(id), ), [availablePlugins], ) const allCatalogPluginIds = useMemo( () => availablePlugins.filter((id) => PLUGIN_CATALOG[id]), [availablePlugins], ) const showPaidAllInOne = !isLoading && hasProProduct return ( <>
{showPaidAllInOne ? (
{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 return ( createPluginKeyMutation.mutate(id)} onUpgrade={handleUpgrade} /> ) })}
) : ( Free plugins Pro plugins

Included on every plan — connect with no upgrade.

{freeConnected.length > 0 && (
Connected {freeConnected.map((plugin) => ( ))}
)}
{freeConnected.length > 0 ? "Add or manage" : "Available"}
{freePluginIds.map((pluginId) => { const plugin = PLUGIN_CATALOG[pluginId] if (!plugin) return null const isConnected = connectedPluginIds.includes(pluginId) const isCurrentlyConnecting = connectingPlugin === pluginId return ( createPluginKeyMutation.mutate(id)} onUpgrade={handleUpgrade} /> ) })}
{!hasProProduct && !isLoading && ( )} {proConnected.length > 0 && (
Connected {proConnected.map((plugin) => ( ))}
)}
{proConnected.length > 0 ? "Add more" : "Available plugins"}
{proPluginIds.map((pluginId) => { const plugin = PLUGIN_CATALOG[pluginId] if (!plugin) return null const isConnected = connectedPluginIds.includes(pluginId) const isCurrentlyConnecting = connectingPlugin === pluginId const needsProUpgrade = !hasProProduct 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.

) }