From 7f8d2e64afd7e8a3ca393ad395b103dbea41a065 Mon Sep 17 00:00:00 2001
From: tusharmagar {errorMessage}
+ Waiting to connect...
+
@@ -129,64 +129,90 @@ export const ToolContent = ({ className, ...props }: ToolContentProps) => (
/>
);
-export type ToolInputProps = ComponentProps<"div"> & {
+/* ── Tabbed content (Parameters / Result) ────────────────────────── */
+
+export type ToolTabbedContentProps = {
input: ToolUIPart["input"];
-};
-
-export const ToolInput = ({ className, input, ...props }: ToolInputProps) => (
-
-
- Parameters
-
-
-
-
-
-);
-
-export type ToolOutputProps = ComponentProps<"div"> & {
output: ToolUIPart["output"];
- errorText: ToolUIPart["errorText"];
+ errorText?: ToolUIPart["errorText"];
};
-export const ToolOutput = ({
- className,
+export const ToolTabbedContent = ({
+ input,
output,
errorText,
- ...props
-}: ToolOutputProps) => {
- if (!(output || errorText)) {
- return null;
- }
+}: ToolTabbedContentProps) => {
+ const [activeTab, setActiveTab] = useState<"parameters" | "result">("parameters");
+ const hasOutput = output != null || !!errorText;
- let Output = {output as ReactNode};
-
- if (typeof output === "object" && !isValidElement(output)) {
- Output = ;
- } else if (typeof output === "string") {
- Output = ;
+ let OutputNode: ReactNode = null;
+ if (errorText) {
+ OutputNode = ;
+ } else if (output != null) {
+ if (typeof output === "object" && !isValidElement(output)) {
+ OutputNode = ;
+ } else if (typeof output === "string") {
+ OutputNode = ;
+ } else {
+ OutputNode = {output as ReactNode};
+ }
}
return (
-
-
- {errorText ? "Error" : "Result"}
-
-
- {errorText && (
-
- {errorText}
+
+ {/* Tabs */}
+
+
+
+
+
+ {/* Tab content */}
+
+ {activeTab === "parameters" && (
+
+
+
+ )}
+ {activeTab === "result" && (
+
+ {hasOutput ? (
+
+ {OutputNode}
+
+ ) : (
+ (pending...)
+ )}
)}
- {Output}
);
};
+
diff --git a/apps/x/apps/renderer/src/components/chat-sidebar.tsx b/apps/x/apps/renderer/src/components/chat-sidebar.tsx
index ac7f23be..f5bf3bb6 100644
--- a/apps/x/apps/renderer/src/components/chat-sidebar.tsx
+++ b/apps/x/apps/renderer/src/components/chat-sidebar.tsx
@@ -16,8 +16,9 @@ import {
MessageResponse,
} from '@/components/ai-elements/message'
import { Shimmer } from '@/components/ai-elements/shimmer'
-import { Tool, ToolContent, ToolHeader, ToolInput, ToolOutput } from '@/components/ai-elements/tool'
+import { Tool, ToolContent, ToolHeader, ToolTabbedContent } from '@/components/ai-elements/tool'
import { WebSearchResult } from '@/components/ai-elements/web-search-result'
+import { ComposioConnectCard } from '@/components/ai-elements/composio-connect-card'
import { PermissionRequest } from '@/components/ai-elements/permission-request'
import { AskHumanRequest } from '@/components/ai-elements/ask-human-request'
import { Suggestions } from '@/components/ai-elements/suggestions'
@@ -34,6 +35,8 @@ import {
type PermissionResponse,
createEmptyChatTabViewState,
getWebSearchCardData,
+ getComposioConnectCardData,
+ getComposioActionCardData,
isChatMessage,
isErrorMessage,
isToolCall,
@@ -121,6 +124,7 @@ interface ChatSidebarProps {
ttsMode?: 'summary' | 'full'
onToggleTts?: () => void
onTtsModeChange?: (mode: 'summary' | 'full') => void
+ onComposioConnected?: (toolkitSlug: string) => void
}
export function ChatSidebar({
@@ -171,6 +175,7 @@ export function ChatSidebar({
ttsMode,
onToggleTts,
onTtsModeChange,
+ onComposioConnected,
}: ChatSidebarProps) {
const [width, setWidth] = useState(() => getInitialPaneWidth(defaultWidth))
const [isResizing, setIsResizing] = useState(false)
@@ -337,6 +342,21 @@ export function ChatSidebar({
/>
)
}
+ const composioConnectData = getComposioConnectCardData(item)
+ if (composioConnectData) {
+ return (
+
+ )
+ }
+ const composioActionData = getComposioActionCardData(item)
+ const toolTitle = composioActionData ? composioActionData.label : item.name
const errorText = item.status === 'error' ? 'Tool error' : ''
const output = normalizeToolOutput(item.result, item.status)
const input = normalizeToolInput(item.input)
@@ -346,10 +366,9 @@ export function ChatSidebar({
open={isToolOpenForTab?.(tabId, item.id) ?? false}
onOpenChange={(open) => onToolOpenChangeForTab?.(tabId, item.id, open)}
>
-
+
-
- {output !== null ? : null}
+
)
diff --git a/apps/x/apps/renderer/src/components/settings-dialog.tsx b/apps/x/apps/renderer/src/components/settings-dialog.tsx
index 72544bd9..9f3ae6fd 100644
--- a/apps/x/apps/renderer/src/components/settings-dialog.tsx
+++ b/apps/x/apps/renderer/src/components/settings-dialog.tsx
@@ -2,7 +2,7 @@
import * as React from "react"
import { useState, useEffect, useCallback, useMemo } from "react"
-import { Server, Key, Shield, Palette, Monitor, Sun, Moon, Loader2, CheckCircle2, Plus, X, Wrench, Search, ChevronDown, ChevronRight, Check, Link2, Unlink, Tags, Mail, BookOpen, User, Plug } from "lucide-react"
+import { Server, Key, Shield, Palette, Monitor, Sun, Moon, Loader2, CheckCircle2, Plus, X, Wrench, Search, ChevronRight, Link2, Tags, Mail, BookOpen, User, Plug } from "lucide-react"
import {
Dialog,
@@ -724,14 +724,6 @@ interface ToolkitInfo {
composio_managed_auth_schemes: string[]
}
-interface ToolInfo {
- slug: string
- name: string
- description: string
- toolkitSlug: string
- inputParameters?: { type?: string; properties?: Record; required?: string[] }
-}
-
function ToolsLibrarySettings({ dialogOpen }: { dialogOpen: boolean }) {
// API key state
const [apiKeyConfigured, setApiKeyConfigured] = useState(false)
@@ -748,18 +740,6 @@ function ToolsLibrarySettings({ dialogOpen }: { dialogOpen: boolean }) {
const [connectedToolkits, setConnectedToolkits] = useState>(new Set())
const [connectingToolkit, setConnectingToolkit] = useState(null)
- // Tool selection state
- const [expandedToolkit, setExpandedToolkit] = useState(null)
- const [toolkitTools, setToolkitTools] = useState>({})
- const [toolsLoading, setToolsLoading] = useState(null)
- const [enabledToolSlugs, setEnabledToolSlugs] = useState>(new Set())
-
- // Tool search state (per-toolkit, server-side via Composio API)
- const [toolSearchQuery, setToolSearchQuery] = useState("")
- const [toolSearchResults, setToolSearchResults] = useState(null)
- const [toolSearchLoading, setToolSearchLoading] = useState(false)
- const toolSearchTimerRef = React.useRef | null>(null)
-
// Check API key configuration
const checkApiKey = useCallback(async () => {
try {
@@ -783,16 +763,6 @@ function ToolsLibrarySettings({ dialogOpen }: { dialogOpen: boolean }) {
}
}, [])
- // Load enabled tools
- const loadEnabledTools = useCallback(async () => {
- try {
- const result = await window.ipc.invoke("composio:get-enabled-tools", null)
- setEnabledToolSlugs(new Set(Object.keys(result.tools)))
- } catch {
- // ignore
- }
- }, [])
-
// Load toolkits
const loadToolkits = useCallback(async () => {
setToolkitsLoading(true)
@@ -811,8 +781,7 @@ function ToolsLibrarySettings({ dialogOpen }: { dialogOpen: boolean }) {
if (!dialogOpen) return
checkApiKey()
loadConnected()
- loadEnabledTools()
- }, [dialogOpen, checkApiKey, loadConnected, loadEnabledTools])
+ }, [dialogOpen, checkApiKey, loadConnected])
// Load toolkits when API key is configured
useEffect(() => {
@@ -883,151 +852,12 @@ function ToolsLibrarySettings({ dialogOpen }: { dialogOpen: boolean }) {
next.delete(toolkitSlug)
return next
})
- // Remove enabled tools for this toolkit from local state
- setEnabledToolSlugs(prev => {
- const toolsForToolkit = toolkitTools[toolkitSlug] || []
- const next = new Set(prev)
- for (const t of toolsForToolkit) {
- next.delete(t.slug)
- }
- return next
- })
- if (expandedToolkit === toolkitSlug) {
- setExpandedToolkit(null)
- }
toast.success(`Disconnected from ${toolkitSlug}`)
} catch {
toast.error("Failed to disconnect")
}
}
- // Load tools for a toolkit
- const loadToolsForToolkit = async (toolkitSlug: string) => {
- if (toolkitTools[toolkitSlug]) return // Already loaded
- setToolsLoading(toolkitSlug)
- try {
- const result = await window.ipc.invoke("composio:list-toolkit-tools", { toolkitSlug })
- setToolkitTools(prev => ({ ...prev, [toolkitSlug]: result.items }))
- } catch {
- toast.error("Failed to load tools")
- } finally {
- setToolsLoading(null)
- }
- }
-
- // Search tools within a toolkit (debounced, server-side)
- const handleToolSearch = useCallback((toolkitSlug: string, query: string) => {
- setToolSearchQuery(query)
-
- // Clear pending timer
- if (toolSearchTimerRef.current) {
- clearTimeout(toolSearchTimerRef.current)
- toolSearchTimerRef.current = null
- }
-
- // Empty query: clear search results, show all tools
- if (!query.trim()) {
- setToolSearchResults(null)
- setToolSearchLoading(false)
- return
- }
-
- setToolSearchLoading(true)
-
- // Debounce 350ms before hitting the API
- toolSearchTimerRef.current = setTimeout(async () => {
- try {
- const result = await window.ipc.invoke("composio:list-toolkit-tools", {
- toolkitSlug,
- search: query.trim(),
- })
- setToolSearchResults(result.items)
- } catch {
- toast.error("Search failed")
- setToolSearchResults(null)
- } finally {
- setToolSearchLoading(false)
- }
- }, 350)
- }, [])
-
- // Clear tool search when switching toolkits
- useEffect(() => {
- setToolSearchQuery("")
- setToolSearchResults(null)
- setToolSearchLoading(false)
- if (toolSearchTimerRef.current) {
- clearTimeout(toolSearchTimerRef.current)
- toolSearchTimerRef.current = null
- }
- }, [expandedToolkit])
-
- // Clean up pending search timer on unmount
- useEffect(() => {
- return () => {
- if (toolSearchTimerRef.current) {
- clearTimeout(toolSearchTimerRef.current)
- }
- }
- }, [])
-
- // Toggle toolkit expansion
- const handleToggleToolkit = (toolkitSlug: string) => {
- if (expandedToolkit === toolkitSlug) {
- setExpandedToolkit(null)
- } else {
- setExpandedToolkit(toolkitSlug)
- if (connectedToolkits.has(toolkitSlug)) {
- loadToolsForToolkit(toolkitSlug)
- }
- }
- }
-
- // Enable/disable a tool
- const handleToggleTool = async (tool: ToolInfo, enable: boolean) => {
- try {
- if (enable) {
- await window.ipc.invoke("composio:enable-tools", { tools: [tool] })
- setEnabledToolSlugs(prev => new Set([...prev, tool.slug]))
- } else {
- await window.ipc.invoke("composio:disable-tools", { toolSlugs: [tool.slug] })
- setEnabledToolSlugs(prev => {
- const next = new Set(prev)
- next.delete(tool.slug)
- return next
- })
- }
- } catch {
- toast.error("Failed to update tool")
- }
- }
-
- // Enable/disable all tools for a toolkit
- const handleToggleAllTools = async (toolkitSlug: string, enable: boolean) => {
- const tools = toolkitTools[toolkitSlug] || []
- if (tools.length === 0) return
-
- try {
- if (enable) {
- await window.ipc.invoke("composio:enable-tools", { tools })
- setEnabledToolSlugs(prev => {
- const next = new Set(prev)
- for (const t of tools) next.add(t.slug)
- return next
- })
- } else {
- await window.ipc.invoke("composio:disable-tools", { toolSlugs: tools.map(t => t.slug) })
- setEnabledToolSlugs(prev => {
- const next = new Set(prev)
- for (const t of tools) next.delete(t.slug)
- return next
- })
- }
- } catch {
- toast.error("Failed to update tools")
- }
- }
-
// Filter toolkits by search
const filteredToolkits = searchQuery.trim()
? toolkits.filter(t =>
@@ -1125,29 +955,10 @@ function ToolsLibrarySettings({ dialogOpen }: { dialogOpen: boolean }) {
{filteredToolkits.map((toolkit) => {
const isConnected = connectedToolkits.has(toolkit.slug)
const isConnecting = connectingToolkit === toolkit.slug
- const isExpanded = expandedToolkit === toolkit.slug
- const tools = toolkitTools[toolkit.slug] || []
- const isLoadingTools = toolsLoading === toolkit.slug
- const enabledCount = tools.filter(t => enabledToolSlugs.has(t.slug)).length
- const allEnabled = tools.length > 0 && enabledCount === tools.length
-
- // Use search results when actively searching, otherwise show all tools
- const displayTools = (isExpanded && toolSearchResults !== null) ? toolSearchResults : tools
- const isSearching = isExpanded && toolSearchQuery.trim().length > 0
return (
-
- {/* Toolkit card header */}
-
)
})}
diff --git a/apps/x/apps/renderer/src/lib/chat-conversation.ts b/apps/x/apps/renderer/src/lib/chat-conversation.ts
index d92c124d..cf3a8550 100644
--- a/apps/x/apps/renderer/src/lib/chat-conversation.ts
+++ b/apps/x/apps/renderer/src/lib/chat-conversation.ts
@@ -1,6 +1,7 @@
import type { ToolUIPart } from 'ai'
import z from 'zod'
import { AskHumanRequestEvent, ToolPermissionRequestEvent } from '@x/shared/src/runs.js'
+import { COMPOSIO_DISPLAY_NAMES } from '@x/shared/src/composio.js'
export interface MessageAttachment {
path: string
@@ -253,6 +254,94 @@ export const parseAttachedFiles = (content: string): { message: string; files: s
return { message: cleanMessage.trim(), files }
}
+// Composio connect card data
+export type ComposioConnectCardData = {
+ toolkitSlug: string
+ toolkitDisplayName: string
+ alreadyConnected: boolean
+}
+
+// Display names imported from @x/shared/composio (single source of truth)
+const composioDisplayNames = COMPOSIO_DISPLAY_NAMES
+
+export const getComposioConnectCardData = (tool: ToolCall): ComposioConnectCardData | null => {
+ if (tool.name !== 'composio-connect-toolkit') return null
+
+ const input = normalizeToolInput(tool.input) as Record | undefined
+ const result = tool.result as Record | undefined
+
+ const toolkitSlug = (input?.toolkitSlug as string) || ''
+ const alreadyConnected = result?.alreadyConnected === true
+
+ return {
+ toolkitSlug,
+ toolkitDisplayName: composioDisplayNames[toolkitSlug] || toolkitSlug,
+ alreadyConnected,
+ }
+}
+
+// Composio action card data (for search, execute, list tools)
+export type ComposioActionCardData = {
+ actionType: 'search' | 'execute' | 'list'
+ label: string
+}
+
+export const getComposioActionCardData = (tool: ToolCall): ComposioActionCardData | null => {
+ const input = normalizeToolInput(tool.input) as Record | undefined
+ const result = tool.result as Record | undefined
+
+ if (tool.name === 'composio-search-tools') {
+ const query = (input?.query as string) || 'tools'
+ const toolkitSlug = input?.toolkitSlug as string | undefined
+ const toolkit = toolkitSlug ? composioDisplayNames[toolkitSlug] || toolkitSlug : null
+ const count = (result?.resultCount as number) ?? null
+
+ let label = `Searching for "${query}"`
+ if (toolkit) label += ` in ${toolkit}`
+ if (count !== null && tool.status === 'completed') {
+ label = count > 0 ? `Found ${count} tool${count !== 1 ? 's' : ''} for "${query}"` : `No tools found for "${query}"`
+ if (toolkit) label += ` in ${toolkit}`
+ }
+ return { actionType: 'search', label }
+ }
+
+ if (tool.name === 'composio-execute-tool') {
+ const toolSlug = (input?.toolSlug as string) || ''
+ const toolkitSlug = (input?.toolkitSlug as string) || ''
+ const toolkit = composioDisplayNames[toolkitSlug] || toolkitSlug
+ const successful = result?.successful as boolean | undefined
+
+ // Make the tool slug human-readable: GITHUB_ISSUES_LIST_FOR_REPO → "Issues list for repo"
+ const readableName = toolSlug
+ .replace(/^[A-Z]+_/, '') // Remove toolkit prefix
+ .toLowerCase()
+ .replace(/_/g, ' ')
+ .replace(/^\w/, c => c.toUpperCase())
+
+ let label = `Running ${readableName}`
+ if (toolkit) label += ` on ${toolkit}`
+ if (tool.status === 'completed') {
+ label = successful === false ? `Failed: ${readableName}` : `${readableName}`
+ if (toolkit) label += ` on ${toolkit}`
+ }
+ return { actionType: 'execute', label }
+ }
+
+ if (tool.name === 'composio-list-toolkits') {
+ const count = (result?.totalCount as number) ?? null
+ const connected = (result?.connectedCount as number) ?? null
+
+ let label = 'Listing available integrations'
+ if (count !== null && tool.status === 'completed') {
+ label = `${count} integrations available`
+ if (connected !== null && connected > 0) label += `, ${connected} connected`
+ }
+ return { actionType: 'list', label }
+ }
+
+ return null
+}
+
export const inferRunTitleFromMessage = (content: string): string | undefined => {
const { message } = parseAttachedFiles(content)
const normalized = message.replace(/\s+/g, ' ').trim()
diff --git a/apps/x/packages/core/src/agents/runtime.ts b/apps/x/packages/core/src/agents/runtime.ts
index 353c434c..7d6006a7 100644
--- a/apps/x/packages/core/src/agents/runtime.ts
+++ b/apps/x/packages/core/src/agents/runtime.ts
@@ -370,8 +370,7 @@ function formatLlmStreamError(rawError: unknown): string {
export async function loadAgent(id: string): Promise> {
if (id === "copilot" || id === "rowboatx") {
- // Rebuild tools from current BuiltinTools to pick up dynamically
- // registered Composio tools (added via refreshComposioTools).
+ // Rebuild tools from current BuiltinTools (includes Composio meta-tools).
const tools: Record> = {};
for (const name of Object.keys(BuiltinTools)) {
tools[name] = { type: "builtin", name };
diff --git a/apps/x/packages/core/src/application/assistant/instructions.ts b/apps/x/packages/core/src/application/assistant/instructions.ts
index 6bd29963..ff15dbfc 100644
--- a/apps/x/packages/core/src/application/assistant/instructions.ts
+++ b/apps/x/packages/core/src/application/assistant/instructions.ts
@@ -1,51 +1,55 @@
import { skillCatalog } from "./skills/index.js"; // eslint-disable-line @typescript-eslint/no-unused-vars -- used in template literal
import { getRuntimeContext, getRuntimeContextPrompt } from "./runtime-context.js";
-import { composioEnabledToolsRepo } from "../../composio/enabled-tools-repo.js";
import { composioAccountsRepo } from "../../composio/repo.js";
import { isConfigured as isComposioConfigured } from "../../composio/client.js";
+import { CURATED_TOOLKITS } from "../../composio/curated-toolkits.js";
const runtimeContextPrompt = getRuntimeContextPrompt(getRuntimeContext());
/**
- * Generate dynamic instructions section for Composio tools.
- * Returns empty string if no tools are enabled.
+ * Generate dynamic instructions section for Composio integrations.
+ * Lists connected toolkits and explains the meta-tool discovery flow.
*/
async function getComposioToolsPrompt(): Promise {
if (!(await isComposioConfigured())) return '';
- const enabledTools = composioEnabledToolsRepo.getAll();
- const toolEntries = Object.values(enabledTools);
- if (toolEntries.length === 0) return '';
+ const connectedToolkits = composioAccountsRepo.getConnectedToolkits();
- // Group tools by toolkit
- const byToolkit: Record> = {};
- for (const tool of toolEntries) {
- if (!byToolkit[tool.toolkitSlug]) {
- byToolkit[tool.toolkitSlug] = [];
- }
- byToolkit[tool.toolkitSlug].push(tool);
+ let prompt = `\n## Composio Integrations\n\n`;
+ prompt += `You can connect to external services (Gmail, Slack, GitHub, Notion, etc.) via Composio.\n\n`;
+
+ if (connectedToolkits.length > 0) {
+ const connectedNames = connectedToolkits
+ .map(slug => CURATED_TOOLKITS.find(t => t.slug === slug)?.displayName ?? slug)
+ .join(', ');
+ prompt += `**Currently connected:** ${connectedNames}\n\n`;
+ } else {
+ prompt += `**No services connected yet.** Use \`composio-list-toolkits\` to show available integrations, or \`composio-connect-toolkit\` to help the user connect one.\n\n`;
}
- // Check which toolkits are connected
- const connectedToolkits = new Set(composioAccountsRepo.getConnectedToolkits());
+ prompt += `**CRITICAL: NEVER say "I can't access [service]" or "I don't have access to [service]" without FIRST trying Composio.** If a user asks about ANY third-party service (LinkedIn, Gmail, GitHub, Slack, etc.), your FIRST action must be to check \`composio-list-toolkits\` or try \`composio-connect-toolkit\`. Never give up before trying.\n\n`;
- let prompt = `\n## Composio Integration Tools\n\n`;
- prompt += `You have access to external service integrations via Composio. These tools are prefixed with \`composio-\` and connect to third-party services on the user's behalf. The user has enabled these tools in Settings > Tools Library.\n\n`;
- prompt += `**How to use Composio tools:**\n`;
- prompt += `- Each tool is named \`composio-{ACTION_SLUG}\` (e.g., \`composio-GMAIL_SEND_EMAIL\`)\n`;
- prompt += `- Call them like any other builtin tool — pass the required parameters and they execute via the connected account\n`;
- prompt += `- If a tool returns an error about the toolkit not being connected, inform the user they need to connect it in Settings > Tools Library\n`;
- prompt += `- Always confirm with the user before taking actions that send messages, create items, or modify data in external services\n\n`;
+ prompt += `**Discovery & Execution Flow:**\n`;
+ prompt += `1. When the user asks to interact with a service (e.g., "get my LinkedIn profile", "check my email", "list GitHub issues"):\n`;
+ prompt += ` a. Check if the service is connected (via \`composio-list-toolkits\` or the connected list above)\n`;
+ prompt += ` b. If NOT connected, call \`composio-connect-toolkit\` immediately — do NOT ask for confirmation or tell the user you can't do it\n`;
+ prompt += ` c. If connected, proceed to search and execute\n`;
+ prompt += `2. Use \`composio-search-tools\` with SHORT keyword queries (e.g., "list issues", "send email", "get profile") — avoid long sentences.\n`;
+ prompt += `3. Read the \`inputSchema\` from search results carefully — note which fields are in \`required\`.\n`;
+ prompt += `4. Call \`composio-execute-tool\` with the tool slug, toolkit slug, AND all required \`arguments\`. For tools with empty \`properties: {}\`, pass \`arguments: {}\`.\n`;
- for (const [toolkitSlug, tools] of Object.entries(byToolkit)) {
- const isConnected = connectedToolkits.has(toolkitSlug);
- const statusBadge = isConnected ? '(Connected)' : '(Not Connected)';
- prompt += `### ${toolkitSlug.charAt(0).toUpperCase() + toolkitSlug.slice(1)} ${statusBadge}\n`;
- for (const tool of tools) {
- prompt += `- \`composio-${tool.slug}\` — ${tool.description}\n`;
- }
- prompt += `\n`;
- }
+ prompt += `**Example — fetching GitHub issues for owner/repo:**\n`;
+ prompt += `1. \`composio-search-tools({ query: "list issues", toolkitSlug: "github" })\` → finds \`GITHUB_ISSUES_LIST_FOR_REPO\`\n`;
+ prompt += `2. Schema shows required: \`["owner", "repo"]\` — extract from user's request (e.g., "rowboatlabs/rowboat" → owner: "rowboatlabs", repo: "rowboat")\n`;
+ prompt += `3. \`composio-execute-tool({ toolSlug: "GITHUB_ISSUES_LIST_FOR_REPO", toolkitSlug: "github", arguments: { owner: "rowboatlabs", repo: "rowboat", state: "open", per_page: 100 } })\`\n\n`;
+
+ prompt += `**Important:**\n`;
+ prompt += `- Use short keyword search queries, NOT full sentences (good: "list issues", bad: "get all open issues for a GitHub repository")\n`;
+ prompt += `- ALWAYS pass required arguments to composio-execute-tool — read the inputSchema from search results\n`;
+ prompt += `- **If a tool call fails (e.g., missing fields), fix the arguments and retry IMMEDIATELY — do NOT stop and narrate the error to the user. Just fix it and continue.**\n`;
+ prompt += `- **Multi-part requests:** When the user asks to "connect X and then do Y", complete BOTH parts in one turn. If part 1 (connect) is already done, proceed directly to part 2 (the actual task).\n`;
+ prompt += `- Confirm with the user before executing tools that send messages, create items, or modify data (NOT for read-only queries or connecting)\n`;
+ prompt += `- Connecting a toolkit is always safe — just do it when needed, don't ask permission\n`;
return prompt;
}
@@ -71,7 +75,9 @@ You're an insightful, encouraging assistant who combines meticulous clarity with
## What Rowboat Is
Rowboat is an agentic assistant for everyday work - emails, meetings, projects, and people. Users give you tasks like "draft a follow-up email," "prep me for this meeting," or "summarize where we are with this project." You figure out what context you need, pull from emails and meetings, and get it done.
-**Email Drafting:** When users ask you to draft emails or respond to emails, load the \`draft-emails\` skill first. It provides structured guidance for processing emails, gathering context from calendar and knowledge base, and creating well-informed draft responses.
+**Email Drafting:** When users ask you to **draft** or **compose** emails (e.g., "draft a follow-up to Monica", "write an email to John about the project"), load the \`draft-emails\` skill first. Do NOT load this skill for reading, fetching, or checking emails — use Composio tools for that instead.
+
+**Live Email/Calendar/Service Queries:** When users ask to **read**, **fetch**, **check**, or **view** emails, calendar events, or any data from a connected service (e.g., "what's my latest email?", "check my inbox", "what meetings do I have today?"), use \`composio-search-tools\` and \`composio-execute-tool\` to query the connected service directly. Do NOT look in local \`gmail_sync/\` or \`calendar_sync/\` folders — use Composio for live data.
**Meeting Prep:** When users ask you to prepare for a meeting, prep for a call, or brief them on attendees, load the \`meeting-prep\` skill first. It provides structured guidance for gathering context about attendees from the knowledge base and creating useful meeting briefs.
@@ -212,13 +218,23 @@ Always consult this catalog first so you load the right skills before taking act
- Never start a response with a heading. Lead with a sentence or two of context first.
- Avoid deeply nested bullets. If nesting beyond 2 levels, restructure.
-## MCP Tool Discovery (CRITICAL)
+## Tool Priority: Composio First, Then MCP
-**ALWAYS check for MCP tools BEFORE saying you can't do something.**
+**When the user wants to interact with a third-party service (GitHub, Gmail, Slack, Notion, Jira, etc.):**
+1. **FIRST** use the \`composio-*\` builtin tools — they are already authenticated and ready. Do NOT load the mcp-integration skill or draft-emails skill for service queries.
+2. **ONLY** if the service is NOT available through Composio, fall back to MCP tools.
-When a user asks for ANY task that might require external capabilities (web search, internet access, APIs, data fetching, etc.), check MCP tools first using \`listMcpServers\` and \`listMcpTools\`. Load the "mcp-integration" skill for detailed guidance on discovering and executing MCP tools.
+**Common Composio tasks (use composio-search-tools + composio-execute-tool):**
+- "What's my latest email?" → search "fetch emails" in gmail toolkit
+- "Check my inbox" → search "fetch emails" in gmail toolkit
+- "What meetings do I have?" → search "list events" in googlecalendar toolkit
+- "Create a GitHub issue" → search "create issue" in github toolkit
+- "Send a Slack message" → search "send message" in slack toolkit
-**DO NOT** immediately respond with "I can't access the internet" or "I don't have that capability" without checking MCP tools first!
+**When the user wants capabilities that Composio does NOT cover** (web search, file scraping, audio generation, etc.):
+- Check MCP tools using \`listMcpServers\` and \`listMcpTools\`. Load the "mcp-integration" skill for guidance.
+
+**DO NOT** immediately respond with "I can't access the internet" or "I don't have that capability" without checking both Composio and MCP tools first!
## Execution Reminders
- Explore existing files and structure before creating new assets.
@@ -258,7 +274,10 @@ ${runtimeContextPrompt}
- \`web-search\` - Search the web. Returns rich results with full text, highlights, and metadata. The \`category\` parameter defaults to \`general\` (full web search) — only use a specific category like \`news\`, \`company\`, \`research paper\` etc. when the query is clearly about that type. For everyday queries (weather, restaurants, prices, how-to), use \`general\`.
- \`app-navigation\` - Control the app UI: open notes, switch views, filter/search the knowledge base, manage saved views. **Load the \`app-navigation\` skill before using this tool.**
- \`save-to-memory\` - Save observations about the user to the agent memory system. Use this proactively during conversations.
-- **Composio tools** (\`composio-*\`) — External service integrations enabled by the user in Settings > Tools Library. These connect to third-party apps like Gmail, GitHub, Linear, Notion, etc. See the "Composio Integration Tools" section below for available tools.
+- \`composio-list-toolkits\` — List available integrations (Gmail, Slack, GitHub, etc.) and their connection status
+- \`composio-search-tools\` — Search for tools by use case (e.g., "send email", "create issue"); returns tool slugs and input schemas
+- \`composio-execute-tool\` — Execute a Composio tool by slug with parameters from search results
+- \`composio-connect-toolkit\` — Connect a service (Gmail, Slack, GitHub, etc.) via OAuth directly from chat
**Prefer these tools whenever possible** — they work instantly with zero friction. For file operations inside \`~/.rowboat/\`, always use these instead of \`executeCommand\`.
@@ -306,8 +325,7 @@ let cachedInstructions: string | null = null;
/**
* Invalidate the cached instructions so the next buildCopilotInstructions() call
- * regenerates the Composio tools section. Call this after enabling/disabling tools
- * or connecting/disconnecting a toolkit.
+ * regenerates the Composio section. Call this after connecting/disconnecting a toolkit.
*/
export function invalidateCopilotInstructionsCache(): void {
cachedInstructions = null;
diff --git a/apps/x/packages/core/src/application/assistant/skills/mcp-integration/skill.ts b/apps/x/packages/core/src/application/assistant/skills/mcp-integration/skill.ts
index eed51898..9d07f7ad 100644
--- a/apps/x/packages/core/src/application/assistant/skills/mcp-integration/skill.ts
+++ b/apps/x/packages/core/src/application/assistant/skills/mcp-integration/skill.ts
@@ -3,9 +3,13 @@ export const skill = String.raw`
**Load this skill proactively** when a user asks for ANY task that might require external capabilities (web search, internet access, APIs, data fetching, time/date, etc.). This skill provides complete guidance on discovering and executing MCP tools.
-## CRITICAL: Always Check MCP Tools First
+## CRITICAL: Composio Tools Take Priority Over MCP
-**IMPORTANT**: When a user asks for ANY task that might require external capabilities (web search, API calls, data fetching, etc.), ALWAYS:
+**If a Composio toolkit is connected for the service the user wants (GitHub, Gmail, Slack, etc.), use the \`composio-search-tools\` and \`composio-execute-tool\` builtin tools — NOT MCP tools.** Composio integrations are already authenticated and ready to use. Only fall back to MCP tools if the service is NOT available through Composio.
+
+## When to Check MCP Tools
+
+**IMPORTANT**: When a user asks for a task that requires external capabilities AND no Composio toolkit covers it, check MCP tools:
1. **First check**: Call \`listMcpServers\` to see what's available
2. **Then list tools**: Call \`listMcpTools\` on relevant servers
@@ -23,9 +27,7 @@ export const skill = String.raw`
| "Read/write files" | filesystem | \`read_file\`, \`write_file\` |
| "Get current time/date" | time | \`get_current_time\` |
| "Make HTTP request" | fetch | \`fetch\`, \`post\` |
-| "GitHub operations" | github | \`create_issue\`, \`search_repos\` |
| "Generate audio/speech" | elevenLabs | \`text_to_speech\` |
-| "Tweet/social media" | twitter, composio | Various social tools |
## Key concepts
- MCP servers expose tools (web scraping, APIs, databases, etc.) declared in \`config/mcp.json\`.
diff --git a/apps/x/packages/core/src/application/lib/builtin-tools.ts b/apps/x/packages/core/src/application/lib/builtin-tools.ts
index ee6872a6..945ab4aa 100644
--- a/apps/x/packages/core/src/application/lib/builtin-tools.ts
+++ b/apps/x/packages/core/src/application/lib/builtin-tools.ts
@@ -13,11 +13,11 @@ import * as workspace from "../../workspace/workspace.js";
import { IAgentsRepo } from "../../agents/repo.js";
import { WorkDir } from "../../config/config.js";
import { composioAccountsRepo } from "../../composio/repo.js";
-import { composioEnabledToolsRepo } from "../../composio/enabled-tools-repo.js";
-import { executeAction as executeComposioAction, isConfigured as isComposioConfigured } from "../../composio/client.js";
-import { invalidateCopilotInstructionsCache } from "../assistant/instructions.js";
+import { executeAction as executeComposioAction, isConfigured as isComposioConfigured, searchTools as searchComposioTools } from "../../composio/client.js";
+import { CURATED_TOOLKITS, CURATED_TOOLKIT_SLUGS } from "../../composio/curated-toolkits.js";
+import { getConnectionInitiator } from "../../composio/connection-bridge.js";
import type { ToolContext } from "./exec-tool.js";
-import { generateText, jsonSchema } from "ai";
+import { generateText } from "ai";
import { createProvider } from "../../models/models.js";
import { IModelConfigRepo } from "../../models/repo.js";
import { isSignedIn } from "../../account/account.js";
@@ -1177,93 +1177,174 @@ export const BuiltinTools: z.infer = {
};
},
},
+
+ // ========================================================================
+ // Composio Meta-Tools
+ // ========================================================================
+
+ 'composio-list-toolkits': {
+ description: 'List available Composio integrations (Gmail, Slack, GitHub, etc.) and their connection status. Use this to show the user what services they can connect to.',
+ inputSchema: z.object({
+ category: z.enum(['all', 'communication', 'productivity', 'development', 'crm', 'social', 'storage', 'support']).optional()
+ .describe('Filter by category. Defaults to "all".'),
+ }),
+ execute: async ({ category }: { category?: string }) => {
+ const toolkits = CURATED_TOOLKITS
+ .filter(t => !category || category === 'all' || t.category === category)
+ .map(t => ({
+ slug: t.slug,
+ name: t.displayName,
+ category: t.category,
+ isConnected: composioAccountsRepo.isConnected(t.slug),
+ }));
+
+ const connectedCount = toolkits.filter(t => t.isConnected).length;
+ return {
+ toolkits,
+ connectedCount,
+ totalCount: toolkits.length,
+ };
+ },
+ isAvailable: async () => isComposioConfigured(),
+ },
+
+ 'composio-search-tools': {
+ description: 'Search for Composio tools by use case across connected services. Returns tool slugs, descriptions, and input schemas so you can call composio-execute-tool with the right parameters. Example: search "send email" to find Gmail tools, "create issue" to find GitHub/Jira tools.',
+ inputSchema: z.object({
+ query: z.string().describe('Natural language description of what you want to do (e.g., "send an email", "create a GitHub issue", "schedule a meeting")'),
+ toolkitSlug: z.string().optional().describe('Optional: limit search to a specific toolkit (e.g., "gmail", "github")'),
+ }),
+ execute: async ({ query, toolkitSlug }: { query: string; toolkitSlug?: string }) => {
+ try {
+ const toolkitFilter = toolkitSlug ? [toolkitSlug] : undefined;
+ const result = await searchComposioTools(query, toolkitFilter);
+
+ // Filter to curated toolkits only (skip if a specific toolkit was requested —
+ // the API already filtered server-side)
+ const filtered = toolkitSlug
+ ? result.items
+ : result.items.filter(t => CURATED_TOOLKIT_SLUGS.has(t.toolkitSlug));
+
+ // Annotate with connection status
+ const tools = filtered.map(t => ({
+ slug: t.slug,
+ name: t.name,
+ description: t.description,
+ toolkitSlug: t.toolkitSlug,
+ isConnected: composioAccountsRepo.isConnected(t.toolkitSlug),
+ inputSchema: t.inputParameters,
+ }));
+
+ return {
+ tools,
+ resultCount: tools.length,
+ hint: tools.some(t => !t.isConnected)
+ ? 'Some tools require connecting the toolkit first. Use composio-connect-toolkit to help the user authenticate.'
+ : undefined,
+ };
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ return { tools: [], resultCount: 0, error: message };
+ }
+ },
+ isAvailable: async () => isComposioConfigured(),
+ },
+
+ 'composio-execute-tool': {
+ description: 'Execute a Composio tool by its slug. You MUST pass the arguments field with all required parameters from the search results inputSchema. Example: composio-execute-tool({ toolSlug: "GITHUB_ISSUES_LIST_FOR_REPO", toolkitSlug: "github", arguments: { owner: "rowboatlabs", repo: "rowboat", state: "open", per_page: 100 } })',
+ inputSchema: z.object({
+ toolSlug: z.string().describe('EXACT tool slug from search results (e.g., "GITHUB_ISSUES_LIST_FOR_REPO"). Copy it exactly — do not modify it.'),
+ toolkitSlug: z.string().describe('The toolkit slug (e.g., "gmail", "github")'),
+ arguments: z.record(z.string(), z.unknown()).describe('REQUIRED: Tool input parameters as key-value pairs. Get the required fields from the inputSchema returned by composio-search-tools. Never omit this.'),
+ }),
+ execute: async ({ toolSlug, toolkitSlug, arguments: args }: { toolSlug: string; toolkitSlug: string; arguments?: Record }) => {
+ // Default arguments to {} if the LLM omits the field entirely
+ const toolArgs = args ?? {};
+
+ // Check connection
+ const account = composioAccountsRepo.getAccount(toolkitSlug);
+ if (!account || account.status !== 'ACTIVE') {
+ return {
+ successful: false,
+ data: null,
+ error: `Toolkit "${toolkitSlug}" is not connected. Use composio-connect-toolkit to help the user connect it first.`,
+ };
+ }
+
+ try {
+ return await executeComposioAction(toolSlug, {
+ connected_account_id: account.id,
+ user_id: 'rowboat-user',
+ version: 'latest',
+ arguments: toolArgs,
+ });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ console.error(`[Composio] Tool execution failed for ${toolSlug}:`, message);
+ return {
+ successful: false,
+ data: null,
+ error: `Failed to execute ${toolSlug}: ${message}. If fields are missing, check the inputSchema and retry with the correct arguments.`,
+ };
+ }
+ },
+ isAvailable: async () => isComposioConfigured(),
+ },
+
+ 'composio-connect-toolkit': {
+ description: 'Connect a Composio service (Gmail, Slack, GitHub, etc.) via OAuth. Opens the user\'s browser for authentication. After authenticating, the user can use tools from that service.',
+ inputSchema: z.object({
+ toolkitSlug: z.string().describe('The toolkit slug to connect (e.g., "gmail", "github", "slack", "notion")'),
+ }),
+ execute: async ({ toolkitSlug }: { toolkitSlug: string }) => {
+ // Validate against curated list
+ if (!CURATED_TOOLKIT_SLUGS.has(toolkitSlug)) {
+ const available = CURATED_TOOLKITS.map(t => `${t.slug} (${t.displayName})`).join(', ');
+ return {
+ success: false,
+ error: `Unknown toolkit "${toolkitSlug}". Available toolkits: ${available}`,
+ };
+ }
+
+ // Check if already connected
+ if (composioAccountsRepo.isConnected(toolkitSlug)) {
+ return {
+ success: true,
+ message: `${toolkitSlug} is already connected. You can search for and execute its tools.`,
+ alreadyConnected: true,
+ };
+ }
+
+ // Use the connection bridge to trigger OAuth
+ const initiator = getConnectionInitiator();
+ if (!initiator) {
+ return {
+ success: false,
+ error: 'Connection system not available. Please try connecting via Settings > Tools Library instead.',
+ };
+ }
+
+ try {
+ const result = await initiator(toolkitSlug);
+ if (result.success) {
+ const toolkit = CURATED_TOOLKITS.find(t => t.slug === toolkitSlug);
+ return {
+ success: true,
+ message: `Opening browser to authenticate with ${toolkit?.displayName ?? toolkitSlug}. Please complete the authentication in your browser, then let me know when you're done.`,
+ };
+ }
+ return {
+ success: false,
+ error: result.error || 'Failed to initiate connection',
+ };
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ return {
+ success: false,
+ error: `Connection failed: ${message}`,
+ };
+ }
+ },
+ isAvailable: async () => isComposioConfigured(),
+ },
};
-
-// ============================================================================
-// Dynamic Composio Tool Registration
-// ============================================================================
-
-const COMPOSIO_TOOL_PREFIX = 'composio-';
-
-/**
- * Unregister all dynamically registered Composio tools
- */
-function unregisterComposioTools(): void {
- for (const key of Object.keys(BuiltinTools)) {
- if (key.startsWith(COMPOSIO_TOOL_PREFIX)) {
- delete BuiltinTools[key];
- }
- }
-}
-
-/**
- * Register enabled Composio tools as builtin tools.
- * Each enabled tool gets a generic execute function that routes
- * to the Composio API via the connected account.
- */
-function registerComposioTools(): void {
- const enabledTools = composioEnabledToolsRepo.getAll();
-
- for (const [slug, tool] of Object.entries(enabledTools)) {
- const toolKey = `${COMPOSIO_TOOL_PREFIX}${slug}`;
- const toolkitSlug = tool.toolkitSlug;
-
- const inputParams = tool.inputParameters ?? { type: 'object', properties: {} };
-
- BuiltinTools[toolKey] = {
- description: `[${tool.toolkitSlug}] ${tool.description}`,
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- inputSchema: jsonSchema({
- type: 'object',
- properties: (inputParams.properties ?? {}) as any,
- ...(inputParams.required ? { required: inputParams.required } : {}),
- } as any) as unknown as ZodType,
- execute: async (input: Record) => {
- const account = composioAccountsRepo.getAccount(toolkitSlug);
- if (!account || account.status !== 'ACTIVE') {
- return {
- success: false,
- error: `Toolkit "${toolkitSlug}" is not connected. Please connect it in Settings > Tools Library.`,
- };
- }
- try {
- return await executeComposioAction(slug, {
- connected_account_id: account.id,
- user_id: 'rowboat-user',
- version: 'latest',
- arguments: input,
- });
- } catch (error) {
- const message = error instanceof Error ? error.message : String(error);
- console.error(`[Composio] Tool execution failed for ${slug}:`, message);
- return {
- success: false,
- error: `Failed to execute ${slug}: ${message}`,
- };
- }
- },
- isAvailable: async () => {
- return (await isComposioConfigured()) && composioAccountsRepo.isConnected(toolkitSlug);
- },
- };
- }
-
- const count = Object.keys(enabledTools).length;
- if (count > 0) {
- console.log(`[Composio] Registered ${count} dynamic tool(s)`);
- }
-}
-
-/**
- * Refresh dynamic Composio tools by unregistering all and re-registering from the repo.
- * Called after enabling/disabling tools or disconnecting a toolkit.
- * Also invalidates the cached agent instructions so they reflect the new tool set.
- */
-export function refreshComposioTools(): void {
- unregisterComposioTools();
- registerComposioTools();
- invalidateCopilotInstructionsCache();
-}
-
-// Register on module load
-refreshComposioTools();
diff --git a/apps/x/packages/core/src/composio/client.ts b/apps/x/packages/core/src/composio/client.ts
index 9fce564f..70b51050 100644
--- a/apps/x/packages/core/src/composio/client.ts
+++ b/apps/x/packages/core/src/composio/client.ts
@@ -291,6 +291,79 @@ export async function deleteConnectedAccount(connectedAccountId: string): Promis
});
}
+/**
+ * Schema for search results: includes toolkit info and full input_parameters.
+ */
+const ZSearchResultTool = z.object({
+ slug: z.string(),
+ name: z.string(),
+ description: z.string(),
+ toolkit: z.object({
+ slug: z.string(),
+ name: z.string(),
+ logo: z.string(),
+ }).optional(),
+ input_parameters: z.object({
+ type: z.literal('object').optional().default('object'),
+ properties: z.record(z.string(), z.unknown()).optional().default({}),
+ required: z.array(z.string()).optional(),
+ }).optional().default({ type: 'object', properties: {} }),
+}).passthrough();
+
+/**
+ * Infer toolkit slug from a tool slug.
+ * Tool naming convention: TOOLKIT_ACTION (e.g., GITHUB_CREATE_ISSUE → github).
+ * For multi-word toolkit slugs (GOOGLECALENDAR_CREATE_EVENT), the prefix before
+ * the first underscore matches the toolkit slug.
+ */
+function inferToolkitSlug(toolSlug: string): string {
+ // Tool slugs are uppercase: GITHUB_CREATE_ISSUE, GMAIL_SEND_EMAIL, etc.
+ // The toolkit prefix is everything before the first action word.
+ // Strategy: lowercase the slug and try progressively shorter prefixes.
+ const lower = toolSlug.toLowerCase();
+ const parts = lower.split('_');
+ // Try joining from 1 part up to all-but-1 to find a known prefix
+ // Most common: single-word prefix (github, gmail, slack)
+ return parts[0] ?? lower;
+}
+
+/**
+ * Search for tools across all toolkits (or optionally filtered by specific toolkit slugs).
+ * Returns tools with full input_parameters so the agent knows what params to pass.
+ */
+export async function searchTools(
+ searchQuery: string,
+ toolkitSlugs?: string[],
+): Promise<{ items: Array<{ slug: string; name: string; description: string; toolkitSlug: string; inputParameters: { type: 'object'; properties: Record; required?: string[] } }> }> {
+ const params: Record = {
+ search: searchQuery,
+ limit: '15',
+ };
+ if (toolkitSlugs && toolkitSlugs.length === 1) {
+ params.toolkit_slug = toolkitSlugs[0];
+ }
+
+ const result = await composioApiCall(ZListResponse(ZSearchResultTool), "/tools", params);
+
+ const items = result.items.map((item) => ({
+ slug: item.slug,
+ name: item.name,
+ description: item.description,
+ // Use toolkit.slug from API response, fall back to inferring from tool slug,
+ // and finally fall back to the requested toolkit slug if one was provided.
+ toolkitSlug: item.toolkit?.slug
+ || inferToolkitSlug(item.slug)
+ || (toolkitSlugs?.length === 1 ? toolkitSlugs[0] : ''),
+ inputParameters: {
+ type: 'object' as const,
+ properties: item.input_parameters?.properties ?? {},
+ required: item.input_parameters?.required,
+ },
+ }));
+
+ return { items };
+}
+
/**
* List available tools for a toolkit
*/
@@ -308,54 +381,6 @@ export async function listToolkitTools(
return composioApiCall(ZListResponse(ZTool), "/tools", params);
}
-/**
- * Schema for the detailed tools response (preserves input_parameters).
- * Uses passthrough so extra API fields don't cause validation failures.
- */
-const ZDetailedTool = z.object({
- slug: z.string(),
- name: z.string(),
- description: z.string(),
- input_parameters: z.object({
- type: z.literal('object').optional().default('object'),
- properties: z.record(z.string(), z.unknown()).optional().default({}),
- required: z.array(z.string()).optional(),
- }).optional().default({ type: 'object', properties: {} }),
-}).passthrough();
-
-/**
- * List available tools for a toolkit with full details including input_parameters.
- * Uses composioApiCall for consistent error handling, logging, and validation.
- */
-export async function listToolkitToolsDetailed(
- toolkitSlug: string,
- searchQuery: string | null = null,
-): Promise<{ items: Array<{ slug: string; name: string; description: string; toolkitSlug: string; inputParameters: { type: 'object'; properties: Record; required?: string[] } }> }> {
- const params: Record = {
- toolkit_slug: toolkitSlug,
- limit: '200',
- };
- if (searchQuery) {
- params.search = searchQuery;
- }
-
- const result = await composioApiCall(ZListResponse(ZDetailedTool), "/tools", params);
-
- return {
- items: result.items.map((item) => ({
- slug: item.slug,
- name: item.name,
- description: item.description,
- toolkitSlug,
- inputParameters: {
- type: 'object' as const,
- properties: item.input_parameters?.properties ?? {},
- required: item.input_parameters?.required,
- },
- })),
- };
-}
-
/**
* Execute a tool action
*/
diff --git a/apps/x/packages/core/src/composio/connection-bridge.ts b/apps/x/packages/core/src/composio/connection-bridge.ts
new file mode 100644
index 00000000..cf6c5d6c
--- /dev/null
+++ b/apps/x/packages/core/src/composio/connection-bridge.ts
@@ -0,0 +1,33 @@
+/**
+ * Connection bridge for Composio toolkit OAuth.
+ *
+ * Builtin tools run in the core package which cannot import Electron-specific
+ * code from the main process. This module provides a callback registry so the
+ * main process can register its `initiateConnection` function at startup, and
+ * builtin tools can call it at runtime.
+ */
+
+type ConnectionInitiator = (toolkitSlug: string) => Promise<{
+ success: boolean;
+ redirectUrl?: string;
+ connectedAccountId?: string;
+ error?: string;
+}>;
+
+let connectionInitiator: ConnectionInitiator | null = null;
+
+/**
+ * Register the connection initiator callback.
+ * Called once by the main process at startup.
+ */
+export function setConnectionInitiator(fn: ConnectionInitiator): void {
+ connectionInitiator = fn;
+}
+
+/**
+ * Get the registered connection initiator.
+ * Returns null if not yet registered (app not fully initialized).
+ */
+export function getConnectionInitiator(): ConnectionInitiator | null {
+ return connectionInitiator;
+}
diff --git a/apps/x/packages/core/src/composio/curated-toolkits.ts b/apps/x/packages/core/src/composio/curated-toolkits.ts
new file mode 100644
index 00000000..5c4077f6
--- /dev/null
+++ b/apps/x/packages/core/src/composio/curated-toolkits.ts
@@ -0,0 +1,74 @@
+/**
+ * Curated list of Composio toolkits available to Rowboat users.
+ * Only these toolkits are shown in the UI and discoverable via chat.
+ * Exact slugs match Composio API naming convention.
+ *
+ * Display names come from @x/shared/composio (single source of truth).
+ */
+
+import { COMPOSIO_DISPLAY_NAMES } from "@x/shared/dist/composio.js";
+
+export { COMPOSIO_DISPLAY_NAMES } from "@x/shared/dist/composio.js";
+
+export type ToolkitCategory = 'communication' | 'productivity' | 'development' | 'crm' | 'social' | 'storage' | 'support';
+
+export interface CuratedToolkit {
+ slug: string;
+ displayName: string;
+ category: ToolkitCategory;
+}
+
+const toolkit = (slug: string, category: ToolkitCategory): CuratedToolkit => ({
+ slug,
+ displayName: COMPOSIO_DISPLAY_NAMES[slug] ?? slug,
+ category,
+});
+
+export const CURATED_TOOLKITS: CuratedToolkit[] = [
+ // Communication
+ toolkit('gmail', 'communication'),
+ toolkit('slack', 'communication'),
+ toolkit('microsoft_outlook', 'communication'),
+ toolkit('microsoft_teams', 'communication'),
+
+ // Productivity
+ toolkit('googlecalendar', 'productivity'),
+ toolkit('googledocs', 'productivity'),
+ toolkit('googlesheets', 'productivity'),
+ toolkit('notion', 'productivity'),
+ toolkit('airtable', 'productivity'),
+ toolkit('calendly', 'productivity'),
+ toolkit('cal', 'productivity'),
+
+ // Storage
+ toolkit('googledrive', 'storage'),
+ toolkit('dropbox', 'storage'),
+ toolkit('onedrive', 'storage'),
+
+ // Development
+ toolkit('github', 'development'),
+ toolkit('linear', 'development'),
+ toolkit('jira', 'development'),
+
+ // Project Management
+ toolkit('asana', 'productivity'),
+ toolkit('trello', 'productivity'),
+
+ // CRM & Sales
+ toolkit('hubspot', 'crm'),
+ toolkit('salesforce', 'crm'),
+
+ // Social
+ toolkit('linkedin', 'social'),
+ toolkit('twitter', 'social'),
+ toolkit('reddit', 'social'),
+
+ // Support
+ toolkit('intercom', 'support'),
+ toolkit('zendesk', 'support'),
+];
+
+/**
+ * Set of curated toolkit slugs for fast lookup.
+ */
+export const CURATED_TOOLKIT_SLUGS = new Set(CURATED_TOOLKITS.map(t => t.slug));
diff --git a/apps/x/packages/shared/src/composio.ts b/apps/x/packages/shared/src/composio.ts
new file mode 100644
index 00000000..f2b3f3cd
--- /dev/null
+++ b/apps/x/packages/shared/src/composio.ts
@@ -0,0 +1,32 @@
+/**
+ * Composio display-name map: toolkit slug → human-readable name.
+ * Single source of truth — used by both core and renderer.
+ */
+export const COMPOSIO_DISPLAY_NAMES: Record = {
+ gmail: 'Gmail',
+ slack: 'Slack',
+ microsoft_outlook: 'Microsoft Outlook',
+ microsoft_teams: 'Microsoft Teams',
+ googlecalendar: 'Google Calendar',
+ googledocs: 'Google Docs',
+ googlesheets: 'Google Sheets',
+ notion: 'Notion',
+ airtable: 'Airtable',
+ calendly: 'Calendly',
+ cal: 'Cal.com',
+ googledrive: 'Google Drive',
+ dropbox: 'Dropbox',
+ onedrive: 'OneDrive',
+ github: 'GitHub',
+ linear: 'Linear',
+ jira: 'Jira',
+ asana: 'Asana',
+ trello: 'Trello',
+ hubspot: 'HubSpot',
+ salesforce: 'Salesforce',
+ linkedin: 'LinkedIn',
+ twitter: 'X',
+ reddit: 'Reddit',
+ intercom: 'Intercom',
+ zendesk: 'Zendesk',
+};
diff --git a/apps/x/packages/shared/src/ipc.ts b/apps/x/packages/shared/src/ipc.ts
index 707709c0..10871606 100644
--- a/apps/x/packages/shared/src/ipc.ts
+++ b/apps/x/packages/shared/src/ipc.ts
@@ -377,18 +377,6 @@ const ipcSchemas = {
toolkits: z.array(z.string()),
}),
},
- 'composio:execute-action': {
- req: z.object({
- actionSlug: z.string(),
- toolkitSlug: z.string(),
- input: z.record(z.string(), z.unknown()),
- }),
- res: z.object({
- data: z.unknown(),
- successful: z.boolean(),
- error: z.string().nullable(),
- }),
- },
'composio:use-composio-for-google': {
req: z.null(),
res: z.object({
@@ -432,58 +420,6 @@ const ipcSchemas = {
totalItems: z.number(),
}),
},
- 'composio:list-toolkit-tools': {
- req: z.object({
- toolkitSlug: z.string(),
- search: z.string().optional(),
- }),
- res: z.object({
- items: z.array(z.object({
- slug: z.string(),
- name: z.string(),
- description: z.string(),
- toolkitSlug: z.string(),
- inputParameters: z.object({
- type: z.string().optional(),
- properties: z.record(z.string(), z.unknown()).optional(),
- required: z.array(z.string()).optional(),
- }).optional(),
- })),
- }),
- },
- 'composio:get-enabled-tools': {
- req: z.null(),
- res: z.object({
- tools: z.record(z.string(), z.object({
- slug: z.string(),
- name: z.string(),
- description: z.string(),
- toolkitSlug: z.string(),
- })),
- }),
- },
- 'composio:enable-tools': {
- req: z.object({
- tools: z.array(z.object({
- slug: z.string(),
- name: z.string(),
- description: z.string(),
- toolkitSlug: z.string(),
- inputParameters: z.object({
- type: z.string().optional(),
- properties: z.record(z.string(), z.unknown()).optional(),
- required: z.array(z.string()).optional(),
- }).optional(),
- })),
- }),
- res: z.object({ success: z.boolean() }),
- },
- 'composio:disable-tools': {
- req: z.object({
- toolSlugs: z.array(z.string()),
- }),
- res: z.object({ success: z.boolean() }),
- },
// Agent schedule channels
'agent-schedule:getConfig': {
req: z.null(),