mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-08 22:21:07 +00:00
Add Cursor & granola plugin card in Integrations page (#1070)
This commit is contained in:
parent
fec9334782
commit
464a2b16b1
2 changed files with 116 additions and 25 deletions
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue