mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-12 19:55:19 +02:00
slack integration to allow users to ask copilot to use slack on their behalf
This commit is contained in:
parent
d04a9cfa84
commit
9bd1009a26
18 changed files with 2310 additions and 75 deletions
|
|
@ -4,6 +4,16 @@ import { URL } from 'url';
|
|||
const OAUTH_CALLBACK_PATH = '/oauth/callback';
|
||||
const DEFAULT_PORT = 8080;
|
||||
|
||||
/** Escape HTML special characters to prevent XSS */
|
||||
function escapeHtml(str: string): string {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
export interface AuthServerResult {
|
||||
server: Server;
|
||||
port: number;
|
||||
|
|
@ -15,7 +25,7 @@ export interface AuthServerResult {
|
|||
*/
|
||||
export function createAuthServer(
|
||||
port: number = DEFAULT_PORT,
|
||||
onCallback: (code: string, state: string) => void
|
||||
onCallback: (code: string, state: string) => void | Promise<void>
|
||||
): Promise<AuthServerResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = createServer((req, res) => {
|
||||
|
|
@ -46,7 +56,7 @@ export function createAuthServer(
|
|||
</head>
|
||||
<body>
|
||||
<h1 class="error">Authorization Failed</h1>
|
||||
<p>Error: ${error}</p>
|
||||
<p>Error: ${escapeHtml(error)}</p>
|
||||
<p>You can close this window.</p>
|
||||
<script>setTimeout(() => window.close(), 3000);</script>
|
||||
</body>
|
||||
|
|
@ -55,48 +65,28 @@ export function createAuthServer(
|
|||
return;
|
||||
}
|
||||
|
||||
if (code && state) {
|
||||
onCallback(code, state);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||
res.end(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Authorization Successful</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
|
||||
.success { color: #2e7d32; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1 class="success">Authorization Successful</h1>
|
||||
<p>You can close this window.</p>
|
||||
<script>setTimeout(() => window.close(), 2000);</script>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
} else {
|
||||
res.writeHead(400, { 'Content-Type': 'text/html' });
|
||||
res.end(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>OAuth Error</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
|
||||
.error { color: #d32f2f; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1 class="error">Invalid Request</h1>
|
||||
<p>Missing code or state parameter.</p>
|
||||
<p>You can close this window.</p>
|
||||
<script>setTimeout(() => window.close(), 3000);</script>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
}
|
||||
// Handle callback - either traditional OAuth with code/state or Composio-style notification
|
||||
// Composio callbacks may not have code/state, just a notification that the flow completed
|
||||
onCallback(code || '', state || '');
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||
res.end(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Authorization Successful</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
|
||||
.success { color: #2e7d32; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1 class="success">Authorization Successful</h1>
|
||||
<p>You can close this window.</p>
|
||||
<script>setTimeout(() => window.close(), 2000);</script>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end('Not Found');
|
||||
|
|
|
|||
296
apps/x/apps/main/src/composio-handler.ts
Normal file
296
apps/x/apps/main/src/composio-handler.ts
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
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 type { LocalConnectedAccount } from '@x/core/dist/composio/types.js';
|
||||
|
||||
const REDIRECT_URI = 'http://localhost:8081/oauth/callback';
|
||||
|
||||
// Store active OAuth flows
|
||||
const activeFlows = new Map<string, {
|
||||
toolkitSlug: string;
|
||||
connectedAccountId: string;
|
||||
authConfigId: string;
|
||||
}>();
|
||||
|
||||
/**
|
||||
* Emit Composio connection event to all renderer windows
|
||||
*/
|
||||
export function emitComposioEvent(event: { toolkitSlug: string; success: boolean; error?: string }): void {
|
||||
const windows = BrowserWindow.getAllWindows();
|
||||
for (const win of windows) {
|
||||
if (!win.isDestroyed() && win.webContents) {
|
||||
win.webContents.send('composio:didConnect', event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Composio is configured with an API key
|
||||
*/
|
||||
export function isConfigured(): { configured: boolean } {
|
||||
return { configured: composioClient.isConfigured() };
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Composio API key
|
||||
*/
|
||||
export function setApiKey(apiKey: string): { success: boolean; error?: string } {
|
||||
try {
|
||||
composioClient.setApiKey(apiKey);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to set API key',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate OAuth connection for a toolkit
|
||||
*/
|
||||
export async function initiateConnection(toolkitSlug: string): Promise<{
|
||||
success: boolean;
|
||||
redirectUrl?: string;
|
||||
connectedAccountId?: string;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
console.log(`[Composio] Initiating connection for ${toolkitSlug}...`);
|
||||
|
||||
// Check if already connected
|
||||
if (composioAccountsRepo.isConnected(toolkitSlug)) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// Get toolkit to check auth schemes
|
||||
const toolkit = await composioClient.getToolkit(toolkitSlug);
|
||||
|
||||
// Check for managed OAuth2
|
||||
if (!toolkit.composio_managed_auth_schemes.includes('OAUTH2')) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Toolkit ${toolkitSlug} does not support managed OAuth2`,
|
||||
};
|
||||
}
|
||||
|
||||
// Find or create managed OAuth2 auth config
|
||||
const authConfigs = await composioClient.listAuthConfigs(toolkitSlug, null, true);
|
||||
let authConfigId: string;
|
||||
|
||||
const managedOauth2 = authConfigs.items.find(
|
||||
cfg => cfg.auth_scheme === 'OAUTH2' && cfg.is_composio_managed
|
||||
);
|
||||
|
||||
if (managedOauth2) {
|
||||
authConfigId = managedOauth2.id;
|
||||
} else {
|
||||
// Create new managed auth config
|
||||
const created = await composioClient.createAuthConfig({
|
||||
toolkit: { slug: toolkitSlug },
|
||||
auth_config: {
|
||||
type: 'use_composio_managed_auth',
|
||||
name: `rowboat-${toolkitSlug}`,
|
||||
},
|
||||
});
|
||||
authConfigId = created.auth_config.id;
|
||||
}
|
||||
|
||||
// Create connected account with callback URL
|
||||
const callbackUrl = REDIRECT_URI;
|
||||
const response = await composioClient.createConnectedAccount({
|
||||
auth_config: { id: authConfigId },
|
||||
connection: {
|
||||
user_id: 'rowboat-user',
|
||||
callback_url: callbackUrl,
|
||||
},
|
||||
});
|
||||
|
||||
const connectedAccountId = response.id;
|
||||
|
||||
// Safely extract redirectUrl with type checking
|
||||
const connectionVal = response.connectionData?.val;
|
||||
const redirectUrl = typeof connectionVal === 'object' && connectionVal !== null && 'redirectUrl' in connectionVal
|
||||
? String((connectionVal as Record<string, unknown>).redirectUrl)
|
||||
: undefined;
|
||||
|
||||
if (!redirectUrl) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'No redirect URL received from Composio',
|
||||
};
|
||||
}
|
||||
|
||||
// Store flow state
|
||||
const flowKey = `${toolkitSlug}-${Date.now()}`;
|
||||
activeFlows.set(flowKey, {
|
||||
toolkitSlug,
|
||||
connectedAccountId,
|
||||
authConfigId,
|
||||
});
|
||||
|
||||
// Save initial account state
|
||||
const account: LocalConnectedAccount = {
|
||||
id: connectedAccountId,
|
||||
authConfigId,
|
||||
status: 'INITIATED',
|
||||
toolkitSlug,
|
||||
createdAt: new Date().toISOString(),
|
||||
lastUpdatedAt: new Date().toISOString(),
|
||||
};
|
||||
composioAccountsRepo.saveAccount(account);
|
||||
|
||||
// Set up callback server
|
||||
let cleanupTimeout: NodeJS.Timeout;
|
||||
const { server } = await createAuthServer(8081, async (_code, _state) => {
|
||||
// OAuth callback received - sync the account status
|
||||
try {
|
||||
const accountStatus = await composioClient.getConnectedAccount(connectedAccountId);
|
||||
composioAccountsRepo.updateAccountStatus(toolkitSlug, accountStatus.status);
|
||||
|
||||
if (accountStatus.status === 'ACTIVE') {
|
||||
emitComposioEvent({ toolkitSlug, success: true });
|
||||
} else {
|
||||
emitComposioEvent({
|
||||
toolkitSlug,
|
||||
success: false,
|
||||
error: `Connection status: ${accountStatus.status}`,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Composio] Failed to sync account status:', error);
|
||||
emitComposioEvent({
|
||||
toolkitSlug,
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
} finally {
|
||||
activeFlows.delete(flowKey);
|
||||
server.close();
|
||||
clearTimeout(cleanupTimeout);
|
||||
}
|
||||
});
|
||||
|
||||
// Timeout for abandoned flows (5 minutes)
|
||||
cleanupTimeout = setTimeout(() => {
|
||||
if (activeFlows.has(flowKey)) {
|
||||
console.log(`[Composio] Cleaning up abandoned flow for ${toolkitSlug}`);
|
||||
activeFlows.delete(flowKey);
|
||||
server.close();
|
||||
emitComposioEvent({
|
||||
toolkitSlug,
|
||||
success: false,
|
||||
error: 'OAuth flow timed out',
|
||||
});
|
||||
}
|
||||
}, 5 * 60 * 1000);
|
||||
|
||||
// Open browser for OAuth
|
||||
shell.openExternal(redirectUrl);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
redirectUrl,
|
||||
connectedAccountId,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[Composio] Connection initiation failed:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get connection status for a toolkit
|
||||
*/
|
||||
export async function getConnectionStatus(toolkitSlug: string): Promise<{
|
||||
isConnected: boolean;
|
||||
status?: string;
|
||||
}> {
|
||||
const account = composioAccountsRepo.getAccount(toolkitSlug);
|
||||
if (!account) {
|
||||
return { isConnected: false };
|
||||
}
|
||||
return {
|
||||
isConnected: account.status === 'ACTIVE',
|
||||
status: account.status,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync connection status with Composio API
|
||||
*/
|
||||
export async function syncConnection(
|
||||
toolkitSlug: string,
|
||||
connectedAccountId: string
|
||||
): Promise<{ status: string }> {
|
||||
try {
|
||||
const accountStatus = await composioClient.getConnectedAccount(connectedAccountId);
|
||||
composioAccountsRepo.updateAccountStatus(toolkitSlug, accountStatus.status);
|
||||
return { status: accountStatus.status };
|
||||
} catch (error) {
|
||||
console.error('[Composio] Failed to sync connection:', error);
|
||||
return { status: 'FAILED' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect a toolkit
|
||||
*/
|
||||
export async function disconnect(toolkitSlug: string): Promise<{ success: boolean }> {
|
||||
try {
|
||||
const account = composioAccountsRepo.getAccount(toolkitSlug);
|
||||
if (account) {
|
||||
// Delete from Composio
|
||||
await composioClient.deleteConnectedAccount(account.id);
|
||||
// Delete local record
|
||||
composioAccountsRepo.deleteAccount(toolkitSlug);
|
||||
}
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[Composio] Disconnect failed:', error);
|
||||
// Still delete local record even if API call fails
|
||||
composioAccountsRepo.deleteAccount(toolkitSlug);
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List connected toolkits
|
||||
*/
|
||||
export function listConnected(): { toolkits: string[] } {
|
||||
return { toolkits: composioAccountsRepo.getConnectedToolkits() };
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a Composio action
|
||||
*/
|
||||
export async function executeAction(
|
||||
actionSlug: string,
|
||||
toolkitSlug: string,
|
||||
input: Record<string, unknown>
|
||||
): Promise<{ success: boolean; data: unknown; error?: string }> {
|
||||
try {
|
||||
const account = composioAccountsRepo.getAccount(toolkitSlug);
|
||||
if (!account || account.status !== 'ACTIVE') {
|
||||
return {
|
||||
success: false,
|
||||
data: null,
|
||||
error: `Toolkit ${toolkitSlug} is not connected`,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await composioClient.executeAction(actionSlug, account.id, input);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('[Composio] Action execution failed:', error);
|
||||
return {
|
||||
success: false,
|
||||
data: null,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -15,11 +15,12 @@ import { bus } from '@x/core/dist/runs/bus.js';
|
|||
import type { FSWatcher } from 'chokidar';
|
||||
import fs from 'node:fs/promises';
|
||||
import z from 'zod';
|
||||
import { RunEvent } from 'packages/shared/dist/runs.js';
|
||||
import { RunEvent } from '@x/shared/dist/runs.js';
|
||||
import container from '@x/core/dist/di/container.js';
|
||||
import { IGranolaConfigRepo } from '@x/core/dist/knowledge/granola/repo.js';
|
||||
import { triggerSync as triggerGranolaSync } from '@x/core/dist/knowledge/granola/sync.js';
|
||||
import { isOnboardingComplete, markOnboardingComplete } from '@x/core/dist/config/note_creation_config.js';
|
||||
import * as composioHandler from './composio-handler.js';
|
||||
|
||||
type InvokeChannels = ipc.InvokeChannels;
|
||||
type IPCChannels = ipc.IPCChannels;
|
||||
|
|
@ -344,5 +345,30 @@ export function setupIpcHandlers() {
|
|||
markOnboardingComplete();
|
||||
return { success: true };
|
||||
},
|
||||
// Composio integration handlers
|
||||
'composio:is-configured': async () => {
|
||||
return composioHandler.isConfigured();
|
||||
},
|
||||
'composio:set-api-key': async (_event, args) => {
|
||||
return composioHandler.setApiKey(args.apiKey);
|
||||
},
|
||||
'composio:initiate-connection': async (_event, args) => {
|
||||
return composioHandler.initiateConnection(args.toolkitSlug);
|
||||
},
|
||||
'composio:get-connection-status': async (_event, args) => {
|
||||
return composioHandler.getConnectionStatus(args.toolkitSlug);
|
||||
},
|
||||
'composio:sync-connection': async (_event, args) => {
|
||||
return composioHandler.syncConnection(args.toolkitSlug, args.connectedAccountId);
|
||||
},
|
||||
'composio:disconnect': async (_event, args) => {
|
||||
return composioHandler.disconnect(args.toolkitSlug);
|
||||
},
|
||||
'composio:list-connected': async () => {
|
||||
return composioHandler.listConnected();
|
||||
},
|
||||
'composio:execute-action': async (_event, args) => {
|
||||
return composioHandler.executeAction(args.actionSlug, args.toolkitSlug, args.input);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
|
||||
interface ComposioApiKeyModalProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onSubmit: (apiKey: string) => void
|
||||
isSubmitting?: boolean
|
||||
}
|
||||
|
||||
export function ComposioApiKeyModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSubmit,
|
||||
isSubmitting = false,
|
||||
}: ComposioApiKeyModalProps) {
|
||||
const [apiKey, setApiKey] = useState("")
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setApiKey("")
|
||||
}
|
||||
}, [open])
|
||||
|
||||
const trimmedApiKey = apiKey.trim()
|
||||
const isValid = trimmedApiKey.length > 0
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!isValid || isSubmitting) return
|
||||
onSubmit(trimmedApiKey)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Enter Composio API Key</DialogTitle>
|
||||
<DialogDescription>
|
||||
Get your API key from{" "}
|
||||
<a
|
||||
href="https://app.composio.dev/settings"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary underline underline-offset-2"
|
||||
>
|
||||
app.composio.dev/settings
|
||||
</a>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-muted-foreground" htmlFor="composio-api-key">
|
||||
API Key
|
||||
</label>
|
||||
<Input
|
||||
id="composio-api-key"
|
||||
type="password"
|
||||
placeholder="Enter your Composio API key"
|
||||
value={apiKey}
|
||||
onChange={(event) => setApiKey(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault()
|
||||
handleSubmit()
|
||||
}
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 flex justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} disabled={!isValid || isSubmitting}>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import * as React from "react"
|
||||
import { useState, useEffect, useCallback } from "react"
|
||||
import { Loader2, Mic, Mail } from "lucide-react"
|
||||
import { Loader2, Mic, Mail, MessageSquare } from "lucide-react"
|
||||
|
||||
import {
|
||||
Popover,
|
||||
|
|
@ -18,6 +18,7 @@ import { Button } from "@/components/ui/button"
|
|||
import { Switch } from "@/components/ui/switch"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { GoogleClientIdModal } from "@/components/google-client-id-modal"
|
||||
import { ComposioApiKeyModal } from "@/components/composio-api-key-modal"
|
||||
import { getGoogleClientId, setGoogleClientId, clearGoogleClientId } from "@/lib/google-client-id-store"
|
||||
import { toast } from "sonner"
|
||||
|
||||
|
|
@ -43,6 +44,12 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps)
|
|||
const [granolaEnabled, setGranolaEnabled] = useState(false)
|
||||
const [granolaLoading, setGranolaLoading] = useState(true)
|
||||
|
||||
// Composio/Slack state
|
||||
const [composioApiKeyOpen, setComposioApiKeyOpen] = useState(false)
|
||||
const [slackConnected, setSlackConnected] = useState(false)
|
||||
const [slackLoading, setSlackLoading] = useState(true)
|
||||
const [slackConnecting, setSlackConnecting] = useState(false)
|
||||
|
||||
// Load available providers on mount
|
||||
useEffect(() => {
|
||||
async function loadProviders() {
|
||||
|
|
@ -89,11 +96,89 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps)
|
|||
}
|
||||
}, [])
|
||||
|
||||
// Load Slack connection status
|
||||
const refreshSlackStatus = useCallback(async () => {
|
||||
try {
|
||||
setSlackLoading(true)
|
||||
const result = await window.ipc.invoke('composio:get-connection-status', { toolkitSlug: 'slack' })
|
||||
setSlackConnected(result.isConnected)
|
||||
} catch (error) {
|
||||
console.error('Failed to load Slack status:', error)
|
||||
setSlackConnected(false)
|
||||
} finally {
|
||||
setSlackLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Connect to Slack via Composio
|
||||
const startSlackConnect = useCallback(async () => {
|
||||
try {
|
||||
setSlackConnecting(true)
|
||||
const result = await window.ipc.invoke('composio:initiate-connection', { toolkitSlug: 'slack' })
|
||||
if (!result.success) {
|
||||
toast.error(result.error || 'Failed to connect to Slack')
|
||||
setSlackConnecting(false)
|
||||
}
|
||||
// Success will be handled by composio:didConnect event
|
||||
} catch (error) {
|
||||
console.error('Failed to connect to Slack:', error)
|
||||
toast.error('Failed to connect to Slack')
|
||||
setSlackConnecting(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Handle Slack connect button click
|
||||
const handleConnectSlack = useCallback(async () => {
|
||||
// Check if Composio is configured
|
||||
const configResult = await window.ipc.invoke('composio:is-configured', null)
|
||||
if (!configResult.configured) {
|
||||
setComposioApiKeyOpen(true)
|
||||
return
|
||||
}
|
||||
await startSlackConnect()
|
||||
}, [startSlackConnect])
|
||||
|
||||
// Handle Composio API key submission
|
||||
const handleComposioApiKeySubmit = useCallback(async (apiKey: string) => {
|
||||
try {
|
||||
await window.ipc.invoke('composio:set-api-key', { apiKey })
|
||||
setComposioApiKeyOpen(false)
|
||||
toast.success('Composio API key saved')
|
||||
// Now start the Slack connection
|
||||
await startSlackConnect()
|
||||
} catch (error) {
|
||||
console.error('Failed to save Composio API key:', error)
|
||||
toast.error('Failed to save API key')
|
||||
}
|
||||
}, [startSlackConnect])
|
||||
|
||||
// Disconnect from Slack
|
||||
const handleDisconnectSlack = useCallback(async () => {
|
||||
try {
|
||||
setSlackLoading(true)
|
||||
const result = await window.ipc.invoke('composio:disconnect', { toolkitSlug: 'slack' })
|
||||
if (result.success) {
|
||||
setSlackConnected(false)
|
||||
toast.success('Disconnected from Slack')
|
||||
} else {
|
||||
toast.error('Failed to disconnect from Slack')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to disconnect from Slack:', error)
|
||||
toast.error('Failed to disconnect from Slack')
|
||||
} finally {
|
||||
setSlackLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Check connection status for all providers
|
||||
const refreshAllStatuses = useCallback(async () => {
|
||||
// Refresh Granola
|
||||
refreshGranolaConfig()
|
||||
|
||||
// Refresh Slack status
|
||||
refreshSlackStatus()
|
||||
|
||||
// Refresh OAuth providers
|
||||
if (providers.length === 0) return
|
||||
|
||||
|
|
@ -120,7 +205,7 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps)
|
|||
)
|
||||
|
||||
setProviderStates(newStates)
|
||||
}, [providers, refreshGranolaConfig])
|
||||
}, [providers, refreshGranolaConfig, refreshSlackStatus])
|
||||
|
||||
// Refresh statuses when popover opens or providers list changes
|
||||
useEffect(() => {
|
||||
|
|
@ -164,6 +249,26 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps)
|
|||
return cleanup
|
||||
}, [refreshAllStatuses])
|
||||
|
||||
// Listen for Composio connection events
|
||||
useEffect(() => {
|
||||
const cleanup = window.ipc.on('composio:didConnect', (event) => {
|
||||
const { toolkitSlug, success, error } = event
|
||||
|
||||
if (toolkitSlug === 'slack') {
|
||||
setSlackConnected(success)
|
||||
setSlackConnecting(false)
|
||||
|
||||
if (success) {
|
||||
toast.success('Connected to Slack')
|
||||
} else {
|
||||
toast.error(error || 'Failed to connect to Slack')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return cleanup
|
||||
}, [])
|
||||
|
||||
const startConnect = useCallback(async (provider: string, clientId?: string) => {
|
||||
setProviderStates(prev => ({
|
||||
...prev,
|
||||
|
|
@ -401,11 +506,71 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps)
|
|||
|
||||
{/* Fireflies */}
|
||||
{providers.includes('fireflies-ai') && renderOAuthProvider('fireflies-ai', 'Fireflies', <Mic className="size-4" />, 'AI meeting transcripts')}
|
||||
|
||||
<Separator className="my-2" />
|
||||
|
||||
{/* Team Communication Section - Slack */}
|
||||
<div className="px-2 py-1.5">
|
||||
<span className="text-xs font-medium text-muted-foreground">Team Communication</span>
|
||||
</div>
|
||||
|
||||
{/* Slack */}
|
||||
<div className="flex items-center justify-between gap-3 rounded-md px-3 py-2 hover:bg-accent">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div className="flex size-8 items-center justify-center rounded-md bg-muted">
|
||||
<MessageSquare className="size-4" />
|
||||
</div>
|
||||
<div className="flex flex-col min-w-0">
|
||||
<span className="text-sm font-medium truncate">Slack</span>
|
||||
{slackLoading ? (
|
||||
<span className="text-xs text-muted-foreground">Checking...</span>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground truncate">
|
||||
Send messages and view channels
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
{slackLoading ? (
|
||||
<Loader2 className="size-4 animate-spin text-muted-foreground" />
|
||||
) : slackConnected ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleDisconnectSlack}
|
||||
className="h-7 px-2 text-xs"
|
||||
>
|
||||
Disconnect
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={handleConnectSlack}
|
||||
disabled={slackConnecting}
|
||||
className="h-7 px-2 text-xs"
|
||||
>
|
||||
{slackConnecting ? (
|
||||
<Loader2 className="size-3 animate-spin" />
|
||||
) : (
|
||||
"Connect"
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<ComposioApiKeyModal
|
||||
open={composioApiKeyOpen}
|
||||
onOpenChange={setComposioApiKeyOpen}
|
||||
onSubmit={handleComposioApiKeySubmit}
|
||||
isSubmitting={slackConnecting}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import * as React from "react"
|
||||
import { useState, useEffect, useCallback } from "react"
|
||||
import { Loader2, Mic, Mail, CheckCircle2, Sailboat } from "lucide-react"
|
||||
import { Loader2, Mic, Mail, CheckCircle2, Sailboat, MessageSquare } from "lucide-react"
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
|
|
@ -15,6 +15,7 @@ import { Button } from "@/components/ui/button"
|
|||
import { Switch } from "@/components/ui/switch"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { GoogleClientIdModal } from "@/components/google-client-id-modal"
|
||||
import { ComposioApiKeyModal } from "@/components/composio-api-key-modal"
|
||||
import { getGoogleClientId, setGoogleClientId } from "@/lib/google-client-id-store"
|
||||
import { toast } from "sonner"
|
||||
|
||||
|
|
@ -44,6 +45,12 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
|
|||
const [granolaEnabled, setGranolaEnabled] = useState(false)
|
||||
const [granolaLoading, setGranolaLoading] = useState(true)
|
||||
|
||||
// Composio/Slack state
|
||||
const [composioApiKeyOpen, setComposioApiKeyOpen] = useState(false)
|
||||
const [slackConnected, setSlackConnected] = useState(false)
|
||||
const [slackLoading, setSlackLoading] = useState(true)
|
||||
const [slackConnecting, setSlackConnecting] = useState(false)
|
||||
|
||||
// Track connected providers for the completion step
|
||||
const connectedProviders = Object.entries(providerStates)
|
||||
.filter(([, state]) => state.isConnected)
|
||||
|
|
@ -97,11 +104,70 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
|
|||
}
|
||||
}, [])
|
||||
|
||||
// Load Slack connection status
|
||||
const refreshSlackStatus = useCallback(async () => {
|
||||
try {
|
||||
setSlackLoading(true)
|
||||
const result = await window.ipc.invoke('composio:get-connection-status', { toolkitSlug: 'slack' })
|
||||
setSlackConnected(result.isConnected)
|
||||
} catch (error) {
|
||||
console.error('Failed to load Slack status:', error)
|
||||
setSlackConnected(false)
|
||||
} finally {
|
||||
setSlackLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Start Slack connection
|
||||
const startSlackConnect = useCallback(async () => {
|
||||
try {
|
||||
setSlackConnecting(true)
|
||||
const result = await window.ipc.invoke('composio:initiate-connection', { toolkitSlug: 'slack' })
|
||||
if (!result.success) {
|
||||
toast.error(result.error || 'Failed to connect to Slack')
|
||||
setSlackConnecting(false)
|
||||
}
|
||||
// Success will be handled by composio:didConnect event
|
||||
} catch (error) {
|
||||
console.error('Failed to connect to Slack:', error)
|
||||
toast.error('Failed to connect to Slack')
|
||||
setSlackConnecting(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Connect to Slack via Composio (checks if configured first)
|
||||
const handleConnectSlack = useCallback(async () => {
|
||||
// Check if Composio is configured
|
||||
const configResult = await window.ipc.invoke('composio:is-configured', null)
|
||||
if (!configResult.configured) {
|
||||
setComposioApiKeyOpen(true)
|
||||
return
|
||||
}
|
||||
await startSlackConnect()
|
||||
}, [startSlackConnect])
|
||||
|
||||
// Handle Composio API key submission
|
||||
const handleComposioApiKeySubmit = useCallback(async (apiKey: string) => {
|
||||
try {
|
||||
await window.ipc.invoke('composio:set-api-key', { apiKey })
|
||||
setComposioApiKeyOpen(false)
|
||||
toast.success('Composio API key saved')
|
||||
// Now start the Slack connection
|
||||
await startSlackConnect()
|
||||
} catch (error) {
|
||||
console.error('Failed to save Composio API key:', error)
|
||||
toast.error('Failed to save API key')
|
||||
}
|
||||
}, [startSlackConnect])
|
||||
|
||||
// Check connection status for all providers
|
||||
const refreshAllStatuses = useCallback(async () => {
|
||||
// Refresh Granola
|
||||
refreshGranolaConfig()
|
||||
|
||||
// Refresh Slack status
|
||||
refreshSlackStatus()
|
||||
|
||||
// Refresh OAuth providers
|
||||
if (providers.length === 0) return
|
||||
|
||||
|
|
@ -128,7 +194,7 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
|
|||
)
|
||||
|
||||
setProviderStates(newStates)
|
||||
}, [providers, refreshGranolaConfig])
|
||||
}, [providers, refreshGranolaConfig, refreshSlackStatus])
|
||||
|
||||
// Refresh statuses when modal opens or providers list changes
|
||||
useEffect(() => {
|
||||
|
|
@ -162,6 +228,26 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
|
|||
return cleanup
|
||||
}, [])
|
||||
|
||||
// Listen for Composio connection events
|
||||
useEffect(() => {
|
||||
const cleanup = window.ipc.on('composio:didConnect', (event) => {
|
||||
const { toolkitSlug, success, error } = event
|
||||
|
||||
if (toolkitSlug === 'slack') {
|
||||
setSlackConnected(success)
|
||||
setSlackConnecting(false)
|
||||
|
||||
if (success) {
|
||||
toast.success('Connected to Slack')
|
||||
} else {
|
||||
toast.error(error || 'Failed to connect to Slack')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return cleanup
|
||||
}, [])
|
||||
|
||||
const startConnect = useCallback(async (provider: string, clientId?: string) => {
|
||||
setProviderStates(prev => ({
|
||||
...prev,
|
||||
|
|
@ -314,6 +400,50 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
|
|||
</div>
|
||||
)
|
||||
|
||||
// Render Slack row
|
||||
const renderSlackRow = () => (
|
||||
<div className="flex items-center justify-between gap-3 rounded-md px-3 py-3 hover:bg-accent">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div className="flex size-10 items-center justify-center rounded-md bg-muted">
|
||||
<MessageSquare className="size-5" />
|
||||
</div>
|
||||
<div className="flex flex-col min-w-0">
|
||||
<span className="text-sm font-medium truncate">Slack</span>
|
||||
{slackLoading ? (
|
||||
<span className="text-xs text-muted-foreground">Checking...</span>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground truncate">
|
||||
Send messages and view channels
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
{slackLoading ? (
|
||||
<Loader2 className="size-4 animate-spin text-muted-foreground" />
|
||||
) : slackConnected ? (
|
||||
<div className="flex items-center gap-1.5 text-sm text-green-600">
|
||||
<CheckCircle2 className="size-4" />
|
||||
<span>Connected</span>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={handleConnectSlack}
|
||||
disabled={slackConnecting}
|
||||
>
|
||||
{slackConnecting ? (
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
) : (
|
||||
"Connect"
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
// Step 0: Welcome
|
||||
const WelcomeStep = () => (
|
||||
<div className="flex flex-col items-center text-center">
|
||||
|
|
@ -381,6 +511,14 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
|
|||
{renderGranolaRow()}
|
||||
{providers.includes('fireflies-ai') && renderOAuthProvider('fireflies-ai', 'Fireflies', <Mic className="size-5" />, 'AI meeting transcripts')}
|
||||
</div>
|
||||
|
||||
{/* Team Communication Section */}
|
||||
<div className="space-y-2">
|
||||
<div className="px-3">
|
||||
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Team Communication</span>
|
||||
</div>
|
||||
{renderSlackRow()}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -398,7 +536,7 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
|
|||
|
||||
// Step 2: Completion
|
||||
const CompletionStep = () => {
|
||||
const hasConnections = connectedProviders.length > 0 || granolaEnabled
|
||||
const hasConnections = connectedProviders.length > 0 || granolaEnabled || slackConnected
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center text-center">
|
||||
|
|
@ -439,6 +577,12 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
|
|||
<span>Granola (Local meeting notes)</span>
|
||||
</div>
|
||||
)}
|
||||
{slackConnected && (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<CheckCircle2 className="size-4 text-green-600" />
|
||||
<span>Slack (Team communication)</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -459,6 +603,12 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
|
|||
onSubmit={handleGoogleClientIdSubmit}
|
||||
isSubmitting={providerStates.google?.isConnecting ?? false}
|
||||
/>
|
||||
<ComposioApiKeyModal
|
||||
open={composioApiKeyOpen}
|
||||
onOpenChange={setComposioApiKeyOpen}
|
||||
onSubmit={handleComposioApiKeySubmit}
|
||||
isSubmitting={slackConnecting}
|
||||
/>
|
||||
<Dialog open={open} onOpenChange={() => {}}>
|
||||
<DialogContent
|
||||
className="w-[60vw] max-w-3xl max-h-[80vh] overflow-y-auto"
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
"@ai-sdk/anthropic": "^2.0.44",
|
||||
"@ai-sdk/google": "^2.0.25",
|
||||
"@ai-sdk/openai": "^2.0.53",
|
||||
"@composio/core": "^0.1.48",
|
||||
"@ai-sdk/openai-compatible": "^1.0.27",
|
||||
"@ai-sdk/provider": "^2.0.0",
|
||||
"@google-cloud/local-auth": "^3.0.1",
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ Rowboat is an agentic assistant for everyday work - emails, meetings, projects,
|
|||
|
||||
**Document Collaboration:** When users ask you to work on a document, collaborate on writing, create a new document, edit/refine existing notes, or say things like "let's work on [X]", "help me write [X]", "create a doc for [X]", or "let's draft [X]", you MUST load the \`doc-collab\` skill first. This is required for any document creation or editing task. The skill provides structured guidance for creating, editing, and refining documents in the knowledge base.
|
||||
|
||||
**Slack:** When users ask about Slack messages, want to send messages to teammates, check channel conversations, or find someone on Slack, load the \`slack\` skill. You can send messages, view channel history, search conversations, and find users. Always check if Slack is connected first with \`slack-checkConnection\`, and always show message drafts to the user before sending.
|
||||
|
||||
## Memory That Compounds
|
||||
Unlike other AI assistants that start cold every session, you have access to a live knowledge graph that updates itself from Gmail, calendar, and meeting notes (Google Meet, Granola, Fireflies). This isn't just summaries - it's structured extraction of decisions, commitments, open questions, and context, routed to long-lived notes for each person, project, and topic.
|
||||
|
||||
|
|
@ -163,6 +165,7 @@ When a user asks for ANY task that might require external capabilities (web sear
|
|||
- \`analyzeAgent\` - Agent analysis
|
||||
- \`addMcpServer\`, \`listMcpServers\`, \`listMcpTools\`, \`executeMcpTool\` - MCP server management and execution
|
||||
- \`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.
|
||||
|
||||
These tools work directly and are NOT filtered by \`.rowboat/config/security.json\`.
|
||||
|
||||
|
|
|
|||
|
|
@ -7,17 +7,16 @@ import draftEmailsSkill from "./draft-emails/skill.js";
|
|||
import mcpIntegrationSkill from "./mcp-integration/skill.js";
|
||||
import meetingPrepSkill from "./meeting-prep/skill.js";
|
||||
import organizeFilesSkill from "./organize-files/skill.js";
|
||||
import slackSkill from "./slack/skill.js";
|
||||
import workflowAuthoringSkill from "./workflow-authoring/skill.js";
|
||||
import workflowRunOpsSkill from "./workflow-run-ops/skill.js";
|
||||
|
||||
const CURRENT_FILE = fileURLToPath(import.meta.url);
|
||||
const CURRENT_DIR = path.dirname(CURRENT_FILE);
|
||||
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
||||
const CATALOG_PREFIX = "src/application/assistant/skills";
|
||||
|
||||
type SkillDefinition = {
|
||||
id: string;
|
||||
id: string; // Also used as folder name
|
||||
title: string;
|
||||
folder: string;
|
||||
summary: string;
|
||||
content: string;
|
||||
};
|
||||
|
|
@ -32,63 +31,60 @@ const definitions: SkillDefinition[] = [
|
|||
{
|
||||
id: "doc-collab",
|
||||
title: "Document Collaboration",
|
||||
folder: "doc-collab",
|
||||
summary: "Collaborate on documents - create, edit, and refine notes and documents in the knowledge base.",
|
||||
content: docCollabSkill,
|
||||
},
|
||||
{
|
||||
id: "draft-emails",
|
||||
title: "Draft Emails",
|
||||
folder: "draft-emails",
|
||||
summary: "Process incoming emails and create draft responses using calendar and knowledge base for context.",
|
||||
content: draftEmailsSkill,
|
||||
},
|
||||
{
|
||||
id: "meeting-prep",
|
||||
title: "Meeting Prep",
|
||||
folder: "meeting-prep",
|
||||
summary: "Prepare for meetings by gathering context about attendees from the knowledge base.",
|
||||
content: meetingPrepSkill,
|
||||
},
|
||||
{
|
||||
id: "organize-files",
|
||||
title: "Organize Files",
|
||||
folder: "organize-files",
|
||||
summary: "Find, organize, and tidy up files on the user's machine. Move files to folders, clean up Desktop/Downloads, locate specific files.",
|
||||
content: organizeFilesSkill,
|
||||
},
|
||||
{
|
||||
id: "slack",
|
||||
title: "Slack Integration",
|
||||
summary: "Send Slack messages, view channel history, search conversations, find users, and manage team communication.",
|
||||
content: slackSkill,
|
||||
},
|
||||
{
|
||||
id: "workflow-authoring",
|
||||
title: "Workflow Authoring",
|
||||
folder: "workflow-authoring",
|
||||
summary: "Creating or editing workflows/agents, validating schema rules, and keeping filenames aligned with JSON ids.",
|
||||
content: workflowAuthoringSkill,
|
||||
},
|
||||
{
|
||||
id: "builtin-tools",
|
||||
title: "Builtin Tools Reference",
|
||||
folder: "builtin-tools",
|
||||
summary: "Understanding and using builtin tools (especially executeCommand for bash/shell) in agent definitions.",
|
||||
content: builtinToolsSkill,
|
||||
},
|
||||
{
|
||||
id: "mcp-integration",
|
||||
title: "MCP Integration Guidance",
|
||||
folder: "mcp-integration",
|
||||
summary: "Discovering, executing, and integrating MCP tools. Use this to check what external capabilities are available and execute MCP tools on behalf of users.",
|
||||
content: mcpIntegrationSkill,
|
||||
},
|
||||
{
|
||||
id: "deletion-guardrails",
|
||||
title: "Deletion Guardrails",
|
||||
folder: "deletion-guardrails",
|
||||
summary: "Following the confirmation process before removing workflows or agents and their dependencies.",
|
||||
content: deletionGuardrailsSkill,
|
||||
},
|
||||
{
|
||||
id: "workflow-run-ops",
|
||||
title: "Workflow Run Operations",
|
||||
folder: "workflow-run-ops",
|
||||
summary: "Commands that list workflow runs, inspect paused executions, or manage cron schedules for workflows.",
|
||||
content: workflowRunOpsSkill,
|
||||
},
|
||||
|
|
@ -96,7 +92,7 @@ const definitions: SkillDefinition[] = [
|
|||
|
||||
const skillEntries = definitions.map((definition) => ({
|
||||
...definition,
|
||||
catalogPath: `${CATALOG_PREFIX}/${definition.folder}/skill.ts`,
|
||||
catalogPath: `${CATALOG_PREFIX}/${definition.id}/skill.ts`,
|
||||
}));
|
||||
|
||||
const catalogSections = skillEntries.map((entry) => [
|
||||
|
|
@ -146,8 +142,8 @@ const registerAliasVariants = (alias: string, entry: ResolvedSkill) => {
|
|||
};
|
||||
|
||||
for (const entry of skillEntries) {
|
||||
const absoluteTs = path.join(CURRENT_DIR, entry.folder, "skill.ts");
|
||||
const absoluteJs = path.join(CURRENT_DIR, entry.folder, "skill.js");
|
||||
const absoluteTs = path.join(CURRENT_DIR, entry.id, "skill.ts");
|
||||
const absoluteJs = path.join(CURRENT_DIR, entry.id, "skill.js");
|
||||
const resolvedEntry: ResolvedSkill = {
|
||||
id: entry.id,
|
||||
catalogPath: entry.catalogPath,
|
||||
|
|
@ -156,14 +152,13 @@ for (const entry of skillEntries) {
|
|||
|
||||
const baseAliases = [
|
||||
entry.id,
|
||||
entry.folder,
|
||||
`${entry.folder}/skill`,
|
||||
`${entry.folder}/skill.ts`,
|
||||
`${entry.folder}/skill.js`,
|
||||
`skills/${entry.folder}/skill.ts`,
|
||||
`skills/${entry.folder}/skill.js`,
|
||||
`${CATALOG_PREFIX}/${entry.folder}/skill.ts`,
|
||||
`${CATALOG_PREFIX}/${entry.folder}/skill.js`,
|
||||
`${entry.id}/skill`,
|
||||
`${entry.id}/skill.ts`,
|
||||
`${entry.id}/skill.js`,
|
||||
`skills/${entry.id}/skill.ts`,
|
||||
`skills/${entry.id}/skill.js`,
|
||||
`${CATALOG_PREFIX}/${entry.id}/skill.ts`,
|
||||
`${CATALOG_PREFIX}/${entry.id}/skill.js`,
|
||||
absoluteTs,
|
||||
absoluteJs,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,121 @@
|
|||
import { slackToolCatalogMarkdown } from "./tool-catalog.js";
|
||||
|
||||
const skill = String.raw`
|
||||
# Slack Integration Skill
|
||||
|
||||
You can interact with Slack to help users communicate with their team. This includes sending messages, viewing channel history, finding users, and searching conversations.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before using Slack tools, ALWAYS check if Slack is connected:
|
||||
\`\`\`
|
||||
slack-checkConnection({})
|
||||
\`\`\`
|
||||
|
||||
If not connected, inform the user they need to connect Slack from the settings/onboarding.
|
||||
|
||||
## Available Tools
|
||||
|
||||
### Check Connection
|
||||
\`\`\`
|
||||
slack-checkConnection({})
|
||||
\`\`\`
|
||||
Returns whether Slack is connected and ready to use.
|
||||
|
||||
### List Users
|
||||
\`\`\`
|
||||
slack-listUsers({ limit: 100 })
|
||||
\`\`\`
|
||||
Lists users in the workspace. Use this to resolve a name to a user ID.
|
||||
|
||||
### List DM Conversations
|
||||
\`\`\`
|
||||
slack-getDirectMessages({ limit: 50 })
|
||||
\`\`\`
|
||||
Lists DM channels (type "im"). Each entry includes the DM channel ID and the user ID.
|
||||
|
||||
### List Channels
|
||||
\`\`\`
|
||||
slack-listChannels({ types: "public_channel,private_channel", limit: 100 })
|
||||
\`\`\`
|
||||
Lists channels the user has access to.
|
||||
|
||||
### Get Conversation History
|
||||
\`\`\`
|
||||
slack-getChannelHistory({ channel: "C01234567", limit: 20 })
|
||||
\`\`\`
|
||||
Fetches recent messages for a channel or DM.
|
||||
|
||||
### Search Messages
|
||||
\`\`\`
|
||||
slack-searchMessages({ query: "in:@username", count: 20 })
|
||||
\`\`\`
|
||||
Searches Slack messages using Slack search syntax.
|
||||
|
||||
### Send a Message
|
||||
\`\`\`
|
||||
slack-sendMessage({ channel: "C01234567", text: "Hello team!" })
|
||||
\`\`\`
|
||||
Sends a message to a channel or DM. Always show the draft first.
|
||||
|
||||
### Execute a Slack Action
|
||||
\`\`\`
|
||||
slack-executeAction({
|
||||
toolSlug: "EXACT_TOOL_SLUG_FROM_DISCOVERY",
|
||||
input: { /* tool-specific parameters */ }
|
||||
})
|
||||
\`\`\`
|
||||
Executes any Slack tool using its exact slug discovered from \`slack-listAvailableTools\`.
|
||||
|
||||
### Discover Available Tools (Fallback)
|
||||
\`\`\`
|
||||
slack-listAvailableTools({ search: "conversation" })
|
||||
\`\`\`
|
||||
Lists available Slack tools from Composio. Use this only if a builtin Slack tool fails and you need a specific slug.
|
||||
|
||||
## Composio Slack Tool Catalog (Pinned)
|
||||
Use the exact tool slugs below with \`slack-executeAction\` when needed. Prefer these over \`slack-listAvailableTools\` to avoid redundant discovery.
|
||||
|
||||
${slackToolCatalogMarkdown}
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Check Connection
|
||||
\`\`\`
|
||||
slack-checkConnection({})
|
||||
\`\`\`
|
||||
|
||||
### Step 2: Choose the Builtin Tool
|
||||
Use the builtin Slack tools above for common tasks. Only fall back to \`slack-listAvailableTools\` + \`slack-executeAction\` if something is missing.
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Find the Most Recent DM with Someone
|
||||
1. Search messages first: \`slack-searchMessages({ query: "in:@Name", count: 1 })\`
|
||||
2. If you need exact DM history:
|
||||
- \`slack-listUsers({})\` to find the user ID
|
||||
- \`slack-getDirectMessages({})\` to find the DM channel for that user
|
||||
- \`slack-getChannelHistory({ channel: "D...", limit: 20 })\`
|
||||
|
||||
### Send a Message
|
||||
1. Draft the message and show it to the user
|
||||
2. ONLY after user approval, send using \`slack-sendMessage\`
|
||||
|
||||
### Search Messages
|
||||
1. Use \`slack-searchMessages({ query: "...", count: 20 })\`
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Always show drafts before sending** - Never send Slack messages without user confirmation
|
||||
- **Summarize, don't dump** - When showing channel history, summarize the key points
|
||||
- **Cross-reference with knowledge base** - Check if mentioned people have notes in the knowledge base
|
||||
|
||||
## Error Handling
|
||||
|
||||
If a Slack operation fails:
|
||||
1. Try \`slack-listAvailableTools\` to verify the tool slug is correct
|
||||
2. Check if Slack is still connected with \`slack-checkConnection\`
|
||||
3. Inform the user of the specific error
|
||||
`;
|
||||
|
||||
export default skill;
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
export type SlackToolDefinition = {
|
||||
name: string;
|
||||
slug: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export const slackToolCatalog: SlackToolDefinition[] = [
|
||||
{ name: "Add Emoji Alias", slug: "SLACK_ADD_AN_EMOJI_ALIAS_IN_SLACK", description: "Adds an alias for an existing custom emoji." },
|
||||
{ name: "Add Remote File", slug: "SLACK_ADD_A_REMOTE_FILE_FROM_A_SERVICE", description: "Adds a reference to an external file (e.g., GDrive, Dropbox) to Slack." },
|
||||
{ name: "Add Star to Item", slug: "SLACK_ADD_A_STAR_TO_AN_ITEM", description: "Stars a channel, file, comment, or message." },
|
||||
{ name: "Add Call Participants", slug: "SLACK_ADD_CALL_PARTICIPANTS", description: "Registers new participants added to a Slack call." },
|
||||
{ name: "Add Emoji", slug: "SLACK_ADD_EMOJI", description: "Adds a custom emoji to a workspace via a unique name and URL." },
|
||||
{ name: "Add Reaction", slug: "SLACK_ADD_REACTION_TO_AN_ITEM", description: "Adds a specified emoji reaction to a message." },
|
||||
{ name: "Archive Channel", slug: "SLACK_ARCHIVE_A_PUBLIC_OR_PRIVATE_CHANNEL", description: "Archives a public or private channel." },
|
||||
{ name: "Archive Conversation", slug: "SLACK_ARCHIVE_A_SLACK_CONVERSATION", description: "Archives a conversation by its ID." },
|
||||
{ name: "Close DM/MPDM", slug: "SLACK_CLOSE_DM_OR_MULTI_PERSON_DM", description: "Closes a DM or MPDM sidebar view for the user." },
|
||||
{ name: "Create Reminder", slug: "SLACK_CREATE_A_REMINDER", description: "Creates a reminder with text and time (natural language supported)." },
|
||||
{ name: "Create User Group", slug: "SLACK_CREATE_A_SLACK_USER_GROUP", description: "Creates a new user group (subteam)." },
|
||||
{ name: "Create Channel", slug: "SLACK_CREATE_CHANNEL", description: "Initiates a public or private channel conversation." },
|
||||
{ name: "Create Channel Conversation", slug: "SLACK_CREATE_CHANNEL_BASED_CONVERSATION", description: "Creates a new channel with specific org-wide or team settings." },
|
||||
{ name: "Customize URL Unfurl", slug: "SLACK_CUSTOMIZE_URL_UNFURL", description: "Defines custom content for URL previews in a specific message." },
|
||||
{ name: "Delete File Comment", slug: "SLACK_DELETE_A_COMMENT_ON_A_FILE", description: "Deletes a specific comment from a file." },
|
||||
{ name: "Delete File", slug: "SLACK_DELETE_A_FILE_BY_ID", description: "Permanently deletes a file by its ID." },
|
||||
{ name: "Delete Channel", slug: "SLACK_DELETE_A_PUBLIC_OR_PRIVATE_CHANNEL", description: "Irreversibly deletes a channel and its history (Enterprise only)." },
|
||||
{ name: "Delete Scheduled Message", slug: "SLACK_DELETE_A_SCHEDULED_MESSAGE_IN_A_CHAT", description: "Deletes a pending scheduled message." },
|
||||
{ name: "Delete Reminder", slug: "SLACK_DELETE_A_SLACK_REMINDER", description: "Deletes an existing reminder." },
|
||||
{ name: "Delete Message", slug: "SLACK_DELETES_A_MESSAGE_FROM_A_CHAT", description: "Deletes a message by channel ID and timestamp." },
|
||||
{ name: "Delete Profile Photo", slug: "SLACK_DELETE_USER_PROFILE_PHOTO", description: "Reverts the user's profile photo to the default avatar." },
|
||||
{ name: "Disable User Group", slug: "SLACK_DISABLE_AN_EXISTING_SLACK_USER_GROUP", description: "Disables (archives) a user group." },
|
||||
{ name: "Enable User Group", slug: "SLACK_ENABLE_A_SPECIFIED_USER_GROUP", description: "Reactivates a disabled user group." },
|
||||
{ name: "Share File Publicly", slug: "SLACK_ENABLE_PUBLIC_SHARING_OF_A_FILE", description: "Generates a public URL for a file." },
|
||||
{ name: "End Call", slug: "SLACK_END_A_CALL_WITH_DURATION_AND_ID", description: "Ends an ongoing call." },
|
||||
{ name: "End Snooze", slug: "SLACK_END_SNOOZE", description: "Ends the current user's snooze mode immediately." },
|
||||
{ name: "End DND Session", slug: "SLACK_END_USER_DO_NOT_DISTURB_SESSION", description: "Ends the current DND session." },
|
||||
{ name: "Fetch Bot Info", slug: "SLACK_FETCH_BOT_USER_INFORMATION", description: "Fetches metadata for a specific bot user." },
|
||||
{ name: "Fetch History", slug: "SLACK_FETCH_CONVERSATION_HISTORY", description: "Fetches chronological messages and events from a channel." },
|
||||
{ name: "Fetch Item Reactions", slug: "SLACK_FETCH_ITEM_REACTIONS", description: "Fetches all reactions for a message, file, or comment." },
|
||||
{ name: "Retrieve Replies", slug: "SLACK_FETCH_MESSAGE_THREAD_FROM_A_CONVERSATION", description: "Retrieves replies to a specific parent message." },
|
||||
{ name: "Fetch Team Info", slug: "SLACK_FETCH_TEAM_INFO", description: "Fetches comprehensive metadata about the team." },
|
||||
{ name: "Fetch Workspace Settings", slug: "SLACK_FETCH_WORKSPACE_SETTINGS_INFORMATION", description: "Retrieves detailed settings for a specific workspace." },
|
||||
{ name: "Find Channels", slug: "SLACK_FIND_CHANNELS", description: "Searches channels by name, topic, or purpose." },
|
||||
{ name: "Find User by Email", slug: "SLACK_FIND_USER_BY_EMAIL_ADDRESS", description: "Finds a user object using their email address." },
|
||||
{ name: "Find Users", slug: "SLACK_FIND_USERS", description: "Searches users by name, email, or display name." },
|
||||
{ name: "Get Conversation Preferences", slug: "SLACK_GET_CHANNEL_CONVERSATION_PREFERENCES", description: "Retrieves posting/threading preferences for a channel." },
|
||||
{ name: "Get Reminder Info", slug: "SLACK_GET_REMINDER_INFORMATION", description: "Retrieves detailed information for a specific reminder." },
|
||||
{ name: "Get Remote File", slug: "SLACK_GET_REMOTE_FILE", description: "Retrieves info about a previously added remote file." },
|
||||
{ name: "Get Team DND Status", slug: "SLACK_GET_TEAM_DND_STATUS", description: "Retrieves the DND status for specific users." },
|
||||
{ name: "Get User Presence", slug: "SLACK_GET_USER_PRESENCE_INFO", description: "Retrieves real-time presence (active/away)." },
|
||||
{ name: "Invite to Channel", slug: "SLACK_INVITE_USERS_TO_A_SLACK_CHANNEL", description: "Invites users to a channel by their user IDs." },
|
||||
{ name: "Invite to Workspace", slug: "SLACK_INVITE_USER_TO_WORKSPACE", description: "Invites a user to a workspace and channels via email." },
|
||||
{ name: "Join Conversation", slug: "SLACK_JOIN_AN_EXISTING_CONVERSATION", description: "Joins a conversation by channel ID." },
|
||||
{ name: "Leave Conversation", slug: "SLACK_LEAVE_A_CONVERSATION", description: "Leaves a conversation." },
|
||||
{ name: "List All Channels", slug: "SLACK_LIST_ALL_CHANNELS", description: "Lists all conversations with various filters." },
|
||||
{ name: "List All Users", slug: "SLACK_LIST_ALL_USERS", description: "Retrieves a paginated list of all users in the workspace." },
|
||||
{ name: "List User Group Members", slug: "SLACK_LIST_ALL_USERS_IN_A_USER_GROUP", description: "Lists all user IDs within a group." },
|
||||
{ name: "List Conversations", slug: "SLACK_LIST_CONVERSATIONS", description: "Retrieves conversations accessible to a specific user." },
|
||||
{ name: "List Files", slug: "SLACK_LIST_FILES_WITH_FILTERS_IN_SLACK", description: "Lists files and metadata with filtering options." },
|
||||
{ name: "List Reminders", slug: "SLACK_LIST_REMINDERS", description: "Lists all reminders for the authenticated user." },
|
||||
{ name: "List Remote Files", slug: "SLACK_LIST_REMOTE_FILES", description: "Retrieves info about a team's remote files." },
|
||||
{ name: "List Scheduled Messages", slug: "SLACK_LIST_SCHEDULED_MESSAGES", description: "Lists pending scheduled messages." },
|
||||
{ name: "List Pinned Items", slug: "SLACK_LISTS_PINNED_ITEMS_IN_A_CHANNEL", description: "Retrieves all messages/files pinned to a channel." },
|
||||
{ name: "List Starred Items", slug: "SLACK_LIST_STARRED_ITEMS", description: "Lists items starred by the user." },
|
||||
{ name: "List Custom Emojis", slug: "SLACK_LIST_TEAM_CUSTOM_EMOJIS", description: "Lists all workspace custom emojis and their URLs." },
|
||||
{ name: "List User Groups", slug: "SLACK_LIST_USER_GROUPS_FOR_TEAM_WITH_OPTIONS", description: "Lists user-created and default user groups." },
|
||||
{ name: "List User Reactions", slug: "SLACK_LIST_USER_REACTIONS", description: "Lists all reactions added by a specific user." },
|
||||
{ name: "List Admin Users", slug: "SLACK_LIST_WORKSPACE_USERS", description: "Retrieves a paginated list of workspace administrators." },
|
||||
{ name: "Set User Presence", slug: "SLACK_MANUALLY_SET_USER_PRESENCE", description: "Manually overrides automated presence status." },
|
||||
{ name: "Mark Reminder Complete", slug: "SLACK_MARK_REMINDER_AS_COMPLETE", description: "Marks a reminder as complete (deprecated by Slack in March 2023)." },
|
||||
{ name: "Open DM", slug: "SLACK_OPEN_DM", description: "Opens/resumes a DM or MPDM." },
|
||||
{ name: "Pin Item", slug: "SLACK_PINS_AN_ITEM_TO_A_CHANNEL", description: "Pins a message to a channel." },
|
||||
{ name: "Remove Remote File", slug: "SLACK_REMOVE_A_REMOTE_FILE", description: "Removes a reference to an external file." },
|
||||
{ name: "Remove Star", slug: "SLACK_REMOVE_A_STAR_FROM_AN_ITEM", description: "Unstars an item." },
|
||||
{ name: "Remove from Channel", slug: "SLACK_REMOVE_A_USER_FROM_A_CONVERSATION", description: "Removes a specified user from a conversation." },
|
||||
{ name: "Remove Call Participants", slug: "SLACK_REMOVE_CALL_PARTICIPANTS", description: "Registers the removal of participants from a call." },
|
||||
{ name: "Remove Reaction", slug: "SLACK_REMOVE_REACTION_FROM_ITEM", description: "Removes an emoji reaction from an item." },
|
||||
{ name: "Rename Conversation", slug: "SLACK_RENAME_A_CONVERSATION", description: "Renames a channel ID/Conversation." },
|
||||
{ name: "Rename Emoji", slug: "SLACK_RENAME_AN_EMOJI", description: "Renames a custom emoji." },
|
||||
{ name: "Rename Channel", slug: "SLACK_RENAME_A_SLACK_CHANNEL", description: "Renames a public or private channel." },
|
||||
{ name: "Retrieve Identity", slug: "SLACK_RETRIEVE_A_USER_S_IDENTITY_DETAILS", description: "Retrieves basic user/team identity details." },
|
||||
{ name: "Retrieve Call Info", slug: "SLACK_RETRIEVE_CALL_INFORMATION", description: "Retrieves a snapshot of a call's status." },
|
||||
{ name: "Retrieve Conversation Info", slug: "SLACK_RETRIEVE_CONVERSATION_INFORMATION", description: "Retrieves metadata for a specific conversation." },
|
||||
{ name: "Get Conversation Members", slug: "SLACK_RETRIEVE_CONVERSATION_MEMBERS_LIST", description: "Lists active user IDs in a conversation." },
|
||||
{ name: "Retrieve User DND", slug: "SLACK_RETRIEVE_CURRENT_USER_DND_STATUS", description: "Retrieves DND status for a user." },
|
||||
{ name: "Retrieve File Details", slug: "SLACK_RETRIEVE_DETAILED_INFORMATION_ABOUT_A_FILE", description: "Retrieves metadata and comments for a file." },
|
||||
{ name: "Retrieve User Details", slug: "SLACK_RETRIEVE_DETAILED_USER_INFORMATION", description: "Retrieves comprehensive info for a specific user ID." },
|
||||
{ name: "Get Message Permalink", slug: "SLACK_RETRIEVE_MESSAGE_PERMALINK_URL", description: "Gets the permalink URL for a specific message." },
|
||||
{ name: "Retrieve Team Profile", slug: "SLACK_RETRIEVE_TEAM_PROFILE_DETAILS", description: "Retrieves the profile field structure for a team." },
|
||||
{ name: "Retrieve User Profile", slug: "SLACK_RETRIEVE_USER_PROFILE_INFORMATION", description: "Retrieves specific profile info for a user." },
|
||||
{ name: "Revoke Public File", slug: "SLACK_REVOKE_PUBLIC_SHARING_ACCESS_FOR_A_FILE", description: "Revokes a file's public sharing URL." },
|
||||
{ name: "Schedule Message", slug: "SLACK_SCHEDULE_MESSAGE", description: "Schedules a message for a future time (up to 120 days)." },
|
||||
{ name: "Search Messages", slug: "SLACK_SEARCH_MESSAGES", description: "Workspace-wide message search with advanced filters." },
|
||||
{ name: "Send Ephemeral", slug: "SLACK_SEND_EPHEMERAL_MESSAGE", description: "Sends a message visible only to a specific user." },
|
||||
{ name: "Send Message", slug: "SLACK_SEND_MESSAGE", description: "Posts a message to a channel, DM, or group." },
|
||||
{ name: "Set Conversation Purpose", slug: "SLACK_SET_A_CONVERSATION_S_PURPOSE", description: "Updates the purpose description of a channel." },
|
||||
{ name: "Set DND Duration", slug: "SLACK_SET_DND_DURATION", description: "Turns on DND or changes its current duration." },
|
||||
{ name: "Set Profile Photo", slug: "SLACK_SET_PROFILE_PHOTO", description: "Sets the user's profile image with cropping." },
|
||||
{ name: "Set Read Cursor", slug: "SLACK_SET_READ_CURSOR_IN_A_CONVERSATION", description: "Marks a specific timestamp as read." },
|
||||
{ name: "Set User Profile", slug: "SLACK_SET_SLACK_USER_PROFILE_INFORMATION", description: "Updates individual or multiple user profile fields." },
|
||||
{ name: "Set Conversation Topic", slug: "SLACK_SET_THE_TOPIC_OF_A_CONVERSATION", description: "Updates the topic of a conversation." },
|
||||
{ name: "Share Me Message", slug: "SLACK_SHARE_A_ME_MESSAGE_IN_A_CHANNEL", description: "Sends a third-person user action message (/me)." },
|
||||
{ name: "Share Remote File", slug: "SLACK_SHARE_REMOTE_FILE_IN_CHANNELS", description: "Shares a registered remote file into channels." },
|
||||
{ name: "Start Call", slug: "SLACK_START_CALL", description: "Registers a new call for third-party integration." },
|
||||
{ name: "Start RTM Session", slug: "SLACK_START_REAL_TIME_MESSAGING_SESSION", description: "Initiates a real-time messaging WebSocket session." },
|
||||
{ name: "Unarchive Channel", slug: "SLACK_UNARCHIVE_A_PUBLIC_OR_PRIVATE_CHANNEL", description: "Unarchives a specific channel." },
|
||||
{ name: "Unarchive Conversation", slug: "SLACK_UNARCHIVE_CHANNEL", description: "Reverses archival for a conversation." },
|
||||
{ name: "Unpin Item", slug: "SLACK_UNPIN_ITEM_FROM_CHANNEL", description: "Unpins a message from a channel." },
|
||||
{ name: "Update User Group", slug: "SLACK_UPDATE_AN_EXISTING_SLACK_USER_GROUP", description: "Updates name, handle, or channels for a user group." },
|
||||
{ name: "Update Remote File", slug: "SLACK_UPDATES_AN_EXISTING_REMOTE_FILE", description: "Updates metadata for a remote file reference." },
|
||||
{ name: "Update Message", slug: "SLACK_UPDATES_A_SLACK_MESSAGE", description: "Modifies the content of an existing message." },
|
||||
{ name: "Update Call Info", slug: "SLACK_UPDATE_SLACK_CALL_INFORMATION", description: "Updates call title or join URLs." },
|
||||
{ name: "Update Group Members", slug: "SLACK_UPDATE_USER_GROUP_MEMBERS", description: "Replaces the member list of a user group." },
|
||||
{ name: "Upload File", slug: "SLACK_UPLOAD_OR_CREATE_A_FILE_IN_SLACK", description: "Uploads content or binary files to Slack." },
|
||||
];
|
||||
|
||||
export const slackToolCatalogMarkdown = slackToolCatalog
|
||||
.map((tool) => `- ${tool.name} (${tool.slug}) - ${tool.description}`)
|
||||
.join("\n");
|
||||
|
|
@ -11,6 +11,9 @@ import { McpServerDefinition } from "@x/shared/dist/mcp.js";
|
|||
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 { executeAction as executeComposioAction, isConfigured as isComposioConfigured, listToolkitTools } from "../../composio/client.js";
|
||||
import { slackToolCatalog } from "../assistant/skills/slack/tool-catalog.js";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const BuiltinToolsSchema = z.record(z.string(), z.object({
|
||||
|
|
@ -22,6 +25,232 @@ const BuiltinToolsSchema = z.record(z.string(), z.object({
|
|||
}),
|
||||
}));
|
||||
|
||||
type SlackToolHint = {
|
||||
search?: string;
|
||||
patterns: string[];
|
||||
fallbackSlugs?: string[];
|
||||
preferSlugIncludes?: string[];
|
||||
excludePatterns?: string[];
|
||||
minScore?: number;
|
||||
};
|
||||
|
||||
const slackToolHints: Record<string, SlackToolHint> = {
|
||||
sendMessage: {
|
||||
search: "message",
|
||||
patterns: ["send", "message", "channel"],
|
||||
fallbackSlugs: [
|
||||
"SLACK_SEND_MESSAGE",
|
||||
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL",
|
||||
"SLACK_SEND_A_MESSAGE",
|
||||
],
|
||||
},
|
||||
listConversations: {
|
||||
search: "conversation",
|
||||
patterns: ["list", "conversation", "channel"],
|
||||
fallbackSlugs: [
|
||||
"SLACK_LIST_CONVERSATIONS",
|
||||
"SLACK_LIST_ALL_CHANNELS",
|
||||
"SLACK_LIST_ALL_SLACK_TEAM_CHANNELS_WITH_VARIOUS_FILTERS",
|
||||
"SLACK_LIST_CHANNELS",
|
||||
"SLACK_LIST_CHANNEL",
|
||||
],
|
||||
preferSlugIncludes: ["list", "conversation"],
|
||||
minScore: 2,
|
||||
},
|
||||
getConversationHistory: {
|
||||
search: "history",
|
||||
patterns: ["history", "conversation", "message"],
|
||||
fallbackSlugs: [
|
||||
"SLACK_FETCH_CONVERSATION_HISTORY",
|
||||
"SLACK_FETCHES_CONVERSATION_HISTORY",
|
||||
"SLACK_GET_CONVERSATION_HISTORY",
|
||||
"SLACK_GET_CHANNEL_HISTORY",
|
||||
],
|
||||
preferSlugIncludes: ["history"],
|
||||
minScore: 2,
|
||||
},
|
||||
listUsers: {
|
||||
search: "user",
|
||||
patterns: ["list", "user"],
|
||||
fallbackSlugs: [
|
||||
"SLACK_LIST_ALL_USERS",
|
||||
"SLACK_LIST_ALL_SLACK_TEAM_USERS_WITH_PAGINATION",
|
||||
"SLACK_LIST_USERS",
|
||||
"SLACK_GET_USERS",
|
||||
"SLACK_USERS_LIST",
|
||||
],
|
||||
preferSlugIncludes: ["list", "user"],
|
||||
excludePatterns: ["find", "by name", "by email", "by_email", "by_name", "lookup", "profile", "info"],
|
||||
minScore: 2,
|
||||
},
|
||||
getUserInfo: {
|
||||
search: "user",
|
||||
patterns: ["user", "info", "profile"],
|
||||
fallbackSlugs: [
|
||||
"SLACK_GET_USER_INFO",
|
||||
"SLACK_GET_USER",
|
||||
"SLACK_USER_INFO",
|
||||
],
|
||||
preferSlugIncludes: ["user", "info"],
|
||||
minScore: 1,
|
||||
},
|
||||
searchMessages: {
|
||||
search: "search",
|
||||
patterns: ["search", "message"],
|
||||
fallbackSlugs: [
|
||||
"SLACK_SEARCH_FOR_MESSAGES_WITH_QUERY",
|
||||
"SLACK_SEARCH_MESSAGES",
|
||||
"SLACK_SEARCH_MESSAGE",
|
||||
],
|
||||
preferSlugIncludes: ["search"],
|
||||
minScore: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const slackToolSlugCache = new Map<string, string>();
|
||||
|
||||
const slackToolSlugOverrides: Partial<Record<keyof typeof slackToolHints, string>> = {
|
||||
sendMessage: "SLACK_SEND_MESSAGE",
|
||||
listConversations: "SLACK_LIST_CONVERSATIONS",
|
||||
getConversationHistory: "SLACK_FETCH_CONVERSATION_HISTORY",
|
||||
listUsers: "SLACK_LIST_ALL_USERS",
|
||||
getUserInfo: "SLACK_RETRIEVE_DETAILED_USER_INFORMATION",
|
||||
searchMessages: "SLACK_SEARCH_MESSAGES",
|
||||
};
|
||||
|
||||
const compactObject = (input: Record<string, unknown>) =>
|
||||
Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
|
||||
|
||||
type SlackToolResult = { success: boolean; data?: unknown; error?: string };
|
||||
|
||||
/** Helper to execute a Slack tool with consistent account validation and error handling */
|
||||
async function executeSlackTool(
|
||||
hintKey: keyof typeof slackToolHints,
|
||||
params: Record<string, unknown>
|
||||
): Promise<SlackToolResult> {
|
||||
const account = composioAccountsRepo.getAccount('slack');
|
||||
if (!account || account.status !== 'ACTIVE') {
|
||||
return { success: false, error: 'Slack is not connected' };
|
||||
}
|
||||
try {
|
||||
const toolSlug = await resolveSlackToolSlug(hintKey);
|
||||
return await executeComposioAction(toolSlug, account.id, compactObject(params));
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeSlackTool = (tool: { slug: string; name?: string; description?: string }) =>
|
||||
`${tool.slug} ${tool.name || ""} ${tool.description || ""}`.toLowerCase();
|
||||
|
||||
const scoreSlackTool = (tool: { slug: string; name?: string; description?: string }, patterns: string[]) => {
|
||||
const slug = tool.slug.toLowerCase();
|
||||
const name = (tool.name || "").toLowerCase();
|
||||
const description = (tool.description || "").toLowerCase();
|
||||
|
||||
let score = 0;
|
||||
for (const pattern of patterns) {
|
||||
const needle = pattern.toLowerCase();
|
||||
if (slug.includes(needle)) score += 3;
|
||||
if (name.includes(needle)) score += 2;
|
||||
if (description.includes(needle)) score += 1;
|
||||
}
|
||||
return score;
|
||||
};
|
||||
|
||||
const pickSlackTool = (
|
||||
tools: Array<{ slug: string; name?: string; description?: string }>,
|
||||
hint: SlackToolHint,
|
||||
) => {
|
||||
let candidates = tools;
|
||||
|
||||
if (hint.excludePatterns && hint.excludePatterns.length > 0) {
|
||||
candidates = candidates.filter((tool) => {
|
||||
const haystack = normalizeSlackTool(tool);
|
||||
return !hint.excludePatterns!.some((pattern) => haystack.includes(pattern.toLowerCase()));
|
||||
});
|
||||
}
|
||||
|
||||
if (hint.preferSlugIncludes && hint.preferSlugIncludes.length > 0) {
|
||||
const preferred = candidates.filter((tool) =>
|
||||
hint.preferSlugIncludes!.every((pattern) => tool.slug.toLowerCase().includes(pattern.toLowerCase()))
|
||||
);
|
||||
if (preferred.length > 0) {
|
||||
candidates = preferred;
|
||||
}
|
||||
}
|
||||
|
||||
let best: { slug: string; name?: string; description?: string } | null = null;
|
||||
let bestScore = 0;
|
||||
|
||||
for (const tool of candidates) {
|
||||
const score = scoreSlackTool(tool, hint.patterns);
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
best = tool;
|
||||
}
|
||||
}
|
||||
|
||||
if (!best || (hint.minScore !== undefined && bestScore < hint.minScore)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return best;
|
||||
};
|
||||
|
||||
const resolveSlackToolSlug = async (hintKey: keyof typeof slackToolHints) => {
|
||||
const cached = slackToolSlugCache.get(hintKey);
|
||||
if (cached) return cached;
|
||||
|
||||
const hint = slackToolHints[hintKey];
|
||||
|
||||
const override = slackToolSlugOverrides[hintKey];
|
||||
if (override && slackToolCatalog.some((tool) => tool.slug === override)) {
|
||||
slackToolSlugCache.set(hintKey, override);
|
||||
return override;
|
||||
}
|
||||
const resolveFromTools = (tools: Array<{ slug: string; name?: string; description?: string }>) => {
|
||||
if (hint.fallbackSlugs && hint.fallbackSlugs.length > 0) {
|
||||
const fallbackSet = new Set(hint.fallbackSlugs.map((slug) => slug.toLowerCase()));
|
||||
const fallback = tools.find((tool) => fallbackSet.has(tool.slug.toLowerCase()));
|
||||
if (fallback) return fallback.slug;
|
||||
}
|
||||
|
||||
const best = pickSlackTool(tools, hint);
|
||||
return best?.slug || null;
|
||||
};
|
||||
|
||||
const initialTools = slackToolCatalog;
|
||||
|
||||
if (!initialTools.length) {
|
||||
throw new Error("No Slack tools returned from Composio");
|
||||
}
|
||||
|
||||
const initialSlug = resolveFromTools(initialTools);
|
||||
if (initialSlug) {
|
||||
slackToolSlugCache.set(hintKey, initialSlug);
|
||||
return initialSlug;
|
||||
}
|
||||
|
||||
const allSlug = resolveFromTools(slackToolCatalog);
|
||||
|
||||
if (!allSlug) {
|
||||
const fallback = await listToolkitTools("slack", hint.search || null);
|
||||
const fallbackSlug = resolveFromTools(fallback.items || []);
|
||||
if (!fallbackSlug) {
|
||||
throw new Error(`Unable to resolve Slack tool for ${hintKey}. Try slack-listAvailableTools.`);
|
||||
}
|
||||
slackToolSlugCache.set(hintKey, fallbackSlug);
|
||||
return fallbackSlug;
|
||||
}
|
||||
|
||||
slackToolSlugCache.set(hintKey, allSlug);
|
||||
return allSlug;
|
||||
};
|
||||
|
||||
export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
|
||||
loadSkill: {
|
||||
description: "Load a Rowboat skill definition into context by fetching its guidance string",
|
||||
|
|
@ -630,7 +859,7 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
|
|||
// }
|
||||
|
||||
const result = await executeCommand(command, { cwd: workingDir });
|
||||
|
||||
|
||||
return {
|
||||
success: result.exitCode === 0,
|
||||
stdout: result.stdout,
|
||||
|
|
@ -648,4 +877,162 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
|
|||
}
|
||||
},
|
||||
},
|
||||
|
||||
// ============================================================================
|
||||
// Slack Tools (via Composio)
|
||||
// ============================================================================
|
||||
|
||||
'slack-checkConnection': {
|
||||
description: 'Check if Slack is connected and ready to use. Use this before other Slack operations.',
|
||||
inputSchema: z.object({}),
|
||||
execute: async () => {
|
||||
if (!isComposioConfigured()) {
|
||||
return {
|
||||
connected: false,
|
||||
error: 'Composio is not configured. Please set up your Composio API key first.',
|
||||
};
|
||||
}
|
||||
const account = composioAccountsRepo.getAccount('slack');
|
||||
if (!account || account.status !== 'ACTIVE') {
|
||||
return {
|
||||
connected: false,
|
||||
error: 'Slack is not connected. Please connect Slack from the settings.',
|
||||
};
|
||||
}
|
||||
return {
|
||||
connected: true,
|
||||
accountId: account.id,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
'slack-listAvailableTools': {
|
||||
description: 'List available Slack tools from Composio. Use this to discover the correct tool slugs before executing actions. Call this first if other Slack tools return errors.',
|
||||
inputSchema: z.object({
|
||||
search: z.string().optional().describe('Optional search query to filter tools (e.g., "message", "channel", "user")'),
|
||||
}),
|
||||
execute: async ({ search }: { search?: string }) => {
|
||||
if (!isComposioConfigured()) {
|
||||
return { success: false, error: 'Composio is not configured' };
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await listToolkitTools('slack', search || null);
|
||||
return {
|
||||
success: true,
|
||||
tools: result.items,
|
||||
count: result.items.length,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
'slack-executeAction': {
|
||||
description: 'Execute a Slack action by its Composio tool slug. Use slack-listAvailableTools first to discover correct slugs. Pass the exact slug and the required input parameters.',
|
||||
inputSchema: z.object({
|
||||
toolSlug: z.string().describe('The exact Composio tool slug (e.g., "SLACKBOT_SEND_A_MESSAGE_TO_A_SLACK_CHANNEL")'),
|
||||
input: z.record(z.string(), z.unknown()).describe('Input parameters for the tool (check the tool description for required fields)'),
|
||||
}),
|
||||
execute: async ({ toolSlug, input }: { toolSlug: string; input: Record<string, unknown> }) => {
|
||||
const account = composioAccountsRepo.getAccount('slack');
|
||||
if (!account || account.status !== 'ACTIVE') {
|
||||
return { success: false, error: 'Slack is not connected' };
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await executeComposioAction(toolSlug, account.id, input);
|
||||
return result;
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
'slack-sendMessage': {
|
||||
description: 'Send a message to a Slack channel or user. Requires channel ID (starts with C for channels, D for DMs) or user ID.',
|
||||
inputSchema: z.object({
|
||||
channel: z.string().describe('Channel ID (e.g., C01234567) or user ID (e.g., U01234567) to send the message to'),
|
||||
text: z.string().describe('The message text to send'),
|
||||
}),
|
||||
execute: async ({ channel, text }: { channel: string; text: string }) => {
|
||||
return executeSlackTool("sendMessage", { channel, text });
|
||||
},
|
||||
},
|
||||
|
||||
'slack-listChannels': {
|
||||
description: 'List Slack channels the user has access to. Returns channel IDs and names.',
|
||||
inputSchema: z.object({
|
||||
types: z.string().optional().describe('Comma-separated channel types: public_channel, private_channel, mpim, im (default: public_channel,private_channel)'),
|
||||
limit: z.number().optional().describe('Maximum number of channels to return (default: 100)'),
|
||||
}),
|
||||
execute: async ({ types, limit }: { types?: string; limit?: number }) => {
|
||||
return executeSlackTool("listConversations", {
|
||||
types: types || "public_channel,private_channel",
|
||||
limit: limit ?? 100,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
'slack-getChannelHistory': {
|
||||
description: 'Get recent messages from a Slack channel. Returns message history with timestamps and user IDs.',
|
||||
inputSchema: z.object({
|
||||
channel: z.string().describe('Channel ID to get history from (e.g., C01234567)'),
|
||||
limit: z.number().optional().describe('Maximum number of messages to return (default: 20, max: 100)'),
|
||||
}),
|
||||
execute: async ({ channel, limit }: { channel: string; limit?: number }) => {
|
||||
return executeSlackTool("getConversationHistory", {
|
||||
channel,
|
||||
limit: limit !== undefined ? Math.min(limit, 100) : 20,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
'slack-listUsers': {
|
||||
description: 'List users in the Slack workspace. Returns user IDs, names, and profile info.',
|
||||
inputSchema: z.object({
|
||||
limit: z.number().optional().describe('Maximum number of users to return (default: 100)'),
|
||||
}),
|
||||
execute: async ({ limit }: { limit?: number }) => {
|
||||
return executeSlackTool("listUsers", { limit: limit ?? 100 });
|
||||
},
|
||||
},
|
||||
|
||||
'slack-getUserInfo': {
|
||||
description: 'Get detailed information about a specific Slack user by their user ID.',
|
||||
inputSchema: z.object({
|
||||
user: z.string().describe('User ID to get info for (e.g., U01234567)'),
|
||||
}),
|
||||
execute: async ({ user }: { user: string }) => {
|
||||
return executeSlackTool("getUserInfo", { user });
|
||||
},
|
||||
},
|
||||
|
||||
'slack-searchMessages': {
|
||||
description: 'Search for messages in Slack. Find messages containing specific text across channels.',
|
||||
inputSchema: z.object({
|
||||
query: z.string().describe('Search query text'),
|
||||
count: z.number().optional().describe('Maximum number of results (default: 20)'),
|
||||
}),
|
||||
execute: async ({ query, count }: { query: string; count?: number }) => {
|
||||
return executeSlackTool("searchMessages", { query, count: count ?? 20 });
|
||||
},
|
||||
},
|
||||
|
||||
'slack-getDirectMessages': {
|
||||
description: 'List direct message (DM) channels. Returns IDs of DM conversations with other users.',
|
||||
inputSchema: z.object({
|
||||
limit: z.number().optional().describe('Maximum number of DM channels to return (default: 50)'),
|
||||
}),
|
||||
execute: async ({ limit }: { limit?: number }) => {
|
||||
return executeSlackTool("listConversations", { types: "im", limit: limit ?? 50 });
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
358
apps/x/packages/core/src/composio/client.ts
Normal file
358
apps/x/packages/core/src/composio/client.ts
Normal file
|
|
@ -0,0 +1,358 @@
|
|||
import { z } from "zod";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { Composio } from "@composio/core";
|
||||
import { WorkDir } from "../config/config.js";
|
||||
import {
|
||||
ZAuthConfig,
|
||||
ZConnectedAccount,
|
||||
ZCreateAuthConfigRequest,
|
||||
ZCreateAuthConfigResponse,
|
||||
ZCreateConnectedAccountRequest,
|
||||
ZCreateConnectedAccountResponse,
|
||||
ZDeleteOperationResponse,
|
||||
ZErrorResponse,
|
||||
ZExecuteActionResponse,
|
||||
ZListResponse,
|
||||
ZToolkit,
|
||||
} from "./types.js";
|
||||
|
||||
const BASE_URL = 'https://backend.composio.dev/api/v3';
|
||||
const CONFIG_FILE = path.join(WorkDir, 'config', 'composio.json');
|
||||
|
||||
// Composio SDK client (lazily initialized)
|
||||
let composioClient: Composio | null = null;
|
||||
|
||||
function getComposioClient(): Composio {
|
||||
if (composioClient) {
|
||||
return composioClient;
|
||||
}
|
||||
|
||||
const apiKey = getApiKey();
|
||||
if (!apiKey) {
|
||||
throw new Error('Composio API key not configured');
|
||||
}
|
||||
|
||||
composioClient = new Composio({ apiKey });
|
||||
return composioClient;
|
||||
}
|
||||
|
||||
function resetComposioClient(): void {
|
||||
composioClient = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration schema for Composio
|
||||
*/
|
||||
const ZComposioConfig = z.object({
|
||||
apiKey: z.string().optional(),
|
||||
});
|
||||
|
||||
type ComposioConfig = z.infer<typeof ZComposioConfig>;
|
||||
|
||||
/**
|
||||
* Load Composio configuration
|
||||
*/
|
||||
function loadConfig(): ComposioConfig {
|
||||
try {
|
||||
if (fs.existsSync(CONFIG_FILE)) {
|
||||
const data = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
||||
return ZComposioConfig.parse(JSON.parse(data));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Composio] Failed to load config:', error);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Save Composio configuration
|
||||
*/
|
||||
export function saveConfig(config: ComposioConfig): void {
|
||||
const dir = path.dirname(CONFIG_FILE);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Composio API key
|
||||
*/
|
||||
export function getApiKey(): string | null {
|
||||
const config = loadConfig();
|
||||
return config.apiKey || process.env.COMPOSIO_API_KEY || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Composio API key
|
||||
*/
|
||||
export function setApiKey(apiKey: string): void {
|
||||
const config = loadConfig();
|
||||
config.apiKey = apiKey;
|
||||
saveConfig(config);
|
||||
resetComposioClient();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Composio is configured
|
||||
*/
|
||||
export function isConfigured(): boolean {
|
||||
return !!getApiKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an API call to Composio
|
||||
*/
|
||||
export async function composioApiCall<T extends z.ZodTypeAny>(
|
||||
schema: T,
|
||||
url: string,
|
||||
options: RequestInit = {},
|
||||
): Promise<z.infer<T>> {
|
||||
const apiKey = getApiKey();
|
||||
if (!apiKey) {
|
||||
throw new Error('Composio API key not configured');
|
||||
}
|
||||
|
||||
console.log(`[Composio] ${options.method || 'GET'} ${url}`);
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
...options.headers,
|
||||
"x-api-key": apiKey,
|
||||
...(options.method === 'POST' ? { "Content-Type": "application/json" } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
console.log(`[Composio] Response in ${duration}ms`);
|
||||
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
const rawText = await response.text();
|
||||
|
||||
if (!response.ok || !contentType.includes('application/json')) {
|
||||
console.error(`[Composio] Error response:`, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
contentType,
|
||||
preview: rawText.slice(0, 200),
|
||||
});
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Composio API error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
if (!contentType.includes('application/json')) {
|
||||
throw new Error('Expected JSON response');
|
||||
}
|
||||
|
||||
let data: unknown;
|
||||
try {
|
||||
data = JSON.parse(rawText);
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : 'Unknown error';
|
||||
throw new Error(`Failed to parse response: ${message}`);
|
||||
}
|
||||
|
||||
if (typeof data === 'object' && data !== null && 'error' in data) {
|
||||
const parsedError = ZErrorResponse.parse(data);
|
||||
throw new Error(`Composio error (${parsedError.error.error_code}): ${parsedError.error.message}`);
|
||||
}
|
||||
|
||||
return schema.parse(data);
|
||||
} catch (error) {
|
||||
console.error(`[Composio] Error:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List available toolkits
|
||||
*/
|
||||
export async function listToolkits(cursor: string | null = null): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZToolkit>>>> {
|
||||
const url = new URL(`${BASE_URL}/toolkits`);
|
||||
url.searchParams.set("sort_by", "usage");
|
||||
if (cursor) {
|
||||
url.searchParams.set("cursor", cursor);
|
||||
}
|
||||
return composioApiCall(ZListResponse(ZToolkit), url.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific toolkit
|
||||
*/
|
||||
export async function getToolkit(toolkitSlug: string): Promise<z.infer<typeof ZToolkit>> {
|
||||
const apiKey = getApiKey();
|
||||
if (!apiKey) {
|
||||
throw new Error('Composio API key not configured');
|
||||
}
|
||||
|
||||
const url = `${BASE_URL}/toolkits/${toolkitSlug}`;
|
||||
console.log(`[Composio] GET ${url}`);
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: { "x-api-key": apiKey },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch toolkit: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const no_auth = data.composio_managed_auth_schemes?.includes('NO_AUTH') ||
|
||||
data.auth_config_details?.some((config: { mode: string }) => config.mode === 'NO_AUTH') ||
|
||||
false;
|
||||
|
||||
return ZToolkit.parse({
|
||||
...data,
|
||||
no_auth,
|
||||
meta: data.meta || { description: '', logo: '', tools_count: 0, triggers_count: 0 },
|
||||
auth_schemes: data.auth_schemes || [],
|
||||
composio_managed_auth_schemes: data.composio_managed_auth_schemes || [],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* List auth configs for a toolkit
|
||||
*/
|
||||
export async function listAuthConfigs(
|
||||
toolkitSlug: string,
|
||||
cursor: string | null = null,
|
||||
managedOnly: boolean = false
|
||||
): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZAuthConfig>>>> {
|
||||
const url = new URL(`${BASE_URL}/auth_configs`);
|
||||
url.searchParams.set("toolkit_slug", toolkitSlug);
|
||||
if (cursor) {
|
||||
url.searchParams.set("cursor", cursor);
|
||||
}
|
||||
if (managedOnly) {
|
||||
url.searchParams.set("is_composio_managed", "true");
|
||||
}
|
||||
return composioApiCall(ZListResponse(ZAuthConfig), url.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an auth config
|
||||
*/
|
||||
export async function createAuthConfig(
|
||||
request: z.infer<typeof ZCreateAuthConfigRequest>
|
||||
): Promise<z.infer<typeof ZCreateAuthConfigResponse>> {
|
||||
const url = new URL(`${BASE_URL}/auth_configs`);
|
||||
return composioApiCall(ZCreateAuthConfigResponse, url.toString(), {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an auth config
|
||||
*/
|
||||
export async function deleteAuthConfig(authConfigId: string): Promise<z.infer<typeof ZDeleteOperationResponse>> {
|
||||
const url = new URL(`${BASE_URL}/auth_configs/${authConfigId}`);
|
||||
return composioApiCall(ZDeleteOperationResponse, url.toString(), {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a connected account
|
||||
*/
|
||||
export async function createConnectedAccount(
|
||||
request: z.infer<typeof ZCreateConnectedAccountRequest>
|
||||
): Promise<z.infer<typeof ZCreateConnectedAccountResponse>> {
|
||||
const url = new URL(`${BASE_URL}/connected_accounts`);
|
||||
return composioApiCall(ZCreateConnectedAccountResponse, url.toString(), {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a connected account
|
||||
*/
|
||||
export async function getConnectedAccount(connectedAccountId: string): Promise<z.infer<typeof ZConnectedAccount>> {
|
||||
const url = new URL(`${BASE_URL}/connected_accounts/${connectedAccountId}`);
|
||||
return composioApiCall(ZConnectedAccount, url.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a connected account
|
||||
*/
|
||||
export async function deleteConnectedAccount(connectedAccountId: string): Promise<z.infer<typeof ZDeleteOperationResponse>> {
|
||||
const url = new URL(`${BASE_URL}/connected_accounts/${connectedAccountId}`);
|
||||
return composioApiCall(ZDeleteOperationResponse, url.toString(), {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* List available tools for a toolkit
|
||||
*/
|
||||
export async function listToolkitTools(
|
||||
toolkitSlug: string,
|
||||
searchQuery: string | null = null,
|
||||
): Promise<{ items: Array<{ slug: string; name: string; description: 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 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) => ({
|
||||
slug: String(item.slug ?? ''),
|
||||
name: String(item.name ?? ''),
|
||||
description: String(item.description ?? ''),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a tool action using Composio SDK
|
||||
*/
|
||||
export async function executeAction(
|
||||
actionSlug: string,
|
||||
connectedAccountId: string,
|
||||
input: Record<string, unknown>
|
||||
): Promise<z.infer<typeof ZExecuteActionResponse>> {
|
||||
console.log(`[Composio] Executing action: ${actionSlug} (account: ${connectedAccountId})`);
|
||||
|
||||
try {
|
||||
const client = getComposioClient();
|
||||
const result = await client.tools.execute(actionSlug, {
|
||||
userId: connectedAccountId,
|
||||
arguments: input,
|
||||
connectedAccountId,
|
||||
});
|
||||
|
||||
console.log(`[Composio] Action completed successfully`);
|
||||
return { success: true, data: result.data };
|
||||
} catch (error) {
|
||||
console.error(`[Composio] Action execution failed:`, error);
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
return { success: false, data: null, error: message };
|
||||
}
|
||||
}
|
||||
5
apps/x/packages/core/src/composio/index.ts
Normal file
5
apps/x/packages/core/src/composio/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// Composio integration for Rowboat X
|
||||
|
||||
export * from './types.js';
|
||||
export * from './client.js';
|
||||
export * from './repo.js';
|
||||
140
apps/x/packages/core/src/composio/repo.ts
Normal file
140
apps/x/packages/core/src/composio/repo.ts
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { z } from "zod";
|
||||
import { WorkDir } from "../config/config.js";
|
||||
import { ZLocalConnectedAccount, LocalConnectedAccount, ConnectedAccountStatus } from "./types.js";
|
||||
|
||||
const ACCOUNTS_FILE = path.join(WorkDir, 'data', 'composio', 'connected_accounts.json');
|
||||
|
||||
/**
|
||||
* Schema for the connected accounts storage file
|
||||
*/
|
||||
const ZConnectedAccountsStorage = z.object({
|
||||
accounts: z.record(z.string(), ZLocalConnectedAccount), // keyed by toolkit slug
|
||||
});
|
||||
|
||||
type ConnectedAccountsStorage = z.infer<typeof ZConnectedAccountsStorage>;
|
||||
|
||||
/**
|
||||
* Interface for Composio accounts repository
|
||||
*/
|
||||
export interface IComposioAccountsRepo {
|
||||
getAccount(toolkitSlug: string): LocalConnectedAccount | null;
|
||||
getAllAccounts(): Record<string, LocalConnectedAccount>;
|
||||
saveAccount(account: LocalConnectedAccount): void;
|
||||
updateAccountStatus(toolkitSlug: string, status: ConnectedAccountStatus): boolean;
|
||||
deleteAccount(toolkitSlug: string): void;
|
||||
isConnected(toolkitSlug: string): boolean;
|
||||
getConnectedToolkits(): string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the storage directory exists
|
||||
*/
|
||||
function ensureStorageDir(): void {
|
||||
const dir = path.dirname(ACCOUNTS_FILE);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load connected accounts from storage
|
||||
*/
|
||||
function loadAccounts(): ConnectedAccountsStorage {
|
||||
try {
|
||||
if (fs.existsSync(ACCOUNTS_FILE)) {
|
||||
const data = fs.readFileSync(ACCOUNTS_FILE, 'utf-8');
|
||||
return ZConnectedAccountsStorage.parse(JSON.parse(data));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ComposioRepo] Failed to load accounts:', error);
|
||||
}
|
||||
return { accounts: {} };
|
||||
}
|
||||
|
||||
/**
|
||||
* Save connected accounts to storage
|
||||
*/
|
||||
function saveAccounts(storage: ConnectedAccountsStorage): void {
|
||||
ensureStorageDir();
|
||||
fs.writeFileSync(ACCOUNTS_FILE, JSON.stringify(storage, null, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Composio Connected Accounts Repository
|
||||
* Stores connected account information locally
|
||||
*/
|
||||
export class ComposioAccountsRepo implements IComposioAccountsRepo {
|
||||
/**
|
||||
* Get a connected account by toolkit slug
|
||||
*/
|
||||
getAccount(toolkitSlug: string): LocalConnectedAccount | null {
|
||||
const storage = loadAccounts();
|
||||
return storage.accounts[toolkitSlug] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all connected accounts
|
||||
*/
|
||||
getAllAccounts(): Record<string, LocalConnectedAccount> {
|
||||
const storage = loadAccounts();
|
||||
return storage.accounts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a connected account
|
||||
*/
|
||||
saveAccount(account: LocalConnectedAccount): void {
|
||||
const storage = loadAccounts();
|
||||
storage.accounts[account.toolkitSlug] = account;
|
||||
saveAccounts(storage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update account status
|
||||
* @returns true if account was found and updated, false if account doesn't exist
|
||||
*/
|
||||
updateAccountStatus(toolkitSlug: string, status: ConnectedAccountStatus): boolean {
|
||||
const storage = loadAccounts();
|
||||
const account = storage.accounts[toolkitSlug];
|
||||
if (!account) {
|
||||
console.warn(`[ComposioRepo] Cannot update status: account '${toolkitSlug}' not found`);
|
||||
return false;
|
||||
}
|
||||
account.status = status;
|
||||
account.lastUpdatedAt = new Date().toISOString();
|
||||
saveAccounts(storage);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a connected account
|
||||
*/
|
||||
deleteAccount(toolkitSlug: string): void {
|
||||
const storage = loadAccounts();
|
||||
delete storage.accounts[toolkitSlug];
|
||||
saveAccounts(storage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a toolkit is connected
|
||||
*/
|
||||
isConnected(toolkitSlug: string): boolean {
|
||||
const account = this.getAccount(toolkitSlug);
|
||||
return account?.status === 'ACTIVE';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of connected toolkit slugs
|
||||
*/
|
||||
getConnectedToolkits(): string[] {
|
||||
const storage = loadAccounts();
|
||||
return Object.entries(storage.accounts)
|
||||
.filter(([, account]) => account.status === 'ACTIVE')
|
||||
.map(([slug]) => slug);
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const composioAccountsRepo = new ComposioAccountsRepo();
|
||||
237
apps/x/packages/core/src/composio/types.ts
Normal file
237
apps/x/packages/core/src/composio/types.ts
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* Composio authentication schemes
|
||||
*/
|
||||
export const ZAuthScheme = z.enum([
|
||||
'API_KEY',
|
||||
'BASIC',
|
||||
'BASIC_WITH_JWT',
|
||||
'BEARER_TOKEN',
|
||||
'COMPOSIO_LINK',
|
||||
'SERVICE_ACCOUNT',
|
||||
'GOOGLE_SERVICE_ACCOUNT',
|
||||
'NO_AUTH',
|
||||
'OAUTH1',
|
||||
'OAUTH2',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Connected account status
|
||||
*/
|
||||
export const ZConnectedAccountStatus = z.enum([
|
||||
'INITIALIZING',
|
||||
'INITIATED',
|
||||
'ACTIVE',
|
||||
'FAILED',
|
||||
'EXPIRED',
|
||||
'INACTIVE',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Toolkit metadata
|
||||
*/
|
||||
export const ZToolkitMeta = z.object({
|
||||
description: z.string(),
|
||||
logo: z.string(),
|
||||
tools_count: z.number(),
|
||||
triggers_count: z.number(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Toolkit schema
|
||||
*/
|
||||
export const ZToolkit = z.object({
|
||||
slug: z.string(),
|
||||
name: z.string(),
|
||||
meta: ZToolkitMeta,
|
||||
no_auth: z.boolean(),
|
||||
auth_schemes: z.array(ZAuthScheme),
|
||||
composio_managed_auth_schemes: z.array(ZAuthScheme),
|
||||
});
|
||||
|
||||
/**
|
||||
* Tool schema
|
||||
*/
|
||||
export const ZTool = z.object({
|
||||
slug: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
toolkit: z.object({
|
||||
slug: z.string(),
|
||||
name: z.string(),
|
||||
logo: z.string(),
|
||||
}),
|
||||
input_parameters: z.object({
|
||||
type: z.literal('object'),
|
||||
properties: z.record(z.string(), z.unknown()),
|
||||
required: z.array(z.string()).optional(),
|
||||
additionalProperties: z.boolean().optional(),
|
||||
}),
|
||||
no_auth: z.boolean(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Auth config schema
|
||||
*/
|
||||
export const ZAuthConfig = z.object({
|
||||
id: z.string(),
|
||||
is_composio_managed: z.boolean(),
|
||||
auth_scheme: ZAuthScheme,
|
||||
});
|
||||
|
||||
/**
|
||||
* Credentials schema
|
||||
*/
|
||||
export const ZCredentials = z.record(z.string(), z.union([z.string(), z.number(), z.boolean()]));
|
||||
|
||||
/**
|
||||
* Create auth config request
|
||||
*/
|
||||
export const ZCreateAuthConfigRequest = z.object({
|
||||
toolkit: z.object({
|
||||
slug: z.string(),
|
||||
}),
|
||||
auth_config: z.discriminatedUnion('type', [
|
||||
z.object({
|
||||
type: z.literal('use_composio_managed_auth'),
|
||||
name: z.string().optional(),
|
||||
credentials: ZCredentials.optional(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('use_custom_auth'),
|
||||
authScheme: ZAuthScheme,
|
||||
credentials: ZCredentials,
|
||||
name: z.string().optional(),
|
||||
}),
|
||||
]).optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Create auth config response
|
||||
*/
|
||||
export const ZCreateAuthConfigResponse = z.object({
|
||||
toolkit: z.object({
|
||||
slug: z.string(),
|
||||
}),
|
||||
auth_config: ZAuthConfig,
|
||||
});
|
||||
|
||||
/**
|
||||
* Connection data schema
|
||||
*/
|
||||
export const ZConnectionData = z.object({
|
||||
authScheme: ZAuthScheme,
|
||||
val: z.record(z.string(), z.unknown())
|
||||
.and(z.object({
|
||||
status: ZConnectedAccountStatus,
|
||||
})),
|
||||
});
|
||||
|
||||
/**
|
||||
* Create connected account request
|
||||
*/
|
||||
export const ZCreateConnectedAccountRequest = z.object({
|
||||
auth_config: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
connection: z.object({
|
||||
state: ZConnectionData.optional(),
|
||||
user_id: z.string().optional(),
|
||||
callback_url: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Create connected account response
|
||||
*/
|
||||
export const ZCreateConnectedAccountResponse = z.object({
|
||||
id: z.string(),
|
||||
connectionData: ZConnectionData,
|
||||
});
|
||||
|
||||
/**
|
||||
* Connected account schema
|
||||
*/
|
||||
export const ZConnectedAccount = z.object({
|
||||
id: z.string(),
|
||||
toolkit: z.object({
|
||||
slug: z.string(),
|
||||
}),
|
||||
auth_config: z.object({
|
||||
id: z.string(),
|
||||
is_composio_managed: z.boolean(),
|
||||
is_disabled: z.boolean(),
|
||||
}),
|
||||
status: ZConnectedAccountStatus,
|
||||
});
|
||||
|
||||
/**
|
||||
* Error response schema
|
||||
*/
|
||||
export const ZErrorResponse = z.object({
|
||||
error: z.object({
|
||||
message: z.string(),
|
||||
error_code: z.number(),
|
||||
suggested_fix: z.string().nullable(),
|
||||
errors: z.array(z.string()).nullable(),
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Delete operation response
|
||||
*/
|
||||
export const ZDeleteOperationResponse = z.object({
|
||||
success: z.boolean(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Generic list response
|
||||
*/
|
||||
export const ZListResponse = <T extends z.ZodTypeAny>(schema: T) => z.object({
|
||||
items: z.array(schema),
|
||||
next_cursor: z.string().nullable(),
|
||||
total_pages: z.number(),
|
||||
current_page: z.number(),
|
||||
total_items: z.number(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Execute action request
|
||||
*/
|
||||
export const ZExecuteActionRequest = z.object({
|
||||
action: z.string(),
|
||||
connected_account_id: z.string(),
|
||||
input: z.record(z.string(), z.unknown()),
|
||||
});
|
||||
|
||||
/**
|
||||
* Execute action response
|
||||
*/
|
||||
export const ZExecuteActionResponse = z.object({
|
||||
success: z.boolean(),
|
||||
data: z.unknown(),
|
||||
error: z.string().optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Local connected account storage schema
|
||||
*/
|
||||
export const ZLocalConnectedAccount = z.object({
|
||||
id: z.string(),
|
||||
authConfigId: z.string(),
|
||||
status: ZConnectedAccountStatus,
|
||||
toolkitSlug: z.string(),
|
||||
createdAt: z.string(),
|
||||
lastUpdatedAt: z.string(),
|
||||
});
|
||||
|
||||
export type AuthScheme = z.infer<typeof ZAuthScheme>;
|
||||
export type ConnectedAccountStatus = z.infer<typeof ZConnectedAccountStatus>;
|
||||
export type Toolkit = z.infer<typeof ZToolkit>;
|
||||
export type Tool = z.infer<typeof ZTool>;
|
||||
export type AuthConfig = z.infer<typeof ZAuthConfig>;
|
||||
export type ConnectedAccount = z.infer<typeof ZConnectedAccount>;
|
||||
export type LocalConnectedAccount = z.infer<typeof ZLocalConnectedAccount>;
|
||||
export type ExecuteActionRequest = z.infer<typeof ZExecuteActionRequest>;
|
||||
export type ExecuteActionResponse = z.infer<typeof ZExecuteActionResponse>;
|
||||
|
|
@ -244,6 +244,85 @@ const ipcSchemas = {
|
|||
success: z.literal(true),
|
||||
}),
|
||||
},
|
||||
// Composio integration channels
|
||||
'composio:is-configured': {
|
||||
req: z.null(),
|
||||
res: z.object({
|
||||
configured: z.boolean(),
|
||||
}),
|
||||
},
|
||||
'composio:set-api-key': {
|
||||
req: z.object({
|
||||
apiKey: z.string(),
|
||||
}),
|
||||
res: z.object({
|
||||
success: z.boolean(),
|
||||
error: z.string().optional(),
|
||||
}),
|
||||
},
|
||||
'composio:initiate-connection': {
|
||||
req: z.object({
|
||||
toolkitSlug: z.string(),
|
||||
}),
|
||||
res: z.object({
|
||||
success: z.boolean(),
|
||||
redirectUrl: z.string().optional(),
|
||||
connectedAccountId: z.string().optional(),
|
||||
error: z.string().optional(),
|
||||
}),
|
||||
},
|
||||
'composio:get-connection-status': {
|
||||
req: z.object({
|
||||
toolkitSlug: z.string(),
|
||||
}),
|
||||
res: z.object({
|
||||
isConnected: z.boolean(),
|
||||
status: z.string().optional(),
|
||||
}),
|
||||
},
|
||||
'composio:sync-connection': {
|
||||
req: z.object({
|
||||
toolkitSlug: z.string(),
|
||||
connectedAccountId: z.string(),
|
||||
}),
|
||||
res: z.object({
|
||||
status: z.string(),
|
||||
}),
|
||||
},
|
||||
'composio:disconnect': {
|
||||
req: z.object({
|
||||
toolkitSlug: z.string(),
|
||||
}),
|
||||
res: z.object({
|
||||
success: z.boolean(),
|
||||
}),
|
||||
},
|
||||
'composio:list-connected': {
|
||||
req: z.null(),
|
||||
res: z.object({
|
||||
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({
|
||||
success: z.boolean(),
|
||||
data: z.unknown(),
|
||||
error: z.string().optional(),
|
||||
}),
|
||||
},
|
||||
'composio:didConnect': {
|
||||
req: z.object({
|
||||
toolkitSlug: z.string(),
|
||||
success: z.boolean(),
|
||||
error: z.string().optional(),
|
||||
}),
|
||||
res: z.null(),
|
||||
},
|
||||
} as const;
|
||||
|
||||
// ============================================================================
|
||||
|
|
|
|||
71
apps/x/pnpm-lock.yaml
generated
71
apps/x/pnpm-lock.yaml
generated
|
|
@ -290,6 +290,9 @@ importers:
|
|||
'@ai-sdk/provider':
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.1
|
||||
'@composio/core':
|
||||
specifier: ^0.1.48
|
||||
version: 0.1.55(zod@4.2.1)
|
||||
'@google-cloud/local-auth':
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.1(encoding@0.1.13)
|
||||
|
|
@ -661,6 +664,19 @@ packages:
|
|||
'@chevrotain/utils@11.0.3':
|
||||
resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==}
|
||||
|
||||
'@composio/client@0.1.0-alpha.37':
|
||||
resolution: {integrity: sha512-2YzXiRXlxqOgEz7nEh1aKdJ9vwQRb69+RX6UH+LTDmuTwW+pOjI2qbhIBxTbP2z9e2SyHVG0/AtMxc9tR5jU0w==}
|
||||
|
||||
'@composio/core@0.1.55':
|
||||
resolution: {integrity: sha512-GLSWTS/gZeycQ7W2wSZQ21DKV+LC6WTAilhSj+JG9Apslx3re9luF1Lyblm4UMSf4DzYWKOrtipwQOaeg7bmTg==}
|
||||
peerDependencies:
|
||||
zod: '>=3.25.76 <5'
|
||||
|
||||
'@composio/json-schema-to-zod@0.1.16':
|
||||
resolution: {integrity: sha512-vu6RUQTWDW/0wLDWsQHtWJ97JsnMjA1olTQwVD88BAtKZTdDLuzjnGRxSllxp5JG/GntnovUphraZWcWfe80eQ==}
|
||||
peerDependencies:
|
||||
zod: '>=3.25.76 <5'
|
||||
|
||||
'@electron-forge/cli@7.11.1':
|
||||
resolution: {integrity: sha512-pk8AoLsr7t7LBAt0cFD06XFA6uxtPdvtLx06xeal7O9o7GHGCbj29WGwFoJ8Br/ENM0Ho868S3PrAn1PtBXt5g==}
|
||||
engines: {node: '>= 16.4.0'}
|
||||
|
|
@ -5253,6 +5269,18 @@ packages:
|
|||
resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
openai@5.23.2:
|
||||
resolution: {integrity: sha512-MQBzmTulj+MM5O8SKEk/gL8a7s5mktS9zUtAkU257WjvobGc9nKcBuVwjyEEcb9SI8a8Y2G/mzn3vm9n1Jlleg==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
ws: ^8.18.0
|
||||
zod: ^3.23.8
|
||||
peerDependenciesMeta:
|
||||
ws:
|
||||
optional: true
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
openid-client@6.8.1:
|
||||
resolution: {integrity: sha512-VoYT6enBo6Vj2j3Q5Ec0AezS+9YGzQo1f5Xc42lreMGlfP4ljiXPKVDvCADh+XHCV/bqPu/wWSiCVXbJKvrODw==}
|
||||
|
||||
|
|
@ -5555,6 +5583,9 @@ packages:
|
|||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
pusher-js@8.4.0:
|
||||
resolution: {integrity: sha512-wp3HqIIUc1GRyu1XrP6m2dgyE9MoCsXVsWNlohj0rjSkLf+a0jLvEyVubdg58oMk7bhjBWnFClgp8jfAa6Ak4Q==}
|
||||
|
||||
qs@6.14.1:
|
||||
resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
|
@ -6152,6 +6183,9 @@ packages:
|
|||
tw-animate-css@1.4.0:
|
||||
resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==}
|
||||
|
||||
tweetnacl@1.0.3:
|
||||
resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==}
|
||||
|
||||
type-check@0.4.0:
|
||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
|
@ -6305,6 +6339,10 @@ packages:
|
|||
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
|
||||
hasBin: true
|
||||
|
||||
uuid@13.0.0:
|
||||
resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==}
|
||||
hasBin: true
|
||||
|
||||
uuid@9.0.1:
|
||||
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
|
||||
hasBin: true
|
||||
|
|
@ -7201,6 +7239,27 @@ snapshots:
|
|||
|
||||
'@chevrotain/utils@11.0.3': {}
|
||||
|
||||
'@composio/client@0.1.0-alpha.37': {}
|
||||
|
||||
'@composio/core@0.1.55(zod@4.2.1)':
|
||||
dependencies:
|
||||
'@composio/client': 0.1.0-alpha.37
|
||||
'@composio/json-schema-to-zod': 0.1.16(zod@4.2.1)
|
||||
'@types/json-schema': 7.0.15
|
||||
chalk: 4.1.2
|
||||
openai: 5.23.2(zod@4.2.1)
|
||||
pusher-js: 8.4.0
|
||||
semver: 7.7.3
|
||||
uuid: 13.0.0
|
||||
zod: 4.2.1
|
||||
zod-to-json-schema: 3.25.1(zod@4.2.1)
|
||||
transitivePeerDependencies:
|
||||
- ws
|
||||
|
||||
'@composio/json-schema-to-zod@0.1.16(zod@4.2.1)':
|
||||
dependencies:
|
||||
zod: 4.2.1
|
||||
|
||||
'@electron-forge/cli@7.11.1(encoding@0.1.13)(esbuild@0.24.2)':
|
||||
dependencies:
|
||||
'@electron-forge/core': 7.11.1(encoding@0.1.13)(esbuild@0.24.2)
|
||||
|
|
@ -12678,6 +12737,10 @@ snapshots:
|
|||
is-docker: 2.2.1
|
||||
is-wsl: 2.2.0
|
||||
|
||||
openai@5.23.2(zod@4.2.1):
|
||||
optionalDependencies:
|
||||
zod: 4.2.1
|
||||
|
||||
openid-client@6.8.1:
|
||||
dependencies:
|
||||
jose: 6.1.3
|
||||
|
|
@ -13024,6 +13087,10 @@ snapshots:
|
|||
|
||||
punycode@2.3.1: {}
|
||||
|
||||
pusher-js@8.4.0:
|
||||
dependencies:
|
||||
tweetnacl: 1.0.3
|
||||
|
||||
qs@6.14.1:
|
||||
dependencies:
|
||||
side-channel: 1.1.0
|
||||
|
|
@ -13730,6 +13797,8 @@ snapshots:
|
|||
|
||||
tw-animate-css@1.4.0: {}
|
||||
|
||||
tweetnacl@1.0.3: {}
|
||||
|
||||
type-check@0.4.0:
|
||||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
|
|
@ -13879,6 +13948,8 @@ snapshots:
|
|||
|
||||
uuid@11.1.0: {}
|
||||
|
||||
uuid@13.0.0: {}
|
||||
|
||||
uuid@9.0.1: {}
|
||||
|
||||
validate-npm-package-license@3.0.4:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue