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.
This commit is contained in:
tusharmagar 2026-04-02 15:35:31 +05:30
parent abf6901cc9
commit 7f8d2e64af
18 changed files with 864 additions and 812 deletions

View file

@ -370,8 +370,7 @@ function formatLlmStreamError(rawError: unknown): string {
export async function loadAgent(id: string): Promise<z.infer<typeof Agent>> {
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<string, z.infer<typeof ToolAttachment>> = {};
for (const name of Object.keys(BuiltinTools)) {
tools[name] = { type: "builtin", name };

View file

@ -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<string> {
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<string, Array<{ slug: string; name: string; description: string }>> = {};
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;

View file

@ -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\`.

View file

@ -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<typeof BuiltinToolsSchema> = {
};
},
},
// ========================================================================
// 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<string, unknown> }) => {
// 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<string, unknown>) => {
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();

View file

@ -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<string, unknown>; required?: string[] } }> }> {
const params: Record<string, string> = {
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<string, unknown>; required?: string[] } }> }> {
const params: Record<string, string> = {
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
*/

View file

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

View file

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