mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-18 20:15:20 +02:00
first version of composio
This commit is contained in:
parent
e8a666499a
commit
04e094a299
10 changed files with 1061 additions and 7 deletions
|
|
@ -2,7 +2,10 @@ 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 } from '@x/core/dist/composio/types.js';
|
||||
import { refreshComposioTools } from '@x/core/dist/application/lib/builtin-tools.js';
|
||||
|
||||
const REDIRECT_URI = 'http://localhost:8081/oauth/callback';
|
||||
|
||||
|
|
@ -249,11 +252,16 @@ export async function disconnect(toolkitSlug: string): Promise<{ success: boolea
|
|||
// 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
|
||||
composioAccountsRepo.deleteAccount(toolkitSlug);
|
||||
composioEnabledToolsRepo.disableAllForToolkit(toolkitSlug);
|
||||
refreshComposioTools();
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
|
@ -294,3 +302,95 @@ export async function executeAction(
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List available Composio toolkits
|
||||
*/
|
||||
export async function listToolkits(cursor?: string): Promise<{
|
||||
items: Array<{
|
||||
slug: string;
|
||||
name: string;
|
||||
meta: { description: string; logo: string; tools_count: number; triggers_count: number };
|
||||
no_auth: boolean;
|
||||
auth_schemes: string[];
|
||||
composio_managed_auth_schemes: string[];
|
||||
}>;
|
||||
nextCursor: string | null;
|
||||
totalItems: number;
|
||||
}> {
|
||||
const result = await composioClient.listToolkits(cursor || null);
|
||||
return {
|
||||
items: result.items,
|
||||
nextCursor: result.next_cursor,
|
||||
totalItems: result.total_items,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -443,6 +443,22 @@ export function setupIpcHandlers() {
|
|||
'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);
|
||||
},
|
||||
// Agent schedule handlers
|
||||
'agent-schedule:getConfig': async () => {
|
||||
const repo = container.resolve<IAgentScheduleRepo>('agentScheduleRepo');
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import * as React from "react"
|
||||
import { useState, useEffect, useCallback } from "react"
|
||||
import { Server, Key, Shield, Palette, Monitor, Sun, Moon, Loader2, CheckCircle2, Plus, X } from "lucide-react"
|
||||
import { Server, Key, Shield, Palette, Monitor, Sun, Moon, Loader2, CheckCircle2, Plus, X, Wrench, Search, ChevronDown, ChevronRight, Check, Link2, Unlink } from "lucide-react"
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
|
|
@ -22,7 +22,7 @@ import { cn } from "@/lib/utils"
|
|||
import { useTheme } from "@/contexts/theme-context"
|
||||
import { toast } from "sonner"
|
||||
|
||||
type ConfigTab = "models" | "mcp" | "security" | "appearance"
|
||||
type ConfigTab = "models" | "mcp" | "security" | "appearance" | "tools"
|
||||
|
||||
interface TabConfig {
|
||||
id: ConfigTab
|
||||
|
|
@ -60,6 +60,12 @@ const tabs: TabConfig[] = [
|
|||
icon: Palette,
|
||||
description: "Customize the look and feel",
|
||||
},
|
||||
{
|
||||
id: "tools",
|
||||
label: "Tools Library",
|
||||
icon: Wrench,
|
||||
description: "Browse and enable Composio toolkits",
|
||||
},
|
||||
]
|
||||
|
||||
interface SettingsDialogProps {
|
||||
|
|
@ -685,6 +691,540 @@ function ModelSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
|||
)
|
||||
}
|
||||
|
||||
// --- Tools Library Settings ---
|
||||
|
||||
interface ToolkitInfo {
|
||||
slug: string
|
||||
name: string
|
||||
meta: { description: string; logo: string; tools_count: number; triggers_count: number }
|
||||
no_auth: boolean
|
||||
auth_schemes: string[]
|
||||
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)
|
||||
const [apiKeyInput, setApiKeyInput] = useState("")
|
||||
const [apiKeySaving, setApiKeySaving] = useState(false)
|
||||
const [showApiKeyInput, setShowApiKeyInput] = useState(false)
|
||||
|
||||
// Toolkit browsing state
|
||||
const [toolkits, setToolkits] = useState<ToolkitInfo[]>([])
|
||||
const [toolkitsLoading, setToolkitsLoading] = useState(false)
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
|
||||
// Connection state
|
||||
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())
|
||||
|
||||
// Check API key configuration
|
||||
const checkApiKey = useCallback(async () => {
|
||||
try {
|
||||
const result = await window.ipc.invoke("composio:is-configured", null)
|
||||
setApiKeyConfigured(result.configured)
|
||||
if (!result.configured) {
|
||||
setShowApiKeyInput(true)
|
||||
}
|
||||
} catch {
|
||||
setApiKeyConfigured(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Load connected toolkits
|
||||
const loadConnected = useCallback(async () => {
|
||||
try {
|
||||
const result = await window.ipc.invoke("composio:list-connected", null)
|
||||
setConnectedToolkits(new Set(result.toolkits))
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 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)
|
||||
try {
|
||||
const result = await window.ipc.invoke("composio:list-toolkits", {})
|
||||
setToolkits(result.items)
|
||||
} catch {
|
||||
toast.error("Failed to load toolkits")
|
||||
} finally {
|
||||
setToolkitsLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Initial load
|
||||
useEffect(() => {
|
||||
if (!dialogOpen) return
|
||||
checkApiKey()
|
||||
loadConnected()
|
||||
loadEnabledTools()
|
||||
}, [dialogOpen, checkApiKey, loadConnected, loadEnabledTools])
|
||||
|
||||
// Load toolkits when API key is configured
|
||||
useEffect(() => {
|
||||
if (dialogOpen && apiKeyConfigured) {
|
||||
loadToolkits()
|
||||
}
|
||||
}, [dialogOpen, apiKeyConfigured, loadToolkits])
|
||||
|
||||
// Listen for composio connection events
|
||||
useEffect(() => {
|
||||
const cleanup = window.ipc.on('composio:didConnect', (event) => {
|
||||
const { toolkitSlug, success, error } = event
|
||||
setConnectingToolkit(null)
|
||||
if (success) {
|
||||
setConnectedToolkits(prev => new Set([...prev, toolkitSlug]))
|
||||
toast.success(`Connected to ${toolkitSlug}`)
|
||||
} else {
|
||||
toast.error(error || `Failed to connect to ${toolkitSlug}`)
|
||||
}
|
||||
})
|
||||
return cleanup
|
||||
}, [])
|
||||
|
||||
// Save API key
|
||||
const handleSaveApiKey = async () => {
|
||||
const trimmed = apiKeyInput.trim()
|
||||
if (!trimmed) return
|
||||
setApiKeySaving(true)
|
||||
try {
|
||||
const result = await window.ipc.invoke("composio:set-api-key", { apiKey: trimmed })
|
||||
if (result.success) {
|
||||
setApiKeyConfigured(true)
|
||||
setShowApiKeyInput(false)
|
||||
setApiKeyInput("")
|
||||
toast.success("Composio API key saved")
|
||||
} else {
|
||||
toast.error(result.error || "Failed to save API key")
|
||||
}
|
||||
} catch {
|
||||
toast.error("Failed to save API key")
|
||||
} finally {
|
||||
setApiKeySaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Connect a toolkit
|
||||
const handleConnect = async (toolkitSlug: string) => {
|
||||
setConnectingToolkit(toolkitSlug)
|
||||
try {
|
||||
const result = await window.ipc.invoke("composio:initiate-connection", { toolkitSlug })
|
||||
if (!result.success) {
|
||||
toast.error(result.error || "Failed to connect")
|
||||
setConnectingToolkit(null)
|
||||
}
|
||||
// Success will be handled by composio:didConnect event
|
||||
} catch {
|
||||
toast.error("Failed to connect")
|
||||
setConnectingToolkit(null)
|
||||
}
|
||||
}
|
||||
|
||||
// Disconnect a toolkit
|
||||
const handleDisconnect = async (toolkitSlug: string) => {
|
||||
try {
|
||||
await window.ipc.invoke("composio:disconnect", { toolkitSlug })
|
||||
setConnectedToolkits(prev => {
|
||||
const next = new Set(prev)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 =>
|
||||
t.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
t.slug.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
t.meta.description.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
: toolkits
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Section A: API Key */}
|
||||
<div className="space-y-2">
|
||||
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Composio API Key</span>
|
||||
{apiKeyConfigured && !showApiKeyInput ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1.5 text-sm text-green-600">
|
||||
<CheckCircle2 className="size-4" />
|
||||
API key configured
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowApiKeyInput(true)}
|
||||
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Change
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Enter your Composio API key to browse and enable tool integrations.
|
||||
Get your key from{" "}
|
||||
<a
|
||||
href="https://app.composio.dev/settings"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
app.composio.dev/settings
|
||||
</a>
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="password"
|
||||
value={apiKeyInput}
|
||||
onChange={(e) => setApiKeyInput(e.target.value)}
|
||||
placeholder="Paste your Composio API key"
|
||||
onKeyDown={(e) => e.key === "Enter" && handleSaveApiKey()}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
onClick={handleSaveApiKey}
|
||||
disabled={!apiKeyInput.trim() || apiKeySaving}
|
||||
size="sm"
|
||||
>
|
||||
{apiKeySaving ? <Loader2 className="size-4 animate-spin" /> : "Save"}
|
||||
</Button>
|
||||
{apiKeyConfigured && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => { setShowApiKeyInput(false); setApiKeyInput("") }}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Section B: Toolkit Browser (only when API key configured) */}
|
||||
{apiKeyConfigured && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Available Toolkits</span>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
||||
<Input
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Search toolkits..."
|
||||
className="pl-8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{toolkitsLoading ? (
|
||||
<div className="flex items-center justify-center py-8 text-muted-foreground text-sm">
|
||||
<Loader2 className="size-4 animate-spin mr-2" />
|
||||
Loading toolkits...
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1.5 max-h-[340px] overflow-y-auto pr-1">
|
||||
{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
|
||||
|
||||
return (
|
||||
<div key={toolkit.slug} className="border rounded-md overflow-hidden">
|
||||
{/* 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"
|
||||
)}
|
||||
>
|
||||
{/* Logo */}
|
||||
{toolkit.meta.logo ? (
|
||||
<img
|
||||
src={toolkit.meta.logo}
|
||||
alt=""
|
||||
className="size-7 rounded object-contain flex-shrink-0"
|
||||
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
|
||||
/>
|
||||
) : (
|
||||
<div className="size-7 rounded bg-muted flex items-center justify-center flex-shrink-0">
|
||||
<Wrench className="size-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Name & description */}
|
||||
<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">
|
||||
{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">
|
||||
{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" />
|
||||
) : (
|
||||
<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 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={(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>
|
||||
)}
|
||||
|
||||
{/* 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 list (only if connected) */}
|
||||
{isConnected && (
|
||||
<div className="space-y-0.5">
|
||||
{isLoadingTools ? (
|
||||
<div className="flex items-center gap-2 py-2 text-xs text-muted-foreground">
|
||||
<Loader2 className="size-3 animate-spin" />
|
||||
Loading tools...
|
||||
</div>
|
||||
) : tools.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground py-1">No tools found</p>
|
||||
) : (
|
||||
<div className="max-h-[200px] overflow-y-auto space-y-0.5">
|
||||
{tools.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 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"
|
||||
)}
|
||||
>
|
||||
{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-1">
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{filteredToolkits.length === 0 && !toolkitsLoading && (
|
||||
<div className="text-center py-6 text-sm text-muted-foreground">
|
||||
{searchQuery ? "No toolkits match your search" : "No toolkits available"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// --- Main Settings Dialog ---
|
||||
|
||||
export function SettingsDialog({ children }: SettingsDialogProps) {
|
||||
|
|
@ -814,11 +1354,13 @@ export function SettingsDialog({ children }: SettingsDialogProps) {
|
|||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className={cn("flex-1 p-4 min-h-0", activeTab === "models" ? "overflow-y-auto" : "overflow-hidden")}>
|
||||
<div className={cn("flex-1 p-4 min-h-0", (activeTab === "models" || activeTab === "tools") ? "overflow-y-auto" : "overflow-hidden")}>
|
||||
{activeTab === "models" ? (
|
||||
<ModelSettings dialogOpen={open} />
|
||||
) : activeTab === "appearance" ? (
|
||||
<AppearanceSettings />
|
||||
) : activeTab === "tools" ? (
|
||||
<ToolsLibrarySettings dialogOpen={open} />
|
||||
) : loading ? (
|
||||
<div className="h-full flex items-center justify-center text-muted-foreground text-sm">
|
||||
Loading...
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { execTool } from "../application/lib/exec-tool.js";
|
|||
import { AskHumanRequestEvent, RunEvent, ToolPermissionRequestEvent } from "@x/shared/dist/runs.js";
|
||||
import { BuiltinTools } from "../application/lib/builtin-tools.js";
|
||||
import { CopilotAgent } from "../application/assistant/agent.js";
|
||||
import { buildCopilotInstructions } from "../application/assistant/instructions.js";
|
||||
import { isBlocked, extractCommandNames } from "../application/lib/command-executor.js";
|
||||
import container from "../di/container.js";
|
||||
import { IModelConfigRepo } from "../models/repo.js";
|
||||
|
|
@ -312,7 +313,15 @@ function formatLlmStreamError(rawError: unknown): string {
|
|||
|
||||
export async function loadAgent(id: string): Promise<z.infer<typeof Agent>> {
|
||||
if (id === "copilot" || id === "rowboatx") {
|
||||
return CopilotAgent;
|
||||
// Rebuild tools from current BuiltinTools to pick up dynamically
|
||||
// registered Composio tools (added via refreshComposioTools).
|
||||
const tools: Record<string, z.infer<typeof ToolAttachment>> = {};
|
||||
for (const name of Object.keys(BuiltinTools)) {
|
||||
tools[name] = { type: "builtin", name };
|
||||
}
|
||||
// Rebuild instructions to include current Composio tools section
|
||||
const instructions = buildCopilotInstructions();
|
||||
return { ...CopilotAgent, tools, instructions };
|
||||
}
|
||||
|
||||
if (id === 'note_creation') {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,56 @@
|
|||
import { skillCatalog } from "./skills/index.js";
|
||||
import { WorkDir as BASE_DIR } from "../../config/config.js";
|
||||
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";
|
||||
|
||||
const runtimeContextPrompt = getRuntimeContextPrompt(getRuntimeContext());
|
||||
|
||||
/**
|
||||
* Generate dynamic instructions section for Composio tools.
|
||||
* Returns empty string if no tools are enabled.
|
||||
*/
|
||||
function getComposioToolsPrompt(): string {
|
||||
if (!isComposioConfigured()) return '';
|
||||
|
||||
const enabledTools = composioEnabledToolsRepo.getAll();
|
||||
const toolEntries = Object.values(enabledTools);
|
||||
if (toolEntries.length === 0) return '';
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Check which toolkits are connected
|
||||
const connectedToolkits = new Set(composioAccountsRepo.getConnectedToolkits());
|
||||
|
||||
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`;
|
||||
|
||||
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`;
|
||||
}
|
||||
|
||||
return prompt;
|
||||
}
|
||||
|
||||
export const CopilotInstructions = `You are Rowboat Copilot - an AI assistant for everyday work. You help users with anything they want. For instance, drafting emails, prepping for meetings, tracking projects, or answering questions - with memory that compounds from their emails, calendar, and notes. Everything runs locally on the user's machine. The nerdy coworker who remembers everything.
|
||||
|
||||
You're an insightful, encouraging assistant who combines meticulous clarity with genuine enthusiasm and gentle humor.
|
||||
|
|
@ -184,6 +231,7 @@ ${runtimeContextPrompt}
|
|||
- \`loadSkill\` - Skill loading
|
||||
- \`slack-checkConnection\`, \`slack-listAvailableTools\`, \`slack-executeAction\` - Slack integration (requires Slack to be connected via Composio). Use \`slack-listAvailableTools\` first to discover available tool slugs, then \`slack-executeAction\` to execute them.
|
||||
- \`web-search\` and \`research-search\` - Web and research search tools (available when configured). **You MUST load the \`web-search\` skill before using either of these tools.** It tells you which tool to pick and how many searches to do.
|
||||
- **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.
|
||||
|
||||
**Prefer these tools whenever possible** — they work instantly with zero friction. For file operations inside \`~/.rowboat/\`, always use these instead of \`executeCommand\`.
|
||||
|
||||
|
|
@ -223,3 +271,13 @@ This renders as an interactive card in the UI that the user can click to open th
|
|||
**IMPORTANT:** Only use filepath blocks for files that already exist. The card is clickable and opens the file, so it must point to a real file. If you are proposing a path for a file that hasn't been created yet (e.g., "Shall I save it at ~/Documents/report.pdf?"), use inline code (\`~/Documents/report.pdf\`) instead of a filepath block. Use the filepath block only after the file has been written/created successfully.
|
||||
|
||||
Never output raw file paths in plain text when they could be wrapped in a filepath block — unless the file does not exist yet.`;
|
||||
|
||||
/**
|
||||
* Build full copilot instructions with dynamic Composio tools section.
|
||||
* Called each time the agent is loaded to reflect currently enabled tools.
|
||||
*/
|
||||
export function buildCopilotInstructions(): string {
|
||||
const composioPrompt = getComposioToolsPrompt();
|
||||
if (!composioPrompt) return CopilotInstructions;
|
||||
return CopilotInstructions + '\n' + composioPrompt;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,10 +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, listToolkitTools } from "../../composio/client.js";
|
||||
import { slackToolCatalog } from "../assistant/skills/slack/tool-catalog.js";
|
||||
import type { ToolContext } from "./exec-tool.js";
|
||||
import { generateText } from "ai";
|
||||
import { generateText, jsonSchema } from "ai";
|
||||
import { createProvider } from "../../models/models.js";
|
||||
import { IModelConfigRepo } from "../../models/repo.js";
|
||||
// Parser libraries are loaded dynamically inside parseFile.execute()
|
||||
|
|
@ -1478,3 +1479,76 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
|
|||
},
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 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.`,
|
||||
};
|
||||
}
|
||||
return executeComposioAction(slug, account.id, input);
|
||||
},
|
||||
isAvailable: async () => {
|
||||
return 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.
|
||||
*/
|
||||
export function refreshComposioTools(): void {
|
||||
unregisterComposioTools();
|
||||
registerComposioTools();
|
||||
}
|
||||
|
||||
// Register on module load
|
||||
refreshComposioTools();
|
||||
|
|
|
|||
|
|
@ -330,6 +330,55 @@ export async function listToolkitTools(
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* List available tools for a toolkit with full details including input_parameters
|
||||
*/
|
||||
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 apiKey = getApiKey();
|
||||
if (!apiKey) {
|
||||
throw new Error('Composio API key not configured');
|
||||
}
|
||||
|
||||
const url = new URL(`${BASE_URL}/tools`);
|
||||
url.searchParams.set('toolkit_slug', toolkitSlug);
|
||||
url.searchParams.set('limit', '200');
|
||||
if (searchQuery) {
|
||||
url.searchParams.set('search', searchQuery);
|
||||
}
|
||||
|
||||
console.log(`[Composio] Listing tools (detailed) for toolkit: ${toolkitSlug}`);
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
headers: { "x-api-key": apiKey },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to list tools: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as { items?: Array<Record<string, unknown>> };
|
||||
|
||||
return {
|
||||
items: (data.items || []).map((item) => {
|
||||
const inputParams = item.input_parameters as Record<string, unknown> | undefined;
|
||||
return {
|
||||
slug: String(item.slug ?? ''),
|
||||
name: String(item.name ?? ''),
|
||||
description: String(item.description ?? ''),
|
||||
toolkitSlug,
|
||||
inputParameters: {
|
||||
type: 'object' as const,
|
||||
properties: (inputParams?.properties as Record<string, unknown>) ?? {},
|
||||
required: Array.isArray(inputParams?.required) ? inputParams.required as string[] : undefined,
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a tool action using Composio SDK
|
||||
*/
|
||||
|
|
|
|||
129
apps/x/packages/core/src/composio/enabled-tools-repo.ts
Normal file
129
apps/x/packages/core/src/composio/enabled-tools-repo.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { z } from "zod";
|
||||
import { WorkDir } from "../config/config.js";
|
||||
|
||||
const ENABLED_TOOLS_FILE = path.join(WorkDir, 'data', 'composio', 'enabled_tools.json');
|
||||
|
||||
/**
|
||||
* Schema for an enabled Composio tool
|
||||
*/
|
||||
export const ZEnabledTool = z.object({
|
||||
slug: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
toolkitSlug: z.string(),
|
||||
inputParameters: 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: {} }),
|
||||
});
|
||||
|
||||
export type EnabledTool = z.infer<typeof ZEnabledTool>;
|
||||
|
||||
/**
|
||||
* Schema for the enabled tools storage file
|
||||
*/
|
||||
const ZEnabledToolsStorage = z.object({
|
||||
tools: z.record(z.string(), ZEnabledTool), // keyed by tool slug
|
||||
});
|
||||
|
||||
type EnabledToolsStorage = z.infer<typeof ZEnabledToolsStorage>;
|
||||
|
||||
/**
|
||||
* Interface for Composio enabled tools repository
|
||||
*/
|
||||
export interface IComposioEnabledToolsRepo {
|
||||
getAll(): Record<string, EnabledTool>;
|
||||
getByToolkit(toolkitSlug: string): EnabledTool[];
|
||||
enable(tool: EnabledTool): void;
|
||||
enableBatch(tools: EnabledTool[]): void;
|
||||
disable(toolSlug: string): void;
|
||||
disableBatch(toolSlugs: string[]): void;
|
||||
disableAllForToolkit(toolkitSlug: string): void;
|
||||
isEnabled(toolSlug: string): boolean;
|
||||
}
|
||||
|
||||
function ensureStorageDir(): void {
|
||||
const dir = path.dirname(ENABLED_TOOLS_FILE);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function loadStorage(): EnabledToolsStorage {
|
||||
try {
|
||||
if (fs.existsSync(ENABLED_TOOLS_FILE)) {
|
||||
const data = fs.readFileSync(ENABLED_TOOLS_FILE, 'utf-8');
|
||||
return ZEnabledToolsStorage.parse(JSON.parse(data));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ComposioEnabledTools] Failed to load storage:', error);
|
||||
}
|
||||
return { tools: {} };
|
||||
}
|
||||
|
||||
function saveStorage(storage: EnabledToolsStorage): void {
|
||||
ensureStorageDir();
|
||||
fs.writeFileSync(ENABLED_TOOLS_FILE, JSON.stringify(storage, null, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Repository for managing enabled Composio tools
|
||||
*/
|
||||
export class ComposioEnabledToolsRepo implements IComposioEnabledToolsRepo {
|
||||
getAll(): Record<string, EnabledTool> {
|
||||
return loadStorage().tools;
|
||||
}
|
||||
|
||||
getByToolkit(toolkitSlug: string): EnabledTool[] {
|
||||
const storage = loadStorage();
|
||||
return Object.values(storage.tools).filter(t => t.toolkitSlug === toolkitSlug);
|
||||
}
|
||||
|
||||
enable(tool: EnabledTool): void {
|
||||
const storage = loadStorage();
|
||||
storage.tools[tool.slug] = tool;
|
||||
saveStorage(storage);
|
||||
}
|
||||
|
||||
enableBatch(tools: EnabledTool[]): void {
|
||||
const storage = loadStorage();
|
||||
for (const tool of tools) {
|
||||
storage.tools[tool.slug] = tool;
|
||||
}
|
||||
saveStorage(storage);
|
||||
}
|
||||
|
||||
disable(toolSlug: string): void {
|
||||
const storage = loadStorage();
|
||||
delete storage.tools[toolSlug];
|
||||
saveStorage(storage);
|
||||
}
|
||||
|
||||
disableBatch(toolSlugs: string[]): void {
|
||||
const storage = loadStorage();
|
||||
for (const slug of toolSlugs) {
|
||||
delete storage.tools[slug];
|
||||
}
|
||||
saveStorage(storage);
|
||||
}
|
||||
|
||||
disableAllForToolkit(toolkitSlug: string): void {
|
||||
const storage = loadStorage();
|
||||
for (const [slug, tool] of Object.entries(storage.tools)) {
|
||||
if (tool.toolkitSlug === toolkitSlug) {
|
||||
delete storage.tools[slug];
|
||||
}
|
||||
}
|
||||
saveStorage(storage);
|
||||
}
|
||||
|
||||
isEnabled(toolSlug: string): boolean {
|
||||
const storage = loadStorage();
|
||||
return toolSlug in storage.tools;
|
||||
}
|
||||
}
|
||||
|
||||
export const composioEnabledToolsRepo = new ComposioEnabledToolsRepo();
|
||||
|
|
@ -46,8 +46,10 @@ export const ZToolkit = z.object({
|
|||
name: z.string(),
|
||||
meta: ZToolkitMeta,
|
||||
no_auth: z.boolean(),
|
||||
auth_schemes: z.array(ZAuthScheme),
|
||||
composio_managed_auth_schemes: z.array(ZAuthScheme),
|
||||
// Use z.string() instead of ZAuthScheme to be resilient against
|
||||
// new auth types added by the Composio API over time.
|
||||
auth_schemes: z.array(z.string()),
|
||||
composio_managed_auth_schemes: z.array(z.string()),
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -362,6 +362,81 @@ const ipcSchemas = {
|
|||
}),
|
||||
res: z.null(),
|
||||
},
|
||||
// Composio Tools Library channels
|
||||
'composio:list-toolkits': {
|
||||
req: z.object({
|
||||
cursor: z.string().optional(),
|
||||
}),
|
||||
res: z.object({
|
||||
items: z.array(z.object({
|
||||
slug: z.string(),
|
||||
name: z.string(),
|
||||
meta: z.object({
|
||||
description: z.string(),
|
||||
logo: z.string(),
|
||||
tools_count: z.number(),
|
||||
triggers_count: z.number(),
|
||||
}),
|
||||
no_auth: z.boolean(),
|
||||
auth_schemes: z.array(z.string()),
|
||||
composio_managed_auth_schemes: z.array(z.string()),
|
||||
})),
|
||||
nextCursor: z.string().nullable(),
|
||||
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