diff --git a/apps/web/components/integrations-view.tsx b/apps/web/components/integrations-view.tsx index a7e5c3c7..f83bca64 100644 --- a/apps/web/components/integrations-view.tsx +++ b/apps/web/components/integrations-view.tsx @@ -19,7 +19,13 @@ import { AppleShortcutsIcon, RaycastIcon, } from "@/components/integration-icons" -import { GoogleDrive, Notion, OneDrive, MCPIcon } from "@ui/assets/icons" +import { + GoogleDrive, + Notion, + OneDrive, + MCPIcon, + Granola, +} from "@ui/assets/icons" import * as DialogPrimitive from "@radix-ui/react-dialog" import { ArrowLeft, @@ -68,11 +74,12 @@ import { } from "@/lib/plugin-catalog" import { INSET, InstallSteps, PillButton } from "./integrations/install-steps" import { MCPSteps } from "./mcp-modal/mcp-detail-view" +import { GranolaConnectModal } from "./granola-connect-modal" import { detectPluginSpace, detectPluginSource } from "@/lib/plugin-space" type Connection = z.infer -type ConnectorProvider = "google-drive" | "notion" | "onedrive" +type ConnectorProvider = "google-drive" | "notion" | "onedrive" | "granola" interface ConnectedKey { keyId: string @@ -232,6 +239,7 @@ function mcpClientIconSrc(key: MCPClientKey): string { const PLUGIN_SIMPLE_TITLES: Record = { claude_code: "Remembers your code conventions and decisions", codex: "Codex sessions that remember your project", + cursor: "Cursor sessions with persistent project memory", opencode: "OpenCode with persistent project memory", openclaw: "Save chats from Telegram, Discord and Slack", hermes: "Persistent memory for the Hermes agent", @@ -297,6 +305,7 @@ interface BaseItem { icon: ReactNode docsUrl?: string pro?: boolean + max?: boolean kind: ItemKind simpleTitle?: string dev?: boolean @@ -427,6 +436,16 @@ const SECTIONS: Array<{ pro: true, docsUrl: "https://supermemory.ai/docs/connectors/onedrive", }, + { + kind: "connector", + id: "granola", + provider: "granola", + name: "Granola", + tagline: "Sync AI meeting notes into your memory", + simpleTitle: "Your meeting notes, ready to recall", + icon: , + max: true, + }, ], }, { @@ -517,7 +536,7 @@ export function DetailWrapper({ ) } -function ProChip() { +function ProChip({ children = "Pro" }: { children?: ReactNode }) { return ( - Pro + {children} ) } @@ -1705,6 +1724,7 @@ function ItemCard({ name, tagline, pro, + max, isNew, docsUrl, leftIndicator, @@ -1718,6 +1738,7 @@ function ItemCard({ name: string tagline: string pro?: boolean + max?: boolean isNew?: boolean docsUrl?: string leftIndicator?: ReactNode @@ -1768,7 +1789,7 @@ function ItemCard({ {name} {isNew && } - {pro && } + {max ? Max : pro && }

(null) const [connectingProvider, setConnectingProvider] = useState(null) + const [granolaModalOpen, setGranolaModalOpen] = useState(false) const [newKey, setNewKey] = useState<{ open: boolean key: string @@ -2303,6 +2327,7 @@ export function IntegrationsView({ "google-drive": [], notion: [], onedrive: [], + granola: [], } for (const c of connections) { const p = c.provider as ConnectorProvider @@ -2400,10 +2425,10 @@ export function IntegrationsView({ } } - const handleUpgrade = async () => { + const handleUpgrade = async (planId: "api_pro" | "api_max" = "api_pro") => { try { const result = await autumn.attach({ - planId: "api_pro", + planId, successUrl: `${window.location.origin}/?view=integrations`, }) if (result?.paymentUrl) { @@ -2851,29 +2876,44 @@ export function IntegrationsView({ } case "connector": { const count = connectionsByProvider[item.provider].length - const needsProUpgrade = !isAutumnLoading && !hasProProduct + const isGranola = item.provider === "granola" + const needsPlanUpgrade = + !isAutumnLoading && (isGranola ? !hasMaxProduct : !hasProProduct) if (count > 0) { return ( - +

+ + +
) } - if (needsProUpgrade) { + if (needsPlanUpgrade) { return ( - + handleUpgrade(isGranola ? "api_max" : "api_pro")} + > Upgrade ) @@ -2883,6 +2923,14 @@ export function IntegrationsView({ { trackCard(item) + if (isGranola) { + if (!hasMaxProduct) { + handleUpgrade("api_max") + return + } + setGranolaModalOpen(true) + return + } addConnectionMutation.mutate(item.provider) }} disabled={!!connectingProvider} @@ -3052,6 +3100,7 @@ export function IntegrationsView({ name={item.name} tagline={item.tagline} pro={item.pro} + max={item.max} isNew={item.isNew} docsUrl={item.docsUrl} leftIndicator={renderLeftIndicator(item)} @@ -3637,6 +3686,11 @@ export function IntegrationsView({ + + ) } diff --git a/apps/web/lib/plugin-catalog.ts b/apps/web/lib/plugin-catalog.ts index e1a4ffde..4653f7f9 100644 --- a/apps/web/lib/plugin-catalog.ts +++ b/apps/web/lib/plugin-catalog.ts @@ -74,6 +74,42 @@ export const PLUGIN_CATALOG: Record = { }, ], }, + cursor: { + id: "cursor", + name: "Cursor", + tagline: "Persistent memory, session hooks, and MCP tools inside Cursor", + icon: "/images/plugins/cursor.png", + docsUrl: "https://github.com/supermemoryai/cursor-supermemory#readme", + githubUrl: "https://github.com/supermemoryai/cursor-supermemory", + installSteps: [ + { + title: "Install the Cursor plugin", + description: + "Install cursor-supermemory from the Cursor Marketplace, then run the auth command on the machine where Cursor runs.", + code: "bunx cursor-supermemory@latest login", + copyLabel: "Login command", + }, + { + title: "Finish browser authentication", + description: + "The login command opens Supermemory in your browser and stores Cursor credentials in ~/.supermemory-cursor/credentials.json.", + }, + { + title: "Manual API-key fallback", + description: + "If browser login is not available, set this environment variable before starting Cursor. This key is shown only once - save it now.", + code: 'export SUPERMEMORY_API_KEY="sm_..."', + copyLabel: "API key command", + secret: true, + optional: true, + }, + { + title: "Restart Cursor", + description: + "Restart Cursor after installing or changing credentials so the plugin hooks and MCP tools are loaded.", + }, + ], + }, opencode: { id: "opencode", name: "OpenCode", @@ -143,6 +179,7 @@ export const PLUGIN_CATALOG: Record = { const SPACE_TO_CATALOG_ID: Record = { "claude-code": "claude_code", codex: "codex", + cursor: "cursor", opencode: "opencode", openclaw: "openclaw", hermes: "hermes",