Refine Codex OAuth and integration status

This commit is contained in:
Ishaan Gupta 2026-06-04 18:19:06 +05:30
parent f5383b6671
commit c7d191e787
9 changed files with 363 additions and 214 deletions

View file

@ -0,0 +1 @@
export { default } from "../connect/page"

View file

@ -99,7 +99,7 @@ const PLUGIN_INFO: Record<string, PluginInfo> = {
"Captures coding decisions and patterns automatically",
"Builds persistent user profile across projects",
],
icon: "/images/plugins/codex.svg",
icon: "/images/plugins/codex.png",
},
}
@ -199,6 +199,7 @@ function AuthConnectContent() {
const redirectUrl = new URL(callback)
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)

View file

@ -28,15 +28,7 @@ export function PluginPreview({ parsed }: { parsed: ParsedPluginDocument }) {
)}
{parsed.pluginLabel}
</span>
<p className="text-[11px] text-[#737373] truncate">
{parsed.formatLabel}
</p>
</div>
{parsed.identifierValue && (
<p className="text-[10px] text-[#737373] truncate max-w-[45%]">
{parsed.identifierValue}
</p>
)}
</div>
<div className="space-y-[6px]">
<p

View file

@ -47,24 +47,6 @@ function PluginHeader({ parsed }: { parsed: ParsedPluginDocument }) {
>
{parsed.pluginLabel}
</span>
<span
className={cn(
dmSansClassName(),
"inline-flex items-center rounded-full border border-[#1F242C] bg-[#0F1318] px-2.5 py-1 text-[11px] font-medium text-[#B7BDC7]",
)}
>
{parsed.formatLabel}
</span>
{parsed.identifierLabel && parsed.identifierValue && (
<span
className={cn(
dmSansClassName(),
"inline-flex items-center rounded-full border border-[#1F242C] bg-[#0F1318] px-2.5 py-1 text-[11px] font-medium text-[#8E97A3]",
)}
>
{parsed.identifierLabel}: {parsed.identifierValue}
</span>
)}
</div>
<p
className={cn(

View file

@ -27,7 +27,6 @@ import type { UseMutationResult } from "@tanstack/react-query"
import { toast } from "sonner"
import { useIsMobile } from "@hooks/use-mobile"
import { parsePluginDocument } from "@/lib/plugin-document"
import { PluginDetails } from "./plugin-details"
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
type DocumentWithMemories = DocumentsResponse["documents"][0]
@ -270,13 +269,8 @@ export function DocumentModal({
],
)
const hasPluginInsights =
pluginDocument &&
pluginDocument.kind !== "claude-code-doc" &&
pluginDocument.kind !== "openclaw-session"
const hasDocumentInsights = Boolean(
hasPluginInsights ||
_document?.summary ||
_document?.summary ||
pluginDocument?.summary ||
(_document?.memoryEntries && _document.memoryEntries.length > 0),
)
@ -303,7 +297,6 @@ export function DocumentModal({
dmSansClassName(),
)}
>
{hasPluginInsights && <PluginDetails parsed={pluginDocument} />}
{_document && (_document.summary || pluginDocument?.summary) && (
<DocumentSummary
memoryEntries={_document.memoryEntries}

View file

@ -1,83 +0,0 @@
import Image from "next/image"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/lib/fonts"
import type { ParsedPluginDocument } from "@/lib/plugin-document"
function DetailPill({ label, value }: { label: string; value: string }) {
return (
<div className="rounded-[12px] border border-[#1E232B] bg-[#0F1318] px-3 py-2">
<p
className={cn(
dmSansClassName(),
"text-[10px] uppercase tracking-[0.08em] text-[#737373]",
)}
>
{label}
</p>
<p className="mt-1 text-[13px] text-[#F3F4F6] break-all">{value}</p>
</div>
)
}
export function PluginDetails({ parsed }: { parsed: ParsedPluginDocument }) {
return (
<div className="bg-[#14161A] p-3 rounded-[14px] space-y-3 shadow-[inset_0_2px_4px_rgba(0,0,0,0.3),inset_0_1px_2px_rgba(0,0,0,0.1)]">
<div className="flex items-center justify-between gap-2">
<p className="text-[16px] font-semibold text-[#FAFAFA] leading-[125%]">
Details
</p>
<span
className={cn(
dmSansClassName(),
"inline-flex items-center gap-1.5 text-[12px] font-medium text-[#FAFAFA]",
)}
>
{parsed.pluginIconSrc && (
<Image
src={parsed.pluginIconSrc}
alt=""
width={16}
height={16}
className="rounded-[3px]"
aria-hidden
/>
)}
{parsed.pluginLabel}
</span>
</div>
<div className="grid grid-cols-1 gap-2">
<DetailPill label="Format" value={parsed.formatLabel} />
{parsed.identifierLabel && parsed.identifierValue && (
<DetailPill
label={parsed.identifierLabel}
value={parsed.identifierValue}
/>
)}
{parsed.clientLabel && parsed.clientValue && (
<DetailPill label={parsed.clientLabel} value={parsed.clientValue} />
)}
</div>
{parsed.artifacts.length > 0 && (
<div className="space-y-2">
<p
className={cn(
dmSansClassName(),
"text-[11px] font-medium uppercase tracking-[0.08em] text-[#737373]",
)}
>
Outputs
</p>
<div className="space-y-2">
{parsed.artifacts.map((artifact, index) => (
<DetailPill
key={`${artifact.label}-${artifact.value}-${index}`}
label={artifact.label}
value={artifact.value}
/>
))}
</div>
</div>
)}
</div>
)
}

View file

@ -23,7 +23,6 @@ import {
ArrowRight,
BookOpen,
Check,
ChevronDown,
Loader,
Search,
X,
@ -46,11 +45,11 @@ import {
import { AnimatePresence, motion } from "motion/react"
import { toast } from "sonner"
import { Dialog, DialogContent, DialogTitle } from "@ui/components/dialog"
import { Popover, PopoverContent, PopoverTrigger } from "@ui/components/popover"
import {
PLUGIN_CATALOG,
FREE_TIER_PLUGIN_IDS,
isFreeTierPlugin,
normalizePluginClientId,
type InstallStep,
} from "@/lib/plugin-catalog"
import { INSET, InstallSteps, PillButton } from "./integrations/install-steps"
@ -66,6 +65,16 @@ interface ConnectedKey {
pluginId: string
}
type ListedApiKey = {
id: string
name?: string | null
createdAt?: string
enabled?: boolean
lastRequest?: string | null
metadata: string | Record<string, unknown> | null
start?: string | null
}
type ItemKind = "plugin" | "connector" | "client" | "mcp-client" | "import"
type MCPClientKey =
@ -487,56 +496,20 @@ function DisconnectButton({ onConfirm }: { onConfirm: () => void }) {
)
}
function ConnectedPill({
keys,
onRevoke,
}: {
keys: ConnectedKey[]
onRevoke: (keyId: string) => void
}) {
function ConnectedButton({ onClick }: { onClick: () => void }) {
return (
<Popover>
<PopoverTrigger asChild>
<button
type="button"
onClick={(e) => e.stopPropagation()}
className={cn(
dmSans125ClassName(),
"flex h-8 min-w-[104px] shrink-0 cursor-pointer items-center justify-center gap-1.5 rounded-full bg-[#0D121A] px-3 text-[12px] font-medium text-[#00AC3F] sm:h-9 sm:min-w-[116px] sm:gap-2 sm:px-4 sm:text-[13px]",
"shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.7)] transition-opacity hover:opacity-80",
)}
>
<span className="size-[7px] rounded-full bg-[#00AC3F]" />
Connected
<ChevronDown className="size-3 text-[#737373]" />
</button>
</PopoverTrigger>
<PopoverContent
align="end"
onClick={(e) => e.stopPropagation()}
className={cn(
dmSans125ClassName(),
"w-[260px] rounded-xl border border-white/10 bg-[#1B1F24] p-2 text-[#FAFAFA]",
)}
>
<p className="px-2 pb-1.5 pt-1 text-[11px] font-medium uppercase tracking-wide text-[#737373]">
{keys.length > 1 ? `${keys.length} connections` : "Connection"}
</p>
<div className="flex flex-col">
{keys.map((k) => (
<div
key={k.keyId}
className="flex items-center justify-between gap-2 rounded-lg px-2 py-1.5"
>
<span className="min-w-0 flex-1 truncate font-mono text-[12px] text-[#A1A1AA]">
{k.keyStart ? `${k.keyStart}` : "API key"}
</span>
<DisconnectButton onConfirm={() => onRevoke(k.keyId)} />
</div>
))}
</div>
</PopoverContent>
</Popover>
<button
type="button"
onClick={onClick}
className={cn(
dmSans125ClassName(),
"flex h-8 min-w-[104px] shrink-0 cursor-pointer items-center justify-center gap-1.5 rounded-full bg-[#0D121A] px-3 text-[12px] font-medium text-[#00AC3F] sm:h-9 sm:min-w-[116px] sm:gap-2 sm:px-4 sm:text-[13px]",
"shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.7)] transition-opacity hover:opacity-80",
)}
>
<span className="size-[7px] rounded-full bg-[#00AC3F]" />
Connected
</button>
)
}
@ -1002,6 +975,9 @@ export function IntegrationsView() {
key: string
pluginId: string | null
}>({ open: false, key: "", pluginId: null })
const [connectedPluginId, setConnectedPluginId] = useState<string | null>(
null,
)
const { data: pluginsData } = useQuery({
queryFn: async () => {
@ -1030,27 +1006,34 @@ export function IntegrationsView() {
enabled: hasProProduct,
})
type ApiKey = {
id: string
metadata: Record<string, unknown> | null
start: string | null
}
const { data: apiKeys = [], refetch: refetchKeys } = useQuery({
queryKey: ["api-keys", org?.id],
queryFn: async () => {
if (!org?.id) return []
const data = (await authClient.apiKey.list({
fetchOptions: { query: { metadata: { organizationId: org.id } } },
})) as unknown as ApiKey[]
return data.filter((key) => key.metadata?.organizationId === org.id)
const { data: apiKeys = [], refetch: refetchKeys } = useQuery<ListedApiKey[]>(
{
queryKey: ["api-keys", org?.id],
queryFn: async () => {
if (!org?.id) return []
const API_URL =
process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"
const res = await fetch(`${API_URL}/v3/auth/keys`, {
credentials: "include",
})
if (!res.ok) return []
const data = (await res.json()) as { keys?: ListedApiKey[] }
return data.keys ?? []
},
enabled: !!org?.id,
staleTime: 30 * 1000,
},
enabled: !!org?.id,
staleTime: 30 * 1000,
})
)
const keyPrefix = useCallback((key: ListedApiKey): string | null => {
return key.start ?? (key.name?.startsWith("sm_") ? key.name : null)
}, [])
const connectedPlugins = useMemo<ConnectedKey[]>(() => {
const out: ConnectedKey[] = []
for (const key of apiKeys) {
if (key.enabled === false) continue
if (!key.lastRequest) continue
if (!key.metadata) continue
try {
const metadata =
@ -1063,14 +1046,14 @@ export function IntegrationsView() {
if (metadata.sm_type === "plugin_auth" && metadata.sm_client) {
out.push({
keyId: key.id,
keyStart: key.start ?? null,
pluginId: metadata.sm_client,
keyStart: keyPrefix(key),
pluginId: normalizePluginClientId(metadata.sm_client),
})
}
} catch {}
}
return out
}, [apiKeys])
}, [apiKeys, keyPrefix])
const connectionsByProvider = useMemo(() => {
const out: Record<ConnectorProvider, Connection[]> = {
@ -1368,7 +1351,14 @@ export function IntegrationsView() {
const needsProUpgrade =
!isAutumnLoading && !hasProProduct && !isFreeTierPlugin(item.pluginId)
if (keys.length > 0) {
return <ConnectedPill keys={keys} onRevoke={handleRevokePluginKey} />
return (
<ConnectedButton
onClick={() => {
trackCard(item)
setConnectedPluginId(item.pluginId)
}}
/>
)
}
if (needsProUpgrade) {
return (
@ -1500,12 +1490,7 @@ export function IntegrationsView() {
const renderLeftIndicator = (item: Item): ReactNode => {
if (item.kind === "plugin") {
const isConnected = connectedPlugins.some(
(k) => k.pluginId === item.pluginId,
)
return isConnected ? (
<span className="size-1.5 shrink-0 rounded-full bg-[#00AC3F]" />
) : null
return null
}
if (item.kind === "connector") {
const count = connectionsByProvider[item.provider].length
@ -1519,6 +1504,12 @@ export function IntegrationsView() {
const dialogPlugin = newKey.pluginId
? PLUGIN_CATALOG[newKey.pluginId]
: undefined
const connectedDialogPlugin = connectedPluginId
? PLUGIN_CATALOG[connectedPluginId]
: undefined
const connectedDialogKeys = connectedPluginId
? connectedPlugins.filter((key) => key.pluginId === connectedPluginId)
: []
const pluginSteps = dialogPlugin?.installSteps ?? []
const stepsEmbedKey = pluginSteps.some((s) => s.code?.includes("sm_..."))
const setupSteps: InstallStep[] = stepsEmbedKey
@ -1699,6 +1690,123 @@ export function IntegrationsView() {
</DialogContent>
</Dialog>
<Dialog
open={!!connectedPluginId}
onOpenChange={(open) => {
if (!open) setConnectedPluginId(null)
}}
>
<DialogContent
showCloseButton={false}
style={{
boxShadow:
"0 2.842px 14.211px 0 rgba(0,0,0,0.25), 0.711px 0.711px 0.711px 0 rgba(255,255,255,0.10) inset",
}}
className={cn(
dmSans125ClassName(),
"flex max-h-[88dvh] flex-col gap-3 overflow-hidden border border-white/[0.12] bg-[#1B1F24] p-0 px-3 pt-3 pb-4 rounded-2xl md:px-4 sm:max-w-[520px] sm:rounded-[22px]",
)}
>
<DialogTitle className="sr-only">
{connectedDialogPlugin?.name ?? "Plugin"} connection
</DialogTitle>
<div className="flex shrink-0 items-center gap-3">
{connectedDialogPlugin && (
<IconBox>
<Image
src={connectedDialogPlugin.icon}
alt={connectedDialogPlugin.name}
width={24}
height={24}
/>
</IconBox>
)}
<div className="min-w-0 flex-1">
<p className="truncate text-[16px] font-semibold leading-tight text-[#FAFAFA]">
{connectedDialogPlugin?.name ?? "Plugin"} connected
</p>
<p className="mt-0.5 flex items-center gap-1.5 text-[12px] text-[#00AC3F]">
<span className="size-[7px] rounded-full bg-[#00AC3F]" />
Active
</p>
</div>
<div className="flex shrink-0 items-center gap-2">
{connectedDialogPlugin?.docsUrl && (
<a
href={connectedDialogPlugin.docsUrl}
target="_blank"
rel="noopener noreferrer"
className={cn(
dmSans125ClassName(),
"flex h-7 items-center gap-1.5 rounded-full bg-[#0D121A] px-3 text-[12px] text-[#A1A1AA] transition-colors hover:text-white",
INSET,
)}
>
<BookOpen className="size-3.5" /> Docs
</a>
)}
<DialogPrimitive.Close
type="button"
aria-label="Close"
className={cn(
"flex size-7 items-center justify-center rounded-full bg-[#0D121A] transition-opacity hover:opacity-80 focus:outline-none",
INSET,
)}
>
<X className="size-4 text-[#737373]" />
</DialogPrimitive.Close>
</div>
</div>
<div
className={cn(
"flex min-w-0 flex-col gap-2 rounded-[14px] bg-[#14161A] p-4",
INSET,
)}
>
<p className="text-[11px] font-medium uppercase tracking-wide text-[#737373]">
{connectedDialogKeys.length > 1
? `${connectedDialogKeys.length} connections`
: "Connection"}
</p>
{connectedDialogKeys.length > 0 ? (
<div className="flex flex-col gap-1.5">
{connectedDialogKeys.map((key) => (
<div
key={key.keyId}
className="flex min-w-0 items-center justify-between gap-3 rounded-lg bg-[#0D121A]/70 px-3 py-2"
>
<span className="min-w-0 flex-1 truncate font-mono text-[12px] text-[#A1A1AA]">
{key.keyStart ? `${key.keyStart}...` : "API key"}
</span>
<DisconnectButton
onConfirm={() => void handleRevokePluginKey(key.keyId)}
/>
</div>
))}
</div>
) : (
<p className="text-[12px] text-[#A1A1AA]">
No active connection was found.
</p>
)}
</div>
<div className="flex shrink-0 items-center justify-end">
<DialogPrimitive.Close asChild>
<button
type="button"
className={cn(
dmSans125ClassName(),
"flex h-9 items-center gap-1.5 rounded-full bg-[#0D121A] px-5 text-[13px] font-medium text-[#FAFAFA] transition-opacity hover:opacity-80",
INSET,
)}
>
<Check className="size-3.5 text-[#4BA0FA]" /> Done
</button>
</DialogPrimitive.Close>
</div>
</DialogContent>
</Dialog>
<Dialog
open={mcpModalOpen}
onOpenChange={(open) => {

View file

@ -8,7 +8,15 @@ import { hasActivePlan } from "@lib/queries"
import { useCustomer } from "autumn-js/react"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { BookOpen, Check, ChevronDown, Loader, X, Zap } from "lucide-react"
import {
BookOpen,
Check,
ChevronDown,
ExternalLink,
Loader,
X,
Zap,
} from "lucide-react"
import Image from "next/image"
import { type ReactNode, useEffect, useMemo, useState } from "react"
import { toast } from "sonner"
@ -17,6 +25,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@ui/components/popover"
import {
PLUGIN_CATALOG,
isFreeTierPlugin,
normalizePluginClientId,
type InstallStep,
type PluginInfo,
} from "@/lib/plugin-catalog"
@ -27,9 +36,20 @@ interface ConnectedPlugin {
keyId: string
pluginId: string
createdAt: string
lastRequest?: string | null
keyStart?: string | null
}
type ListedApiKey = {
id: string
name?: string | null
createdAt: string
enabled?: boolean
lastRequest: string | null
metadata: string | Record<string, unknown> | null
start?: string | null
}
function SectionHeader({ children }: { children: ReactNode }) {
return (
<p
@ -121,13 +141,77 @@ function DisconnectButton({ onConfirm }: { onConfirm: () => void }) {
)
}
function ConnectedPill({
function toDate(value: string | Date | null | undefined): Date | null {
if (!value) return null
const date = value instanceof Date ? value : new Date(value)
return Number.isNaN(date.getTime()) ? null : date
}
function formatRelativeTime(value: string | Date | null | undefined): string {
const date = toDate(value)
if (!date) return "Never"
const absMs = Math.abs(Date.now() - date.getTime())
const minute = 60 * 1000
const hour = 60 * minute
const day = 24 * hour
if (absMs < minute) return "Just now"
if (absMs < hour) return `${Math.round(absMs / minute)}m ago`
if (absMs < day) return `${Math.round(absMs / hour)}h ago`
return `${Math.round(absMs / day)}d ago`
}
function formatDate(value: string | Date | null | undefined): string {
const date = toDate(value)
if (!date) return "Unknown"
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
})
}
function maskKey(start: string | null | undefined): string {
if (!start) return "sm_********"
return `${start}********`
}
function keyPrefix(key: ListedApiKey): string | null {
return key.start ?? (key.name?.startsWith("sm_") ? key.name : null)
}
function DetailStat({ label, value }: { label: string; value: string }) {
return (
<div className="min-w-0">
<p
className={cn(
dmSans125ClassName(),
"text-[10px] font-medium uppercase tracking-wide text-[#737373]",
)}
>
{label}
</p>
<p
className={cn(
dmSans125ClassName(),
"mt-1 truncate text-[13px] font-medium text-[#FAFAFA]",
)}
>
{value}
</p>
</div>
)
}
function ActivePill({
plugin,
connectedKeys,
onRevoke,
}: {
plugin: PluginInfo
connectedKeys: ConnectedPlugin[]
onRevoke: (keyId: string) => void
}) {
const primaryKey = connectedKeys[0]
return (
<Popover>
<PopoverTrigger asChild>
@ -140,7 +224,7 @@ function ConnectedPill({
)}
>
<span className="size-[7px] rounded-full bg-[#00AC3F]" />
Connected
ACTIVE
<ChevronDown className="size-3 text-[#737373]" />
</button>
</PopoverTrigger>
@ -148,13 +232,67 @@ function ConnectedPill({
align="end"
className={cn(
dmSans125ClassName(),
"w-[260px] rounded-xl border border-white/10 bg-[#1B1F24] p-2 text-[#FAFAFA]",
"w-[min(380px,calc(100vw-32px))] rounded-xl border border-white/10 bg-[#1B1F24] p-4 text-[#FAFAFA]",
)}
>
<div className="flex items-center gap-3">
<PluginIconBox src={plugin.icon} alt={plugin.name} />
<div className="min-w-0 flex-1">
<p
className={cn(
dmSans125ClassName(),
"truncate text-[15px] font-semibold text-[#FAFAFA]",
)}
>
{plugin.name}
</p>
<p className="mt-0.5 text-[12px] text-[#A1A1AA]">Connected</p>
</div>
<span className="size-3 rounded-full bg-[#37D67A]" />
</div>
<div className="mt-4 grid grid-cols-1 gap-3 border-y border-white/[0.08] py-4 sm:grid-cols-3">
<DetailStat label="API key" value={maskKey(primaryKey?.keyStart)} />
<DetailStat
label="Last active"
value={formatRelativeTime(primaryKey?.lastRequest)}
/>
<DetailStat
label="Connected"
value={formatRelativeTime(primaryKey?.createdAt)}
/>
</div>
<div className="mt-4 flex flex-wrap items-center gap-3">
{plugin.docsUrl && (
<a
href={plugin.docsUrl}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 text-[13px] text-[#4BA0FA] transition-colors hover:text-[#86C5FF]"
>
Docs <ExternalLink className="size-3" />
</a>
)}
{plugin.githubUrl && (
<a
href={plugin.githubUrl}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 text-[13px] text-[#4BA0FA] transition-colors hover:text-[#86C5FF]"
>
GitHub <ExternalLink className="size-3" />
</a>
)}
<span className="ml-auto text-[11px] text-[#737373]">
Connected {formatDate(primaryKey?.createdAt)}
</span>
</div>
<p
className={cn(
dmSans125ClassName(),
"px-2 pb-1.5 pt-1 text-[11px] font-medium uppercase tracking-wide text-[#737373]",
"px-2 pb-1.5 pt-4 text-[11px] font-medium uppercase tracking-wide text-[#737373]",
)}
>
{connectedKeys.length > 1
@ -240,7 +378,11 @@ function PluginRow({
<div className="col-start-2 flex min-w-0 shrink-0 items-center gap-2 sm:col-start-auto sm:gap-4">
{plugin.docsUrl && <DocsLink href={plugin.docsUrl} />}
{isConnected ? (
<ConnectedPill connectedKeys={connectedKeys} onRevoke={onRevoke} />
<ActivePill
plugin={plugin}
connectedKeys={connectedKeys}
onRevoke={onRevoke}
/>
) : needsProUpgrade ? (
<PillButton onClick={onUpgrade}>
<Zap className="size-3.5 text-[#4BA0FA]" /> Upgrade
@ -332,21 +474,29 @@ export function PluginsDetail() {
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)
const { data: apiKeys = [], refetch: refetchKeys } = useQuery<ListedApiKey[]>(
{
enabled: !!org?.id,
queryFn: async () => {
if (!org?.id) return []
const API_URL =
process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"
const res = await fetch(`${API_URL}/v3/auth/keys`, {
credentials: "include",
})
if (!res.ok) return []
const data = (await res.json()) as { keys?: ListedApiKey[] }
return data.keys ?? []
},
queryKey: ["api-keys", org?.id],
},
queryKey: ["api-keys", org?.id],
})
)
const connectedPlugins = useMemo<ConnectedPlugin[]>(() => {
const plugins: ConnectedPlugin[] = []
for (const key of apiKeys) {
if (key.enabled === false) continue
if (!key.lastRequest) continue
if (!key.metadata) continue
try {
const metadata =
@ -358,12 +508,15 @@ export function PluginsDetail() {
: (key.metadata as { sm_type?: string; sm_client?: string })
if (metadata.sm_type === "plugin_auth" && metadata.sm_client) {
const createdAt = toDate(key.createdAt)?.toISOString()
const lastRequest = toDate(key.lastRequest)?.toISOString()
plugins.push({
id: key.id,
keyId: key.id,
pluginId: metadata.sm_client,
createdAt: key.createdAt.toISOString(),
keyStart: key.start ?? null,
pluginId: normalizePluginClientId(metadata.sm_client),
createdAt: createdAt ?? new Date().toISOString(),
lastRequest: lastRequest ?? null,
keyStart: keyPrefix(key),
})
}
} catch {}

View file

@ -14,6 +14,7 @@ export interface PluginInfo {
tagline: string
icon: string
docsUrl?: string
githubUrl?: string
/** Steps shown after a key is minted. The literal `sm_...` is replaced
* with the freshly generated key when rendered. */
installSteps?: InstallStep[]
@ -55,6 +56,7 @@ export const PLUGIN_CATALOG: Record<string, PluginInfo> = {
tagline: "Persistent memory for the Codex CLI — free on every plan",
icon: "/images/plugins/codex.png",
docsUrl: "https://docs.supermemory.ai/integrations/codex",
githubUrl: "https://github.com/supermemoryai/codex-supermemory",
installSteps: [
{
title: "Save your API key",