Refactor Composio integration and improve component structure

- Updated imports in composio-handler.ts and various components to utilize shared/composio.js for consistency.
- Simplified ComposioConnectCard by removing unnecessary state management and improving event handling.
- Enhanced chat-conversation.ts to directly reference COMPOSIO_DISPLAY_NAMES from shared/composio.js.
- Cleaned up unused functions and types in client.ts and types.ts for better maintainability.
- Removed deprecated curated-toolkits.ts file to streamline the codebase.
This commit is contained in:
tusharmagar 2026-04-02 16:15:15 +05:30
parent e0e7945bd9
commit 587778cb07
10 changed files with 49 additions and 149 deletions

View file

@ -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';

View file

@ -29,14 +29,6 @@ export function ComposioConnectCard({
"idle" | "connecting" | "connected" | "error"
>(alreadyConnected ? "connected" : "idle");
const [errorMessage, setErrorMessage] = useState<string | null>(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 (
<div className="not-prose mb-4 flex items-center gap-3 rounded-lg border px-3 py-2.5">
{/* Icon */}
<div className="size-7 rounded bg-muted flex items-center justify-center flex-shrink-0">
<WrenchIcon className="size-3.5 text-muted-foreground" />
</div>
{/* Name & status text */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1.5">
<span className="text-sm font-medium truncate">
@ -107,13 +89,10 @@ export function ComposioConnectCard({
<p className="text-xs text-destructive truncate">{errorMessage}</p>
)}
{connectionState === "idle" && isToolRunning && (
<p className="text-xs text-muted-foreground">
Waiting to connect...
</p>
<p className="text-xs text-muted-foreground">Waiting to connect...</p>
)}
</div>
{/* Action area */}
{connectionState === "connected" ? (
<CheckCircleIcon className="size-4 text-green-600 flex-shrink-0" />
) : connectionState === "connecting" ? (
@ -124,23 +103,14 @@ export function ComposioConnectCard({
) : connectionState === "error" ? (
<div className="flex items-center gap-1.5 flex-shrink-0">
<XCircleIcon className="size-3.5 text-destructive" />
<Button
size="sm"
variant="outline"
onClick={handleConnect}
className="text-xs h-7"
>
<Button size="sm" variant="outline" onClick={handleConnect} className="text-xs h-7">
Retry
</Button>
</div>
) : isToolRunning ? (
<LoaderIcon className="size-3.5 animate-spin text-muted-foreground flex-shrink-0" />
) : (
<Button
size="sm"
onClick={handleConnect}
className="text-xs h-7 flex-shrink-0"
>
<Button size="sm" onClick={handleConnect} className="text-xs h-7 flex-shrink-0">
<Link2Icon className="size-3 mr-1" />
Connect
</Button>

View file

@ -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"

View file

@ -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<string> {
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.

View file

@ -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"
}
}
\`\`\`

View file

@ -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";

View file

@ -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<z.infer<typeof ZDeleteOperationResponse>> {
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<z.infer<ReturnType<typeof ZListResponse<typeof ZTool>>>> {
const params: Record<string, string> = {
toolkit_slug: toolkitSlug,
limit: '200',
};
if (searchQuery) {
params.search = searchQuery;
}
return composioApiCall(ZListResponse(ZTool), "/tools", params);
}
/**
* Execute a tool action
*/

View file

@ -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";

View file

@ -229,12 +229,5 @@ export const ZLocalConnectedAccount = z.object({
lastUpdatedAt: z.string(),
});
export type AuthScheme = z.infer<typeof ZAuthScheme>;
export type ConnectedAccountStatus = z.infer<typeof ZConnectedAccountStatus>;
export type Toolkit = z.infer<typeof ZToolkit>;
export type Tool = z.infer<typeof ZTool>;
export type AuthConfig = z.infer<typeof ZAuthConfig>;
export type ConnectedAccount = z.infer<typeof ZConnectedAccount>;
export type LocalConnectedAccount = z.infer<typeof ZLocalConnectedAccount>;
export type ExecuteActionRequest = z.infer<typeof ZExecuteActionRequest>;
export type ExecuteActionResponse = z.infer<typeof ZExecuteActionResponse>;
export type ConnectedAccountStatus = z.infer<typeof ZConnectedAccountStatus>;

View file

@ -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(),