diff --git a/apps/x/apps/main/src/composio-handler.ts b/apps/x/apps/main/src/composio-handler.ts index e4986416..f4692916 100644 --- a/apps/x/apps/main/src/composio-handler.ts +++ b/apps/x/apps/main/src/composio-handler.ts @@ -3,7 +3,7 @@ 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 { invalidateCopilotInstructionsCache } from '@x/core/dist/application/assistant/instructions.js'; -import { CURATED_TOOLKIT_SLUGS } from '@x/core/dist/composio/curated-toolkits.js'; +import { CURATED_TOOLKIT_SLUGS } from '@x/shared/dist/composio.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'; 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 index cf31a860..434ce9fa 100644 --- 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 @@ -29,14 +29,6 @@ export function ComposioConnectCard({ "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(() => { @@ -47,6 +39,7 @@ export function ComposioConnectCard({ if (event.success) { setConnectionState("connected"); setErrorMessage(null); + if (!alreadyConnected) onConnected?.(toolkitSlug); } else { setConnectionState("error"); setErrorMessage(event.error || "Connection failed"); @@ -54,15 +47,7 @@ export function ComposioConnectCard({ } ); return cleanup; - }, [toolkitSlug]); - - // Fire onConnected callback once when connected - useEffect(() => { - if (connectionState === "connected" && !didFireCallback && !alreadyConnected) { - setDidFireCallback(true); - onConnected?.(toolkitSlug); - } - }, [connectionState, didFireCallback, alreadyConnected, onConnected, toolkitSlug]); + }, [toolkitSlug, alreadyConnected, onConnected]); const handleConnect = useCallback(async () => { setConnectionState("connecting"); @@ -75,7 +60,6 @@ export function ComposioConnectCard({ 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"); @@ -86,12 +70,10 @@ export function ComposioConnectCard({ return (
- {/* Icon */}
- {/* Name & status text */}
@@ -107,13 +89,10 @@ export function ComposioConnectCard({

{errorMessage}

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

- Waiting to connect... -

+

Waiting to connect...

)}
- {/* Action area */} {connectionState === "connected" ? ( ) : connectionState === "connecting" ? ( @@ -124,23 +103,14 @@ export function ComposioConnectCard({ ) : connectionState === "error" ? (
-
) : isToolRunning ? ( ) : ( - diff --git a/apps/x/apps/renderer/src/lib/chat-conversation.ts b/apps/x/apps/renderer/src/lib/chat-conversation.ts index cf3a8550..863b23ed 100644 --- a/apps/x/apps/renderer/src/lib/chat-conversation.ts +++ b/apps/x/apps/renderer/src/lib/chat-conversation.ts @@ -261,8 +261,6 @@ export type ComposioConnectCardData = { 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 @@ -275,7 +273,7 @@ export const getComposioConnectCardData = (tool: ToolCall): ComposioConnectCardD return { toolkitSlug, - toolkitDisplayName: composioDisplayNames[toolkitSlug] || toolkitSlug, + toolkitDisplayName: COMPOSIO_DISPLAY_NAMES[toolkitSlug] || toolkitSlug, alreadyConnected, } } @@ -293,7 +291,7 @@ export const getComposioActionCardData = (tool: ToolCall): ComposioActionCardDat 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 toolkit = toolkitSlug ? COMPOSIO_DISPLAY_NAMES[toolkitSlug] || toolkitSlug : null const count = (result?.resultCount as number) ?? null let label = `Searching for "${query}"` @@ -308,7 +306,7 @@ export const getComposioActionCardData = (tool: ToolCall): ComposioActionCardDat if (tool.name === 'composio-execute-tool') { const toolSlug = (input?.toolSlug as string) || '' const toolkitSlug = (input?.toolkitSlug as string) || '' - const toolkit = composioDisplayNames[toolkitSlug] || toolkitSlug + const toolkit = COMPOSIO_DISPLAY_NAMES[toolkitSlug] || toolkitSlug const successful = result?.successful as boolean | undefined // Make the tool slug human-readable: GITHUB_ISSUES_LIST_FOR_REPO → "Issues list for repo" diff --git a/apps/x/packages/core/src/application/assistant/instructions.ts b/apps/x/packages/core/src/application/assistant/instructions.ts index ff15dbfc..175aceb4 100644 --- a/apps/x/packages/core/src/application/assistant/instructions.ts +++ b/apps/x/packages/core/src/application/assistant/instructions.ts @@ -2,7 +2,7 @@ import { skillCatalog } from "./skills/index.js"; // eslint-disable-line @typesc import { getRuntimeContext, getRuntimeContextPrompt } from "./runtime-context.js"; import { composioAccountsRepo } from "../../composio/repo.js"; import { isConfigured as isComposioConfigured } from "../../composio/client.js"; -import { CURATED_TOOLKITS } from "../../composio/curated-toolkits.js"; +import { CURATED_TOOLKITS } from "@x/shared/dist/composio.js"; const runtimeContextPrompt = getRuntimeContextPrompt(getRuntimeContext()); @@ -14,44 +14,36 @@ async function getComposioToolsPrompt(): Promise { if (!(await isComposioConfigured())) return ''; const connectedToolkits = composioAccountsRepo.getConnectedToolkits(); + const connectedSection = connectedToolkits.length > 0 + ? `**Currently connected:** ${connectedToolkits.map(slug => CURATED_TOOLKITS.find(t => t.slug === slug)?.displayName ?? slug).join(', ')}` + : `**No services connected yet.** Use \`composio-list-toolkits\` to show available integrations, or \`composio-connect-toolkit\` to help the user connect one.`; - let prompt = `\n## Composio Integrations\n\n`; - prompt += `You can connect to external services (Gmail, Slack, GitHub, Notion, etc.) via Composio.\n\n`; + return ` +## Composio Integrations - 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`; - } +You can connect to external services (Gmail, Slack, GitHub, Notion, etc.) via Composio. - 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`; +${connectedSection} - 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`; +**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. - 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`; +**Discovery & Execution Flow:** +1. When the user asks to interact with a service (e.g., "get my LinkedIn profile", "check my email", "list GitHub issues"): + a. Check if the service is connected (via \`composio-list-toolkits\` or the connected list above) + b. If NOT connected, call \`composio-connect-toolkit\` immediately — do NOT ask for confirmation + c. If connected, proceed to search and execute +2. Use \`composio-search-tools\` with SHORT keyword queries (e.g., "list issues", "send email", "get profile") — avoid long sentences. +3. Read the \`inputSchema\` from search results carefully — note which fields are in \`required\`. +4. Call \`composio-execute-tool\` with the tool slug, toolkit slug, AND all required \`arguments\`. For tools with empty \`properties: {}\`, pass \`arguments: {}\`. - 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; +**Important:** +- Use short keyword search queries, NOT full sentences (good: "list issues", bad: "get all open issues for a GitHub repository") +- ALWAYS pass required arguments to composio-execute-tool — read the inputSchema from search results +- **If a tool call fails, fix the arguments and retry IMMEDIATELY — do NOT stop and narrate the error to the user.** +- **Multi-part requests:** When the user asks to "connect X and then do Y", complete BOTH parts. If part 1 (connect) is already done, proceed directly to part 2. +- Confirm with the user before executing tools that send messages, create items, or modify data (NOT for read-only queries or connecting) +- Connecting a toolkit is always safe — just do it when needed, don't ask permission +`; } export const CopilotInstructions = `You are Rowboat Copilot - an AI assistant for everyday work. You help users with anything they want. For instance, drafting emails, prepping for meetings, tracking projects, or answering questions - with memory that compounds from their emails, calendar, and notes. Everything runs locally on the user's machine. The nerdy coworker who remembers everything. @@ -220,21 +212,7 @@ Always consult this catalog first so you load the right skills before taking act ## Tool Priority: Composio First, Then MCP -**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. - -**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 - -**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! +For third-party services (GitHub, Gmail, Slack, etc.), use \`composio-*\` builtin tools first. Only fall back to MCP tools for capabilities Composio doesn't cover (web search, file scraping, audio). See the dynamic "Composio Integrations" section below for full details. ## Execution Reminders - Explore existing files and structure before creating new assets. 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 9d07f7ad..cdebbfdd 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 @@ -22,7 +22,7 @@ export const skill = String.raw` | User Request | Check For | Likely Tool | |--------------|-----------|-------------| -| "Search the web/internet" | firecrawl, composio, fetch | \`firecrawl_search\`, \`COMPOSIO_SEARCH_WEB\` | +| "Search the web/internet" | firecrawl, fetch | \`firecrawl_search\` | | "Scrape this website" | firecrawl | \`firecrawl_scrape\` | | "Read/write files" | filesystem | \`read_file\`, \`write_file\` | | "Get current time/date" | time | \`get_current_time\` | @@ -244,7 +244,7 @@ The schema tells you: **Example schema from listMcpTools:** \`\`\`json { - "name": "COMPOSIO_SEARCH_WEB", + "name": "firecrawl_search", "inputSchema": { "type": "object", "properties": { @@ -265,10 +265,10 @@ The schema tells you: **Correct executeMcpTool call:** \`\`\`json { - "serverName": "composio", - "toolName": "COMPOSIO_SEARCH_WEB", + "serverName": "firecrawl", + "toolName": "firecrawl_search", "arguments": { - "query": "elon musk latest news" + "query": "latest AI news" } } \`\`\` @@ -276,18 +276,18 @@ The schema tells you: **WRONG - Missing arguments:** \`\`\`json { - "serverName": "composio", - "toolName": "COMPOSIO_SEARCH_WEB" + "serverName": "firecrawl", + "toolName": "firecrawl_search" } \`\`\` **WRONG - Wrong parameter name:** \`\`\`json { - "serverName": "composio", - "toolName": "COMPOSIO_SEARCH_WEB", + "serverName": "firecrawl", + "toolName": "firecrawl_search", "arguments": { - "search": "elon musk" // Wrong! Should be "query" + "search": "latest AI news" // Wrong! Should be "query" } } \`\`\` 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 945ab4aa..3f82afdd 100644 --- a/apps/x/packages/core/src/application/lib/builtin-tools.ts +++ b/apps/x/packages/core/src/application/lib/builtin-tools.ts @@ -14,7 +14,7 @@ import { IAgentsRepo } from "../../agents/repo.js"; import { WorkDir } from "../../config/config.js"; import { composioAccountsRepo } from "../../composio/repo.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 { CURATED_TOOLKITS, CURATED_TOOLKIT_SLUGS } from "@x/shared/dist/composio.js"; import { getConnectionInitiator } from "../../composio/connection-bridge.js"; import type { ToolContext } from "./exec-tool.js"; import { generateText } from "ai"; diff --git a/apps/x/packages/core/src/composio/client.ts b/apps/x/packages/core/src/composio/client.ts index 70b51050..e16573d6 100644 --- a/apps/x/packages/core/src/composio/client.ts +++ b/apps/x/packages/core/src/composio/client.ts @@ -72,7 +72,7 @@ function loadConfig(): ComposioConfig { /** * Save Composio configuration */ -export function saveConfig(config: ComposioConfig): void { +function saveConfig(config: ComposioConfig): void { const dir = path.dirname(CONFIG_FILE); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); @@ -254,15 +254,6 @@ export async function createAuthConfig( }); } -/** - * Delete an auth config - */ -export async function deleteAuthConfig(authConfigId: string): Promise> { - return composioApiCall(ZDeleteOperationResponse, `/auth_configs/${authConfigId}`, {}, { - method: 'DELETE', - }); -} - /** * Create a connected account */ @@ -364,23 +355,6 @@ export async function searchTools( return { items }; } -/** - * List available tools for a toolkit - */ -export async function listToolkitTools( - toolkitSlug: string, - searchQuery: string | null = null, -): Promise>>> { - const params: Record = { - toolkit_slug: toolkitSlug, - limit: '200', - }; - if (searchQuery) { - params.search = searchQuery; - } - return composioApiCall(ZListResponse(ZTool), "/tools", params); -} - /** * Execute a tool action */ diff --git a/apps/x/packages/core/src/composio/curated-toolkits.ts b/apps/x/packages/core/src/composio/curated-toolkits.ts deleted file mode 100644 index b3d4deed..00000000 --- a/apps/x/packages/core/src/composio/curated-toolkits.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Re-exports from @x/shared/composio — kept for backward compatibility - * so existing core imports don't need to change paths. - */ -export { - type ToolkitCategory, - type CuratedToolkit, - CURATED_TOOLKITS, - CURATED_TOOLKIT_SLUGS, - COMPOSIO_DISPLAY_NAMES, -} from "@x/shared/dist/composio.js"; diff --git a/apps/x/packages/core/src/composio/types.ts b/apps/x/packages/core/src/composio/types.ts index 8bd2418a..968c9abd 100644 --- a/apps/x/packages/core/src/composio/types.ts +++ b/apps/x/packages/core/src/composio/types.ts @@ -229,12 +229,5 @@ export const ZLocalConnectedAccount = z.object({ lastUpdatedAt: z.string(), }); -export type AuthScheme = z.infer; -export type ConnectedAccountStatus = z.infer; -export type Toolkit = z.infer; -export type Tool = z.infer; -export type AuthConfig = z.infer; -export type ConnectedAccount = z.infer; export type LocalConnectedAccount = z.infer; -export type ExecuteActionRequest = z.infer; -export type ExecuteActionResponse = z.infer; +export type ConnectedAccountStatus = z.infer; diff --git a/apps/x/packages/shared/src/ipc.ts b/apps/x/packages/shared/src/ipc.ts index 10871606..465ef09f 100644 --- a/apps/x/packages/shared/src/ipc.ts +++ b/apps/x/packages/shared/src/ipc.ts @@ -399,9 +399,7 @@ const ipcSchemas = { }, // Composio Tools Library channels 'composio:list-toolkits': { - req: z.object({ - cursor: z.string().optional(), - }), + req: z.object({}), res: z.object({ items: z.array(z.object({ slug: z.string(),