From c6b20b5b87e826adfb51f2f6365dca2dab0f59a9 Mon Sep 17 00:00:00 2001 From: ved015 <122012786+ved015@users.noreply.github.com> Date: Fri, 19 Jun 2026 17:46:21 +0000 Subject: [PATCH 01/22] Fix delete organization dialog focus race (#1139) ## Summary - Delay opening the delete organization dialog until after the Danger zone popover begins closing - Explicitly focus the organization confirmation input when the dialog opens - Prevent intermittent focus loss where users could not type the org name --- .../components/settings/settings-content.tsx | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/apps/web/components/settings/settings-content.tsx b/apps/web/components/settings/settings-content.tsx index c0f2a167..a2ee63a4 100644 --- a/apps/web/components/settings/settings-content.tsx +++ b/apps/web/components/settings/settings-content.tsx @@ -3,7 +3,7 @@ import { Logo } from "@ui/assets/Logo" import { useAuth } from "@lib/auth-context" import NovaOrb from "@/components/nova/nova-orb" -import { useState } from "react" +import { useRef, useState } from "react" import { cn } from "@lib/utils" import { dmSansClassName, dmSans125ClassName } from "@/lib/fonts" import Account from "@/components/settings/account" @@ -147,6 +147,7 @@ export function SettingsContent({ const [isDeleteOrgDialogOpen, setIsDeleteOrgDialogOpen] = useState(false) const [deleteOrgConfirm, setDeleteOrgConfirm] = useState("") + const deleteOrgInputRef = useRef(null) const deleteOrganization = useDeleteOrganization() // Only owners can delete the organization. @@ -166,6 +167,13 @@ export function SettingsContent({ const [dangerMenuOpen, setDangerMenuOpen] = useState(false) + const openDeleteOrganizationDialog = () => { + setDangerMenuOpen(false) + window.requestAnimationFrame(() => { + setIsDeleteOrgDialogOpen(true) + }) + } + const displayName = user?.displayUsername || localStorageUsername || @@ -361,10 +369,7 @@ export function SettingsContent({ @@ -467,8 +483,8 @@ export function StepSources({ onChange={onChange} isLocked={isLocked} guard={guard} - setState={setState} openExternal={openExternal} + requestWaitlist={requestWaitlist} connectRealProvider={connectRealProvider} /> @@ -917,8 +933,8 @@ function MoreSourcesGrid({ onChange, isLocked, guard, - setState, openExternal, + requestWaitlist, connectRealProvider, }: { mode: BrainMode @@ -930,8 +946,8 @@ function MoreSourcesGrid({ title: string, fn: () => void, ) => () => void - setState: (id: SourceId, state: SourceState) => void openExternal: (id: SourceId, url: string) => void + requestWaitlist: (id: SourceId) => void connectRealProvider: ( provider: "google-drive" | "notion" | "onedrive", id: SourceId, @@ -1027,7 +1043,7 @@ function MoreSourcesGrid({ "Decisions and follow-ups surfaced", "You control which labels sync", ]} - onConnect={guard("max", "Gmail", () => setState("gmail", "waitlist"))} + onConnect={guard("max", "Gmail", () => requestWaitlist("gmail"))} /> setState("github", "waitlist"))} + onConnect={guard("max", "GitHub", () => requestWaitlist("github"))} /> - toast.info("Granola is coming soon."), - )} + onConnect={guard("max", "Granola", () => { + analytics.onboardingIntegrationClicked({ integration: "granola" }) + toast.info("Granola is coming soon.") + })} /> ) diff --git a/apps/web/lib/analytics.ts b/apps/web/lib/analytics.ts index 71987d22..ae78e306 100644 --- a/apps/web/lib/analytics.ts +++ b/apps/web/lib/analytics.ts @@ -1,16 +1,53 @@ import posthog from "posthog-js" +import type { BrainStep } from "@/components/onboarding-brain/types" -export type OnboardingStep = "profile_input" | "processing" | "done" | "error" -export type OnboardingSource = "x" | "linkedin" | "resume" +const pendingEvents: Array<{ + eventName: string + properties?: Record +}> = [] +let flushTimer: ReturnType | undefined +let flushTimeout: ReturnType | undefined + +const flushPendingEvents = () => { + if (!posthog.__loaded) return + while (pendingEvents.length > 0) { + const event = pendingEvents.shift() + if (!event) return + posthog.capture(event.eventName, event.properties) + } + if (flushTimer) { + clearInterval(flushTimer) + flushTimer = undefined + } + if (flushTimeout) { + clearTimeout(flushTimeout) + flushTimeout = undefined + } +} + +const scheduleFlush = () => { + if (flushTimer) return + flushTimer = setInterval(flushPendingEvents, 200) + flushTimeout = setTimeout(() => { + if (!flushTimer) return + clearInterval(flushTimer) + flushTimer = undefined + flushTimeout = undefined + pendingEvents.length = 0 + }, 10000) +} -// Helper function to safely capture events const safeCapture = ( eventName: string, properties?: Record, ) => { if (posthog.__loaded) { + flushPendingEvents() posthog.capture(eventName, properties) + return } + pendingEvents.push({ eventName, properties }) + scheduleFlush() } export const analytics = { @@ -82,32 +119,62 @@ export const analytics = { addDocumentModalOpened: () => safeCapture("add_document_modal_opened"), // onboarding analytics + onboardingStarted: (props: { mode: string; entry_step: BrainStep }) => + safeCapture("onboarding_started", props), + onboardingStepViewed: (props: { - step: OnboardingStep + step: BrainStep + index: number trigger: "user" | "auto" }) => safeCapture("onboarding_step_viewed", props), - onboardingProfileSubmitted: (props: { source: OnboardingSource }) => - safeCapture("onboarding_profile_submitted", props), + onboardingStepCompleted: (props: { step: BrainStep; index: number }) => + safeCapture("onboarding_step_completed", props), + + onboardingModeSelected: (props: { mode: string }) => + safeCapture("onboarding_mode_selected", props), + + onboardingWorkspaceCreated: (props: { + mode: string + has_about: boolean + has_domain: boolean + }) => safeCapture("onboarding_workspace_created", props), + + onboardingWorkspaceCreateFailed: (props: { error: string }) => + safeCapture("onboarding_workspace_create_failed", props), onboardingIntegrationClicked: (props: { integration: string }) => safeCapture("onboarding_integration_clicked", props), + onboardingSourcesCompleted: (props: { connected_count: number }) => + safeCapture("onboarding_sources_completed", props), + + onboardingAgentSelected: (props: { agent: string }) => + safeCapture("onboarding_agent_selected", props), + + onboardingIngestCompleted: () => safeCapture("onboarding_ingest_completed"), + + onboardingIngestSkipped: () => safeCapture("onboarding_ingest_skipped"), + + onboardingInvitesSent: (props: { sent: number; failed: number }) => + safeCapture("onboarding_invites_sent", props), + + onboardingTeamSkipped: () => safeCapture("onboarding_team_skipped"), + onboardingChromeExtensionClicked: (props: { source: "onboarding" | "settings" | "integrations" }) => safeCapture("onboarding_chrome_extension_clicked", props), onboardingMcpDetailOpened: () => safeCapture("onboarding_mcp_detail_opened"), - onboardingXBookmarksDetailOpened: () => - safeCapture("onboarding_x_bookmarks_detail_opened"), - - onboardingSkipped: (props: { from_step: OnboardingStep }) => + onboardingSkipped: (props: { from_step: BrainStep }) => safeCapture("onboarding_skipped", props), - onboardingCompleted: (props?: { - source?: OnboardingSource - memories_count?: number + onboardingCompleted: (props: { + mode: string + steps_completed: number + sources_connected: number + invites_sent: number }) => safeCapture("onboarding_completed", props), // main app analytics From 504940414ce7eafec3155e71c4a1ef3e9a1d5689 Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Mon, 22 Jun 2026 17:54:13 +0000 Subject: [PATCH 03/22] feat(web): company brain entitlement helper, hook + ?org deep-link activation (#1110) Add hasCompanyBrain helper + useHasCompanyBrain hook reading the company_brain add-on from org metadata to gate Company Brain UI. Fixes ENG-806 --- apps/web/hooks/use-company-brain.ts | 16 +++++ apps/web/lib/billing-utils.ts | 94 +++++++++++++++++++++++++++++ apps/web/stores/index.ts | 19 ++++-- packages/lib/auth-context.tsx | 33 ++++++++++ packages/lib/constants.ts | 2 + packages/lib/types.ts | 1 + packages/validation/api.ts | 4 ++ 7 files changed, 164 insertions(+), 5 deletions(-) create mode 100644 apps/web/hooks/use-company-brain.ts diff --git a/apps/web/hooks/use-company-brain.ts b/apps/web/hooks/use-company-brain.ts new file mode 100644 index 00000000..fb80f7be --- /dev/null +++ b/apps/web/hooks/use-company-brain.ts @@ -0,0 +1,16 @@ +import { useAuth } from "@lib/auth-context" +import { + getBrainMode, + getCompanyBrainOverride, + hasCompanyBrain, +} from "@/lib/billing-utils" + +export function useHasCompanyBrain(): boolean { + const { org } = useAuth() + const metadata = org?.metadata as Record | string | undefined + // An explicit concierge override wins over the team-onboarding fallback. + const override = getCompanyBrainOverride(metadata) + if (override !== undefined) return override + // Team-brain orgs use brain spaces even before the add-on webhook lands. + return hasCompanyBrain(metadata) || getBrainMode(metadata) === "team" +} diff --git a/apps/web/lib/billing-utils.ts b/apps/web/lib/billing-utils.ts index b07e5c07..4169c257 100644 --- a/apps/web/lib/billing-utils.ts +++ b/apps/web/lib/billing-utils.ts @@ -1,3 +1,97 @@ +const COMPANY_BRAIN_PRODUCT_ID = "company_brain" + +// Add-on resolved by product presence, not tier. +// better-auth returns org.metadata as a JSON string, so accept string or object. +export function hasCompanyBrain( + metadataRaw: Record | string | null | undefined, +): boolean { + if (!metadataRaw) return false + let metadata: Record + if (typeof metadataRaw === "string") { + try { + metadata = JSON.parse(metadataRaw) as Record + } catch { + return false + } + } else { + metadata = metadataRaw + } + const overrides = metadata.featureOverrides as + | Record + | undefined + const override = overrides?.[COMPANY_BRAIN_PRODUCT_ID] + if (override) return Boolean(override.allow) + const activeProducts = Array.isArray(metadata.activeProducts) + ? (metadata.activeProducts as string[]) + : [] + return activeProducts.includes(COMPANY_BRAIN_PRODUCT_ID) +} + +// Explicit concierge override for company_brain, or undefined when none is set. +export function getCompanyBrainOverride( + metadataRaw: Record | string | null | undefined, +): boolean | undefined { + if (!metadataRaw) return undefined + let metadata: Record + if (typeof metadataRaw === "string") { + try { + metadata = JSON.parse(metadataRaw) as Record + } catch { + return undefined + } + } else { + metadata = metadataRaw + } + const overrides = metadata.featureOverrides as + | Record + | undefined + const override = overrides?.[COMPANY_BRAIN_PRODUCT_ID] + return override ? Boolean(override.allow) : undefined +} + +// Origin of the org. Consumer (app.supermemory) orgs get company_brain attached, +// but the add-on lands async — signupSource is set at creation, so it's the +// reliable "this org uses brain spaces" signal in the UI. +export function getSignupSource( + metadataRaw: Record | string | null | undefined, +): string | null { + if (!metadataRaw) return null + let metadata: Record + if (typeof metadataRaw === "string") { + try { + metadata = JSON.parse(metadataRaw) as Record + } catch { + return null + } + } else { + metadata = metadataRaw + } + return typeof metadata.signupSource === "string" + ? (metadata.signupSource as string) + : null +} + +// Brain mode chosen during onboarding ("personal" | "team"). Set synchronously +// at org creation, so it's the reliable pre-webhook signal for company brain. +export function getBrainMode( + metadataRaw: Record | string | null | undefined, +): string | null { + if (!metadataRaw) return null + let metadata: Record + if (typeof metadataRaw === "string") { + try { + metadata = JSON.parse(metadataRaw) as Record + } catch { + return null + } + } else { + metadata = metadataRaw + } + return typeof metadata.brainMode === "string" + ? (metadata.brainMode as string) + : null +} + /** * Format a number with K/M suffix for display * @example formatUsageNumber(1500000) => "1.5M" diff --git a/apps/web/stores/index.ts b/apps/web/stores/index.ts index 4d754f77..e13cccef 100644 --- a/apps/web/stores/index.ts +++ b/apps/web/stores/index.ts @@ -3,18 +3,27 @@ import { useQueryState } from "nuqs" import { projectParam } from "@/lib/search-params" import { useCallback } from "react" -import { DEFAULT_PROJECT_ID } from "@lib/constants" +import { DEFAULT_PROJECT_ID, SHARED_TEAM_BRAIN_TAG } from "@lib/constants" +import { useHasCompanyBrain } from "@/hooks/use-company-brain" export function useProject() { const [selectedProjects, _setSelectedProjects] = useQueryState( "project", projectParam, ) + const hasCompanyBrain = useHasCompanyBrain() + const defaultTag = hasCompanyBrain + ? SHARED_TEAM_BRAIN_TAG + : DEFAULT_PROJECT_ID - const selectedProject = selectedProjects[0] ?? DEFAULT_PROJECT_ID + // Normalize empty selection to the default tag so the selector, counts, and + // queries all agree (shared Team Brain for company-brain orgs). + const normalizedProjects = + selectedProjects.length === 0 ? [defaultTag] : selectedProjects - const effectiveContainerTags = - selectedProjects.length === 0 ? [DEFAULT_PROJECT_ID] : selectedProjects + const selectedProject = normalizedProjects[0] + + const effectiveContainerTags = normalizedProjects const setSelectedProjects = useCallback( (projects: string[]) => { @@ -31,7 +40,7 @@ export function useProject() { ) return { - selectedProjects, + selectedProjects: normalizedProjects, selectedProject, setSelectedProjects, setSelectedProject, diff --git a/packages/lib/auth-context.tsx b/packages/lib/auth-context.tsx index d3d0bedb..acd15e88 100644 --- a/packages/lib/auth-context.tsx +++ b/packages/lib/auth-context.tsx @@ -18,6 +18,23 @@ type OrganizationListItem = NonNullable< const STORAGE_KEY = "supermemory-consumer-last-org-slug" +// Reads ?org= from the URL once and removes it, so a deep link that +// selects an org doesn't re-fire on refresh or back-navigation. +function consumeRequestedOrgSlug(): string | null { + if (typeof window === "undefined") return null + const params = new URLSearchParams(window.location.search) + const slug = params.get("org") + if (!slug) return null + params.delete("org") + const qs = params.toString() + window.history.replaceState( + null, + "", + `${window.location.pathname}${qs ? `?${qs}` : ""}${window.location.hash}`, + ) + return slug +} + interface AuthContextType { session: SessionData["session"] | null user: SessionData["user"] | null @@ -123,6 +140,22 @@ export function AuthProvider({ children }: { children: ReactNode }) { const activeOrgId = session.session.activeOrganizationId + // Deep link (?org=) takes priority — used when arriving from + // the console. Strip the param so refresh/back doesn't re-trigger. + const requestedSlug = consumeRequestedOrgSlug() + if (requestedSlug) { + const match = orgs.find((o) => o.slug === requestedSlug) + if (match) { + if (activeOrgId === match.id) { + const full = await authClient.organization.getFullOrganization() + if (!cancelled) setOrg(full?.data ?? null) + } else { + await setActiveOrg(requestedSlug) + } + return + } + } + if (orgs.length === 1) { const one = orgs[0] if (!one) return diff --git a/packages/lib/constants.ts b/packages/lib/constants.ts index 01439d1e..7758cdb6 100644 --- a/packages/lib/constants.ts +++ b/packages/lib/constants.ts @@ -1,5 +1,6 @@ const BIG_DIMENSIONS_NEW = 1536 const DEFAULT_PROJECT_ID = "sm_project_default" +const SHARED_TEAM_BRAIN_TAG = "sm_org_shared" const SEARCH_MEMORY_SHORTCUT_URL = "https://www.icloud.com/shortcuts/b0a132cc3c0d475196bc7014aa702a5c" const ADD_MEMORY_SHORTCUT_URL = @@ -12,6 +13,7 @@ const POKE_RECIPE_URL = "https://supermemory.link/poke" export { BIG_DIMENSIONS_NEW, DEFAULT_PROJECT_ID, + SHARED_TEAM_BRAIN_TAG, SEARCH_MEMORY_SHORTCUT_URL, ADD_MEMORY_SHORTCUT_URL, RAYCAST_EXTENSION_URL, diff --git a/packages/lib/types.ts b/packages/lib/types.ts index 9afe1945..34e39c3f 100644 --- a/packages/lib/types.ts +++ b/packages/lib/types.ts @@ -6,6 +6,7 @@ export interface Project { updatedAt: string isExperimental?: boolean emoji?: string + visibility?: "public" | "private" | "unlisted" } export interface ContainerTagListType extends Project { diff --git a/packages/validation/api.ts b/packages/validation/api.ts index e1e8c8ef..23ccff2a 100644 --- a/packages/validation/api.ts +++ b/packages/validation/api.ts @@ -1496,6 +1496,10 @@ export const ContainerTagListTypeSchema = z description: "True if containerTag starts with 'sm_project_'", example: true, }), + visibility: z.enum(["public", "private", "unlisted"]).optional().openapi({ + description: "Space visibility (company brain spaces)", + example: "public", + }), }) .openapi({ description: From ab3dcd7a7334e6520f92fd09d27a7e4a16249345 Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Mon, 22 Jun 2026 18:12:18 +0000 Subject: [PATCH 04/22] feat(web): create org from settings launches onboarding (#1136) Settings 'create organization' now routes to /onboarding?new=1&name=... (team/personal, invites) instead of a bare authClient.create. Adds the forceCreate path with name prefill, clears new=1 after a successful create to prevent duplicate orgs, and fixes the Radix popover-to-dialog pointer-events lock. --- apps/web/app/(app)/onboarding/page.tsx | 27 +++++++++-- .../settings/settings-org-switcher.tsx | 47 ++++++------------- 2 files changed, 38 insertions(+), 36 deletions(-) diff --git a/apps/web/app/(app)/onboarding/page.tsx b/apps/web/app/(app)/onboarding/page.tsx index 8ee25591..311791bf 100644 --- a/apps/web/app/(app)/onboarding/page.tsx +++ b/apps/web/app/(app)/onboarding/page.tsx @@ -44,6 +44,10 @@ export default function BrainOnboardingPage() { const { user, org, organizations, setActiveOrg, refetchOrganizations } = useAuth() + // `?new=1` forces creating an additional org even when the user already has one. + const forceCreate = params?.get("new") === "1" + const nameParam = params?.get("name")?.trim() || "" + const stepFromUrl = (params?.get("step") as BrainStep | null) ?? "about" const initialStep: BrainStep = BRAIN_STEPS.includes(stepFromUrl) ? stepFromUrl @@ -68,7 +72,7 @@ export default function BrainOnboardingPage() { const [about, setAbout] = useState({ name: user?.name ?? "", about: "", - workspaceName: suggestedWorkspaceName, + workspaceName: nameParam || suggestedWorkspaceName, workspaceDomain: domain ?? "", }) const [sources, setSources] = useState({ @@ -82,6 +86,7 @@ export default function BrainOnboardingPage() { }) useEffect(() => { + if (forceCreate) return try { const raw = localStorage.getItem(STORAGE_KEY) if (!raw) return @@ -96,7 +101,7 @@ export default function BrainOnboardingPage() { if (cached.sources) setSources((s) => ({ ...s, ...cached.sources })) if (cached.team) setTeam((t) => ({ ...t, ...cached.team })) } catch {} - }, []) + }, [forceCreate]) useEffect(() => { try { @@ -175,8 +180,13 @@ export default function BrainOnboardingPage() { try { localStorage.removeItem(STORAGE_KEY) } catch {} + // Extra org from settings: hard-reload so org-scoped caches don't show the previous org's data. + if (forceCreate) { + window.location.href = "/?onboarded=1" + return + } router.push("/?onboarded=1") - }, [router, mode, sources, team]) + }, [router, mode, sources, team, forceCreate]) const goNext = useCallback(() => { const idx = BRAIN_STEPS.indexOf(step) @@ -193,7 +203,7 @@ export default function BrainOnboardingPage() { const creatingOrgRef = useRef(false) const ensureOrg = useCallback(async () => { - if (organizations && organizations.length > 0) return + if (!forceCreate && organizations && organizations.length > 0) return const name = (about.workspaceName || suggestedWorkspaceName).trim() const slug = generateOrgSlug(name) const metadata: BrainMetadata & { signupSource: string } = { @@ -225,6 +235,13 @@ export default function BrainOnboardingPage() { has_about: Boolean(about.about.trim()), has_domain: Boolean(mode === "team" && (about.workspaceDomain || domain)), }) + // Drop new=1 so a reload or back+Continue reuses this org instead of creating a duplicate. + if (forceCreate) { + const url = new URL(window.location.href) + url.searchParams.delete("new") + url.searchParams.delete("name") + router.replace(url.pathname + url.search, { scroll: false }) + } }, [ organizations, about, @@ -234,6 +251,8 @@ export default function BrainOnboardingPage() { containerTag, setActiveOrg, refetchOrganizations, + forceCreate, + router, ]) const handleAboutContinue = useCallback(async () => { diff --git a/apps/web/components/settings/settings-org-switcher.tsx b/apps/web/components/settings/settings-org-switcher.tsx index 9ebcf9b5..e3e4be84 100644 --- a/apps/web/components/settings/settings-org-switcher.tsx +++ b/apps/web/components/settings/settings-org-switcher.tsx @@ -1,6 +1,7 @@ "use client" -import { useMemo, useState } from "react" +import { useEffect, useMemo, useState } from "react" +import { useRouter } from "next/navigation" import { useCustomer } from "autumn-js/react" import { toast } from "sonner" import { @@ -13,7 +14,6 @@ import { import { cn } from "@lib/utils" import { dmSansClassName, dmSans125ClassName } from "@/lib/fonts" import { useAuth } from "@lib/auth-context" -import { authClient } from "@lib/auth" import { Popover, PopoverContent, PopoverTrigger } from "@ui/components/popover" import { Dialog, DialogContent, DialogTitle } from "@ui/components/dialog" import { OrgPlanBadge, resolveOrgPlan } from "@/components/org-plan-badge" @@ -23,17 +23,9 @@ import { useTokenUsage, type PlanType } from "@/hooks/use-token-usage" const SURFACE_SHADOW = "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" -function generateOrgSlug(name: string): string { - const base = - name - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/(^-|-$)/g, "") || "org" - return `${base}-${Math.floor(100000 + Math.random() * 900000)}` -} - export function SettingsOrgSwitcher() { const { org, organizations, setActiveOrg } = useAuth() + const router = useRouter() const autumn = useCustomer() const { currentPlan } = useTokenUsage(autumn) const { data: orgSummaries } = useOrgSummaries() @@ -44,6 +36,11 @@ export function SettingsOrgSwitcher() { const [createName, setCreateName] = useState("") const [creating, setCreating] = useState(false) + // Clear a stale Radix `pointer-events: none` left on so the dialog accepts clicks. + useEffect(() => { + if (createOpen) document.body.style.pointerEvents = "" + }, [createOpen]) + const planByOrgId = useMemo(() => { const map = new Map() for (const summary of orgSummaries ?? []) { @@ -78,29 +75,14 @@ export function SettingsOrgSwitcher() { } } - const handleCreate = async () => { + const handleCreate = () => { const name = createName.trim() if (!name || creating) return setCreating(true) - try { - const result = await authClient.organization.create({ - name, - slug: generateOrgSlug(name), - metadata: { signupSource: "consumer" }, - }) - if (result.error) { - throw new Error(result.error.message ?? "Failed to create organization") - } - await setActiveOrg(result.data?.slug ?? "") - window.location.reload() - } catch (error) { - setCreating(false) - toast.error( - error instanceof Error - ? error.message - : "Failed to create organization", - ) - } + // Org creation now happens through the onboarding flow (team/personal, name, invites). + setCreateOpen(false) + setOpen(false) + router.push(`/onboarding?new=1&name=${encodeURIComponent(name)}`) } return ( @@ -179,8 +161,9 @@ export function SettingsOrgSwitcher() { + ) +} + +function SecondaryButton({ + children, + onClick, + disabled, +}: { + children: React.ReactNode + onClick: () => void + disabled?: boolean +}) { + return ( + + ) +} + +function IconTile({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ) +} + +function Title({ children }: { children: React.ReactNode }) { + return ( +

+ {children} +

+ ) +} + +function Subtitle({ children }: { children: React.ReactNode }) { + return ( +

+ {children} +

+ ) +} + +function StatusCard({ + icon, + title, + description, + actionLabel, + onAction, +}: { + icon: React.ReactNode + title: string + description: string + actionLabel: string + onAction: () => void +}) { + return ( +
+
+
+ {icon} +
+ {title} + {description} +
+ {actionLabel} +
+
+
+ ) +} + +export default function InvitePage() { + const params = useParams<{ invitationId: string }>() + const invitationId = params.invitationId + const { data: session, isPending: sessionPending } = useSession() + const { setActiveOrg, refetchOrganizations } = useAuth() + const router = useRouter() + + const [state, setState] = useState("loading") + const [invitation, setInvitation] = useState(null) + const [accepting, setAccepting] = useState(false) + const [declining, setDeclining] = useState(false) + + useEffect(() => { + if (sessionPending) return + if (!session) { + setState("no_session") + return + } + let cancelled = false + ;(async () => { + const { data, error } = await authClient.organization.getInvitation({ + query: { id: invitationId }, + }) + if (cancelled) return + if (error) { + setState(error.status === 403 ? "wrong_account" : "not_found") + return + } + if (!data) { + setState("not_found") + return + } + const inv = data as unknown as InvitationData + if (inv.status === "accepted") setState("already_accepted") + else if (inv.status === "canceled" || inv.status === "rejected") + setState("not_found") + else if (new Date(inv.expiresAt) < new Date()) setState("expired") + else { + setInvitation(inv) + setState("ready") + } + })() + return () => { + cancelled = true + } + }, [session, sessionPending, invitationId]) + + const handleAccept = useCallback(async () => { + setAccepting(true) + try { + const { error } = await authClient.organization.acceptInvitation({ + invitationId, + }) + if (error) { + toast.error(error.message ?? "Failed to accept invitation") + return + } + if (invitation?.organizationSlug) { + await setActiveOrg(invitation.organizationSlug) + } + await refetchOrganizations() + toast.success( + `You've joined ${invitation?.organizationName ?? "the team"}`, + ) + router.push("/") + } finally { + setAccepting(false) + } + }, [invitationId, invitation, setActiveOrg, refetchOrganizations, router]) + + const handleDecline = useCallback(async () => { + setDeclining(true) + try { + const { error } = await authClient.organization.rejectInvitation({ + invitationId, + }) + if (error) { + toast.error(error.message ?? "Failed to decline invitation") + return + } + toast.success("Invitation declined") + router.push("/") + } finally { + setDeclining(false) + } + }, [invitationId, router]) + + if (state === "loading") return + + if (state === "no_session") { + const loginHref = `/login?redirect=${encodeURIComponent( + typeof window !== "undefined" ? window.location.href : "", + )}` + return ( +
+
+
+ + + +
+ You're not logged in + Log in to view and accept this invitation. +
+ router.push(loginHref)}> + Log in + +
+
+
+ ) + } + + if (state === "wrong_account") { + return ( + } + title="This invitation isn't for you" + description={`It was sent to a different email${ + session?.user?.email ? ` than ${session.user.email}` : "" + }.`} + actionLabel="Go to dashboard" + onAction={() => router.push("/")} + /> + ) + } + + if ( + state === "not_found" || + state === "expired" || + state === "already_accepted" + ) { + const copy = { + not_found: { + title: "Invitation not found", + body: "This invitation doesn't exist or has been revoked.", + }, + expired: { + title: "Invitation expired", + body: "Ask your team admin to send a new one.", + }, + already_accepted: { + title: "Already joined", + body: "You've already accepted this invitation.", + }, + }[state] + return ( + } + title={copy.title} + description={copy.body} + actionLabel="Go to dashboard" + onAction={() => router.push("/")} + /> + ) + } + + return ( +
+
+
+ + + +
+ {invitation?.organizationName} + + You've been invited to join{" "} + + {invitation?.organizationName} + {" "} + as {invitation?.role}. + + {invitation?.inviterEmail && ( +

+ Invited by {invitation.inviterEmail} +

+ )} + {session?.user?.email && ( +

+ Signed in as {session.user.email} +

+ )} +
+
+ + {accepting ? ( + <> + + Accepting… + + ) : ( + "Accept invitation" + )} + + + {declining ? ( + <> + + Declining… + + ) : ( + "Decline" + )} + +
+
+
+
+ ) +} diff --git a/apps/web/components/select-spaces-modal.tsx b/apps/web/components/select-spaces-modal.tsx index 7198d435..03b4ae76 100644 --- a/apps/web/components/select-spaces-modal.tsx +++ b/apps/web/components/select-spaces-modal.tsx @@ -5,6 +5,7 @@ import Image from "next/image" import { dmSans125ClassName, dmSansClassName } from "@/lib/fonts" import { Dialog, DialogContent, DialogTitle } from "@repo/ui/components/dialog" import { Drawer, DrawerContent, DrawerTitle } from "@repo/ui/components/drawer" +import { Avatar, AvatarFallback, AvatarImage } from "@ui/components/avatar" import { cn } from "@lib/utils" import { useIsMobile } from "@hooks/use-mobile" import * as DialogPrimitive from "@radix-ui/react-dialog" @@ -21,10 +22,11 @@ import { Loader, Pencil, Check, + Lock, } from "lucide-react" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { toast } from "sonner" -import { DEFAULT_PROJECT_ID } from "@lib/constants" +import { DEFAULT_PROJECT_ID, SHARED_TEAM_BRAIN_TAG } from "@lib/constants" import { authClient } from "@lib/auth" import { useAuth } from "@lib/auth-context" import type { ContainerTagListType } from "@lib/types" @@ -50,6 +52,7 @@ import { AUTO_CHAT_SPACE_ID } from "@/lib/chat-auto-space" import NovaOrb from "@/components/nova/nova-orb" import { AutoSpaceIcon } from "@/components/nova/auto-space-icon" import { SpaceGlyph } from "./space-glyph" +import { useHasCompanyBrain } from "@/hooks/use-company-brain" interface SelectSpacesModalProps { isOpen: boolean @@ -130,8 +133,15 @@ export function SelectSpacesModal({ ) const pluginMetaMap = usePluginSpaceMeta(pluginTags) + const hasCompanyBrain = useHasCompanyBrain() const allSpaces = useMemo(() => { + const rest = projects + .filter((p) => p.containerTag !== DEFAULT_PROJECT_ID) + .sort(compareSpacesUserFirst) + // Company brain orgs use real Private + Team Brain spaces; skip the + // synthetic "My Space" default that would otherwise duplicate Private. + if (hasCompanyBrain) return rest const defaultSpace = { id: "default", name: "My Space", @@ -142,11 +152,8 @@ export function SelectSpacesModal({ createdAt: "", updatedAt: "", } as ContainerTagListType - const rest = projects - .filter((p) => p.containerTag !== DEFAULT_PROJECT_ID) - .sort(compareSpacesUserFirst) return [defaultSpace, ...rest] - }, [projects]) + }, [projects, hasCompanyBrain]) const { categories, connectedCatalogIds } = useMemo<{ categories: Category[] @@ -588,6 +595,20 @@ export function SelectSpacesModal({ ) const isDefault = project.containerTag === DEFAULT_PROJECT_ID const isOwnSpace = isOwnConversationSpace(project, user?.id) + const isCbSpace = + hasCompanyBrain && !plugin && !isOwnSpace && !!project.visibility + const isShared = project.visibility === "public" + const orgName = org?.name ?? "your team" + const orgMembers = org?.members ?? [] + const memberCount = orgMembers.length + const isDefaultBrain = project.containerTag === SHARED_TEAM_BRAIN_TAG + const descriptor = isCbSpace + ? isShared + ? `${orgName} · ${memberCount} ${ + memberCount === 1 ? "member" : "members" + }` + : "Only you" + : null const canEdit = !isDefault && !plugin && !isOwnSpace const canBulkDelete = enableDelete && !isDefault const isEditing = editingProject?.containerTag === project.containerTag @@ -716,6 +737,54 @@ export function SelectSpacesModal({ ) ) : isOwnSpace ? ( + ) : isCbSpace ? ( + isShared ? ( + + {orgMembers.slice(0, 3).map((m, i) => ( + 0 && "-ml-2", + )} + > + + + {(m.user?.name ?? m.user?.email ?? "U") + .charAt(0) + .toUpperCase()} + + + ))} + {memberCount > 3 && ( + + +{memberCount - 3} + + )} + + ) : ( + + + + + {(user?.name ?? user?.email ?? "U") + .charAt(0) + .toUpperCase()} + + + + + + + ) ) : ( )} - - {plugin ? ( - <> - {plugin.label} - {pluginIdLabel && ( - - · {pluginIdLabel} - - )} - - ) : ( - displayName + + + {plugin ? ( + <> + {plugin.label} + {pluginIdLabel && ( + + · {pluginIdLabel} + + )} + + ) : ( + displayName + )} + + {descriptor && ( + + {descriptor} + )} + {isCbSpace && isDefaultBrain && ( + + Default + + )} )} {canEdit && !isEditing && !isBulkDeleteMode && ( @@ -787,6 +868,7 @@ export function SelectSpacesModal({ enableDelete, handleEditKeyDown, handleSelect, + hasCompanyBrain, isBulkDeleteMode, onDeleteRequest, pluginMetaMap, @@ -795,6 +877,11 @@ export function SelectSpacesModal({ toggleBulkDeleteTag, updateProjectMutation.isPending, user?.id, + org?.name, + org?.members, + user?.email, + user?.image, + user?.name, ], ) @@ -960,7 +1047,36 @@ export function SelectSpacesModal({ )} - {mainList.map(renderRow)} + {hasCompanyBrain && recentProjects.length === 0 + ? (() => { + const shared = mainList.filter( + (p) => p.visibility === "public", + ) + const personal = mainList.filter( + (p) => p.visibility !== "public", + ) + return ( + <> + {shared.length > 0 && ( + <> +
+ Shared +
+ {shared.map(renderRow)} + + )} + {personal.length > 0 && ( + <> +
+ Personal +
+ {personal.map(renderRow)} + + )} + + ) + })() + : mainList.map(renderRow)} )} diff --git a/apps/web/components/settings/account.tsx b/apps/web/components/settings/account.tsx index 33fd2175..cb38e85c 100644 --- a/apps/web/components/settings/account.tsx +++ b/apps/web/components/settings/account.tsx @@ -194,16 +194,18 @@ export default function Account() { () => org?.members?.find((member) => member.userId === user?.id) ?? null, [org?.members, user?.id], ) + // Only treat as a personal single-member org when members are actually loaded — + // otherwise default to least privilege (member), never owner. + const membersLoaded = Array.isArray(org?.members) const isSingleMemberPersonalOrg = + membersLoaded && (org?.members?.length ?? 0) <= 1 && (!org?.members?.[0]?.userId || org.members[0].userId === user?.id) - const currentRole = isSingleMemberPersonalOrg - ? "owner" - : ( - activeMemberRoleQuery.data ?? - currentMember?.role ?? - "member" - ).toLowerCase() + const currentRole = ( + activeMemberRoleQuery.data ?? + currentMember?.role ?? + (isSingleMemberPersonalOrg ? "owner" : "member") + ).toLowerCase() const canManageTeam = currentRole === "owner" || currentRole === "admin" const isOwner = currentRole === "owner" @@ -496,6 +498,14 @@ export default function Account() { > {org?.name ?? "Personal"}
+ + {currentRole} + {canManageTeam ? ( + )} + + ) +} + +function GithubMark({ className }: { className?: string }) { + return ( + + GitHub + + + ) +} + +function LinearMark({ className }: { className?: string }) { + return ( + + Linear + + + ) +} + +function SlackMark({ className }: { className?: string }) { + return ( + + ) +} diff --git a/apps/web/components/dashboard-view.tsx b/apps/web/components/dashboard-view.tsx index 6edbc1ca..b24e93fa 100644 --- a/apps/web/components/dashboard-view.tsx +++ b/apps/web/components/dashboard-view.tsx @@ -31,6 +31,7 @@ import { import { StaticGraphPreview } from "@/components/memory-graph/graph-card" import { Tooltip, TooltipContent, TooltipTrigger } from "@ui/components/tooltip" import { ChromeIcon, RaycastIcon } from "@/components/integration-icons" +import { SlackConnectCard } from "@/components/slack-connect-card" import { GoogleDrive, Notion, MCPIcon } from "@ui/assets/icons" import { analytics } from "@/lib/analytics" import type { IntegrationParamValue } from "@/lib/search-params" @@ -1331,6 +1332,7 @@ export function DashboardView({ )} >
+ {headerNotice ?
{headerNotice}
: null} {/* Header */} diff --git a/apps/web/components/onboarding-brain/shell.tsx b/apps/web/components/onboarding-brain/shell.tsx index c05b6a9a..f2eb9f8a 100644 --- a/apps/web/components/onboarding-brain/shell.tsx +++ b/apps/web/components/onboarding-brain/shell.tsx @@ -9,11 +9,12 @@ import { BRAIN_STEPS, BRAIN_STEP_LABELS, type BrainStep } from "./types" interface ShellProps { step: BrainStep domain?: string | null + steps?: BrainStep[] children: React.ReactNode } -export function BrainShell({ step, children }: ShellProps) { - const visibleSteps: BrainStep[] = BRAIN_STEPS +export function BrainShell({ step, steps, children }: ShellProps) { + const visibleSteps: BrainStep[] = steps ?? BRAIN_STEPS return (
void + allowTeam: boolean domain: string | null suggestedWorkspaceName: string defaultName: string @@ -58,6 +60,7 @@ const inputClass = export function StepAbout({ mode, onModeChange, + allowTeam, domain, suggestedWorkspaceName, defaultName, @@ -80,8 +83,11 @@ export function StepAbout({ if (Object.keys(patch).length > 0) onChange({ ...values, ...patch }) }, [defaultName, suggestedWorkspaceName, domain]) + const teamGated = mode === "team" && !allowTeam const canContinue = - values.name.trim().length > 0 && values.workspaceName.trim().length > 0 + !teamGated && + values.name.trim().length > 0 && + values.workspaceName.trim().length > 0 return (
@@ -146,7 +152,9 @@ export function StepAbout({
- {mode === "team" ? ( + {teamGated ? ( + onModeChange("personal")} /> + ) : mode === "team" ? ( @@ -358,6 +366,57 @@ function PersonalWorkspaceCard({ ) } +function TeamBetaGate({ onUsePersonal }: { onUsePersonal: () => void }) { + return ( +
+
+

+ Private beta +

+

+ Team workspaces are invite-only +

+

+ We're onboarding teams to Company Brain one at a time. Email us for + access — or start with a personal workspace and invite your team later. +

+
+ + + support@supermemory.com + + + +
+
+ ) +} + function ModeToggle({ mode, onChange, diff --git a/apps/web/components/onboarding-brain/step-ingest.tsx b/apps/web/components/onboarding-brain/step-ingest.tsx index 3f795157..b786a141 100644 --- a/apps/web/components/onboarding-brain/step-ingest.tsx +++ b/apps/web/components/onboarding-brain/step-ingest.tsx @@ -1,21 +1,89 @@ "use client" -import { useState, useEffect } from "react" +import { useEffect, useMemo, useState } from "react" +import type { ReactNode } from "react" import Image from "next/image" -import { useQueryState, parseAsString } from "nuqs" +import Link from "next/link" import { Button } from "@ui/components/button" -import { MCPIcon } from "@ui/assets/icons" -import { ArrowRight, Check, Copy, EyeOff, Eye } from "lucide-react" +import { + ArrowRight, + Check, + Copy, + ExternalLink, + Loader2, + Plug, +} from "lucide-react" import { cn } from "@lib/utils" -import { dmSans125ClassName, dmSansClassName } from "@/lib/fonts" +import { dmSans125ClassName } from "@/lib/fonts" import { toast } from "sonner" -import { MCPSteps } from "@/components/mcp-modal/mcp-detail-view" import { PLUGIN_CATALOG } from "@/lib/plugin-catalog" import { analytics } from "@/lib/analytics" +import type { BrainMode } from "./types" -interface Props { - mcpUrl: string - onContinue: () => void +const BACKEND = + process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" + +const TEST_PROMPT = "What do we know about [topic]?" + +type FlowToolId = "slack" | "mcp" | "codex" | "claude-code" +type FlowToolKind = "slack" | "mcp" | "plugin" + +type FlowTool = { + id: FlowToolId + label: string + blurb: string + kind: FlowToolKind + pluginId?: string + recommended?: boolean +} + +const TOOL_OPTIONS: Record = { + team: [ + { + id: "slack", + label: "Slack", + blurb: "Ask questions in-channel.", + kind: "slack", + recommended: true, + }, + { + id: "claude-code", + label: "Claude Code", + blurb: "Shared context in your terminal.", + kind: "plugin", + pluginId: "claude_code", + }, + { + id: "codex", + label: "Codex", + blurb: "OpenAI's coding agent.", + kind: "plugin", + pluginId: "codex", + }, + ], + personal: [ + { + id: "mcp", + label: "MCP", + blurb: "Use the universal URL in any client.", + kind: "mcp", + recommended: true, + }, + { + id: "codex", + label: "Codex", + blurb: "OpenAI's coding agent.", + kind: "plugin", + pluginId: "codex", + }, + { + id: "claude-code", + label: "Claude Code", + blurb: "Context in your terminal.", + kind: "plugin", + pluginId: "claude_code", + }, + ], } const modalCardStyle = { @@ -28,102 +96,24 @@ const inputBevelStyle = { "0px 1px 2px 0px rgba(0,43,87,0.1), inset 0px 0px 0px 1px rgba(43,49,67,0.08), inset 0px 1px 1px 0px rgba(0,0,0,0.08), inset 0px 2px 4px 0px rgba(0,0,0,0.02)", } -type AgentCategory = "coding" | "productivity" - -type Agent = { - key: string - name: string - tagline: string - category: AgentCategory - pluginId?: string +interface Props { + mode: BrainMode + mcpUrl: string + onContinue: () => void } -const AGENTS: Agent[] = [ - { - key: "cursor", - name: "Cursor", - tagline: "Persistent context across coding sessions.", - category: "coding", - }, - { - key: "claude-code", - name: "Claude Code", - tagline: "Memory and decisions across CLI sessions.", - category: "coding", - pluginId: "claude_code", - }, - { - key: "vscode", - name: "VS Code", - tagline: "Inline context while you write.", - category: "coding", - }, - { - key: "cline", - name: "Cline", - tagline: "Agentic dev tasks with your memory.", - category: "coding", - }, - { - key: "codex", - name: "Codex", - tagline: "OpenAI Codex with persistent memory.", - category: "coding", - pluginId: "codex", - }, - { - key: "gemini-cli", - name: "Gemini CLI", - tagline: "Gemini in your terminal, brain-aware.", - category: "coding", - }, - { - key: "claude", - name: "Claude Desktop", - tagline: "Memory across every Claude conversation.", - category: "productivity", - }, - { - key: "chatgpt", - name: "ChatGPT", - tagline: "Custom GPT backed by your brain.", - category: "productivity", - }, -] - -const CATEGORY_ORDER: { id: AgentCategory; label: string }[] = [ - { id: "coding", label: "Coding" }, - { id: "productivity", label: "Productivity" }, -] - -function agentIcon(agent: Agent) { - if (agent.pluginId) { - const plugin = PLUGIN_CATALOG[agent.pluginId] - if (plugin) return plugin.icon - } - const file = agent.key === "claude-code" ? "claude" : agent.key - return `/mcp-supported-tools/${file}.png` -} - -export function StepIngest({ mcpUrl, onContinue }: Props) { - const [activeCategory, setActiveCategory] = useState("coding") - const [selectedKey, setSelectedKey] = useState("cursor") - const [, setMcpClient] = useQueryState("mcpClient", parseAsString) - - const selectedAgent = AGENTS.find((a) => a.key === selectedKey) ?? AGENTS[0] +export function StepIngest({ mode, mcpUrl, onContinue }: Props) { + const tools = TOOL_OPTIONS[mode] + const [selected, setSelected] = useState(tools[0].id) useEffect(() => { - if (selectedAgent && !selectedAgent.pluginId) { - setMcpClient(selectedAgent.key) - } else { - setMcpClient(null) - } - }, [selectedAgent, setMcpClient]) + setSelected(tools[0].id) + }, [tools]) - const selectAgent = (agent: Agent) => { - analytics.onboardingAgentSelected({ agent: agent.key }) - setSelectedKey(agent.key) - } + const activeTool = useMemo( + () => tools.find((t) => t.id === selected) ?? tools[0], + [tools, selected], + ) const handleContinue = () => { analytics.onboardingIngestCompleted() @@ -135,79 +125,411 @@ export function StepIngest({ mcpUrl, onContinue }: Props) { onContinue() } - const filtered = AGENTS.filter((a) => a.category === activeCategory) - return ( -
-
-

- Use your brain anywhere -

-

- Now plug it into the tools you already use to write code, chat, think. -

-
+
+
+
+

+ Use your brain where you work +

+

+ {mode === "team" + ? "Pick where your team asks questions, then set it up." + : "Pick the tool you open every day — about 60 seconds."} +

+
- - -
- -
- {selectedAgent?.pluginId ? ( - - ) : ( - - )} -
+ {/* Right pane: setup detail */} +
+
+ +
+
+
+ +
+
+

+ Set up {activeTool.label} +

+

+ {activeTool.blurb} +

+
+
+ + {activeTool.kind === "slack" ? ( + + ) : activeTool.kind === "mcp" ? ( + + ) : activeTool.pluginId ? ( + + ) : null} +
+ +
+ + +
+
+
+
+
+ ) +} + +function ToolIcon({ id, className }: { id: FlowToolId; className?: string }) { + if (id === "slack") return + if (id === "mcp") + return + const file = id === "claude-code" ? "claude" : id + return ( + + ) +} + +function FlowToolRow({ + tool, + active, + onSelect, +}: { + tool: FlowTool + active: boolean + onSelect: () => void +}) { + return ( + - +
+
+

+ {tool.label} +

+ {tool.recommended && ( + + Recommended + + )} +
+

+ {tool.blurb} +

+
+ + + {active && } + + + ) +} + +function StepRow({ + index, + title, + done, + children, +}: { + index: number + title: ReactNode + done?: boolean + children?: ReactNode +}) { + return ( +
+ + {done ? : index} + +
+
+ {title} +
+ {children ?
{children}
: null}
) } -function McpHero({ url }: { url: string }) { +// Coding-agent plugins (Codex, Claude Code) auto-login via OAuth, so the +// "Save your API key" step is dropped — we render the remaining install steps. +function PluginSetup({ pluginId }: { pluginId: string }) { + const plugin = PLUGIN_CATALOG[pluginId] + const steps = (plugin?.installSteps ?? []).filter( + (s) => !s.secret && !s.code?.includes("sm_..."), + ) + + return ( +
+ {steps.map((step, i) => ( + + {step.description ? ( +

+ {step.description} +

+ ) : null} + {step.code ? : null} +
+ ))} + + + +
+ ) +} + +function McpGenericSetup({ mcpUrl }: { mcpUrl: string }) { + return ( +
+ + + + + + Per-client setup guides + + + + + + +
+ ) +} + +function SlackSetupPanel() { + const [status, setStatus] = useState<{ + connected: boolean + teamName: string | null + } | null>(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + let active = true + ;(async () => { + try { + const res = await fetch(`${BACKEND}/brain/slack/status`, { + credentials: "include", + }) + if (active && res.ok) { + setStatus( + (await res.json()) as { + connected: boolean + teamName: string | null + }, + ) + } + } finally { + if (active) setLoading(false) + } + })() + return () => { + active = false + } + }, []) + + const connected = status?.connected ?? false + + return ( +
+ + {!connected && + (loading ? ( + + + Checking… + + ) : ( + + ))} + + + Mention @supermemory in{" "} + #general + + } + /> + + + +
+ ) +} + +function CopyCodeBlock({ code }: { code: string }) { const [copied, setCopied] = useState(false) + + const copy = async () => { + try { + await navigator.clipboard.writeText(code) + setCopied(true) + toast.success("Copied") + setTimeout(() => setCopied(false), 1500) + } catch { + toast.error("Could not copy") + } + } + + return ( +
+
+				{code}
+			
+ +
+ ) +} + +function McpUrlRow({ url }: { url: string }) { + const [copied, setCopied] = useState(false) + const copy = async () => { try { await navigator.clipboard.writeText(url) @@ -220,36 +542,22 @@ function McpHero({ url }: { url: string }) { } return ( -
+
-
- -
-
-

+

Universal MCP URL

-

+

{url}

-
- ) -} - -function PluginSteps({ pluginId }: { pluginId: string }) { - const plugin = PLUGIN_CATALOG[pluginId] - if (!plugin) return null - const steps = plugin.installSteps ?? [] - return ( -
-
-
- {plugin.name} -
-
-

- Set up {plugin.name} -

-

- {plugin.tagline} -

-
- {plugin.docsUrl && ( - - Docs ↗ - - )} -
- -
- {steps.map((step, i) => ( - - ))} -
- -
-
- Your API key is minted in - Settings → Integrations → Plugins. Mint it once and paste into the - step above. -
-
) } -function PluginStep({ - idx, - step, -}: { - idx: number - step: import("@/lib/plugin-catalog").InstallStep -}) { - const [revealed, setRevealed] = useState(false) - const [copied, setCopied] = useState(false) - const copy = async () => { - if (!step.code) return - try { - await navigator.clipboard.writeText(step.code) - setCopied(true) - toast.success("Copied") - setTimeout(() => setCopied(false), 1500) - } catch { - toast.error("Could not copy") - } - } +function SlackMark({ className }: { className?: string }) { return ( -
-
-
- {idx} -
-
-
-

- {step.title} - {step.optional && ( - - Optional - - )} -

- {step.description && ( -

- {step.description} -

- )} - {step.code && ( -
-
-							{step.code}
-						
-
- {step.secret && ( - - )} - -
-
- )} -
-
- ) -} - -function CategoryTabs({ - value, - onChange, -}: { - value: AgentCategory - onChange: (c: AgentCategory) => void -}) { - const counts: Record = { - coding: 0, - productivity: 0, - } - for (const a of AGENTS) counts[a.category] += 1 - return ( -
- {CATEGORY_ORDER.map((cat) => { - const isActive = value === cat.id - return ( - - ) - })} -
- ) -} - -function AgentRow({ - agent, - active, - onClick, -}: { - agent: Agent - active: boolean - onClick: () => void -}) { - return ( - + ) } diff --git a/apps/web/components/settings/company-brain-connections.tsx b/apps/web/components/settings/company-brain-connections.tsx new file mode 100644 index 00000000..b9045e8b --- /dev/null +++ b/apps/web/components/settings/company-brain-connections.tsx @@ -0,0 +1,297 @@ +"use client" + +import { authClient } from "@lib/auth" +import { cn } from "@lib/utils" +import { useQuery } from "@tanstack/react-query" +import { Check, Loader2, Lock } from "lucide-react" +import { useCallback, useEffect, useState } from "react" +import { toast } from "sonner" +import { dmSans125ClassName } from "@/lib/fonts" +import { useHasCompanyBrain } from "@/hooks/use-company-brain" +import { PillButton } from "../integrations/install-steps" + +const BACKEND = + process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" + +type ConnRow = { toolkit: string; org: boolean; user: boolean } + +function GithubMark({ className }: { className?: string }) { + return ( + + GitHub + + + ) +} + +function LinearMark({ className }: { className?: string }) { + return ( + + Linear + + + ) +} + +const TOOLKITS: Record< + string, + { label: string; subtitle: string; icon: React.ReactNode } +> = { + github: { + label: "GitHub", + subtitle: "Repos, pull requests and issues", + icon: , + }, + linear: { + label: "Linear", + subtitle: "Issues, projects and cycles", + icon: , + }, +} + +function StatusDot({ connected }: { connected: boolean }) { + return ( + + + {connected ? "Connected" : "Not connected"} + + ) +} + +function AppCard({ + toolkit, + connected, + canConnect, + lockedHint, + busy, + onConnect, +}: { + toolkit: string + connected: boolean + canConnect: boolean + lockedHint?: string + busy: boolean + onConnect: () => void +}) { + const meta = TOOLKITS[toolkit] ?? { + label: toolkit, + subtitle: "", + icon: null, + } + return ( +
+
+
+ {meta.icon} +
+
+

+ {meta.label} +

+

+ {meta.subtitle} +

+
+
+
+ + {!connected && + (canConnect ? ( + + {busy && } + Connect + + ) : lockedHint ? ( + + + {lockedHint} + + ) : null)} +
+
+ ) +} + +function Section({ + title, + description, + children, +}: { + title: string + description: string + children: React.ReactNode +}) { + return ( +
+
+

+ {title} +

+

+ {description} +

+
+ {children} +
+ ) +} + +function CardSkeleton() { + return ( +
+
+
+
+
+
+
+ ) +} + +export default function CompanyBrainConnections() { + const isCompanyBrain = useHasCompanyBrain() + const [rows, setRows] = useState(null) + const [busy, setBusy] = useState(null) + + const roleQuery = useQuery({ + queryKey: ["company-brain-connections", "role"], + queryFn: async () => + (await authClient.organization.getActiveMember()).data?.role ?? null, + staleTime: 60_000, + enabled: isCompanyBrain, + }) + const role = (roleQuery.data ?? "").toLowerCase() + const isAdmin = role === "owner" || role === "admin" + + const load = useCallback(async () => { + const res = await fetch(`${BACKEND}/brain/connections`, { + credentials: "include", + }) + if (res.ok) + setRows(((await res.json()) as { toolkits: ConnRow[] }).toolkits) + }, []) + + useEffect(() => { + if (!isCompanyBrain) return + void load() + const onFocus = () => void load() + window.addEventListener("focus", onFocus) + return () => window.removeEventListener("focus", onFocus) + }, [isCompanyBrain, load]) + + const connect = async (toolkit: string, scope: "user" | "org") => { + setBusy(`${toolkit}:${scope}`) + try { + const res = await fetch( + `${BACKEND}/brain/connections/${toolkit}/link?scope=${scope}`, + { method: "POST", credentials: "include" }, + ) + if (res.status === 403) { + toast.error("Only admins can connect the shared org account.") + return + } + if (!res.ok) { + toast.error("Couldn't start the connection.") + return + } + const data = (await res.json()) as { url?: string; error?: string } + if (data.url) window.open(data.url, "_blank", "noopener") + else toast.error(data.error ?? "Couldn't start the connection.") + } catch { + toast.error("Couldn't start the connection.") + } finally { + setBusy(null) + } + } + + if (!isCompanyBrain) { + return ( +

+ Company Brain isn't enabled for this organization. +

+ ) + } + + const loading = rows === null + + return ( +
+
+ {loading ? ( + + ) : ( + rows.map((row) => ( + connect(row.toolkit, "org")} + /> + )) + )} +
+ +
+ {loading ? ( + + ) : ( + rows.map((row) => ( + connect(row.toolkit, "user")} + /> + )) + )} +
+
+ ) +} diff --git a/apps/web/components/settings/settings-content.tsx b/apps/web/components/settings/settings-content.tsx index a2ee63a4..578a88bd 100644 --- a/apps/web/components/settings/settings-content.tsx +++ b/apps/web/components/settings/settings-content.tsx @@ -10,6 +10,7 @@ import Account from "@/components/settings/account" import Billing from "@/components/settings/billing" import Integrations from "@/components/settings/integrations" import ConnectionsMCP from "@/components/settings/connections-mcp" +import CompanyBrainConnections from "@/components/settings/company-brain-connections" import Support from "@/components/settings/support" import { ErrorBoundary } from "@/components/error-boundary" import { useRouter } from "next/navigation" @@ -44,6 +45,7 @@ export const TABS = [ "billing", "integrations", "connections", + "company-brain", "support", ] as const export type SettingsTab = (typeof TABS)[number] @@ -80,6 +82,12 @@ const NAV_ITEMS: NavItem[] = [ description: "Drive, Notion, OneDrive, MCP", icon: , }, + { + id: "company-brain", + label: "Company Brain", + description: "GitHub & Linear — org and personal", + icon: , + }, { id: "support", label: "Support & Help", @@ -421,6 +429,7 @@ export function SettingsContent({ {activeTab === "billing" && } {activeTab === "integrations" && } {activeTab === "connections" && } + {activeTab === "company-brain" && } {activeTab === "support" && } diff --git a/apps/web/components/slack-connect-card.tsx b/apps/web/components/slack-connect-card.tsx new file mode 100644 index 00000000..2fc6de2c --- /dev/null +++ b/apps/web/components/slack-connect-card.tsx @@ -0,0 +1,106 @@ +"use client" + +import { useEffect, useState } from "react" +import { useHasCompanyBrain } from "@/hooks/use-company-brain" + +const BACKEND = + process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" + +type SlackStatus = { connected: boolean; teamName: string | null } + +function SlackMark({ className }: { className?: string }) { + return ( + + ) +} + +export function SlackConnectCard() { + const isCompanyBrain = useHasCompanyBrain() + const [status, setStatus] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + if (!isCompanyBrain) return + let active = true + ;(async () => { + try { + const res = await fetch(`${BACKEND}/brain/slack/status`, { + credentials: "include", + }) + if (active && res.ok) setStatus((await res.json()) as SlackStatus) + } finally { + if (active) setLoading(false) + } + })() + return () => { + active = false + } + }, [isCompanyBrain]) + + if (!isCompanyBrain || loading) return null + + const connected = status?.connected + + return ( +
+
+

+ Add Supermemory to your Slack +

+

+ {connected + ? `Connected to ${status?.teamName ?? "your workspace"}.` + : "Answer from your company brain and act on connected apps — right inside Slack."} +

+
+ {connected ? ( + + + Connected + + ) : ( + + + Add to Slack + + )} +
+ ) +} diff --git a/apps/web/components/space-selector.tsx b/apps/web/components/space-selector.tsx index f4af92b8..8bba3f38 100644 --- a/apps/web/components/space-selector.tsx +++ b/apps/web/components/space-selector.tsx @@ -61,8 +61,8 @@ export interface SpaceSelectorProps { const triggerVariants = { default: - "h-10 min-h-10 shrink-0 rounded-full border border-[#161F2C] bg-muted px-3 gap-2 " + - "hover:bg-white/5 hover:border-[#2261CA33] " + + "h-10 min-h-10 shrink-0 rounded-full bg-muted px-3 gap-2 " + + "hover:bg-white/5 " + "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#2261CA33]/35", insideOut: "h-10 min-h-10 gap-2 px-3 rounded-full bg-[#0D121A] shadow-inside-out hover:bg-[#121820]", diff --git a/packages/lib/posthog.tsx b/packages/lib/posthog.tsx index 540f2ab2..2d326976 100644 --- a/packages/lib/posthog.tsx +++ b/packages/lib/posthog.tsx @@ -2,6 +2,7 @@ import { usePathname, useSearchParams } from "next/navigation" import posthog from "posthog-js" +import { PostHogProvider as PHProvider } from "posthog-js/react" import { Suspense, useEffect } from "react" import { useSession } from "./auth" @@ -65,12 +66,12 @@ export function PostHogProvider({ children }: { children: React.ReactNode }) { }, [session?.user]) return ( - <> + {process.env.NODE_ENV === "production" && } {children} - + ) } From 32a6055f7c994bb43961d5242e808d1a09020e40 Mon Sep 17 00:00:00 2001 From: Mahesh Sanikommu Date: Tue, 23 Jun 2026 07:31:40 -0700 Subject: [PATCH 07/22] Fix integrations page double-refresh on load (#1134) Co-authored-by: Claude Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Vedant Mahajan --- apps/web/components/ensure-workspace.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/components/ensure-workspace.tsx b/apps/web/components/ensure-workspace.tsx index 73218af2..0d4a69bb 100644 --- a/apps/web/components/ensure-workspace.tsx +++ b/apps/web/components/ensure-workspace.tsx @@ -20,12 +20,12 @@ export function EnsureWorkspace({ children }: { children: React.ReactNode }) { const pathname = usePathname() const router = useRouter() const searchParams = useSearchParams() - const { session, organizations, isRestoring } = useAuth() + const { session, organizations, isRestoring, isSessionPending } = useAuth() const isPublicAppPage = pathname === "/" && ["integrations", "mcp"].includes(searchParams.get("view") ?? "") - const isGuestPublicAppPage = isPublicAppPage && !session + const isGuestPublicAppPage = isPublicAppPage && !session && !isSessionPending const isOnboarding = pathname.startsWith("/onboarding") useEffect(() => { From b3017eb121b8a633318e3343a9fd068056b1fc75 Mon Sep 17 00:00:00 2001 From: Oluwabusayo Jacobs <68024640+TropicolX@users.noreply.github.com> Date: Tue, 23 Jun 2026 18:00:59 +0100 Subject: [PATCH 08/22] chore(web): remove unused @lobbyside/react integration (#1156) Co-authored-by: Cursor --- apps/web/components/next-app-research-cta.tsx | 310 ------------------ apps/web/lib/analytics.ts | 7 - apps/web/package.json | 1 - bun.lock | 11 - 4 files changed, 329 deletions(-) delete mode 100644 apps/web/components/next-app-research-cta.tsx diff --git a/apps/web/components/next-app-research-cta.tsx b/apps/web/components/next-app-research-cta.tsx deleted file mode 100644 index e391c89e..00000000 --- a/apps/web/components/next-app-research-cta.tsx +++ /dev/null @@ -1,310 +0,0 @@ -"use client" - -import { useCallback, useEffect, useState } from "react" -import { usePathname } from "next/navigation" -import { Phone, Users, X as XIcon } from "lucide-react" -import { useLobbyside } from "@lobbyside/react" -import { useAuth } from "@lib/auth-context" -import { cn } from "@lib/utils" -import { dmSans125ClassName } from "@/lib/fonts" -import { analytics } from "@/lib/analytics" - -const STORAGE_KEY = "sm_next_app_research_cta_dismissed_v1" - -const BOOK_CALL_HREF = "https://cal.com/supermemory/growth" - -const LOBBYSIDE_WIDGET_ID = "e385c52f-4dd3-4fb2-81eb-da3a78059014" - -function ResearchCtaHeroGraphic({ - avatarUrl, - hostName, -}: { - avatarUrl?: string - hostName?: string -}) { - return ( -
-
-
-
-
-
-
-
- -
- - × - - {avatarUrl ? ( - - {hostName - - - ) : ( -
- -
- )} -
-
- ) -} - -export function NextAppResearchCta() { - const pathname = usePathname() - const [mounted, setMounted] = useState(false) - const [dismissed, setDismissed] = useState(false) - const widget = useLobbyside(LOBBYSIDE_WIDGET_ID) - const { user, org } = useAuth() - - useEffect(() => { - setMounted(true) - setDismissed(localStorage.getItem(STORAGE_KEY) === "1") - }, []) - - const handleDismiss = useCallback((e: React.MouseEvent) => { - e.stopPropagation() - localStorage.setItem(STORAGE_KEY, "1") - setDismissed(true) - analytics.nextAppResearchCtaDismissed() - }, []) - - const handleJoinCall = useCallback(async () => { - if (widget.status !== "online" || widget.isQueueFull) return - analytics.nextAppResearchCtaLobbysideCallClicked() - // Open the tab synchronously so Safari/iOS keep the user-activation - // gesture. We redirect it once joinCall() resolves, or fall back to - // the book-a-call URL if the host goes offline / queue fills / the - // request errors between render and click. - const pendingTab = window.open("", "_blank") - const navigate = (url: string) => { - if (pendingTab && !pendingTab.closed) { - pendingTab.location.href = url - } else { - window.open(url, "_blank", "noopener,noreferrer") - } - } - try { - const visitor: Record = {} - if (user?.email) visitor.email = user.email - if (user?.name) visitor.name = user.name - if (org?.name) visitor.company = org.name - const github = (user as { github?: unknown } | null)?.github - if (typeof github === "string" && github) visitor.github = github - const joinArgs = Object.keys(visitor).length > 0 ? { visitor } : undefined - const { entryUrl } = await widget.joinCall(joinArgs) - navigate(entryUrl) - } catch (err) { - console.error("[Lobbyside] joinCall failed", err) - navigate(BOOK_CALL_HREF) - } - }, [widget, user, org]) - - const handleCardKeyDown = useCallback( - (e: React.KeyboardEvent) => { - if (e.target !== e.currentTarget) return - if (e.key === "Enter" || e.key === " ") { - e.preventDefault() - handleJoinCall() - } - }, - [handleJoinCall], - ) - - const handleBookClick = useCallback(() => { - analytics.nextAppResearchCtaBookCallClicked() - }, []) - - if ( - !mounted || - dismissed || - pathname.startsWith("/onboarding") || - widget.status === "loading" - ) { - return null - } - - const cardBaseClasses = cn( - "fixed z-[45] bottom-4 left-4 min-w-[280px] max-w-[min(calc(100vw-2rem),22.5rem)]", - "rounded-xl border border-white/[0.08] bg-[#0D121A]/95 backdrop-blur-md", - "shadow-[0_8px_32px_rgba(0,0,0,0.35)] p-3.5", - ) - - if (widget.status !== "online" || widget.isQueueFull) { - return ( -
-
- -
-
-

- Be part of the next supermemory app -

- -
-

- Share what you want next. We’d love a quick call. -

- -
-
-
- ) - } - - return ( - -
-
-
-

- {widget.hostName} -

- {widget.hostTitle ? ( -

- {widget.hostTitle} -

- ) : null} -
- -
-
-
- - ) -} diff --git a/apps/web/lib/analytics.ts b/apps/web/lib/analytics.ts index ae78e306..eebd1697 100644 --- a/apps/web/lib/analytics.ts +++ b/apps/web/lib/analytics.ts @@ -94,13 +94,6 @@ export const analytics = { close_reason: "dismiss" | "close_button" | "im_good" | "action" }) => safeCapture("integration_info_modal_closed", props), - nextAppResearchCtaDismissed: () => - safeCapture("next_app_research_cta_dismissed"), - nextAppResearchCtaBookCallClicked: () => - safeCapture("next_app_research_cta_book_call_clicked"), - nextAppResearchCtaLobbysideCallClicked: () => - safeCapture("next_app_research_cta_lobbyside_call_clicked"), - mcpViewOpened: () => safeCapture("mcp_view_opened"), mcpInstallCmdCopied: () => safeCapture("mcp_install_cmd_copied"), diff --git a/apps/web/package.json b/apps/web/package.json index cd4e10b3..cddbcd0a 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -30,7 +30,6 @@ "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@floating-ui/react": "^0.27.0", - "@lobbyside/react": "0.2.0", "@opennextjs/cloudflare": "^1.12.0", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "^1.1.14", diff --git a/bun.lock b/bun.lock index 6672891e..c6f5c06e 100644 --- a/bun.lock +++ b/bun.lock @@ -140,7 +140,6 @@ "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@floating-ui/react": "^0.27.0", - "@lobbyside/react": "0.2.0", "@opennextjs/cloudflare": "^1.12.0", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "^1.1.14", @@ -981,10 +980,6 @@ "@inquirer/type": ["@inquirer/type@3.0.10", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="], - "@instantdb/core": ["@instantdb/core@1.0.15", "", { "dependencies": { "@instantdb/version": "1.0.15", "mutative": "^1.0.10", "uuid": "^11.1.0" } }, "sha512-1A4n47U0YLHKhvl0G+CiPGfBynq1cj+NqIVRhUMc/yHYT6rePeRWesxvevNiEJU2sLxOKML6Htcbtdh7jUjSQA=="], - - "@instantdb/version": ["@instantdb/version@1.0.15", "", {}, "sha512-xHDT23QK0tKAdxC2Z98mBx+znwnVShYWDsp0juY4xxzTGjrb37qNqRYb+6IIJc1J3GZ+xW/fCIexVK3b0B6fog=="], - "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], "@isaacs/ttlcache": ["@isaacs/ttlcache@2.1.4", "", {}, "sha512-7kMz0BJpMvgAMkyglums7B2vtrn5g0a0am77JY0GjkZZNetOBCFn7AG7gKCwT0QPiXyxW7YIQSgtARknUEOcxQ=="], @@ -1015,8 +1010,6 @@ "@levischuck/tiny-cbor": ["@levischuck/tiny-cbor@0.2.11", "", {}, "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow=="], - "@lobbyside/react": ["@lobbyside/react@0.2.0", "", { "peerDependencies": { "@instantdb/core": ">=1.0.0", "react": ">=18.0.0" } }, "sha512-24dTNDImAqZrlCu+vlPKulP1fvL3yTIc6qwxqBsqvit4z9WXnAdMRwB5Bvn31dTuaBokQEVZa3t0VfMnyhGvuw=="], - "@lukeed/csprng": ["@lukeed/csprng@1.1.0", "", {}, "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA=="], "@lukeed/uuid": ["@lukeed/uuid@2.0.1", "", { "dependencies": { "@lukeed/csprng": "^1.1.0" } }, "sha512-qC72D4+CDdjGqJvkFMMEAtancHUQ7/d/tAiHf64z8MopFDmcrtbcJuerDtFceuAfQJ2pDSfCKCtbqoGBNnwg0w=="], @@ -3877,8 +3870,6 @@ "multimatch": ["multimatch@6.0.0", "", { "dependencies": { "@types/minimatch": "^3.0.5", "array-differ": "^4.0.0", "array-union": "^3.0.1", "minimatch": "^3.0.4" } }, "sha512-I7tSVxHGPlmPN/enE3mS1aOSo6bWBfls+3HmuEeCUBCE7gWnm3cBXCBkpurzFjVRwC6Kld8lLaZ1Iv5vOcjvcQ=="], - "mutative": ["mutative@1.3.0", "", {}, "sha512-8MJj6URmOZAV70dpFe1YnSppRTKC4DsMkXQiBDFayLcDI4ljGokHxmpqaBQuDWa4iAxWaJJ1PS8vAmbntjjKmQ=="], - "mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="], "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], @@ -5241,8 +5232,6 @@ "@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], - "@instantdb/core/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], - "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], From 1e1b0b1a3733abe95eeb141c01ab9cd902610d24 Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Tue, 23 Jun 2026 20:42:29 +0000 Subject: [PATCH 09/22] feat(web): make /integrations a real route with connect deeplinks (#1155) - Promote integrations from ?view=integrations to real /integrations and nested /integrations/[card] routes; the page body is shared via AppExperience and useViewMode is path-aware. - Legacy ?view= URLs (and /settings/integrations) redirect to the new routes for back-compat; middleware/ensure-workspace allow the public routes. - Add ?connect= deeplink that opens a card's connect modal instantly with a loading state (e.g. Hermes API key). --- .../app/(app)/integrations/[card]/page.tsx | 13 + apps/web/app/(app)/integrations/page.tsx | 5 + apps/web/app/(app)/page.tsx | 879 +----------------- .../app/(app)/settings/integrations/page.tsx | 16 +- apps/web/app/(app)/settings/page.tsx | 2 +- apps/web/components/app-experience.tsx | 879 ++++++++++++++++++ apps/web/components/ensure-workspace.tsx | 6 +- apps/web/components/header.tsx | 4 +- apps/web/components/integrations-view.tsx | 163 +++- .../integrations/plugins-detail.tsx | 2 +- .../components/settings/connections-mcp.tsx | 2 +- .../components/settings/settings-content.tsx | 2 +- apps/web/lib/integration-routes.ts | 40 + apps/web/lib/view-mode-context.tsx | 64 +- apps/web/middleware.ts | 8 + 15 files changed, 1154 insertions(+), 931 deletions(-) create mode 100644 apps/web/app/(app)/integrations/[card]/page.tsx create mode 100644 apps/web/app/(app)/integrations/page.tsx create mode 100644 apps/web/components/app-experience.tsx create mode 100644 apps/web/lib/integration-routes.ts diff --git a/apps/web/app/(app)/integrations/[card]/page.tsx b/apps/web/app/(app)/integrations/[card]/page.tsx new file mode 100644 index 00000000..b8d28cf1 --- /dev/null +++ b/apps/web/app/(app)/integrations/[card]/page.tsx @@ -0,0 +1,13 @@ +import { notFound } from "next/navigation" +import { AppExperience } from "@/components/app-experience" +import { isIntegrationCard } from "@/lib/integration-routes" + +export default async function IntegrationCardPage({ + params, +}: { + params: Promise<{ card: string }> +}) { + const { card } = await params + if (!isIntegrationCard(card)) notFound() + return +} diff --git a/apps/web/app/(app)/integrations/page.tsx b/apps/web/app/(app)/integrations/page.tsx new file mode 100644 index 00000000..350ef97a --- /dev/null +++ b/apps/web/app/(app)/integrations/page.tsx @@ -0,0 +1,5 @@ +import { AppExperience } from "@/components/app-experience" + +export default function IntegrationsPage() { + return +} diff --git a/apps/web/app/(app)/page.tsx b/apps/web/app/(app)/page.tsx index 6e3a3603..240d355e 100644 --- a/apps/web/app/(app)/page.tsx +++ b/apps/web/app/(app)/page.tsx @@ -1,878 +1,5 @@ -"use client" +import { AppExperience } from "@/components/app-experience" -import { - useState, - useCallback, - useEffect, - useMemo, - useRef, - useSyncExternalStore, -} from "react" -import { AnimatePresence, motion } from "motion/react" -import { useQueryState } from "nuqs" -import { Header, PublicHeader } from "@/components/header" -import { MobileBottomNav } from "@/components/bottom-nav" -import { ChatSidebar, HomeChatComposer } from "@/components/chat" -import type { ChatAttachmentDraft } from "@/components/chat/attachments" -import { DashboardView } from "@/components/dashboard-view" -import { BrainHomeView } from "@/components/brain-home/brain-home-view" -import { useHasCompanyBrain } from "@/hooks/use-company-brain" -import { MemoriesGrid } from "@/components/memories-grid" -import { GraphLayoutView } from "@/components/graph-layout-view" -import { IntegrationsView, DetailWrapper } from "@/components/integrations-view" -import { MCPDetailView } from "@/components/mcp-modal/mcp-detail-view" -import { XBookmarksDetailView } from "@/components/onboarding/x-bookmarks-detail-view" -import { ChromeDetail } from "@/components/integrations/chrome-detail" -import { ShortcutsDetail } from "@/components/integrations/shortcuts-detail" -import { RaycastDetail } from "@/components/integrations/raycast-detail" -import { PluginsDetail } from "@/components/integrations/plugins-detail" -import { AnimatedGradientBackground } from "@/components/animated-gradient-background" -import { OnboardingConfetti } from "@/components/onboarding-brain/onboarding-confetti" -import { AddDocumentModal } from "@/components/add-document" -import { DocumentModal } from "@/components/document-modal" -import { DocumentsCommandPalette } from "@/components/documents-command-palette" -import { FullscreenNoteModal } from "@/components/fullscreen-note-modal" -import type { HighlightItem } from "@/components/highlights-card" -import { DigestsView } from "@/components/digests-view" -import { HotkeysProvider } from "react-hotkeys-hook" -import { useHotkeys } from "react-hotkeys-hook" -import { useIsMobile } from "@hooks/use-mobile" -import { useAuth } from "@lib/auth-context" -import { useProject } from "@/stores" -import { useContainerTags } from "@/hooks/use-container-tags" -import { DEFAULT_PROJECT_ID } from "@lib/constants" -import { - useQuickNoteDraftReset, - useQuickNoteDraft, -} from "@/stores/quick-note-draft" -import { analytics } from "@/lib/analytics" -import type { ModelId, ReasoningEffort } from "@/lib/models" -import { useDocumentMutations } from "@/hooks/use-document-mutations" -import { useQuery, useQueryClient } from "@tanstack/react-query" -import { toast } from "sonner" -import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api" -import type { z } from "zod" -import { useViewMode } from "@/lib/view-mode-context" -import type { MemoryOfDay } from "@/components/dashboard-view" -import { ErrorBoundary } from "@/components/error-boundary" -import { cn } from "@lib/utils" -import { - addDocumentParam, - searchParam, - qParam, - docParam, - fullscreenParam, - threadParam, - type IntegrationParamValue, -} from "@/lib/search-params" -import { getChatSpaceDisplayLabel } from "@/lib/chat-space-label" -import { getToolDocumentSpace } from "@/lib/plugin-space" - -type DocumentsResponse = z.infer -type DocumentWithMemories = DocumentsResponse["documents"][0] - -function subscribeViewportWidth(cb: () => void) { - window.addEventListener("resize", cb) - return () => window.removeEventListener("resize", cb) -} - -function getViewportWidth() { - return window.innerWidth -} - -const GRADIENT_TOP_WIDTH_MAX = 1440 - -function gradientTopPositionForWidth(width: number) { - const minW = 320 - const pctWide = 15 - const pctNarrow = 55 - const w = Math.min(GRADIENT_TOP_WIDTH_MAX, Math.max(minW, width)) - const t = (w - minW) / (GRADIENT_TOP_WIDTH_MAX - minW) - const eased = t * t - return `${Math.round(pctNarrow + eased * (pctWide - pctNarrow))}%` -} - -function ViewErrorFallback() { - return ( -
-

- Something went wrong.{" "} - -

-
- ) -} - -export default function NewPage() { - const isMobile = useIsMobile() - const { user, session, isSessionPending, org } = useAuth() - - const { selectedProject, selectedProjects, setSelectedProject } = useProject() - const selectedProjectTag = selectedProjects[0] - const { allProjects } = useContainerTags() - const dashboardSpaceLabel = useMemo( - () => - getChatSpaceDisplayLabel({ - selectedProject, - allProjects, - }), - [selectedProject, allProjects], - ) - const emptyStateSpaceName = selectedProjectTag - ? selectedProjectTag === DEFAULT_PROJECT_ID - ? "My Space" - : (allProjects.find((p) => p.containerTag === selectedProjectTag)?.name ?? - selectedProjectTag) - : undefined - - const { viewMode, setViewMode } = useViewMode() - const isCompanyBrain = useHasCompanyBrain() - - // Slack OAuth redirects back here with ?slack=connected — toast then clean up. - useEffect(() => { - const sp = new URLSearchParams(window.location.search) - if (sp.get("slack") !== "connected") return - const team = sp.get("team") - toast.success( - team - ? `Supermemory added to ${team} on Slack` - : "Supermemory added to your Slack", - ) - sp.delete("slack") - sp.delete("team") - const qs = sp.toString() - window.history.replaceState( - null, - "", - window.location.pathname + (qs ? `?${qs}` : ""), - ) - }, []) - const queryClient = useQueryClient() - const [highlightsForceAt, setHighlightsForceAt] = useState(0) - - // Chrome extension auth: send session token via postMessage so the content script can store it - useEffect(() => { - const url = new URL(window.location.href) - if (!url.searchParams.get("extension-auth-success")) return - const sessionToken = session?.token - const userData = { email: user?.email, name: user?.name, userId: user?.id } - if (sessionToken && userData.email) { - window.postMessage( - { token: encodeURIComponent(sessionToken), userData }, - window.location.origin, - ) - url.searchParams.delete("extension-auth-success") - window.history.replaceState({}, "", url.toString()) - } - }, [user, session]) - - // URL-driven modal states - const [addDoc, setAddDoc] = useQueryState("add", addDocumentParam) - const [isSearchOpen, setIsSearchOpen] = useQueryState("search", searchParam) - const [searchPrefill, setSearchPrefill] = useQueryState("q", qParam) - const [docId, setDocId] = useQueryState("doc", docParam) - const [isFullscreen, setIsFullscreen] = useQueryState( - "fullscreen", - fullscreenParam, - ) - const [, setThreadIdUrl] = useQueryState("thread", threadParam) - - // Ephemeral local state (not worth URL-encoding) - const [fullscreenInitialContent, setFullscreenInitialContent] = useState("") - const [queuedChatSeed, setQueuedChatSeed] = useState(null) - const [queuedChatModel, setQueuedChatModel] = useState(null) - const [queuedChatReasoningEffort, setQueuedChatReasoningEffort] = - useState(null) - const [queuedChatProject, setQueuedChatProject] = useState( - null, - ) - const [queuedChatAttachments, setQueuedChatAttachments] = useState< - ChatAttachmentDraft[] | null - >(null) - const [queuedHighlightContent, setQueuedHighlightContent] = useState< - string | null - >(null) - const [queuedMessageSource, setQueuedMessageSource] = useState< - "highlight" | "home" - >("highlight") - const [selectedDocument, setSelectedDocument] = - useState(null) - - // Clear document when docId is removed (e.g. back button) - useEffect(() => { - if (!docId) setSelectedDocument(null) - }, [docId]) - - useEffect(() => { - if (viewMode === "dashboard") void setThreadIdUrl(null) - }, [viewMode, setThreadIdUrl]) - - // Resolve document from cache when loading with ?doc= (deep link / refresh) - useEffect(() => { - if (!docId || selectedDocument) return - - const tryResolve = () => { - const queries = queryClient.getQueriesData<{ - pages: DocumentsResponse[] - }>({ queryKey: ["documents-with-memories"] }) - for (const [, data] of queries) { - if (!data?.pages) continue - for (const page of data.pages) { - const doc = page.documents?.find((d) => d.id === docId) - if (doc) { - setSelectedDocument(doc) - return true - } - } - } - return false - } - - if (tryResolve()) return - - const unsubscribe = queryClient.getQueryCache().subscribe(() => { - if (tryResolve()) unsubscribe() - }) - return unsubscribe - }, [docId, selectedDocument, queryClient]) - - const resetDraft = useQuickNoteDraftReset(selectedProject) - const { draft: quickNoteDraft } = useQuickNoteDraft(selectedProject || "") - const quickNoteDraftRef = useRef(quickNoteDraft) - quickNoteDraftRef.current = quickNoteDraft - - const { noteMutation, bulkDeleteMutation } = useDocumentMutations({ - onClose: () => { - resetDraft() - setIsFullscreen(false) - }, - }) - - const [selectedDocumentIds, setSelectedDocumentIds] = useState>( - new Set(), - ) - const [isSelectionMode, setIsSelectionMode] = useState(false) - - const handleToggleSelection = useCallback((documentId: string) => { - setSelectedDocumentIds((prev) => { - const next = new Set(prev) - if (next.has(documentId)) { - next.delete(documentId) - } else { - next.add(documentId) - } - return next - }) - }, []) - - const handleClearSelection = useCallback(() => { - setSelectedDocumentIds(new Set()) - setIsSelectionMode(false) - }, []) - - const handleEnterSelectionMode = useCallback(() => { - setIsSelectionMode(true) - }, []) - - const handleSelectAllVisible = useCallback((visibleIds: string[]) => { - setSelectedDocumentIds((prev) => { - const next = new Set(prev) - for (const id of visibleIds) { - next.add(id) - } - return next - }) - }, []) - - const handleBulkDelete = useCallback(() => { - const ids = Array.from(selectedDocumentIds) - if (ids.length === 0) return - bulkDeleteMutation.mutate( - { documentIds: ids }, - { - onSuccess: () => { - setSelectedDocumentIds(new Set()) - setIsSelectionMode(false) - if (selectedDocument && ids.includes(selectedDocument.id ?? "")) { - setDocId(null) - } - }, - }, - ) - }, [selectedDocumentIds, bulkDeleteMutation, selectedDocument, setDocId]) - - type SpaceHighlightsResponse = { - highlights: HighlightItem[] - questions: string[] - generatedAt: string - } - - const HIGHLIGHTS_CACHE_NAME = "space-highlights-v1" - const HIGHLIGHTS_MAX_AGE = 4 * 60 * 60 * 1000 // 4 hours - - const handleResetHighlights = useCallback(async () => { - toast.success("Refreshing daily brief…") - try { - await caches.delete(HIGHLIGHTS_CACHE_NAME) - } catch {} - setHighlightsForceAt(Date.now()) - }, []) - - const { data: highlightsData, isLoading: isLoadingHighlights } = - useQuery({ - queryKey: ["space-highlights", selectedProject, highlightsForceAt], - queryFn: async (): Promise => { - const spaceId = selectedProject || "sm_project_default" - const forceRefresh = highlightsForceAt > 0 - const cacheKey = `${process.env.NEXT_PUBLIC_BACKEND_URL}/v3/space-highlights?spaceId=${spaceId}` - - if (!forceRefresh) { - const cache = await caches.open(HIGHLIGHTS_CACHE_NAME) - const cached = await cache.match(cacheKey) - if (cached) { - const age = - Date.now() - Number(cached.headers.get("x-cached-at") || 0) - if (age < HIGHLIGHTS_MAX_AGE) { - return cached.json() - } - } - } - - const response = await fetch( - `${process.env.NEXT_PUBLIC_BACKEND_URL}/v3/space-highlights`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - credentials: "include", - body: JSON.stringify({ - spaceId, - highlightsCount: 3, - questionsCount: 4, - includeHighlights: true, - includeQuestions: true, - forceRefresh, - }), - }, - ) - - if (!response.ok) { - throw new Error("Failed to fetch space highlights") - } - - const data = await response.json() - - // Update browser cache with fresh data (works for both normal and forced refresh) - try { - const freshCache = await caches.open(HIGHLIGHTS_CACHE_NAME) - const cacheResponse = new Response(JSON.stringify(data), { - headers: { - "Content-Type": "application/json", - "x-cached-at": String(Date.now()), - }, - }) - await freshCache.put(cacheKey, cacheResponse) - } catch {} - - // Reset force flag after the forced fetch completes so future project-switches - // use the normal cache path instead of always bypassing it. - if (forceRefresh) setHighlightsForceAt(0) - - return data - }, - staleTime: HIGHLIGHTS_MAX_AGE, - refetchOnWindowFocus: false, - }) - - const { data: memoryOfDay = null } = useQuery({ - queryKey: [ - "memory-of-day", - user?.id, - org?.id, - new Date().toISOString().slice(0, 10), - ], - queryFn: async (): Promise => { - const cacheKey = `memory-of-day:v2:${user?.id}:${org?.id}:${new Date().toISOString().slice(0, 10)}` - try { - const stored = localStorage.getItem(cacheKey) - if (stored) return JSON.parse(stored) as MemoryOfDay - } catch {} - - const response = await fetch( - `${process.env.NEXT_PUBLIC_BACKEND_URL}/v3/memory-of-day`, - { credentials: "include" }, - ) - if (!response.ok) return null - const data = (await response.json()) as MemoryOfDay | null - if (data) { - try { - localStorage.setItem(cacheKey, JSON.stringify(data)) - } catch {} - } - return data - }, - staleTime: 24 * 60 * 60 * 1000, - refetchOnWindowFocus: false, - enabled: !!user && !!org, - }) - - useHotkeys("c", () => { - analytics.addDocumentModalOpened() - setAddDoc("note") - }) - useHotkeys("mod+k", (e) => { - e.preventDefault() - analytics.searchOpened({ source: "hotkey" }) - setIsSearchOpen(true) - }) - - const handleOpenDocument = useCallback( - (document: DocumentWithMemories) => { - if (document.id) { - analytics.documentModalOpened({ document_id: document.id }) - setSelectedDocument(document) - setDocId(document.id) - } - }, - [setDocId], - ) - - const handleOpenToolDocument = useCallback( - (document: DocumentWithMemories, pluginClientId: string) => { - const documentSpace = getToolDocumentSpace(document, pluginClientId) - if (documentSpace) { - setSelectedProject(documentSpace) - } - handleOpenDocument(document) - void setViewMode("list") - }, - [handleOpenDocument, setSelectedProject, setViewMode], - ) - - // Separate from handleOpenDocument because the graph view only has a document ID, - // not the full document object. The modal will fetch the document via the docId - // query param, so there may be a brief loading state (unlike handleOpenDocument - // which pre-populates via setSelectedDocument). - const handleOpenDocumentById = useCallback( - (documentId: string) => { - analytics.documentModalOpened({ document_id: documentId }) - setDocId(documentId) - }, - [setDocId], - ) - - const handleQuickNoteSave = useCallback( - (content: string) => { - if (content.trim()) { - const hadPreviousContent = quickNoteDraftRef.current.trim().length > 0 - noteMutation.mutate( - { content, project: selectedProject }, - { - onSuccess: () => { - if (hadPreviousContent) { - analytics.quickNoteEdited() - } else { - analytics.quickNoteCreated() - } - }, - }, - ) - } - }, - [selectedProject, noteMutation], - ) - - const handleFullScreenSave = useCallback( - (content: string) => { - if (content.trim()) { - const hadInitialContent = fullscreenInitialContent.trim().length > 0 - noteMutation.mutate( - { content, project: selectedProject }, - { - onSuccess: () => { - if (hadInitialContent) { - analytics.quickNoteEdited() - } else { - analytics.quickNoteCreated() - } - }, - }, - ) - } - }, - [selectedProject, noteMutation, fullscreenInitialContent], - ) - - const handleMaximize = useCallback( - (content: string) => { - analytics.fullscreenNoteModalOpened() - setFullscreenInitialContent(content) - setIsFullscreen(true) - }, - [setIsFullscreen], - ) - - const handleHighlightsChat = useCallback( - (highlightContent: string, userReply: string) => { - setQueuedHighlightContent(highlightContent) - setQueuedChatSeed(userReply) - setQueuedChatModel(null) - setQueuedChatReasoningEffort(null) - setQueuedChatProject(null) - setQueuedChatAttachments(null) - setQueuedMessageSource("highlight") - void setViewMode("chat") - }, - [setViewMode], - ) - - const handleHomeChatStart = useCallback( - ( - message: string, - model: ModelId, - projectId: string, - reasoningEffort: ReasoningEffort, - attachments?: ChatAttachmentDraft[], - ) => { - setQueuedHighlightContent(null) - setQueuedChatSeed(message) - setQueuedChatModel(model) - setQueuedChatReasoningEffort(reasoningEffort) - setQueuedChatProject(projectId) - setQueuedChatAttachments(attachments ?? null) - setQueuedMessageSource("home") - void setViewMode("chat") - }, - [setViewMode], - ) - - const consumeQueuedChat = useCallback(() => { - setQueuedChatSeed(null) - setQueuedChatModel(null) - setQueuedChatReasoningEffort(null) - setQueuedChatProject(null) - setQueuedChatAttachments(null) - setQueuedHighlightContent(null) - setQueuedMessageSource("highlight") - }, []) - - const handleHighlightsShowRelated = useCallback( - (query: string) => { - analytics.searchOpened({ source: "highlight_related" }) - setSearchPrefill(query) - setIsSearchOpen(true) - }, - [setSearchPrefill, setIsSearchOpen], - ) - - const handleOpenIntegrations = useCallback( - (integration?: IntegrationParamValue) => { - if (integration === "notion" || integration === "google-drive") { - void setAddDoc("connect") - return - } - void setViewMode(integration ?? "integrations") - }, - [setViewMode, setAddDoc], - ) - - const handleOpenPlugins = useCallback(() => { - void setViewMode("plugins") - }, [setViewMode]) - - const handleAddMemory = useCallback( - (tab: "note" | "link") => { - analytics.addDocumentModalOpened() - setAddDoc(tab) - }, - [setAddDoc], - ) - - const viewportWidth = useSyncExternalStore( - subscribeViewportWidth, - getViewportWidth, - () => GRADIENT_TOP_WIDTH_MAX, - ) - const gradientTopPosition = gradientTopPositionForWidth(viewportWidth) - - const isChatView = viewMode === "chat" - const showNovaBackdrop = - viewMode === "graph" || - viewMode === "list" || - viewMode === "dashboard" || - viewMode === "digests" - const isDashboardShell = - viewMode === "dashboard" || (viewMode === "graph" && isMobile) - const isGraphMode = viewMode === "graph" - const showBottomNav = isMobile && !!session && !isChatView - const isPublicIntegrations = - !session && !isSessionPending && viewMode === "integrations" - - return ( - - -
- {showNovaBackdrop && ( -
- -
-
-
- )} - {isPublicIntegrations ? ( - - ) : !session && viewMode === "mcp" ? ( - - ) : ( -
{ - analytics.addDocumentModalOpened() - setAddDoc("note") - }} - onOpenSearch={() => { - analytics.searchOpened({ source: "header" }) - setIsSearchOpen(true) - }} - /> - )} - - -
- }> - {isChatView ? ( -
- { - if (!open) void setViewMode("dashboard") - }} - queuedMessage={queuedChatSeed} - queuedHighlightContent={queuedHighlightContent} - onConsumeQueuedMessage={consumeQueuedChat} - queuedMessageSource={queuedMessageSource} - queuedAttachments={queuedChatAttachments} - initialSelectedModel={queuedChatModel} - initialReasoningEffort={queuedChatReasoningEffort} - initialChatProject={queuedChatProject} - /> -
- ) : viewMode === "integrations" ? ( -
- -
- ) : viewMode === "mcp" ? ( - void setViewMode("integrations")} - /> - ) : viewMode === "plugins" ? ( - void setViewMode("integrations")} - > - - - ) : viewMode === "chrome" ? ( - void setViewMode("integrations")} - > - - - ) : viewMode === "shortcuts" ? ( - void setViewMode("integrations")} - > - - - ) : viewMode === "raycast" ? ( - void setViewMode("integrations")} - > - - - ) : viewMode === "import" ? ( - void setViewMode("integrations")} - /> - ) : viewMode === "digests" ? ( -
- -
- ) : viewMode === "graph" ? ( -
- -
- ) : viewMode === "list" ? ( -
- -
- ) : isCompanyBrain ? ( -
- -
- ) : ( - { - analytics.searchOpened({ source: "header" }) - setIsSearchOpen(true) - }} - onOpenIntegrations={handleOpenIntegrations} - onOpenPlugins={handleOpenPlugins} - onNavigateToMemories={() => void setViewMode("list")} - onNavigateToGraph={() => void setViewMode("graph")} - onOpenDocument={handleOpenDocument} - onOpenToolDocument={handleOpenToolDocument} - onHighlightsChat={handleHighlightsChat} - onHighlightsShowRelated={handleHighlightsShowRelated} - onResetHighlights={handleResetHighlights} - onOpenDigests={() => void setViewMode("digests")} - memoryOfDay={memoryOfDay} - /> - )} -
-
-
-
- - {isDashboardShell && showBottomNav && ( -
- )} - {isDashboardShell && ( -
-
- -
-
- )} - - {showBottomNav && ( - { - analytics.addDocumentModalOpened() - setAddDoc("note") - }} - onOpenSearch={() => { - analytics.searchOpened({ source: "header" }) - setIsSearchOpen(true) - }} - /> - )} - - setAddDoc(null)} - /> - { - setIsSearchOpen(open) - if (!open) setSearchPrefill("") - }} - projectId={selectedProject} - onOpenDocument={handleOpenDocument} - onAddMemory={() => { - analytics.addDocumentModalOpened() - setAddDoc("note") - }} - onOpenIntegrations={() => setViewMode("integrations")} - initialSearch={searchPrefill} - /> - setDocId(null)} - /> - setIsFullscreen(false)} - initialContent={fullscreenInitialContent} - onSave={handleFullScreenSave} - isSaving={noteMutation.isPending} - /> -
- - ) +export default function Page() { + return } diff --git a/apps/web/app/(app)/settings/integrations/page.tsx b/apps/web/app/(app)/settings/integrations/page.tsx index aa8a456b..b128db96 100644 --- a/apps/web/app/(app)/settings/integrations/page.tsx +++ b/apps/web/app/(app)/settings/integrations/page.tsx @@ -1,17 +1,5 @@ -"use client" - -import { useEffect } from "react" -import { useRouter, useSearchParams } from "next/navigation" +import { redirect } from "next/navigation" export default function SettingsIntegrationsPage() { - const router = useRouter() - const searchParams = useSearchParams() - - useEffect(() => { - const params = new URLSearchParams(searchParams.toString()) - params.set("view", "integrations") - router.replace(`/?${params.toString()}`) - }, [router, searchParams]) - - return null + redirect("/integrations") } diff --git a/apps/web/app/(app)/settings/page.tsx b/apps/web/app/(app)/settings/page.tsx index 32b74126..657a4f60 100644 --- a/apps/web/app/(app)/settings/page.tsx +++ b/apps/web/app/(app)/settings/page.tsx @@ -15,7 +15,7 @@ export default function SettingsRedirect() { const hash = typeof window !== "undefined" ? window.location.hash : "" const tab = parseHashToTab(hash) router.replace( - tab === "integrations" ? "/?view=integrations" : `/?settings=${tab}`, + tab === "integrations" ? "/integrations" : `/?settings=${tab}`, ) }, [router]) diff --git a/apps/web/components/app-experience.tsx b/apps/web/components/app-experience.tsx new file mode 100644 index 00000000..4f017ca4 --- /dev/null +++ b/apps/web/components/app-experience.tsx @@ -0,0 +1,879 @@ +"use client" + +import { + useState, + useCallback, + useEffect, + useMemo, + useRef, + useSyncExternalStore, +} from "react" +import { AnimatePresence, motion } from "motion/react" +import { useQueryState } from "nuqs" +import { Header, PublicHeader } from "@/components/header" +import { MobileBottomNav } from "@/components/bottom-nav" +import { ChatSidebar, HomeChatComposer } from "@/components/chat" +import type { ChatAttachmentDraft } from "@/components/chat/attachments" +import { DashboardView } from "@/components/dashboard-view" +import { BrainHomeView } from "@/components/brain-home/brain-home-view" +import { useHasCompanyBrain } from "@/hooks/use-company-brain" +import { MemoriesGrid } from "@/components/memories-grid" +import { GraphLayoutView } from "@/components/graph-layout-view" +import { IntegrationsView, DetailWrapper } from "@/components/integrations-view" +import { MCPDetailView } from "@/components/mcp-modal/mcp-detail-view" +import { XBookmarksDetailView } from "@/components/onboarding/x-bookmarks-detail-view" +import { ChromeDetail } from "@/components/integrations/chrome-detail" +import { ShortcutsDetail } from "@/components/integrations/shortcuts-detail" +import { RaycastDetail } from "@/components/integrations/raycast-detail" +import { PluginsDetail } from "@/components/integrations/plugins-detail" +import { AnimatedGradientBackground } from "@/components/animated-gradient-background" +import { OnboardingConfetti } from "@/components/onboarding-brain/onboarding-confetti" +import { AddDocumentModal } from "@/components/add-document" +import { DocumentModal } from "@/components/document-modal" +import { DocumentsCommandPalette } from "@/components/documents-command-palette" +import { FullscreenNoteModal } from "@/components/fullscreen-note-modal" +import type { HighlightItem } from "@/components/highlights-card" +import { DigestsView } from "@/components/digests-view" +import { HotkeysProvider } from "react-hotkeys-hook" +import { useHotkeys } from "react-hotkeys-hook" +import { useIsMobile } from "@hooks/use-mobile" +import { useAuth } from "@lib/auth-context" +import { useProject } from "@/stores" +import { useContainerTags } from "@/hooks/use-container-tags" +import { DEFAULT_PROJECT_ID } from "@lib/constants" +import { + useQuickNoteDraftReset, + useQuickNoteDraft, +} from "@/stores/quick-note-draft" +import { analytics } from "@/lib/analytics" +import type { ModelId, ReasoningEffort } from "@/lib/models" +import { useDocumentMutations } from "@/hooks/use-document-mutations" +import { useQuery, useQueryClient } from "@tanstack/react-query" +import { toast } from "sonner" +import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api" +import type { z } from "zod" +import { useViewMode, useLegacyViewRedirect } from "@/lib/view-mode-context" +import type { MemoryOfDay } from "@/components/dashboard-view" +import { ErrorBoundary } from "@/components/error-boundary" +import { cn } from "@lib/utils" +import { + addDocumentParam, + searchParam, + qParam, + docParam, + fullscreenParam, + threadParam, + type IntegrationParamValue, +} from "@/lib/search-params" +import { getChatSpaceDisplayLabel } from "@/lib/chat-space-label" +import { getToolDocumentSpace } from "@/lib/plugin-space" + +type DocumentsResponse = z.infer +type DocumentWithMemories = DocumentsResponse["documents"][0] + +function subscribeViewportWidth(cb: () => void) { + window.addEventListener("resize", cb) + return () => window.removeEventListener("resize", cb) +} + +function getViewportWidth() { + return window.innerWidth +} + +const GRADIENT_TOP_WIDTH_MAX = 1440 + +function gradientTopPositionForWidth(width: number) { + const minW = 320 + const pctWide = 15 + const pctNarrow = 55 + const w = Math.min(GRADIENT_TOP_WIDTH_MAX, Math.max(minW, width)) + const t = (w - minW) / (GRADIENT_TOP_WIDTH_MAX - minW) + const eased = t * t + return `${Math.round(pctNarrow + eased * (pctWide - pctNarrow))}%` +} + +function ViewErrorFallback() { + return ( +
+

+ Something went wrong.{" "} + +

+
+ ) +} + +export function AppExperience() { + const isMobile = useIsMobile() + const { user, session, isSessionPending, org } = useAuth() + + const { selectedProject, selectedProjects, setSelectedProject } = useProject() + const selectedProjectTag = selectedProjects[0] + const { allProjects } = useContainerTags() + const dashboardSpaceLabel = useMemo( + () => + getChatSpaceDisplayLabel({ + selectedProject, + allProjects, + }), + [selectedProject, allProjects], + ) + const emptyStateSpaceName = selectedProjectTag + ? selectedProjectTag === DEFAULT_PROJECT_ID + ? "My Space" + : (allProjects.find((p) => p.containerTag === selectedProjectTag)?.name ?? + selectedProjectTag) + : undefined + + const { viewMode, setViewMode } = useViewMode() + useLegacyViewRedirect() + const isCompanyBrain = useHasCompanyBrain() + + // Slack OAuth redirects back here with ?slack=connected — toast then clean up. + useEffect(() => { + const sp = new URLSearchParams(window.location.search) + if (sp.get("slack") !== "connected") return + const team = sp.get("team") + toast.success( + team + ? `Supermemory added to ${team} on Slack` + : "Supermemory added to your Slack", + ) + sp.delete("slack") + sp.delete("team") + const qs = sp.toString() + window.history.replaceState( + null, + "", + window.location.pathname + (qs ? `?${qs}` : ""), + ) + }, []) + const queryClient = useQueryClient() + const [highlightsForceAt, setHighlightsForceAt] = useState(0) + + // Chrome extension auth: send session token via postMessage so the content script can store it + useEffect(() => { + const url = new URL(window.location.href) + if (!url.searchParams.get("extension-auth-success")) return + const sessionToken = session?.token + const userData = { email: user?.email, name: user?.name, userId: user?.id } + if (sessionToken && userData.email) { + window.postMessage( + { token: encodeURIComponent(sessionToken), userData }, + window.location.origin, + ) + url.searchParams.delete("extension-auth-success") + window.history.replaceState({}, "", url.toString()) + } + }, [user, session]) + + // URL-driven modal states + const [addDoc, setAddDoc] = useQueryState("add", addDocumentParam) + const [isSearchOpen, setIsSearchOpen] = useQueryState("search", searchParam) + const [searchPrefill, setSearchPrefill] = useQueryState("q", qParam) + const [docId, setDocId] = useQueryState("doc", docParam) + const [isFullscreen, setIsFullscreen] = useQueryState( + "fullscreen", + fullscreenParam, + ) + const [, setThreadIdUrl] = useQueryState("thread", threadParam) + + // Ephemeral local state (not worth URL-encoding) + const [fullscreenInitialContent, setFullscreenInitialContent] = useState("") + const [queuedChatSeed, setQueuedChatSeed] = useState(null) + const [queuedChatModel, setQueuedChatModel] = useState(null) + const [queuedChatReasoningEffort, setQueuedChatReasoningEffort] = + useState(null) + const [queuedChatProject, setQueuedChatProject] = useState( + null, + ) + const [queuedChatAttachments, setQueuedChatAttachments] = useState< + ChatAttachmentDraft[] | null + >(null) + const [queuedHighlightContent, setQueuedHighlightContent] = useState< + string | null + >(null) + const [queuedMessageSource, setQueuedMessageSource] = useState< + "highlight" | "home" + >("highlight") + const [selectedDocument, setSelectedDocument] = + useState(null) + + // Clear document when docId is removed (e.g. back button) + useEffect(() => { + if (!docId) setSelectedDocument(null) + }, [docId]) + + useEffect(() => { + if (viewMode === "dashboard") void setThreadIdUrl(null) + }, [viewMode, setThreadIdUrl]) + + // Resolve document from cache when loading with ?doc= (deep link / refresh) + useEffect(() => { + if (!docId || selectedDocument) return + + const tryResolve = () => { + const queries = queryClient.getQueriesData<{ + pages: DocumentsResponse[] + }>({ queryKey: ["documents-with-memories"] }) + for (const [, data] of queries) { + if (!data?.pages) continue + for (const page of data.pages) { + const doc = page.documents?.find((d) => d.id === docId) + if (doc) { + setSelectedDocument(doc) + return true + } + } + } + return false + } + + if (tryResolve()) return + + const unsubscribe = queryClient.getQueryCache().subscribe(() => { + if (tryResolve()) unsubscribe() + }) + return unsubscribe + }, [docId, selectedDocument, queryClient]) + + const resetDraft = useQuickNoteDraftReset(selectedProject) + const { draft: quickNoteDraft } = useQuickNoteDraft(selectedProject || "") + const quickNoteDraftRef = useRef(quickNoteDraft) + quickNoteDraftRef.current = quickNoteDraft + + const { noteMutation, bulkDeleteMutation } = useDocumentMutations({ + onClose: () => { + resetDraft() + setIsFullscreen(false) + }, + }) + + const [selectedDocumentIds, setSelectedDocumentIds] = useState>( + new Set(), + ) + const [isSelectionMode, setIsSelectionMode] = useState(false) + + const handleToggleSelection = useCallback((documentId: string) => { + setSelectedDocumentIds((prev) => { + const next = new Set(prev) + if (next.has(documentId)) { + next.delete(documentId) + } else { + next.add(documentId) + } + return next + }) + }, []) + + const handleClearSelection = useCallback(() => { + setSelectedDocumentIds(new Set()) + setIsSelectionMode(false) + }, []) + + const handleEnterSelectionMode = useCallback(() => { + setIsSelectionMode(true) + }, []) + + const handleSelectAllVisible = useCallback((visibleIds: string[]) => { + setSelectedDocumentIds((prev) => { + const next = new Set(prev) + for (const id of visibleIds) { + next.add(id) + } + return next + }) + }, []) + + const handleBulkDelete = useCallback(() => { + const ids = Array.from(selectedDocumentIds) + if (ids.length === 0) return + bulkDeleteMutation.mutate( + { documentIds: ids }, + { + onSuccess: () => { + setSelectedDocumentIds(new Set()) + setIsSelectionMode(false) + if (selectedDocument && ids.includes(selectedDocument.id ?? "")) { + setDocId(null) + } + }, + }, + ) + }, [selectedDocumentIds, bulkDeleteMutation, selectedDocument, setDocId]) + + type SpaceHighlightsResponse = { + highlights: HighlightItem[] + questions: string[] + generatedAt: string + } + + const HIGHLIGHTS_CACHE_NAME = "space-highlights-v1" + const HIGHLIGHTS_MAX_AGE = 4 * 60 * 60 * 1000 // 4 hours + + const handleResetHighlights = useCallback(async () => { + toast.success("Refreshing daily brief…") + try { + await caches.delete(HIGHLIGHTS_CACHE_NAME) + } catch {} + setHighlightsForceAt(Date.now()) + }, []) + + const { data: highlightsData, isLoading: isLoadingHighlights } = + useQuery({ + queryKey: ["space-highlights", selectedProject, highlightsForceAt], + queryFn: async (): Promise => { + const spaceId = selectedProject || "sm_project_default" + const forceRefresh = highlightsForceAt > 0 + const cacheKey = `${process.env.NEXT_PUBLIC_BACKEND_URL}/v3/space-highlights?spaceId=${spaceId}` + + if (!forceRefresh) { + const cache = await caches.open(HIGHLIGHTS_CACHE_NAME) + const cached = await cache.match(cacheKey) + if (cached) { + const age = + Date.now() - Number(cached.headers.get("x-cached-at") || 0) + if (age < HIGHLIGHTS_MAX_AGE) { + return cached.json() + } + } + } + + const response = await fetch( + `${process.env.NEXT_PUBLIC_BACKEND_URL}/v3/space-highlights`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify({ + spaceId, + highlightsCount: 3, + questionsCount: 4, + includeHighlights: true, + includeQuestions: true, + forceRefresh, + }), + }, + ) + + if (!response.ok) { + throw new Error("Failed to fetch space highlights") + } + + const data = await response.json() + + // Update browser cache with fresh data (works for both normal and forced refresh) + try { + const freshCache = await caches.open(HIGHLIGHTS_CACHE_NAME) + const cacheResponse = new Response(JSON.stringify(data), { + headers: { + "Content-Type": "application/json", + "x-cached-at": String(Date.now()), + }, + }) + await freshCache.put(cacheKey, cacheResponse) + } catch {} + + // Reset force flag after the forced fetch completes so future project-switches + // use the normal cache path instead of always bypassing it. + if (forceRefresh) setHighlightsForceAt(0) + + return data + }, + staleTime: HIGHLIGHTS_MAX_AGE, + refetchOnWindowFocus: false, + }) + + const { data: memoryOfDay = null } = useQuery({ + queryKey: [ + "memory-of-day", + user?.id, + org?.id, + new Date().toISOString().slice(0, 10), + ], + queryFn: async (): Promise => { + const cacheKey = `memory-of-day:v2:${user?.id}:${org?.id}:${new Date().toISOString().slice(0, 10)}` + try { + const stored = localStorage.getItem(cacheKey) + if (stored) return JSON.parse(stored) as MemoryOfDay + } catch {} + + const response = await fetch( + `${process.env.NEXT_PUBLIC_BACKEND_URL}/v3/memory-of-day`, + { credentials: "include" }, + ) + if (!response.ok) return null + const data = (await response.json()) as MemoryOfDay | null + if (data) { + try { + localStorage.setItem(cacheKey, JSON.stringify(data)) + } catch {} + } + return data + }, + staleTime: 24 * 60 * 60 * 1000, + refetchOnWindowFocus: false, + enabled: !!user && !!org, + }) + + useHotkeys("c", () => { + analytics.addDocumentModalOpened() + setAddDoc("note") + }) + useHotkeys("mod+k", (e) => { + e.preventDefault() + analytics.searchOpened({ source: "hotkey" }) + setIsSearchOpen(true) + }) + + const handleOpenDocument = useCallback( + (document: DocumentWithMemories) => { + if (document.id) { + analytics.documentModalOpened({ document_id: document.id }) + setSelectedDocument(document) + setDocId(document.id) + } + }, + [setDocId], + ) + + const handleOpenToolDocument = useCallback( + (document: DocumentWithMemories, pluginClientId: string) => { + const documentSpace = getToolDocumentSpace(document, pluginClientId) + if (documentSpace) { + setSelectedProject(documentSpace) + } + handleOpenDocument(document) + void setViewMode("list") + }, + [handleOpenDocument, setSelectedProject, setViewMode], + ) + + // Separate from handleOpenDocument because the graph view only has a document ID, + // not the full document object. The modal will fetch the document via the docId + // query param, so there may be a brief loading state (unlike handleOpenDocument + // which pre-populates via setSelectedDocument). + const handleOpenDocumentById = useCallback( + (documentId: string) => { + analytics.documentModalOpened({ document_id: documentId }) + setDocId(documentId) + }, + [setDocId], + ) + + const handleQuickNoteSave = useCallback( + (content: string) => { + if (content.trim()) { + const hadPreviousContent = quickNoteDraftRef.current.trim().length > 0 + noteMutation.mutate( + { content, project: selectedProject }, + { + onSuccess: () => { + if (hadPreviousContent) { + analytics.quickNoteEdited() + } else { + analytics.quickNoteCreated() + } + }, + }, + ) + } + }, + [selectedProject, noteMutation], + ) + + const handleFullScreenSave = useCallback( + (content: string) => { + if (content.trim()) { + const hadInitialContent = fullscreenInitialContent.trim().length > 0 + noteMutation.mutate( + { content, project: selectedProject }, + { + onSuccess: () => { + if (hadInitialContent) { + analytics.quickNoteEdited() + } else { + analytics.quickNoteCreated() + } + }, + }, + ) + } + }, + [selectedProject, noteMutation, fullscreenInitialContent], + ) + + const handleMaximize = useCallback( + (content: string) => { + analytics.fullscreenNoteModalOpened() + setFullscreenInitialContent(content) + setIsFullscreen(true) + }, + [setIsFullscreen], + ) + + const handleHighlightsChat = useCallback( + (highlightContent: string, userReply: string) => { + setQueuedHighlightContent(highlightContent) + setQueuedChatSeed(userReply) + setQueuedChatModel(null) + setQueuedChatReasoningEffort(null) + setQueuedChatProject(null) + setQueuedChatAttachments(null) + setQueuedMessageSource("highlight") + void setViewMode("chat") + }, + [setViewMode], + ) + + const handleHomeChatStart = useCallback( + ( + message: string, + model: ModelId, + projectId: string, + reasoningEffort: ReasoningEffort, + attachments?: ChatAttachmentDraft[], + ) => { + setQueuedHighlightContent(null) + setQueuedChatSeed(message) + setQueuedChatModel(model) + setQueuedChatReasoningEffort(reasoningEffort) + setQueuedChatProject(projectId) + setQueuedChatAttachments(attachments ?? null) + setQueuedMessageSource("home") + void setViewMode("chat") + }, + [setViewMode], + ) + + const consumeQueuedChat = useCallback(() => { + setQueuedChatSeed(null) + setQueuedChatModel(null) + setQueuedChatReasoningEffort(null) + setQueuedChatProject(null) + setQueuedChatAttachments(null) + setQueuedHighlightContent(null) + setQueuedMessageSource("highlight") + }, []) + + const handleHighlightsShowRelated = useCallback( + (query: string) => { + analytics.searchOpened({ source: "highlight_related" }) + setSearchPrefill(query) + setIsSearchOpen(true) + }, + [setSearchPrefill, setIsSearchOpen], + ) + + const handleOpenIntegrations = useCallback( + (integration?: IntegrationParamValue) => { + if (integration === "notion" || integration === "google-drive") { + void setAddDoc("connect") + return + } + void setViewMode(integration ?? "integrations") + }, + [setViewMode, setAddDoc], + ) + + const handleOpenPlugins = useCallback(() => { + void setViewMode("plugins") + }, [setViewMode]) + + const handleAddMemory = useCallback( + (tab: "note" | "link") => { + analytics.addDocumentModalOpened() + setAddDoc(tab) + }, + [setAddDoc], + ) + + const viewportWidth = useSyncExternalStore( + subscribeViewportWidth, + getViewportWidth, + () => GRADIENT_TOP_WIDTH_MAX, + ) + const gradientTopPosition = gradientTopPositionForWidth(viewportWidth) + + const isChatView = viewMode === "chat" + const showNovaBackdrop = + viewMode === "graph" || + viewMode === "list" || + viewMode === "dashboard" || + viewMode === "digests" + const isDashboardShell = + viewMode === "dashboard" || (viewMode === "graph" && isMobile) + const isGraphMode = viewMode === "graph" + const showBottomNav = isMobile && !!session && !isChatView + const isPublicIntegrations = + !session && !isSessionPending && viewMode === "integrations" + + return ( + + +
+ {showNovaBackdrop && ( +
+ +
+
+
+ )} + {isPublicIntegrations ? ( + + ) : !session && viewMode === "mcp" ? ( + + ) : ( +
{ + analytics.addDocumentModalOpened() + setAddDoc("note") + }} + onOpenSearch={() => { + analytics.searchOpened({ source: "header" }) + setIsSearchOpen(true) + }} + /> + )} + + +
+ }> + {isChatView ? ( +
+ { + if (!open) void setViewMode("dashboard") + }} + queuedMessage={queuedChatSeed} + queuedHighlightContent={queuedHighlightContent} + onConsumeQueuedMessage={consumeQueuedChat} + queuedMessageSource={queuedMessageSource} + queuedAttachments={queuedChatAttachments} + initialSelectedModel={queuedChatModel} + initialReasoningEffort={queuedChatReasoningEffort} + initialChatProject={queuedChatProject} + /> +
+ ) : viewMode === "integrations" ? ( +
+ +
+ ) : viewMode === "mcp" ? ( + void setViewMode("integrations")} + /> + ) : viewMode === "plugins" ? ( + void setViewMode("integrations")} + > + + + ) : viewMode === "chrome" ? ( + void setViewMode("integrations")} + > + + + ) : viewMode === "shortcuts" ? ( + void setViewMode("integrations")} + > + + + ) : viewMode === "raycast" ? ( + void setViewMode("integrations")} + > + + + ) : viewMode === "import" ? ( + void setViewMode("integrations")} + /> + ) : viewMode === "digests" ? ( +
+ +
+ ) : viewMode === "graph" ? ( +
+ +
+ ) : viewMode === "list" ? ( +
+ +
+ ) : isCompanyBrain ? ( +
+ +
+ ) : ( + { + analytics.searchOpened({ source: "header" }) + setIsSearchOpen(true) + }} + onOpenIntegrations={handleOpenIntegrations} + onOpenPlugins={handleOpenPlugins} + onNavigateToMemories={() => void setViewMode("list")} + onNavigateToGraph={() => void setViewMode("graph")} + onOpenDocument={handleOpenDocument} + onOpenToolDocument={handleOpenToolDocument} + onHighlightsChat={handleHighlightsChat} + onHighlightsShowRelated={handleHighlightsShowRelated} + onResetHighlights={handleResetHighlights} + onOpenDigests={() => void setViewMode("digests")} + memoryOfDay={memoryOfDay} + /> + )} +
+
+
+
+ + {isDashboardShell && showBottomNav && ( +
+ )} + {isDashboardShell && ( +
+
+ +
+
+ )} + + {showBottomNav && ( + { + analytics.addDocumentModalOpened() + setAddDoc("note") + }} + onOpenSearch={() => { + analytics.searchOpened({ source: "header" }) + setIsSearchOpen(true) + }} + /> + )} + + setAddDoc(null)} + /> + { + setIsSearchOpen(open) + if (!open) setSearchPrefill("") + }} + projectId={selectedProject} + onOpenDocument={handleOpenDocument} + onAddMemory={() => { + analytics.addDocumentModalOpened() + setAddDoc("note") + }} + onOpenIntegrations={() => setViewMode("integrations")} + initialSearch={searchPrefill} + /> + setDocId(null)} + /> + setIsFullscreen(false)} + initialContent={fullscreenInitialContent} + onSave={handleFullScreenSave} + isSaving={noteMutation.isPending} + /> +
+ + ) +} diff --git a/apps/web/components/ensure-workspace.tsx b/apps/web/components/ensure-workspace.tsx index 0d4a69bb..675edff4 100644 --- a/apps/web/components/ensure-workspace.tsx +++ b/apps/web/components/ensure-workspace.tsx @@ -23,8 +23,10 @@ export function EnsureWorkspace({ children }: { children: React.ReactNode }) { const { session, organizations, isRestoring, isSessionPending } = useAuth() const isPublicAppPage = - pathname === "/" && - ["integrations", "mcp"].includes(searchParams.get("view") ?? "") + pathname === "/integrations" || + pathname === "/integrations/mcp" || + (pathname === "/" && + ["integrations", "mcp"].includes(searchParams.get("view") ?? "")) const isGuestPublicAppPage = isPublicAppPage && !session && !isSessionPending const isOnboarding = pathname.startsWith("/onboarding") diff --git a/apps/web/components/header.tsx b/apps/web/components/header.tsx index 8abe42ab..3d977209 100644 --- a/apps/web/components/header.tsx +++ b/apps/web/components/header.tsx @@ -514,7 +514,7 @@ export function PublicHeader({ return (
@@ -523,7 +523,7 @@ export function PublicHeader({

- +
) diff --git a/apps/web/components/integrations/plugins-detail.tsx b/apps/web/components/integrations/plugins-detail.tsx index 0bd044ae..2f28e791 100644 --- a/apps/web/components/integrations/plugins-detail.tsx +++ b/apps/web/components/integrations/plugins-detail.tsx @@ -613,7 +613,7 @@ export function PluginsDetail() { try { const result = await autumn.attach({ planId: "api_pro", - successUrl: `${window.location.origin}/?view=integrations`, + successUrl: `${window.location.origin}/integrations`, }) if (result?.paymentUrl) { window.open(result.paymentUrl, "_self") diff --git a/apps/web/components/settings/connections-mcp.tsx b/apps/web/components/settings/connections-mcp.tsx index 43bbcee2..ccd098d6 100644 --- a/apps/web/components/settings/connections-mcp.tsx +++ b/apps/web/components/settings/connections-mcp.tsx @@ -710,7 +710,7 @@ export default function ConnectionsMCP() {

router.push("/?view=integrations&cat=ai-clients")} + onClick={() => router.push("/integrations?cat=ai-clients")} > diff --git a/apps/web/components/settings/settings-content.tsx b/apps/web/components/settings/settings-content.tsx index 578a88bd..a257c223 100644 --- a/apps/web/components/settings/settings-content.tsx +++ b/apps/web/components/settings/settings-content.tsx @@ -195,7 +195,7 @@ export function SettingsContent({ } const handleIntegrations = () => { - void router.push("/?view=integrations") + void router.push("/integrations") } const handleDeleteAccount = async () => { diff --git a/apps/web/lib/integration-routes.ts b/apps/web/lib/integration-routes.ts new file mode 100644 index 00000000..f525da55 --- /dev/null +++ b/apps/web/lib/integration-routes.ts @@ -0,0 +1,40 @@ +import type { ViewParamValue } from "@/lib/search-params" + +// Integration-family views that live under the real /integrations route. +export const INTEGRATION_VIEWS = [ + "integrations", + "mcp", + "plugins", + "chrome", + "connections", + "shortcuts", + "raycast", + "import", +] as const + +export type IntegrationView = (typeof INTEGRATION_VIEWS)[number] + +// Sub-view cards — each is a nested route segment under /integrations. +export const INTEGRATION_CARDS = INTEGRATION_VIEWS.filter( + (v) => v !== "integrations", +) as Exclude[] + +export function isIntegrationView(view: string): view is IntegrationView { + return (INTEGRATION_VIEWS as readonly string[]).includes(view) +} + +export function isIntegrationCard(slug: string): slug is IntegrationView { + return (INTEGRATION_CARDS as readonly string[]).includes(slug) +} + +export function integrationViewToPath(view: IntegrationView): string { + return view === "integrations" ? "/integrations" : `/integrations/${view}` +} + +export function pathToIntegrationView(pathname: string): ViewParamValue | null { + const trimmed = pathname.replace(/\/$/, "") + if (trimmed === "/integrations") return "integrations" + const slug = trimmed.match(/^\/integrations\/([^/]+)$/)?.[1] + if (slug && isIntegrationCard(slug)) return slug + return null +} diff --git a/apps/web/lib/view-mode-context.tsx b/apps/web/lib/view-mode-context.tsx index e797b47a..d26fc731 100644 --- a/apps/web/lib/view-mode-context.tsx +++ b/apps/web/lib/view-mode-context.tsx @@ -1,24 +1,76 @@ "use client" import { useQueryState } from "nuqs" +import { usePathname, useRouter, useSearchParams } from "next/navigation" import { viewParam, type ViewParamValue } from "@/lib/search-params" +import { + integrationViewToPath, + isIntegrationView, + pathToIntegrationView, +} from "@/lib/integration-routes" import { analytics } from "@/lib/analytics" -import { useCallback } from "react" +import { useCallback, useEffect } from "react" export type ViewMode = ViewParamValue -type SetViewMode = (value: ViewMode | null) => Promise +const TRACKED_VIEW_MODES = [ + "dashboard", + "graph", + "list", + "integrations", + "chat", + "digests", +] as const + +function isTrackedViewMode( + mode: ViewMode, +): mode is (typeof TRACKED_VIEW_MODES)[number] { + return (TRACKED_VIEW_MODES as readonly string[]).includes(mode) +} export function useViewMode() { - const [viewMode, _setViewMode] = useQueryState("view", viewParam) + const pathname = usePathname() + const router = useRouter() + const [paramView, setParamView] = useQueryState("view", viewParam) + + // On /integrations[/card] the path is the source of truth; elsewhere the ?view param is. + const pathView = pathToIntegrationView(pathname) + const viewMode: ViewMode = pathView ?? paramView const setViewMode = useCallback( (mode: ViewMode) => { - analytics.viewModeChanged(mode) - ;(_setViewMode as SetViewMode)(mode) + if (isTrackedViewMode(mode)) analytics.viewModeChanged(mode) + if (isIntegrationView(mode)) { + router.push(integrationViewToPath(mode)) + return + } + // Leaving (or already off) the integrations route for a non-integration view. + if (pathToIntegrationView(pathname)) { + router.push(mode === "dashboard" ? "/" : `/?view=${mode}`) + return + } + void setParamView(mode) }, - [_setViewMode], + [router, pathname, setParamView], ) return { viewMode, setViewMode, isInitialized: true } } + +// Forwards legacy /?view=integrations (and sub-views) to the canonical /integrations route, +// preserving any other query params. Call once near the app root. +export function useLegacyViewRedirect() { + const pathname = usePathname() + const router = useRouter() + const searchParams = useSearchParams() + + useEffect(() => { + if (pathname !== "/") return + const view = searchParams.get("view") + if (!view || !isIntegrationView(view)) return + const params = new URLSearchParams(searchParams.toString()) + params.delete("view") + const qs = params.toString() + router.replace(integrationViewToPath(view) + (qs ? `?${qs}` : "")) + }, [pathname, searchParams, router]) +} diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index 2094e5aa..b0e7879f 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -36,6 +36,14 @@ export default async function proxy(request: Request) { return NextResponse.next() } + // Real integrations routes, public in guest mode (mirrors view=integrations / view=mcp). + if ( + url.pathname === "/integrations" || + url.pathname === "/integrations/mcp" + ) { + return NextResponse.next() + } + if (url.pathname.startsWith("/api/")) { if (!sessionCookie) { console.debug("[MIDDLEWARE] API route without session, returning 401") From 3769ffa41fe033549ef7027e5285116ecde5abbb Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Wed, 24 Jun 2026 01:16:16 +0000 Subject: [PATCH 10/22] feat(web): Company Brain connections disconnect and Slack reconnect (#1157) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Add disconnect actions for org and personal GitHub/Linear connections on the Company Brain settings page - Show connected Slack workspace name and an admin-only **Reconnect Slack** button (pairs with API admin gate in supermemoryai/mono#1912) - Fix infinite loading skeleton when `/brain/connections` fails by falling back to empty state with a toast ## Test plan - [ ] Open Settings → Company Brain connections as org admin - [ ] Confirm Slack team name shows when workspace is connected - [ ] Confirm **Reconnect Slack** is visible for admin/owner only - [ ] Connect and disconnect GitHub/Linear for org (admin) and personal scopes - [ ] Simulate failed connections fetch (e.g. offline) and confirm page renders instead of infinite skeleton --- .../settings/company-brain-connections.tsx | 152 +++++++++++++++++- 1 file changed, 144 insertions(+), 8 deletions(-) diff --git a/apps/web/components/settings/company-brain-connections.tsx b/apps/web/components/settings/company-brain-connections.tsx index b9045e8b..7720969e 100644 --- a/apps/web/components/settings/company-brain-connections.tsx +++ b/apps/web/components/settings/company-brain-connections.tsx @@ -3,7 +3,7 @@ import { authClient } from "@lib/auth" import { cn } from "@lib/utils" import { useQuery } from "@tanstack/react-query" -import { Check, Loader2, Lock } from "lucide-react" +import { Loader2, Lock } from "lucide-react" import { useCallback, useEffect, useState } from "react" import { toast } from "sonner" import { dmSans125ClassName } from "@/lib/fonts" @@ -14,6 +14,67 @@ const BACKEND = process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" type ConnRow = { toolkit: string; org: boolean; user: boolean } +type SlackStatus = { connected: boolean; teamName: string | null } + +function SecondaryButton({ + children, + href, +}: { + children: React.ReactNode + href: string +}) { + return ( + + {children} + + ) +} + +function SlackMark({ className }: { className?: string }) { + return ( + + ) +} function GithubMark({ className }: { className?: string }) { return ( @@ -73,16 +134,20 @@ function AppCard({ toolkit, connected, canConnect, + canDisconnect, lockedHint, busy, onConnect, + onDisconnect, }: { toolkit: string connected: boolean canConnect: boolean + canDisconnect: boolean lockedHint?: string busy: boolean onConnect: () => void + onDisconnect: () => void }) { const meta = TOOLKITS[toolkit] ?? { label: toolkit, @@ -116,7 +181,13 @@ function AppCard({
- {!connected && + {connected && canDisconnect ? ( + + {busy && } + Disconnect + + ) : ( + !connected && (canConnect ? ( {busy && } @@ -127,7 +198,8 @@ function AppCard({ {lockedHint} - ) : null)} + ) : null) + )}
) @@ -182,6 +254,7 @@ function CardSkeleton() { export default function CompanyBrainConnections() { const isCompanyBrain = useHasCompanyBrain() const [rows, setRows] = useState(null) + const [slackStatus, setSlackStatus] = useState(null) const [busy, setBusy] = useState(null) const roleQuery = useQuery({ @@ -195,11 +268,21 @@ export default function CompanyBrainConnections() { const isAdmin = role === "owner" || role === "admin" const load = useCallback(async () => { - const res = await fetch(`${BACKEND}/brain/connections`, { - credentials: "include", - }) - if (res.ok) - setRows(((await res.json()) as { toolkits: ConnRow[] }).toolkits) + const [connRes, slackRes] = await Promise.all([ + fetch(`${BACKEND}/brain/connections`, { credentials: "include" }), + fetch(`${BACKEND}/brain/slack/status`, { credentials: "include" }), + ]) + if (connRes.ok) { + setRows(((await connRes.json()) as { toolkits: ConnRow[] }).toolkits) + } else { + setRows([]) + toast.error("Couldn't load connections.") + } + if (slackRes.ok) { + setSlackStatus((await slackRes.json()) as SlackStatus) + } else { + setSlackStatus({ connected: false, teamName: null }) + } }, []) useEffect(() => { @@ -235,6 +318,37 @@ export default function CompanyBrainConnections() { } } + const disconnect = async (toolkit: string, scope: "user" | "org") => { + const label = TOOLKITS[toolkit]?.label ?? toolkit + if ( + !window.confirm( + `Disconnect ${label} from ${scope === "org" ? "the shared org account" : "your personal account"}?`, + ) + ) + return + setBusy(`${toolkit}:${scope}`) + try { + const res = await fetch( + `${BACKEND}/brain/connections/${toolkit}?scope=${scope}`, + { method: "DELETE", credentials: "include" }, + ) + if (res.status === 403) { + toast.error("Only admins can disconnect the shared org account.") + return + } + if (!res.ok) { + toast.error("Couldn't disconnect.") + return + } + toast.success(`${label} disconnected.`) + await load() + } catch { + toast.error("Couldn't disconnect.") + } finally { + setBusy(null) + } + } + if (!isCompanyBrain) { return (

+

+ {slackStatus?.connected && slackStatus.teamName ? ( +

+ Slack · {slackStatus.teamName} +

+ ) : null} + {isAdmin ? ( + + + Reconnect Slack + + ) : null} +
connect(row.toolkit, "org")} + onDisconnect={() => disconnect(row.toolkit, "org")} /> )) )} @@ -286,8 +420,10 @@ export default function CompanyBrainConnections() { toolkit={row.toolkit} connected={row.user} canConnect + canDisconnect busy={busy === `${row.toolkit}:user`} onConnect={() => connect(row.toolkit, "user")} + onDisconnect={() => disconnect(row.toolkit, "user")} /> )) )} From 46759041595e0263014cef5ebe94bf322670ac49 Mon Sep 17 00:00:00 2001 From: sreedharsreeram <141047751+sreedharsreeram@users.noreply.github.com> Date: Wed, 24 Jun 2026 23:02:43 +0000 Subject: [PATCH 11/22] Fix Claude Code plugin install command (#1161) ## Summary - update the Claude Code plugin install step to use `/plugin install supermemory` - keep the marketplace add command pointing at `supermemoryai/claude-supermemory` ## Testing - Not run; copy-only change --- apps/web/lib/plugin-catalog.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/lib/plugin-catalog.ts b/apps/web/lib/plugin-catalog.ts index 4653f7f9..68208dcb 100644 --- a/apps/web/lib/plugin-catalog.ts +++ b/apps/web/lib/plugin-catalog.ts @@ -47,7 +47,7 @@ export const PLUGIN_CATALOG: Record = { { title: "Install the plugin", description: "Run these commands inside a Claude Code session:", - code: "/plugin marketplace add supermemoryai/claude-supermemory\n/plugin install claude-supermemory", + code: "/plugin marketplace add supermemoryai/claude-supermemory\n/plugin install supermemory", }, ], }, From 4ce13cd4cfb50be791efa5ae3b423a7a3bc5ca3e Mon Sep 17 00:00:00 2001 From: ishaanxgupta <124028055+ishaanxgupta@users.noreply.github.com> Date: Thu, 25 Jun 2026 17:06:59 +0000 Subject: [PATCH 12/22] Add plugin changelog page (#1160) ## Summary - Add a dedicated plugin changelog page under the Changelog tab, with recent updates tagged by plugin. --- apps/docs/changelog/overview.mdx | 1 + apps/docs/changelog/plugins.mdx | 81 ++++++++++++++++++++++++++++++++ apps/docs/docs.json | 2 +- apps/docs/integrations/codex.mdx | 8 ++-- apps/docs/vibe-coding.mdx | 4 +- 5 files changed, 90 insertions(+), 6 deletions(-) create mode 100644 apps/docs/changelog/plugins.mdx diff --git a/apps/docs/changelog/overview.mdx b/apps/docs/changelog/overview.mdx index ac565e10..77ca603d 100644 --- a/apps/docs/changelog/overview.mdx +++ b/apps/docs/changelog/overview.mdx @@ -1,5 +1,6 @@ --- title: "Changelog" +sidebarTitle: "Supermemory" description: "New updates and improvements to Supermemory" --- diff --git a/apps/docs/changelog/plugins.mdx b/apps/docs/changelog/plugins.mdx new file mode 100644 index 00000000..f11aaefb --- /dev/null +++ b/apps/docs/changelog/plugins.mdx @@ -0,0 +1,81 @@ +--- +title: "Plugin changelog" +sidebarTitle: "Plugins" +description: "Recent updates and improvements to Supermemory plugins" +--- + + + +### OpenCode entity context + +OpenCode now sends entity context with memory operations, so saved context can stay tied to the active project and conversation. The entity-context prompt was also moved out of the API client for cleaner reuse across capture and compaction flows. + +### Cursor session auth + +Cursor now starts the auth flow from the session hook when needed, and the OAuth success screen uses the Cursor-branded callback path. + + + + + +### Claude Code update notices + +Claude Code now surfaces plugin update notices during sessions and includes the latest package/version metadata. + +### OpenCode context prompt + +OpenCode gained an entity-context prompt so memory recall and capture can carry more precise source context. + + + + + +### Claude Code marketplace polish + +The Claude Code plugin manifest was polished for the official marketplace listing, including refreshed metadata and naming. + +### Codex update notices + +Codex now checks for plugin updates during session start and shows a user-visible notice when a newer version is available. + + + + + +### Claude Code rename migration + +Claude Code completed the rename to the `supermemory` plugin while keeping migration safe for users already on the new plugin name. Configuration also supports custom `baseUrl` values for local or self-hosted Supermemory installs. + +### Cursor web OAuth + +Cursor OAuth now routes through the Supermemory web app, keeping the plugin auth flow consistent with the rest of the integrations. + + + + + +### Codex auth and status tooling + +Codex added status, logout, and web-auth flows, plus Windows-safe auth URL opening and entity context for saved memories. The installer now includes a `supermemory-status` skill so Codex can report connection, hook, config, and installed-skill health from inside a session. + +### OAuth status refinements + +Codex and OpenCode integration status now renders more clearly in the Supermemory app during OAuth connection and setup. + + + + + +### Claude Code recall reasoning + +Claude Code gained reasoned per-turn memory recall with auto-approve support, refreshed bundled scripts, and updated skill names for `supermemory-save` and `supermemory-search`. + +### Cursor session hooks + +Cursor session hooks now load reliably and persist real project sessions into the correct container. + +### OpenClaw and Hermes memory attribution + +Saved plugin memories now parse source attribution more accurately, and the dashboard shows the correct plugin logos and recent-memory rows for OpenClaw and Hermes. + + diff --git a/apps/docs/docs.json b/apps/docs/docs.json index 6be467ff..92a32d9a 100644 --- a/apps/docs/docs.json +++ b/apps/docs/docs.json @@ -306,7 +306,7 @@ "anchors": [ { "anchor": "Changelog", - "pages": ["changelog/overview"] + "pages": ["changelog/overview", "changelog/plugins"] } ], "tab": "Changelog" diff --git a/apps/docs/integrations/codex.mdx b/apps/docs/integrations/codex.mdx index 6307d996..0b86c82c 100644 --- a/apps/docs/integrations/codex.mdx +++ b/apps/docs/integrations/codex.mdx @@ -49,7 +49,7 @@ This command: - Copies hook and skill scripts to `~/.codex/supermemory/` - Enables `codex_hooks = true` in `~/.codex/config.toml` - Registers `UserPromptSubmit` (recall) and `Stop` (capture) hooks in `~/.codex/hooks.json` -- Installs `supermemory-search`, `supermemory-save`, and `supermemory-forget` skills to `~/.codex/skills/` +- Installs `supermemory-search`, `supermemory-save`, `supermemory-forget`, and `supermemory-status` skills to `~/.codex/skills/` Restart Codex CLI after installing. @@ -81,13 +81,14 @@ Tags are generated automatically — no configuration needed. You can override t ## Explicit Memory Skills -The installer includes three skills that Codex auto-discovers from `~/.codex/skills/`. They use the same `SUPERMEMORY_CODEX_API_KEY` as the hooks — no separate login needed. +The installer includes four skills that Codex auto-discovers from `~/.codex/skills/`. They use the same `SUPERMEMORY_CODEX_API_KEY` as the hooks — no separate login needed. | Skill | Description | |-------|-------------| | `supermemory-search` | Search your memories by natural-language query | | `supermemory-save` | Save important project knowledge to memory | | `supermemory-forget` | Remove outdated or incorrect memories | +| `supermemory-status` | Check Supermemory connection, hook, config, and skill status | These skills let you interact with memory explicitly — for example: @@ -95,6 +96,7 @@ These skills let you interact with memory explicitly — for example: > Remember that this project uses Vitest for unit tests and Playwright for E2E. > What do you remember about our database schema? > Forget the memory about the old API endpoint. +> Is Supermemory connected? ``` ## Verify Installation @@ -111,7 +113,7 @@ codex-supermemory status: API key: ✓ set (SUPERMEMORY_CODEX_API_KEY) Hook scripts: ✓ installed at ~/.codex/supermemory hooks.json: ✓ registered (implicit memory) - Skills: ✓ installed (supermemory-search, supermemory-save, supermemory-forget) + Skills: ✓ installed (supermemory-search, supermemory-save, supermemory-forget, supermemory-status) config.toml: ✓ exists All good! Memory is active. diff --git a/apps/docs/vibe-coding.mdx b/apps/docs/vibe-coding.mdx index 0899692c..6a5c3279 100644 --- a/apps/docs/vibe-coding.mdx +++ b/apps/docs/vibe-coding.mdx @@ -47,7 +47,7 @@ Replace `claude` with: `cursor`, `opencode`, or `vscode` After adding the MCP, paste this in your agent session: -``` +```` You are integrating Supermemory into my application. Supermemory provides user memory, semantic search, and automatic knowledge extraction for AI applications. Note: You can always reference the documentation by using the **SearchSupermemoryDocs MCP** or running a web search tool for content on **supermemory.ai/docs**. @@ -386,7 +386,7 @@ NOW: 3. Include installation, settings config, and full integration DOCS: https://supermemory.ai/docs -``` +```` From 886ee692f98ac2f7611dac6051bf26c1e3ebd8b9 Mon Sep 17 00:00:00 2001 From: Dhravya Shah Date: Thu, 25 Jun 2026 13:35:23 -0700 Subject: [PATCH 13/22] Add request timeouts to Supermemory MCP client calls (#1146) Co-authored-by: Cursor Agent --- apps/mcp/pnpm-lock.yaml | 3480 +++++++++++++++++++++++++++++++++++++++ apps/mcp/src/client.ts | 13 + 2 files changed, 3493 insertions(+) create mode 100644 apps/mcp/pnpm-lock.yaml diff --git a/apps/mcp/pnpm-lock.yaml b/apps/mcp/pnpm-lock.yaml new file mode 100644 index 00000000..f7587960 --- /dev/null +++ b/apps/mcp/pnpm-lock.yaml @@ -0,0 +1,3480 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@cloudflare/workers-oauth-provider': + specifier: ^0.2.2 + version: 0.2.4 + '@modelcontextprotocol/ext-apps': + specifier: ^1.0.0 + version: 1.7.4(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(react@19.2.7)(zod@3.25.76) + '@modelcontextprotocol/sdk': + specifier: ^1.25.2 + version: 1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76) + agents: + specifier: ^0.3.5 + version: 0.3.10(@cloudflare/ai-chat@0.0.6)(@cloudflare/codemode@0.0.6)(@cloudflare/workers-types@4.20260620.1)(ai@6.0.208(zod@3.25.76))(hono@4.12.26)(react@19.2.7)(zod@3.25.76) + hono: + specifier: ^4.11.1 + version: 4.12.26 + posthog-node: + specifier: ^5.18.0 + version: 5.38.2 + supermemory: + specifier: ^4.0.0 + version: 4.24.12 + zod: + specifier: ^3.25.76 + version: 3.25.76 + devDependencies: + '@cloudflare/workers-types': + specifier: ^4.20250620.0 + version: 4.20260620.1 + d3-force-3d: + specifier: ^3.0.5 + version: 3.0.6 + force-graph: + specifier: ^1.49.0 + version: 1.51.4 + typescript: + specifier: ^5.8.3 + version: 5.9.3 + vite: + specifier: ^6.0.0 + version: 6.4.3 + vite-plugin-singlefile: + specifier: ^2.3.0 + version: 2.3.3(rollup@4.62.2)(vite@6.4.3) + vitest: + specifier: ^3.2.4 + version: 3.2.6 + wrangler: + specifier: ^4.4.0 + version: 4.103.0(@cloudflare/workers-types@4.20260620.1) + +packages: + + '@ai-sdk/gateway@3.0.133': + resolution: {integrity: sha512-Ebs+7iS9zUgJu5B0RlxM2JmDWzq79Cpd6YdiqcCzB5qFdpfQJPUDiXutqlQP89F2XGjOdDeidulBTXUdXWzOxw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider-utils@4.0.30': + resolution: {integrity: sha512-VO7I+vPffqI5sMnPoUq5DCSqKIgQIk/naJWRdQVpz2ma2zoprC/lqiJiUEl2s6DfvTD76TbhD3q39ROjlA6rGw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider@3.0.10': + resolution: {integrity: sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw==} + engines: {node: '>=18'} + + '@apidevtools/json-schema-ref-parser@11.9.3': + resolution: {integrity: sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==} + engines: {node: '>= 16'} + + '@babel/runtime-corejs3@7.29.7': + resolution: {integrity: sha512-ppj9ouYku+RX0ljtgZd+KMO5mkM2bCqg8H2PYAFWnLsHEIKIdRojqbJ2i3eVHrisuxy7nOFCmngTDdWtUCdXUQ==} + engines: {node: '>=6.9.0'} + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@cfworker/json-schema@4.1.1': + resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==} + + '@cloudflare/ai-chat@0.0.6': + resolution: {integrity: sha512-XDJP7ywORzQd17f09hMY1n3+bQsLw+BmXh7HzuWGb7gBBbD23OlQXccuQQqFNBYHi+IfIpe9YdGbHaUGsfCUwA==} + peerDependencies: + agents: ^0.3.10 + ai: ^6.0.0 + react: ^19.0.0 + zod: ^3.25.0 || ^4.0.0 + + '@cloudflare/codemode@0.0.6': + resolution: {integrity: sha512-P8ba7fgyeOOEODgU+lyUj82P89VfslKExsdF6nPGRebIbgiCbbpoLHBmBtdRRsIasVHUhceSazJxIS6dg70yRA==} + peerDependencies: + agents: ^0.3.7 + ai: ^6.0.0 + zod: ^3.25.0 || ^4.0.0 + + '@cloudflare/kv-asset-handler@0.5.0': + resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==} + engines: {node: '>=22.0.0'} + + '@cloudflare/unenv-preset@2.16.1': + resolution: {integrity: sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==} + peerDependencies: + unenv: 2.0.0-rc.24 + workerd: '>1.20260305.0 <2.0.0-0' + peerDependenciesMeta: + workerd: + optional: true + + '@cloudflare/workerd-darwin-64@1.20260617.1': + resolution: {integrity: sha512-jWwmgEVVWbsHNrLSNXzwjJaH90VzRxq1cWkQFUidxyeUPnMxemeNE8I9qFAfrpzGgE11e9sKDcE3ettJW08swQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20260617.1': + resolution: {integrity: sha512-LHH7b565g9znfCUOkwbec6FG2rmRbsgCy6aJiU9KN662mNheWl5sw/iKleiFSiljPKQQP3HkjnC/NSkdgi/aSA==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-linux-64@1.20260617.1': + resolution: {integrity: sha512-FMnaAKXe4Cfd8TQurCVd9fs2XQVBFRCsP+Id/SRdUv89MlwYu9zXfoyx6BxM+brPTIUK38SHbo8iaxiwzLi9JQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20260617.1': + resolution: {integrity: sha512-MRoifFYcqbxxIIQy7PqO5tFY/qPFSnjXzakWl0sO93l+HLyG35jRAgOi6jfqa4kBxc7gKKtH861DcewjxUfkjA==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-windows-64@1.20260617.1': + resolution: {integrity: sha512-rgBV9wQrv0OSKgCTTbhFUFY3sLGNANZ88aqaLvtmEn2gmbFVb1J4PDGochVUdB7NSEp4D/ghHva6/8SZmbONpw==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@cloudflare/workers-oauth-provider@0.2.4': + resolution: {integrity: sha512-xovAzPPj0QT+HeSTgRIMlxlHciaUOOrojwra9/sRoIWsHRFbN04cNvXnXQEFlpc0pDENE23OZS2Do8wswySR2g==} + + '@cloudflare/workers-types@4.20260620.1': + resolution: {integrity: sha512-WB81w9u1bAS7KcekpC7/nYhLpIXAEtgybso7XgGJV8CQKNkNPYcyjvICLdghOlDBi/9Ivk+f7NRckV2Bkq1bDg==} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@hono/node-server@1.19.14': + resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@jsdevtools/ono@7.1.3': + resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} + + '@modelcontextprotocol/ext-apps@1.7.4': + resolution: {integrity: sha512-QQqysE549cf/Y0VabBmAACXhj92EhB3t8yVct2BHbkWiPTFA1S91EqTVjYXXcZEefXU0pmHcdObhsNMcomJIOQ==} + engines: {node: '>=20'} + peerDependencies: + '@modelcontextprotocol/sdk': ^1.29.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + + '@modelcontextprotocol/sdk@1.25.2': + resolution: {integrity: sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + + '@modelcontextprotocol/sdk@1.29.0': + resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + engines: {node: '>=8.0.0'} + + '@poppinss/colors@4.1.6': + resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} + + '@poppinss/dumper@0.6.5': + resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} + + '@poppinss/exception@1.2.3': + resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + + '@posthog/core@1.35.3': + resolution: {integrity: sha512-EsGPbSLl39Jgo2KZ+kI9UAxFnh5nddaN5bNm2rXvUwF+vGmam9eN1EXeNbxhRU7ulEeIiGdm7XjoU7pzavkgIQ==} + + '@posthog/types@1.390.2': + resolution: {integrity: sha512-WcfKz2GNn2vfDX8vXmJYbKxegPxVWHuDQ/pHdAn0HoZDXDFnEp/+x3qBQA+fEvtbPjjtjgAt2wIgJMlM7asx7g==} + + '@rollup/rollup-android-arm-eabi@4.62.2': + resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.2': + resolution: {integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.2': + resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.2': + resolution: {integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.2': + resolution: {integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.2': + resolution: {integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.62.2': + resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.62.2': + resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.62.2': + resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.62.2': + resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.62.2': + resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.2': + resolution: {integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + resolution: {integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + resolution: {integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.2': + resolution: {integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.2': + resolution: {integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==} + cpu: [x64] + os: [win32] + + '@sindresorhus/is@7.2.0': + resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} + engines: {node: '>=18'} + + '@speed-highlight/core@1.2.17': + resolution: {integrity: sha512-Z92FwKpCtfaW1V0jTU/fh3QzYEZN8wDwrzRIBoADCJfn4mJCNcJN/XegifX7BDrQ8/h9Xh/JnbyMchL0FqXrkg==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tweenjs/tween.js@25.0.0': + resolution: {integrity: sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/lodash@4.17.24': + resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==} + + '@vercel/oidc@3.2.0': + resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==} + engines: {node: '>= 20'} + + '@vitest/expect@3.2.6': + resolution: {integrity: sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==} + + '@vitest/mocker@3.2.6': + resolution: {integrity: sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.6': + resolution: {integrity: sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==} + + '@vitest/runner@3.2.6': + resolution: {integrity: sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==} + + '@vitest/snapshot@3.2.6': + resolution: {integrity: sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==} + + '@vitest/spy@3.2.6': + resolution: {integrity: sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==} + + '@vitest/utils@3.2.6': + resolution: {integrity: sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + accessor-fn@1.5.3: + resolution: {integrity: sha512-rkAofCwe/FvYFUlMB0v0gWmhqtfAtV1IUkdPbfhTUyYniu5LrC0A0UJkTH0Jv3S8SvwkmfuAlY+mQIJATdocMA==} + engines: {node: '>=12'} + + agents@0.3.10: + resolution: {integrity: sha512-hKj3nbej14GA2SgE6/stQheJF35LCS7DxMuHG0QDl1/npnnECYyG0Yf5DXl6AvJAZOFNDwT3iEzKi8yLZngPDA==} + hasBin: true + peerDependencies: + '@ai-sdk/openai': ^3.0.0 + '@ai-sdk/react': ^3.0.0 + '@cloudflare/ai-chat': ^0.0.6 + '@cloudflare/codemode': ^0.0.6 + ai: ^6.0.0 + react: ^19.0.0 + viem: '>=2.0.0' + x402: ^0.7.1 + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + '@ai-sdk/openai': + optional: true + '@ai-sdk/react': + optional: true + viem: + optional: true + x402: + optional: true + + ai@6.0.208: + resolution: {integrity: sha512-STz+AaZqJ4ZjH7UkpXkbHx+bjgIDOsE8fIUoZjkZ2whoZcfVmG9K/TqEKouJZ03SuZuD7lagntlU3zBhAEkRpQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + bezier-js@6.1.4: + resolution: {integrity: sha512-PA0FW9ZpcHbojUCMu28z9Vg/fNkwTj5YhusSAjHHDfHDGLxJ6YUKrAN2vk1fP2MMOxVw4Oko16FMlRGVBGqLKg==} + + blake3-wasm@2.1.5: + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + + body-parser@2.3.0: + resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} + engines: {node: '>=18'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + canvas-color-tracker@1.3.2: + resolution: {integrity: sha512-ryQkDX26yJ3CXzb3hxUVNlg1NKE4REc5crLBq661Nxzr8TNd236SaEf2ffYLXyI5tSABSeguHLqcVq4vf9L3Zg==} + engines: {node: '>=12'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + cliui@9.0.1: + resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} + engines: {node: '>=20'} + + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + content-type@2.0.0: + resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + engines: {node: '>=18'} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + core-js-pure@3.49.0: + resolution: {integrity: sha512-XM4RFka59xATyJv/cS3O3Kml72hQXUeGRuuTmMYFxwzc9/7C8OYTaIR/Ji+Yt8DXzsFLNhat15cE/JP15HrCgw==} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + cron-schedule@6.0.0: + resolution: {integrity: sha512-BoZaseYGXOo5j5HUwTaegIog3JJbuH4BbrY9A1ArLjXpy+RWb3mV28F/9Gv1dDA7E2L8kngWva4NWisnLTyfgQ==} + engines: {node: '>=20'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-binarytree@1.0.2: + resolution: {integrity: sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-force-3d@3.0.6: + resolution: {integrity: sha512-4tsKHUPLOVkyfEffZo1v6sFHvGFwAIIjt/W8IThbp08DYAsXZck+2pSHEG5W1+gQgEvFLdZkYvmJAbRM2EzMnA==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-octree@1.1.0: + resolution: {integrity: sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + event-target-polyfill@0.0.4: + resolution: {integrity: sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ==} + + eventsource-parser@3.1.0: + resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + express-rate-limit@7.5.1: + resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express-rate-limit@8.5.2: + resolution: {integrity: sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + float-tooltip@1.7.5: + resolution: {integrity: sha512-/kXzuDnnBqyyWyhDMH7+PfP8J/oXiAavGzcRxASOMRHFuReDtofizLLJsf7nnDLAfEaMW4pVWaXrAjtnglpEkg==} + engines: {node: '>=12'} + + force-graph@1.51.4: + resolution: {integrity: sha512-TdJ2KbkoiDQ7NIRx8IPGD0mAXXpLhamS7c+b7W98b0MHG7lphnda1VOQX/98UDTsttIAdH4TcP0l0MauSnLK8w==} + engines: {node: '>=12'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + hono@4.12.26: + resolution: {integrity: sha512-uyZtpnYxM9CmQ7QsQknM4zN8EftNqhON1qYeIKM0Se67CCEe2c44xyGURwB0axX2fBDu1dqHrHAc1hmNT8ITkw==} + engines: {node: '>=16.9.0'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + index-array-by@1.4.2: + resolution: {integrity: sha512-SP23P27OUKzXWEC/TOyWlwLviofQkCSCKONnc62eItjp69yCZZPqDQtr3Pw5gJDnPeUMqExmKydNZaJO0FU9pw==} + engines: {node: '>=12'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + ip-address@10.2.0: + resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + + js-base64@3.7.8: + resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + js-yaml@4.2.0: + resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} + hasBin: true + + json-schema-to-typescript@15.0.4: + resolution: {integrity: sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ==} + engines: {node: '>=16.0.0'} + hasBin: true + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + + kapsule@1.16.3: + resolution: {integrity: sha512-4+5mNNf4vZDSwPhKprKwz3330iisPrb08JyMgbsdFrimBCKNHecua/WBwvVg3n7vwx0C1ARjfhwIpbrbd9n5wg==} + engines: {node: '>=12'} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mimetext@3.0.28: + resolution: {integrity: sha512-eQXpbNrtxLCjUtiVbR/qR09dbPgZ2o+KR1uA7QKqGhbn8QV7HIL16mXXsobBL4/8TqoYh1us31kfz+dNfCev9g==} + + miniflare@4.20260617.1: + resolution: {integrity: sha512-Go3/gzStm99QHptsSgU+q1S+xDfLoRgwjJNY80kaTVi0ENhTyqKq+sc4xZiWBSbM7uUcJwmzm8+QFKtcYLJ9nw==} + engines: {node: '>=22.0.0'} + hasBin: true + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.14: + resolution: {integrity: sha512-U9kYi5bpVMEI31yC8iw4bJJp0avcHXA0W8/wNfLfnvJYzihQo2ZRPYPvpAAd570HAcCBjCTN7vnr+v4StKl1IQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanoid@5.1.15: + resolution: {integrity: sha512-kBg3RpGtIe+RpTbyXwoI6pk5yD7KUiI3sygUqgeBMRst42KmhB4RZC7eiO9Wa1HIpaCCtpE2DJ6OI4Wi5ebwFw==} + engines: {node: ^18 || >=20} + hasBin: true + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + partyserver@0.1.5: + resolution: {integrity: sha512-kaE3GYaYWFc70EJQDQEhyYbO2Wczz/NgsFXerfjRo0t2s7ZxL1XggWT+HkMrdEyqbZOv3b66CV93WG0Lcg/ThQ==} + peerDependencies: + '@cloudflare/workers-types': ^4.20240729.0 + + partysocket@1.1.11: + resolution: {integrity: sha512-P0EtOQiAwvLriqLgdThcSaREfz3bP77LkLSdmXq680BosPKvGSoGTh/d0g3S+UNmaqcw89Ad7JXHHKyRx3xU9Q==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + posthog-node@5.38.2: + resolution: {integrity: sha512-eiKpU+vX4hVuHbO/EosvPHsmh2AVIdoVmWss/uUOs1t4b0ViCblw2o8OIFqHxKj3mYRnSOBlX0Dw3wBvcCaYpA==} + engines: {node: ^20.20.0 || >=22.22.0} + peerDependencies: + rxjs: ^7.0.0 + peerDependenciesMeta: + rxjs: + optional: true + + preact@10.29.2: + resolution: {integrity: sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==} + + prettier@3.8.4: + resolution: {integrity: sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==} + engines: {node: '>=14'} + hasBin: true + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + qs@6.15.2: + resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} + engines: {node: '>=0.6'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + react@19.2.7: + resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + rollup@4.62.2: + resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + supermemory@4.24.12: + resolution: {integrity: sha512-xAFextuqk4JuoW33jJaFGqT1oMppN2IgfWUrV18Fv3qAAZ6M1SR1tb+7EBq8vrEQIx4iY2MQh5p+qnfL6lI8Yw==} + hasBin: true + + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinycolor2@1.6.0: + resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici@7.28.0: + resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} + engines: {node: '>=20.18.1'} + + unenv@2.0.0-rc.24: + resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite-plugin-singlefile@2.3.3: + resolution: {integrity: sha512-XVnGH0QzbOa8fxRSsHdCarVN1BSBXNi7uLMQYlrGRN5apdHkk62XQWRJhVever0lnfuyBkwn+kvVChdm/OoOUg==} + engines: {node: '>18.0.0'} + peerDependencies: + rollup: ^4.59.0 + vite: ^5.4.21 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + rollup: + optional: true + + vite@6.4.3: + resolution: {integrity: sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.2.6: + resolution: {integrity: sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.6 + '@vitest/ui': 3.2.6 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + workerd@1.20260617.1: + resolution: {integrity: sha512-Re5pl6pdowt3ZmWUzGlOuB7jbRIIPetgKalmo4cYmucQnVhpo7/3e4MfpekbhLi2EhZZz5EY9NWRu8zFzuEZew==} + engines: {node: '>=16'} + hasBin: true + + wrangler@4.103.0: + resolution: {integrity: sha512-3Lv1P5t2xcSEkSTKtG+Lz+3JFryuU7YPLkaCUj7gNe+CJsjZJLtUwqsh1x595QBxkIbCE0GAvDx2DCJUU4+oqw==} + engines: {node: '>=22.0.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^4.20260617.1 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yargs@18.0.0: + resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + youch-core@0.3.3: + resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} + + youch@4.1.0-beta.10: + resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} + + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} + peerDependencies: + zod: ^3.25.28 || ^4 + + zod-to-ts@2.1.0: + resolution: {integrity: sha512-jZP1GokTqR99FLmtGU+B9acjD9u/R++b++Vxe1sWpBQDd82/9/j5LyLZZ7/Oy3h2nxcz5NihikgX4D0hhdu3+g==} + peerDependencies: + typescript: ^5 || ^6 + zod: ^3.25.0 || ^4.0.0 + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + +snapshots: + + '@ai-sdk/gateway@3.0.133(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.10 + '@ai-sdk/provider-utils': 4.0.30(zod@3.25.76) + '@vercel/oidc': 3.2.0 + zod: 3.25.76 + + '@ai-sdk/provider-utils@4.0.30(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.10 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.1.0 + zod: 3.25.76 + + '@ai-sdk/provider@3.0.10': + dependencies: + json-schema: 0.4.0 + + '@apidevtools/json-schema-ref-parser@11.9.3': + dependencies: + '@jsdevtools/ono': 7.1.3 + '@types/json-schema': 7.0.15 + js-yaml: 4.2.0 + + '@babel/runtime-corejs3@7.29.7': + dependencies: + core-js-pure: 3.49.0 + + '@babel/runtime@7.29.7': {} + + '@cfworker/json-schema@4.1.1': {} + + '@cloudflare/ai-chat@0.0.6(agents@0.3.10)(ai@6.0.208(zod@3.25.76))(react@19.2.7)(zod@3.25.76)': + dependencies: + agents: 0.3.10(@cloudflare/ai-chat@0.0.6)(@cloudflare/codemode@0.0.6)(@cloudflare/workers-types@4.20260620.1)(ai@6.0.208(zod@3.25.76))(hono@4.12.26)(react@19.2.7)(zod@3.25.76) + ai: 6.0.208(zod@3.25.76) + react: 19.2.7 + zod: 3.25.76 + + '@cloudflare/codemode@0.0.6(agents@0.3.10)(ai@6.0.208(zod@3.25.76))(typescript@5.9.3)(zod@3.25.76)': + dependencies: + agents: 0.3.10(@cloudflare/ai-chat@0.0.6)(@cloudflare/codemode@0.0.6)(@cloudflare/workers-types@4.20260620.1)(ai@6.0.208(zod@3.25.76))(hono@4.12.26)(react@19.2.7)(zod@3.25.76) + ai: 6.0.208(zod@3.25.76) + zod: 3.25.76 + zod-to-ts: 2.1.0(typescript@5.9.3)(zod@3.25.76) + transitivePeerDependencies: + - typescript + + '@cloudflare/kv-asset-handler@0.5.0': {} + + '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260617.1)': + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20260617.1 + + '@cloudflare/workerd-darwin-64@1.20260617.1': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20260617.1': + optional: true + + '@cloudflare/workerd-linux-64@1.20260617.1': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20260617.1': + optional: true + + '@cloudflare/workerd-windows-64@1.20260617.1': + optional: true + + '@cloudflare/workers-oauth-provider@0.2.4': {} + + '@cloudflare/workers-types@4.20260620.1': {} + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/aix-ppc64@0.28.1': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.28.1': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-arm@0.28.1': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/android-x64@0.28.1': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.28.1': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.28.1': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.28.1': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.28.1': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.28.1': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-arm@0.28.1': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.28.1': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.28.1': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.28.1': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.28.1': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.28.1': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.28.1': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.28.1': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.28.1': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.28.1': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.28.1': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@esbuild/win32-x64@0.28.1': + optional: true + + '@hono/node-server@1.19.14(hono@4.12.26)': + dependencies: + hono: 4.12.26 + + '@img/colour@1.1.0': {} + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.11.1 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@jsdevtools/ono@7.1.3': {} + + '@modelcontextprotocol/ext-apps@1.7.4(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(react@19.2.7)(zod@3.25.76)': + dependencies: + '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76) + '@standard-schema/spec': 1.1.0 + zod: 3.25.76 + optionalDependencies: + react: 19.2.7 + + '@modelcontextprotocol/sdk@1.25.2(@cfworker/json-schema@4.1.1)(hono@4.12.26)(zod@3.25.76)': + dependencies: + '@hono/node-server': 1.19.14(hono@4.12.26) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.1.0 + express: 5.2.1 + express-rate-limit: 7.5.1(express@5.2.1) + jose: 6.2.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 3.25.76 + zod-to-json-schema: 3.25.2(zod@3.25.76) + optionalDependencies: + '@cfworker/json-schema': 4.1.1 + transitivePeerDependencies: + - hono + - supports-color + + '@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76)': + dependencies: + '@hono/node-server': 1.19.14(hono@4.12.26) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.1.0 + express: 5.2.1 + express-rate-limit: 8.5.2(express@5.2.1) + hono: 4.12.26 + jose: 6.2.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 3.25.76 + zod-to-json-schema: 3.25.2(zod@3.25.76) + optionalDependencies: + '@cfworker/json-schema': 4.1.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/api@1.9.1': {} + + '@poppinss/colors@4.1.6': + dependencies: + kleur: 4.1.5 + + '@poppinss/dumper@0.6.5': + dependencies: + '@poppinss/colors': 4.1.6 + '@sindresorhus/is': 7.2.0 + supports-color: 10.2.2 + + '@poppinss/exception@1.2.3': {} + + '@posthog/core@1.35.3': + dependencies: + '@posthog/types': 1.390.2 + + '@posthog/types@1.390.2': {} + + '@rollup/rollup-android-arm-eabi@4.62.2': + optional: true + + '@rollup/rollup-android-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-x64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.2': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.2': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.2': + optional: true + + '@sindresorhus/is@7.2.0': {} + + '@speed-highlight/core@1.2.17': {} + + '@standard-schema/spec@1.1.0': {} + + '@tweenjs/tween.js@25.0.0': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/json-schema@7.0.15': {} + + '@types/lodash@4.17.24': {} + + '@vercel/oidc@3.2.0': {} + + '@vitest/expect@3.2.6': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.6 + '@vitest/utils': 3.2.6 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.6(vite@6.4.3)': + dependencies: + '@vitest/spy': 3.2.6 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 6.4.3 + + '@vitest/pretty-format@3.2.6': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.6': + dependencies: + '@vitest/utils': 3.2.6 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.6': + dependencies: + '@vitest/pretty-format': 3.2.6 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.6': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.6': + dependencies: + '@vitest/pretty-format': 3.2.6 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + accessor-fn@1.5.3: {} + + agents@0.3.10(@cloudflare/ai-chat@0.0.6)(@cloudflare/codemode@0.0.6)(@cloudflare/workers-types@4.20260620.1)(ai@6.0.208(zod@3.25.76))(hono@4.12.26)(react@19.2.7)(zod@3.25.76): + dependencies: + '@cfworker/json-schema': 4.1.1 + '@cloudflare/ai-chat': 0.0.6(agents@0.3.10)(ai@6.0.208(zod@3.25.76))(react@19.2.7)(zod@3.25.76) + '@cloudflare/codemode': 0.0.6(agents@0.3.10)(ai@6.0.208(zod@3.25.76))(typescript@5.9.3)(zod@3.25.76) + '@modelcontextprotocol/sdk': 1.25.2(@cfworker/json-schema@4.1.1)(hono@4.12.26)(zod@3.25.76) + ai: 6.0.208(zod@3.25.76) + cron-schedule: 6.0.0 + escape-html: 1.0.3 + json-schema: 0.4.0 + json-schema-to-typescript: 15.0.4 + mimetext: 3.0.28 + nanoid: 5.1.15 + partyserver: 0.1.5(@cloudflare/workers-types@4.20260620.1) + partysocket: 1.1.11 + react: 19.2.7 + yargs: 18.0.0 + zod: 3.25.76 + transitivePeerDependencies: + - '@cloudflare/workers-types' + - hono + - supports-color + + ai@6.0.208(zod@3.25.76): + dependencies: + '@ai-sdk/gateway': 3.0.133(zod@3.25.76) + '@ai-sdk/provider': 3.0.10 + '@ai-sdk/provider-utils': 4.0.30(zod@3.25.76) + '@opentelemetry/api': 1.9.1 + zod: 3.25.76 + + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.2 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@6.2.2: {} + + ansi-styles@6.2.3: {} + + argparse@2.0.1: {} + + assertion-error@2.0.1: {} + + bezier-js@6.1.4: {} + + blake3-wasm@2.1.5: {} + + body-parser@2.3.0: + dependencies: + bytes: 3.1.2 + content-type: 2.0.0 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.2 + raw-body: 3.0.2 + type-is: 2.1.0 + transitivePeerDependencies: + - supports-color + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + bytes@3.1.2: {} + + cac@6.7.14: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + canvas-color-tracker@1.3.2: + dependencies: + tinycolor2: 1.6.0 + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + check-error@2.1.3: {} + + cliui@9.0.1: + dependencies: + string-width: 7.2.0 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 + + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + content-type@2.0.0: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cookie@1.1.1: {} + + core-js-pure@3.49.0: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cron-schedule@6.0.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-binarytree@1.0.2: {} + + d3-color@3.1.0: {} + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-ease@3.0.1: {} + + d3-force-3d@3.0.6: + dependencies: + d3-binarytree: 1.0.2 + d3-dispatch: 3.0.1 + d3-octree: 1.1.0 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-octree@1.1.0: {} + + d3-quadtree@3.0.1: {} + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + depd@2.0.0: {} + + detect-libc@2.1.2: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ee-first@1.1.1: {} + + emoji-regex@10.6.0: {} + + encodeurl@2.0.0: {} + + error-stack-parser-es@1.0.5: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@1.7.0: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + etag@1.8.1: {} + + event-target-polyfill@0.0.4: {} + + eventsource-parser@3.1.0: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.1.0 + + expect-type@1.3.0: {} + + express-rate-limit@7.5.1(express@5.2.1): + dependencies: + express: 5.2.1 + + express-rate-limit@8.5.2(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.2.0 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.3.0 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.2 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.1.0 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fast-deep-equal@3.1.3: {} + + fast-uri@3.1.2: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + float-tooltip@1.7.5: + dependencies: + d3-selection: 3.0.0 + kapsule: 1.16.3 + preact: 10.29.2 + + force-graph@1.51.4: + dependencies: + '@tweenjs/tween.js': 25.0.0 + accessor-fn: 1.5.3 + bezier-js: 6.1.4 + canvas-color-tracker: 1.3.2 + d3-array: 3.2.4 + d3-drag: 3.0.0 + d3-force-3d: 3.0.6 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-zoom: 3.0.0 + float-tooltip: 1.7.5 + index-array-by: 1.4.2 + kapsule: 1.16.3 + lodash-es: 4.18.1 + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.6.0: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + hono@4.12.26: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + index-array-by@1.4.2: {} + + inherits@2.0.4: {} + + internmap@2.0.3: {} + + ip-address@10.2.0: {} + + ipaddr.js@1.9.1: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-promise@4.0.0: {} + + isexe@2.0.0: {} + + jose@6.2.3: {} + + js-base64@3.7.8: {} + + js-tokens@9.0.1: {} + + js-yaml@4.2.0: + dependencies: + argparse: 2.0.1 + + json-schema-to-typescript@15.0.4: + dependencies: + '@apidevtools/json-schema-ref-parser': 11.9.3 + '@types/json-schema': 7.0.15 + '@types/lodash': 4.17.24 + is-glob: 4.0.3 + js-yaml: 4.2.0 + lodash: 4.18.1 + minimist: 1.2.8 + prettier: 3.8.4 + tinyglobby: 0.2.17 + + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + + json-schema@0.4.0: {} + + kapsule@1.16.3: + dependencies: + lodash-es: 4.18.1 + + kleur@4.1.5: {} + + lodash-es@4.18.1: {} + + lodash@4.18.1: {} + + loupe@3.2.1: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mimetext@3.0.28: + dependencies: + '@babel/runtime': 7.29.7 + '@babel/runtime-corejs3': 7.29.7 + js-base64: 3.7.8 + mime-types: 2.1.35 + + miniflare@4.20260617.1: + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.34.5 + undici: 7.28.0 + workerd: 1.20260617.1 + ws: 8.21.0 + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + minimist@1.2.8: {} + + ms@2.1.3: {} + + nanoid@3.3.14: {} + + nanoid@5.1.15: {} + + negotiator@1.0.0: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + parseurl@1.3.3: {} + + partyserver@0.1.5(@cloudflare/workers-types@4.20260620.1): + dependencies: + '@cloudflare/workers-types': 4.20260620.1 + nanoid: 5.1.15 + + partysocket@1.1.11: + dependencies: + event-target-polyfill: 0.0.4 + + path-key@3.1.1: {} + + path-to-regexp@6.3.0: {} + + path-to-regexp@8.4.2: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.4: {} + + pkce-challenge@5.0.1: {} + + postcss@8.5.15: + dependencies: + nanoid: 3.3.14 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + posthog-node@5.38.2: + dependencies: + '@posthog/core': 1.35.3 + + preact@10.29.2: {} + + prettier@3.8.4: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + qs@6.15.2: + dependencies: + side-channel: 1.1.1 + + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + + react@19.2.7: {} + + require-from-string@2.0.2: {} + + rollup@4.62.2: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.62.2 + '@rollup/rollup-android-arm64': 4.62.2 + '@rollup/rollup-darwin-arm64': 4.62.2 + '@rollup/rollup-darwin-x64': 4.62.2 + '@rollup/rollup-freebsd-arm64': 4.62.2 + '@rollup/rollup-freebsd-x64': 4.62.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.2 + '@rollup/rollup-linux-arm-musleabihf': 4.62.2 + '@rollup/rollup-linux-arm64-gnu': 4.62.2 + '@rollup/rollup-linux-arm64-musl': 4.62.2 + '@rollup/rollup-linux-loong64-gnu': 4.62.2 + '@rollup/rollup-linux-loong64-musl': 4.62.2 + '@rollup/rollup-linux-ppc64-gnu': 4.62.2 + '@rollup/rollup-linux-ppc64-musl': 4.62.2 + '@rollup/rollup-linux-riscv64-gnu': 4.62.2 + '@rollup/rollup-linux-riscv64-musl': 4.62.2 + '@rollup/rollup-linux-s390x-gnu': 4.62.2 + '@rollup/rollup-linux-x64-gnu': 4.62.2 + '@rollup/rollup-linux-x64-musl': 4.62.2 + '@rollup/rollup-openbsd-x64': 4.62.2 + '@rollup/rollup-openharmony-arm64': 4.62.2 + '@rollup/rollup-win32-arm64-msvc': 4.62.2 + '@rollup/rollup-win32-ia32-msvc': 4.62.2 + '@rollup/rollup-win32-x64-gnu': 4.62.2 + '@rollup/rollup-win32-x64-msvc': 4.62.2 + fsevents: 2.3.3 + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + + safer-buffer@2.1.2: {} + + semver@7.8.5: {} + + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + sharp@0.34.5: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + statuses@2.0.2: {} + + std-env@3.10.0: {} + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + supermemory@4.24.12: {} + + supports-color@10.2.2: {} + + tinybench@2.9.0: {} + + tinycolor2@1.6.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + tslib@2.8.1: + optional: true + + type-is@2.1.0: + dependencies: + content-type: 2.0.0 + media-typer: 1.1.0 + mime-types: 3.0.2 + + typescript@5.9.3: {} + + undici@7.28.0: {} + + unenv@2.0.0-rc.24: + dependencies: + pathe: 2.0.3 + + unpipe@1.0.0: {} + + vary@1.1.2: {} + + vite-node@3.2.4: + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 6.4.3 + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite-plugin-singlefile@2.3.3(rollup@4.62.2)(vite@6.4.3): + dependencies: + micromatch: 4.0.8 + vite: 6.4.3 + optionalDependencies: + rollup: 4.62.2 + + vite@6.4.3: + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.15 + rollup: 4.62.2 + tinyglobby: 0.2.17 + optionalDependencies: + fsevents: 2.3.3 + + vitest@3.2.6: + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.6 + '@vitest/mocker': 3.2.6(vite@6.4.3) + '@vitest/pretty-format': 3.2.6 + '@vitest/runner': 3.2.6 + '@vitest/snapshot': 3.2.6 + '@vitest/spy': 3.2.6 + '@vitest/utils': 3.2.6 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.17 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 6.4.3 + vite-node: 3.2.4 + why-is-node-running: 2.3.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + workerd@1.20260617.1: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20260617.1 + '@cloudflare/workerd-darwin-arm64': 1.20260617.1 + '@cloudflare/workerd-linux-64': 1.20260617.1 + '@cloudflare/workerd-linux-arm64': 1.20260617.1 + '@cloudflare/workerd-windows-64': 1.20260617.1 + + wrangler@4.103.0(@cloudflare/workers-types@4.20260620.1): + dependencies: + '@cloudflare/kv-asset-handler': 0.5.0 + '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260617.1) + blake3-wasm: 2.1.5 + esbuild: 0.28.1 + miniflare: 4.20260617.1 + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.24 + workerd: 1.20260617.1 + optionalDependencies: + '@cloudflare/workers-types': 4.20260620.1 + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + + wrappy@1.0.2: {} + + ws@8.21.0: {} + + y18n@5.0.8: {} + + yargs-parser@22.0.0: {} + + yargs@18.0.0: + dependencies: + cliui: 9.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + string-width: 7.2.0 + y18n: 5.0.8 + yargs-parser: 22.0.0 + + youch-core@0.3.3: + dependencies: + '@poppinss/exception': 1.2.3 + error-stack-parser-es: 1.0.5 + + youch@4.1.0-beta.10: + dependencies: + '@poppinss/colors': 4.1.6 + '@poppinss/dumper': 0.6.5 + '@speed-highlight/core': 1.2.17 + cookie: 1.1.1 + youch-core: 0.3.3 + + zod-to-json-schema@3.25.2(zod@3.25.76): + dependencies: + zod: 3.25.76 + + zod-to-ts@2.1.0(typescript@5.9.3)(zod@3.25.76): + dependencies: + typescript: 5.9.3 + zod: 3.25.76 + + zod@3.25.76: {} diff --git a/apps/mcp/src/client.ts b/apps/mcp/src/client.ts index ee35fcf1..1a405979 100644 --- a/apps/mcp/src/client.ts +++ b/apps/mcp/src/client.ts @@ -142,6 +142,7 @@ export class SupermemoryClient { this.client = new Supermemory({ apiKey: bearerToken, baseURL: apiUrl, + timeout: 30_000, }) this.containerTag = containerTag || DEFAULT_PROJECT_ID } @@ -336,6 +337,7 @@ export class SupermemoryClient { Authorization: `Bearer ${this.bearerToken}`, "Content-Type": "application/json", }, + signal: AbortSignal.timeout(30_000), }) if (!response.ok) { @@ -374,6 +376,7 @@ export class SupermemoryClient { order: "desc", containerTags, }), + signal: AbortSignal.timeout(30_000), }) if (!response.ok) { throw Object.assign(new Error("Failed to fetch documents"), { @@ -387,6 +390,16 @@ export class SupermemoryClient { } private handleError(error: unknown): never { + // Handle request timeout / abort + if ( + error instanceof Error && + (error.name === "AbortError" || error.name === "TimeoutError") + ) { + throw new Error( + "Request timed out after 30 seconds. The service may be slow or unavailable. Please try again.", + ) + } + // Handle network/fetch errors if (error instanceof TypeError) { if ( From d169dc078eaf796ce08676549f7fff6de71bb1d8 Mon Sep 17 00:00:00 2001 From: Mahesh Sanikommu Date: Fri, 26 Jun 2026 16:48:35 -0700 Subject: [PATCH 14/22] Fix brain home overflow (#1166) --- apps/web/components/brain-home/brain-home-view.tsx | 7 +++++-- apps/web/components/brain-home/connections-board.tsx | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/web/components/brain-home/brain-home-view.tsx b/apps/web/components/brain-home/brain-home-view.tsx index c5120fcc..bab6b4c8 100644 --- a/apps/web/components/brain-home/brain-home-view.tsx +++ b/apps/web/components/brain-home/brain-home-view.tsx @@ -128,7 +128,7 @@ export function BrainHomeView() { setupDone={stepsDone} /> -
+
+

{accent && ( From f1ff7beb0f6d96a4f95c191cff39ce916c5bb358 Mon Sep 17 00:00:00 2001 From: Dhravya Shah Date: Fri, 26 Jun 2026 20:51:40 -0700 Subject: [PATCH 15/22] Draft: Add chat source annotations (#1165) --- apps/web/components/chat/index.tsx | 11 +- .../components/chat/message/agent-message.tsx | 252 +++++++++++- apps/web/lib/chat-highlight-documents.ts | 24 +- apps/web/lib/chat-memory-tools.test.ts | 177 +++++++++ apps/web/lib/chat-memory-tools.ts | 375 ++++++++++++++++++ apps/web/lib/source-annotations.test.ts | 92 +++++ apps/web/lib/source-annotations.ts | 194 +++++++++ 7 files changed, 1103 insertions(+), 22 deletions(-) create mode 100644 apps/web/lib/chat-memory-tools.test.ts create mode 100644 apps/web/lib/chat-memory-tools.ts create mode 100644 apps/web/lib/source-annotations.test.ts create mode 100644 apps/web/lib/source-annotations.ts diff --git a/apps/web/components/chat/index.tsx b/apps/web/components/chat/index.tsx index a2d8e305..aae5920b 100644 --- a/apps/web/components/chat/index.tsx +++ b/apps/web/components/chat/index.tsx @@ -48,7 +48,10 @@ import { import { SpaceSelector } from "@/components/space-selector" import { SuperLoader } from "../superloader" import { UserMessage } from "./message/user-message" -import { AgentMessage } from "./message/agent-message" +import { + AgentMessage, + isChatToolDisplayPartType, +} from "./message/agent-message" import { ChatGraphContextRail } from "./chat-graph-context-rail" import { ChainOfThought } from "./input/chain-of-thought" import { useIsMobile } from "@hooks/use-mobile" @@ -1147,14 +1150,14 @@ export function ChatSidebar({ }) => ({ id: m.id, role: m.role, - // Strip tool parts (they break convertToModelMessages with tool_use/tool_result - // mismatches); keep text/reasoning + source parts so citations survive reload. + // Keep chat tool outputs that are meaningful to render after thread reload. parts: (m.parts || []).filter( (p) => p.type === "text" || p.type === "reasoning" || p.type === "source-url" || - p.type === "source-document", + p.type === "source-document" || + isChatToolDisplayPartType(p.type), ), metadata: m.metadata, createdAt: new Date(m.createdAt), diff --git a/apps/web/components/chat/message/agent-message.tsx b/apps/web/components/chat/message/agent-message.tsx index 1b7f69a8..eac0f7b9 100644 --- a/apps/web/components/chat/message/agent-message.tsx +++ b/apps/web/components/chat/message/agent-message.tsx @@ -2,6 +2,7 @@ import { type ReactNode, useEffect, useMemo, useRef, useState } from "react" import type { UIMessage } from "@ai-sdk/react" +import { useQuery } from "@tanstack/react-query" import { Streamdown } from "streamdown" import { BookOpenIcon, @@ -19,18 +20,37 @@ import { } from "lucide-react" import { cn } from "@lib/utils" import { isWebSearchToolName } from "@/lib/chat-web-search-tools" +import { + buildCitationIndex, + fetchDocumentsByIds, + getDocumentSourceUrl, + isMemoryToolOutputReady, + mapDocumentsByKnownIds, + type CitationTarget, + type DocumentWithMemories, + extractMemoryToolOutputs, +} from "@/lib/chat-memory-tools" +import { + parseSourceAnnotatedMarkdown, + stripSourceMarkup, +} from "@/lib/source-annotations" import { modelNames, type ModelId } from "@/lib/models" import { RelatedMemories } from "./related-memories" import { MessageActions } from "./message-actions" const TOOL_META: Record = { bash: { label: "Memory", icon: TerminalIcon }, + recallContext: { label: "Recall Memories", icon: BookOpenIcon }, + discoverSpaces: { label: "Discover Spaces", icon: SearchIcon }, web_search: { label: "Web search", icon: GlobeIcon }, google_search: { label: "Google search", icon: GlobeIcon }, // legacy tool names kept for existing persisted messages searchMemories: { label: "Search Memories", icon: SearchIcon }, addMemory: { label: "Add Memory", icon: PlusIcon }, fetchMemory: { label: "Fetch Memory", icon: BookOpenIcon }, + forgetMemory: { label: "Forget Memory", icon: XCircleIcon }, + updateMemory: { label: "Update Memory", icon: BookOpenIcon }, + forgetDocument: { label: "Forget Document", icon: XCircleIcon }, scheduleTask: { label: "Schedule Task", icon: ClockIcon }, listSchedules: { label: "List Schedules", icon: ListIcon }, cancelSchedule: { label: "Cancel Schedule", icon: XCircleIcon }, @@ -38,7 +58,7 @@ const TOOL_META: Record = { type ToolCallDisplayPart = { type: string - state: string + state?: string input?: unknown output?: unknown toolCallId?: string @@ -64,6 +84,20 @@ function faviconUrl(host: string): string { return `https://www.google.com/s2/favicons?sz=64&domain=${host}` } +function safeExternalUrl(url: string | null | undefined): string | null { + if (!url) return null + if (url.startsWith("/") && !url.startsWith("//")) return url + if (url.startsWith("#") && !url.startsWith("#sm-source:")) return url + try { + const parsed = new URL(url) + return parsed.protocol === "http:" || parsed.protocol === "https:" + ? url + : null + } catch { + return null + } +} + function isWebSearchPart(part: { type: string; toolName?: string }): boolean { if (part.type === "dynamic-tool") { return isWebSearchToolName(part.toolName ?? "") @@ -74,6 +108,25 @@ function isWebSearchPart(part: { type: string; toolName?: string }): boolean { return false } +function isMemoryRetrievalToolName(toolName: string): boolean { + return ( + toolName === "searchMemories" || + toolName === "recallContext" || + toolName === "discoverSpaces" + ) +} + +export function isChatToolDisplayPartType(type: string): boolean { + return ( + type === "tool-searchMemories" || + type === "tool-recallContext" || + type === "tool-discoverSpaces" || + type === "tool-forgetMemory" || + type === "tool-updateMemory" || + type === "tool-forgetDocument" + ) +} + function CitationLink({ href, label, @@ -83,7 +136,8 @@ function CitationLink({ label: string source?: SourceUrlPart }) { - const url = source?.url ?? href + const url = safeExternalUrl(source?.url ?? href) ?? "" + if (!url) return <>{label} const host = sourceHost(url) const rawTitle = source?.title?.trim() const hasTitle = @@ -136,9 +190,138 @@ function CitationLink({ ) } -function makeMarkdownComponents(sources: SourceUrlPart[]) { +function sourceTitle( + target: CitationTarget, + document?: DocumentWithMemories, +): string { + return ( + document?.title?.trim() || + target.title?.trim() || + document?.customId || + target.customId || + target.documentId || + target.sourceId + ) +} + +function sourceSummary( + target: CitationTarget, + document?: DocumentWithMemories, +): string | null { + const summary = + document?.summary || + target.summary || + (document as { content?: string } | undefined)?.content || + null + return summary ? summary.trim() : null +} + +function sourceKind( + target: CitationTarget, + document?: DocumentWithMemories, +): string { + return (document?.type || target.type || "memory").replaceAll("_", " ") +} + +function SourceCitationLink({ + sourceId, + children, + citationIndex, + documentByKnownId, +}: { + sourceId: string + children: ReactNode + citationIndex: Map + documentByKnownId: Map +}) { + const target = citationIndex.get(sourceId) + if (!target) return <>{children} + + const document = + (target.documentId + ? documentByKnownId.get(target.documentId) + : undefined) ?? + (target.customId ? documentByKnownId.get(target.customId) : undefined) + const url = safeExternalUrl( + document ? getDocumentSourceUrl(document) : target.url, + ) + const title = sourceTitle(target, document) + const summary = sourceSummary(target, document) + + return ( + + {url ? ( + + {children} + + ) : ( + + )} + + {sourceId} + + + + + + {title} + + + {sourceKind(target, document)} + + + {summary ? ( + + {summary} + + ) : null} + {url ? ( + + Open source + + ) : null} + + + + ) +} + +function makeMarkdownComponents( + sources: SourceUrlPart[], + citationIndex: Map, + documentByKnownId: Map, +) { return { a: ({ href, children }: { href?: string; children?: ReactNode }) => { + if (href?.startsWith("#sm-source:")) { + const sourceId = (() => { + try { + return decodeURIComponent(href.slice("#sm-source:".length)) + } catch { + return null + } + })() + if (!sourceId) return <>{children} + return ( + + {children} + + ) + } const label = typeof children === "string" ? children @@ -146,14 +329,16 @@ function makeMarkdownComponents(sources: SourceUrlPart[]) { ? children.join("") : "" const match = label.match(/^\[?(\d+)\]?$/) - if (match && href) { + const safeHref = safeExternalUrl(href) + if (match && safeHref) { const n = Number(match[1]) - const source = sources.find((s) => s.url === href) ?? sources[n - 1] - return + const source = sources.find((s) => s.url === safeHref) ?? sources[n - 1] + return } + if (!safeHref) return <>{children} return ( @@ -516,7 +704,38 @@ export function AgentMessage({ .filter((part) => part.type === "text") .map((part) => part.text) .join(" ") - const webSources = (() => { + const copyText = stripSourceMarkup(messageText) + const memoryOutputs = useMemo( + () => extractMemoryToolOutputs(message), + [message], + ) + const citationIndex = useMemo( + () => buildCitationIndex(memoryOutputs), + [memoryOutputs], + ) + const allowedSourceIds = useMemo( + () => new Set(citationIndex.keys()), + [citationIndex], + ) + const sourceDocumentIds = useMemo(() => { + const ids = new Set() + for (const target of citationIndex.values()) { + if (target.documentId) ids.add(target.documentId) + if (target.customId) ids.add(target.customId) + } + return [...ids].sort() + }, [citationIndex]) + const { data: sourceDocuments = [] } = useQuery({ + queryKey: ["chat-source-documents", sourceDocumentIds], + queryFn: () => fetchDocumentsByIds(sourceDocumentIds), + enabled: sourceDocumentIds.length > 0, + staleTime: 5 * 60 * 1000, + }) + const documentByKnownId = useMemo( + () => mapDocumentsByKnownIds(sourceDocuments), + [sourceDocuments], + ) + const webSources = useMemo(() => { const seen = new Set() const out: SourceUrlPart[] = [] for (const part of message.parts) { @@ -527,15 +746,13 @@ export function AgentMessage({ out.push(source) } return out - })() + }, [message.parts]) const hasAssistantText = message.parts.some( (p) => p.type === "text" && (p as { text?: string }).text?.trim(), ) - const sourceKey = webSources.map((s) => s.url).join("|") - // biome-ignore lint/correctness/useExhaustiveDependencies: keyed by stable source urls const markdownComponents = useMemo( - () => makeMarkdownComponents(webSources), - [sourceKey], + () => makeMarkdownComponents(webSources, citationIndex, documentByKnownId), + [webSources, citationIndex, documentByKnownId], ) const responseModelLabel = responseModel ? `${modelNames[responseModel].name} ${modelNames[responseModel].version}` @@ -603,7 +820,10 @@ export function AgentMessage({ className="text-sm text-white/90 chat-markdown-content" > - {runText} + { + parseSourceAnnotatedMarkdown(runText, allowedSourceIds) + .markdown + }

) @@ -613,7 +833,7 @@ export function AgentMessage({ type: "dynamic-tool" toolName: string toolCallId: string - state: string + state?: string input?: unknown output?: unknown errorText?: string @@ -651,7 +871,7 @@ export function AgentMessage({
() // [doc:] annotations from sgrep --include-doc-ids (highest confidence) for (const m of text.matchAll(DOC_ANNOTATION)) { - found.add(m[1]) + const id = m[1] + if (id) found.add(id) } // Standard UUID format for (const m of text.matchAll(UUID_IN_STRING)) { @@ -52,7 +57,8 @@ export function documentIdsFromBashText(text: string): string[] { const quoted = /"documentId"\s*:\s*"([^"]+)"/g let q = quoted.exec(text) while (q !== null) { - found.add(q[1]) + const id = q[1] + if (id) found.add(id) q = quoted.exec(text) } return [...found] @@ -136,9 +142,23 @@ export function extractHighlightDocumentIdsFromMessages( if (message.role !== "assistant") continue const parts = message.parts if (!parts) continue + for (const memoryOutput of extractMemoryToolOutputs(message)) { + for (const id of extractDocumentIdsFromMemoryOutput( + memoryOutput.output, + )) { + ids.add(id) + } + } for (const part of parts) { const p = part as Record + if ( + p.type === "tool-searchMemories" || + p.type === "tool-recallContext" || + p.type === "tool-discoverSpaces" + ) { + continue + } if (p.type === "source-document") { const sid = (p as { sourceId?: unknown }).sourceId diff --git a/apps/web/lib/chat-memory-tools.test.ts b/apps/web/lib/chat-memory-tools.test.ts new file mode 100644 index 00000000..9b6411c7 --- /dev/null +++ b/apps/web/lib/chat-memory-tools.test.ts @@ -0,0 +1,177 @@ +import { describe, expect, it } from "bun:test" +import { extractHighlightDocumentIdsFromMessages } from "./chat-highlight-documents" +import { + buildCitationIndex, + extractDocumentIdsFromMemoryOutput, + extractMemoryToolOutputs, + getDocumentSourceUrl, + mapDocumentsByKnownIds, +} from "./chat-memory-tools" + +const assistantMessage = { + id: "m1", + role: "assistant", + parts: [ + { + type: "tool-recallContext", + state: "output-available", + output: { + sourceIds: ["S1"], + documentIds: ["topDoc"], + results: [ + { + citationId: "S1", + content: "memo", + document: { + id: "docA", + customId: "customA", + title: "Doc A", + type: "google_doc", + summary: "sum", + }, + }, + ], + }, + }, + { + type: "tool-discoverSpaces", + state: "input-streaming", + output: { sourceIds: ["ignored"], documentIds: ["ignoredDoc"] }, + }, + { + type: "text", + text: 'Answer from memory', + }, + ], +} as const + +describe("chat memory tool citation mapping", () => { + it("extracts only ready memory tool outputs", () => { + const outputs = extractMemoryToolOutputs({ + parts: [ + ...assistantMessage.parts, + { + type: "tool-searchMemories", + state: "done", + output: { sourceIds: ["done"], documentIds: ["doneDoc"] }, + }, + { + type: "tool-searchMemories", + output: { sourceIds: ["stateless"], documentIds: ["statelessDoc"] }, + }, + ], + }) + + expect(outputs).toHaveLength(3) + expect(outputs.map((output) => output.output.sourceIds?.[0])).toEqual([ + "S1", + "done", + "stateless", + ]) + }) + + it("maps citation ids to document and custom ids", () => { + const [output] = extractMemoryToolOutputs(assistantMessage) + const index = buildCitationIndex(output ? [output] : []) + + expect(index.get("S1")?.documentId).toBe("docA") + expect(index.get("S1")?.customId).toBe("customA") + expect(index.has("ignored")).toBe(false) + }) + + it("extracts graph highlight document ids from memory outputs", () => { + const [output] = extractMemoryToolOutputs(assistantMessage) + + expect(output && extractDocumentIdsFromMemoryOutput(output.output)).toEqual( + ["topDoc", "docA", "customA"], + ) + expect( + extractHighlightDocumentIdsFromMessages([assistantMessage as never]), + ).toEqual(["topDoc", "docA", "customA"]) + }) + + it("keeps graph highlights for legacy memory tool states and ids", () => { + const legacyMessage = { + id: "legacy", + role: "assistant", + parts: [ + { + type: "tool-searchMemories", + state: "done", + output: { results: [{ id: "legacyDoc" }] }, + }, + { + type: "tool-recallContext", + output: { documentIds: ["statelessDoc"] }, + }, + ], + } as const + + expect(extractMemoryToolOutputs(legacyMessage)).toHaveLength(2) + expect( + extractHighlightDocumentIdsFromMessages([legacyMessage as never]), + ).toEqual(["legacyDoc", "statelessDoc"]) + }) + + it("normalizes nested discoverSpaces memory results", () => { + const outputs = extractMemoryToolOutputs({ + parts: [ + { + type: "tool-discoverSpaces", + state: "output-available", + output: { + spaces: [ + { + sourceIds: ["S2"], + documentIds: ["spaceDoc"], + results: [{ citationId: "S2", documentIds: ["nestedDoc"] }], + }, + ], + }, + }, + ], + }) + + const index = buildCitationIndex(outputs) + expect(index.get("S2")?.documentId).toBe("nestedDoc") + expect( + extractDocumentIdsFromMemoryOutput(outputs[0]?.output ?? {}), + ).toEqual(["spaceDoc", "nestedDoc"]) + }) + + it("builds editable Google source URLs from custom ids and API URLs", () => { + expect( + getDocumentSourceUrl({ + type: "google_doc", + customId: "docCustom", + url: "https://docs.googleapis.com/v1/documents/apiDoc", + } as never), + ).toBe("https://docs.google.com/document/d/docCustom/edit") + expect( + getDocumentSourceUrl({ + type: "google_doc", + url: "https://docs.googleapis.com/v1/documents/apiDoc", + } as never), + ).toBe("https://docs.google.com/document/d/apiDoc/edit") + expect( + getDocumentSourceUrl({ + type: "google_sheet", + url: "https://sheets.googleapis.com/v4/spreadsheets/sheetId/values/A1", + } as never), + ).toBe("https://docs.google.com/spreadsheets/d/sheetId/edit") + expect( + getDocumentSourceUrl({ + type: "google_slide", + url: "https://slides.googleapis.com/v1/presentations/slideId/pages", + } as never), + ).toBe("https://docs.google.com/presentation/d/slideId/edit") + }) + + it("maps documents by all known ids", () => { + const mapped = mapDocumentsByKnownIds([ + { id: "docA", customId: "customA", type: "text", url: null } as never, + ]) + expect(mapped.get("docA")?.id).toBe("docA") + expect(mapped.get("customA")?.id).toBe("docA") + }) +}) diff --git a/apps/web/lib/chat-memory-tools.ts b/apps/web/lib/chat-memory-tools.ts new file mode 100644 index 00000000..140c2ae6 --- /dev/null +++ b/apps/web/lib/chat-memory-tools.ts @@ -0,0 +1,375 @@ +import { $fetch } from "@lib/api" +import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api" +import type { z } from "zod" +import { isSafeSourceId } from "./source-annotations" + +export const MEMORY_TOOL_PART_TYPES = [ + "tool-searchMemories", + "tool-recallContext", + "tool-discoverSpaces", +] as const +export const MAX_INLINE_GRAPH_DOCUMENT_IDS = 20 + +export type MemoryToolName = + | "searchMemories" + | "recallContext" + | "discoverSpaces" +export type ToolDocumentMetadata = { + id?: string | undefined + internalDocumentId?: string | undefined + customId?: string | null | undefined + title?: string | null | undefined + type?: string | null | undefined + summary?: string | null | undefined + url?: string | null | undefined +} +export type MemoryToolResultItem = { + id?: string | undefined + citationId?: string | undefined + kind?: "memory" | "chunk" | "aggregate" | string | undefined + content?: string | undefined + score?: number | undefined + documentId?: string | undefined + documentIds?: string[] | undefined + internalDocumentId?: string | undefined + customId?: string | undefined + documents?: ToolDocumentMetadata[] | undefined + document?: ToolDocumentMetadata | undefined +} +export type MemoryToolResult = { + query?: string | undefined + count?: number | undefined + sourceIds?: string[] | undefined + documentIds?: string[] | undefined + results?: MemoryToolResultItem[] | undefined + spaces?: + | Array<{ + results?: MemoryToolResultItem[] | undefined + sourceIds?: string[] | undefined + documentIds?: string[] | undefined + }> + | undefined +} +export type MemoryToolOutput = { + output: MemoryToolResult +} +export type CitationTarget = { + sourceId: string + documentId?: string | undefined + customId?: string | null | undefined + title?: string | null | undefined + type?: string | null | undefined + summary?: string | null | undefined + url?: string | null | undefined +} + +export type DocumentWithMemories = z.infer< + typeof DocumentsWithMemoriesResponseSchema +>["documents"][0] + +function isObject(value: unknown): value is Record { + return typeof value === "object" && value !== null +} + +function strings(values: unknown): string[] { + if (!Array.isArray(values)) return [] + return values.filter( + (value): value is string => typeof value === "string" && value.length > 0, + ) +} + +function normalizeOutput(output: unknown): MemoryToolResult { + if (!isObject(output)) return {} + + const nested = [output] + for (const key of [ + "memory", + "search", + "searchResult", + "memoryResult", + "memoryOutput", + "hints", + ]) { + const value = output[key] + if (isObject(value)) nested.push(value) + } + + const sourceIds: string[] = [] + const documentIds: string[] = [] + const results: MemoryToolResultItem[] = [] + + const merged: MemoryToolResult = { + query: typeof output.query === "string" ? output.query : undefined, + count: typeof output.count === "number" ? output.count : undefined, + sourceIds, + documentIds, + results, + spaces: Array.isArray(output.spaces) + ? (output.spaces.filter(isObject) as MemoryToolResult["spaces"]) + : undefined, + } + + for (const value of nested) { + sourceIds.push(...strings(value.sourceIds)) + documentIds.push(...strings(value.documentIds)) + if (Array.isArray(value.results)) + results.push( + ...(value.results.filter(isObject) as MemoryToolResultItem[]), + ) + } + + for (const space of merged.spaces ?? []) { + sourceIds.push(...strings(space.sourceIds)) + documentIds.push(...strings(space.documentIds)) + if (Array.isArray(space.results)) + results.push( + ...(space.results.filter(isObject) as MemoryToolResultItem[]), + ) + } + + merged.sourceIds = dedupe(merged.sourceIds ?? []) + merged.documentIds = dedupe(merged.documentIds ?? []) + return merged +} + +function dedupe(values: string[]): string[] { + return Array.from(new Set(values.filter(Boolean))) +} + +function addTarget( + index: Map, + key: unknown, + target: CitationTarget, +) { + if (typeof key !== "string" || !isSafeSourceId(key)) return + if (!index.has(key)) index.set(key, { ...target, sourceId: key }) +} + +function citationTargetForResult( + sourceId: string, + result: MemoryToolResultItem, +): CitationTarget { + const doc = firstDocumentForResult(result) + const docId = + doc?.internalDocumentId ?? + result.internalDocumentId ?? + doc?.id ?? + result.documentIds?.find(Boolean) ?? + result.documentId + const target = documentTarget(sourceId, doc) + target.documentId = target.documentId ?? docId + target.customId = target.customId ?? result.customId + return target +} + +function documentTarget( + sourceId: string, + doc?: ToolDocumentMetadata | null, +): CitationTarget { + return { + sourceId, + documentId: doc?.internalDocumentId ?? doc?.id, + customId: + doc?.customId ?? + (doc?.internalDocumentId && doc.id !== doc.internalDocumentId + ? doc.id + : undefined), + title: doc?.title, + type: doc?.type, + summary: doc?.summary, + url: doc?.url, + } +} + +function firstDocumentForResult( + result: MemoryToolResultItem, +): ToolDocumentMetadata | null { + if (result.document && isObject(result.document)) return result.document + if (Array.isArray(result.documents) && result.documents.length > 0) + return result.documents.find(isObject) ?? null + const firstId = + result.documentIds?.find(Boolean) ?? + result.documentId ?? + result.internalDocumentId ?? + result.customId + return firstId ? { id: firstId, customId: result.customId } : null +} + +export function isMemoryToolOutputReady( + part: Record, +): boolean { + return ( + part.state === "output-available" || + part.state === "done" || + (part.state === undefined && part.output !== undefined) + ) +} + +export function extractMemoryToolOutputs(message: { + parts?: readonly unknown[] +}): MemoryToolOutput[] { + const parts = Array.isArray(message.parts) ? message.parts : [] + const outputs: MemoryToolOutput[] = [] + + for (let partIndex = 0; partIndex < parts.length; partIndex++) { + const part = parts[partIndex] + if (!isObject(part)) continue + const type = part.type + if ( + typeof type !== "string" || + !MEMORY_TOOL_PART_TYPES.includes( + type as (typeof MEMORY_TOOL_PART_TYPES)[number], + ) + ) + continue + if (!isMemoryToolOutputReady(part)) continue + outputs.push({ output: normalizeOutput(part.output) }) + } + + return outputs +} + +export function buildCitationIndex( + outputs: MemoryToolOutput[], +): Map { + const index = new Map() + + for (const { output } of outputs) { + for (const result of output.results ?? []) { + if (result.citationId) + addTarget( + index, + result.citationId, + citationTargetForResult(result.citationId, result), + ) + } + + for (const sourceId of output.sourceIds ?? []) { + if (index.has(sourceId)) continue + const matchingResult = (output.results ?? []).find( + (result) => result.citationId === sourceId, + ) + if (matchingResult) + addTarget( + index, + sourceId, + citationTargetForResult(sourceId, matchingResult), + ) + } + } + + return index +} + +export function extractDocumentIdsFromMemoryOutput( + output: MemoryToolResult, +): string[] { + const ids: string[] = [] + ids.push(...(output.documentIds ?? [])) + for (const result of output.results ?? []) { + if (result.id) ids.push(result.id) + if (result.documentId) ids.push(result.documentId) + if (result.internalDocumentId) ids.push(result.internalDocumentId) + ids.push(...(result.documentIds ?? [])) + if (result.document?.id) ids.push(result.document.id) + if (result.document?.customId) ids.push(result.document.customId) + for (const doc of result.documents ?? []) { + if (doc.internalDocumentId) ids.push(doc.internalDocumentId) + if (doc.id) ids.push(doc.id) + if (doc.customId) ids.push(doc.customId) + } + } + return dedupe(ids).slice(0, MAX_INLINE_GRAPH_DOCUMENT_IDS) +} + +export async function fetchDocumentsByIds( + ids: string[], +): Promise { + const uniqueIds = dedupe(ids) + if (uniqueIds.length === 0) return [] + + const fetchBy = async (by: "id" | "customId", requestedIds: string[]) => { + const response = await $fetch("@post/documents/documents/by-ids", { + body: { + ids: requestedIds, + by, + }, + disableValidation: true, + }) + const result = response as { + error?: { message?: string } | null + data?: { documents?: DocumentWithMemories[] } | null + } + if (result.error) { + throw new Error("Failed to fetch source documents", { + cause: result.error, + }) + } + return result.data?.documents ?? [] + } + + const byIdDocs = await fetchBy("id", uniqueIds) + const seen = new Set() + const foundLookup = new Set() + for (const doc of byIdDocs) { + if (doc.id) { + seen.add(doc.id) + foundLookup.add(doc.id) + } + if (doc.customId) foundLookup.add(doc.customId) + } + + const unresolved = uniqueIds.filter((id) => !foundLookup.has(id)) + const byCustomDocs = + unresolved.length > 0 ? await fetchBy("customId", unresolved) : [] + const merged = [...byIdDocs] + for (const doc of byCustomDocs) { + if (doc.id && !seen.has(doc.id)) { + seen.add(doc.id) + merged.push(doc) + } + } + + return merged +} + +export function mapDocumentsByKnownIds( + documents: DocumentWithMemories[], +): Map { + const map = new Map() + for (const doc of documents) { + if (doc.id) map.set(doc.id, doc) + if (doc.customId) map.set(doc.customId, doc) + } + return map +} + +export function getDocumentSourceUrl( + document: Pick & { + customId?: string | null + }, +) { + const url = document.url ?? null + const googleDocTypes: Record = + { + google_doc: { + prefix: "https://docs.google.com/document/d/", + apiPattern: /docs\.googleapis\.com\/v1\/documents\/([A-Za-z0-9_-]+)/, + }, + google_sheet: { + prefix: "https://docs.google.com/spreadsheets/d/", + apiPattern: + /sheets\.googleapis\.com\/v4\/spreadsheets\/([A-Za-z0-9_-]+)/, + }, + google_slide: { + prefix: "https://docs.google.com/presentation/d/", + apiPattern: + /slides\.googleapis\.com\/v1\/presentations\/([A-Za-z0-9_-]+)/, + }, + } + const googleDoc = document.type ? googleDocTypes[document.type] : undefined + if (!googleDoc) return url + if (document.customId) return `${googleDoc.prefix}${document.customId}/edit` + const apiId = url?.match(googleDoc.apiPattern)?.[1] + return apiId ? `${googleDoc.prefix}${apiId}/edit` : url +} diff --git a/apps/web/lib/source-annotations.test.ts b/apps/web/lib/source-annotations.test.ts new file mode 100644 index 00000000..dce37e1a --- /dev/null +++ b/apps/web/lib/source-annotations.test.ts @@ -0,0 +1,92 @@ +import { describe, expect, it } from "bun:test" +import { + isSafeSourceId, + parseSourceAnnotatedMarkdown, + stripSourceMarkup, +} from "./source-annotations" + +describe("source annotation parsing", () => { + it("turns allowed response source spans into internal citation links", () => { + const parsed = parseSourceAnnotatedMarkdown( + 'Alpha Beta [x] Gamma', + new Set(["S1"]), + ) + + expect(parsed.markdown).toBe("Alpha [Beta \\[x\\]](#sm-source:S1) Gamma") + }) + + it("renders repeated allowed citations as separate internal links", () => { + const parsed = parseSourceAnnotatedMarkdown( + 'First and Second', + new Set(["S1"]), + ) + + expect(parsed.markdown).toBe( + "[First](#sm-source:S1) and [Second](#sm-source:S1)", + ) + }) + + it("renders unknown or unsafe source ids as plain text", () => { + expect( + parseSourceAnnotatedMarkdown( + 'Unknown plain', + new Set(["S1"]), + ).markdown, + ).toBe("Unknown plain") + + expect( + parseSourceAnnotatedMarkdown( + 'Unsafe plain', + new Set(["bad/id"]), + ).markdown, + ).toBe("Unsafe plain") + }) + + it("keeps unclosed, incomplete, nested, and malformed source markup safe", () => { + expect( + parseSourceAnnotatedMarkdown( + 'Lead unfinished answer', + new Set(["S1"]), + ).markdown, + ).toBe("Lead unfinished answer") + + expect( + parseSourceAnnotatedMarkdown("Lead A B C', + new Set(["S1", "S2"]), + ).markdown, + ).toBe("Outer A B C") + + expect( + parseSourceAnnotatedMarkdown( + "Malformed plain", + new Set(["S1"]), + ).markdown, + ).toBe("Malformed plain") + }) + + it("does not mutate inline code or fenced code blocks", () => { + const input = + '`code`\n```\nfenced\n```' + + expect(parseSourceAnnotatedMarkdown(input, new Set(["S1"])).markdown).toBe( + input, + ) + }) + + it("strips source markup for copy text", () => { + expect( + stripSourceMarkup('Alpha Beta'), + ).toBe("Alpha Beta") + }) + + it("allows only source ids that are safe in internal fragments", () => { + expect(isSafeSourceId("S1._:-")).toBe(true) + expect(isSafeSourceId("bad/id")).toBe(false) + expect(isSafeSourceId("bad space")).toBe(false) + }) +}) diff --git a/apps/web/lib/source-annotations.ts b/apps/web/lib/source-annotations.ts new file mode 100644 index 00000000..7abe6859 --- /dev/null +++ b/apps/web/lib/source-annotations.ts @@ -0,0 +1,194 @@ +export type ParsedSourceAnnotations = { + markdown: string +} + +const RESPONSE_OPEN_PREFIX = " 0 && SAFE_SOURCE_ID_RE.test(id) +} + +function escapeMarkdownLinkText(text: string): string { + return text.replace(/([\\[\]])/g, "\\$1").replace(/\n/g, " ") +} + +function parseOpeningTag( + text: string, + index: number, +): { end: number; sourceId: string } | null | "incomplete" { + if (!text.startsWith(RESPONSE_OPEN_PREFIX, index)) return null + + const tagEnd = text.indexOf(">", index + RESPONSE_OPEN_PREFIX.length) + if (tagEnd === -1) return "incomplete" + + const rawTag = text.slice(index, tagEnd + 1) + const inside = rawTag.slice(1, -1).trim() + if (!inside.startsWith("response")) return null + + let cursor = "response".length + while ( + inside[cursor] === " " || + inside[cursor] === "\t" || + inside[cursor] === "\n" || + inside[cursor] === "\r" + ) + cursor++ + if (!inside.startsWith(SOURCE_ATTR_PREFIX, cursor)) return null + cursor += SOURCE_ATTR_PREFIX.length + + const sourceEnd = inside.indexOf('"', cursor) + if (sourceEnd === -1) return null + const sourceId = inside.slice(cursor, sourceEnd) + cursor = sourceEnd + 1 + while ( + inside[cursor] === " " || + inside[cursor] === "\t" || + inside[cursor] === "\n" || + inside[cursor] === "\r" + ) + cursor++ + if (cursor !== inside.length) return null + if (!isSafeSourceId(sourceId)) return null + + return { end: tagEnd + 1, sourceId } +} + +function advanceCodeState( + text: string, + index: number, + state: { inFence: boolean; inInlineCode: boolean; lineStart: boolean }, +): boolean { + if (state.lineStart && text.startsWith("```", index)) { + state.inFence = !state.inFence + return true + } + + if (!state.inFence && text[index] === "`") { + state.inInlineCode = !state.inInlineCode + return true + } + + return false +} + +function appendChar( + text: string, + index: number, + output: string[], + state: { lineStart: boolean }, +) { + const ch = text[index] ?? "" + output.push(ch) + state.lineStart = ch === "\n" +} + +export function parseSourceAnnotatedMarkdown( + text: string, + allowedSourceIds: ReadonlySet, +): ParsedSourceAnnotations { + const output: string[] = [] + const codeState = { inFence: false, inInlineCode: false, lineStart: true } + + let i = 0 + while (i < text.length) { + if (advanceCodeState(text, i, codeState)) { + appendChar(text, i, output, codeState) + i++ + continue + } + + if ( + !codeState.inFence && + !codeState.inInlineCode && + text.startsWith(RESPONSE_OPEN_PREFIX, i) + ) { + const opening = parseOpeningTag(text, i) + if (opening === "incomplete") { + break + } + + if (opening) { + const closeIndex = text.indexOf(RESPONSE_CLOSE_TAG, opening.end) + if (closeIndex === -1) { + output.push(stripSourceMarkup(text.slice(opening.end))) + break + } + + const inner = text.slice(opening.end, closeIndex) + const hasNested = + inner.includes(RESPONSE_OPEN_PREFIX) || + inner.includes(RESPONSE_CLOSE_TAG) + const isAllowed = allowedSourceIds.has(opening.sourceId) + + if (hasNested) { + const outerCloseIndex = text.indexOf( + RESPONSE_CLOSE_TAG, + closeIndex + RESPONSE_CLOSE_TAG.length, + ) + const fallbackEnd = + outerCloseIndex === -1 ? closeIndex : outerCloseIndex + output.push(stripSourceMarkup(text.slice(opening.end, fallbackEnd))) + i = fallbackEnd + RESPONSE_CLOSE_TAG.length + continue + } + + const plainInner = stripSourceMarkup(inner) + if (isAllowed && plainInner.trim().length > 0) { + output.push( + `[${escapeMarkdownLinkText(plainInner)}](#sm-source:${encodeURIComponent(opening.sourceId)})`, + ) + } else { + output.push(plainInner) + } + + i = closeIndex + RESPONSE_CLOSE_TAG.length + codeState.lineStart = + output.length === 0 || + output[output.length - 1]?.endsWith("\n") === true + continue + } + + const nextClose = text.indexOf(RESPONSE_CLOSE_TAG, i) + if (nextClose !== -1) { + const tagEnd = text.indexOf(">", i) + if (tagEnd !== -1 && tagEnd < nextClose) { + output.push(stripSourceMarkup(text.slice(tagEnd + 1, nextClose))) + i = nextClose + RESPONSE_CLOSE_TAG.length + continue + } + } + } + + appendChar(text, i, output, codeState) + i++ + } + + return { markdown: output.join("") } +} + +export function stripSourceMarkup(text: string): string { + let output = "" + let i = 0 + + while (i < text.length) { + if (text.startsWith(RESPONSE_CLOSE_TAG, i)) { + i += RESPONSE_CLOSE_TAG.length + continue + } + + if (text.startsWith(RESPONSE_OPEN_PREFIX, i)) { + const tagEnd = text.indexOf(">", i + RESPONSE_OPEN_PREFIX.length) + if (tagEnd === -1) break + i = tagEnd + 1 + continue + } + + output += text[i] + i++ + } + + return output +} From 3a9310778af85b7d2e28eb9a80c9da74ceb2048b Mon Sep 17 00:00:00 2001 From: ved015 <122012786+ved015@users.noreply.github.com> Date: Sat, 27 Jun 2026 14:27:36 +0000 Subject: [PATCH 16/22] Fix settings organization flows (#1159) ## Summary - Fix delete-organization dialog focus when switching orgs inside Settings. - Restyle delete-organization modal to match the app modal theme and remove extra organization icons. - Make the organization switcher list scrollable when many orgs exist. - Send Create organization directly to onboarding instead of opening the create-org modal. - Stop onboarding from completing when org creation fails, show an error toast, and return existing users to the dashboard. --- apps/web/app/(app)/onboarding/page.tsx | 29 ++- .../components/settings/settings-content.tsx | 191 +++++++++++++----- .../components/settings/settings-modal.tsx | 5 + .../settings/settings-org-switcher.tsx | 181 +++++------------ packages/ui/components/dialog.tsx | 7 +- 5 files changed, 217 insertions(+), 196 deletions(-) diff --git a/apps/web/app/(app)/onboarding/page.tsx b/apps/web/app/(app)/onboarding/page.tsx index a8e0499e..c819d0f8 100644 --- a/apps/web/app/(app)/onboarding/page.tsx +++ b/apps/web/app/(app)/onboarding/page.tsx @@ -39,6 +39,16 @@ const STORAGE_KEY = "supermemory-brain-onboarding-v1" const countsAsConnectedSource = (state: unknown) => state === "connected" || state === "waitlist" +const getErrorMessage = (error: unknown, fallback: string) => { + if (error instanceof Error && error.message) return error.message + if (typeof error === "string" && error.trim()) return error + if (typeof error === "object" && error !== null && "message" in error) { + const message = (error as { message?: unknown }).message + if (typeof message === "string" && message.trim()) return message + } + return fallback +} + export default function BrainOnboardingPage() { const router = useRouter() const params = useSearchParams() @@ -240,7 +250,12 @@ export default function BrainOnboardingPage() { slug, metadata, }) - await setActiveOrg(result.data?.slug ?? slug) + if (result.error || !result.data?.slug) { + throw new Error( + getErrorMessage(result.error, "Organization was not created."), + ) + } + await setActiveOrg(result.data.slug) if (about.name.trim()) { await authClient.updateUser({ name: about.name.trim(), @@ -283,16 +298,22 @@ export default function BrainOnboardingPage() { await ensureOrg() goNext() } catch (e) { + const message = getErrorMessage(e, "Organization was not created.") console.error("Failed to create organization:", e) analytics.onboardingWorkspaceCreateFailed({ - error: e instanceof Error ? e.message : String(e), + error: message, }) - toast.error("Couldn't create your workspace. Please try again.") + toast.error("Organization was not created", { + description: "Please try again from Settings.", + }) + if (forceCreate && (organizations?.length ?? 0) > 0) { + router.replace("/") + } } finally { creatingOrgRef.current = false setCreatingOrg(false) } - }, [ensureOrg, goNext]) + }, [ensureOrg, goNext, forceCreate, organizations, router]) const [sendingInvites, setSendingInvites] = useState(false) const sendingInvitesRef = useRef(false) diff --git a/apps/web/components/settings/settings-content.tsx b/apps/web/components/settings/settings-content.tsx index a257c223..429274b6 100644 --- a/apps/web/components/settings/settings-content.tsx +++ b/apps/web/components/settings/settings-content.tsx @@ -3,7 +3,7 @@ import { Logo } from "@ui/assets/Logo" import { useAuth } from "@lib/auth-context" import NovaOrb from "@/components/nova/nova-orb" -import { useRef, useState } from "react" +import { useEffect, useRef, useState } from "react" import { cn } from "@lib/utils" import { dmSansClassName, dmSans125ClassName } from "@/lib/fonts" import Account from "@/components/settings/account" @@ -31,9 +31,15 @@ import { ChevronRight, ArrowUpRight, Building2, + X, } from "lucide-react" import { authClient } from "@lib/auth" -import { Dialog, DialogContent, DialogClose } from "@ui/components/dialog" +import { + Dialog, + DialogContent, + DialogClose, + DialogTitle, +} from "@ui/components/dialog" import { Popover, PopoverContent, PopoverTrigger } from "@ui/components/popover" import { useResetOrganization } from "@/hooks/use-reset-organization" import { useDeleteUserAccount } from "@/hooks/use-account-settings" @@ -96,6 +102,11 @@ const NAV_ITEMS: NavItem[] = [ }, ] +const MODAL_SURFACE_SHADOW = + "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" + +const INSET_SHADOW = "inset 1.313px 1.313px 3.938px rgba(0,0,0,0.7)" + export function parseHashToTab(hash: string): SettingsTab { const cleaned = hash.replace("#", "").toLowerCase() return TABS.includes(cleaned as SettingsTab) @@ -132,11 +143,13 @@ function IdentityCard({ displayName }: { displayName: string }) { export function SettingsContent({ activeTab, onTabChange, + dialogPortalContainer, className, showIdentity = true, }: { activeTab: SettingsTab onTabChange: (tab: SettingsTab) => void + dialogPortalContainer?: HTMLElement | null className?: string showIdentity?: boolean }) { @@ -156,7 +169,12 @@ export function SettingsContent({ const [isDeleteOrgDialogOpen, setIsDeleteOrgDialogOpen] = useState(false) const [deleteOrgConfirm, setDeleteOrgConfirm] = useState("") const deleteOrgInputRef = useRef(null) + const deleteOrgDialogTimerRef = useRef | null>( + null, + ) const deleteOrganization = useDeleteOrganization() + const activeOrgId = org?.id + const previousOrgIdRef = useRef(activeOrgId) // Only owners can delete the organization. const activeMemberRoleQuery = useQuery({ @@ -175,11 +193,44 @@ export function SettingsContent({ const [dangerMenuOpen, setDangerMenuOpen] = useState(false) + useEffect(() => { + if (previousOrgIdRef.current === activeOrgId) return + previousOrgIdRef.current = activeOrgId + setDangerMenuOpen(false) + setIsDeleteOrgDialogOpen(false) + setDeleteOrgConfirm("") + }, [activeOrgId]) + + useEffect(() => { + if (!isDeleteOrgDialogOpen) return + + document.body.style.pointerEvents = "" + const focusTimer = setTimeout(() => { + deleteOrgInputRef.current?.focus() + }, 0) + + return () => clearTimeout(focusTimer) + }, [isDeleteOrgDialogOpen]) + + useEffect(() => { + return () => { + if (deleteOrgDialogTimerRef.current) { + clearTimeout(deleteOrgDialogTimerRef.current) + } + } + }, []) + const openDeleteOrganizationDialog = () => { setDangerMenuOpen(false) - window.requestAnimationFrame(() => { + setDeleteOrgConfirm("") + if (deleteOrgDialogTimerRef.current) { + clearTimeout(deleteOrgDialogTimerRef.current) + } + deleteOrgDialogTimerRef.current = setTimeout(() => { + document.body.style.pointerEvents = "" setIsDeleteOrgDialogOpen(true) - }) + deleteOrgDialogTimerRef.current = null + }, 120) } const displayName = @@ -610,6 +661,7 @@ export function SettingsContent({ {/* Delete organization dialog */} { setIsDeleteOrgDialogOpen(open) @@ -617,33 +669,58 @@ export function SettingsContent({ }} > { event.preventDefault() deleteOrgInputRef.current?.focus() }} > -
-
-

+
+
+ Delete this organization? -

-

- Permanently deletes{" "} - - {org?.name || "this organization"} - {" "} - — its documents, spaces, connections, and members.{" "} - - This cannot be undone. - + +

+ This action permanently removes the selected workspace.

-
-

- Type {org?.name} to - confirm: -

+ + + +
+ +
+

+ Permanently deletes{" "} + + {org?.name || "this organization"} + {" "} + and all of its documents, spaces, connections, and members.{" "} + + This cannot be undone. + +

+
-
- - - + +
+ +
+ -
+ +
diff --git a/apps/web/components/settings/settings-modal.tsx b/apps/web/components/settings/settings-modal.tsx index 89c52a3f..f94b889d 100644 --- a/apps/web/components/settings/settings-modal.tsx +++ b/apps/web/components/settings/settings-modal.tsx @@ -5,6 +5,7 @@ import { useCallback, useContext, useMemo, + useState, type ReactNode, } from "react" import { useQueryState } from "nuqs" @@ -50,6 +51,8 @@ const SettingsModalContext = createContext( export function SettingsModalProvider({ children }: { children: ReactNode }) { const [param, setParam] = useQueryState(SETTINGS_PARAM) + const [settingsDialogContent, setSettingsDialogContent] = + useState(null) const open = param !== null const tab = parseTab(param) @@ -85,6 +88,7 @@ export function SettingsModalProvider({ children }: { children: ReactNode }) { }} > diff --git a/apps/web/components/settings/settings-org-switcher.tsx b/apps/web/components/settings/settings-org-switcher.tsx index 217f30b3..1e2dcd82 100644 --- a/apps/web/components/settings/settings-org-switcher.tsx +++ b/apps/web/components/settings/settings-org-switcher.tsx @@ -1,6 +1,6 @@ "use client" -import { useEffect, useMemo, useState } from "react" +import { useMemo, useState } from "react" import { useRouter } from "next/navigation" import { useCustomer } from "autumn-js/react" import { toast } from "sonner" @@ -12,17 +12,13 @@ import { Plus, } from "lucide-react" import { cn } from "@lib/utils" -import { dmSansClassName, dmSans125ClassName } from "@/lib/fonts" +import { dmSansClassName } from "@/lib/fonts" import { useAuth } from "@lib/auth-context" import { Popover, PopoverContent, PopoverTrigger } from "@ui/components/popover" -import { Dialog, DialogContent, DialogTitle } from "@ui/components/dialog" import { OrgPlanBadge, resolveOrgPlan } from "@/components/org-plan-badge" import { useOrgSummaries } from "@/hooks/use-org-summaries" import { useTokenUsage, type PlanType } from "@/hooks/use-token-usage" -const SURFACE_SHADOW = - "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" - export function SettingsOrgSwitcher() { const { org, organizations, setActiveOrg } = useAuth() const router = useRouter() @@ -32,15 +28,6 @@ export function SettingsOrgSwitcher() { const [open, setOpen] = useState(false) const [switchingId, setSwitchingId] = useState(null) - const [createOpen, setCreateOpen] = useState(false) - const [createName, setCreateName] = useState("") - const [creating, setCreating] = useState(false) - - // Safety net: a prior Radix overlay can leave `pointer-events: none` stuck on - // , making the dialog appear but ignore clicks. Clear it when it opens. - useEffect(() => { - if (createOpen) document.body.style.pointerEvents = "" - }, [createOpen]) const planByOrgId = useMemo(() => { const map = new Map() @@ -77,50 +64,45 @@ export function SettingsOrgSwitcher() { } const handleCreate = () => { - const name = createName.trim() - if (!name || creating) return - setCreating(true) - // Org creation now happens through the onboarding flow (team/personal, name, invites). - setCreateOpen(false) setOpen(false) - router.push(`/onboarding?new=1&name=${encodeURIComponent(name)}`) + router.push("/onboarding?new=1") } return ( - <> - - - - - + + + + +
event.stopPropagation()} + onTouchMoveCapture={(event) => event.stopPropagation()} > {sortedOrgs.map((organization) => { const isCurrent = organization.id === org?.id @@ -156,92 +138,19 @@ export function SettingsOrgSwitcher() { ) })} +
-
+
- - - - - { - setCreateOpen(next) - if (!next) setCreateName("") - }} - > - -
-
- - Create organization - -

- A separate workspace with its own memories, connections, and - members. -

-
- setCreateName(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") handleCreate() - }} - placeholder="Organization name" - maxLength={80} - className="w-full rounded-xl border border-[#2A2D35] bg-[#0D0F14] px-4 py-2.5 text-sm text-white placeholder:text-[#525D6E] focus:outline-none focus:border-[#4BA0FA]/50 transition-colors" - /> -
- - -
-
-
-
- + + Create organization + + + ) } diff --git a/packages/ui/components/dialog.tsx b/packages/ui/components/dialog.tsx index 87d295c1..114a6306 100644 --- a/packages/ui/components/dialog.tsx +++ b/packages/ui/components/dialog.tsx @@ -48,13 +48,18 @@ function DialogOverlay({ function DialogContent({ className, children, + portalContainer, showCloseButton = true, ...props }: React.ComponentProps & { + portalContainer?: HTMLElement | null showCloseButton?: boolean }) { return ( - + Date: Sat, 27 Jun 2026 23:25:37 +0530 Subject: [PATCH 17/22] Show the orbit animation on the login panel (#1168) Co-authored-by: Claude Opus 4.8 --- apps/web/components/login-tools-panel.tsx | 382 +------------- apps/web/components/orbit-memory.tsx | 582 ++++++++++++++++++++++ 2 files changed, 602 insertions(+), 362 deletions(-) create mode 100644 apps/web/components/orbit-memory.tsx diff --git a/apps/web/components/login-tools-panel.tsx b/apps/web/components/login-tools-panel.tsx index 8b6f1da1..40c3b6d5 100644 --- a/apps/web/components/login-tools-panel.tsx +++ b/apps/web/components/login-tools-panel.tsx @@ -1,367 +1,8 @@ "use client" -import Image from "next/image" -import { useEffect, useState } from "react" -import { motion } from "motion/react" import { cn } from "@lib/utils" import { dmSansClassName } from "@/lib/fonts" -import { ChromeIcon, RaycastIcon } from "@/components/integration-icons" -import { - ClaudeDesktopIcon, - GoogleDrive, - MCPIcon, - Notion, -} from "@ui/assets/icons" -import { Logo } from "@ui/assets/Logo" -import NovaOrb from "@/components/nova/nova-orb" - -type ToolNode = { - id: string - name: string - x: number - y: number - icon?: React.ComponentType<{ className?: string }> - iconSrc?: string -} - -type ContextConnection = { - from: ToolNode - to: ToolNode -} - -type ContextPhase = "idle" | "capture" | "hold" | "recall" - -const CENTER = { x: 50, y: 50 } -const IN_MS = 1100 -const HOLD_MS = 700 -const OUT_MS = 1100 -const TOTAL_MS = IN_MS + HOLD_MS + OUT_MS - -const TOOL_NODES: ToolNode[] = [ - { id: "chrome", name: "Chrome", x: 14, y: 20, icon: ChromeIcon }, - { id: "notion", name: "Notion", x: 84, y: 16, icon: Notion }, - { id: "drive", name: "Google Drive", x: 10, y: 52, icon: GoogleDrive }, - { id: "claude", name: "Claude", x: 90, y: 44, icon: ClaudeDesktopIcon }, - { id: "raycast", name: "Raycast", x: 76, y: 76, icon: RaycastIcon }, - { id: "mcp", name: "MCP", x: 22, y: 84, icon: MCPIcon }, - { - id: "claude-code", - name: "Claude Code", - x: 20, - y: 36, - iconSrc: "/images/plugins/claude-code.svg", - }, - { - id: "codex", - name: "Codex", - x: 92, - y: 28, - iconSrc: "/images/plugins/codex.png", - }, - { - id: "opencode", - name: "OpenCode", - x: 58, - y: 10, - iconSrc: "/images/plugins/opencode.svg", - }, - { - id: "hermes", - name: "Hermes", - x: 36, - y: 90, - iconSrc: "/images/plugins/hermes.svg", - }, - { - id: "openclaw", - name: "OpenClaw", - x: 68, - y: 68, - iconSrc: "/images/plugins/openclaw.svg", - }, -] - -const CONTEXT_FLOWS: [string, string][] = [ - ["chrome", "claude"], - ["notion", "raycast"], - ["drive", "claude-code"], - ["opencode", "codex"], - ["claude-code", "mcp"], - ["hermes", "openclaw"], - ["claude", "mcp"], - ["notion", "claude"], -] - -function nodeById(id: string) { - return TOOL_NODES.find((node) => node.id === id) -} - -function pickContextFlow(): ContextConnection | null { - const flow = CONTEXT_FLOWS[Math.floor(Math.random() * CONTEXT_FLOWS.length)] - if (!flow) return null - const [fromId, toId] = flow - const from = nodeById(fromId) - const to = nodeById(toId) - if (!from || !to) return null - return { from, to } -} - -function ToolNodeIcon({ - node, - role, -}: { - node: ToolNode - role?: "source" | "destination" -}) { - const Icon = node.icon - - return ( -
-
- {Icon ? ( - - ) : node.iconSrc ? ( - - ) : null} -
- - {node.name} - -
- ) -} - -function MemoryChip() { - return ( -
-
- - - -
-
- ) -} - -function AnimatedContextFlow({ - connection, - phase, -}: { - connection: ContextConnection - phase: ContextPhase -}) { - const { from, to } = connection - const dIn = `M ${from.x} ${from.y} L ${CENTER.x} ${CENTER.y}` - const dOut = `M ${CENTER.x} ${CENTER.y} L ${to.x} ${to.y}` - - const chipLeft = - phase === "recall" - ? [`${CENTER.x}%`, `${to.x}%`] - : phase === "hold" - ? `${CENTER.x}%` - : [`${from.x}%`, `${CENTER.x}%`] - - const chipTop = - phase === "recall" - ? [`${CENTER.y}%`, `${to.y}%`] - : phase === "hold" - ? `${CENTER.y}%` - : [`${from.y}%`, `${CENTER.y}%`] - - return ( -
- - - {phase !== "idle" && ( - - - - )} -
- ) -} - -function ToolsContextNetwork() { - const [connection, setConnection] = useState(null) - const [pulseId, setPulseId] = useState(0) - const [phase, setPhase] = useState("idle") - - useEffect(() => { - let cancelled = false - let pulseTimeout: ReturnType - - const runPulse = () => { - if (cancelled) return - const next = pickContextFlow() - if (!next) return - setConnection(next) - setPulseId((id) => id + 1) - pulseTimeout = setTimeout(runPulse, TOTAL_MS + 900 + Math.random() * 500) - } - - runPulse() - return () => { - cancelled = true - clearTimeout(pulseTimeout) - } - }, []) - - useEffect(() => { - if (!connection) return - - setPhase("capture") - const holdTimer = setTimeout(() => setPhase("hold"), IN_MS) - const recallTimer = setTimeout(() => setPhase("recall"), IN_MS + HOLD_MS) - const idleTimer = setTimeout(() => setPhase("idle"), TOTAL_MS) - - return () => { - clearTimeout(holdTimer) - clearTimeout(recallTimer) - clearTimeout(idleTimer) - } - }, [connection]) - - const sourceRole = (nodeId: string) => - connection && - connection.from.id === nodeId && - (phase === "capture" || phase === "hold") - ? ("source" as const) - : undefined - - const destRole = (nodeId: string) => - connection && connection.to.id === nodeId && phase === "recall" - ? ("destination" as const) - : undefined - - return ( -
- {connection && phase !== "idle" ? ( - - ) : null} - - {TOOL_NODES.map((node) => ( - - ))} - -
- - -
- -
-
-
-
- ) -} +import OrbitMemory from "@/components/orbit-memory" function LoginPanelBackground() { return ( @@ -373,6 +14,15 @@ function LoginPanelBackground() {
+
) } @@ -382,8 +32,16 @@ export function LoginToolsPanel() {