mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-28 18:06:30 +02:00
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:
parent
abf6901cc9
commit
7f8d2e64af
18 changed files with 864 additions and 812 deletions
|
|
@ -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<string, unknown>
|
||||
): Promise<z.infer<typeof ZExecuteActionResponse>> {
|
||||
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<string, unknown>; required?: string[] };
|
||||
}>;
|
||||
}> {
|
||||
return composioClient.listToolkitToolsDetailed(toolkitSlug, search || null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all enabled tools
|
||||
*/
|
||||
export function getEnabledTools(): {
|
||||
tools: Record<string, { slug: string; name: string; description: string; toolkitSlug: string }>;
|
||||
} {
|
||||
const all = composioEnabledToolsRepo.getAll();
|
||||
const tools: Record<string, { slug: string; name: string; description: string; toolkitSlug: string }> = {};
|
||||
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<string, unknown>; 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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<ComposioConnectCard
|
||||
key={item.id}
|
||||
toolkitSlug={composioConnectData.toolkitSlug}
|
||||
toolkitDisplayName={composioConnectData.toolkitDisplayName}
|
||||
status={item.status}
|
||||
alreadyConnected={composioConnectData.alreadyConnected}
|
||||
onConnected={handleComposioConnected}
|
||||
/>
|
||||
)
|
||||
}
|
||||
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)}
|
||||
>
|
||||
<ToolHeader
|
||||
title={item.name}
|
||||
title={toolTitle}
|
||||
type={`tool-${item.name}`}
|
||||
state={toToolState(item.status)}
|
||||
/>
|
||||
<ToolContent>
|
||||
<ToolInput input={input} />
|
||||
{output !== null ? (
|
||||
<ToolOutput output={output} errorText={errorText} />
|
||||
) : null}
|
||||
<ToolTabbedContent input={input} output={output} errorText={errorText} />
|
||||
</ToolContent>
|
||||
</Tool>
|
||||
)
|
||||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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<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(() => {
|
||||
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 (
|
||||
<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">
|
||||
{toolkitDisplayName || toolkitSlug}
|
||||
</span>
|
||||
{connectionState === "connected" && (
|
||||
<span className="rounded-full bg-green-500/10 px-1.5 py-0.5 text-[10px] font-medium leading-none text-green-600">
|
||||
Connected
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{connectionState === "error" && errorMessage && (
|
||||
<p className="text-xs text-destructive truncate">{errorMessage}</p>
|
||||
)}
|
||||
{connectionState === "idle" && isToolRunning && (
|
||||
<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" ? (
|
||||
<Button size="sm" disabled className="text-xs h-7 flex-shrink-0">
|
||||
<LoaderIcon className="size-3 animate-spin mr-1" />
|
||||
Connecting...
|
||||
</Button>
|
||||
) : 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"
|
||||
>
|
||||
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"
|
||||
>
|
||||
<Link2Icon className="size-3 mr-1" />
|
||||
Connect
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 = ({
|
|||
</p>
|
||||
</div>
|
||||
{isResponded && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={cn(
|
||||
"shrink-0",
|
||||
isApproved
|
||||
? "bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-400"
|
||||
isApproved
|
||||
? "bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-400"
|
||||
: "bg-red-100 text-red-700 dark:bg-red-900/50 dark:text-red-400"
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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 = ({
|
|||
}) => (
|
||||
<pre
|
||||
className={cn(
|
||||
"whitespace-pre-wrap text-xs font-mono",
|
||||
"whitespace-pre-wrap text-xs font-mono break-all",
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
|
@ -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) => (
|
||||
<div className={cn("space-y-2 overflow-hidden p-4", className)} {...props}>
|
||||
<h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
||||
Parameters
|
||||
</h4>
|
||||
<div className="rounded-md border bg-muted/50 p-4 text-foreground">
|
||||
<ToolCode code={formatToolValue(input ?? {})} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
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 = <div>{output as ReactNode}</div>;
|
||||
|
||||
if (typeof output === "object" && !isValidElement(output)) {
|
||||
Output = <ToolCode code={formatToolValue(output ?? null)} />;
|
||||
} else if (typeof output === "string") {
|
||||
Output = <ToolCode code={formatToolValue(output)} />;
|
||||
let OutputNode: ReactNode = null;
|
||||
if (errorText) {
|
||||
OutputNode = <ToolCode code={errorText} className="text-destructive" />;
|
||||
} else if (output != null) {
|
||||
if (typeof output === "object" && !isValidElement(output)) {
|
||||
OutputNode = <ToolCode code={formatToolValue(output)} />;
|
||||
} else if (typeof output === "string") {
|
||||
OutputNode = <ToolCode code={output} />;
|
||||
} else {
|
||||
OutputNode = <div>{output as ReactNode}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("space-y-2 p-4", className)} {...props}>
|
||||
<h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
||||
{errorText ? "Error" : "Result"}
|
||||
</h4>
|
||||
<div
|
||||
className={cn(
|
||||
"overflow-x-auto rounded-md border p-4 text-xs [&_table]:w-full",
|
||||
errorText
|
||||
? "bg-destructive/10 text-destructive"
|
||||
: "bg-muted/50 text-foreground"
|
||||
)}
|
||||
>
|
||||
{errorText && (
|
||||
<div className="mb-2 font-sans text-xs text-destructive">
|
||||
{errorText}
|
||||
<div className="border-t">
|
||||
{/* Tabs */}
|
||||
<div className="flex">
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"px-4 py-2 text-xs font-medium transition-colors border-b-2",
|
||||
activeTab === "parameters"
|
||||
? "border-foreground text-foreground"
|
||||
: "border-transparent text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
onClick={() => setActiveTab("parameters")}
|
||||
>
|
||||
Parameters
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"px-4 py-2 text-xs font-medium transition-colors border-b-2",
|
||||
activeTab === "result"
|
||||
? "border-foreground text-foreground"
|
||||
: "border-transparent text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
onClick={() => setActiveTab("result")}
|
||||
>
|
||||
Result
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tab content */}
|
||||
<div className="p-3">
|
||||
{activeTab === "parameters" && (
|
||||
<div className="rounded-md border bg-muted/50 p-3 max-h-64 overflow-auto">
|
||||
<ToolCode code={formatToolValue(input ?? {})} />
|
||||
</div>
|
||||
)}
|
||||
{activeTab === "result" && (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-md border p-3 max-h-64 overflow-auto",
|
||||
errorText ? "bg-destructive/10" : "bg-muted/50"
|
||||
)}
|
||||
>
|
||||
{hasOutput ? (
|
||||
<div className={cn(errorText && "text-destructive")}>
|
||||
{OutputNode}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">(pending...)</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{Output}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<ComposioConnectCard
|
||||
key={item.id}
|
||||
toolkitSlug={composioConnectData.toolkitSlug}
|
||||
toolkitDisplayName={composioConnectData.toolkitDisplayName}
|
||||
status={item.status}
|
||||
alreadyConnected={composioConnectData.alreadyConnected}
|
||||
onConnected={onComposioConnected}
|
||||
/>
|
||||
)
|
||||
}
|
||||
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)}
|
||||
>
|
||||
<ToolHeader title={item.name} type={`tool-${item.name}`} state={toToolState(item.status)} />
|
||||
<ToolHeader title={toolTitle} type={`tool-${item.name}`} state={toToolState(item.status)} />
|
||||
<ToolContent>
|
||||
<ToolInput input={input} />
|
||||
{output !== null ? <ToolOutput output={output} errorText={errorText} /> : null}
|
||||
<ToolTabbedContent input={input} output={output} errorText={errorText} />
|
||||
</ToolContent>
|
||||
</Tool>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>; 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<Set<string>>(new Set())
|
||||
const [connectingToolkit, setConnectingToolkit] = useState<string | null>(null)
|
||||
|
||||
// Tool selection state
|
||||
const [expandedToolkit, setExpandedToolkit] = useState<string | null>(null)
|
||||
const [toolkitTools, setToolkitTools] = useState<Record<string, ToolInfo[]>>({})
|
||||
const [toolsLoading, setToolsLoading] = useState<string | null>(null)
|
||||
const [enabledToolSlugs, setEnabledToolSlugs] = useState<Set<string>>(new Set())
|
||||
|
||||
// Tool search state (per-toolkit, server-side via Composio API)
|
||||
const [toolSearchQuery, setToolSearchQuery] = useState("")
|
||||
const [toolSearchResults, setToolSearchResults] = useState<ToolInfo[] | null>(null)
|
||||
const [toolSearchLoading, setToolSearchLoading] = useState(false)
|
||||
const toolSearchTimerRef = React.useRef<ReturnType<typeof setTimeout> | 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 (
|
||||
<div key={toolkit.slug} className={cn(
|
||||
"border rounded-lg overflow-hidden transition-colors",
|
||||
isExpanded && "border-border/80 shadow-sm"
|
||||
)}>
|
||||
{/* Toolkit card header */}
|
||||
<button
|
||||
onClick={() => handleToggleToolkit(toolkit.slug)}
|
||||
className={cn(
|
||||
"w-full flex items-center gap-3 px-3 py-2.5 text-left transition-colors hover:bg-accent/50",
|
||||
isExpanded && "bg-accent/30"
|
||||
)}
|
||||
>
|
||||
<div key={toolkit.slug} className="border rounded-lg overflow-hidden">
|
||||
<div className="flex items-center gap-3 px-3 py-2.5">
|
||||
{/* Logo */}
|
||||
{toolkit.meta.logo ? (
|
||||
<img
|
||||
|
|
@ -1166,188 +977,42 @@ function ToolsLibrarySettings({ dialogOpen }: { dialogOpen: boolean }) {
|
|||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-sm font-medium truncate">{toolkit.name}</span>
|
||||
<span className="text-[10px] text-muted-foreground tabular-nums">
|
||||
{toolkit.meta.tools_count} tools
|
||||
</span>
|
||||
{isConnected && (
|
||||
<span className="rounded-full bg-green-500/10 px-1.5 py-0.5 text-[10px] font-medium leading-none text-green-600">
|
||||
Connected
|
||||
</span>
|
||||
)}
|
||||
{enabledCount > 0 && (
|
||||
<span className="rounded-full bg-primary/10 px-1.5 py-0.5 text-[10px] font-medium leading-none text-primary tabular-nums">
|
||||
{enabledCount} enabled
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground truncate">
|
||||
{toolkit.meta.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Expand icon */}
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="size-4 text-muted-foreground flex-shrink-0" />
|
||||
{/* Connect / Disconnect button */}
|
||||
{isConnected ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleDisconnect(toolkit.slug)}
|
||||
className="text-xs h-7 flex-shrink-0"
|
||||
>
|
||||
Disconnect
|
||||
</Button>
|
||||
) : (
|
||||
<ChevronRight className="size-4 text-muted-foreground flex-shrink-0" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Expanded content */}
|
||||
{isExpanded && (
|
||||
<div className="border-t px-3 py-2.5 space-y-2.5 bg-muted/20">
|
||||
{/* Connection controls */}
|
||||
<div className="flex items-center gap-2">
|
||||
{isConnected ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={(e) => { e.stopPropagation(); handleDisconnect(toolkit.slug) }}
|
||||
className="text-xs h-7"
|
||||
>
|
||||
<Unlink className="size-3 mr-1" />
|
||||
Disconnect
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleConnect(toolkit.slug)}
|
||||
disabled={isConnecting}
|
||||
className="text-xs h-7 flex-shrink-0"
|
||||
>
|
||||
{isConnecting ? (
|
||||
<><Loader2 className="size-3 animate-spin mr-1" />Connecting...</>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={(e) => { e.stopPropagation(); handleConnect(toolkit.slug) }}
|
||||
disabled={isConnecting}
|
||||
className="text-xs h-7"
|
||||
>
|
||||
{isConnecting ? (
|
||||
<><Loader2 className="size-3 animate-spin mr-1" />Connecting...</>
|
||||
) : (
|
||||
<><Link2 className="size-3 mr-1" />Connect</>
|
||||
)}
|
||||
</Button>
|
||||
<><Link2 className="size-3 mr-1" />Connect</>
|
||||
)}
|
||||
|
||||
{/* Enable/Disable all (only if connected and tools loaded) */}
|
||||
{isConnected && tools.length > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleToggleAllTools(toolkit.slug, !allEnabled)
|
||||
}}
|
||||
className="text-xs h-7 ml-auto"
|
||||
>
|
||||
{allEnabled ? "Disable All" : "Enable All"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tools section (only if connected) */}
|
||||
{isConnected && (
|
||||
<div className="space-y-2">
|
||||
{/* Tool search input — shown when toolkit has many tools */}
|
||||
{!isLoadingTools && tools.length > 5 && (
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-1/2 -translate-y-1/2 size-3.5 text-muted-foreground" />
|
||||
<input
|
||||
type="text"
|
||||
value={toolSearchQuery}
|
||||
onChange={(e) => 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 && (
|
||||
<Loader2 className="absolute right-2 top-1/2 -translate-y-1/2 size-3 animate-spin text-muted-foreground" />
|
||||
)}
|
||||
{!toolSearchLoading && toolSearchQuery && (
|
||||
<button
|
||||
onClick={() => handleToolSearch(toolkit.slug, "")}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<X className="size-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Search results summary */}
|
||||
{isSearching && !toolSearchLoading && toolSearchResults !== null && (
|
||||
<div className="flex items-center gap-1.5 text-[11px] text-muted-foreground">
|
||||
<span className="tabular-nums">{toolSearchResults.length}</span>
|
||||
<span>{toolSearchResults.length === 1 ? 'tool' : 'tools'} matching “{toolSearchQuery}”</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tool list */}
|
||||
{isLoadingTools ? (
|
||||
<div className="flex items-center gap-2 py-3 text-xs text-muted-foreground">
|
||||
<Loader2 className="size-3 animate-spin" />
|
||||
Loading tools...
|
||||
</div>
|
||||
) : displayTools.length === 0 ? (
|
||||
<div className="py-3 text-center">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{isSearching
|
||||
? `No tools found for "${toolSearchQuery}"`
|
||||
: "No tools found"
|
||||
}
|
||||
</p>
|
||||
{isSearching && (
|
||||
<button
|
||||
onClick={() => handleToolSearch(toolkit.slug, "")}
|
||||
className="mt-1 text-[11px] text-primary hover:underline"
|
||||
>
|
||||
Clear search
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-h-[240px] overflow-y-auto space-y-0.5 -mx-1 px-1">
|
||||
{displayTools.map((tool) => {
|
||||
const isEnabled = enabledToolSlugs.has(tool.slug)
|
||||
return (
|
||||
<label
|
||||
key={tool.slug}
|
||||
className={cn(
|
||||
"flex items-start gap-2 px-2 py-1.5 rounded-md cursor-pointer transition-colors",
|
||||
isEnabled ? "bg-primary/5" : "hover:bg-accent/50"
|
||||
)}
|
||||
>
|
||||
<div className="pt-0.5">
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
handleToggleTool(tool, !isEnabled)
|
||||
}}
|
||||
className={cn(
|
||||
"size-4 rounded border flex items-center justify-center transition-colors cursor-pointer",
|
||||
isEnabled
|
||||
? "bg-primary border-primary"
|
||||
: "border-border hover:border-primary/50"
|
||||
)}
|
||||
>
|
||||
{isEnabled && <Check className="size-3 text-primary-foreground" />}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-xs font-medium">{tool.name}</div>
|
||||
<div className="text-[11px] text-muted-foreground line-clamp-2">
|
||||
{tool.description}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Not connected hint */}
|
||||
{!isConnected && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Connect this toolkit to browse and enable its tools.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -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<string, unknown> | undefined
|
||||
const result = tool.result as Record<string, unknown> | 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<string, unknown> | undefined
|
||||
const result = tool.result as Record<string, unknown> | 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()
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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\`.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
33
apps/x/packages/core/src/composio/connection-bridge.ts
Normal file
33
apps/x/packages/core/src/composio/connection-bridge.ts
Normal 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;
|
||||
}
|
||||
74
apps/x/packages/core/src/composio/curated-toolkits.ts
Normal file
74
apps/x/packages/core/src/composio/curated-toolkits.ts
Normal 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));
|
||||
32
apps/x/packages/shared/src/composio.ts
Normal file
32
apps/x/packages/shared/src/composio.ts
Normal file
|
|
@ -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<string, string> = {
|
||||
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',
|
||||
};
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue