add composio tools

This commit is contained in:
Ramnique Singh 2025-07-03 15:19:48 +05:30
parent 8038d52495
commit 078f785a9e
27 changed files with 2514 additions and 140 deletions

View file

@ -0,0 +1,226 @@
"use server";
import { z } from "zod";
import {
listToolkits as libListToolkits,
listTools as libListTools,
getConnectedAccount as libGetConnectedAccount,
deleteConnectedAccount as libDeleteConnectedAccount,
listAuthConfigs as libListAuthConfigs,
createAuthConfig as libCreateAuthConfig,
getToolkit as libGetToolkit,
createConnectedAccount as libCreateConnectedAccount,
getAuthConfig as libGetAuthConfig,
deleteAuthConfig as libDeleteAuthConfig,
ZToolkit,
ZGetToolkitResponse,
ZTool,
ZListResponse,
ZCreateConnectedAccountResponse,
ZAuthScheme,
ZCredentials,
} from "@/app/lib/composio/composio";
import { ComposioConnectedAccount } from "@/app/lib/types/project_types";
import { getProjectConfig, projectAuthCheck } from "./project_actions";
import { projectsCollection } from "../lib/mongodb";
const ZCreateCustomConnectedAccountRequest = z.object({
toolkitSlug: z.string(),
authConfig: z.object({
authScheme: ZAuthScheme,
credentials: ZCredentials,
}),
callbackUrl: z.string(),
});
export async function listToolkits(projectId: string, cursor: string | null = null): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZToolkit>>>> {
await projectAuthCheck(projectId);
return await libListToolkits(cursor);
}
export async function getToolkit(projectId: string, toolkitSlug: string): Promise<z.infer<typeof ZGetToolkitResponse>> {
await projectAuthCheck(projectId);
return await libGetToolkit(toolkitSlug);
}
export async function listTools(projectId: string, toolkitSlug: string, cursor: string | null = null): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZTool>>>> {
await projectAuthCheck(projectId);
return await libListTools(toolkitSlug, cursor);
}
export async function createComposioManagedOauth2ConnectedAccount(projectId: string, toolkitSlug: string, callbackUrl: string): Promise<z.infer<typeof ZCreateConnectedAccountResponse>> {
await projectAuthCheck(projectId);
// fetch managed auth configs
const configs = await libListAuthConfigs(toolkitSlug, null, true);
// check if managed oauth2 config exists
let authConfigId: string | undefined = undefined;
const authConfig = configs.items.find(config => config.auth_scheme === 'OAUTH2' && config.is_composio_managed);
authConfigId = authConfig?.id;
if (!authConfig) {
// create a new managed oauth2 auth config
const newAuthConfig = await libCreateAuthConfig({
toolkit: {
slug: toolkitSlug,
},
auth_config: {
type: 'use_composio_managed_auth',
name: 'composio-managed-oauth2',
},
});
authConfigId = newAuthConfig.auth_config.id;
}
if (!authConfigId) {
throw new Error(`No managed oauth2 auth config found for toolkit ${toolkitSlug}`);
}
// create new connected account
const response = await libCreateConnectedAccount({
auth_config: {
id: authConfigId,
},
connection: {
user_id: projectId,
callback_url: callbackUrl,
},
});
// update project with new connected account
const key = `composioConnectedAccounts.${toolkitSlug}`;
const data: z.infer<typeof ComposioConnectedAccount> = {
id: response.id,
authConfigId: authConfigId,
status: 'INITIATED',
createdAt: new Date().toISOString(),
lastUpdatedAt: new Date().toISOString(),
}
await projectsCollection.updateOne({ _id: projectId }, { $set: { [key]: data } });
return response;
}
export async function createCustomConnectedAccount(projectId: string, request: z.infer<typeof ZCreateCustomConnectedAccountRequest>): Promise<z.infer<typeof ZCreateConnectedAccountResponse>> {
await projectAuthCheck(projectId);
// first, create the auth config
const authConfig = await libCreateAuthConfig({
toolkit: {
slug: request.toolkitSlug,
},
auth_config: {
type: 'use_custom_auth',
authScheme: request.authConfig.authScheme,
credentials: request.authConfig.credentials,
name: `pid-${projectId}-${Date.now()}`,
},
});
// then, create the connected account
let state = undefined;
if (request.authConfig.authScheme !== 'OAUTH2') {
state = {
authScheme: request.authConfig.authScheme,
val: {
status: 'ACTIVE' as const,
...request.authConfig.credentials,
},
};
}
const response = await libCreateConnectedAccount({
auth_config: {
id: authConfig.auth_config.id,
},
connection: {
state,
user_id: projectId,
callback_url: request.callbackUrl,
},
});
// update project with new connected account
const key = `composioConnectedAccounts.${request.toolkitSlug}`;
const data: z.infer<typeof ComposioConnectedAccount> = {
id: response.id,
authConfigId: authConfig.auth_config.id,
status: 'INITIATED',
createdAt: new Date().toISOString(),
lastUpdatedAt: new Date().toISOString(),
}
await projectsCollection.updateOne({ _id: projectId }, { $set: { [key]: data } });
// return the connected account
return response;
}
export async function syncConnectedAccount(projectId: string, toolkitSlug: string, connectedAccountId: string): Promise<z.infer<typeof ComposioConnectedAccount>> {
await projectAuthCheck(projectId);
// ensure that the connected account belongs to this project
const project = await getProjectConfig(projectId);
const account = project.composioConnectedAccounts?.[toolkitSlug];
if (!account || account.id !== connectedAccountId) {
throw new Error(`Connected account ${connectedAccountId} not found in project ${projectId}`);
}
// if account is already active, nothing to sync
if (account.status === 'ACTIVE') {
return account;
}
// get the connected account
const response = await libGetConnectedAccount(connectedAccountId);
// update project with new connected account
const key = `composioConnectedAccounts.${response.toolkit.slug}`;
switch (response.status) {
case 'INITIALIZING':
case 'INITIATED':
account.status = 'INITIATED';
break;
case 'ACTIVE':
account.status = 'ACTIVE';
break;
default:
account.status = 'FAILED';
break;
}
account.lastUpdatedAt = new Date().toISOString();
await projectsCollection.updateOne({ _id: projectId }, { $set: { [key]: account } });
return account;
}
export async function deleteConnectedAccount(projectId: string, toolkitSlug: string, connectedAccountId: string): Promise<boolean> {
await projectAuthCheck(projectId);
// ensure that the connected account belongs to this project
const project = await getProjectConfig(projectId);
const account = project.composioConnectedAccounts?.[toolkitSlug];
if (!account || account.id !== connectedAccountId) {
throw new Error(`Connected account ${connectedAccountId} not found in project ${projectId} for toolkit ${toolkitSlug}`);
}
// delete the connected account
await libDeleteConnectedAccount(connectedAccountId);
// get auth config data
const authConfig = await libGetAuthConfig(account.authConfigId);
// delete the auth config if it is NOT managed by composio
if (!authConfig.is_composio_managed) {
await libDeleteAuthConfig(account.authConfigId);
}
// update project with deleted connected account
const key = `composioConnectedAccounts.${toolkitSlug}`;
await projectsCollection.updateOne({ _id: projectId }, { $unset: { [key]: "" } });
return true;
}
export async function updateComposioSelectedTools(projectId: string, tools: z.infer<typeof ZTool>[]): Promise<void> {
await projectAuthCheck(projectId);
// update project with new selected tools
await projectsCollection.updateOne({ _id: projectId }, { $set: { composioSelectedTools: tools } });
}

View file

@ -11,7 +11,7 @@ import { check_query_limit } from "../lib/rate_limiting";
import { QueryLimitError } from "../lib/client_utils";
import { projectAuthCheck } from "./project_actions";
import { redisClient } from "../lib/redis";
import { fetchProjectMcpTools } from "../lib/project_tools";
import { collectProjectTools } from "../lib/project_tools";
import { mergeProjectTools } from "../lib/types/project_types";
import { authorizeUserAction, logUsage } from "./billing_actions";
import { USE_BILLING } from "../lib/feature_flags";
@ -46,12 +46,12 @@ export async function getCopilotResponseStream(
}
// Get MCP tools from project and merge with workflow tools
const mcpTools = await fetchProjectMcpTools(projectId);
const projectTools = await collectProjectTools(projectId);
// Convert workflow to copilot format with both workflow and project tools
const wflow = {
...current_workflow_config,
tools: mergeProjectTools(current_workflow_config.tools, mcpTools)
tools: mergeProjectTools(current_workflow_config.tools, projectTools)
};
// prepare request
@ -98,12 +98,12 @@ export async function getCopilotAgentInstructions(
}
// Get MCP tools from project and merge with workflow tools
const mcpTools = await fetchProjectMcpTools(projectId);
const projectTools = await collectProjectTools(projectId);
// Convert workflow to copilot format with both workflow and project tools
const wflow = {
...current_workflow_config,
tools: mergeProjectTools(current_workflow_config.tools, mcpTools)
tools: mergeProjectTools(current_workflow_config.tools, projectTools)
};
// prepare request

View file

@ -296,37 +296,6 @@ export async function getSelectedMcpTools(projectId: string, serverName: string)
return server.tools.map(t => t.id);
}
export async function listProjectMcpTools(projectId: string): Promise<z.infer<typeof WorkflowTool>[]> {
await projectAuthCheck(projectId);
try {
// Get project's MCP servers and their tools
const project = await projectsCollection.findOne({ _id: projectId });
if (!project?.mcpServers) return [];
// Convert MCP tools to workflow tools format, but only from ready servers
return project.mcpServers
.filter(server => server.isReady) // Only include tools from ready servers
.flatMap(server => {
return server.tools.map(tool => ({
name: tool.name,
description: tool.description || "",
parameters: {
type: 'object' as const,
properties: tool.parameters?.properties || {},
required: tool.parameters?.required || []
},
isMcp: true,
mcpServerName: server.name,
mcpServerURL: server.serverUrl,
}));
});
} catch (error) {
console.error('Error fetching project tools:', error);
return [];
}
}
export async function testMcpTool(
projectId: string,
serverName: string,

View file

@ -14,6 +14,8 @@ import { USE_AUTH } from "../lib/feature_flags";
import { deleteMcpServerInstance, listActiveServerInstances } from "./klavis_actions";
import { authorizeUserAction } from "./billing_actions";
import { Workflow } from "../lib/types/workflow_types";
import { WorkflowTool } from "../lib/types/workflow_types";
import { collectProjectTools as libCollectProjectTools } from "../lib/project_tools";
const KLAVIS_API_KEY = process.env.KLAVIS_API_KEY || '';
@ -344,3 +346,8 @@ export async function createProjectFromWorkflowJson(formData: FormData): Promise
});
return { id: projectId };
}
export async function collectProjectTools(projectId: string): Promise<z.infer<typeof WorkflowTool>[]> {
await projectAuthCheck(projectId);
return libCollectProjectTools(projectId);
}

View file

@ -7,7 +7,7 @@ import { ApiRequest, ApiResponse } from "../../../../lib/types/types";
import { check_query_limit } from "../../../../lib/rate_limiting";
import { PrefixLogger } from "../../../../lib/utils";
import { TestProfile } from "@/app/lib/types/testing_types";
import { fetchProjectMcpTools } from "@/app/lib/project_tools";
import { collectProjectTools } from "@/app/lib/project_tools";
import { authorize, getCustomerIdForProject, logUsage } from "@/app/lib/billing";
import { USE_BILLING } from "@/app/lib/feature_flags";
import { getResponse } from "@/app/lib/agents";
@ -62,7 +62,7 @@ export async function POST(
}
// fetch project tools
const projectTools = await fetchProjectMcpTools(projectId);
const projectTools = await collectProjectTools(projectId);
// if workflow id is provided in the request, use it, else use the published workflow id
let workflowId = result.data.workflowId ?? project.publishedWorkflowId;

View file

@ -6,7 +6,7 @@ import { ObjectId, WithId } from "mongodb";
import { authCheck } from "../../../utils";
import { check_query_limit } from "../../../../../../lib/rate_limiting";
import { PrefixLogger } from "../../../../../../lib/utils";
import { fetchProjectMcpTools } from "@/app/lib/project_tools";
import { collectProjectTools } from "@/app/lib/project_tools";
import { authorize, getCustomerIdForProject, logUsage } from "@/app/lib/billing";
import { USE_BILLING } from "@/app/lib/feature_flags";
import { getResponse } from "@/app/lib/agents";
@ -182,7 +182,7 @@ export async function POST(
}
// fetch project tools
const projectTools = await fetchProjectMcpTools(session.projectId);
const projectTools = await collectProjectTools(session.projectId);
// fetch workflow
const workflow = await agentWorkflowsCollection.findOne({

View file

@ -0,0 +1,72 @@
'use client';
import { useEffect, useState } from 'react';
import { CheckCircle, XCircle } from 'lucide-react';
export default function OAuth2CallbackPage() {
const [isVisible, setIsVisible] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
// Small delay for smooth animation
const timer = setTimeout(() => setIsVisible(true), 100);
// Check for error parameters in URL
const urlParams = new URLSearchParams(window.location.search);
const error = urlParams.get('error');
const errorDescription = urlParams.get('error_description');
if (error) {
setIsError(true);
}
// Send message to parent window that OAuth is complete
if (window.opener) {
window.opener.postMessage({
type: 'OAUTH_COMPLETE',
success: !error,
error: error || null,
errorDescription: errorDescription || null,
timestamp: Date.now()
}, window.location.origin);
// Close this window after a short delay
setTimeout(() => {
window.close();
}, 3000);
}
return () => clearTimeout(timer);
}, []);
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4">
<div className={`max-w-md w-full bg-white rounded-lg shadow-lg p-8 text-center transition-all duration-500 ${
isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'
}`}>
<div className="mb-6">
{isError ? (
<XCircle className="w-16 h-16 text-red-500 mx-auto" />
) : (
<CheckCircle className="w-16 h-16 text-green-500 mx-auto" />
)}
</div>
<h1 className="text-2xl font-semibold text-gray-900 mb-4">
{isError ? 'OAuth2 Flow Failed' : 'OAuth2 Flow Completed'}
</h1>
<p className="text-gray-600 mb-6">
{isError
? 'There was an issue with the authentication. Please try again.'
: 'Your authentication was successful. You can safely close this page now.'
}
</p>
<div className="text-sm text-gray-500">
This window will automatically close in a few seconds...
</div>
</div>
</div>
);
}

View file

@ -6,11 +6,12 @@ import { createOpenAI } from "@ai-sdk/openai";
import { CoreMessage, embed, generateText } from "ai";
import { ObjectId } from "mongodb";
import { z } from "zod";
import { Composio } from '@composio/core';
// Internal dependencies
import { embeddingModel } from '../lib/embedding';
import { getMcpClient } from "./mcp";
import { dataSourceDocsCollection, dataSourcesCollection } from "./mongodb";
import { dataSourceDocsCollection, dataSourcesCollection, projectsCollection } from "./mongodb";
import { qdrantClient } from '../lib/qdrant';
import { EmbeddingRecord } from "./types/datasource_types";
import { ConnectedEntity, sanitizeTextWithMentions, Workflow, WorkflowAgent, WorkflowPrompt, WorkflowTool } from "./types/workflow_types";
@ -192,6 +193,44 @@ async function invokeMcpTool(
return result;
}
// Helper to handle composio tool calls
async function invokeComposioTool(
logger: PrefixLogger,
projectId: string,
name: string,
composioData: z.infer<typeof WorkflowTool>['composioData'] & {},
input: any,
) {
logger = logger.child(`invokeComposioTool`);
logger.log(`projectId: ${projectId}`);
logger.log(`name: ${name}`);
logger.log(`input: ${JSON.stringify(input)}`);
const { slug, toolkitSlug, noAuth } = composioData;
let connectedAccountId: string | undefined = undefined;
if (!noAuth) {
const project = await projectsCollection.findOne({ _id: projectId });
if (!project) {
throw new Error(`project ${projectId} not found`);
}
connectedAccountId = project.composioConnectedAccounts?.[toolkitSlug]?.id;
if (!connectedAccountId) {
throw new Error(`connected account id not found for project ${projectId} and toolkit ${toolkitSlug}`);
}
}
const composio = new Composio();
const result = await composio.tools.execute(slug, {
userId: projectId,
arguments: input,
connectedAccountId: connectedAccountId,
});
logger.log(`composio tool result: ${JSON.stringify(result)}`);
return result.data;
}
// Helper to create RAG tool
function createRagTool(
logger: PrefixLogger,
@ -291,6 +330,44 @@ function createMcpTool(
});
}
// Helper to create a composio tool
function createComposioTool(
logger: PrefixLogger,
config: z.infer<typeof WorkflowTool>,
projectId: string
): Tool {
const { name, description, parameters, composioData } = config;
if (!composioData) {
throw new Error(`composio data not found for tool ${name}`);
}
return tool({
name,
description,
strict: false,
parameters: {
type: 'object',
properties: parameters.properties,
required: parameters.required || [],
additionalProperties: true,
},
async execute(input: any) {
try {
const result = await invokeComposioTool(logger, projectId, name, composioData, input);
return JSON.stringify({
result,
});
} catch (error) {
logger.log(`Error executing composio tool ${name}:`, error);
return JSON.stringify({
error: `Tool execution failed: ${error}`,
});
}
}
});
}
// Helper to create an agent
function createAgent(
logger: PrefixLogger,
@ -594,6 +671,9 @@ function createTools(logger: PrefixLogger, workflow: z.infer<typeof Workflow>, t
if (config.isMcp) {
tools[toolName] = createMcpTool(logger, config, workflow.projectId);
logger.log(`created mcp tool: ${toolName}`);
} else if (config.isComposio) {
tools[toolName] = createComposioTool(logger, config, workflow.projectId);
logger.log(`created composio tool: ${toolName}`);
} else if (config.mockTool) {
tools[toolName] = createMockTool(logger, config);
logger.log(`created mock tool: ${toolName}`);

View file

@ -0,0 +1,410 @@
import { z } from "zod";
import { PrefixLogger } from "../utils";
const BASE_URL = 'https://backend.composio.dev/api/v3';
const COMPOSIO_API_KEY = process.env.COMPOSIO_API_KEY || "";
export const ZAuthScheme = z.enum([
'API_KEY',
'BASIC',
'BASIC_WITH_JWT',
'BEARER_TOKEN',
'BILLCOM_AUTH',
'CALCOM_AUTH',
'COMPOSIO_LINK',
'GOOGLE_SERVICE_ACCOUNT',
'NO_AUTH',
'OAUTH1',
'OAUTH2',
]);
export const ZConnectedAccountStatus = z.enum([
'INITIALIZING',
'INITIATED',
'ACTIVE',
'FAILED',
'EXPIRED',
'INACTIVE',
]);
export const ZToolkit = z.object({
slug: z.string(),
name: z.string(),
meta: z.object({
description: z.string(),
logo: z.string(),
tools_count: z.number(),
}),
no_auth: z.boolean(),
auth_schemes: z.array(ZAuthScheme),
composio_managed_auth_schemes: z.array(ZAuthScheme),
});
export const ZComposioField = z.object({
name: z.string(),
displayName: z.string(),
type: z.string(),
description: z.string(),
required: z.boolean(),
default: z.string().nullable().optional(),
});
export const ZGetToolkitResponse = z.object({
slug: z.string(),
name: z.string(),
composio_managed_auth_schemes: z.array(ZAuthScheme),
auth_config_details: z.array(z.object({
name: z.string(),
mode: ZAuthScheme,
fields: z.object({
auth_config_creation: z.object({
required: z.array(ZComposioField),
optional: z.array(ZComposioField),
}),
connected_account_initiation: z.object({
required: z.array(ZComposioField),
optional: z.array(ZComposioField),
}),
})
})).nullable(),
});
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.any()),
required: z.array(z.string()).optional(),
additionalProperties: z.boolean().optional(),
}),
no_auth: z.boolean(),
});
export const ZAuthConfig = z.object({
id: z.string(),
is_composio_managed: z.boolean(),
auth_scheme: ZAuthScheme,
});
export const ZCredentials = z.record(z.string(), z.union([z.string(), z.number(), z.boolean()]));
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(),
restrict_to_following_tools: z.array(z.string()).optional(),
}),
z.object({
type: z.literal('use_custom_auth'),
authScheme: ZAuthScheme,
credentials: ZCredentials,
name: z.string().optional(),
proxy_config: z.object({
proxy_url: z.string(),
proxy_auth_key: z.string().optional(),
}).optional(),
restrict_to_following_tools: z.array(z.string()).optional(),
}),
]).optional(),
});
/*
{
"toolkit": {
"slug": "github"
},
"auth_config": {
"id": "ac_ZiLwFAWuGA7G",
"auth_scheme": "OAUTH2",
"is_composio_managed": false,
"restrict_to_following_tools": []
}
}
*/
export const ZCreateAuthConfigResponse = z.object({
toolkit: z.object({
slug: z.string(),
}),
auth_config: ZAuthConfig,
});
const ZConnectionData = z.object({
authScheme: ZAuthScheme,
val: z.record(z.string(), z.unknown())
.and(z.object({
status: ZConnectedAccountStatus,
})),
});
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(),
}),
});
/*
{
"id": "ca_vTkCeLZSGab-",
"connectionData": {
"authScheme": "OAUTH2",
"val": {
"status": "INITIATED",
"code_verifier": "cd0103c5d8836a387adab1635b65ff0d2f51f77a1a79b7ff",
"redirectUrl": "https://backend.composio.dev/api/v3/s/DbTOWAyR",
"callback_url": "https://backend.composio.dev/api/v1/auth-apps/add"
}
},
"status": "INITIATED",
"redirect_url": "https://backend.composio.dev/api/v3/s/DbTOWAyR",
"redirect_uri": "https://backend.composio.dev/api/v3/s/DbTOWAyR",
"deprecated": {
"uuid": "fe66d24b-59d8-4abf-adb2-d8f74353da9e",
"authConfigUuid": "8c4d4c84-56e2-4a80-aa59-9e84503381d8"
}
}
*/
export const ZCreateConnectedAccountResponse = z.object({
id: z.string(),
connectionData: ZConnectionData,
});
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,
});
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(),
}),
});
export const ZError = z.object({
error: z.enum([
'CUSTOM_OAUTH2_CONFIG_REQUIRED',
]),
});
export const ZDeleteOperationResponse = z.object({
success: z.boolean(),
});
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(),
});
export async function composioApiCall<T extends z.ZodTypeAny>(
schema: T,
url: string,
options: RequestInit = {},
): Promise<z.infer<T>> {
const logger = new PrefixLogger('composioApiCall');
logger.log(`[${options.method || 'GET'}] ${url}`, options);
const then = Date.now();
try {
const response = await fetch(url, {
...options,
headers: {
...options.headers,
"x-api-key": COMPOSIO_API_KEY,
...(options.method === 'POST' ? {
"Content-Type": "application/json",
} : {}),
},
});
const duration = Date.now() - then;
logger.log(`Took: ${duration}ms`);
const data = await response.json();
if ('error' in data) {
const response = ZErrorResponse.parse(data);
throw new Error(`(code: ${response.error.error_code}): ${response.error.message}: ${response.error.suggested_fix}: ${response.error.errors?.join(', ')}`);
}
return schema.parse(data);
} catch (error) {
logger.log(`Error:`, error);
throw error;
}
}
export async function listToolkits(cursor: string | null = null): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZToolkit>>>> {
const url = new URL(`${BASE_URL}/toolkits`);
// set params
url.searchParams.set("sort_by", "usage");
if (cursor) {
url.searchParams.set("cursor", cursor);
}
// fetch
return composioApiCall(ZListResponse(ZToolkit), url.toString());
}
export async function getToolkit(toolkitSlug: string): Promise<z.infer<typeof ZGetToolkitResponse>> {
const url = new URL(`${BASE_URL}/toolkits/${toolkitSlug}`);
return composioApiCall(ZGetToolkitResponse, url.toString());
}
export async function listTools(toolkitSlug: string, cursor: string | null = null): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZTool>>>> {
const url = new URL(`${BASE_URL}/tools`);
// set params
url.searchParams.set("toolkit_slug", toolkitSlug);
if (cursor) {
url.searchParams.set("cursor", cursor);
}
// fetch
return composioApiCall(ZListResponse(ZTool), url.toString());
}
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");
}
// fetch
return composioApiCall(ZListResponse(ZAuthConfig), url.toString());
}
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),
});
}
export async function getAuthConfig(authConfigId: string): Promise<z.infer<typeof ZAuthConfig>> {
const url = new URL(`${BASE_URL}/auth_configs/${authConfigId}`);
return composioApiCall(ZAuthConfig, url.toString());
}
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',
});
}
// export async function createComposioManagedOauth2AuthConfig(toolkitSlug: string): Promise<z.infer<typeof ZAuthConfig>> {
// const response = await createAuthConfig({
// toolkit: {
// slug: toolkitSlug,
// },
// auth_config: {
// type: 'use_composio_managed_auth',
// },
// });
// return response.auth_config;
// }
// export async function autocreateOauth2Integration(toolkitSlug: string): Promise<z.infer<typeof ZAuthConfig | typeof ZError>> {
// // fetch toolkit
// const toolkit = await getToolkit(toolkitSlug);
// // ensure oauth2 is supported
// if (!toolkit.auth_config_details?.some(config => config.mode === 'OAUTH2')) {
// throw new Error(`OAuth2 is not supported for toolkit ${toolkitSlug}`);
// }
// // fetch existing auth configs
// const authConfigs = await fetchAuthConfigs(toolkitSlug);
// // find a valid oauth2 config
// const oauth2AuthConfig = authConfigs.items.find(config => config.auth_scheme === 'OAUTH2');
// // if valid auth config, return it
// if (oauth2AuthConfig) {
// return oauth2AuthConfig;
// }
// // check if composio managed oauth2 is supported
// if (toolkit.composio_managed_auth_schemes.includes('OAUTH2')) {
// return await createComposioManagedOauth2AuthConfig(toolkitSlug);
// }
// // else return error
// return {
// error: 'CUSTOM_OAUTH2_CONFIG_REQUIRED',
// };
// }
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),
});
}
// export async function createOauth2ConnectedAccount(toolkitSlug: string, userId: string, callbackUrl: string): Promise<z.infer<typeof ZCreateConnectedAccountResponse | typeof ZError>> {
// // fetch auth config
// const authConfig = await autocreateOauth2Integration(toolkitSlug);
// // if error, return error
// if ('error' in authConfig) {
// return authConfig;
// }
// // create connected account
// return await createConnectedAccount({
// auth_config: {
// id: authConfig.id,
// },
// connection: {
// user_id: userId,
// callback_url: callbackUrl,
// },
// });
// }
export async function getConnectedAccount(connectedAccountId: string): Promise<z.infer<typeof ZConnectedAccount>> {
const url = new URL(`${BASE_URL}/connected_accounts/${connectedAccountId}`);
return await composioApiCall(ZConnectedAccount, url.toString());
}
export async function deleteConnectedAccount(connectedAccountId: string): Promise<z.infer<typeof ZDeleteOperationResponse>> {
const url = new URL(`${BASE_URL}/connected_accounts/${connectedAccountId}`);
return await composioApiCall(ZDeleteOperationResponse, url.toString(), {
method: 'DELETE',
});
}

View file

@ -6,6 +6,7 @@ export const USE_AUTH = process.env.USE_AUTH === 'true';
export const USE_RAG_S3_UPLOADS = process.env.USE_RAG_S3_UPLOADS === 'true';
export const USE_GEMINI_FILE_PARSING = process.env.USE_GEMINI_FILE_PARSING === 'true';
export const USE_BILLING = process.env.USE_BILLING === 'true';
export const USE_COMPOSIO_TOOLS = process.env.USE_COMPOSIO_TOOLS === 'true';
// Hardcoded flags
export const USE_MULTIPLE_PROJECTS = true;

View file

@ -2,50 +2,59 @@ import { z } from "zod";
import { projectsCollection } from "./mongodb";
import { WorkflowTool } from "./types/workflow_types";
export async function fetchProjectMcpTools(projectId: string): Promise<z.infer<typeof WorkflowTool>[]> {
// Get project's MCP servers and their tools
const project = await projectsCollection.findOne({ _id: projectId });
if (!project?.mcpServers) return [];
export async function collectProjectTools(projectId: string): Promise<z.infer<typeof WorkflowTool>[]> {
const tools: z.infer<typeof WorkflowTool>[] = [];
console.log('[MCP] Getting tools from project:', {
serverCount: project.mcpServers.length,
servers: project.mcpServers.map(s => ({
name: s.name,
isReady: s.isReady,
toolCount: s.tools.length,
tools: s.tools.map(t => ({
name: t.name,
hasParams: !!t.parameters,
paramCount: t.parameters ? Object.keys(t.parameters.properties).length : 0,
required: t.parameters?.required || []
}))
}))
});
// Get project data
const project = await projectsCollection.findOne({ _id: projectId });
if (!project) {
throw new Error(`Project ${projectId} not found`);
}
// Convert MCP tools to workflow tools format, but only from ready servers
const mcpTools = project.mcpServers
.filter(server => server.isReady) // Only include tools from ready servers
.flatMap(server => {
return server.tools.map(tool => ({
name: tool.name,
if (project.mcpServers) {
for (const server of project.mcpServers) {
if (server.isReady) {
for (const tool of server.tools) {
tools.push({
name: tool.name,
description: tool.description || "",
parameters: {
type: 'object' as const,
properties: tool.parameters?.properties || {},
required: tool.parameters?.required || []
},
isMcp: true,
mcpServerName: server.name,
mcpServerURL: server.serverUrl,
});
}
}
}
}
// Add Composio tools
if (project.composioSelectedTools) {
for (const tool of project.composioSelectedTools) {
tools.push({
name: tool.slug,
description: tool.description || "",
parameters: {
type: 'object' as const,
properties: tool.parameters?.properties || {},
required: tool.parameters?.required || []
properties: tool.input_parameters?.properties || {},
required: tool.input_parameters?.required || []
},
isMcp: true,
mcpServerName: server.name,
mcpServerURL: server.serverUrl,
}));
});
isComposio: true,
composioData: {
slug: tool.slug,
noAuth: tool.no_auth,
toolkitName: tool.toolkit.name,
toolkitSlug: tool.toolkit.slug,
logo: tool.toolkit.logo,
},
});
}
}
console.log('[MCP] Converted tools from ready servers:', mcpTools.map(t => ({
name: t.name,
hasParams: !!t.parameters,
paramCount: t.parameters ? Object.keys(t.parameters.properties).length : 0,
required: t.parameters?.required || []
})));
return mcpTools;
return tools;
}

View file

@ -1,6 +1,19 @@
import { z } from "zod";
import { MCPServer } from "./types";
import { WorkflowTool } from "./workflow_types";
import { ZTool } from "../composio/composio";
export const ComposioConnectedAccount = z.object({
id: z.string(),
authConfigId: z.string(),
status: z.enum([
'INITIATED',
'ACTIVE',
'FAILED',
]),
createdAt: z.string().datetime(),
lastUpdatedAt: z.string().datetime(),
});
export const Project = z.object({
_id: z.string().uuid(),
@ -15,6 +28,8 @@ export const Project = z.object({
nextWorkflowNumber: z.number().optional(),
testRunCounter: z.number().default(0),
mcpServers: z.array(MCPServer).optional(),
composioConnectedAccounts: z.record(z.string(), ComposioConnectedAccount).optional(),
composioSelectedTools: z.array(ZTool).optional(),
});
export const ProjectMember = z.object({
@ -38,32 +53,11 @@ export function mergeProjectTools(
// Filter out any existing MCP tools from workflow tools
const nonMcpTools = workflowTools.filter(t => !t.isMcp);
// Merge with MCP tools
// Merge with project tools
const merged = [
...nonMcpTools,
...projectTools.map(tool => ({
...tool,
isMcp: true as const, // Ensure isMcp is set
parameters: {
type: 'object' as const,
properties: tool.parameters?.properties || {},
required: tool.parameters?.required || []
}
}))
...projectTools
];
console.log('[mergeMcpTools] Merged tools:', {
totalCount: merged.length,
nonMcpCount: nonMcpTools.length,
mcpCount: projectTools.length,
tools: merged.map(t => ({
name: t.name,
isMcp: t.isMcp,
hasParams: !!t.parameters,
paramCount: t.parameters ? Object.keys(t.parameters.properties).length : 0,
parameters: t.parameters
}))
});
return merged;
}

View file

@ -51,6 +51,14 @@ export const WorkflowTool = z.object({
isLibrary: z.boolean().default(false).optional(),
mcpServerName: z.string().optional(),
mcpServerURL: z.string().optional(),
isComposio: z.boolean().optional(), // whether this is a Composio tool
composioData: z.object({
slug: z.string(), // the slug for the Composio tool e.g. "GITHUB_CREATE_AN_ISSUE"
noAuth: z.boolean(), // whether the tool requires no authentication
toolkitName: z.string(), // the name for the Composio toolkit e.g. "GITHUB"
toolkitSlug: z.string(), // the slug for the Composio toolkit e.g. "GITHUB"
logo: z.string(), // the logo for the Composio tool
}).optional(), // the data for the Composio tool, if it is a Composio tool
});
export const Workflow = z.object({
name: z.string().optional(),

View file

@ -170,7 +170,7 @@ export function ToolConfig({
});
const [selectedParams, setSelectedParams] = useState(new Set([]));
const isReadOnly = tool.isMcp || tool.isLibrary;
const isReadOnly = tool.isMcp || tool.isLibrary || tool.isComposio;
const [nameError, setNameError] = useState<string | null>(null);
// Log when parameters are being rendered

View file

@ -0,0 +1,283 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { useParams } from 'next/navigation';
import { Button } from '@/components/ui/button';
import { Info, RefreshCw, Search } from 'lucide-react';
import clsx from 'clsx';
import { listToolkits, listTools, updateComposioSelectedTools } from '@/app/actions/composio_actions';
import { getProjectConfig } from '@/app/actions/project_actions';
import { z } from 'zod';
import { ZToolkit, ZListResponse, ZTool } from '@/app/lib/composio/composio';
import { Project } from '@/app/lib/types/project_types';
import { ComposioToolsPanel } from './ComposioToolsPanel';
import { ToolkitCard } from './ToolkitCard';
type ToolkitType = z.infer<typeof ZToolkit>;
type ToolkitListResponse = z.infer<ReturnType<typeof ZListResponse<typeof ZToolkit>>>;
type ProjectType = z.infer<typeof Project>;
export function Composio() {
const params = useParams();
const projectId = typeof params.projectId === 'string' ? params.projectId : params.projectId?.[0];
if (!projectId) throw new Error('Project ID is required');
const [toolkits, setToolkits] = useState<ToolkitType[]>([]);
const [projectConfig, setProjectConfig] = useState<ProjectType | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const [selectedToolkit, setSelectedToolkit] = useState<ToolkitType | null>(null);
const [isToolsPanelOpen, setIsToolsPanelOpen] = useState(false);
const [savingTools, setSavingTools] = useState(false);
const loadProjectConfig = useCallback(async () => {
try {
const config = await getProjectConfig(projectId);
setProjectConfig(config);
} catch (err: any) {
console.error('Error fetching project config:', err);
setError('Unable to load project configuration.');
}
}, [projectId]);
const loadAllToolkits = useCallback(async () => {
let cursor: string | null = null;
let allToolkits: ToolkitType[] = [];
try {
setLoading(true);
do {
const response: ToolkitListResponse = await listToolkits(projectId, cursor);
allToolkits = [...allToolkits, ...response.items];
cursor = response.next_cursor;
} while (cursor !== null);
// // Only show those toolkits that
// // - either do not require authentication, OR
// // - have oauth2 managed by Composio
// const filteredToolkits = allToolkits.filter(toolkit => {
// const noAuth = toolkit.no_auth;
// const hasOAuth2 = toolkit.auth_schemes.includes('OAUTH2');
// const hasComposioManagedOAuth2 = toolkit.composio_managed_auth_schemes.includes('OAUTH2');
// return noAuth || hasOAuth2;
// });
setToolkits(allToolkits);
setError(null);
} catch (err: any) {
setError('Unable to load all Composio toolkits. Please check your connection and try again.');
console.error('Error fetching all toolkits:', err);
setToolkits([]);
} finally {
setLoading(false);
}
}, [projectId]);
const handleManageTools = useCallback((toolkit: ToolkitType) => {
setSelectedToolkit(toolkit);
setIsToolsPanelOpen(true);
}, []);
const handleCloseToolsPanel = useCallback(() => {
setSelectedToolkit(null);
setIsToolsPanelOpen(false);
}, []);
const handleProjectConfigUpdate = useCallback(() => {
loadProjectConfig();
}, [loadProjectConfig]);
const handleUpdateToolsSelection = useCallback(async (selectedToolObjects: z.infer<typeof ZTool>[]) => {
if (!projectId) return;
setSavingTools(true);
try {
// Get existing selected tools from project config
const existingSelectedTools = projectConfig?.composioSelectedTools || [];
// Create a map of existing tools by slug for easy lookup
const existingToolsMap = new Map(existingSelectedTools.map(tool => [tool.slug, tool]));
// Add or update the new selections
for (const tool of selectedToolObjects) {
existingToolsMap.set(tool.slug, tool);
}
// Convert back to array
const mergedSelectedTools = Array.from(existingToolsMap.values());
await updateComposioSelectedTools(projectId, mergedSelectedTools);
// Refresh project config to get updated data
await loadProjectConfig();
} catch (error) {
console.error('Error saving tool selection:', error);
} finally {
setSavingTools(false);
}
}, [projectId, projectConfig, loadProjectConfig]);
const handleRemoveToolkitTools = useCallback(async (toolkitSlug: string) => {
if (!projectId) return;
setSavingTools(true);
try {
// Get existing selected tools from project config
const existingSelectedTools = projectConfig?.composioSelectedTools || [];
// Filter out all tools from the specified toolkit
const filteredSelectedTools = existingSelectedTools.filter(tool =>
tool.toolkit.slug !== toolkitSlug
);
await updateComposioSelectedTools(projectId, filteredSelectedTools);
// Refresh project config to get updated data
await loadProjectConfig();
} catch (error) {
console.error('Error removing toolkit tools:', error);
} finally {
setSavingTools(false);
}
}, [projectId, projectConfig, loadProjectConfig]);
useEffect(() => {
loadProjectConfig();
}, [loadProjectConfig]);
useEffect(() => {
loadAllToolkits();
}, [loadAllToolkits]);
const filteredToolkits = toolkits.filter(toolkit => {
const searchLower = searchQuery.toLowerCase();
return (
toolkit.name.toLowerCase().includes(searchLower) ||
toolkit.meta.description.toLowerCase().includes(searchLower) ||
toolkit.slug.toLowerCase().includes(searchLower)
);
});
if (loading) {
return (
<div className="text-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-800 dark:border-gray-200 mx-auto"></div>
<p className="mt-4 text-sm text-gray-600 dark:text-gray-400">Loading Composio toolkits...</p>
</div>
);
}
if (error) {
return (
<div className="flex flex-col items-center justify-center h-[50vh] space-y-6 px-4">
<p className="text-center text-red-500 dark:text-red-400 max-w-[600px]">
{error}
</p>
<Button
variant="secondary"
onClick={() => {
loadProjectConfig();
loadAllToolkits();
}}
>
<RefreshCw className="h-4 w-4 mr-2" />
Try Again
</Button>
</div>
);
}
return (
<div className="space-y-6">
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-100 dark:border-blue-800 rounded-lg p-4">
<div className="flex gap-3">
<div className="shrink-0">
<Info className="h-5 w-5 text-blue-600 dark:text-blue-400" />
</div>
</div>
</div>
<div className="flex flex-col gap-6">
<div className="flex items-center justify-between gap-4">
<div className="flex-1 flex items-center gap-4">
<div className="relative flex-1">
<div className="absolute inset-y-0 left-2 flex items-center pointer-events-none">
<Search className="h-4 w-4 text-gray-400 dark:text-gray-500" />
</div>
<input
type="text"
placeholder="Search toolkits..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-8 pr-4 py-2 text-sm border border-gray-200 dark:border-gray-700 rounded-md
bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100
placeholder-gray-400 dark:placeholder-gray-500
focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400
hover:border-gray-300 dark:hover:border-gray-600 transition-colors"
/>
</div>
<div className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
{filteredToolkits.length} {filteredToolkits.length === 1 ? 'toolkit' : 'toolkits'}
</div>
<div className="h-4 w-px bg-gray-200 dark:bg-gray-700" />
</div>
<Button
size="sm"
variant="secondary"
onClick={() => {
loadProjectConfig();
loadAllToolkits();
}}
disabled={loading}
>
<div className="inline-flex items-center">
<RefreshCw className={clsx("h-4 w-4", loading && "animate-spin")} />
<span className="ml-2">Refresh</span>
</div>
</Button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredToolkits.map((toolkit) => {
const isConnected = toolkit.no_auth || projectConfig?.composioConnectedAccounts?.[toolkit.slug]?.status === 'ACTIVE';
const connectedAccountId = projectConfig?.composioConnectedAccounts?.[toolkit.slug]?.id;
return (
<ToolkitCard
key={toolkit.slug}
toolkit={toolkit}
projectId={projectId}
isConnected={isConnected}
connectedAccountId={connectedAccountId}
projectConfig={projectConfig}
onManageTools={() => handleManageTools(toolkit)}
onProjectConfigUpdate={handleProjectConfigUpdate}
onRemoveToolkitTools={handleRemoveToolkitTools}
/>
);
})}
</div>
{filteredToolkits.length === 0 && !loading && (
<div className="text-center py-12">
<p className="text-gray-500 dark:text-gray-400">
{searchQuery ? 'No toolkits found matching your search.' : 'No toolkits available.'}
</p>
</div>
)}
{/* Tools Panel */}
<ComposioToolsPanel
toolkit={selectedToolkit}
isOpen={isToolsPanelOpen}
onClose={handleCloseToolsPanel}
projectConfig={projectConfig}
onUpdateToolsSelection={handleUpdateToolsSelection}
isSaving={savingTools}
/>
</div>
);
}

View file

@ -0,0 +1,269 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { useParams } from 'next/navigation';
import { Button } from '@/components/ui/button';
import { PictureImg } from '@/components/ui/picture-img';
import { Checkbox } from '@heroui/react';
import { ChevronLeft, ChevronRight } from 'lucide-react';
import { listTools } from '@/app/actions/composio_actions';
import { z } from 'zod';
import { ZTool, ZListResponse } from '@/app/lib/composio/composio';
import { SlidePanel } from '@/components/ui/slide-panel';
import { Project } from '@/app/lib/types/project_types';
type ToolType = z.infer<typeof ZTool>;
type ToolListResponse = z.infer<ReturnType<typeof ZListResponse<typeof ZTool>>>;
type ProjectType = z.infer<typeof Project>;
interface ComposioToolsPanelProps {
toolkit: {
slug: string;
name: string;
meta: {
logo: string;
};
no_auth?: boolean;
} | null;
isOpen: boolean;
onClose: () => void;
projectConfig: ProjectType | null;
onUpdateToolsSelection: (selectedToolObjects: ToolType[]) => void;
isSaving: boolean;
}
export function ComposioToolsPanel({
toolkit,
isOpen,
onClose,
projectConfig,
onUpdateToolsSelection,
isSaving
}: ComposioToolsPanelProps) {
const params = useParams();
const projectId = typeof params.projectId === 'string' ? params.projectId : params.projectId?.[0];
if (!projectId) throw new Error('Project ID is required');
const [tools, setTools] = useState<ToolType[]>([]);
const [toolsLoading, setToolsLoading] = useState(false);
const [currentCursor, setCurrentCursor] = useState<string | null>(null);
const [nextCursor, setNextCursor] = useState<string | null>(null);
const [cursorHistory, setCursorHistory] = useState<string[]>([]);
const [selectedTools, setSelectedTools] = useState<Set<string>>(new Set());
const [hasChanges, setHasChanges] = useState(false);
const loadToolsForToolkit = useCallback(async (toolkitSlug: string, cursor: string | null = null) => {
try {
setToolsLoading(true);
const response: ToolListResponse = await listTools(projectId, toolkitSlug, cursor);
setTools(response.items);
setNextCursor(response.next_cursor);
if (cursor === null) {
// First page - reset pagination state
setCurrentCursor(null);
setCursorHistory([]);
}
} catch (err: any) {
console.error('Error fetching tools:', err);
setTools([]);
} finally {
setToolsLoading(false);
}
}, [projectId]);
const handleNextPage = useCallback(async () => {
if (!nextCursor || !toolkit) return;
// Add current cursor to history
setCursorHistory(prev => [...prev, currentCursor || '']);
setCurrentCursor(nextCursor);
await loadToolsForToolkit(toolkit.slug, nextCursor);
}, [nextCursor, toolkit, currentCursor, loadToolsForToolkit]);
const handlePreviousPage = useCallback(async () => {
if (cursorHistory.length === 0 || !toolkit) return;
// Get the previous cursor from history
const previousCursor = cursorHistory[cursorHistory.length - 1];
const newHistory = cursorHistory.slice(0, -1);
setCursorHistory(newHistory);
setCurrentCursor(previousCursor);
await loadToolsForToolkit(toolkit.slug, previousCursor);
}, [cursorHistory, toolkit, loadToolsForToolkit]);
const handleToolSelectionChange = useCallback((toolSlug: string, selected: boolean) => {
setSelectedTools(prev => {
const next = new Set(prev);
if (selected) {
next.add(toolSlug);
} else {
next.delete(toolSlug);
}
setHasChanges(true);
return next;
});
}, []);
const handleSaveTools = useCallback(async () => {
// Convert selected tool slugs to actual tool objects
const selectedToolObjects = tools.filter(tool => selectedTools.has(tool.slug));
await onUpdateToolsSelection(selectedToolObjects);
setHasChanges(false);
}, [onUpdateToolsSelection, selectedTools, tools]);
const handleClose = useCallback(() => {
setTools([]);
setSelectedTools(new Set());
setHasChanges(false);
if (hasChanges) {
if (window.confirm('You have unsaved changes. Are you sure you want to close?')) {
onClose();
}
} else {
onClose();
}
}, [onClose, hasChanges]);
// Initialize selected tools from project config when opening the panel
useEffect(() => {
if (toolkit && isOpen && projectConfig?.composioSelectedTools) {
const toolSlugs = new Set(projectConfig.composioSelectedTools.map(tool => tool.slug));
setSelectedTools(toolSlugs);
setHasChanges(false);
}
}, [toolkit, isOpen, projectConfig]);
useEffect(() => {
if (toolkit && isOpen) {
loadToolsForToolkit(toolkit.slug, null);
}
}, [toolkit, isOpen, loadToolsForToolkit]);
if (!toolkit) return null;
// Check if the toolkit is connected (has an active connected account) or doesn't require auth
const isToolkitConnected = toolkit.no_auth || projectConfig?.composioConnectedAccounts?.[toolkit.slug]?.status === 'ACTIVE';
return (
<SlidePanel
isOpen={isOpen}
onClose={handleClose}
title={
<div className="flex items-center gap-3">
{toolkit.meta.logo && (
<PictureImg
src={toolkit.meta.logo}
alt={`${toolkit.name} logo`}
width={24}
height={24}
className="rounded-md object-cover"
/>
)}
<span>{toolkit.name}</span>
</div>
}
>
<div className="flex flex-col h-full">
{/* Header */}
<div className="mb-6">
<div className="flex items-center justify-between">
<h4 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Available Tools</h4>
<div className="flex items-center gap-2">
{!isToolkitConnected && !toolkit.no_auth && (
<div className="text-sm text-orange-600 dark:text-orange-400 px-3 py-1 rounded-full bg-orange-50 dark:bg-orange-900/20">
Toolkit not connected
</div>
)}
{hasChanges && (
<Button
variant="primary"
size="sm"
onClick={handleSaveTools}
disabled={isSaving || !isToolkitConnected}
>
{isSaving ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-2 border-b-transparent border-white mr-2" />
Saving...
</>
) : (
'Save Changes'
)}
</Button>
)}
</div>
</div>
</div>
{/* Scrollable Tools List */}
<div className="flex-1 overflow-y-auto">
{toolsLoading ? (
<div className="text-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-800 dark:border-gray-200 mx-auto"></div>
<p className="mt-4 text-sm text-gray-600 dark:text-gray-400">Loading tools...</p>
</div>
) : (
<div className="space-y-4">
{tools.map((tool) => (
<div key={tool.slug} className={`group p-4 rounded-lg transition-all duration-200 border border-transparent ${
isToolkitConnected
? 'bg-gray-50/50 dark:bg-gray-800/50 hover:bg-gray-100/50 dark:hover:bg-gray-700/50 hover:border-gray-200 dark:hover:border-gray-600'
: 'bg-gray-100/50 dark:bg-gray-900/50 opacity-60'
}`}>
<div className="flex items-start gap-3">
<Checkbox
isSelected={selectedTools.has(tool.slug)}
onValueChange={(selected) => handleToolSelectionChange(tool.slug, selected)}
size="sm"
isDisabled={!isToolkitConnected}
/>
<div className="flex-1">
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-1">
{tool.name}
</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
{tool.description}
</p>
</div>
</div>
</div>
))}
</div>
)}
</div>
{/* Fixed Pagination Controls */}
<div className="border-t border-gray-200 dark:border-gray-700 pt-4 mt-4">
<div className="flex items-center justify-end">
<div className="flex items-center gap-2">
<Button
variant="secondary"
size="sm"
onClick={handlePreviousPage}
disabled={cursorHistory.length === 0 || toolsLoading}
>
<ChevronLeft className="h-4 w-4 mr-1" />
Previous
</Button>
<Button
variant="secondary"
size="sm"
onClick={handleNextPage}
disabled={!nextCursor || toolsLoading}
>
Next
<ChevronRight className="h-4 w-4 ml-1" />
</Button>
</div>
</div>
</div>
</div>
</SlidePanel>
);
}

View file

@ -0,0 +1,518 @@
'use client';
import { useState, useCallback, useEffect } from 'react';
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Spinner, Button as HeroButton, Input } from "@heroui/react";
import { PictureImg } from '@/components/ui/picture-img';
import { Wrench, Shield, Key, Globe, ArrowLeft } from "lucide-react";
import { getToolkit, createComposioManagedOauth2ConnectedAccount, syncConnectedAccount, listToolkits, createCustomConnectedAccount } from '@/app/actions/composio_actions';
import { z } from 'zod';
import { ZGetToolkitResponse, ZToolkit, ZComposioField, ZAuthScheme } from '@/app/lib/composio/composio';
interface ToolkitAuthModalProps {
isOpen: boolean;
onClose: () => void;
toolkitSlug: string;
projectId: string;
onComplete: () => void;
}
export function ToolkitAuthModal({
isOpen,
onClose,
toolkitSlug,
projectId,
onComplete
}: ToolkitAuthModalProps) {
const [toolkit, setToolkit] = useState<z.infer<typeof ZGetToolkitResponse> | null>(null);
const [toolkitDetails, setToolkitDetails] = useState<z.infer<typeof ZToolkit> | null>(null);
const [loading, setLoading] = useState(false);
const [processing, setProcessing] = useState(false);
const [error, setError] = useState<string | null>(null);
// Form state
const [showForm, setShowForm] = useState(false);
const [selectedAuthScheme, setSelectedAuthScheme] = useState<z.infer<typeof ZAuthScheme> | null>(null);
const [formData, setFormData] = useState<Record<string, string>>({});
// Fetch toolkit details when modal opens
useEffect(() => {
if (isOpen && toolkitSlug) {
setLoading(true);
setError(null);
// Fetch both toolkit auth details and full toolkit info
Promise.all([
getToolkit(projectId, toolkitSlug),
listToolkits(projectId).then(response =>
response.items.find(t => t.slug === toolkitSlug) || null
)
])
.then(([authDetails, fullDetails]) => {
setToolkit(authDetails);
setToolkitDetails(fullDetails);
})
.catch(err => {
console.error('Failed to fetch toolkit:', err);
setError('Failed to load toolkit details');
})
.finally(() => setLoading(false));
}
}, [isOpen, toolkitSlug, projectId]);
// Reset form state when modal closes
useEffect(() => {
if (!isOpen) {
setShowForm(false);
setSelectedAuthScheme(null);
setFormData({});
setError(null);
}
}, [isOpen]);
const handleOAuthCompletion = useCallback(async (connectedAccountId: string) => {
try {
// Sync the connected account to get the latest status
await syncConnectedAccount(projectId, toolkitSlug, connectedAccountId);
// Call completion callback
onComplete();
onClose();
} catch (error) {
console.error('Error syncing connected account after OAuth:', error);
setError('Authentication completed but failed to sync status. Please refresh and try again.');
}
}, [projectId, toolkitSlug, onComplete, onClose]);
const handleComposioOAuth2 = useCallback(async () => {
setError(null);
setProcessing(true);
try {
// Start OAuth flow
const returnUrl = `${window.location.origin}/composio/oauth2/callback`;
const response = await createComposioManagedOauth2ConnectedAccount(projectId, toolkitSlug, returnUrl);
console.log('OAuth response:', JSON.stringify(response, null, 2));
// if error, set error
if ('error' in response) {
if (response.error === 'CUSTOM_OAUTH2_CONFIG_REQUIRED') {
setError('Please set up a custom OAuth2 configuration for this toolkit in the Composio dashboard');
} else {
setError('Failed to connect to toolkit');
}
return;
}
// Open OAuth window
const authWindow = window.open(
response.connectionData.val.redirectUrl as string,
'_blank',
'width=600,height=700'
);
if (authWindow) {
// Use postMessage since we control the callback URL
const handleMessage = (event: MessageEvent) => {
// Only accept messages from our own origin
if (event.origin !== window.location.origin) {
return;
}
// Check if this is an OAuth completion message
if (event.data && event.data.type === 'OAUTH_COMPLETE') {
window.removeEventListener('message', handleMessage);
clearInterval(checkInterval);
if (event.data.success) {
// Handle successful OAuth completion
handleOAuthCompletion(response.id);
} else {
// Handle OAuth error
const errorMessage = event.data.errorDescription || event.data.error || 'OAuth authentication failed';
setError(errorMessage);
}
}
};
// Listen for postMessage from our callback page
window.addEventListener('message', handleMessage);
// Minimal fallback: check if window closes without message
const checkInterval = setInterval(() => {
if (authWindow.closed) {
clearInterval(checkInterval);
window.removeEventListener('message', handleMessage);
// If we didn't get a postMessage, still try to sync
// (in case the message was missed for some reason)
handleOAuthCompletion(response.id);
}
}, 1000); // Check less frequently since we expect postMessage
} else {
window.alert('Failed to open authentication window. Please check your popup blocker settings.');
setError('Failed to open authentication window');
}
} catch (err: any) {
console.error('OAuth flow failed:', err);
const errorMessage = err.message || 'Failed to connect to toolkit';
setError(errorMessage);
} finally {
setProcessing(false);
}
}, [projectId, toolkitSlug, handleOAuthCompletion]);
const handleCustomAuth = useCallback((authScheme: z.infer<typeof ZAuthScheme>) => {
setSelectedAuthScheme(authScheme);
// Initialize form data with default values
const authConfig = toolkit?.auth_config_details?.find(config => config.mode === authScheme);
if (authConfig) {
const initialData: Record<string, string> = {};
// Try connected_account_initiation first, fallback to auth_config_creation
const requiredFields = authConfig.fields.connected_account_initiation.required.length > 0
? authConfig.fields.connected_account_initiation.required
: authConfig.fields.auth_config_creation.required;
const optionalFields = authConfig.fields.connected_account_initiation.optional.length > 0
? authConfig.fields.connected_account_initiation.optional
: authConfig.fields.auth_config_creation.optional;
// Add defaults for required fields
requiredFields.forEach(field => {
if (field.default) {
initialData[field.name] = field.default;
}
});
// Add defaults for optional fields
optionalFields.forEach(field => {
if (field.default) {
initialData[field.name] = field.default;
}
});
setFormData(initialData);
}
setShowForm(true);
}, [toolkit]);
const handleFormSubmit = useCallback(async () => {
if (!selectedAuthScheme || !toolkit) return;
setError(null);
setProcessing(true);
try {
const callbackUrl = `${window.location.origin}/composio/oauth2/callback`;
const response = await createCustomConnectedAccount(projectId, {
toolkitSlug: toolkit.slug,
authConfig: {
authScheme: selectedAuthScheme,
credentials: formData,
},
callbackUrl,
});
console.log('Custom auth response:', JSON.stringify(response, null, 2));
// Check if we need to open a popup window (OAuth2 flow)
if ('connectionData' in response &&
response.connectionData.val &&
'redirectUrl' in response.connectionData.val &&
response.connectionData.val.redirectUrl) {
// Open OAuth window for custom OAuth2
const authWindow = window.open(
response.connectionData.val.redirectUrl as string,
'_blank',
'width=600,height=700'
);
if (authWindow) {
// Use the same postMessage logic as Composio OAuth2
const handleMessage = (event: MessageEvent) => {
if (event.origin !== window.location.origin) {
return;
}
if (event.data && event.data.type === 'OAUTH_COMPLETE') {
window.removeEventListener('message', handleMessage);
clearInterval(checkInterval);
if (event.data.success) {
handleOAuthCompletion(response.id);
} else {
const errorMessage = event.data.errorDescription || event.data.error || 'OAuth authentication failed';
setError(errorMessage);
}
}
};
window.addEventListener('message', handleMessage);
const checkInterval = setInterval(() => {
if (authWindow.closed) {
clearInterval(checkInterval);
window.removeEventListener('message', handleMessage);
handleOAuthCompletion(response.id);
}
}, 1000);
} else {
window.alert('Failed to open authentication window. Please check your popup blocker settings.');
setError('Failed to open authentication window');
}
} else {
// No redirect needed, just sync and complete
await syncConnectedAccount(projectId, toolkitSlug, response.id);
onComplete();
onClose();
}
} catch (err: any) {
console.error('Custom auth failed:', err);
const errorMessage = err.message || 'Failed to authenticate with toolkit';
setError(errorMessage);
} finally {
setProcessing(false);
}
}, [selectedAuthScheme, toolkit, projectId, formData, handleOAuthCompletion, onComplete, onClose, toolkitSlug]);
const handleBackToOptions = useCallback(() => {
setShowForm(false);
setSelectedAuthScheme(null);
setFormData({});
setError(null);
}, []);
const getAuthMethodIcon = (authScheme: string) => {
switch (authScheme) {
case 'OAUTH2':
return <Shield className="h-5 w-5" />;
case 'API_KEY':
return <Key className="h-5 w-5" />;
case 'BEARER_TOKEN':
return <Key className="h-5 w-5" />;
default:
return <Globe className="h-5 w-5" />;
}
};
const getAuthMethodName = (authScheme: string) => {
switch (authScheme) {
case 'OAUTH2':
return 'OAuth2';
case 'API_KEY':
return 'API Key';
case 'BEARER_TOKEN':
return 'Bearer Token';
case 'BASIC':
return 'Basic Auth';
default:
return authScheme.toLowerCase().replace('_', ' ');
}
};
return (
<Modal
isOpen={isOpen}
onOpenChange={onClose}
size="md"
classNames={{
base: "bg-white dark:bg-gray-900",
header: "border-b border-gray-200 dark:border-gray-800",
footer: "border-t border-gray-200 dark:border-gray-800",
}}
>
<ModalContent>
<ModalHeader className="flex gap-3 items-center">
{showForm && (
<HeroButton
variant="light"
size="sm"
isIconOnly
onPress={handleBackToOptions}
className="mr-1"
>
<ArrowLeft className="h-4 w-4" />
</HeroButton>
)}
{toolkitDetails?.meta?.logo ? (
<PictureImg
src={toolkitDetails.meta.logo}
alt={`${toolkitSlug} logo`}
className="w-8 h-8 rounded-md object-cover"
/>
) : (
<Wrench className="w-5 h-5 text-blue-500" />
)}
<span>
{showForm
? `Configure ${getAuthMethodName(selectedAuthScheme || '')}`
: `Connect to ${toolkitSlug}`
}
</span>
</ModalHeader>
<ModalBody>
{loading ? (
<div className="flex justify-center py-8">
<Spinner size="lg" />
</div>
) : error ? (
<div className="text-sm text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/20 p-3 rounded-md">
{error}
</div>
) : toolkit ? (
showForm ? (
// Form view
<div className="space-y-4">
<div className="text-sm text-gray-600 dark:text-gray-400">
Enter your credentials for {getAuthMethodName(selectedAuthScheme || '')} authentication:
</div>
{(() => {
const authConfig = toolkit.auth_config_details?.find(config => config.mode === selectedAuthScheme);
if (!authConfig) {
return <div>No configuration found for {selectedAuthScheme}</div>;
}
// Try connected_account_initiation first, fallback to auth_config_creation
const allFields = [
...authConfig.fields.connected_account_initiation.required.map(field => ({ ...field, required: true })),
...authConfig.fields.connected_account_initiation.optional.map(field => ({ ...field, required: false }))
];
// If no fields in connected_account_initiation, try auth_config_creation
if (allFields.length === 0) {
allFields.push(
...authConfig.fields.auth_config_creation.required.map(field => ({ ...field, required: true })),
...authConfig.fields.auth_config_creation.optional.map(field => ({ ...field, required: false }))
);
}
return (
<div className="space-y-4">
{allFields.map(field => (
<Input
key={field.name}
label={field.displayName}
placeholder={field.description}
value={formData[field.name] || ''}
onValueChange={(value) => setFormData(prev => ({ ...prev, [field.name]: value }))}
isRequired={field.required}
type={field.type === 'password' ? 'password' : 'text'}
variant="bordered"
description={field.description}
required={field.required}
/>
))}
</div>
);
})()}
</div>
) : (
// Auth options view
<div className="space-y-4">
<div className="text-sm text-gray-600 dark:text-gray-400">
Choose how you&apos;d like to authenticate with this toolkit:
</div>
<div className="space-y-3">
{/* OAuth2 Composio Managed */}
{toolkit.composio_managed_auth_schemes.includes('OAUTH2') && (
<HeroButton
className="w-full justify-start gap-3 h-auto py-4 px-4"
variant="bordered"
onPress={handleComposioOAuth2}
isDisabled={processing}
size="lg"
>
<div className="bg-green-100 dark:bg-green-900/20 p-2 rounded-lg">
<Shield className="h-5 w-5 text-green-600 dark:text-green-400" />
</div>
<div className="text-left">
<div className="font-medium">Connect using OAuth2</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
Secure authentication managed by Composio
</div>
</div>
{processing && <Spinner size="sm" className="ml-auto" />}
</HeroButton>
)}
{/* Custom OAuth2 - always show if OAuth2 is supported */}
{(toolkit.composio_managed_auth_schemes.includes('OAUTH2') ||
toolkit.auth_config_details?.some(config => config.mode === 'OAUTH2')) && (
<HeroButton
className="w-full justify-start gap-3 h-auto py-4 px-4"
variant="bordered"
onPress={() => handleCustomAuth('OAUTH2')}
isDisabled={processing}
size="lg"
>
<div className="bg-orange-100 dark:bg-orange-900/20 p-2 rounded-lg">
<Shield className="h-5 w-5 text-orange-600 dark:text-orange-400" />
</div>
<div className="text-left">
<div className="font-medium">Connect using custom OAuth2 app</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
Use your own OAuth2 configuration
</div>
</div>
</HeroButton>
)}
{/* Other auth schemes (excluding OAuth2 since it's shown above) */}
{toolkit.auth_config_details?.filter(config => config.mode !== 'OAUTH2').map(config => (
<HeroButton
key={config.mode}
className="w-full justify-start gap-3 h-auto py-4 px-4"
variant="bordered"
onPress={() => handleCustomAuth(config.mode)}
isDisabled={processing}
size="lg"
>
<div className="bg-blue-100 dark:bg-blue-900/20 p-2 rounded-lg">
{getAuthMethodIcon(config.mode)}
</div>
<div className="text-left">
<div className="font-medium">Connect using {getAuthMethodName(config.mode)}</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
Enter your credentials
</div>
</div>
</HeroButton>
))}
</div>
</div>
)
) : null}
</ModalBody>
<ModalFooter>
{showForm ? (
<>
<HeroButton variant="bordered" onPress={handleBackToOptions} isDisabled={processing}>
Back
</HeroButton>
<HeroButton
variant="solid"
color="primary"
onPress={handleFormSubmit}
isDisabled={processing}
isLoading={processing}
>
{processing ? 'Connecting...' : 'Connect'}
</HeroButton>
</>
) : (
<HeroButton variant="bordered" onPress={onClose}>
Cancel
</HeroButton>
)}
</ModalFooter>
</ModalContent>
</Modal>
);
}

View file

@ -0,0 +1,220 @@
'use client';
import { useState, useCallback } from 'react';
import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
import { PictureImg } from '@/components/ui/picture-img';
import { Wrench } from 'lucide-react';
import clsx from 'clsx';
import { Spinner } from '@heroui/react';
import { deleteConnectedAccount } from '@/app/actions/composio_actions';
import { z } from 'zod';
import { ZToolkit } from '@/app/lib/composio/composio';
import { Project } from '@/app/lib/types/project_types';
import { ToolkitAuthModal } from './ToolkitAuthModal';
type ToolkitType = z.infer<typeof ZToolkit>;
type ProjectType = z.infer<typeof Project>;
const toolkitCardStyles = {
base: clsx(
"group p-6 rounded-xl transition-all duration-200",
"bg-white dark:bg-gray-900 shadow-sm dark:shadow-none",
"border-2 border-gray-200/80 dark:border-gray-700/80",
"hover:shadow-md dark:hover:shadow-none",
"hover:border-blue-200 dark:hover:border-blue-900",
"min-h-[280px] flex flex-col"
),
};
interface ToolkitCardProps {
toolkit: ToolkitType;
projectId: string;
isConnected: boolean;
connectedAccountId?: string;
projectConfig: ProjectType | null;
onManageTools: () => void;
onProjectConfigUpdate: () => void;
onRemoveToolkitTools: (toolkitSlug: string) => void;
}
export function ToolkitCard({
toolkit,
projectId,
isConnected,
connectedAccountId,
projectConfig,
onManageTools,
onProjectConfigUpdate,
onRemoveToolkitTools
}: ToolkitCardProps) {
const [isProcessing, setIsProcessing] = useState(false);
const [error, setError] = useState<string | null>(null);
const [showAuthModal, setShowAuthModal] = useState(false);
const handleToggleConnection = useCallback(async () => {
const newState = !isConnected;
// Clear any previous error when starting a new operation
setError(null);
if (newState) {
// Show authentication modal
setShowAuthModal(true);
} else {
// Disconnect - remove the connected account
setIsProcessing(true);
try {
if (connectedAccountId) {
await deleteConnectedAccount(projectId, toolkit.slug, connectedAccountId);
onProjectConfigUpdate();
onRemoveToolkitTools(toolkit.slug);
} else {
// Fallback: just refresh the project config
onProjectConfigUpdate();
}
} catch (err: any) {
console.error('Disconnect failed:', err);
const errorMessage = err.message || 'Failed to disconnect toolkit';
setError(errorMessage);
} finally {
setIsProcessing(false);
}
}
}, [projectId, toolkit.slug, isConnected, connectedAccountId, onProjectConfigUpdate, onRemoveToolkitTools]);
const handleAuthComplete = useCallback(() => {
// Update project config when authentication completes
onProjectConfigUpdate();
}, [onProjectConfigUpdate]);
// Calculate selected tools count for this toolkit
const selectedToolsCount = projectConfig?.composioSelectedTools?.filter(tool =>
tool.toolkit.slug === toolkit.slug
).length || 0;
return (
<div className={toolkitCardStyles.base}>
<div className="flex flex-col h-full">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
{toolkit.meta.logo && (
<PictureImg
src={toolkit.meta.logo}
alt={`${toolkit.name} logo`}
className="w-8 h-8 rounded-md object-cover"
/>
)}
<div>
<h3 className="font-semibold text-lg text-gray-900 dark:text-gray-100">
{toolkit.name}
</h3>
<div className="flex items-center gap-2 mt-1">
<span className="px-1.5 py-0.5 rounded-full text-xs font-medium
bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300">
{toolkit.meta.tools_count} tools
</span>
{selectedToolsCount > 0 && (
<span className="px-1.5 py-0.5 rounded-full text-xs font-medium
bg-purple-50 dark:bg-purple-900/20 text-purple-700 dark:text-purple-300">
{selectedToolsCount} selected
</span>
)}
{toolkit.no_auth && (
<span className="px-1.5 py-0.5 rounded-full text-xs font-medium
bg-emerald-50 dark:bg-emerald-900/20 text-emerald-700 dark:text-emerald-300">
No Auth
</span>
)}
</div>
</div>
</div>
<div className="flex items-center gap-2">
{toolkit.no_auth ? (
<div className="flex items-center gap-2">
<Switch
checked={true}
onCheckedChange={() => {}} // No-op for no-auth toolkits
disabled={true}
className={clsx(
"data-[state=checked]:bg-emerald-500 dark:data-[state=checked]:bg-emerald-600",
"data-[state=unchecked]:bg-emerald-500 dark:data-[state=unchecked]:bg-emerald-600",
"opacity-50 cursor-not-allowed",
"scale-75"
)}
/>
<span className="text-xs text-emerald-600 dark:text-emerald-400 font-medium">
Always Available
</span>
</div>
) : (
<Switch
checked={isConnected}
onCheckedChange={handleToggleConnection}
disabled={isProcessing}
className={clsx(
"data-[state=checked]:bg-blue-500 dark:data-[state=checked]:bg-blue-600",
"data-[state=unchecked]:bg-gray-200 dark:data-[state=unchecked]:bg-gray-700",
isProcessing && "opacity-50 cursor-not-allowed",
"scale-75"
)}
/>
)}
</div>
</div>
<div className="flex-1">
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4 line-clamp-3">
{toolkit.meta.description}
</p>
</div>
<div className="mt-auto">
<div className="flex items-center justify-between">
<div className="text-xs text-gray-400 dark:text-gray-500">
ID: {toolkit.slug}
</div>
<div className="flex items-center gap-2">
{isProcessing && (
<div className="flex items-center gap-1 text-xs py-1 px-2 rounded-full text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20">
<Spinner size="sm" />
<span>Processing...</span>
</div>
)}
{(isConnected || toolkit.no_auth) && !isProcessing && (
<div className="text-xs py-1 px-2 rounded-full text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-900/20">
{toolkit.no_auth ? 'Available' : 'Connected'}
</div>
)}
{error && (
<div className="text-xs py-1 px-2 rounded-full text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/20">
Error: {error}
</div>
)}
<Button
size="sm"
variant="secondary"
onClick={onManageTools}
className="text-xs"
>
<div className="inline-flex items-center">
<Wrench className="h-3.5 w-3.5" />
<span className="ml-1.5">Tools</span>
</div>
</Button>
</div>
</div>
</div>
</div>
<ToolkitAuthModal
key={toolkit.slug}
isOpen={showAuthModal}
onClose={() => setShowAuthModal(false)}
toolkitSlug={toolkit.slug}
projectId={projectId}
onComplete={handleAuthComplete}
/>
</div>
);
}

View file

@ -4,10 +4,11 @@ import { useState } from 'react';
import { Tabs, Tab } from '@/components/ui/tabs';
import { HostedServers } from './HostedServers';
import { CustomServers } from './CustomServers';
import { Composio } from './Composio';
import type { Key } from 'react';
export function ToolsConfig() {
const [activeTab, setActiveTab] = useState('hosted');
export function ToolsConfig({ useComposioTools }: { useComposioTools: boolean }) {
const [activeTab, setActiveTab] = useState(useComposioTools ? 'composio' : 'hosted');
const handleTabChange = (key: Key) => {
setActiveTab(key.toString());
@ -22,6 +23,13 @@ export function ToolsConfig() {
className="w-full"
fullWidth
>
{useComposioTools && (
<Tab key="composio" title="Composio">
<div className="mt-4 p-6">
<Composio />
</div>
</Tab>
)}
<Tab key="hosted" title={
<div className="flex items-center gap-2">
<span>Tools Library</span>

View file

@ -2,6 +2,7 @@ import { Suspense } from 'react';
import { ToolsConfig } from './components/ToolsConfig';
import { PageHeader } from '@/components/ui/page-header';
import { requireActiveBillingSubscription } from '@/app/lib/billing';
import { USE_COMPOSIO_TOOLS } from '@/app/lib/feature_flags';
export default async function ToolsPage() {
await requireActiveBillingSubscription();
@ -14,7 +15,7 @@ export default async function ToolsPage() {
/>
<div className="flex-1 p-6">
<Suspense fallback={<div>Loading...</div>}>
<ToolsConfig />
<ToolsConfig useComposioTools={USE_COMPOSIO_TOOLS} />
</Suspense>
</div>
</div>

View file

@ -9,7 +9,8 @@ import { WorkflowSelector } from "./workflow_selector";
import { Spinner } from "@heroui/react";
import { cloneWorkflow, createWorkflow, fetchPublishedWorkflowId, fetchWorkflow } from "../../../actions/workflow_actions";
import { listDataSources } from "../../../actions/datasource_actions";
import { listMcpServers, listProjectMcpTools } from "@/app/actions/mcp_actions";
import { listMcpServers } from "@/app/actions/mcp_actions";
import { collectProjectTools } from "@/app/actions/project_actions";
import { getProjectConfig } from "@/app/actions/project_actions";
import { WorkflowTool } from "@/app/lib/types/workflow_types";
import { getEligibleModels } from "@/app/actions/billing_actions";
@ -51,7 +52,7 @@ export function App({
listDataSources(projectId),
listMcpServers(projectId),
getProjectConfig(projectId),
listProjectMcpTools(projectId),
collectProjectTools(projectId),
getEligibleModels(),
]);

View file

@ -8,6 +8,7 @@ import { SortableContext, sortableKeyboardCoordinates, useSortable, verticalList
import { CSS } from '@dnd-kit/utilities';
import { Panel } from "@/components/common/panel-common";
import { Button } from "@/components/ui/button";
import { PictureImg } from "@/components/ui/picture-img";
import { clsx } from "clsx";
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from "@/components/ui/resizable";
import { ServerLogo } from '../tools/components/MCPServersCommon';
@ -204,6 +205,13 @@ const ServerCard = ({
);
};
type ComposioToolkit = {
slug: string;
name: string;
logo: string;
tools: z.infer<typeof WorkflowTool>[];
}
export function EntityList({
agents,
tools,
@ -234,6 +242,22 @@ export function EntityList({
const containerRef = useRef<HTMLDivElement>(null);
const [containerHeight, setContainerHeight] = useState<number>(0);
// collect composio tools
const composioTools: Record<string, ComposioToolkit> = {};
for (const tool of mergedTools) {
if (tool.isComposio) {
if (!composioTools[tool.composioData?.toolkitSlug || '']) {
composioTools[tool.composioData?.toolkitSlug || ''] = {
name: tool.composioData?.toolkitName || '',
slug: tool.composioData?.toolkitSlug || '',
logo: tool.composioData?.logo || '',
tools: []
};
}
composioTools[tool.composioData?.toolkitSlug || ''].tools.push(tool);
}
}
// Panel expansion states
const [expandedPanels, setExpandedPanels] = useState({
agents: true,
@ -469,7 +493,7 @@ export function EntityList({
{/* Group tools by server */}
{(() => {
// Get custom tools (non-MCP tools)
const customTools = mergedTools.filter(tool => !tool.isMcp);
const customTools = mergedTools.filter(tool => !tool.isMcp && !tool.isComposio);
// Group MCP tools by server
const serverTools = mergedTools.reduce((acc, tool) => {
@ -484,7 +508,19 @@ export function EntityList({
return (
<>
{/* Show MCP server cards first */}
{/* Show composio cards */}
{Object.values(composioTools).map((card) => (
<ComposioCard
key={card.slug}
card={card}
selectedEntity={selectedEntity}
onSelectTool={handleToolSelection}
onDeleteTool={onDeleteTool}
selectedRef={selectedRef}
/>
))}
{/* Show MCP server cards */}
{Object.entries(serverTools).map(([serverName, tools]) => (
<ServerCard
key={serverName}
@ -690,6 +726,90 @@ function EntityDropdown({
);
}
interface ComposioCardProps {
card: ComposioToolkit;
selectedEntity: {
type: "agent" | "tool" | "prompt";
name: string;
} | null;
onSelectTool: (name: string) => void;
onDeleteTool: (name: string) => void;
selectedRef: React.RefObject<HTMLButtonElement | null>;
}
const ComposioCard = ({
card,
selectedEntity,
onSelectTool,
onDeleteTool,
selectedRef,
}: ComposioCardProps) => {
const [isExpanded, setIsExpanded] = useState(false);
return (
<div className="mb-2">
<button
onClick={() => setIsExpanded(!isExpanded)}
className="w-full flex items-center gap-2 px-2 py-1.5 hover:bg-zinc-50 dark:hover:bg-zinc-800 rounded-md text-sm text-left"
>
{isExpanded ? (
<ChevronDown className="w-4 h-4 text-gray-500" />
) : (
<ChevronRight className="w-4 h-4 text-gray-500" />
)}
<div className="flex items-center gap-1">
{card.logo ? (
<div className="relative w-4 h-4">
<PictureImg
src={card.logo}
alt={`${card.name} logo`}
className="w-full h-full object-contain rounded"
/>
</div>
) : (
<ImportIcon className="w-4 h-4 text-blue-600 dark:text-blue-500" />
)}
<span>{card.name}</span>
</div>
</button>
{isExpanded && (
<div className="ml-6 mt-1 space-y-1">
{card.tools.map((tool, index) => (
<ListItemWithMenu
key={`composio-tool-${index}`}
name={tool.name}
isSelected={selectedEntity?.type === "tool" && selectedEntity.name === tool.name}
onClick={() => onSelectTool(tool.name)}
disabled={tool.isLibrary}
selectedRef={selectedEntity?.type === "tool" && selectedEntity.name === tool.name ? selectedRef : undefined}
icon={
card.logo ? (
<div className="relative w-4 h-4">
<PictureImg
src={card.logo}
alt={`${card.name} logo`}
className="w-full h-full object-contain rounded"
/>
</div>
) : (
<ImportIcon className="w-4 h-4 text-blue-600 dark:text-blue-500" />
)
}
menuContent={
<EntityDropdown
name={tool.name}
onDelete={onDeleteTool}
isLocked={tool.isComposio}
/>
}
/>
))}
</div>
)}
</div>
);
};
// Add SortableItem component for agents
const SortableAgentItem = ({ agent, isSelected, onClick, selectedRef, statusLabel, onToggle, onSetMainAgent, onDelete, isStartAgent }: {
agent: z.infer<typeof WorkflowAgent>;

View file

@ -0,0 +1,61 @@
import React from 'react';
interface SourceProps {
srcSet: string;
media?: string;
type?: string;
sizes?: string;
}
interface PictureImgProps extends React.ImgHTMLAttributes<HTMLImageElement> {
src: string;
alt: string;
sources?: SourceProps[];
fallbackSrc?: string;
onError?: React.ReactEventHandler<HTMLImageElement>;
}
export function PictureImg({
src,
alt,
sources = [],
fallbackSrc,
onError,
className,
...imgProps
}: PictureImgProps) {
const handleError: React.ReactEventHandler<HTMLImageElement> = (e) => {
if (fallbackSrc && e.currentTarget.src !== fallbackSrc) {
e.currentTarget.src = fallbackSrc;
return;
}
if (onError) {
onError(e);
} else {
// Default error handling - hide the image
e.currentTarget.style.display = 'none';
}
};
return (
<picture>
{sources.map((source, index) => (
<source
key={index}
srcSet={source.srcSet}
media={source.media}
type={source.type}
sizes={source.sizes}
/>
))}
<img
src={src}
alt={alt}
className={className}
onError={handleError}
{...imgProps}
/>
</picture>
);
}

View file

@ -12,6 +12,7 @@
"@auth0/nextjs-auth0": "^4.7.0",
"@aws-sdk/client-s3": "^3.743.0",
"@aws-sdk/s3-request-presigner": "^3.743.0",
"@composio/core": "^0.1.36-next.7",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
@ -1356,6 +1357,59 @@
"node": ">=6.9.0"
}
},
"node_modules/@cfworker/json-schema": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz",
"integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==",
"license": "MIT"
},
"node_modules/@composio/client": {
"version": "0.1.0-alpha.26",
"resolved": "https://registry.npmjs.org/@composio/client/-/client-0.1.0-alpha.26.tgz",
"integrity": "sha512-6cLf8sABDe0tjG4U33GDd3ZS4S3YdVCEpFyi9bt22vanqtNKJk8wNEOFOui9TgI131B45Tt/XtlCeCR+PpxQLQ==",
"license": "Apache-2.0"
},
"node_modules/@composio/core": {
"version": "0.1.36-next.7",
"resolved": "https://registry.npmjs.org/@composio/core/-/core-0.1.36-next.7.tgz",
"integrity": "sha512-MqwYKBIoCMn9VegS67UO/RkO/C5L7aA0r28K7VbIS4ZwMY/9/hl7UPqat7EZapyhpLFw4OaTHeAjAMcu6dRm1g==",
"license": "ISC",
"dependencies": {
"@composio/client": "0.1.0-alpha.26",
"@composio/json-schema-to-zod": "0.1.10-next.3",
"@types/json-schema": "^7.0.15",
"chalk": "^4.1.2",
"openai": "^4.94.0",
"pusher-js": "^8.4.0",
"semver": "^7.7.2",
"uuid": "^11.1.0",
"zod": "^3.24.2",
"zod-to-json-schema": "^3.24.5"
}
},
"node_modules/@composio/core/node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/@composio/json-schema-to-zod": {
"version": "0.1.10-next.3",
"resolved": "https://registry.npmjs.org/@composio/json-schema-to-zod/-/json-schema-to-zod-0.1.10-next.3.tgz",
"integrity": "sha512-kglZdteq+18Tcikb1US7D47VX3A3x8haeE+qFCwpQbBAgp3vG+eMQnZ6WgMkLNroZNrTm5oiAJrSdjerbiAqIA==",
"license": "ISC",
"dependencies": {
"@types/json-schema": "^7.0.15",
"zod": "^3.24.2"
}
},
"node_modules/@dnd-kit/accessibility": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
@ -4331,20 +4385,22 @@
}
},
"node_modules/@langchain/core": {
"version": "0.3.7",
"resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.3.7.tgz",
"integrity": "sha512-6wsnEtw5GlhmBhoLfw/g8Hrp09BNwQwDLXyuv3GyK+ay4/3H3YuhAphqQLO4HNphuZIZKlW9ihSrqdCMvvbvZQ==",
"version": "0.3.61",
"resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.3.61.tgz",
"integrity": "sha512-4O7fw5SXNSE+uBnathLQrhm3t+7dZGagt/5kt37A+pXw0AkudxEBvveg73sSnpBd9SIz3/Vc7F4k8rCKXGbEDA==",
"license": "MIT",
"dependencies": {
"@cfworker/json-schema": "^4.0.2",
"ansi-styles": "^5.0.0",
"camelcase": "6",
"decamelize": "1.2.0",
"js-tiktoken": "^1.0.12",
"langsmith": "^0.1.56",
"langsmith": "^0.3.33",
"mustache": "^4.2.0",
"p-queue": "^6.6.2",
"p-retry": "4",
"uuid": "^10.0.0",
"zod": "^3.22.4",
"zod": "^3.25.32",
"zod-to-json-schema": "^3.22.3"
},
"engines": {
@ -7576,6 +7632,12 @@
"@types/unist": "*"
}
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"license": "MIT"
},
"node_modules/@types/json5": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
@ -7661,7 +7723,8 @@
"node_modules/@types/retry": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==",
"license": "MIT"
},
"node_modules/@types/styled-system": {
"version": "5.1.23",
@ -7696,7 +7759,8 @@
"node_modules/@types/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ=="
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
"license": "MIT"
},
"node_modules/@types/webidl-conversions": {
"version": "7.0.3",
@ -8133,7 +8197,6 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
@ -8687,7 +8750,6 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
@ -8904,6 +8966,15 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
},
"node_modules/console-table-printer": {
"version": "2.14.6",
"resolved": "https://registry.npmjs.org/console-table-printer/-/console-table-printer-2.14.6.tgz",
"integrity": "sha512-MCBl5HNVaFuuHW6FGbL/4fB7N/ormCy+tQ+sxTrF6QtSbSNETvPuOVbkJBhzDgYhvjWGrTma4eYJa37ZuoQsPw==",
"license": "MIT",
"dependencies": {
"simple-wcswidth": "^1.0.1"
}
},
"node_modules/content-disposition": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
@ -10151,7 +10222,8 @@
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"license": "MIT"
},
"node_modules/eventsource": {
"version": "3.0.5",
@ -10821,7 +10893,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"engines": {
"node": ">=8"
}
@ -11853,34 +11924,40 @@
}
},
"node_modules/langsmith": {
"version": "0.1.61",
"resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.1.61.tgz",
"integrity": "sha512-XQE4KPScwPmdaT0mWDzhNxj9gvqXUR+C7urLA0QFi27XeoQdm17eYpudenn4wxC0gIyUJutQCyuYJpfwlT5JnQ==",
"version": "0.3.37",
"resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.3.37.tgz",
"integrity": "sha512-aDFM+LbT01gP8hsJNs4QJjmbRNfoifqhpCSpk8j4k/V8wejEgvgATbgj9W9DQsfQFdtfwx+8G48sK5/0PqQisg==",
"license": "MIT",
"dependencies": {
"@types/uuid": "^10.0.0",
"commander": "^10.0.1",
"chalk": "^4.1.2",
"console-table-printer": "^2.12.1",
"p-queue": "^6.6.2",
"p-retry": "4",
"semver": "^7.6.3",
"uuid": "^10.0.0"
},
"peerDependencies": {
"@opentelemetry/api": "*",
"@opentelemetry/exporter-trace-otlp-proto": "*",
"@opentelemetry/sdk-trace-base": "*",
"openai": "*"
},
"peerDependenciesMeta": {
"@opentelemetry/api": {
"optional": true
},
"@opentelemetry/exporter-trace-otlp-proto": {
"optional": true
},
"@opentelemetry/sdk-trace-base": {
"optional": true
},
"openai": {
"optional": true
}
}
},
"node_modules/langsmith/node_modules/commander": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
"integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
"engines": {
"node": ">=14"
}
},
"node_modules/langsmith/node_modules/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
@ -11889,6 +11966,7 @@
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
@ -14129,9 +14207,10 @@
}
},
"node_modules/openai": {
"version": "4.67.2",
"resolved": "https://registry.npmjs.org/openai/-/openai-4.67.2.tgz",
"integrity": "sha512-u4FJFGXgqEHrCYcD5jAD4nHj6JCiicH+/dskQY7qka9R6hOw29R0kOz7GwcA9k2JKcLf86lzAWPtPagPbO8KnQ==",
"version": "4.104.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz",
"integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==",
"license": "Apache-2.0",
"dependencies": {
"@types/node": "^18.11.18",
"@types/node-fetch": "^2.6.4",
@ -14145,9 +14224,13 @@
"openai": "bin/cli"
},
"peerDependencies": {
"ws": "^8.18.0",
"zod": "^3.23.8"
},
"peerDependenciesMeta": {
"ws": {
"optional": true
},
"zod": {
"optional": true
}
@ -14200,6 +14283,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
"integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==",
"license": "MIT",
"engines": {
"node": ">=4"
}
@ -14238,6 +14322,7 @@
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz",
"integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==",
"license": "MIT",
"dependencies": {
"eventemitter3": "^4.0.4",
"p-timeout": "^3.2.0"
@ -14253,6 +14338,7 @@
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
"integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==",
"license": "MIT",
"dependencies": {
"@types/retry": "0.12.0",
"retry": "^0.13.1"
@ -14265,6 +14351,7 @@
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
"integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
"license": "MIT",
"dependencies": {
"p-finally": "^1.0.0"
},
@ -14542,6 +14629,15 @@
"node": ">=6"
}
},
"node_modules/pusher-js": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-8.4.0.tgz",
"integrity": "sha512-wp3HqIIUc1GRyu1XrP6m2dgyE9MoCsXVsWNlohj0rjSkLf+a0jLvEyVubdg58oMk7bhjBWnFClgp8jfAa6Ak4Q==",
"license": "MIT",
"dependencies": {
"tweetnacl": "^1.0.3"
}
},
"node_modules/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
@ -14953,6 +15049,7 @@
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
"integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
"license": "MIT",
"engines": {
"node": ">= 4"
}
@ -15404,6 +15501,12 @@
"is-arrayish": "^0.3.1"
}
},
"node_modules/simple-wcswidth": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.1.2.tgz",
"integrity": "sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==",
"license": "MIT"
},
"node_modules/source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@ -15759,7 +15862,6 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},
@ -16006,6 +16108,12 @@
"fsevents": "~2.3.3"
}
},
"node_modules/tweetnacl": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==",
"license": "Unlicense"
},
"node_modules/twilio": {
"version": "5.4.5",
"resolved": "https://registry.npmjs.org/twilio/-/twilio-5.4.5.tgz",
@ -16672,9 +16780,10 @@
}
},
"node_modules/zod-to-json-schema": {
"version": "3.24.3",
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.3.tgz",
"integrity": "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A==",
"version": "3.24.6",
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz",
"integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==",
"license": "ISC",
"peerDependencies": {
"zod": "^3.24.1"
}

View file

@ -19,6 +19,7 @@
"@auth0/nextjs-auth0": "^4.7.0",
"@aws-sdk/client-s3": "^3.743.0",
"@aws-sdk/s3-request-presigner": "^3.743.0",
"@composio/core": "^0.1.36-next.7",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",

View file

@ -54,6 +54,8 @@ services:
- USE_BILLING=${USE_BILLING}
- BILLING_API_URL=${BILLING_API_URL}
- BILLING_API_KEY=${BILLING_API_KEY}
- USE_COMPOSIO_TOOLS=${USE_COMPOSIO_TOOLS}
- COMPOSIO_API_KEY=${COMPOSIO_API_KEY}
restart: unless-stopped
volumes:
- uploads:/app/uploads

View file

@ -9,6 +9,11 @@ mkdir -p data/mongo
export USE_RAG=true
export USE_RAG_UPLOADS=true
# enable composio tools if API key is set
if [ -n "$COMPOSIO_API_KEY" ]; then
export USE_COMPOSIO_TOOLS=true
fi
# Start with the base command and profile flags
CMD="docker-compose"
CMD="$CMD --profile setup_qdrant"