From 7f8d2e64afd7e8a3ca393ad395b103dbea41a065 Mon Sep 17 00:00:00 2001 From: tusharmagar Date: Thu, 2 Apr 2026 15:35:31 +0530 Subject: [PATCH] Refactor Composio integration handling and improve UI components - Updated composio-handler.ts to invalidate the copilot instructions cache upon connection and disconnection. - Removed unused functions related to tool management in composio-handler.ts. - Enhanced IPC handlers in ipc.ts to streamline Composio connection processes. - Introduced ComposioConnectCard in the renderer to display connection status and handle events. - Refactored tool rendering in App.tsx and chat-sidebar.tsx to utilize new tabbed content for parameters and results. - Improved Composio tools prompt generation in instructions.ts to clarify integration usage and discovery flow. - Cleaned up unused code and improved overall structure for better maintainability. --- apps/x/apps/main/src/composio-handler.ts | 137 +------ apps/x/apps/main/src/ipc.ts | 19 +- apps/x/apps/renderer/src/App.tsx | 35 +- .../ai-elements/composio-connect-card.tsx | 150 +++++++ .../ai-elements/permission-request.tsx | 10 +- .../src/components/ai-elements/tool.tsx | 122 +++--- .../renderer/src/components/chat-sidebar.tsx | 27 +- .../src/components/settings-dialog.tsx | 387 ++---------------- .../renderer/src/lib/chat-conversation.ts | 89 ++++ apps/x/packages/core/src/agents/runtime.ts | 3 +- .../src/application/assistant/instructions.ts | 96 +++-- .../assistant/skills/mcp-integration/skill.ts | 10 +- .../core/src/application/lib/builtin-tools.ts | 267 +++++++----- apps/x/packages/core/src/composio/client.ts | 121 +++--- .../core/src/composio/connection-bridge.ts | 33 ++ .../core/src/composio/curated-toolkits.ts | 74 ++++ apps/x/packages/shared/src/composio.ts | 32 ++ apps/x/packages/shared/src/ipc.ts | 64 --- 18 files changed, 864 insertions(+), 812 deletions(-) create mode 100644 apps/x/apps/renderer/src/components/ai-elements/composio-connect-card.tsx create mode 100644 apps/x/packages/core/src/composio/connection-bridge.ts create mode 100644 apps/x/packages/core/src/composio/curated-toolkits.ts create mode 100644 apps/x/packages/shared/src/composio.ts diff --git a/apps/x/apps/main/src/composio-handler.ts b/apps/x/apps/main/src/composio-handler.ts index aac7c453..c065dd2b 100644 --- a/apps/x/apps/main/src/composio-handler.ts +++ b/apps/x/apps/main/src/composio-handler.ts @@ -2,11 +2,9 @@ import { shell, BrowserWindow } from 'electron'; import { createAuthServer } from './auth-server.js'; import * as composioClient from '@x/core/dist/composio/client.js'; import { composioAccountsRepo } from '@x/core/dist/composio/repo.js'; -import { composioEnabledToolsRepo } from '@x/core/dist/composio/enabled-tools-repo.js'; -import type { EnabledTool } from '@x/core/dist/composio/enabled-tools-repo.js'; -import type { LocalConnectedAccount, ZExecuteActionResponse } from '@x/core/dist/composio/types.js'; -import { refreshComposioTools } from '@x/core/dist/application/lib/builtin-tools.js'; -import { z } from 'zod'; +import { invalidateCopilotInstructionsCache } from '@x/core/dist/application/assistant/instructions.js'; +import { CURATED_TOOLKIT_SLUGS } from '@x/core/dist/composio/curated-toolkits.js'; +import type { LocalConnectedAccount } from '@x/core/dist/composio/types.js'; import { triggerSync as triggerGmailSync } from '@x/core/dist/knowledge/sync_gmail.js'; import { triggerSync as triggerCalendarSync } from '@x/core/dist/knowledge/sync_calendar.js'; @@ -163,6 +161,8 @@ export async function initiateConnection(toolkitSlug: string): Promise<{ composioAccountsRepo.updateAccountStatus(toolkitSlug, accountStatus.status); if (accountStatus.status === 'ACTIVE') { + // Invalidate instructions cache so the copilot knows about the new connection + invalidateCopilotInstructionsCache(); emitComposioEvent({ toolkitSlug, success: true }); if (toolkitSlug === 'gmail') { triggerGmailSync(); @@ -273,23 +273,16 @@ export async function disconnect(toolkitSlug: string): Promise<{ success: boolea try { const account = composioAccountsRepo.getAccount(toolkitSlug); if (account) { - // Delete from Composio await composioClient.deleteConnectedAccount(account.id); - // Delete local record - composioAccountsRepo.deleteAccount(toolkitSlug); } - // Clean up enabled tools for this toolkit - composioEnabledToolsRepo.disableAllForToolkit(toolkitSlug); - refreshComposioTools(); - return { success: true }; } catch (error) { console.error('[Composio] Disconnect failed:', error); - // Still delete local record even if API call fails + } finally { + // Always clean up local state, even if the API call fails composioAccountsRepo.deleteAccount(toolkitSlug); - composioEnabledToolsRepo.disableAllForToolkit(toolkitSlug); - refreshComposioTools(); - return { success: true }; + invalidateCopilotInstructionsCache(); } + return { success: true }; } /** @@ -314,42 +307,7 @@ export async function useComposioForGoogleCalendar(): Promise<{ enabled: boolean } /** - * Execute a Composio action - */ -export async function executeAction( - actionSlug: string, - toolkitSlug: string, - input: Record -): Promise> { - try { - const account = composioAccountsRepo.getAccount(toolkitSlug); - if (!account || account.status !== 'ACTIVE') { - return { - data: null, - successful: false, - error: `Toolkit ${toolkitSlug} is not connected`, - }; - } - - const result = await composioClient.executeAction(actionSlug, { - connected_account_id: account.id, - user_id: 'rowboat-user', - version: 'latest', - arguments: input, - }); - return result; - } catch (error) { - console.error('[Composio] Action execution failed:', error); - return { - successful: false, - data: null, - error: error instanceof Error ? error.message : 'Unknown error', - }; - } -} - -/** - * List available Composio toolkits + * List available Composio toolkits — filtered to curated list only */ export async function listToolkits(cursor?: string): Promise<{ items: Array<{ @@ -363,79 +321,12 @@ export async function listToolkits(cursor?: string): Promise<{ nextCursor: string | null; totalItems: number; }> { + // Fetch all toolkits and filter to curated list const result = await composioClient.listToolkits(cursor || null); + const filtered = result.items.filter(item => CURATED_TOOLKIT_SLUGS.has(item.slug)); return { - items: result.items, + items: filtered, nextCursor: result.next_cursor, - totalItems: result.total_items, + totalItems: filtered.length, }; } - -/** - * List tools for a toolkit with full details - */ -export async function listToolkitToolsDetailed(toolkitSlug: string, search?: string): Promise<{ - items: Array<{ - slug: string; - name: string; - description: string; - toolkitSlug: string; - inputParameters?: { type?: string; properties?: Record; required?: string[] }; - }>; -}> { - return composioClient.listToolkitToolsDetailed(toolkitSlug, search || null); -} - -/** - * Get all enabled tools - */ -export function getEnabledTools(): { - tools: Record; -} { - const all = composioEnabledToolsRepo.getAll(); - const tools: Record = {}; - for (const [slug, tool] of Object.entries(all)) { - tools[slug] = { - slug: tool.slug, - name: tool.name, - description: tool.description, - toolkitSlug: tool.toolkitSlug, - }; - } - return { tools }; -} - -/** - * Enable specific tools from a toolkit - */ -export function enableTools(tools: Array<{ - slug: string; - name: string; - description: string; - toolkitSlug: string; - inputParameters?: { type?: string; properties?: Record; required?: string[] }; -}>): { success: boolean } { - const enabledTools: EnabledTool[] = tools.map(t => ({ - slug: t.slug, - name: t.name, - description: t.description, - toolkitSlug: t.toolkitSlug, - inputParameters: { - type: 'object' as const, - properties: t.inputParameters?.properties ?? {}, - required: t.inputParameters?.required, - }, - })); - composioEnabledToolsRepo.enableBatch(enabledTools); - refreshComposioTools(); - return { success: true }; -} - -/** - * Disable specific tools - */ -export function disableTools(toolSlugs: string[]): { success: boolean } { - composioEnabledToolsRepo.disableBatch(toolSlugs); - refreshComposioTools(); - return { success: true }; -} diff --git a/apps/x/apps/main/src/ipc.ts b/apps/x/apps/main/src/ipc.ts index aa5222f8..f0fd0b7c 100644 --- a/apps/x/apps/main/src/ipc.ts +++ b/apps/x/apps/main/src/ipc.ts @@ -34,6 +34,7 @@ import { triggerSync as triggerGranolaSync } from '@x/core/dist/knowledge/granol import { ISlackConfigRepo } from '@x/core/dist/slack/repo.js'; import { isOnboardingComplete, markOnboardingComplete } from '@x/core/dist/config/note_creation_config.js'; import * as composioHandler from './composio-handler.js'; +import { setConnectionInitiator } from '@x/core/dist/composio/connection-bridge.js'; import { IAgentScheduleRepo } from '@x/core/dist/agent-schedule/repo.js'; import { IAgentScheduleStateRepo } from '@x/core/dist/agent-schedule/state-repo.js'; import { triggerRun as triggerAgentScheduleRun } from '@x/core/dist/agent-schedule/runner.js'; @@ -376,6 +377,9 @@ export function setupIpcHandlers() { // Forward knowledge commit events to renderer for panel refresh versionHistory.onCommit(() => emitKnowledgeCommitEvent()); + // Wire the connection bridge so builtin tools (in core) can trigger OAuth (in main) + setConnectionInitiator(composioHandler.initiateConnection); + registerIpcHandlers({ 'app:getVersions': async () => { // args is null for this channel (no request payload) @@ -559,25 +563,10 @@ export function setupIpcHandlers() { 'composio:list-connected': async () => { return composioHandler.listConnected(); }, - 'composio:execute-action': async (_event, args) => { - return composioHandler.executeAction(args.actionSlug, args.toolkitSlug, args.input); - }, // Composio Tools Library handlers 'composio:list-toolkits': async (_event, args) => { return composioHandler.listToolkits(args.cursor); }, - 'composio:list-toolkit-tools': async (_event, args) => { - return composioHandler.listToolkitToolsDetailed(args.toolkitSlug, args.search); - }, - 'composio:get-enabled-tools': async () => { - return composioHandler.getEnabledTools(); - }, - 'composio:enable-tools': async (_event, args) => { - return composioHandler.enableTools(args.tools); - }, - 'composio:disable-tools': async (_event, args) => { - return composioHandler.disableTools(args.toolSlugs); - }, 'composio:use-composio-for-google': async () => { return composioHandler.useComposioForGoogle(); }, diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index 8292a196..0bdc08a8 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -34,9 +34,10 @@ import { import { Shimmer } from '@/components/ai-elements/shimmer'; import { useSmoothedText } from './hooks/useSmoothedText'; -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 { AppActionCard } from '@/components/ai-elements/app-action-card'; +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'; @@ -67,6 +68,8 @@ import { createEmptyChatTabViewState, getWebSearchCardData, getAppActionCardData, + getComposioConnectCardData, + getComposioActionCardData, inferRunTitleFromMessage, isChatMessage, isErrorMessage, @@ -76,6 +79,7 @@ import { parseAttachedFiles, toToolState, } from '@/lib/chat-conversation' +import { COMPOSIO_DISPLAY_NAMES as composioDisplayNames } from '@x/shared/src/composio.js' import { AgentScheduleConfig } from '@x/shared/dist/agent-schedule.js' import { AgentScheduleState } from '@x/shared/dist/agent-schedule-state.js' import { toast } from "sonner" @@ -2242,6 +2246,12 @@ function App() { } handlePromptSubmitRef.current = handlePromptSubmit + const handleComposioConnected = useCallback((toolkitSlug: string) => { + // Auto-send a continuation message when a Composio toolkit connects + const name = composioDisplayNames[toolkitSlug] || toolkitSlug + handlePromptSubmitRef.current?.({ text: `${name} connected successfully.`, files: [] }) + }, []) + const handleStop = useCallback(async () => { if (!runId) return const now = Date.now() @@ -3826,6 +3836,21 @@ function App() { /> ) } + 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) @@ -3836,15 +3861,12 @@ function App() { onOpenChange={(open) => setToolOpenForTab(tabId, item.id, open)} > - - {output !== null ? ( - - ) : null} + ) @@ -4470,6 +4492,7 @@ function App() { ttsMode={ttsMode} onToggleTts={handleToggleTts} onTtsModeChange={handleTtsModeChange} + onComposioConnected={handleComposioConnected} /> )} {/* Rendered last so its no-drag region paints over the sidebar drag region */} diff --git a/apps/x/apps/renderer/src/components/ai-elements/composio-connect-card.tsx b/apps/x/apps/renderer/src/components/ai-elements/composio-connect-card.tsx new file mode 100644 index 00000000..cf31a860 --- /dev/null +++ b/apps/x/apps/renderer/src/components/ai-elements/composio-connect-card.tsx @@ -0,0 +1,150 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import { + CheckCircleIcon, + Link2Icon, + LoaderIcon, + WrenchIcon, + XCircleIcon, +} from "lucide-react"; +import { Button } from "@/components/ui/button"; + +interface ComposioConnectCardProps { + toolkitSlug: string; + toolkitDisplayName: string; + status: "pending" | "running" | "completed" | "error"; + alreadyConnected?: boolean; + onConnected?: (toolkitSlug: string) => void; +} + +export function ComposioConnectCard({ + toolkitSlug, + toolkitDisplayName, + status, + alreadyConnected, + onConnected, +}: ComposioConnectCardProps) { + const [connectionState, setConnectionState] = useState< + "idle" | "connecting" | "connected" | "error" + >(alreadyConnected ? "connected" : "idle"); + const [errorMessage, setErrorMessage] = useState(null); + const [didFireCallback, setDidFireCallback] = useState(false); + + // If the tool result already says connected, reflect that + useEffect(() => { + if (alreadyConnected) { + setConnectionState("connected"); + } + }, [alreadyConnected]); + + // Listen for composio:didConnect events + useEffect(() => { + const cleanup = window.ipc.on( + "composio:didConnect", + (event: { toolkitSlug: string; success: boolean; error?: string }) => { + if (event.toolkitSlug !== toolkitSlug) return; + if (event.success) { + setConnectionState("connected"); + setErrorMessage(null); + } else { + setConnectionState("error"); + setErrorMessage(event.error || "Connection failed"); + } + } + ); + return cleanup; + }, [toolkitSlug]); + + // Fire onConnected callback once when connected + useEffect(() => { + if (connectionState === "connected" && !didFireCallback && !alreadyConnected) { + setDidFireCallback(true); + onConnected?.(toolkitSlug); + } + }, [connectionState, didFireCallback, alreadyConnected, onConnected, toolkitSlug]); + + const handleConnect = useCallback(async () => { + setConnectionState("connecting"); + setErrorMessage(null); + try { + const result = await window.ipc.invoke("composio:initiate-connection", { + toolkitSlug, + }); + if (!result.success) { + setConnectionState("error"); + setErrorMessage(result.error || "Failed to initiate connection"); + } + // Success will be handled by composio:didConnect event + } catch { + setConnectionState("error"); + setErrorMessage("Failed to initiate connection"); + } + }, [toolkitSlug]); + + const isToolRunning = status === "pending" || status === "running"; + + return ( +
+ {/* Icon */} +
+ +
+ + {/* Name & status text */} +
+
+ + {toolkitDisplayName || toolkitSlug} + + {connectionState === "connected" && ( + + Connected + + )} +
+ {connectionState === "error" && errorMessage && ( +

{errorMessage}

+ )} + {connectionState === "idle" && isToolRunning && ( +

+ Waiting to connect... +

+ )} +
+ + {/* Action area */} + {connectionState === "connected" ? ( + + ) : connectionState === "connecting" ? ( + + ) : connectionState === "error" ? ( +
+ + +
+ ) : isToolRunning ? ( + + ) : ( + + )} +
+ ); +} diff --git a/apps/x/apps/renderer/src/components/ai-elements/permission-request.tsx b/apps/x/apps/renderer/src/components/ai-elements/permission-request.tsx index e9cef6dc..2a94a097 100644 --- a/apps/x/apps/renderer/src/components/ai-elements/permission-request.tsx +++ b/apps/x/apps/renderer/src/components/ai-elements/permission-request.tsx @@ -36,7 +36,7 @@ export const PermissionRequest = ({ ...props }: PermissionRequestProps) => { // Extract command from arguments if it's executeCommand - const command = toolCall.toolName === "executeCommand" + const command = toolCall.toolName === "executeCommand" ? (typeof toolCall.arguments === "object" && toolCall.arguments !== null && "command" in toolCall.arguments ? String(toolCall.arguments.command) : JSON.stringify(toolCall.arguments)) @@ -80,12 +80,12 @@ export const PermissionRequest = ({

{isResponded && ( - diff --git a/apps/x/apps/renderer/src/components/ai-elements/tool.tsx b/apps/x/apps/renderer/src/components/ai-elements/tool.tsx index d9453aa1..18af8b0e 100644 --- a/apps/x/apps/renderer/src/components/ai-elements/tool.tsx +++ b/apps/x/apps/renderer/src/components/ai-elements/tool.tsx @@ -16,8 +16,8 @@ import { WrenchIcon, XCircleIcon, } from "lucide-react"; -import type { ComponentProps, ReactNode } from "react"; -import { isValidElement } from "react"; +import { type ComponentProps, type ReactNode, isValidElement, useState } from "react"; + const formatToolValue = (value: unknown) => { if (typeof value === "string") return value; try { @@ -37,7 +37,7 @@ const ToolCode = ({ }) => (
@@ -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 */} - ) : ( - - )} - - - {/* Expanded content */} - {isExpanded && ( -
- {/* Connection controls */} -
- {isConnected ? ( - + + <>Connect )} - - {/* Enable/Disable all (only if connected and tools loaded) */} - {isConnected && tools.length > 0 && ( - - )} -
- - {/* Tools section (only if connected) */} - {isConnected && ( -
- {/* Tool search input — shown when toolkit has many tools */} - {!isLoadingTools && tools.length > 5 && ( -
- - handleToolSearch(toolkit.slug, e.target.value)} - placeholder={`Search ${toolkit.meta.tools_count} tools...`} - className="w-full h-7 pl-7 pr-7 text-xs rounded-md border border-border bg-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring" - /> - {toolSearchLoading && ( - - )} - {!toolSearchLoading && toolSearchQuery && ( - - )} -
- )} - - {/* Search results summary */} - {isSearching && !toolSearchLoading && toolSearchResults !== null && ( -
- {toolSearchResults.length} - {toolSearchResults.length === 1 ? 'tool' : 'tools'} matching “{toolSearchQuery}” -
- )} - - {/* Tool list */} - {isLoadingTools ? ( -
- - Loading tools... -
- ) : displayTools.length === 0 ? ( -
-

- {isSearching - ? `No tools found for "${toolSearchQuery}"` - : "No tools found" - } -

- {isSearching && ( - - )} -
- ) : ( -
- {displayTools.map((tool) => { - const isEnabled = enabledToolSlugs.has(tool.slug) - return ( - - ) - })} -
- )} -
- )} - - {/* Not connected hint */} - {!isConnected && ( -

- Connect this toolkit to browse and enable its tools. -

- )} -
- )} + + )} +
) })} 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(),