mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-28 13:32:23 +00:00
Add full Composio managed-auth toolkit catalog (#1093)
This commit is contained in:
@@ -318,6 +318,9 @@ export default function ComposioConnectModal({
|
||||
<span className="px-1.5 py-0.5 text-[10px] font-medium rounded-md bg-primary-500/15 text-primary-600">
|
||||
composio
|
||||
</span>
|
||||
<span className="px-1.5 py-0.5 text-[10px] font-medium rounded-md bg-sky-500/10 text-sky-700">
|
||||
managed auth
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-stone-400 mt-1.5 line-clamp-2">{toolkit.description}</p>
|
||||
</div>
|
||||
@@ -343,9 +346,21 @@ export default function ComposioConnectModal({
|
||||
{phase === 'idle' && (
|
||||
<>
|
||||
<p className="text-sm text-stone-600">
|
||||
Connect your {toolkit.name} account. We will open a browser window where you can
|
||||
grant access, and then this app will detect the connection automatically.
|
||||
Connect your {toolkit.name} account through Composio's hosted authorization
|
||||
flow. We'll open a browser window, you approve access there, and this app will
|
||||
detect the connection automatically.
|
||||
</p>
|
||||
<div className="rounded-xl border border-stone-200 bg-stone-50 p-3">
|
||||
<p className="text-[11px] font-medium uppercase tracking-wide text-stone-500">
|
||||
Provider access
|
||||
</p>
|
||||
<p className="mt-1 text-xs leading-relaxed text-stone-600">
|
||||
{toolkit.name} can expose{' '}
|
||||
<span className="font-medium">{toolkit.permissionLabel}</span>. After you connect,
|
||||
OpenHuman's own agent permissions are controlled below as read, write, and
|
||||
admin toggles.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleConnect()}
|
||||
@@ -478,9 +493,9 @@ function ScopeToggles({ scopes, savingScope, onToggle, error }: ScopeTogglesProp
|
||||
<div className="border-t border-stone-100 pt-3 mt-1 space-y-2">
|
||||
<div className="flex items-baseline justify-between">
|
||||
<h3 className="text-xs font-semibold text-stone-700 uppercase tracking-wide">
|
||||
Agent permissions
|
||||
OpenHuman agent permissions
|
||||
</h3>
|
||||
<p className="text-[10px] text-stone-400">Read+Write enabled by default</p>
|
||||
<p className="text-[10px] text-stone-400">Read + Write enabled by default</p>
|
||||
</div>
|
||||
<ul className="space-y-1.5">
|
||||
{SCOPE_ROWS.map(row => {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { composioToolkitMeta, KNOWN_COMPOSIO_TOOLKITS } from './toolkitMeta';
|
||||
|
||||
describe('composioToolkitMeta', () => {
|
||||
it('ships the full Composio managed-auth catalog fallback', () => {
|
||||
expect(KNOWN_COMPOSIO_TOOLKITS).toHaveLength(118);
|
||||
expect(KNOWN_COMPOSIO_TOOLKITS).toContain('gmail');
|
||||
expect(KNOWN_COMPOSIO_TOOLKITS).toContain('discord');
|
||||
expect(KNOWN_COMPOSIO_TOOLKITS).toContain('supabase');
|
||||
expect(KNOWN_COMPOSIO_TOOLKITS).toContain('zoom');
|
||||
});
|
||||
|
||||
it('preserves canonical names for managed-auth toolkits and renders logo URLs', () => {
|
||||
const gmail = composioToolkitMeta('gmail');
|
||||
const calendar = composioToolkitMeta('google_calendar');
|
||||
|
||||
expect(gmail.name).toBe('Gmail');
|
||||
expect(gmail.logoUrl).toContain('/gmail');
|
||||
expect(gmail.permissionLabel).toBe('Docs, files, tasks, and workspace data');
|
||||
|
||||
expect(calendar.slug).toBe('googlecalendar');
|
||||
expect(calendar.name).toBe('Google Calendar');
|
||||
expect(calendar.logoUrl).toContain('/googlecalendar');
|
||||
});
|
||||
|
||||
it('falls back cleanly for unknown slugs', () => {
|
||||
const meta = composioToolkitMeta('my_custom_toolkit');
|
||||
|
||||
expect(meta.slug).toBe('my_custom_toolkit');
|
||||
expect(meta.name).toBe('My Custom Toolkit');
|
||||
expect(meta.logoUrl).toContain('/my_custom_toolkit');
|
||||
});
|
||||
});
|
||||
@@ -1,33 +1,20 @@
|
||||
/**
|
||||
* Display metadata for Composio toolkits shown in the Skills grid.
|
||||
*
|
||||
* The backend allowlist (`GET /agent-integrations/composio/toolkits`)
|
||||
* returns plain toolkit slugs. This table turns each slug into a
|
||||
* humanised name, description, category, and icon so the
|
||||
* `UnifiedSkillCard` can render them next to regular skills without
|
||||
* special-casing.
|
||||
* We intentionally keep a local catalog of every Composio managed-auth
|
||||
* toolkit so the desktop UI can render a broad connection surface even
|
||||
* before the live backend allowlist expands further. The live toolkit
|
||||
* list still wins for runtime availability; this file provides stable
|
||||
* names, categories, descriptions, and logos for rendering.
|
||||
*
|
||||
* Unknown slugs fall back to a generic entry with a title-cased name —
|
||||
* new backend toolkits will still render, just without custom copy.
|
||||
* Source of truth for the managed-auth list:
|
||||
* https://docs.composio.dev/toolkits/managed-auth (118 toolkits as of
|
||||
* May 1, 2026).
|
||||
*/
|
||||
import type { ReactNode } from 'react';
|
||||
import {
|
||||
SiFacebook,
|
||||
SiGithub,
|
||||
SiGmail,
|
||||
SiGooglecalendar,
|
||||
SiGoogledrive,
|
||||
SiGooglesheets,
|
||||
SiInstagram,
|
||||
SiLinear,
|
||||
SiNotion,
|
||||
SiReddit,
|
||||
SiSlack,
|
||||
} from 'react-icons/si';
|
||||
import { type ReactNode, useState } from 'react';
|
||||
|
||||
import { canonicalizeComposioToolkitSlug } from '../../lib/composio/toolkitSlug';
|
||||
import type { SkillCategory } from '../skills/skillCategories';
|
||||
import { SkillIconBadge } from '../skills/skillIcons';
|
||||
|
||||
export interface ComposioToolkitMeta {
|
||||
/** Toolkit slug as returned by the backend, e.g. `"gmail"`. */
|
||||
@@ -40,128 +27,203 @@ export interface ComposioToolkitMeta {
|
||||
category: SkillCategory;
|
||||
/** Small branded icon rendered on the card and connect modal. */
|
||||
icon: ReactNode;
|
||||
/** Composio-hosted logo URL for richer provider branding. */
|
||||
logoUrl: string;
|
||||
/** Short UX hint for what the user is authorizing. */
|
||||
permissionLabel: string;
|
||||
}
|
||||
|
||||
function GmailIcon() {
|
||||
return (
|
||||
<SkillIconBadge
|
||||
icon={SiGmail}
|
||||
label="Gmail"
|
||||
bgClassName="bg-white"
|
||||
iconClassName="text-[#EA4335]"
|
||||
/>
|
||||
);
|
||||
interface ManagedToolkitEntry {
|
||||
slug: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
function GoogleCalendarIcon() {
|
||||
return (
|
||||
<SkillIconBadge
|
||||
icon={SiGooglecalendar}
|
||||
label="Google Calendar"
|
||||
bgClassName="bg-[#E8F0FE]"
|
||||
iconClassName="text-[#4285F4]"
|
||||
/>
|
||||
);
|
||||
}
|
||||
const MANAGED_COMPOSIO_TOOLKITS: readonly ManagedToolkitEntry[] = Object.freeze([
|
||||
{ slug: 'airtable', name: 'Airtable' },
|
||||
{ slug: 'apaleo', name: 'Apaleo' },
|
||||
{ slug: 'asana', name: 'Asana' },
|
||||
{ slug: 'attio', name: 'Attio' },
|
||||
{ slug: 'basecamp', name: 'Basecamp' },
|
||||
{ slug: 'bitbucket', name: 'Bitbucket' },
|
||||
{ slug: 'blackbaud', name: 'Blackbaud' },
|
||||
{ slug: 'boldsign', name: 'Boldsign' },
|
||||
{ slug: 'box', name: 'Box' },
|
||||
{ slug: 'cal', name: 'Cal' },
|
||||
{ slug: 'calendly', name: 'Calendly' },
|
||||
{ slug: 'canva', name: 'Canva' },
|
||||
{ slug: 'capsule_crm', name: 'Capsule CRM' },
|
||||
{ slug: 'clickup', name: 'ClickUp' },
|
||||
{ slug: 'confluence', name: 'Confluence' },
|
||||
{ slug: 'contentful', name: 'Contentful' },
|
||||
{ slug: 'convex', name: 'Convex' },
|
||||
{ slug: 'crowdin', name: 'Crowdin' },
|
||||
{ slug: 'dart', name: 'Dart' },
|
||||
{ slug: 'dialpad', name: 'Dialpad' },
|
||||
{ slug: 'digital_ocean', name: 'DigitalOcean' },
|
||||
{ slug: 'discord', name: 'Discord' },
|
||||
{ slug: 'discordbot', name: 'Discord Bot' },
|
||||
{ slug: 'dropbox', name: 'Dropbox' },
|
||||
{ slug: 'dub', name: 'Dub' },
|
||||
{ slug: 'dynamics365', name: 'Dynamics 365' },
|
||||
{ slug: 'eventbrite', name: 'Eventbrite' },
|
||||
{ slug: 'excel', name: 'Excel' },
|
||||
{ slug: 'exist', name: 'Exist' },
|
||||
{ slug: 'facebook', name: 'Facebook' },
|
||||
{ slug: 'fathom', name: 'Fathom' },
|
||||
{ slug: 'figma', name: 'Figma' },
|
||||
{ slug: 'freeagent', name: 'Freeagent' },
|
||||
{ slug: 'freshbooks', name: 'FreshBooks' },
|
||||
{ slug: 'github', name: 'GitHub' },
|
||||
{ slug: 'gitlab', name: 'GitLab' },
|
||||
{ slug: 'gmail', name: 'Gmail' },
|
||||
{ slug: 'googleads', name: 'Google Ads' },
|
||||
{ slug: 'google_analytics', name: 'Google Analytics' },
|
||||
{ slug: 'googlebigquery', name: 'Google BigQuery' },
|
||||
{ slug: 'googlecalendar', name: 'Google Calendar' },
|
||||
{ slug: 'google_classroom', name: 'Google Classroom' },
|
||||
{ slug: 'googledocs', name: 'Google Docs' },
|
||||
{ slug: 'googledrive', name: 'Google Drive' },
|
||||
{ slug: 'google_maps', name: 'Google Maps' },
|
||||
{ slug: 'googlemeet', name: 'Google Meet' },
|
||||
{ slug: 'googlephotos', name: 'Google Photos' },
|
||||
{ slug: 'google_search_console', name: 'Google Search Console' },
|
||||
{ slug: 'googlesheets', name: 'Google Sheets' },
|
||||
{ slug: 'googleslides', name: 'Google Slides' },
|
||||
{ slug: 'googlesuper', name: 'Google Super' },
|
||||
{ slug: 'googletasks', name: 'Google Tasks' },
|
||||
{ slug: 'gorgias', name: 'Gorgias' },
|
||||
{ slug: 'gumroad', name: 'Gumroad' },
|
||||
{ slug: 'harvest', name: 'Harvest' },
|
||||
{ slug: 'hubspot', name: 'HubSpot' },
|
||||
{ slug: 'hugging_face', name: 'Hugging Face' },
|
||||
{ slug: 'instagram', name: 'Instagram' },
|
||||
{ slug: 'intercom', name: 'Intercom' },
|
||||
{ slug: 'jira', name: 'Jira' },
|
||||
{ slug: 'kit', name: 'Kit' },
|
||||
{ slug: 'linear', name: 'Linear' },
|
||||
{ slug: 'linkedin', name: 'LinkedIn' },
|
||||
{ slug: 'linkhut', name: 'Linkhut' },
|
||||
{ slug: 'mailchimp', name: 'Mailchimp' },
|
||||
{ slug: 'microsoft_teams', name: 'Microsoft Teams' },
|
||||
{ slug: 'miro', name: 'Miro' },
|
||||
{ slug: 'monday', name: 'Monday' },
|
||||
{ slug: 'moneybird', name: 'Moneybird' },
|
||||
{ slug: 'mural', name: 'Mural' },
|
||||
{ slug: 'notion', name: 'Notion' },
|
||||
{ slug: 'omnisend', name: 'Omnisend' },
|
||||
{ slug: 'one_drive', name: 'OneDrive' },
|
||||
{ slug: 'outlook', name: 'Outlook' },
|
||||
{ slug: 'pagerduty', name: 'PagerDuty' },
|
||||
{ slug: 'prisma', name: 'Prisma' },
|
||||
{ slug: 'productboard', name: 'Productboard' },
|
||||
{ slug: 'pushbullet', name: 'Pushbullet' },
|
||||
{ slug: 'quickbooks', name: 'QuickBooks' },
|
||||
{ slug: 'reddit', name: 'Reddit' },
|
||||
{ slug: 'reddit_ads', name: 'Reddit Ads' },
|
||||
{ slug: 'roam', name: 'Roam' },
|
||||
{ slug: 'salesforce', name: 'Salesforce' },
|
||||
{ slug: 'sentry', name: 'Sentry' },
|
||||
{ slug: 'servicem8', name: 'Servicem8' },
|
||||
{ slug: 'share_point', name: 'SharePoint' },
|
||||
{ slug: 'shippo', name: 'Shippo' },
|
||||
{ slug: 'slack', name: 'Slack' },
|
||||
{ slug: 'slackbot', name: 'Slackbot' },
|
||||
{ slug: 'splitwise', name: 'Splitwise' },
|
||||
{ slug: 'square', name: 'Square' },
|
||||
{ slug: 'stack_exchange', name: 'Stack Exchange' },
|
||||
{ slug: 'strava', name: 'Strava' },
|
||||
{ slug: 'stripe', name: 'Stripe' },
|
||||
{ slug: 'supabase', name: 'Supabase' },
|
||||
{ slug: 'ticketmaster', name: 'Ticketmaster' },
|
||||
{ slug: 'ticktick', name: 'Ticktick' },
|
||||
{ slug: 'timely', name: 'Timely' },
|
||||
{ slug: 'todoist', name: 'Todoist' },
|
||||
{ slug: 'toneden', name: 'Toneden' },
|
||||
{ slug: 'trello', name: 'Trello' },
|
||||
{ slug: 'typeform', name: 'Typeform' },
|
||||
{ slug: 'wakatime', name: 'WakaTime' },
|
||||
{ slug: 'webex', name: 'Webex' },
|
||||
{ slug: 'whatsapp', name: 'WhatsApp' },
|
||||
{ slug: 'wrike', name: 'Wrike' },
|
||||
{ slug: 'yandex', name: 'Yandex' },
|
||||
{ slug: 'ynab', name: 'YNAB' },
|
||||
{ slug: 'youtube', name: 'YouTube' },
|
||||
{ slug: 'zendesk', name: 'Zendesk' },
|
||||
{ slug: 'zoho', name: 'Zoho' },
|
||||
{ slug: 'zoho_bigin', name: 'Zoho Bigin' },
|
||||
{ slug: 'zoho_books', name: 'Zoho Books' },
|
||||
{ slug: 'zoho_desk', name: 'Zoho Desk' },
|
||||
{ slug: 'zoho_inventory', name: 'Zoho Inventory' },
|
||||
{ slug: 'zoho_invoice', name: 'Zoho Invoice' },
|
||||
{ slug: 'zoho_mail', name: 'Zoho Mail' },
|
||||
{ slug: 'zoom', name: 'Zoom' },
|
||||
]);
|
||||
|
||||
function GoogleDriveIcon() {
|
||||
return (
|
||||
<SkillIconBadge
|
||||
icon={SiGoogledrive}
|
||||
label="Google Drive"
|
||||
bgClassName="bg-white"
|
||||
iconClassName="text-[#0F9D58]"
|
||||
/>
|
||||
);
|
||||
}
|
||||
const MANAGED_TOOLKIT_NAME_BY_SLUG = new Map(
|
||||
MANAGED_COMPOSIO_TOOLKITS.map(entry => [entry.slug, entry.name])
|
||||
);
|
||||
|
||||
function NotionIcon() {
|
||||
return (
|
||||
<SkillIconBadge
|
||||
icon={SiNotion}
|
||||
label="Notion"
|
||||
bgClassName="bg-white"
|
||||
iconClassName="text-[#111111]"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function GitHubIcon() {
|
||||
return (
|
||||
<SkillIconBadge
|
||||
icon={SiGithub}
|
||||
label="GitHub"
|
||||
bgClassName="bg-[#111827]"
|
||||
iconClassName="text-white"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SlackIcon() {
|
||||
return (
|
||||
<SkillIconBadge
|
||||
icon={SiSlack}
|
||||
label="Slack"
|
||||
bgClassName="bg-white"
|
||||
iconClassName="text-[#4A154B]"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function LinearIcon() {
|
||||
return (
|
||||
<SkillIconBadge
|
||||
icon={SiLinear}
|
||||
label="Linear"
|
||||
bgClassName="bg-[#0F172A]"
|
||||
iconClassName="text-white"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function FacebookIcon() {
|
||||
return (
|
||||
<SkillIconBadge
|
||||
icon={SiFacebook}
|
||||
label="Facebook"
|
||||
bgClassName="bg-[#1877F2]"
|
||||
iconClassName="text-white"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function GoogleSheetsIcon() {
|
||||
return (
|
||||
<SkillIconBadge
|
||||
icon={SiGooglesheets}
|
||||
label="Google Sheets"
|
||||
bgClassName="bg-[#E6F4EA]"
|
||||
iconClassName="text-[#0F9D58]"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function InstagramIcon() {
|
||||
return (
|
||||
<SkillIconBadge
|
||||
icon={SiInstagram}
|
||||
label="Instagram"
|
||||
bgClassName="bg-[radial-gradient(circle_at_30%_107%,_#fdf497_0%,_#fdf497_5%,_#fd5949_45%,_#d6249f_60%,_#285AEB_90%)]"
|
||||
iconClassName="text-white"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function RedditIcon() {
|
||||
return (
|
||||
<SkillIconBadge
|
||||
icon={SiReddit}
|
||||
label="Reddit"
|
||||
bgClassName="bg-[#FF4500]"
|
||||
iconClassName="text-white"
|
||||
/>
|
||||
);
|
||||
}
|
||||
const CHAT_KEYWORDS = ['discord', 'slack', 'teams', 'webex', 'whatsapp', 'dialpad'];
|
||||
const SOCIAL_KEYWORDS = [
|
||||
'facebook',
|
||||
'instagram',
|
||||
'linkedin',
|
||||
'reddit',
|
||||
'youtube',
|
||||
'stack_exchange',
|
||||
];
|
||||
const PRODUCTIVITY_KEYWORDS = [
|
||||
'gmail',
|
||||
'calendar',
|
||||
'drive',
|
||||
'docs',
|
||||
'doc',
|
||||
'sheets',
|
||||
'slides',
|
||||
'tasks',
|
||||
'todoist',
|
||||
'trello',
|
||||
'notion',
|
||||
'box',
|
||||
'dropbox',
|
||||
'sharepoint',
|
||||
'one_drive',
|
||||
'onedrive',
|
||||
'outlook',
|
||||
'miro',
|
||||
'mural',
|
||||
'monday',
|
||||
'clickup',
|
||||
'linear',
|
||||
'jira',
|
||||
'confluence',
|
||||
'asana',
|
||||
'basecamp',
|
||||
'wrike',
|
||||
'cal',
|
||||
'calendly',
|
||||
'typeform',
|
||||
'excel',
|
||||
'figma',
|
||||
'google',
|
||||
];
|
||||
const PLATFORM_KEYWORDS = [
|
||||
'github',
|
||||
'gitlab',
|
||||
'bitbucket',
|
||||
'digital_ocean',
|
||||
'contentful',
|
||||
'supabase',
|
||||
'convex',
|
||||
'prisma',
|
||||
'sentry',
|
||||
'stripe',
|
||||
'salesforce',
|
||||
'hubspot',
|
||||
'quickbooks',
|
||||
'zendesk',
|
||||
'zoho',
|
||||
];
|
||||
|
||||
function GenericIntegrationIcon() {
|
||||
return (
|
||||
@@ -179,124 +241,98 @@ function GenericIntegrationIcon() {
|
||||
);
|
||||
}
|
||||
|
||||
const CATALOG: Record<string, Omit<ComposioToolkitMeta, 'slug'>> = {
|
||||
gmail: {
|
||||
name: 'Gmail',
|
||||
description: 'Read, search, and send email through your Google account.',
|
||||
category: 'Productivity',
|
||||
icon: <GmailIcon />,
|
||||
},
|
||||
googlecalendar: {
|
||||
name: 'Google Calendar',
|
||||
description: 'List and manage events across your Google calendars.',
|
||||
category: 'Productivity',
|
||||
icon: <GoogleCalendarIcon />,
|
||||
},
|
||||
google_calendar: {
|
||||
name: 'Google Calendar',
|
||||
description: 'List and manage events across your Google calendars.',
|
||||
category: 'Productivity',
|
||||
icon: <GoogleCalendarIcon />,
|
||||
},
|
||||
googledrive: {
|
||||
name: 'Google Drive',
|
||||
description: 'Browse and fetch files from your Google Drive.',
|
||||
category: 'Productivity',
|
||||
icon: <GoogleDriveIcon />,
|
||||
},
|
||||
google_drive: {
|
||||
name: 'Google Drive',
|
||||
description: 'Browse and fetch files from your Google Drive.',
|
||||
category: 'Productivity',
|
||||
icon: <GoogleDriveIcon />,
|
||||
},
|
||||
notion: {
|
||||
name: 'Notion',
|
||||
description: 'Read and edit pages, databases, and comments in Notion.',
|
||||
category: 'Productivity',
|
||||
icon: <NotionIcon />,
|
||||
},
|
||||
github: {
|
||||
name: 'GitHub',
|
||||
description: 'Inspect repos, issues, and pull requests on GitHub.',
|
||||
category: 'Tools & Automation',
|
||||
icon: <GitHubIcon />,
|
||||
},
|
||||
slack: {
|
||||
name: 'Slack',
|
||||
description: 'Send messages and read channel history in Slack.',
|
||||
category: 'Social',
|
||||
icon: <SlackIcon />,
|
||||
},
|
||||
linear: {
|
||||
name: 'Linear',
|
||||
description: 'Triage and update issues in your Linear workspace.',
|
||||
category: 'Tools & Automation',
|
||||
icon: <LinearIcon />,
|
||||
},
|
||||
facebook: {
|
||||
name: 'Facebook',
|
||||
description: 'Create posts, manage pages, and work with Facebook social data.',
|
||||
category: 'Social',
|
||||
icon: <FacebookIcon />,
|
||||
},
|
||||
google_sheets: {
|
||||
name: 'Google Sheets',
|
||||
description: 'Read, update, and organize spreadsheets in Google Sheets.',
|
||||
category: 'Productivity',
|
||||
icon: <GoogleSheetsIcon />,
|
||||
},
|
||||
googlesheets: {
|
||||
name: 'Google Sheets',
|
||||
description: 'Read, update, and organize spreadsheets in Google Sheets.',
|
||||
category: 'Productivity',
|
||||
icon: <GoogleSheetsIcon />,
|
||||
},
|
||||
instagram: {
|
||||
name: 'Instagram',
|
||||
description: 'Manage Instagram publishing, messaging, and social content workflows.',
|
||||
category: 'Social',
|
||||
icon: <InstagramIcon />,
|
||||
},
|
||||
reddit: {
|
||||
name: 'Reddit',
|
||||
description: 'Read posts, monitor communities, and participate in Reddit discussions.',
|
||||
category: 'Social',
|
||||
icon: <RedditIcon />,
|
||||
},
|
||||
};
|
||||
function ComposioLogoBadge({ slug, name }: { slug: string; name: string }) {
|
||||
const [failed, setFailed] = useState(false);
|
||||
const logoUrl = composioLogoUrl(slug);
|
||||
|
||||
if (failed) {
|
||||
return <GenericIntegrationIcon />;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="flex h-8 w-8 items-center justify-center overflow-hidden rounded-xl bg-white shadow-sm ring-1 ring-black/5">
|
||||
<img
|
||||
src={logoUrl}
|
||||
alt={`${name} logo`}
|
||||
className="h-full w-full object-contain p-1"
|
||||
loading="lazy"
|
||||
onError={() => setFailed(true)}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function composioLogoUrl(slug: string): string {
|
||||
return `https://logos.composio.dev/api/${slug}`;
|
||||
}
|
||||
|
||||
function guessCategory(slug: string, name: string): SkillCategory {
|
||||
const key = `${slug} ${name}`.toLowerCase();
|
||||
if (CHAT_KEYWORDS.some(keyword => key.includes(keyword))) return 'Chat';
|
||||
if (SOCIAL_KEYWORDS.some(keyword => key.includes(keyword))) return 'Social';
|
||||
if (PRODUCTIVITY_KEYWORDS.some(keyword => key.includes(keyword))) return 'Productivity';
|
||||
if (PLATFORM_KEYWORDS.some(keyword => key.includes(keyword))) return 'Platform';
|
||||
return 'Tools & Automation';
|
||||
}
|
||||
|
||||
function defaultDescription(name: string, category: SkillCategory): string {
|
||||
switch (category) {
|
||||
case 'Chat':
|
||||
return `Connect ${name} for messaging, inbox, and team communication workflows.`;
|
||||
case 'Social':
|
||||
return `Connect ${name} for social publishing, community, and audience workflows.`;
|
||||
case 'Productivity':
|
||||
return `Connect ${name} for documents, planning, file, and day-to-day productivity workflows.`;
|
||||
case 'Platform':
|
||||
return `Connect ${name} for developer, platform, CRM, and business system workflows.`;
|
||||
default:
|
||||
return `Connect ${name} through Composio managed auth.`;
|
||||
}
|
||||
}
|
||||
|
||||
function permissionLabelFor(category: SkillCategory): string {
|
||||
switch (category) {
|
||||
case 'Chat':
|
||||
return 'Messages, channels, and communication data';
|
||||
case 'Social':
|
||||
return 'Posts, profiles, and social content';
|
||||
case 'Productivity':
|
||||
return 'Docs, files, tasks, and workspace data';
|
||||
case 'Platform':
|
||||
return 'Repos, records, tickets, and system data';
|
||||
default:
|
||||
return 'Connected account data';
|
||||
}
|
||||
}
|
||||
|
||||
function prettifyUnknownSlug(slug: string): string {
|
||||
return slug
|
||||
.split(/[_-]+/)
|
||||
.filter(Boolean)
|
||||
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Canonical toolkit slugs used as the default catalog when the backend
|
||||
* allowlist hasn't loaded yet. One entry per integration — CATALOG
|
||||
* handles alternate slug variants (e.g. `google_calendar` →
|
||||
* `googlecalendar`) so they don't need to appear here.
|
||||
* allowlist hasn't loaded yet. One entry per Composio managed-auth
|
||||
* integration.
|
||||
*/
|
||||
export const KNOWN_COMPOSIO_TOOLKITS = Object.freeze([
|
||||
'gmail',
|
||||
'googlecalendar',
|
||||
'googledrive',
|
||||
'googlesheets',
|
||||
'notion',
|
||||
'github',
|
||||
'slack',
|
||||
'linear',
|
||||
'facebook',
|
||||
'instagram',
|
||||
'reddit',
|
||||
]);
|
||||
export const KNOWN_COMPOSIO_TOOLKITS = Object.freeze(
|
||||
MANAGED_COMPOSIO_TOOLKITS.map(entry => entry.slug)
|
||||
);
|
||||
|
||||
export function composioToolkitMeta(slug: string): ComposioToolkitMeta {
|
||||
const key = canonicalizeComposioToolkitSlug(slug);
|
||||
const hit = CATALOG[key];
|
||||
if (hit) return { slug: key, ...hit };
|
||||
// Fallback: title-case the slug and bucket it under "Other".
|
||||
const name = key.charAt(0).toUpperCase() + key.slice(1);
|
||||
const name = MANAGED_TOOLKIT_NAME_BY_SLUG.get(key) ?? prettifyUnknownSlug(key);
|
||||
const category = guessCategory(key, name);
|
||||
return {
|
||||
slug: key,
|
||||
name,
|
||||
description: `Integration for ${name}.`,
|
||||
category: 'Other',
|
||||
icon: <GenericIntegrationIcon />,
|
||||
description: defaultDescription(name, category),
|
||||
category,
|
||||
icon: <ComposioLogoBadge slug={key} name={name} />,
|
||||
logoUrl: composioLogoUrl(key),
|
||||
permissionLabel: permissionLabelFor(category),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -43,20 +43,24 @@ describe('Skills page — Composio catalog fallback', () => {
|
||||
it('shows known composio integrations in their configured category groups when the live toolkit list is empty', () => {
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/skills'] });
|
||||
|
||||
expect(screen.getByRole('heading', { name: 'Chat' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: 'Productivity' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: 'Tools & Automation' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: 'Social' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: 'Platform' })).toBeInTheDocument();
|
||||
expect(screen.getByText('Discord')).toBeInTheDocument();
|
||||
expect(screen.getByText('Google Calendar')).toBeInTheDocument();
|
||||
expect(screen.getByText('Google Drive')).toBeInTheDocument();
|
||||
expect(screen.getByText('Gmail')).toBeInTheDocument();
|
||||
expect(screen.getByText('Google Sheets')).toBeInTheDocument();
|
||||
expect(screen.getByText('Notion')).toBeInTheDocument();
|
||||
expect(screen.getByText('Facebook')).toBeInTheDocument();
|
||||
expect(screen.getByText('GitHub')).toBeInTheDocument();
|
||||
expect(screen.getByText('Instagram')).toBeInTheDocument();
|
||||
expect(screen.getByText('Linear')).toBeInTheDocument();
|
||||
expect(screen.getByText('Reddit')).toBeInTheDocument();
|
||||
expect(screen.getByText('Slack')).toBeInTheDocument();
|
||||
expect(screen.getByText('Supabase')).toBeInTheDocument();
|
||||
expect(screen.getByText('Zoom')).toBeInTheDocument();
|
||||
expect(screen.queryByRole('heading', { name: 'Other' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ describe('Skills page — Notion composio integration', () => {
|
||||
fireEvent.click(within(notionCard as HTMLElement).getByRole('button', { name: 'Connect' }));
|
||||
|
||||
expect(await screen.findByRole('heading', { name: 'Connect Notion' })).toBeInTheDocument();
|
||||
expect(screen.getByText(/Connect your Notion account\./i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Connect your Notion account through Composio/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/OpenHuman's own agent permissions/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,6 +24,52 @@ import {
|
||||
vi.stubEnv('DEV', true);
|
||||
vi.stubEnv('MODE', 'test');
|
||||
|
||||
function createStorageMock(): Storage {
|
||||
const store = new Map<string, string>();
|
||||
return {
|
||||
get length() {
|
||||
return store.size;
|
||||
},
|
||||
clear() {
|
||||
store.clear();
|
||||
},
|
||||
getItem(key: string) {
|
||||
return store.has(key) ? store.get(key)! : null;
|
||||
},
|
||||
key(index: number) {
|
||||
return Array.from(store.keys())[index] ?? null;
|
||||
},
|
||||
removeItem(key: string) {
|
||||
store.delete(key);
|
||||
},
|
||||
setItem(key: string, value: string) {
|
||||
store.set(String(key), String(value));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function ensureStorage(name: 'localStorage' | 'sessionStorage') {
|
||||
const current = globalThis[name];
|
||||
if (
|
||||
current &&
|
||||
typeof current.getItem === 'function' &&
|
||||
typeof current.setItem === 'function' &&
|
||||
typeof current.removeItem === 'function' &&
|
||||
typeof current.clear === 'function'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.defineProperty(globalThis, name, {
|
||||
value: createStorageMock(),
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
|
||||
ensureStorage('localStorage');
|
||||
ensureStorage('sessionStorage');
|
||||
|
||||
// Polyfill ResizeObserver for cmdk/Radix components in jsdom
|
||||
if (typeof globalThis.ResizeObserver === 'undefined') {
|
||||
globalThis.ResizeObserver = class ResizeObserver {
|
||||
|
||||
Reference in New Issue
Block a user