import React, { useMemo, useState } from "react"; import { json, type LoaderFunctionArgs, type ActionFunctionArgs, } from "@remix-run/node"; import { useLoaderData, useParams } from "@remix-run/react"; import { requireUserId, requireWorkpace } from "~/services/session.server"; import { getIntegrationDefinitions } from "~/services/integrationDefinition.server"; import { getIntegrationAccounts } from "~/services/integrationAccount.server"; import { getIcon, type IconType } from "~/components/icon-utils"; import { Checkbox } from "~/components/ui/checkbox"; import { MCPAuthSection } from "~/components/integrations/mcp-auth-section"; import { ConnectedAccountSection } from "~/components/integrations/connected-account-section"; import { IngestionRuleSection } from "~/components/integrations/ingestion-rule-section"; import { ApiKeyAuthSection } from "~/components/integrations/api-key-auth-section"; import { OAuthAuthSection } from "~/components/integrations/oauth-auth-section"; import { getIngestionRuleBySource, upsertIngestionRule, } from "~/services/ingestionRule.server"; import { Section } from "~/components/integrations/section"; import { PageHeader } from "~/components/common/page-header"; import { Check, Copy, Plus } from "lucide-react"; import { FIXED_INTEGRATIONS } from "~/components/integrations/utils"; import { IngestionRule, type IntegrationAccount, IntegrationDefinitionV2, } from "@prisma/client"; import { Input } from "~/components/ui/input"; import { Button } from "~/components/ui"; export async function loader({ request, params }: LoaderFunctionArgs) { const userId = await requireUserId(request); const workspace = await requireWorkpace(request); const { slug } = params; const [integrationDefinitions, integrationAccounts] = await Promise.all([ getIntegrationDefinitions(workspace.id), getIntegrationAccounts(userId), ]); // Combine fixed integrations with dynamic ones const allIntegrations = [...FIXED_INTEGRATIONS, ...integrationDefinitions]; const integration = allIntegrations.find( (def) => def.slug === slug || def.id === slug, ); if (!integration) { throw new Response("Integration not found", { status: 404 }); } const activeAccount = integrationAccounts.find( (acc) => acc.integrationDefinitionId === integration.id && acc.isActive, ); let ingestionRule = null; if (activeAccount) { ingestionRule = await getIngestionRuleBySource( activeAccount.id, workspace.id, ); } return json({ integration, integrationAccounts, userId, ingestionRule, }); } export async function action({ request, params }: ActionFunctionArgs) { const userId = await requireUserId(request); const workspace = await requireWorkpace(request); const { slug } = params; const formData = await request.formData(); const ingestionRuleText = formData.get("ingestionRule") as string; if (!ingestionRuleText) { return json({ error: "Ingestion rule is required" }, { status: 400 }); } const [integrationDefinitions, integrationAccounts] = await Promise.all([ getIntegrationDefinitions(workspace.id), getIntegrationAccounts(userId), ]); // Combine fixed integrations with dynamic ones const allIntegrations = [...FIXED_INTEGRATIONS, ...integrationDefinitions]; const integration = allIntegrations.find( (def) => def.slug === slug || def.id === slug, ); if (!integration) { throw new Response("Integration not found", { status: 404 }); } const activeAccount = integrationAccounts.find( (acc) => acc.integrationDefinitionId === integration.id && acc.isActive, ); if (!activeAccount) { return json( { error: "No active integration account found" }, { status: 400 }, ); } await upsertIngestionRule({ text: ingestionRuleText, source: activeAccount.id, workspaceId: workspace.id, userId, }); return json({ success: true }); } function parseSpec(spec: any) { if (!spec) return {}; if (typeof spec === "string") { try { return JSON.parse(spec); } catch { return {}; } } return spec; } function CustomIntegrationContent({ integration }: { integration: any }) { const memoryUrl = `https://core.heysol.ai/api/v1/mcp?source=${integration.slug}`; const [copied, setCopied] = useState(false); const copyToClipboard = async () => { try { await navigator.clipboard.writeText(memoryUrl); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch (err) { console.error("Failed to copy:", err); } }; const getCustomContent = () => { switch (integration.id) { case "claude": return { title: "About Claude", content: (

Claude is an AI assistant created by Anthropic. It can help with a wide variety of tasks including:

For Claude Web, Desktop, and Code - OAuth authentication handled automatically

), }; case "cursor": return { title: "About Cursor", content: (

Cursor is an AI-powered code editor that helps developers write code faster and more efficiently.

                  {JSON.stringify(
                    {
                      memory: {
                        url: memoryUrl,
                      },
                    },
                    null,
                    2,
                  )}
                
), }; case "cline": return { title: "About Cline", content: (

Cline is an AI coding assistant that works directly in your terminal and command line environment.

), }; case "vscode": return { title: "About Visual Studio Code", content: (

Visual Studio Code is a lightweight but powerful source code editor with extensive extension support.

You need to enable MCP in settings

                  {JSON.stringify(
                    {
                      "chat.mcp.enabled": true,
                      "chat.mcp.discovery.enabled": true,
                    },
                    null,
                    2,
                  )}
                
                  {JSON.stringify(
                    {
                      memory: {
                        type: "http",
                        url: memoryUrl,
                      },
                    },
                    null,
                    2,
                  )}
                
), }; default: return null; } }; const customContent = getCustomContent(); if (!customContent) return null; const Component = getIcon(integration.icon as IconType); return (
, onClick: () => window.open( "https://github.com/redplanethq/core/issues/new", "_blank", ), variant: "secondary", }, ]} />
} >
{customContent.content}
); } interface IntegrationDetailProps { integration: any; integrationAccounts: any; ingestionRule: any; } export function IntegrationDetail({ integration, integrationAccounts, ingestionRule, }: IntegrationDetailProps) { const activeAccount = useMemo( () => integrationAccounts.find( (acc: IntegrationAccount) => acc.integrationDefinitionId === integration.id && acc.isActive, ), [integrationAccounts, integration.id], ); const specData = useMemo( () => parseSpec(integration.spec), [integration.spec], ); const hasApiKey = !!specData?.auth?.api_key; const hasOAuth2 = !!specData?.auth?.OAuth2; const hasMCPAuth = !!( specData?.mcp.type === "http" && specData?.mcp.needsAuth ); const Component = getIcon(integration.icon as IconType); return (
, onClick: () => window.open( "https://github.com/redplanethq/core/issues/new", "_blank", ), variant: "secondary", }, ]} />
} >
{/* Authentication Methods */}

Authentication Methods

{hasApiKey && (
API Key authentication
)} {hasOAuth2 && (
OAuth 2.0 authentication
)} {!hasApiKey && !hasOAuth2 && !hasMCPAuth && (
No authentication method specified
)}
{/* Connect Section */} {!activeAccount && (hasApiKey || hasOAuth2) && (

Connect to {integration.name}

{/* API Key Authentication */} {/* OAuth Authentication */}
)} {/* Connected Account Info */} {/* MCP Authentication Section */} {/* Ingestion Rule Section */}
); } export default function IntegrationDetailWrapper() { const { integration, integrationAccounts, ingestionRule } = useLoaderData(); const { slug } = useParams(); // You can now use the `slug` param in your component const fixedIntegration = FIXED_INTEGRATIONS.some( (fixedInt) => fixedInt.slug === slug, ); return ( <> {fixedIntegration ? ( ) : ( )} ); }