Add Cursor & granola plugin card in Integrations page (#1070)

This commit is contained in:
Ishaan Gupta 2026-06-10 20:23:24 +05:30 committed by GitHub
parent fec9334782
commit 464a2b16b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 116 additions and 25 deletions

View file

@ -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<typeof ConnectionResponseSchema>
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<string, string> = {
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: <Granola className="size-6" />,
max: true,
},
],
},
{
@ -517,7 +536,7 @@ export function DetailWrapper({
)
}
function ProChip() {
function ProChip({ children = "Pro" }: { children?: ReactNode }) {
return (
<span
className={cn(
@ -525,7 +544,7 @@ function ProChip() {
"ml-auto shrink-0 pl-2 text-[10px] font-semibold uppercase tracking-wide text-[#4BA0FA]",
)}
>
Pro
{children}
</span>
)
}
@ -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}
</span>
{isNew && <NewChip />}
{pro && <ProChip />}
{max ? <ProChip>Max</ProChip> : pro && <ProChip />}
</div>
<p
className={cn(
@ -2193,11 +2214,14 @@ export function IntegrationsView({
const autumn = useCustomer({ queryOptions: { enabled: !publicMode } })
const hasProProduct =
!publicMode && hasActivePlan(autumn.data?.subscriptions, "api_pro")
const hasMaxProduct =
!publicMode && hasActivePlan(autumn.data?.subscriptions, "api_max")
const isAutumnLoading = !publicMode && autumn.isLoading
const [connectingPlugin, setConnectingPlugin] = useState<string | null>(null)
const [connectingProvider, setConnectingProvider] =
useState<ConnectorProvider | null>(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 (
<button
type="button"
aria-label="Add another knowledge source"
title="Add another knowledge source"
onClick={() => {
trackCard(item)
void setAddDoc("connect")
}}
className={cn(
"flex size-8 shrink-0 items-center justify-center rounded-full bg-[#0D121A] text-[#A1A1AA] transition-colors hover:text-[#FAFAFA] sm:size-9",
"shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.7)]",
)}
>
<Plus className="size-4" />
</button>
<div className="flex w-full items-center justify-between gap-2">
<ConnectionsCountPill count={count} />
<button
type="button"
aria-label="Add another knowledge source"
title="Add another knowledge source"
onClick={() => {
trackCard(item)
if (isGranola) {
if (!hasMaxProduct) {
handleUpgrade("api_max")
return
}
setGranolaModalOpen(true)
return
}
void setAddDoc("connect")
}}
className={cn(
"flex size-8 shrink-0 items-center justify-center rounded-full bg-[#0D121A] text-[#A1A1AA] transition-colors hover:text-[#FAFAFA] sm:size-9",
"shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.7)]",
)}
>
<Plus className="size-4" />
</button>
</div>
)
}
if (needsProUpgrade) {
if (needsPlanUpgrade) {
return (
<PillButton onClick={handleUpgrade}>
<PillButton
onClick={() => handleUpgrade(isGranola ? "api_max" : "api_pro")}
>
<Zap className="size-3.5 text-[#4BA0FA]" /> Upgrade
</PillButton>
)
@ -2883,6 +2923,14 @@ export function IntegrationsView({
<PillButton
onClick={() => {
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({
</div>
</DialogContent>
</Dialog>
<GranolaConnectModal
open={granolaModalOpen}
onOpenChange={setGranolaModalOpen}
/>
</div>
)
}

View file

@ -74,6 +74,42 @@ export const PLUGIN_CATALOG: Record<string, PluginInfo> = {
},
],
},
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<string, PluginInfo> = {
const SPACE_TO_CATALOG_ID: Record<string, string> = {
"claude-code": "claude_code",
codex: "codex",
cursor: "cursor",
opencode: "opencode",
openclaw: "openclaw",
hermes: "hermes",