diff --git a/apps/rowboat/app/actions/composio_actions.ts b/apps/rowboat/app/actions/composio_actions.ts index b123b80d..7e60a105 100644 --- a/apps/rowboat/app/actions/composio_actions.ts +++ b/apps/rowboat/app/actions/composio_actions.ts @@ -3,6 +3,9 @@ import { z } from "zod"; import { listToolkits as libListToolkits, listTools as libListTools, + searchTools as libSearchTools, + getToolsByIds as libGetToolsByIds, + getTool as libGetTool, getConnectedAccount as libGetConnectedAccount, deleteConnectedAccount as libDeleteConnectedAccount, listAuthConfigs as libListAuthConfigs, @@ -20,6 +23,7 @@ import { ZCredentials, } from "@/app/lib/composio/composio"; import { ComposioConnectedAccount } from "@/app/lib/types/project_types"; +import { WorkflowTool } from "@/app/lib/types/workflow_types"; import { getProjectConfig, projectAuthCheck } from "./project_actions"; import { projectsCollection } from "../lib/mongodb"; @@ -47,6 +51,24 @@ export async function listTools(projectId: string, toolkitSlug: string, cursor: return await libListTools(toolkitSlug, cursor); } +// New efficient search functions + +export async function searchTools(projectId: string, searchQuery: string, cursor: string | null = null, limit?: number): Promise>>> { + await projectAuthCheck(projectId); + return await libSearchTools(searchQuery, cursor, limit); +} + +export async function getToolsByIds(projectId: string, toolSlugs: string[], cursor: string | null = null): Promise>>> { + await projectAuthCheck(projectId); + return await libGetToolsByIds(toolSlugs, cursor); +} + +export async function getTool(projectId: string, toolSlug: string): Promise> { + await projectAuthCheck(projectId); + return await libGetTool(toolSlug); +} + + export async function createComposioManagedOauth2ConnectedAccount(projectId: string, toolkitSlug: string, callbackUrl: string): Promise> { await projectAuthCheck(projectId); @@ -215,12 +237,196 @@ export async function deleteConnectedAccount(projectId: string, toolkitSlug: str const key = `composioConnectedAccounts.${toolkitSlug}`; await projectsCollection.updateOne({ _id: projectId }, { $unset: { [key]: "" } }); + // Notify other tabs about the tools update (lightweight refresh) + if (typeof window !== 'undefined') { + localStorage.setItem(`tools-light-refresh-${projectId}`, Date.now().toString()); + } + return true; } +// Note: composio tools are now stored in workflow.tools array with isComposio: true +// This function provides backward compatibility by updating workflow tools +export async function getComposioToolsFromWorkflow(projectId: string): Promise[]> { + await projectAuthCheck(projectId); + + // Get the project to access draft workflow + const project = await projectsCollection.findOne({ _id: projectId }); + if (!project || !project.draftWorkflow) { + return []; + } + + // Extract composio tools from workflow and convert back to ZTool format + const composioTools = project.draftWorkflow.tools + .filter(tool => tool.isComposio && tool.composioData) + .map(tool => ({ + slug: tool.composioData!.slug, + name: tool.name, + description: tool.description, + no_auth: tool.composioData!.noAuth, + input_parameters: { + type: 'object' as const, + properties: tool.parameters.properties, + required: tool.parameters.required || [] + }, + toolkit: { + name: tool.composioData!.toolkitName, + slug: tool.composioData!.toolkitSlug, + logo: tool.composioData!.logo, + } + })); + + return composioTools; +} + export async function updateComposioSelectedTools(projectId: string, tools: z.infer[]): Promise { await projectAuthCheck(projectId); - // update project with new selected tools - await projectsCollection.updateOne({ _id: projectId }, { $set: { composioSelectedTools: tools } }); + // Get the project to access draft workflow + const project = await projectsCollection.findOne({ _id: projectId }); + if (!project || !project.draftWorkflow) { + throw new Error(`Project ${projectId} not found or has no draft workflow`); + } + + // Convert Composio tools to workflow tool format + const composioWorkflowTools: z.infer[] = tools.map(tool => ({ + name: tool.slug, + description: tool.description || "", + parameters: { + type: 'object' as const, + properties: tool.input_parameters?.properties || {}, + required: tool.input_parameters?.required || [] + }, + isComposio: true, + composioData: { + slug: tool.slug, + noAuth: tool.no_auth, + toolkitName: tool.toolkit.name, + toolkitSlug: tool.toolkit.slug, + logo: tool.toolkit.logo, + }, + })); + + // Remove existing composio tools and add new ones + const nonComposioTools = project.draftWorkflow.tools.filter(tool => !tool.isComposio); + const updatedWorkflow = { + ...project.draftWorkflow, + tools: [...nonComposioTools, ...composioWorkflowTools], + lastUpdatedAt: new Date().toISOString() + }; + + // Update the project's draft workflow + const result = await projectsCollection.updateOne( + { _id: projectId }, + { $set: { draftWorkflow: updatedWorkflow } } + ); + + if (result.modifiedCount === 0) { + throw new Error(`Failed to update workflow for project ${projectId}`); + } + + // Notify other tabs about the tools update (lightweight refresh) + if (typeof window !== 'undefined') { + localStorage.setItem(`tools-light-refresh-${projectId}`, Date.now().toString()); + } +} + +// Note: composio mock states are now stored in workflow.composioMockToolkitStates +// This function provides backward compatibility by updating workflow mock states +export async function toggleMockToolkitState(projectId: string, toolkitSlug: string, isMocked: boolean, mockInstructions?: string): Promise { + await projectAuthCheck(projectId); + + // Get the project to access draft workflow + const project = await projectsCollection.findOne({ _id: projectId }); + if (!project || !project.draftWorkflow) { + throw new Error(`Project ${projectId} not found or has no draft workflow`); + } + + const now = new Date().toISOString(); + let updatedMockToolkitStates = { ...(project.draftWorkflow.composioMockToolkitStates || {}) }; + + if (isMocked) { + // Enable mock mode + updatedMockToolkitStates[toolkitSlug] = { + toolkitSlug, + isMocked: true, + mockInstructions: mockInstructions || 'Mock responses using GPT-4.1 based on tool descriptions.', + autoSubmitMockedResponse: false, + createdAt: now, + lastUpdatedAt: now, + }; + } else { + // Disable mock mode - remove the toolkit from the object + delete updatedMockToolkitStates[toolkitSlug]; + } + + // Update the workflow with new mock states + const updatedWorkflow = { + ...project.draftWorkflow, + composioMockToolkitStates: updatedMockToolkitStates, + lastUpdatedAt: now + }; + + // Update the project's draft workflow + const result = await projectsCollection.updateOne( + { _id: projectId }, + { $set: { draftWorkflow: updatedWorkflow } } + ); + + if (result.modifiedCount === 0) { + throw new Error(`Failed to update workflow mock states for project ${projectId}`); + } + + // Notify other tabs about the tools update (lightweight refresh) + if (typeof window !== 'undefined') { + localStorage.setItem(`tools-light-refresh-${projectId}`, Date.now().toString()); + } +} + +// Note: composio mock states are now stored in workflow.composioMockToolkitStates +// This function provides backward compatibility by updating workflow mock states +export async function updateMockToolkitInstructions(projectId: string, toolkitSlug: string, mockInstructions: string): Promise { + await projectAuthCheck(projectId); + + // Get the project to access draft workflow + const project = await projectsCollection.findOne({ _id: projectId }); + if (!project || !project.draftWorkflow) { + throw new Error(`Project ${projectId} not found or has no draft workflow`); + } + + const now = new Date().toISOString(); + let updatedMockToolkitStates = { ...(project.draftWorkflow.composioMockToolkitStates || {}) }; + + // Update the mock instructions for the specified toolkit + if (updatedMockToolkitStates[toolkitSlug]) { + updatedMockToolkitStates[toolkitSlug] = { + ...updatedMockToolkitStates[toolkitSlug], + mockInstructions, + lastUpdatedAt: now + }; + + // Update the workflow with new mock states + const updatedWorkflow = { + ...project.draftWorkflow, + composioMockToolkitStates: updatedMockToolkitStates, + lastUpdatedAt: now + }; + + // Update the project's draft workflow + const result = await projectsCollection.updateOne( + { _id: projectId }, + { $set: { draftWorkflow: updatedWorkflow } } + ); + + if (result.modifiedCount === 0) { + throw new Error(`Failed to update workflow mock instructions for project ${projectId}`); + } + + // Notify other tabs about the tools update + if (typeof window !== 'undefined') { + localStorage.setItem(`tools-updated-${projectId}`, Date.now().toString()); + } + } else { + throw new Error(`Mock toolkit state for ${toolkitSlug} not found in project ${projectId}`); + } } \ No newline at end of file diff --git a/apps/rowboat/app/actions/project_actions.ts b/apps/rowboat/app/actions/project_actions.ts index 66cb58f6..d9809494 100644 --- a/apps/rowboat/app/actions/project_actions.ts +++ b/apps/rowboat/app/actions/project_actions.ts @@ -16,6 +16,13 @@ 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"; +import { + searchTools as libSearchTools, + getToolsByIds as libGetToolsByIds, + getTool as libGetTool, + ZTool, + ZToolkit +} from "../lib/composio/composio"; const KLAVIS_API_KEY = process.env.KLAVIS_API_KEY || ''; @@ -306,6 +313,113 @@ export async function createProjectFromPrompt(formData: FormData): Promise<{ id: return { id: projectId }; } +async function detectAndAddComposioTools(projectId: string, workflow: z.infer) { + // Extract tool mentions from agent instructions + const toolMentionPattern = /\[@tool:([^\]]+)\]\(#mention[^\)]*\)/g; + const mentionedToolNames = new Set(); + + // Scan all agent instructions for tool mentions + for (const agent of workflow.agents || []) { + const instructions = agent.instructions || ""; + let match: RegExpExecArray | null; + while ((match = toolMentionPattern.exec(instructions))) { + mentionedToolNames.add(match[1]); + } + } + + if (mentionedToolNames.size === 0) { + return; // No tool mentions found + } + + console.log(`Found ${mentionedToolNames.size} tool mentions in workflow:`, Array.from(mentionedToolNames)); + + // Search for these tools in Composio using the new efficient search methods + const foundTools: z.infer[] = []; + + try { + // Method 1: Try to get tools directly by their exact slugs/names + const mentionedToolNamesArray = Array.from(mentionedToolNames); + + try { + const directToolsResponse = await libGetToolsByIds(mentionedToolNamesArray); + foundTools.push(...directToolsResponse.items); + console.log(`Found ${directToolsResponse.items.length} tools by direct lookup`); + } catch (error) { + console.log('Direct tool lookup failed, trying search approach'); + } + + // Method 2: For any remaining tools, use search functionality + const foundToolSlugs = new Set(foundTools.map(tool => tool.slug)); + const foundToolNames = new Set(foundTools.map(tool => tool.name)); + const remainingToolNames = mentionedToolNamesArray.filter(name => + !foundToolSlugs.has(name) && !foundToolNames.has(name) + ); + + for (const toolName of remainingToolNames) { + try { + // Search for tools by name/description + const searchResponse = await libSearchTools(toolName, null, 10); + + // Find exact matches by name or slug + const exactMatches = searchResponse.items.filter(tool => + tool.name === toolName || + tool.slug === toolName || + tool.name.toLowerCase() === toolName.toLowerCase() || + tool.slug.toLowerCase() === toolName.toLowerCase() + ); + + if (exactMatches.length > 0) { + foundTools.push(...exactMatches); + console.log(`Found ${exactMatches.length} tools for search term "${toolName}"`); + } else { + console.log(`No exact matches found for tool "${toolName}"`); + } + } catch (error) { + console.error(`Error searching for tool "${toolName}":`, error); + } + } + + } catch (error) { + console.error('Error searching for Composio tools:', error); + return; + } + + if (foundTools.length > 0) { + console.log(`Adding ${foundTools.length} Composio tools to workflow`); + + // Remove duplicates based on slug + const uniqueTools = foundTools.filter((tool, index, self) => + index === self.findIndex(t => t.slug === tool.slug) + ); + + // Convert Composio tools to workflow tool format + const composioWorkflowTools: z.infer[] = uniqueTools.map(tool => ({ + name: tool.slug, + description: tool.description || "", + parameters: { + type: 'object' as const, + properties: tool.input_parameters?.properties || {}, + required: tool.input_parameters?.required || [] + }, + isComposio: true, + composioData: { + slug: tool.slug, + noAuth: tool.no_auth, + toolkitName: tool.toolkit.name, + toolkitSlug: tool.toolkit.slug, + logo: tool.toolkit.logo, + }, + })); + + // Add these tools to the workflow.tools array + workflow.tools = [...workflow.tools, ...composioWorkflowTools]; + + console.log(`Added ${composioWorkflowTools.length} Composio tools to workflow`); + } else { + console.log('No matching Composio tools found for the mentioned tool names'); + } +} + export async function createProjectFromWorkflowJson(formData: FormData): Promise<{ id: string } | { billingError: string }> { const user = await authCheck(); const workflowJson = formData.get('workflowJson') as string; @@ -327,6 +441,15 @@ export async function createProjectFromWorkflowJson(formData: FormData): Promise return response; } const projectId = response.id; + + // Automatically detect and add Composio tools mentioned in agent instructions + try { + await detectAndAddComposioTools(projectId, workflow); + } catch (error) { + // Log error but don't fail the import if tool detection fails + console.error('Failed to auto-detect Composio tools:', error); + } + return { id: projectId }; } diff --git a/apps/rowboat/app/lib/agents.ts b/apps/rowboat/app/lib/agents.ts index ef06efc0..618a5e3e 100644 --- a/apps/rowboat/app/lib/agents.ts +++ b/apps/rowboat/app/lib/agents.ts @@ -17,6 +17,7 @@ import { dataSourceDocsCollection, dataSourcesCollection, projectsCollection } f import { qdrantClient } from '../lib/qdrant'; import { EmbeddingRecord } from "./types/datasource_types"; import { ConnectedEntity, sanitizeTextWithMentions, Workflow, WorkflowAgent, WorkflowPrompt, WorkflowTool } from "./types/workflow_types"; +import { Project } from "./types/project_types"; import { CHILD_TRANSFER_RELATED_INSTRUCTIONS, CONVERSATION_TYPE_INSTRUCTIONS, RAG_INSTRUCTIONS, TASK_TYPE_INSTRUCTIONS } from "./agent_instructions"; import { PrefixLogger } from "./utils"; import { Message, AssistantMessage, AssistantMessageWithToolCalls, ToolMessage } from "./types/types"; @@ -280,6 +281,8 @@ async function invokeComposioTool( name: string, composioData: z.infer['composioData'] & {}, input: any, + workflow: z.infer, + toolDescription?: string, ) { logger = logger.child(`invokeComposioTool`); logger.log(`projectId: ${projectId}`); @@ -288,12 +291,36 @@ async function invokeComposioTool( const { slug, toolkitSlug, noAuth } = composioData; + // Get project configuration to check for connected accounts (still stored in project) + const project = await projectsCollection.findOne({ _id: projectId }); + if (!project) { + throw new Error(`project ${projectId} not found`); + } + + // Check if toolkit is in mock mode (now from workflow) + const mockState = workflow.composioMockToolkitStates?.[toolkitSlug]; + if (mockState?.isMocked) { + logger.log(`toolkit ${toolkitSlug} is in mock mode, using mock response`); + + // Use the existing invokeMockTool function to generate a mock response + const mockInstructions = mockState.mockInstructions || 'Mock responses using GPT-4.1 based on tool descriptions.'; + const description = toolDescription || `${name} tool from ${toolkitSlug} toolkit`; + + const mockResponse = await invokeMockTool( + logger, + name, + JSON.stringify(input), + description, + mockInstructions + ); + + logger.log(`mock tool result: ${mockResponse}`); + return mockResponse; + } + + // Normal execution path - check for authentication 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}`); @@ -452,7 +479,8 @@ function createMcpTool( function createComposioTool( logger: PrefixLogger, config: z.infer, - projectId: string + projectId: string, + workflow: z.infer ): Tool { const { name, description, parameters, composioData } = config; @@ -472,7 +500,7 @@ function createComposioTool( }, async execute(input: any) { try { - const result = await invokeComposioTool(logger, projectId, name, composioData, input); + const result = await invokeComposioTool(logger, projectId, name, composioData, input, workflow, description); return JSON.stringify({ result, }); @@ -813,7 +841,7 @@ function createTools( tools[toolName] = createMcpTool(logger, config, projectId); logger.log(`created mcp tool: ${toolName}`); } else if (config.isComposio) { - tools[toolName] = createComposioTool(logger, config, projectId); + tools[toolName] = createComposioTool(logger, config, projectId, workflow); logger.log(`created composio tool: ${toolName}`); } else if (config.mockTool) { tools[toolName] = createMockTool(logger, config); diff --git a/apps/rowboat/app/lib/composio/composio.ts b/apps/rowboat/app/lib/composio/composio.ts index 3441f9fb..64c07679 100644 --- a/apps/rowboat/app/lib/composio/composio.ts +++ b/apps/rowboat/app/lib/composio/composio.ts @@ -291,6 +291,39 @@ export async function listTools(toolkitSlug: string, cursor: string | null = nul return composioApiCall(ZListResponse(ZTool), url.toString()); } +export async function searchTools(searchQuery: string, cursor: string | null = null, limit: number = 50): Promise>>> { + const url = new URL(`${BASE_URL}/tools`); + + // set params + url.searchParams.set("search", searchQuery); + if (cursor) { + url.searchParams.set("cursor", cursor); + } + url.searchParams.set("limit", limit.toString()); + + // fetch + return composioApiCall(ZListResponse(ZTool), url.toString()); +} + +export async function getToolsByIds(toolSlugs: string[], cursor: string | null = null): Promise>>> { + const url = new URL(`${BASE_URL}/tools`); + + // set params - pass tool slugs as comma-separated string + url.searchParams.set("tool_slugs", toolSlugs.join(",")); + if (cursor) { + url.searchParams.set("cursor", cursor); + } + + // fetch + return composioApiCall(ZListResponse(ZTool), url.toString()); +} + +export async function getTool(toolSlug: string): Promise> { + const url = new URL(`${BASE_URL}/tools/${toolSlug}`); + return composioApiCall(ZTool, url.toString()); +} + + export async function listAuthConfigs(toolkitSlug: string, cursor: string | null = null, managedOnly: boolean = false): Promise>>> { const url = new URL(`${BASE_URL}/auth_configs`); url.searchParams.set("toolkit_slug", toolkitSlug); diff --git a/apps/rowboat/app/lib/project_tools.ts b/apps/rowboat/app/lib/project_tools.ts index a65ba535..7d8fb8f5 100644 --- a/apps/rowboat/app/lib/project_tools.ts +++ b/apps/rowboat/app/lib/project_tools.ts @@ -33,28 +33,8 @@ export async function collectProjectTools(projectId: string): Promise mockInstructions + composioMockToolkitStates: z.record(z.string(), z.object({ + toolkitSlug: z.string(), + isMocked: z.boolean(), + mockInstructions: z.string().optional(), + autoSubmitMockedResponse: z.boolean().default(false), + createdAt: z.string().datetime(), + lastUpdatedAt: z.string().datetime(), + })).optional(), }); export const WorkflowTemplate = Workflow .omit({ diff --git a/apps/rowboat/app/projects/[projectId]/tools/components/Composio.tsx b/apps/rowboat/app/projects/[projectId]/tools/components/Composio.tsx index dbb7c5f1..380bb5b7 100644 --- a/apps/rowboat/app/projects/[projectId]/tools/components/Composio.tsx +++ b/apps/rowboat/app/projects/[projectId]/tools/components/Composio.tsx @@ -5,7 +5,7 @@ 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 { listToolkits, listTools, updateComposioSelectedTools, getComposioToolsFromWorkflow } 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'; @@ -30,6 +30,7 @@ export function Composio() { const [selectedToolkit, setSelectedToolkit] = useState(null); const [isToolsPanelOpen, setIsToolsPanelOpen] = useState(false); const [savingTools, setSavingTools] = useState(false); + const [composioSelectedTools, setComposioSelectedTools] = useState[]>([]); const loadProjectConfig = useCallback(async () => { try { @@ -41,6 +42,15 @@ export function Composio() { } }, [projectId]); + const loadComposioSelectedTools = useCallback(async () => { + try { + const tools = await getComposioToolsFromWorkflow(projectId); + setComposioSelectedTools(tools); + } catch (err: any) { + console.error('Error fetching composio selected tools:', err); + } + }, [projectId]); + const loadAllToolkits = useCallback(async () => { let cursor: string | null = null; let allToolkits: ToolkitType[] = []; @@ -87,15 +97,16 @@ export function Composio() { const handleProjectConfigUpdate = useCallback(() => { loadProjectConfig(); - }, [loadProjectConfig]); + loadComposioSelectedTools(); + }, [loadProjectConfig, loadComposioSelectedTools]); const handleUpdateToolsSelection = useCallback(async (selectedToolObjects: z.infer[]) => { if (!projectId) return; setSavingTools(true); try { - // Get existing selected tools from project config - const existingSelectedTools = projectConfig?.composioSelectedTools || []; + // Get existing selected tools from workflow + const existingSelectedTools = composioSelectedTools; // Create a map of existing tools by slug for easy lookup const existingToolsMap = new Map(existingSelectedTools.map(tool => [tool.slug, tool])); @@ -110,22 +121,22 @@ export function Composio() { await updateComposioSelectedTools(projectId, mergedSelectedTools); - // Refresh project config to get updated data - await loadProjectConfig(); + // Refresh data to get updated tools + await loadComposioSelectedTools(); } catch (error) { console.error('Error saving tool selection:', error); } finally { setSavingTools(false); } - }, [projectId, projectConfig, loadProjectConfig]); + }, [projectId, composioSelectedTools, loadComposioSelectedTools]); const handleRemoveToolkitTools = useCallback(async (toolkitSlug: string) => { if (!projectId) return; setSavingTools(true); try { - // Get existing selected tools from project config - const existingSelectedTools = projectConfig?.composioSelectedTools || []; + // Get existing selected tools from workflow + const existingSelectedTools = composioSelectedTools; // Filter out all tools from the specified toolkit const filteredSelectedTools = existingSelectedTools.filter(tool => @@ -134,18 +145,19 @@ export function Composio() { await updateComposioSelectedTools(projectId, filteredSelectedTools); - // Refresh project config to get updated data - await loadProjectConfig(); + // Refresh data to get updated tools + await loadComposioSelectedTools(); } catch (error) { console.error('Error removing toolkit tools:', error); } finally { setSavingTools(false); } - }, [projectId, projectConfig, loadProjectConfig]); + }, [projectId, composioSelectedTools, loadComposioSelectedTools]); useEffect(() => { loadProjectConfig(); - }, [loadProjectConfig]); + loadComposioSelectedTools(); + }, [loadProjectConfig, loadComposioSelectedTools]); useEffect(() => { loadAllToolkits(); diff --git a/apps/rowboat/app/projects/[projectId]/tools/components/ComposioToolsPanel.tsx b/apps/rowboat/app/projects/[projectId]/tools/components/ComposioToolsPanel.tsx index 7cfb5348..e05140a7 100644 --- a/apps/rowboat/app/projects/[projectId]/tools/components/ComposioToolsPanel.tsx +++ b/apps/rowboat/app/projects/[projectId]/tools/components/ComposioToolsPanel.tsx @@ -5,7 +5,7 @@ import { useParams } from 'next/navigation'; import { PictureImg } from '@/components/ui/picture-img'; import { Button, Checkbox } from '@heroui/react'; import { ChevronLeft, ChevronRight, LinkIcon, Loader2, UnlinkIcon } from 'lucide-react'; -import { listTools, deleteConnectedAccount } from '@/app/actions/composio_actions'; +import { listTools, deleteConnectedAccount, getComposioToolsFromWorkflow } from '@/app/actions/composio_actions'; import { z } from 'zod'; import { ZTool, ZListResponse } from '@/app/lib/composio/composio'; import { SlidePanel } from '@/components/ui/slide-panel'; @@ -57,6 +57,7 @@ export function ComposioToolsPanel({ const [hasChanges, setHasChanges] = useState(false); const [showAuthModal, setShowAuthModal] = useState(false); const [isProcessingAuth, setIsProcessingAuth] = useState(false); + const [composioSelectedTools, setComposioSelectedTools] = useState([]); const loadToolsForToolkit = useCallback(async (toolkitSlug: string, cursor: string | null = null) => { try { @@ -80,6 +81,15 @@ export function ComposioToolsPanel({ } }, [projectId]); + const loadComposioSelectedTools = useCallback(async () => { + try { + const tools = await getComposioToolsFromWorkflow(projectId); + setComposioSelectedTools(tools); + } catch (err: any) { + console.error('Error fetching composio selected tools:', err); + } + }, [projectId]); + const handleNextPage = useCallback(async () => { if (!nextCursor || !toolkit) return; @@ -164,14 +174,21 @@ export function ComposioToolsPanel({ } }, [onClose, hasChanges]); - // Initialize selected tools from project config when opening the panel + // Initialize selected tools from workflow when opening the panel useEffect(() => { - if (toolkit && isOpen && projectConfig?.composioSelectedTools) { - const toolSlugs = new Set(projectConfig.composioSelectedTools.map(tool => tool.slug)); + if (toolkit && isOpen) { + loadComposioSelectedTools(); + } + }, [toolkit, isOpen, loadComposioSelectedTools]); + + // Set selected tools when composioSelectedTools is loaded + useEffect(() => { + if (toolkit && composioSelectedTools.length > 0) { + const toolSlugs = new Set(composioSelectedTools.map(tool => tool.slug)); setSelectedTools(toolSlugs); setHasChanges(false); } - }, [toolkit, isOpen, projectConfig]); + }, [toolkit, composioSelectedTools]); useEffect(() => { if (toolkit && isOpen) { @@ -210,44 +227,46 @@ export function ComposioToolsPanel({

{isToolkitConnected ? 'Toolkit Connected' : 'Authentication Required'}

{isToolkitConnected ? 'You can select and use tools from this toolkit' - : 'Connect your account to access and use tools' + : 'You can select tools now. Authentication will be required in the build view to use them.' }

- + {isToolkitConnected && ( + + )}
)} @@ -263,7 +282,7 @@ export function ComposioToolsPanel({ size="sm" color="primary" onPress={handleSaveTools} - disabled={isSaving || !isToolkitConnected} + disabled={isSaving} isLoading={isSaving} > Save Changes @@ -283,17 +302,12 @@ export function ComposioToolsPanel({ ) : (
{tools.map((tool) => ( -
+
handleToolSelectionChange(tool.slug, selected)} size="sm" - isDisabled={!isToolkitConnected} />

diff --git a/apps/rowboat/app/projects/[projectId]/tools/components/CustomServers.tsx b/apps/rowboat/app/projects/[projectId]/tools/components/CustomServers.tsx index 366e58d2..ccf95d69 100644 --- a/apps/rowboat/app/projects/[projectId]/tools/components/CustomServers.tsx +++ b/apps/rowboat/app/projects/[projectId]/tools/components/CustomServers.tsx @@ -24,7 +24,7 @@ import { Modal } from '@/components/ui/modal'; type McpServerType = z.infer; type McpToolType = z.infer['tools'][number]; -export function CustomServers() { +export function CustomServers({ onToolsUpdated }: { onToolsUpdated?: () => void }) { const params = useParams(); const projectId = typeof params.projectId === 'string' ? params.projectId : params.projectId?.[0]; if (!projectId) throw new Error('Project ID is required'); @@ -92,6 +92,9 @@ export function CustomServers() { return s; }); }); + + // Notify parent component about tool updates + onToolsUpdated?.(); } catch (err) { console.error('Toggle failed:', { server: server.name, error: err }); } finally { @@ -161,6 +164,9 @@ export function CustomServers() { // Update selectedTools to include all tools for the custom server setSelectedTools(new Set(updatedAvailableTools.map(tool => tool.id))); } + + // Notify parent component about tool updates + onToolsUpdated?.(); } finally { setSyncingServers(prev => { const next = new Set(prev); @@ -207,6 +213,9 @@ export function CustomServers() { // Fetch tools for the new server using the formatted URL await handleSyncServer(formattedServer); + + // Notify parent component about tool updates + onToolsUpdated?.(); } catch (err) { console.error('Error adding server:', err); setError('Failed to add server. Please try again.'); @@ -229,6 +238,9 @@ export function CustomServers() { if (selectedServer?.name === server.name) { setSelectedServer(null); } + + // Notify parent component about tool updates + onToolsUpdated?.(); } catch (err) { console.error('Error removing server:', err); setError('Failed to remove server. Please try again.'); @@ -271,6 +283,9 @@ export function CustomServers() { }); setHasToolChanges(false); + + // Notify parent component about tool updates + onToolsUpdated?.(); } catch (error) { console.error('Error saving tool selection:', error); } finally { diff --git a/apps/rowboat/app/projects/[projectId]/tools/components/ToolkitCard.tsx b/apps/rowboat/app/projects/[projectId]/tools/components/ToolkitCard.tsx index 9fc7e278..a0147ac3 100644 --- a/apps/rowboat/app/projects/[projectId]/tools/components/ToolkitCard.tsx +++ b/apps/rowboat/app/projects/[projectId]/tools/components/ToolkitCard.tsx @@ -52,9 +52,8 @@ export function ToolkitCard({ }, [onManageTools]); // Calculate selected tools count for this toolkit - const selectedToolsCount = projectConfig?.composioSelectedTools?.filter(tool => - tool.toolkit.slug === toolkit.slug - ).length || 0; + // TODO: Update to use workflow-based tools count + const selectedToolsCount = 0; return (
diff --git a/apps/rowboat/app/projects/[projectId]/tools/page.tsx b/apps/rowboat/app/projects/[projectId]/tools/page.tsx index 2fa0d764..c2bdd9d4 100644 --- a/apps/rowboat/app/projects/[projectId]/tools/page.tsx +++ b/apps/rowboat/app/projects/[projectId]/tools/page.tsx @@ -16,7 +16,7 @@ export default async function ToolsPage() {
Loading...
}> diff --git a/apps/rowboat/app/projects/[projectId]/workflow/app.tsx b/apps/rowboat/app/projects/[projectId]/workflow/app.tsx index aeec01a3..21d9c521 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/app.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/app.tsx @@ -1,6 +1,7 @@ "use client"; import { MCPServer, WithStringId } from "../../../lib/types/types"; import { DataSource } from "../../../lib/types/datasource_types"; +import { Project } from "../../../lib/types/project_types"; import { z } from "zod"; import { useCallback, useEffect, useState } from "react"; import { WorkflowEditor } from "./workflow_editor"; @@ -11,7 +12,6 @@ import { getProjectConfig } from "@/app/actions/project_actions"; import { Workflow, WorkflowTool } from "@/app/lib/types/workflow_types"; import { getEligibleModels } from "@/app/actions/billing_actions"; import { ModelsResponse } from "@/app/lib/types/billing_types"; -import { Project } from "@/app/lib/types/project_types"; export function App({ projectId, @@ -26,6 +26,7 @@ export function App({ const [project, setProject] = useState> | null>(null); const [dataSources, setDataSources] = useState>[] | null>(null); const [projectTools, setProjectTools] = useState[] | null>(null); + const [projectConfig, setProjectConfig] = useState | null>(null); const [loading, setLoading] = useState(false); const [eligibleModels, setEligibleModels] = useState | "*">("*"); const [projectMcpServers, setProjectMcpServers] = useState>>([]); @@ -66,11 +67,70 @@ export function App({ setLoading(false); }, [projectId]); + const handleProjectToolsUpdate = useCallback(async () => { + // Lightweight refresh for tool-only updates + const [projectConfig, projectTools] = await Promise.all([ + getProjectConfig(projectId), + collectProjectTools(projectId), + ]); + + setProject(projectConfig); + setProjectConfig(projectConfig); + setProjectTools(projectTools); + + // Update MCP servers if they changed + if (projectConfig.mcpServers) { + setProjectMcpServers(projectConfig.mcpServers); + } + + // Update webhook URL if it changed + if (projectConfig.webhookUrl) { + setWebhookUrl(projectConfig.webhookUrl); + } + }, [projectId]); // Add this useEffect for initial load useEffect(() => { loadData(); }, [mode, loadData, projectId]); + // Add focus-based refresh to handle cross-page updates + useEffect(() => { + const handleFocus = () => { + // Refresh data when user returns to this page/tab + loadData(); + }; + + const handleVisibilityChange = () => { + if (!document.hidden) { + loadData(); + } + }; + + const handleStorageChange = (e: StorageEvent) => { + // Listen for tool updates from other tabs + if (e.key === `tools-updated-${projectId}` && e.newValue) { + loadData(); + // Clear the flag + localStorage.removeItem(`tools-updated-${projectId}`); + } else if (e.key === `tools-light-refresh-${projectId}` && e.newValue) { + // Lightweight refresh for tool-only updates + handleProjectToolsUpdate(); + // Clear the flag + localStorage.removeItem(`tools-light-refresh-${projectId}`); + } + }; + + window.addEventListener('focus', handleFocus); + document.addEventListener('visibilitychange', handleVisibilityChange); + window.addEventListener('storage', handleStorageChange); + + return () => { + window.removeEventListener('focus', handleFocus); + document.removeEventListener('visibilitychange', handleVisibilityChange); + window.removeEventListener('storage', handleStorageChange); + }; + }, [loadData, handleProjectToolsUpdate, projectId]); + function handleSetMode(mode: 'draft' | 'live') { setMode(mode); } @@ -95,6 +155,7 @@ export function App({ workflow={workflow} dataSources={dataSources} projectTools={projectTools} + projectConfig={projectConfig || project} useRag={useRag} mcpServerUrls={projectMcpServers} toolWebhookUrl={webhookUrl} @@ -102,6 +163,7 @@ export function App({ eligibleModels={eligibleModels} onChangeMode={handleSetMode} onRevertToLive={handleRevertToLive} + onProjectToolsUpdated={handleProjectToolsUpdate} />} } diff --git a/apps/rowboat/app/projects/[projectId]/workflow/components/ComposioToolsModal.tsx b/apps/rowboat/app/projects/[projectId]/workflow/components/ComposioToolsModal.tsx new file mode 100644 index 00000000..3e8a409d --- /dev/null +++ b/apps/rowboat/app/projects/[projectId]/workflow/components/ComposioToolsModal.tsx @@ -0,0 +1,74 @@ +'use client'; + +import React, { useState } from 'react'; +import { Modal, ModalContent, ModalHeader, ModalBody, Tabs, Tab } from '@heroui/react'; +import { Composio } from '../../tools/components/Composio'; +import { ComposioWithCallback } from './ComposioWithCallback'; +import { CustomServers } from '../../tools/components/CustomServers'; +import { WebhookConfig } from '../../tools/components/WebhookConfig'; +import type { Key } from 'react'; + +interface ComposioToolsModalProps { + isOpen: boolean; + onClose: () => void; + projectId: string; + onToolsUpdated?: () => void; +} + +export function ComposioToolsModal({ isOpen, onClose, projectId, onToolsUpdated }: ComposioToolsModalProps) { + const [activeTab, setActiveTab] = useState('composio'); + + const handleTabChange = (key: Key) => { + setActiveTab(key.toString()); + }; + + return ( + + + +

+ Tools +

+
+ + + +
+ +
+
+ +
+ +
+
+ +
+ +
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/apps/rowboat/app/projects/[projectId]/workflow/components/ComposioWithCallback.tsx b/apps/rowboat/app/projects/[projectId]/workflow/components/ComposioWithCallback.tsx new file mode 100644 index 00000000..e2d5a92d --- /dev/null +++ b/apps/rowboat/app/projects/[projectId]/workflow/components/ComposioWithCallback.tsx @@ -0,0 +1,279 @@ +'use client'; + +import { useState, useEffect, useCallback } from 'react'; +import { Button } from '@/components/ui/button'; +import { Info, RefreshCw, Search } from 'lucide-react'; +import clsx from 'clsx'; +import { listToolkits, listTools, updateComposioSelectedTools, getComposioToolsFromWorkflow } 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 '../../tools/components/ComposioToolsPanel'; +import { ToolkitCard } from '../../tools/components/ToolkitCard'; + +type ToolkitType = z.infer; +type ToolkitListResponse = z.infer>>; +type ProjectType = z.infer; + +interface ComposioWithCallbackProps { + projectId: string; + onToolsUpdated?: () => void; +} + +export function ComposioWithCallback({ projectId, onToolsUpdated }: ComposioWithCallbackProps) { + + const [toolkits, setToolkits] = useState([]); + const [projectConfig, setProjectConfig] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [searchQuery, setSearchQuery] = useState(''); + const [selectedToolkit, setSelectedToolkit] = useState(null); + const [isToolsPanelOpen, setIsToolsPanelOpen] = useState(false); + const [savingTools, setSavingTools] = useState(false); + const [composioSelectedTools, setComposioSelectedTools] = useState[]>([]); + + 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 loadComposioSelectedTools = useCallback(async () => { + try { + const tools = await getComposioToolsFromWorkflow(projectId); + setComposioSelectedTools(tools); + } catch (err: any) { + console.error('Error fetching composio selected tools:', err); + } + }, [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); + + 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(); + loadComposioSelectedTools(); + }, [loadProjectConfig, loadComposioSelectedTools]); + + const handleUpdateToolsSelection = useCallback(async (selectedToolObjects: z.infer[]) => { + if (!projectId) return; + + setSavingTools(true); + try { + // Get existing selected tools from workflow + const existingSelectedTools = 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 data to get updated tools + await loadComposioSelectedTools(); + + // Notify parent component that tools were updated + if (onToolsUpdated) { + onToolsUpdated(); + } + } catch (error) { + console.error('Error saving tool selection:', error); + } finally { + setSavingTools(false); + } + }, [projectId, composioSelectedTools, loadComposioSelectedTools, onToolsUpdated]); + + const handleRemoveToolkitTools = useCallback(async (toolkitSlug: string) => { + if (!projectId) return; + + setSavingTools(true); + try { + // Get existing selected tools from workflow + const existingSelectedTools = composioSelectedTools; + + // Filter out all tools from the specified toolkit + const filteredSelectedTools = existingSelectedTools.filter(tool => + tool.toolkit.slug !== toolkitSlug + ); + + await updateComposioSelectedTools(projectId, filteredSelectedTools); + + // Refresh data to get updated tools + await loadComposioSelectedTools(); + + // Notify parent component that tools were updated + if (onToolsUpdated) { + onToolsUpdated(); + } + } catch (error) { + console.error('Error removing toolkit tools:', error); + } finally { + setSavingTools(false); + } + }, [projectId, composioSelectedTools, loadComposioSelectedTools, onToolsUpdated]); + + useEffect(() => { + loadProjectConfig(); + }, [loadProjectConfig]); + + useEffect(() => { + loadAllToolkits(); + loadComposioSelectedTools(); + }, [loadAllToolkits, loadComposioSelectedTools]); + + 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) + ); + }).sort((a, b) => { + // Sort by actual connection status first (only connected tools, not no-auth) + const aConnected = !a.no_auth && projectConfig?.composioConnectedAccounts?.[a.slug]?.status === 'ACTIVE'; + const bConnected = !b.no_auth && projectConfig?.composioConnectedAccounts?.[b.slug]?.status === 'ACTIVE'; + + if (aConnected && !bConnected) return -1; + if (!aConnected && bConnected) return 1; + + // If both have same connection status, maintain original order (don't sort alphabetically) + return 0; + }); + + if (loading) { + return ( +
+
+

Loading Composio toolkits...

+
+ ); + } + + if (error) { + return ( +
+

+ {error} +

+ +
+ ); + } + + return ( +
+ {/* Search Bar */} +
+
+ + setSearchQuery(e.target.value)} + /> +
+
+ + {/* Toolkits Grid */} +
+ {filteredToolkits.length === 0 ? ( +
+

+ {searchQuery ? 'No toolkits found matching your search.' : 'No toolkits available.'} +

+
+ ) : ( +
+ {filteredToolkits.map((toolkit) => { + const isConnected = toolkit.no_auth || projectConfig?.composioConnectedAccounts?.[toolkit.slug]?.status === 'ACTIVE'; + const connectedAccountId = projectConfig?.composioConnectedAccounts?.[toolkit.slug]?.id; + + return ( + handleManageTools(toolkit)} + onProjectConfigUpdate={handleProjectConfigUpdate} + onRemoveToolkitTools={handleRemoveToolkitTools} + /> + ); + })} +
+ )} +
+ + {/* Tools Panel */} + +
+ ); +} \ No newline at end of file diff --git a/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx b/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx index 03c256e1..445b3c75 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx @@ -1,8 +1,10 @@ import { z } from "zod"; -import { WorkflowPrompt, WorkflowAgent, WorkflowTool } from "../../../lib/types/workflow_types"; +import { WorkflowPrompt, WorkflowAgent, WorkflowTool, Workflow } from "../../../lib/types/workflow_types"; +import { Project } from "../../../lib/types/project_types"; import { Dropdown, DropdownItem, DropdownTrigger, DropdownMenu } from "@heroui/react"; import { useRef, useEffect, useState } from "react"; -import { EllipsisVerticalIcon, ImportIcon, PlusIcon, Brain, Boxes, Wrench, PenLine, Library, ChevronDown, ChevronRight, ServerIcon, Component, ScrollText, GripVertical, Users, Cog, CheckCircle2, Eye } from "lucide-react"; +import { EllipsisVerticalIcon, ImportIcon, PlusIcon, Brain, Boxes, Wrench, PenLine, Library, ChevronDown, ChevronRight, ServerIcon, Component, ScrollText, GripVertical, Users, Cog, CheckCircle2, LinkIcon, UnlinkIcon, TestTube, Play, MoreVertical, Eye } from "lucide-react"; +import { Tooltip } from "@heroui/react"; import { DndContext, DragEndEvent, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'; import { SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; @@ -13,6 +15,9 @@ import { clsx } from "clsx"; import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from "@/components/ui/resizable"; import { ServerLogo } from '../tools/components/MCPServersCommon'; import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@heroui/react"; +import { ComposioToolsModal } from './components/ComposioToolsModal'; +import { ToolkitAuthModal } from '../tools/components/ToolkitAuthModal'; +import { deleteConnectedAccount, toggleMockToolkitState } from '@/app/actions/composio_actions'; // Reduced gap size to match Cursor's UI const GAP_SIZE = 4; // 1 unit * 4px (tailwind's default spacing unit) @@ -35,6 +40,7 @@ interface EntityListProps { tools: z.infer[]; projectTools: z.infer[]; prompts: z.infer[]; + workflow: z.infer; selectedEntity: { type: "agent" | "tool" | "prompt" | "visualise"; name: string; @@ -52,6 +58,8 @@ interface EntityListProps { onDeleteTool: (name: string) => void; onDeletePrompt: (name: string) => void; onShowVisualise: (name: string) => void; + onProjectToolsUpdated?: () => void; + projectConfig?: z.infer; } interface EmptyStateProps { @@ -95,7 +103,7 @@ const ListItemWithMenu = ({ }) => { return (
-
+
{mcpServerName ? ( } + className="h-3 w-3" + fallback={} /> ) : icon}
- {name} + {name} -
+
{statusLabel} -
- {menuContent} -
+ {menuContent}
); @@ -166,43 +172,56 @@ const ServerCard = ({ const [isExpanded, setIsExpanded] = useState(false); return ( -
- - {isExpanded && ( -
- {tools.map((tool, index) => ( - onSelectTool(tool.name)} - selectedRef={selectedEntity?.type === "tool" && selectedEntity.name === tool.name ? selectedRef : undefined} - mcpServerName={serverName} - menuContent={ - - } +
+
+ + +
+ + {isExpanded && ( +
+ {tools.map((tool, index) => ( +
+ onSelectTool(tool.name)} + selectedRef={selectedEntity?.type === "tool" && selectedEntity.name === tool.name ? selectedRef : undefined} + mcpServerName={serverName} + menuContent={ +
+ +
+ } + /> +
))}
)} @@ -222,6 +241,7 @@ export function EntityList({ tools, projectTools, prompts, + workflow, selectedEntity, startAgentName, onSelectAgent, @@ -235,7 +255,9 @@ export function EntityList({ onDeleteAgent, onDeleteTool, onDeletePrompt, + onProjectToolsUpdated, projectId, + projectConfig, onReorderAgents, onShowVisualise, }: EntityListProps & { @@ -243,6 +265,7 @@ export function EntityList({ onReorderAgents: (agents: z.infer[]) => void }) { const [showAgentTypeModal, setShowAgentTypeModal] = useState(false); + const [showComposioToolsModal, setShowComposioToolsModal] = useState(false); const handleAddAgentWithType = (agentType: 'internal' | 'user_facing') => { onAddAgent({ @@ -496,20 +519,36 @@ export function EntityList({ Tools
- +
+ + +
} > @@ -536,17 +575,37 @@ export function EntityList({ return ( <> - {/* Show composio cards */} - {Object.values(composioTools).map((card) => ( - - ))} + {/* Show composio cards - ordered by status */} + {Object.values(composioTools) + .sort((a, b) => { + // Helper function to get toolkit status priority + const getStatusPriority = (toolkit: ComposioToolkit) => { + const hasAuth = toolkit.tools.some(tool => tool.composioData && !tool.composioData.noAuth); + const isConnected = !hasAuth || projectConfig?.composioConnectedAccounts?.[toolkit.slug]?.status === 'ACTIVE'; + const isMocked = workflow?.composioMockToolkitStates?.[toolkit.slug]?.isMocked || false; + + // Priority: Connected (1) > Mock (2) > Disconnected (3) + if (isMocked) return 2; + if (isConnected) return 1; + return 3; + }; + + return getStatusPriority(a) - getStatusPriority(b); + }) + .map((card) => ( + + ))} {/* Show MCP server cards */} {Object.entries(serverTools).map(([serverName, tools]) => ( @@ -682,6 +741,12 @@ export function EntityList({ onClose={() => setShowAgentTypeModal(false)} onConfirm={handleAddAgentWithType} /> + setShowComposioToolsModal(false)} + projectId={projectId} + onToolsUpdated={onProjectToolsUpdated} + />
); } @@ -769,6 +834,10 @@ interface ComposioCardProps { onSelectTool: (name: string) => void; onDeleteTool: (name: string) => void; selectedRef: React.RefObject; + projectConfig?: z.infer; + projectId: string; + workflow: z.infer; + onProjectToolsUpdated?: () => void; } const ComposioCard = ({ @@ -777,70 +846,231 @@ const ComposioCard = ({ onSelectTool, onDeleteTool, selectedRef, + projectConfig, + projectId, + workflow, + onProjectToolsUpdated, }: ComposioCardProps) => { const [isExpanded, setIsExpanded] = useState(false); + const [showAuthModal, setShowAuthModal] = useState(false); + const [isProcessingAuth, setIsProcessingAuth] = useState(false); + const [isProcessingMock, setIsProcessingMock] = useState(false); + + // Check if the toolkit requires authentication + const hasToolkitWithAuth = card.tools.some(tool => tool.composioData && !tool.composioData.noAuth); + + // Check if toolkit is connected + const isToolkitConnected = !hasToolkitWithAuth || projectConfig?.composioConnectedAccounts?.[card.slug]?.status === 'ACTIVE'; + + // Check if toolkit is mocked + const isToolkitMocked = workflow?.composioMockToolkitStates?.[card.slug]?.isMocked || false; + + const handleConnect = () => { + setShowAuthModal(true); + }; + + const handleDisconnect = async () => { + const connectedAccountId = projectConfig?.composioConnectedAccounts?.[card.slug]?.id; + + setIsProcessingAuth(true); + try { + if (connectedAccountId) { + await deleteConnectedAccount(projectId, card.slug, connectedAccountId); + onProjectToolsUpdated?.(); + } + } catch (err: any) { + console.error('Disconnect failed:', err); + } finally { + setIsProcessingAuth(false); + } + }; + + const handleAuthComplete = () => { + setShowAuthModal(false); + onProjectToolsUpdated?.(); + }; + + const handleToggleMock = async () => { + setIsProcessingMock(true); + try { + await toggleMockToolkitState(projectId, card.slug, !isToolkitMocked); + onProjectToolsUpdated?.(); + } catch (err: any) { + console.error('Mock toggle failed:', err); + } finally { + setIsProcessingMock(false); + } + }; return ( -
- + + {/* Compact Status Badge */} + +
+ {isToolkitMocked ? 'M' : isToolkitConnected ? '●' : '○'} +
+
+ + {/* Actions Dropdown - only show on hover */} +
+ + + + + { + switch (key) { + case 'mock': + handleToggleMock(); + break; + case 'connect': + handleConnect(); + break; + case 'disconnect': + handleDisconnect(); + break; + } + }} + disabledKeys={[ + ...(isProcessingMock ? ['mock'] : []), + ...(isProcessingAuth ? ['connect', 'disconnect'] : []), + ...(hasToolkitWithAuth && !isToolkitMocked && isToolkitConnected ? [] : ['disconnect']), + ...(hasToolkitWithAuth && !isToolkitConnected ? [] : ['connect']) + ]} + > +
+ ) : isToolkitMocked ? ( + + ) : ( + + ) + } + > + {isProcessingMock + ? (isToolkitMocked ? 'Disabling Mock...' : 'Enabling Mock...') + : (isToolkitMocked ? 'Switch to Real Mode' : 'Switch to Mock Mode') + } + + +
+ ) : ( + + ) + } + > + {isProcessingAuth ? 'Disconnecting...' : 'Disconnect'} + + +
+ ) : ( + + ) + } + > + {isProcessingAuth ? 'Connecting...' : 'Connect'} + + + +
- {isExpanded && ( -
+
{card.tools.map((tool, index) => ( - onSelectTool(tool.name)} - disabled={tool.isLibrary} - selectedRef={selectedEntity?.type === "tool" && selectedEntity.name === tool.name ? selectedRef : undefined} - icon={ - card.logo ? ( -
- + onSelectTool(tool.name)} + disabled={tool.isLibrary} + selectedRef={selectedEntity?.type === "tool" && selectedEntity.name === tool.name ? selectedRef : undefined} + icon={ +
+ } + menuContent={ +
+
- ) : ( - - ) - } - menuContent={ - - } - /> + } + /> +
))}
)} -
+

+ + {/* Auth Modal */} + {hasToolkitWithAuth && ( + setShowAuthModal(false)} + toolkitSlug={card.slug} + projectId={projectId} + onComplete={handleAuthComplete} + /> + )} + ); }; diff --git a/apps/rowboat/app/projects/[projectId]/workflow/mcp_imports.tsx b/apps/rowboat/app/projects/[projectId]/workflow/mcp_imports.tsx new file mode 100644 index 00000000..c6700480 --- /dev/null +++ b/apps/rowboat/app/projects/[projectId]/workflow/mcp_imports.tsx @@ -0,0 +1,151 @@ +"use client"; +import { useCallback, useEffect, useState } from "react"; +import { Button, Spinner, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Checkbox } from "@heroui/react"; +import { z } from "zod"; +import { WorkflowTool } from "@/app/lib/types/workflow_types"; +import { RefreshCwIcon } from "lucide-react"; +import { fetchMcpTools } from "@/app/actions/mcp_actions"; + +interface McpImportToolsProps { + projectId: string; + isOpen: boolean; + onOpenChange: (open: boolean) => void; + onImport: (tools: z.infer[]) => void; +} + +export function McpImportTools({ projectId, isOpen, onOpenChange, onImport }: McpImportToolsProps) { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [tools, setTools] = useState[]>([]); + const [selectedTools, setSelectedTools] = useState>(new Set()); + + const process = useCallback(async () => { + setLoading(true); + setError(null); + setSelectedTools(new Set()); + try { + const result = await fetchMcpTools(projectId); + setTools(result); + // Select all tools by default + setSelectedTools(new Set(result.map((_, index) => index))); + } catch (error) { + setError(`Unable to fetch tools: ${error}`); + } finally { + setLoading(false); + } + }, [projectId]); + + useEffect(() => { + console.log("mcp import tools useEffect", isOpen); + if (isOpen) { + process(); + } + }, [isOpen, process]); + + return ( + + + {(onClose) => ( + <> + Import from MCP servers + + {loading &&
+ + Fetching tools... +
} + {error &&
+ {error} + +
} + {!loading && !error && <> +
+
+ {tools.length === 0 ? "No tools found" : `Found ${tools.length} tools:`} +
+ +
+ {tools.length > 0 &&
+
+
+ 0 && selectedTools.size < tools.length} + onValueChange={(checked) => { + if (checked) { + setSelectedTools(new Set(tools.map((_, i) => i))); + } else { + setSelectedTools(new Set()); + } + }} + /> +
+
Server
+
Tool Name
+
+
+ {tools.map((t, index) => ( +
+
+ { + const newSelected = new Set(selectedTools); + if (checked) { + newSelected.add(index); + } else { + newSelected.delete(index); + } + setSelectedTools(newSelected); + }} + /> +
+
+
+ {t.mcpServerName} +
+
+
{t.name}
+
+ ))} +
+
} + {tools.length > 0 && ( +
+ {selectedTools.size} of {tools.length} tools selected +
+ )} + } +
+ + + {tools.length > 0 && } + + + )} +
+
+ ); +} \ No newline at end of file diff --git a/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx b/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx index 5f5a8bce..94ba6696 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx @@ -3,6 +3,7 @@ import React, { useReducer, Reducer, useState, useCallback, useEffect, useRef, c import { MCPServer, Message, WithStringId } from "../../../lib/types/types"; import { Workflow, WorkflowTool, WorkflowPrompt, WorkflowAgent } from "../../../lib/types/workflow_types"; import { DataSource } from "../../../lib/types/datasource_types"; +import { Project } from "../../../lib/types/project_types"; import { produce, applyPatches, enablePatches, produceWithPatches, Patch } from 'immer'; import { AgentConfig } from "../entities/agent_config"; import { ToolConfig } from "../entities/tool_config"; @@ -142,6 +143,9 @@ export type Action = { type: "show_visualise"; } | { type: "hide_visualise"; +} | { + type: "sync_workflow"; + workflow: z.infer; }; function reducer(state: State, action: Action): State { @@ -207,6 +211,13 @@ function reducer(state: State, action: Action): State { }); break; } + case "sync_workflow": { + newState = produce(state, draft => { + draft.present.workflow = action.workflow; + draft.present.lastUpdatedAt = action.workflow.lastUpdatedAt; + }); + break; + } case "reorder_agents": { const newState = produce(state.present, draft => { draft.workflow.agents = action.agents; @@ -579,10 +590,12 @@ export function WorkflowEditor({ toolWebhookUrl, defaultModel, projectTools, + projectConfig, eligibleModels, isLive, onChangeMode, onRevertToLive, + onProjectToolsUpdated, }: { projectId: string; dataSources: WithStringId>[]; @@ -592,10 +605,12 @@ export function WorkflowEditor({ toolWebhookUrl: string; defaultModel: string; projectTools: z.infer[]; + projectConfig: z.infer; eligibleModels: z.infer | "*"; isLive: boolean; onChangeMode: (mode: 'draft' | 'live') => void; onRevertToLive: () => void; + onProjectToolsUpdated?: () => void; }) { const [state, dispatch] = useReducer(reducer, { @@ -615,6 +630,12 @@ export function WorkflowEditor({ isLive, } }); + + // Sync workflow prop changes with reducer state (e.g., when composio tools are updated) + useEffect(() => { + dispatch({ type: "sync_workflow", workflow }); + }, [workflow]); + const [chatMessages, setChatMessages] = useState[]>([]); const updateChatMessages = useCallback((messages: z.infer[]) => { setChatMessages(messages); @@ -980,6 +1001,7 @@ export function WorkflowEditor({ tools={state.present.workflow.tools} projectTools={projectTools} prompts={state.present.workflow.prompts} + workflow={state.present.workflow} selectedEntity={ state.present.selection && (state.present.selection.type === "agent" || @@ -1002,6 +1024,8 @@ export function WorkflowEditor({ onDeletePrompt={handleDeletePrompt} onShowVisualise={handleShowVisualise} projectId={projectId} + onProjectToolsUpdated={onProjectToolsUpdated} + projectConfig={projectConfig} onReorderAgents={handleReorderAgents} />
diff --git a/apps/rowboat/app/projects/layout/components/sidebar.tsx b/apps/rowboat/app/projects/layout/components/sidebar.tsx index 3859e8eb..3960b4fa 100644 --- a/apps/rowboat/app/projects/layout/components/sidebar.tsx +++ b/apps/rowboat/app/projects/layout/components/sidebar.tsx @@ -14,8 +14,7 @@ import { ChevronRightIcon, Moon, Sun, - HelpCircle, - Wrench + HelpCircle } from "lucide-react"; import { getProjectConfig } from "@/app/actions/project_actions"; import { useTheme } from "@/app/providers/theme-provider"; @@ -69,13 +68,6 @@ export default function Sidebar({ projectId, useRag, useAuth, collapsed = false, icon: DatabaseIcon, requiresProject: true }] : []), - { - href: 'tools', - label: 'Tools', - icon: Wrench, - requiresProject: true, - beta: true - }, { href: 'config', label: 'Settings', @@ -162,14 +154,7 @@ export default function Sidebar({ projectId, useRag, useAuth, collapsed = false, `} /> {!collapsed && ( - <> - {item.label} - {item.beta && ( - - BETA - - )} - + {item.label} )} diff --git a/apps/rowboat/package-lock.json b/apps/rowboat/package-lock.json index 39ba889c..ffa101b8 100644 --- a/apps/rowboat/package-lock.json +++ b/apps/rowboat/package-lock.json @@ -43,7 +43,7 @@ "ioredis": "^5.6.1", "jose": "^5.9.6", "lucide-react": "^0.465.0", - "mermaid": "^11.8.1", + "mermaid": "^11.9.0", "mongodb": "^6.8.0", "next": "15.3.4", "openai": "^4.67.2", @@ -1396,7 +1396,6 @@ "version": "11.0.3", "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", - "license": "Apache-2.0", "dependencies": { "@chevrotain/gast": "11.0.3", "@chevrotain/types": "11.0.3", @@ -1407,7 +1406,6 @@ "version": "11.0.3", "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz", "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", - "license": "Apache-2.0", "dependencies": { "@chevrotain/types": "11.0.3", "lodash-es": "4.17.21" @@ -1416,20 +1414,17 @@ "node_modules/@chevrotain/regexp-to-ast": { "version": "11.0.3", "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", - "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==", - "license": "Apache-2.0" + "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==" }, "node_modules/@chevrotain/types": { "version": "11.0.3", "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz", - "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==", - "license": "Apache-2.0" + "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==" }, "node_modules/@chevrotain/utils": { "version": "11.0.3", "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz", - "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", - "license": "Apache-2.0" + "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==" }, "node_modules/@composio/client": { "version": "0.1.0-alpha.26", @@ -4582,10 +4577,9 @@ } }, "node_modules/@mermaid-js/parser": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.1.tgz", - "integrity": "sha512-lCQNpV8R4lgsGcjX5667UiuDLk2micCtjtxR1YKbBXvN5w2v+FeLYoHrTSSrjwXdMcDYvE4ZBPvKT31dfeSmmA==", - "license": "MIT", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.2.tgz", + "integrity": "sha512-+PO02uGF6L6Cs0Bw8RpGhikVvMWEysfAyl27qTlroUB8jSWr1lL0Sf6zi78ZxlSnmgSY2AMMKVgghnN9jTtwkQ==", "dependencies": { "langium": "3.3.1" } @@ -9222,7 +9216,6 @@ "version": "11.0.3", "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", - "license": "Apache-2.0", "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", @@ -9236,7 +9229,6 @@ "version": "0.3.1", "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz", "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==", - "license": "MIT", "dependencies": { "lodash-es": "^4.17.21" }, @@ -12928,7 +12920,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/langium/-/langium-3.3.1.tgz", "integrity": "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==", - "license": "MIT", "dependencies": { "chevrotain": "~11.0.3", "chevrotain-allstar": "~0.3.0", @@ -13440,15 +13431,14 @@ } }, "node_modules/marked": { - "version": "15.0.12", - "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", - "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", - "license": "MIT", + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.1.0.tgz", + "integrity": "sha512-Me7BNa1aqrxVinDnFfvCgHh2yHvLbFvILBs899MhuBpbE5VPzpSqv7alaESfkqkgc9JNvUGH4gqwZeOzLnY8Jg==", "bin": { "marked": "bin/marked.js" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/math-intrinsics": { @@ -13781,14 +13771,13 @@ } }, "node_modules/mermaid": { - "version": "11.8.1", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.8.1.tgz", - "integrity": "sha512-VSXJLqP1Sqw5sGr273mhvpPRhXwE6NlmMSqBZQw+yZJoAJkOIPPn/uT3teeCBx60Fkt5zEI3FrH2eVT0jXRDzw==", - "license": "MIT", + "version": "11.9.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.9.0.tgz", + "integrity": "sha512-YdPXn9slEwO0omQfQIsW6vS84weVQftIyyTGAZCwM//MGhPzL1+l6vO6bkf0wnP4tHigH1alZ5Ooy3HXI2gOag==", "dependencies": { "@braintree/sanitize-url": "^7.0.4", "@iconify/utils": "^2.1.33", - "@mermaid-js/parser": "^0.6.1", + "@mermaid-js/parser": "^0.6.2", "@types/d3": "^7.4.3", "cytoscape": "^3.29.3", "cytoscape-cose-bilkent": "^4.1.0", @@ -13798,10 +13787,10 @@ "dagre-d3-es": "7.0.11", "dayjs": "^1.11.13", "dompurify": "^3.2.5", - "katex": "^0.16.9", + "katex": "^0.16.22", "khroma": "^2.1.0", "lodash-es": "^4.17.21", - "marked": "^15.0.7", + "marked": "^16.0.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", @@ -17797,7 +17786,6 @@ "version": "8.2.0", "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", - "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -17806,7 +17794,6 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", - "license": "MIT", "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, @@ -17818,7 +17805,6 @@ "version": "3.17.5", "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", - "license": "MIT", "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" @@ -17827,20 +17813,17 @@ "node_modules/vscode-languageserver-textdocument": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", - "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", - "license": "MIT" + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==" }, "node_modules/vscode-languageserver-types": { "version": "3.17.5", "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", - "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", - "license": "MIT" + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" }, "node_modules/vscode-uri": { "version": "3.0.8", "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", - "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", - "license": "MIT" + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==" }, "node_modules/web-streams-polyfill": { "version": "4.0.0-beta.3", diff --git a/apps/rowboat/package.json b/apps/rowboat/package.json index 43198960..46750a5c 100644 --- a/apps/rowboat/package.json +++ b/apps/rowboat/package.json @@ -50,7 +50,7 @@ "ioredis": "^5.6.1", "jose": "^5.9.6", "lucide-react": "^0.465.0", - "mermaid": "^11.8.1", + "mermaid": "^11.9.0", "mongodb": "^6.8.0", "next": "15.3.4", "openai": "^4.67.2",