├── .dockerignore ├── .gitignore ├── CHANGELOG.md ├── DOCKER.md ├── Dockerfile ├── LICENSE ├── README.md ├── docker-compose.yml ├── docs ├── README.md ├── api-reference │ └── README.md ├── architecture │ └── README.md ├── contributing │ ├── README.md │ └── documentation-guidelines.md ├── features │ ├── README.md │ └── mcp │ │ └── docker-servers.md └── getting-started │ └── README.md ├── eslint.config.mjs ├── githubpages ├── img │ ├── api-keys.png │ ├── cline-integration1.png │ ├── cline-integration2.png │ ├── favicon.png │ ├── flows.png │ ├── integration-roo.png │ ├── logo_grok_i6tPUEYtog0qXnrt-generated_image.jpg │ ├── mcp-manager-inspector.png │ ├── mit-license.png │ ├── ollama-model.png │ ├── processnode-prompt.png │ └── server-env.png └── index.html ├── index.html ├── next-env.d.ts ├── next.config.ts ├── package-lock.json ├── package.json ├── plan.md ├── postcss.config.mjs ├── public ├── favicon.ico ├── file.svg ├── globe.svg ├── next.svg ├── vercel.svg └── window.svg ├── scripts ├── build-docker.sh ├── conditional-postinstall.js ├── docker-entrypoint.sh ├── prepare-docker-package.js └── run-docker.sh ├── server.js ├── src ├── app │ ├── ___favicon.ico │ ├── _favicon.ico │ ├── api │ │ ├── README.md │ │ ├── backup │ │ │ ├── README.md │ │ │ └── route.ts │ │ ├── cwd │ │ │ ├── README.md │ │ │ └── route.ts │ │ ├── encryption │ │ │ └── secure │ │ │ │ ├── README.md │ │ │ │ └── route.ts │ │ ├── env │ │ │ ├── README.md │ │ │ └── route.ts │ │ ├── flow │ │ │ ├── README.md │ │ │ ├── flow-adapter.ts │ │ │ ├── handlers.ts │ │ │ ├── prompt-renderer │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── git │ │ │ ├── README.md │ │ │ └── route.ts │ │ ├── init │ │ │ ├── README.md │ │ │ └── route.ts │ │ ├── mcp │ │ │ ├── README.md │ │ │ ├── cancel │ │ │ │ └── route.ts │ │ │ ├── config-adapter.ts │ │ │ ├── config.ts │ │ │ ├── connection-adapter.ts │ │ │ ├── connection.ts │ │ │ ├── handlers.ts │ │ │ ├── route.ts │ │ │ ├── tools-adapter.ts │ │ │ ├── tools.ts │ │ │ └── types.ts │ │ ├── model │ │ │ ├── README.md │ │ │ ├── backend-model-adapter.ts │ │ │ ├── backend-provider-adapter.ts │ │ │ ├── frontend-model-adapter.ts │ │ │ ├── provider │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── restore │ │ │ ├── README.md │ │ │ └── route.ts │ │ └── storage │ │ │ ├── README.md │ │ │ └── route.ts │ ├── chat │ │ └── page.tsx │ ├── flows │ │ └── page.tsx │ ├── globals.css │ ├── layout.tsx │ ├── mcp │ │ └── page.tsx │ ├── models │ │ ├── ModelClient.tsx │ │ ├── README-1.svg │ │ ├── README.md │ │ ├── error.tsx │ │ ├── loading.tsx │ │ └── page.tsx │ ├── page.tsx │ ├── settings │ │ └── page.tsx │ └── v1 │ │ ├── api │ │ └── tags │ │ │ └── route.ts │ │ ├── chat │ │ ├── completions │ │ │ ├── README.md │ │ │ ├── chatCompletionService.backup_passthrough.ts │ │ │ ├── chatCompletionService.ts │ │ │ ├── requestParser.ts │ │ │ └── route.ts │ │ └── conversations │ │ │ ├── [conversationId] │ │ │ ├── cancel │ │ │ │ └── route.ts │ │ │ ├── debug │ │ │ │ ├── continue │ │ │ │ │ └── route.ts │ │ │ │ └── step │ │ │ │ │ └── route.ts │ │ │ ├── respond │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ │ └── route.ts │ │ └── models │ │ └── route.ts ├── backend │ ├── execution │ │ └── flow │ │ │ ├── FlowConverter.ts │ │ │ ├── FlowConverter.ts.backup │ │ │ ├── FlowExecutor.ts │ │ │ ├── FlowExecutor.ts.backup │ │ │ ├── errorFactory.ts │ │ │ ├── errors.ts │ │ │ ├── handlers │ │ │ ├── MCPHandler.ts │ │ │ ├── ModelHandler.ts │ │ │ └── ToolHandler.ts │ │ │ ├── index.ts │ │ │ ├── nodes │ │ │ ├── FinishNode.ts │ │ │ ├── MCPNode.ts │ │ │ ├── ProcessNode.ts │ │ │ ├── StartNode.ts │ │ │ ├── handlers │ │ │ │ ├── ProcessNodeModelHandler.ts │ │ │ │ └── ProcessNodeToolHandler.ts │ │ │ ├── index.ts │ │ │ └── util │ │ │ │ ├── MCPNodeUtility.ts │ │ │ │ ├── ProcessNodeParsingUtility.ts │ │ │ │ └── ProcessNodeUtility.ts │ │ │ ├── temp_pocket.ts │ │ │ ├── types.ts │ │ │ └── types │ │ │ ├── mcpHandler.ts │ │ │ ├── modelHandler.ts │ │ │ └── toolHandler.ts │ ├── services │ │ ├── flow │ │ │ ├── README.md │ │ │ └── index.ts │ │ ├── mcp │ │ │ ├── README.md │ │ │ ├── config.ts │ │ │ ├── connection.ts │ │ │ ├── index.ts │ │ │ └── tools.ts │ │ └── model │ │ │ ├── README.md │ │ │ ├── encryption.ts │ │ │ ├── index.ts │ │ │ └── provider.ts │ ├── types │ │ └── index.ts │ └── utils │ │ ├── PromptRenderer.test.ts │ │ ├── PromptRenderer.ts │ │ └── resolveGlobalVars.ts ├── config │ └── features.ts ├── frontend │ ├── components │ │ ├── AppWrapper.tsx │ │ ├── Chat │ │ │ ├── ChatHistory.tsx │ │ │ ├── ChatInput.tsx │ │ │ ├── ChatMessages.tsx │ │ │ ├── DebuggerCanvas.tsx │ │ │ ├── FlowSelector.tsx │ │ │ └── index.tsx │ │ ├── ClientOnly.tsx │ │ ├── EncryptionAuthDialog.tsx │ │ ├── Flow │ │ │ ├── FlowDashboard │ │ │ │ ├── FlowCard.tsx │ │ │ │ ├── FlowDashboard.tsx │ │ │ │ └── index.tsx │ │ │ ├── FlowManager │ │ │ │ ├── FlowBuilder │ │ │ │ │ ├── Canvas │ │ │ │ │ │ ├── Canvas.tsx │ │ │ │ │ │ ├── components │ │ │ │ │ │ │ ├── CanvasControls.tsx │ │ │ │ │ │ │ └── CanvasToolbar.tsx │ │ │ │ │ │ ├── hooks │ │ │ │ │ │ │ ├── useCanvasEvents.ts │ │ │ │ │ │ │ ├── useCanvasState.ts │ │ │ │ │ │ │ └── useProximityConnect.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── types.ts │ │ │ │ │ │ └── utils │ │ │ │ │ │ │ ├── edgeUtils.ts │ │ │ │ │ │ │ └── nodeUtils.ts │ │ │ │ │ ├── ContextMenu.tsx │ │ │ │ │ ├── CustomEdges │ │ │ │ │ │ ├── CustomEdge.tsx │ │ │ │ │ │ ├── MCPEdge.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── CustomNodes │ │ │ │ │ │ ├── BaseNode.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── FlowList.tsx │ │ │ │ │ ├── Modals │ │ │ │ │ │ ├── FinishNodePropertiesModal.tsx │ │ │ │ │ │ ├── MCPNodePropertiesModal.tsx │ │ │ │ │ │ ├── NodePropertiesModal.tsx │ │ │ │ │ │ ├── ProcessNodePropertiesModal.tsx │ │ │ │ │ │ ├── ProcessNodePropertiesModal │ │ │ │ │ │ │ ├── ModelBinding.tsx │ │ │ │ │ │ │ ├── ModelBinding │ │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ │ ├── NodeConfiguration.tsx │ │ │ │ │ │ │ ├── NodeProperties.tsx │ │ │ │ │ │ │ ├── PromptTemplateEditor.tsx │ │ │ │ │ │ │ ├── ServerTools │ │ │ │ │ │ │ │ ├── AgentTools.tsx │ │ │ │ │ │ │ │ ├── ServerTools.tsx │ │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ │ ├── hooks │ │ │ │ │ │ │ │ ├── useHandoffTools.ts │ │ │ │ │ │ │ │ ├── useModelManagement.ts │ │ │ │ │ │ │ │ ├── useNodeData.ts │ │ │ │ │ │ │ │ └── useServerConnection.ts │ │ │ │ │ │ │ ├── types.ts │ │ │ │ │ │ │ └── utils.ts │ │ │ │ │ │ └── StartNodePropertiesModal.tsx │ │ │ │ │ ├── NodePalette.tsx │ │ │ │ │ ├── PropertiesPanel.tsx │ │ │ │ │ └── index.tsx │ │ │ │ └── FlowLayout.tsx │ │ │ └── index.ts │ │ ├── Navigation │ │ │ └── index.tsx │ │ ├── Settings │ │ │ ├── BackupSettings.tsx │ │ │ ├── EncryptionSettings.tsx │ │ │ ├── GlobalEnvSettings.tsx │ │ │ ├── SpeechRecognitionSettings.tsx │ │ │ ├── ThemeSettings.tsx │ │ │ └── index.tsx │ │ ├── mcp │ │ │ ├── MCPEnvManager │ │ │ │ ├── EnvEditor.tsx │ │ │ │ └── index.tsx │ │ │ ├── MCPServerManager │ │ │ │ ├── Modals │ │ │ │ │ └── ServerModal │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── tabs │ │ │ │ │ │ ├── DockerTab │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── GitHubTab │ │ │ │ │ │ │ ├── GitHubActions.tsx │ │ │ │ │ │ │ ├── GitHubForm.tsx │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── LocalServerTab │ │ │ │ │ │ │ ├── ArgumentsManager.tsx │ │ │ │ │ │ │ ├── BuildTools.tsx │ │ │ │ │ │ │ ├── ConsoleOutput.tsx │ │ │ │ │ │ │ ├── EnvManager.tsx │ │ │ │ │ │ │ ├── LocalServerForm.tsx │ │ │ │ │ │ │ ├── RunTools.tsx │ │ │ │ │ │ │ ├── components │ │ │ │ │ │ │ │ ├── BuildSection.tsx │ │ │ │ │ │ │ │ ├── ConsoleToggle.tsx │ │ │ │ │ │ │ │ ├── DefineServerSection.tsx │ │ │ │ │ │ │ │ ├── RunSection.tsx │ │ │ │ │ │ │ │ └── SectionHeader.tsx │ │ │ │ │ │ │ ├── hooks │ │ │ │ │ │ │ │ ├── useConsoleOutput.ts │ │ │ │ │ │ │ │ └── useLocalServerState.ts │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ └── utils │ │ │ │ │ │ │ │ └── formHandlers.ts │ │ │ │ │ │ ├── ReferenceServersTab │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ └── SmitheryTab.tsx │ │ │ │ │ │ ├── types.ts │ │ │ │ │ │ └── utils │ │ │ │ │ │ ├── buildUtils.ts │ │ │ │ │ │ ├── configDetection.ts │ │ │ │ │ │ ├── configUtils.ts │ │ │ │ │ │ ├── errorHandling.ts │ │ │ │ │ │ └── gitHubUtils.ts │ │ │ │ ├── ServerCard.tsx │ │ │ │ ├── ServerList.tsx │ │ │ │ └── index.tsx │ │ │ ├── MCPToolManager │ │ │ │ ├── ToolTester.tsx │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── models │ │ │ ├── list │ │ │ │ ├── ModelCard.tsx │ │ │ │ └── ModelList.tsx │ │ │ └── modal │ │ │ │ └── index.tsx │ │ └── shared │ │ │ ├── PromptBuilder │ │ │ ├── index.tsx │ │ │ └── promptBuilder.css │ │ │ ├── PromptRendererDemo.tsx │ │ │ └── Spinner.tsx │ ├── contexts │ │ ├── StorageContext.tsx │ │ ├── ThemeContext.tsx │ │ └── index.ts │ ├── hooks │ │ ├── useKeyPress.ts │ │ ├── useServerStatus.ts │ │ └── useServerTools.ts │ ├── services │ │ ├── flow │ │ │ ├── README.md │ │ │ └── index.ts │ │ ├── mcp │ │ │ ├── README.md │ │ │ └── index.ts │ │ ├── model │ │ │ ├── README.md │ │ │ └── index.ts │ │ └── transcription │ │ │ ├── client.tsx │ │ │ ├── index.ts │ │ │ └── webSpeech.ts │ ├── types │ │ ├── flow │ │ │ ├── custom-events.d.ts │ │ │ └── flow.ts │ │ └── model │ │ │ └── index.ts │ └── utils │ │ ├── README.md │ │ ├── index.ts │ │ ├── muiTheme.ts │ │ └── theme.ts ├── shared │ └── types │ │ ├── chat.ts │ │ ├── constants.ts │ │ ├── flow │ │ ├── flow.ts │ │ ├── index.ts │ │ └── response.ts │ │ ├── index.ts │ │ ├── mcp │ │ ├── index.ts │ │ └── mcp.ts │ │ ├── model │ │ ├── index.ts │ │ ├── model.ts │ │ ├── provider.ts │ │ └── response.ts │ │ └── storage │ │ ├── index.ts │ │ └── storage.ts └── utils │ ├── encryption │ ├── README.md │ ├── index.ts │ ├── secure.ts │ └── session.ts │ ├── logger │ ├── README.md │ ├── index.ts │ └── logger.ts │ ├── mcp │ ├── configparse │ │ ├── index.ts │ │ ├── java.ts │ │ ├── kotlin.ts │ │ ├── python.ts │ │ ├── types.ts │ │ ├── typescript.ts │ │ └── utils.ts │ ├── directExecution.ts │ ├── index.ts │ ├── parseServerConfig.ts │ ├── parseServerConfigFromClipboard.ts │ ├── processPathLikeArgument.ts │ ├── types.ts │ └── utils.ts │ ├── shared.ts │ ├── shared │ ├── common.ts │ └── index.ts │ └── storage │ ├── backend.ts │ ├── frontend.ts │ └── index.ts └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | # Version control 2 | .git 3 | .gitignore 4 | 5 | # Node.js 6 | node_modules 7 | npm-debug.log 8 | yarn-debug.log 9 | yarn-error.log 10 | 11 | # Build artifacts 12 | .next 13 | out 14 | dist 15 | build 16 | 17 | # Docker 18 | Dockerfile 19 | docker-compose.yml 20 | .dockerignore 21 | 22 | # Development and environment files 23 | .env 24 | .env.local 25 | .env.development 26 | .env.test 27 | .env.production 28 | *.log 29 | 30 | # Editor directories and files 31 | .idea 32 | .vscode 33 | *.swp 34 | *.swo 35 | 36 | # OS generated files 37 | .DS_Store 38 | Thumbs.db 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | node_modules 133 | dist 134 | mcp-servers 135 | build 136 | .vscode 137 | *.code-workspace 138 | 139 | # *.html 140 | # *.ts 141 | # *.js 142 | # *.mjs 143 | # *.json 144 | # *.md 145 | # public 146 | # src 147 | 148 | ### all except documentation 149 | # * 150 | # **/*.md 151 | implementation-details.md 152 | implementation-plan.md 153 | files-without-logger.md 154 | src/testbridge.ps1 155 | summary.md 156 | update-logger.js 157 | /db 158 | check-deprecated.js 159 | check-deprecated.ps1 160 | check-deps.js 161 | check-punycode.js 162 | deprecated-packages-report.md 163 | deprecated-packages.txt 164 | deprecation-report.md 165 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.1.3] - 2025-04-07 4 | 5 | ### Added 6 | - Handoff tools in flowbuilder for improved flow control 7 | - Agent Tools tab in Process Node properties modal 8 | - Message editing functionality in chat interface 9 | - Background execution capabilities for improved performance 10 | - FlujoChatMessage Type for better internal message handling 11 | - Debugging capabilities with step-by-step execution (disabled until ready) 12 | - DebuggerCanvas component for visualizing flow execution (disabled until ready) 13 | 14 | ### Fixed 15 | - Timestamp validation and handling issues 16 | - Improved error handling in flow execution 17 | - Enhanced logging for better debugging 18 | - Fixed issues with handoff tool generation in ProcessNode 19 | 20 | ### Changed 21 | - Refactored ProcessNodePropertiesModal for better organization 22 | - Updated UI styling for handoff tools 23 | - Improved API response handling 24 | 25 | ## [0.1.2] - 2025-03-14 26 | 27 | - Flowbuilder UI Rework 28 | - React Re-Rendering Issues 29 | - better stop_reason handling 30 | - better chat experience 31 | 32 | ## [0.1.1] - Initial Release 33 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage 1: Prepare stage 2 | FROM node:20-alpine AS prepare 3 | 4 | # Set working directory 5 | WORKDIR /app 6 | 7 | # Copy package files and scripts 8 | COPY package.json package-lock.json ./ 9 | COPY scripts/prepare-docker-package.js scripts/conditional-postinstall.js ./scripts/ 10 | 11 | # Generate Docker-specific package.json 12 | RUN node scripts/prepare-docker-package.js ./package.json 13 | 14 | # Stage 2: Build stage 15 | FROM node:20-alpine AS builder 16 | 17 | # Set working directory 18 | WORKDIR /app 19 | 20 | # Copy Docker-specific package.json, lock file, and scripts 21 | COPY --from=prepare /app/package.json ./ 22 | COPY --from=prepare /app/scripts ./scripts 23 | COPY package-lock.json ./ 24 | 25 | # Install dependencies 26 | RUN npm ci 27 | 28 | # Copy source code 29 | COPY . . 30 | 31 | # Build the application 32 | RUN npm run build 33 | 34 | # Stage 3: Runtime stage with Docker-in-Docker support 35 | FROM docker:dind 36 | 37 | # Install Node.js and npm 38 | RUN apk add --no-cache nodejs npm 39 | 40 | # Set working directory 41 | WORKDIR /app 42 | 43 | # Copy Docker-specific package.json and lock file 44 | COPY --from=prepare /app/package.json ./ 45 | COPY package-lock.json ./ 46 | 47 | # Copy built application from builder stage 48 | COPY --from=builder /app/.next ./.next 49 | COPY --from=builder /app/public ./public 50 | COPY --from=builder /app/next.config.ts ./ 51 | COPY --from=builder /app/server.js ./ 52 | COPY --from=builder /app/scripts ./scripts 53 | 54 | # Install production dependencies only 55 | RUN npm ci --production --omit=dev 56 | 57 | # Create directory for MCP servers 58 | RUN mkdir -p /app/mcp-servers 59 | 60 | # Create directory for persistent storage 61 | RUN mkdir -p /app/data 62 | 63 | # Expose the application port 64 | EXPOSE 4200 65 | 66 | # Create entrypoint script 67 | COPY scripts/docker-entrypoint.sh /usr/local/bin/ 68 | RUN chmod +x /usr/local/bin/docker-entrypoint.sh 69 | 70 | # Set entrypoint 71 | ENTRYPOINT ["docker-entrypoint.sh"] 72 | 73 | # Default command 74 | CMD ["npm", "start"] 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 mario-andreschak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | flujo: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | image: flujo:latest 9 | container_name: flujo 10 | privileged: true # Required for Docker-in-Docker 11 | ports: 12 | - "4200:4200" # Flujo web interface 13 | volumes: 14 | - flujo-data:/app/data # Persistent storage 15 | restart: unless-stopped 16 | environment: 17 | - NODE_ENV=production 18 | 19 | volumes: 20 | flujo-data: # Named volume for persistent storage 21 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Flujo Documentation 2 | 3 | Welcome to the Flujo documentation. This repository contains comprehensive documentation for Flujo, an AI-powered workflow automation platform. 4 | 5 | ## Documentation Structure 6 | 7 | The documentation is organized into the following sections: 8 | 9 | - **[Getting Started](./getting-started/README.md)**: Quick start guides and installation instructions 10 | - **[Features](./features/README.md)**: Detailed documentation for Flujo's features 11 | - **[Architecture](./architecture/README.md)**: Technical architecture and design documentation 12 | - **[Contributing](./contributing/README.md)**: Guidelines for contributing to Flujo 13 | - **[API Reference](./api-reference/README.md)**: API documentation for developers 14 | 15 | ## About Flujo 16 | 17 | Flujo is an AI-powered workflow automation platform that helps you create, manage, and execute complex workflows with ease. It provides a visual interface for designing workflows and integrates with various AI models and external services through the Model Context Protocol (MCP). 18 | 19 | ## Contributing to Documentation 20 | 21 | We welcome contributions to the Flujo documentation. Please see the [Documentation Guidelines](./contributing/documentation-guidelines.md) for more information on how to contribute. 22 | -------------------------------------------------------------------------------- /docs/api-reference/README.md: -------------------------------------------------------------------------------- 1 | # API Reference 2 | 3 | This section provides API documentation for developers working with Flujo. 4 | 5 | ## REST API 6 | 7 | - **Authentication**: API authentication methods 8 | - **Endpoints**: Available API endpoints 9 | - **Request/Response Formats**: API request and response formats 10 | - **Error Handling**: API error handling 11 | 12 | ## MCP API 13 | 14 | - **MCP Protocol**: Model Context Protocol specification 15 | - **Server Implementation**: Implementing MCP servers 16 | - **Client Implementation**: Implementing MCP clients 17 | - **Tool Development**: Developing tools for MCP servers 18 | 19 | ## JavaScript API 20 | 21 | - **Client SDK**: JavaScript client SDK 22 | - **Flow API**: Flow creation and management API 23 | - **Model API**: Model integration API 24 | -------------------------------------------------------------------------------- /docs/architecture/README.md: -------------------------------------------------------------------------------- 1 | # Flujo Architecture 2 | 3 | This section provides technical architecture and design documentation for Flujo. 4 | 5 | ## System Architecture 6 | 7 | - **Overview**: High-level system architecture 8 | - **Components**: Major system components and their interactions 9 | - **Data Flow**: How data flows through the system 10 | 11 | ## Backend Architecture 12 | 13 | - **Server Architecture**: Backend server architecture 14 | - **Database Design**: Database schema and design 15 | - **API Design**: API architecture and design 16 | 17 | ## Frontend Architecture 18 | 19 | - **Component Structure**: Frontend component structure 20 | - **State Management**: State management approach 21 | - **UI/UX Design**: UI/UX design principles 22 | 23 | ## Integration Architecture 24 | 25 | - **Model Integration**: How Flujo integrates with AI models 26 | - **MCP Integration**: Model Context Protocol integration 27 | - **External Service Integration**: Integration with external services 28 | -------------------------------------------------------------------------------- /docs/contributing/README.md: -------------------------------------------------------------------------------- 1 | # Contributing to Flujo 2 | 3 | This section provides guidelines for contributing to the Flujo project. 4 | 5 | ## Documentation Guidelines 6 | 7 | - **[Documentation Guidelines](./documentation-guidelines.md)**: Guidelines for contributing to documentation 8 | 9 | ## Code Contribution Guidelines 10 | 11 | - **Code Style**: Guidelines for code style and formatting 12 | - **Pull Request Process**: How to submit pull requests 13 | - **Issue Reporting**: How to report issues 14 | 15 | ## Development Setup 16 | 17 | - **Local Development Environment**: Setting up a local development environment 18 | - **Testing**: Running and writing tests 19 | -------------------------------------------------------------------------------- /docs/features/README.md: -------------------------------------------------------------------------------- 1 | # Flujo Features 2 | 3 | This section provides detailed documentation for Flujo's features. 4 | 5 | ## Model Context Protocol (MCP) 6 | 7 | - **[Overview](./mcp/overview.md)**: Introduction to the Model Context Protocol 8 | - **[Docker Servers](./mcp/docker-servers.md)**: Using Docker-based MCP servers 9 | - **[Local Servers](./mcp/local-servers.md)**: Running local MCP servers 10 | - **[GitHub Servers](./mcp/github-servers.md)**: Using GitHub MCP servers 11 | 12 | ## Flows 13 | 14 | - **[Creating Flows](./flows/creating-flows.md)**: How to create and design flows 15 | - **[Running Flows](./flows/running-flows.md)**: How to run and monitor flows 16 | - **[Flow Templates](./flows/templates.md)**: Using and creating flow templates 17 | 18 | ## Models 19 | 20 | - **[Connecting Models](./models/connecting.md)**: How to connect to AI models 21 | - **[Model Settings](./models/settings.md)**: Configuring model settings 22 | -------------------------------------------------------------------------------- /docs/getting-started/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Flujo 2 | 3 | This section provides quick start guides and installation instructions for Flujo. 4 | 5 | ## Installation 6 | 7 | - **System Requirements**: Hardware and software requirements 8 | - **Installation Guide**: Step-by-step installation instructions 9 | - **Docker Installation**: Running Flujo in Docker 10 | 11 | ## Quick Start 12 | 13 | - **First Steps**: Your first steps with Flujo 14 | - **Creating Your First Flow**: How to create your first flow 15 | - **Connecting to Models**: How to connect to AI models 16 | 17 | ## Configuration 18 | 19 | - **Basic Configuration**: Basic configuration options 20 | - **Advanced Configuration**: Advanced configuration options 21 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | import importPlugin from "eslint-plugin-import"; 5 | // eslint-import-resolver-typescript doesn't have a default export 6 | 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = dirname(__filename); 9 | 10 | const compat = new FlatCompat({ 11 | baseDirectory: __dirname, 12 | }); 13 | 14 | const eslintConfig = [ 15 | { ignores: ['mcp-servers/**/*'] }, 16 | // Import plugin configuration 17 | { 18 | plugins: { 19 | import: importPlugin 20 | }, 21 | rules: { 22 | // Enable import checking rules 23 | "import/no-unresolved": "error", 24 | "import/named": "error", 25 | "import/default": "error", 26 | "import/namespace": "error", 27 | "import/export": "error" 28 | }, 29 | settings: { 30 | "import/parsers": { 31 | "@typescript-eslint/parser": [".ts", ".tsx"] 32 | }, 33 | "import/resolver": { 34 | typescript: { 35 | alwaysTryTypes: true, 36 | project: "./tsconfig.json" 37 | }, 38 | node: { 39 | extensions: [".js", ".jsx", ".ts", ".tsx"] 40 | } 41 | } 42 | } 43 | }, 44 | ...compat.config({ 45 | extends: ['next/core-web-vitals', 'next/typescript'], 46 | parser: '@typescript-eslint/parser', 47 | parserOptions: { 48 | project: './tsconfig.json', 49 | tsconfigRootDir: __dirname, 50 | ecmaVersion: 2022, 51 | sourceType: 'module', 52 | ecmaFeatures: { 53 | jsx: true 54 | } 55 | }, 56 | rules: { 57 | // Disable TypeScript-specific rules that are causing many errors 58 | "@typescript-eslint/no-unused-vars": "off", 59 | "@typescript-eslint/no-explicit-any": "off", // Changed from error to warning 60 | "@typescript-eslint/ban-ts-comment": "off", // Changed from error to warning 61 | 62 | // React hooks rules that are causing warnings 63 | "react-hooks/exhaustive-deps": "off", 64 | 65 | // Other rules 66 | "react/no-unescaped-entities": "off", 67 | "prefer-const": "warn" 68 | } 69 | }) 70 | ]; 71 | 72 | export default eslintConfig; 73 | -------------------------------------------------------------------------------- /githubpages/img/api-keys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mario-andreschak/FLUJO/1e6a2d6a77f012a15162419eeb7f9d07fa95d060/githubpages/img/api-keys.png -------------------------------------------------------------------------------- /githubpages/img/cline-integration1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mario-andreschak/FLUJO/1e6a2d6a77f012a15162419eeb7f9d07fa95d060/githubpages/img/cline-integration1.png -------------------------------------------------------------------------------- /githubpages/img/cline-integration2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mario-andreschak/FLUJO/1e6a2d6a77f012a15162419eeb7f9d07fa95d060/githubpages/img/cline-integration2.png -------------------------------------------------------------------------------- /githubpages/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mario-andreschak/FLUJO/1e6a2d6a77f012a15162419eeb7f9d07fa95d060/githubpages/img/favicon.png -------------------------------------------------------------------------------- /githubpages/img/flows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mario-andreschak/FLUJO/1e6a2d6a77f012a15162419eeb7f9d07fa95d060/githubpages/img/flows.png -------------------------------------------------------------------------------- /githubpages/img/integration-roo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mario-andreschak/FLUJO/1e6a2d6a77f012a15162419eeb7f9d07fa95d060/githubpages/img/integration-roo.png -------------------------------------------------------------------------------- /githubpages/img/logo_grok_i6tPUEYtog0qXnrt-generated_image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mario-andreschak/FLUJO/1e6a2d6a77f012a15162419eeb7f9d07fa95d060/githubpages/img/logo_grok_i6tPUEYtog0qXnrt-generated_image.jpg -------------------------------------------------------------------------------- /githubpages/img/mcp-manager-inspector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mario-andreschak/FLUJO/1e6a2d6a77f012a15162419eeb7f9d07fa95d060/githubpages/img/mcp-manager-inspector.png -------------------------------------------------------------------------------- /githubpages/img/mit-license.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mario-andreschak/FLUJO/1e6a2d6a77f012a15162419eeb7f9d07fa95d060/githubpages/img/mit-license.png -------------------------------------------------------------------------------- /githubpages/img/ollama-model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mario-andreschak/FLUJO/1e6a2d6a77f012a15162419eeb7f9d07fa95d060/githubpages/img/ollama-model.png -------------------------------------------------------------------------------- /githubpages/img/processnode-prompt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mario-andreschak/FLUJO/1e6a2d6a77f012a15162419eeb7f9d07fa95d060/githubpages/img/processnode-prompt.png -------------------------------------------------------------------------------- /githubpages/img/server-env.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mario-andreschak/FLUJO/1e6a2d6a77f012a15162419eeb7f9d07fa95d060/githubpages/img/server-env.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | import path from 'path'; 3 | 4 | const nextConfig: NextConfig = { 5 | /* config options here */ 6 | typescript: { 7 | // Ignore all TypeScript errors during build 8 | ignoreBuildErrors: true, 9 | }, 10 | eslint: { 11 | // Ignore all ESLint errors during build 12 | ignoreDuringBuilds: true, 13 | }, 14 | transpilePackages: [ 15 | '@mui/material', 16 | '@mui/icons-material', 17 | '@mui/system', 18 | '@mui/utils', 19 | '@emotion/react', 20 | '@emotion/styled' 21 | ], 22 | // Increase the webpack chunk loading timeout and configure other performance settings 23 | webpack: (config, { dev, isServer }) => { 24 | // Only apply these settings in development mode 25 | if (dev && !isServer) { 26 | // Increase chunk loading timeout to 60 seconds (60000ms) 27 | config.output = { 28 | ...config.output, 29 | chunkLoadTimeout: 60000, 30 | }; 31 | 32 | // Optimize for development performance 33 | config.optimization = { 34 | ...config.optimization, 35 | runtimeChunk: 'single', 36 | splitChunks: { 37 | chunks: 'all', 38 | cacheGroups: { 39 | vendors: { 40 | test: /[\\/]node_modules[\\/](?!.*\.css$)/, // Exclude CSS files from vendors chunk 41 | name: 'vendors', 42 | priority: -10, 43 | reuseExistingChunk: true, 44 | }, 45 | styles: { 46 | name: 'styles', 47 | test: /\.css$/, 48 | chunks: 'all', 49 | enforce: true, 50 | priority: 20, 51 | }, 52 | }, 53 | }, 54 | }; 55 | 56 | // Configure watchOptions for better file watching 57 | config.watchOptions = { 58 | ...config.watchOptions, 59 | poll: 1000, // Check for changes every second 60 | aggregateTimeout: 300, // Delay before rebuilding 61 | }; 62 | } 63 | 64 | // Exclude node binary files from being processed by webpack 65 | config.externals = [...(config.externals || []), 66 | { 67 | sharp: 'commonjs sharp', 68 | 'node-gyp-build': 'commonjs node-gyp-build' 69 | } 70 | ]; 71 | 72 | // Handle binary modules properly 73 | config.module = { 74 | ...config.module, 75 | rules: [ 76 | ...(config.module?.rules || []), 77 | { 78 | test: /\.node$/, 79 | use: 'node-loader', 80 | }, 81 | ], 82 | }; 83 | 84 | // Enable WebAssembly 85 | config.experiments = { 86 | ...config.experiments, 87 | asyncWebAssembly: true, 88 | }; 89 | 90 | return config; 91 | }, 92 | }; 93 | 94 | export default nextConfig; 95 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flujo", 3 | "version": "0.1.4", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev -p 4200", 7 | "turbo": "next dev --turbopack -p 4200", 8 | "build": "next build", 9 | "start": "next start -p 4200", 10 | "lint": "next lint", 11 | "postinstall": "node scripts/conditional-postinstall.js" 12 | }, 13 | "dependencies": { 14 | "@emotion/react": "^11.14.0", 15 | "@emotion/styled": "^11.14.0", 16 | "@isomorphic-git/lightning-fs": "^4.6.0", 17 | "@modelcontextprotocol/sdk": "^1.6.0", 18 | "@mui/icons-material": "^6.4.5", 19 | "@mui/material": "^6.4.5", 20 | "@types/crypto-js": "^4.2.2", 21 | "@types/uuid": "^10.0.0", 22 | "@uiw/react-md-editor": "^4.0.5", 23 | "@xenova/transformers": "^2.17.2", 24 | "@xyflow/react": "^12.4.4", 25 | "axios": "^1.8.4", 26 | "crypto-js": "^4.2.0", 27 | "isomorphic-git": "^1.29.0", 28 | "jest": "^29.7.0", 29 | "jszip": "^3.10.1", 30 | "lodash": "^4.17.21", 31 | "mcp-remote": "^0.0.22", 32 | "next": "^15.3.1", 33 | "node-fetch": "^3.3.2", 34 | "openai": "^4.86.2", 35 | "react": "^19.0.0", 36 | "react-dom": "^19.0.0", 37 | "react-markdown": "^10.1.0", 38 | "remark-gfm": "^4.0.1", 39 | "simple-git": "^3.27.0", 40 | "slate": "^0.112.0", 41 | "slate-history": "^0.110.3", 42 | "slate-react": "^0.112.1", 43 | "uuid": "^11.1.0" 44 | }, 45 | "devDependencies": { 46 | "@eslint/eslintrc": "^3", 47 | "@types/lodash": "^4.17.16", 48 | "@types/node": "20.17.30", 49 | "@types/react": "19.1.0", 50 | "@types/react-dom": "^19", 51 | "@typescript-eslint/parser": "^8.26.1", 52 | "buffer": "^6.0.3", 53 | "concurrently": "^9.1.2", 54 | "eslint": "^9", 55 | "eslint-config-next": "15.1.7", 56 | "eslint-import-resolver-typescript": "^3.8.4", 57 | "eslint-plugin-import": "^2.31.0", 58 | "node-loader": "^2.1.0", 59 | "postcss": "^8", 60 | "postcss-flexbugs-fixes": "^5.0.2", 61 | "postcss-preset-env": "^10.1.5", 62 | "process": "^0.11.10", 63 | "typescript": "5.8.3", 64 | "wait-on": "^8.0.2", 65 | "webpack": "^5.98.0" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | 'postcss-flexbugs-fixes': {}, 5 | 'postcss-preset-env': { 6 | autoprefixer: { 7 | flexbox: 'no-2009', 8 | }, 9 | stage: 3, 10 | features: { 11 | 'custom-properties': false, 12 | }, 13 | }, 14 | }, 15 | }; 16 | 17 | export default config; 18 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mario-andreschak/FLUJO/1e6a2d6a77f012a15162419eeb7f9d07fa95d060/public/favicon.ico -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/build-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Force immediate output 5 | exec 1>&1 6 | 7 | # This script provides a lightweight Docker build pipeline 8 | # that stores logs rather than printing them directly. 9 | 10 | # Parse command line arguments 11 | VERBOSE=false 12 | TAG="latest" 13 | while [[ "$#" -gt 0 ]]; do 14 | case $1 in 15 | --verbose) VERBOSE=true ;; 16 | --tag=*) TAG="${1#*=}" ;; 17 | *) echo "Unknown parameter: $1"; exit 1 ;; 18 | esac 19 | shift 20 | done 21 | 22 | # Create temp directory for logs if it doesn't exist 23 | TEMP_DIR="/tmp/flujo-docker" 24 | mkdir -p "$TEMP_DIR" 25 | 26 | # Setup colored output 27 | GREEN='\033[0;32m' 28 | RED='\033[0;31m' 29 | YELLOW='\033[1;33m' 30 | NC='\033[0m' # No Color 31 | CHECK_MARK="✓" 32 | X_MARK="✗" 33 | WARNING_MARK="⚠" 34 | 35 | # Function to check log file size and show warning if needed 36 | check_log_size() { 37 | local log_file=$1 38 | if [ -f "$log_file" ]; then 39 | local line_count=$(wc -l < "$log_file") 40 | if [ $line_count -gt 100 ]; then 41 | echo -e "${YELLOW}${WARNING_MARK} Large log file detected ($line_count lines)${NC}" 42 | echo " Tips for viewing large logs:" 43 | echo " • head -n 20 $log_file (view first 20 lines)" 44 | echo " • tail -n 20 $log_file (view last 20 lines)" 45 | echo " • less $log_file (scroll through file)" 46 | echo " • grep 'error' $log_file (search for specific terms)" 47 | fi 48 | fi 49 | } 50 | 51 | # Build Docker image 52 | echo "→ Preparing Docker-specific package.json..." 53 | echo "→ Building Docker image (tag: flujo:$TAG)..." 54 | if [ "$VERBOSE" = true ]; then 55 | if docker build -t flujo:$TAG .; then 56 | echo -e "${GREEN}${CHECK_MARK} Docker build successful${NC}" 57 | else 58 | echo -e "${RED}${X_MARK} Docker build failed${NC}" 59 | exit 1 60 | fi 61 | else 62 | DOCKER_LOG="$TEMP_DIR/docker-build.log" 63 | if docker build -t flujo:$TAG . > "$DOCKER_LOG" 2>&1; then 64 | echo -e "${GREEN}${CHECK_MARK} Docker build successful${NC} (log: $DOCKER_LOG)" 65 | check_log_size "$DOCKER_LOG" 66 | else 67 | echo -e "${RED}${X_MARK} Docker build failed${NC} (see details in $DOCKER_LOG)" 68 | check_log_size "$DOCKER_LOG" 69 | exit 1 70 | fi 71 | fi 72 | 73 | echo -e "\n${GREEN}Build complete!${NC} Image tagged as flujo:$TAG" 74 | -------------------------------------------------------------------------------- /scripts/conditional-postinstall.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * This script detects if we're running in a Docker environment. 5 | */ 6 | 7 | // Check if we're in a Docker environment 8 | const isDocker = () => { 9 | try { 10 | const fs = require('fs'); 11 | 12 | // Method 1: Check for .dockerenv file 13 | if (fs.existsSync('/.dockerenv')) { 14 | return true; 15 | } 16 | 17 | // Method 2: Check for container env var 18 | if (process.env.container === 'docker') { 19 | return true; 20 | } 21 | 22 | // Method 3: Check overlay filesystem in /proc/mounts 23 | try { 24 | const mounts = fs.readFileSync('/proc/mounts', 'utf8'); 25 | if (mounts.includes('overlay / overlay')) { 26 | return true; 27 | } 28 | } catch (err) { 29 | // Ignore if can't read /proc/mounts 30 | } 31 | 32 | // Method 4: Check cgroup v1 and v2 33 | try { 34 | const contents = fs.readFileSync('/proc/self/cgroup', 'utf8'); 35 | return contents.includes('docker') || contents.includes('0::/') || contents.includes('/docker/'); 36 | } catch (err) { 37 | // Ignore if can't read cgroups 38 | return false; 39 | } 40 | } catch (err) { 41 | return false; 42 | } 43 | }; 44 | 45 | // Check and log Docker environment status 46 | const inDocker = isDocker(); 47 | console.log(`Running in Docker environment: ${inDocker ? 'Yes' : 'No'}`); 48 | 49 | // This script previously ran Electron-related tasks, which have been removed 50 | -------------------------------------------------------------------------------- /scripts/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | # Start the Docker daemon if not already running 5 | if ! docker info > /dev/null 2>&1; then 6 | echo "Starting Docker daemon..." 7 | dockerd --host=unix:///var/run/docker.sock & 8 | 9 | # Wait for Docker daemon to start 10 | echo "Waiting for Docker daemon to start..." 11 | until docker info > /dev/null 2>&1; do 12 | sleep 1 13 | done 14 | echo "Docker daemon started" 15 | fi 16 | 17 | # Execute the command passed to the container 18 | exec "$@" 19 | -------------------------------------------------------------------------------- /scripts/prepare-docker-package.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * This script prepares the package.json for Docker builds 5 | */ 6 | 7 | const fs = require('fs'); 8 | const path = require('path'); 9 | 10 | // Read the original package.json 11 | const packageJsonPath = path.join(process.cwd(), 'package.json'); 12 | const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); 13 | 14 | // Create a Docker-specific version 15 | const dockerPackageJson = { ...packageJson }; 16 | 17 | // Keep all scripts except any that might be added in the future 18 | dockerPackageJson.scripts = { ...packageJson.scripts }; 19 | 20 | // Set main to server.js 21 | dockerPackageJson.main = 'server.js'; 22 | 23 | // Add Docker-specific metadata 24 | dockerPackageJson.name = `${packageJson.name}-docker`; 25 | dockerPackageJson.description = `${packageJson.description || 'Flujo'} (Docker version)`; 26 | 27 | // Write the Docker-specific package.json 28 | const outputPath = process.argv[2] || path.join(process.cwd(), 'package.docker.json'); 29 | fs.writeFileSync(outputPath, JSON.stringify(dockerPackageJson, null, 2)); 30 | 31 | console.log(`Docker-specific package.json written to ${outputPath}`); 32 | -------------------------------------------------------------------------------- /scripts/run-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # This script runs the Flujo Docker container with appropriate volume mounts 5 | # and port mappings. 6 | 7 | # Parse command line arguments 8 | TAG="latest" 9 | DETACHED=false 10 | PRIVILEGED=true 11 | PORT=4200 12 | 13 | while [[ "$#" -gt 0 ]]; do 14 | case $1 in 15 | --tag=*) TAG="${1#*=}" ;; 16 | --detached) DETACHED=true ;; 17 | --no-privileged) PRIVILEGED=false ;; 18 | --port=*) PORT="${1#*=}" ;; 19 | *) echo "Unknown parameter: $1"; exit 1 ;; 20 | esac 21 | shift 22 | done 23 | 24 | # Setup colored output 25 | GREEN='\033[0;32m' 26 | RED='\033[0;31m' 27 | YELLOW='\033[1;33m' 28 | NC='\033[0m' # No Color 29 | INFO_MARK="ℹ" 30 | 31 | # Create data directory if it doesn't exist 32 | DATA_DIR="$HOME/.flujo" 33 | mkdir -p "$DATA_DIR" 34 | echo -e "${GREEN}${INFO_MARK} Using data directory: $DATA_DIR${NC}" 35 | 36 | # Build the Docker run command 37 | DOCKER_CMD="docker run" 38 | 39 | # Add detached mode if requested 40 | if [ "$DETACHED" = true ]; then 41 | DOCKER_CMD="$DOCKER_CMD -d" 42 | echo -e "${GREEN}${INFO_MARK} Running in detached mode${NC}" 43 | else 44 | DOCKER_CMD="$DOCKER_CMD -it" 45 | fi 46 | 47 | # Add privileged mode if requested 48 | if [ "$PRIVILEGED" = true ]; then 49 | DOCKER_CMD="$DOCKER_CMD --privileged" 50 | echo -e "${YELLOW}${INFO_MARK} Running in privileged mode (required for Docker-in-Docker)${NC}" 51 | fi 52 | 53 | # Add port mapping 54 | DOCKER_CMD="$DOCKER_CMD -p $PORT:4200" 55 | echo -e "${GREEN}${INFO_MARK} Mapping port $PORT to container port 4200${NC}" 56 | 57 | # Add volume mounts 58 | DOCKER_CMD="$DOCKER_CMD -v $DATA_DIR:/app/data" 59 | echo -e "${GREEN}${INFO_MARK} Mounting $DATA_DIR to /app/data${NC}" 60 | 61 | # Add container name 62 | DOCKER_CMD="$DOCKER_CMD --name flujo" 63 | 64 | # Add image name and tag 65 | DOCKER_CMD="$DOCKER_CMD flujo:$TAG" 66 | 67 | # Run the container 68 | echo -e "${GREEN}${INFO_MARK} Starting Flujo container...${NC}" 69 | echo "Command: $DOCKER_CMD" 70 | eval $DOCKER_CMD 71 | 72 | # If running in detached mode, show how to view logs 73 | if [ "$DETACHED" = true ]; then 74 | echo -e "\n${GREEN}Container started in detached mode.${NC}" 75 | echo "To view logs: docker logs -f flujo" 76 | echo "To stop container: docker stop flujo" 77 | fi 78 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const { createServer } = require('http'); 2 | const { parse } = require('url'); 3 | const next = require('next'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | 7 | // Default port 8 | const port = parseInt(process.env.PORT || '4200', 10); 9 | 10 | // Determine if we should bind to all interfaces or just localhost 11 | let networkMode = false; 12 | 13 | // Override with environment variable if set 14 | if (process.env.FLUJO_NETWORK_MODE === '1' || process.env.FLUJO_NETWORK_MODE === 'true') { 15 | networkMode = true; 16 | } 17 | 18 | // Host to bind to 19 | const hostname = networkMode ? '0.0.0.0' : 'localhost'; 20 | 21 | // Prepare the Next.js app 22 | const app = next({ 23 | dev: process.env.NODE_ENV !== 'production', 24 | hostname, 25 | port, 26 | }); 27 | 28 | const handle = app.getRequestHandler(); 29 | 30 | app.prepare().then(() => { 31 | createServer((req, res) => { 32 | const parsedUrl = parse(req.url, true); 33 | handle(req, res, parsedUrl); 34 | }).listen(port, hostname, (err) => { 35 | if (err) throw err; 36 | 37 | const addressInfo = networkMode ? 38 | `all interfaces (${hostname}) on port ${port}` : 39 | `${hostname}:${port}`; 40 | 41 | console.log(`> Ready on ${addressInfo}`); 42 | 43 | if (networkMode) { 44 | // Log the actual IP addresses for network access 45 | const { networkInterfaces } = require('os'); 46 | const nets = networkInterfaces(); 47 | 48 | console.log('> Available on:'); 49 | 50 | for (const name of Object.keys(nets)) { 51 | for (const net of nets[name]) { 52 | // Skip internal and non-IPv4 addresses 53 | if (net.family === 'IPv4' && !net.internal) { 54 | console.log(`> http://${net.address}:${port}`); 55 | } 56 | } 57 | } 58 | } 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /src/app/___favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mario-andreschak/FLUJO/1e6a2d6a77f012a15162419eeb7f9d07fa95d060/src/app/___favicon.ico -------------------------------------------------------------------------------- /src/app/_favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mario-andreschak/FLUJO/1e6a2d6a77f012a15162419eeb7f9d07fa95d060/src/app/_favicon.ico -------------------------------------------------------------------------------- /src/app/api/cwd/README.md: -------------------------------------------------------------------------------- 1 | # Current Working Directory API 2 | 3 | This directory contains the API endpoint for retrieving the current working directory and related path information. 4 | 5 | ## Architecture 6 | 7 | The Current Working Directory API follows a simple architecture: 8 | 9 | ``` 10 | ┌─────────────────┐ ┌─────────────────┐ 11 | │ │ │ │ 12 | │ Frontend │◄───►│ API Layer │ 13 | │ Components │ │ (route.ts) │ 14 | └─────────────────┘ └─────────────────┘ 15 | ``` 16 | 17 | ## Components 18 | 19 | ### API Handler 20 | 21 | - `route.ts`: Handles HTTP GET requests to retrieve the current working directory and MCP servers directory path 22 | 23 | ## Flow of Control 24 | 25 | 1. Frontend components make a GET request to the API endpoint 26 | 2. API handler retrieves the current working directory using `process.cwd()` 27 | 3. API handler calculates the MCP servers directory path 28 | 4. Results are returned to the frontend 29 | 30 | ## API Endpoints 31 | 32 | ### GET /api/cwd 33 | 34 | Returns the current working directory and MCP servers directory path. 35 | 36 | #### Response 37 | 38 | ```json 39 | { 40 | "success": true, 41 | "cwd": "/path/to/current/working/directory", 42 | "mcpServersDir": "/path/to/current/working/directory/mcp-servers" 43 | } 44 | ``` 45 | 46 | #### Error Response 47 | 48 | ```json 49 | { 50 | "success": false, 51 | "error": "Error message" 52 | } 53 | ``` 54 | 55 | ## Usage Examples 56 | 57 | ### Frontend Usage 58 | 59 | ```typescript 60 | // Get the current working directory 61 | const response = await fetch('/api/cwd'); 62 | const data = await response.json(); 63 | 64 | if (data.success) { 65 | // Access the current working directory 66 | const cwd = data.cwd; 67 | 68 | // Access the MCP servers directory 69 | const mcpServersDir = data.mcpServersDir; 70 | 71 | console.log(`Current working directory: ${cwd}`); 72 | console.log(`MCP servers directory: ${mcpServersDir}`); 73 | } else { 74 | console.error(`Error: ${data.error}`); 75 | } 76 | ``` 77 | 78 | ## Error Handling 79 | 80 | The API returns appropriate HTTP status codes and error messages: 81 | 82 | - `200 OK`: Successful response with directory information 83 | - `500 Internal Server Error`: Server-side errors 84 | 85 | Error responses include a descriptive message and a `success: false` flag: 86 | 87 | ```json 88 | { 89 | "success": false, 90 | "error": "Failed to get current working directory: Error message" 91 | } 92 | ``` 93 | 94 | ## Security Considerations 95 | 96 | This API only provides read-only access to directory paths and does not expose any sensitive information. It is used primarily for internal application functionality to determine the correct paths for MCP server operations. 97 | -------------------------------------------------------------------------------- /src/app/api/cwd/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import path from 'path'; 3 | import { createLogger } from '@/utils/logger'; 4 | // eslint-disable-next-line import/named 5 | import { v4 as uuidv4 } from 'uuid'; 6 | 7 | const log = createLogger('app/api/cwd/route'); 8 | 9 | export async function GET() { 10 | const requestId = uuidv4(); 11 | log.info(`Handling GET request [RequestID: ${requestId}]`); 12 | 13 | try { 14 | // Get the current working directory 15 | const cwd = process.cwd(); 16 | log.debug(`Retrieved current working directory [${requestId}]`, cwd); 17 | 18 | // Get the mcp-servers directory path 19 | const mcpServersDir = path.join(cwd, 'mcp-servers'); 20 | log.debug(`Generated mcp-servers directory path [${requestId}]`, mcpServersDir); 21 | 22 | log.info(`Returning successful response [${requestId}]`); 23 | return NextResponse.json({ 24 | success: true, 25 | cwd, 26 | mcpServersDir 27 | }); 28 | } catch (error) { 29 | log.error(`Error getting current working directory [${requestId}]`, error); 30 | return NextResponse.json({ 31 | success: false, 32 | error: `Failed to get current working directory: ${error instanceof Error ? error.message : 'Unknown error'}` 33 | }, { status: 500 }); 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/app/api/flow/prompt-renderer/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { promptRenderer } from '@/backend/utils/PromptRenderer'; 3 | import { createLogger } from '@/utils/logger'; 4 | 5 | const log = createLogger('api/flow/prompt-renderer'); 6 | 7 | /** 8 | * API endpoint to render a prompt for a node in a flow 9 | * 10 | * @param request - The request object 11 | * @returns The rendered prompt 12 | */ 13 | export async function POST(request: NextRequest): Promise { 14 | try { 15 | // Parse the request body 16 | const body = await request.json(); 17 | const { flowId, nodeId, options } = body; 18 | 19 | log.debug('Received prompt render request', { flowId, nodeId, options }); 20 | 21 | // Validate required parameters 22 | if (!flowId || !nodeId) { 23 | return NextResponse.json( 24 | { error: 'Missing required parameters: flowId and nodeId are required' }, 25 | { status: 400 } 26 | ); 27 | } 28 | 29 | log.info(`Rendering prompt for node ${nodeId} in flow ${flowId}`, { options }); 30 | 31 | // Render the prompt 32 | const prompt = await promptRenderer.renderPrompt(flowId, nodeId, options); 33 | 34 | // Return the rendered prompt 35 | return NextResponse.json({ prompt }); 36 | } catch (error) { 37 | log.error('Error rendering prompt:', error); 38 | return NextResponse.json( 39 | { error: `Failed to render prompt: ${error instanceof Error ? error.message : 'Unknown error'}` }, 40 | { status: 500 } 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/app/api/flow/route.ts: -------------------------------------------------------------------------------- 1 | import { createLogger } from '@/utils/logger'; 2 | import { flowService } from '@/backend/services/flow'; 3 | 4 | // Create a logger instance for this file 5 | const log = createLogger('app/api/flow/route'); 6 | 7 | // Export the handlers 8 | export { GET, POST } from './handlers'; 9 | 10 | // Export the service instance for backward compatibility 11 | export { flowService }; 12 | -------------------------------------------------------------------------------- /src/app/api/init/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { verifyStorage } from '@/utils/storage/backend'; 3 | import { createLogger } from '@/utils/logger'; 4 | // eslint-disable-next-line import/named 5 | import { v4 as uuidv4 } from 'uuid'; 6 | import { mcpService } from '@/backend/services/mcp'; 7 | 8 | const log = createLogger('app/api/init/route'); 9 | 10 | /** 11 | * API route for application initialization 12 | * This runs server-side initialization tasks 13 | */ 14 | export async function GET(req: NextRequest) { 15 | const requestId = uuidv4(); 16 | log.info(`Handling initialization request [RequestID: ${requestId}]`); 17 | 18 | try { 19 | // Verify storage system 20 | await verifyStorage(); 21 | 22 | // Start all enabled MCP servers 23 | log.info('Initializing MCP servers'); 24 | await mcpService.startEnabledServers().catch(error => { 25 | log.error('Failed to start enabled servers:', error); 26 | // Make sure the flag is reset even if there's an unhandled error 27 | if (mcpService.isStartingUp()) { 28 | log.warn('Resetting startup flag after error'); 29 | (mcpService as any).setStartingUp(false); 30 | } 31 | }); 32 | 33 | return NextResponse.json({ 34 | success: true, 35 | message: 'Application initialized successfully' 36 | }); 37 | } catch (error) { 38 | log.error(`Initialization failed [${requestId}]:`, error); 39 | return NextResponse.json({ 40 | success: false, 41 | error: `Initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}` 42 | }, { status: 500 }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/app/api/mcp/cancel/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { mcpService } from '@/backend/services/mcp'; 3 | import { createLogger } from '@/utils/logger'; 4 | import { cancelToolExecution } from '@/app/api/mcp/tools'; 5 | 6 | const log = createLogger('app/api/mcp/cancel/route'); 7 | 8 | /** 9 | * API endpoint to cancel a tool execution in progress 10 | */ 11 | export async function POST(request: NextRequest) { 12 | const searchParams = request.nextUrl.searchParams; 13 | const token = searchParams.get('token'); 14 | const serverName = searchParams.get('serverName'); 15 | 16 | if (!serverName) { 17 | log.error('Missing serverName parameter'); 18 | return NextResponse.json({ error: 'Missing serverName parameter' }, { status: 400 }); 19 | } 20 | 21 | try { 22 | // Get the client for this server 23 | const client = mcpService.getClient(serverName); 24 | if (!client) { 25 | log.error(`Server "${serverName}" not found or not connected`); 26 | return NextResponse.json({ error: `Server "${serverName}" not found or not connected` }, { status: 404 }); 27 | } 28 | 29 | // Parse the request body to get the reason 30 | const body = await request.json(); 31 | const reason = body.reason || 'User cancelled operation'; 32 | 33 | if (token) { 34 | // If we have a token, use the standard cancellation mechanism 35 | log.info(`Cancelling tool execution with token ${token} for server ${serverName}`); 36 | await cancelToolExecution(client, token, reason); 37 | } else { 38 | // If no token is provided, attempt to force-cancel by closing and reconnecting the client 39 | log.info(`Force-cancelling all operations for server ${serverName} (no token provided)`); 40 | 41 | // First, try to disconnect the server 42 | await mcpService.disconnectServer(serverName); 43 | 44 | // Then, reconnect the server using the existing configuration 45 | // This uses the public connectServer method which will load the config internally 46 | const reconnectResult = await mcpService.connectServer(serverName); 47 | 48 | if (reconnectResult.success) { 49 | log.info(`Successfully reconnected server ${serverName} after force-cancel`); 50 | } else { 51 | log.warn(`Could not reconnect server ${serverName} after force-cancel: ${reconnectResult.error}`); 52 | } 53 | } 54 | 55 | log.info(`Successfully processed cancellation request for server ${serverName}`); 56 | return NextResponse.json({ success: true }); 57 | } catch (error) { 58 | log.error(`Error cancelling tool execution: ${error instanceof Error ? error.message : 'Unknown error'}`); 59 | return NextResponse.json( 60 | { error: `Failed to cancel: ${error instanceof Error ? error.message : 'Unknown error'}` }, 61 | { status: 500 } 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/app/api/mcp/config-adapter.ts: -------------------------------------------------------------------------------- 1 | import { createLogger } from '@/utils/logger'; 2 | import { MCPServerConfig, MCPServiceResponse } from '@/shared/types/mcp'; 3 | import { mcpService } from '@/backend/services/mcp'; 4 | 5 | const log = createLogger('app/api/mcp/config-adapter'); 6 | 7 | /** 8 | * Load MCP server configurations from storage 9 | * 10 | * This adapter delegates to the backend service 11 | */ 12 | export async function loadServerConfigs(): Promise { 13 | log.debug('Delegating loadServerConfigs to backend service'); 14 | return mcpService.loadServerConfigs(); 15 | } 16 | 17 | /** 18 | * Save MCP server configurations to storage 19 | * 20 | * This adapter delegates to the backend service 21 | */ 22 | export async function saveConfig(configs: Map): Promise { 23 | log.debug('This adapter method is deprecated. Use mcpService.updateServerConfig instead'); 24 | 25 | // This is a compatibility layer that shouldn't be used directly 26 | // Instead, use mcpService.updateServerConfig for each config 27 | 28 | try { 29 | // Process each config individually 30 | for (const [name, config] of configs.entries()) { 31 | await mcpService.updateServerConfig(name, config); 32 | } 33 | 34 | return { success: true }; 35 | } catch (error) { 36 | log.warn('Failed to save configs through adapter:', error); 37 | return { 38 | success: false, 39 | error: `Failed to save config: ${error instanceof Error ? error.message : 'Unknown error'}` 40 | }; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/api/mcp/config.ts: -------------------------------------------------------------------------------- 1 | import { createLogger } from '@/utils/logger'; 2 | import { MCPServerConfig, MCPServiceResponse } from '@/shared/types/mcp'; 3 | 4 | // Import from adapter 5 | import { loadServerConfigs as loadServerConfigsAdapter, saveConfig as saveConfigAdapter } from './config-adapter'; 6 | 7 | const log = createLogger('app/api/mcp/config'); 8 | 9 | /** 10 | * Load MCP server configurations from storage 11 | * 12 | * This function delegates to the adapter 13 | */ 14 | export async function loadServerConfigs(): Promise { 15 | log.debug('Delegating to adapter'); 16 | return loadServerConfigsAdapter(); 17 | } 18 | 19 | /** 20 | * Save MCP server configurations to storage 21 | * 22 | * This function delegates to the adapter 23 | */ 24 | export async function saveConfig(configs: Map): Promise { 25 | log.debug('Delegating to adapter'); 26 | return saveConfigAdapter(configs); 27 | } 28 | -------------------------------------------------------------------------------- /src/app/api/mcp/connection-adapter.ts: -------------------------------------------------------------------------------- 1 | import { Client } from '@modelcontextprotocol/sdk/client/index.js'; 2 | import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; 3 | import { WebSocketClientTransport } from '@modelcontextprotocol/sdk/client/websocket.js'; 4 | import { createLogger } from '@/utils/logger'; 5 | import { MCPServerConfig } from '@/shared/types/mcp'; 6 | import { mcpService } from '@/backend/services/mcp'; 7 | 8 | const log = createLogger('app/api/mcp/connection-adapter'); 9 | 10 | /** 11 | * Create a new MCP client with proper capabilities 12 | * 13 | * This adapter delegates to the backend service 14 | */ 15 | export function createNewClient(config: MCPServerConfig): Client { 16 | log.debug('This adapter method is deprecated. Use mcpService directly'); 17 | 18 | // Get the client from the backend service if it exists 19 | const existingClient = mcpService.getClient(config.name); 20 | if (existingClient) { 21 | return existingClient; 22 | } 23 | 24 | // Otherwise, throw an error - clients should be created through the backend service 25 | throw new Error('Clients should be created through the backend service'); 26 | } 27 | 28 | /** 29 | * Create a transport for the MCP client 30 | * 31 | * This adapter is deprecated and should not be used directly 32 | */ 33 | export function createTransport(config: MCPServerConfig): StdioClientTransport | WebSocketClientTransport { 34 | log.debug('This adapter method is deprecated. Use mcpService directly'); 35 | throw new Error('Transports should be created through the backend service'); 36 | } 37 | 38 | /** 39 | * Check if an existing client needs to be recreated 40 | * 41 | * This adapter is deprecated and should not be used directly 42 | */ 43 | export function shouldRecreateClient( 44 | client: Client, 45 | config: MCPServerConfig 46 | ): { needsNewClient: boolean; reason?: string } { 47 | log.debug('This adapter method is deprecated. Use mcpService directly'); 48 | return { needsNewClient: false }; 49 | } 50 | 51 | /** 52 | * Safely close a client connection following the MCP shutdown sequence 53 | * 54 | * This adapter delegates to the backend service 55 | */ 56 | export async function safelyCloseClient(client: Client, serverName: string): Promise { 57 | log.debug('Delegating to mcpService.disconnectServer'); 58 | await mcpService.disconnectServer(serverName); 59 | } 60 | -------------------------------------------------------------------------------- /src/app/api/mcp/connection.ts: -------------------------------------------------------------------------------- 1 | import { createLogger } from '@/utils/logger'; 2 | import { Client } from '@modelcontextprotocol/sdk/client/index.js'; 3 | import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; 4 | import { WebSocketClientTransport } from '@modelcontextprotocol/sdk/client/websocket.js'; 5 | import { MCPServerConfig } from '@/shared/types/mcp'; 6 | 7 | // Import from adapter 8 | import { 9 | createNewClient as createNewClientAdapter, 10 | createTransport as createTransportAdapter, 11 | shouldRecreateClient as shouldRecreateClientAdapter, 12 | safelyCloseClient as safelyCloseClientAdapter 13 | } from './connection-adapter'; 14 | 15 | const log = createLogger('app/api/mcp/connection'); 16 | 17 | /** 18 | * Create a new MCP client with proper capabilities 19 | * 20 | * This function delegates to the adapter 21 | */ 22 | export function createNewClient(config: MCPServerConfig): Client { 23 | log.debug('Delegating to adapter'); 24 | return createNewClientAdapter(config); 25 | } 26 | 27 | /** 28 | * Create a transport for the MCP client 29 | * 30 | * This function delegates to the adapter 31 | */ 32 | export function createTransport(config: MCPServerConfig): StdioClientTransport | WebSocketClientTransport { 33 | log.debug('Delegating to adapter'); 34 | return createTransportAdapter(config); 35 | } 36 | 37 | /** 38 | * Check if an existing client needs to be recreated 39 | * 40 | * This function delegates to the adapter 41 | */ 42 | export function shouldRecreateClient( 43 | client: Client, 44 | config: MCPServerConfig 45 | ): { needsNewClient: boolean; reason?: string } { 46 | log.debug('Delegating to adapter'); 47 | return shouldRecreateClientAdapter(client, config); 48 | } 49 | 50 | /** 51 | * Safely close a client connection following the MCP shutdown sequence 52 | * 53 | * This function delegates to the adapter 54 | */ 55 | export async function safelyCloseClient(client: Client, serverName: string): Promise { 56 | log.debug('Delegating to adapter'); 57 | return safelyCloseClientAdapter(client, serverName); 58 | } 59 | -------------------------------------------------------------------------------- /src/app/api/mcp/route.ts: -------------------------------------------------------------------------------- 1 | import { createLogger } from '@/utils/logger'; 2 | import { mcpService } from '@/backend/services/mcp'; 3 | 4 | const log = createLogger('app/api/mcp/route'); 5 | 6 | // Re-export the handlers 7 | export { GET, POST, PUT } from './handlers'; 8 | 9 | // Re-export the service instance for backward compatibility 10 | export { mcpService }; 11 | -------------------------------------------------------------------------------- /src/app/api/mcp/tools-adapter.ts: -------------------------------------------------------------------------------- 1 | import { Client } from '@modelcontextprotocol/sdk/client/index.js'; 2 | import { NextResponse } from 'next/server'; 3 | import { createLogger } from '@/utils/logger'; 4 | import { MCPToolResponse as ToolResponse, MCPServiceResponse } from '@/shared/types/mcp'; 5 | import { mcpService } from '@/backend/services/mcp'; 6 | 7 | const log = createLogger('app/api/mcp/tools-adapter'); 8 | 9 | /** 10 | * List tools available from an MCP server 11 | * 12 | * This adapter delegates to the backend service 13 | */ 14 | export async function listServerTools(client: Client | undefined, serverName: string): Promise<{ tools: ToolResponse[], error?: string }> { 15 | log.debug('Delegating listServerTools to backend service'); 16 | return mcpService.listServerTools(serverName); 17 | } 18 | 19 | /** 20 | * Call a tool on an MCP server with support for progress tracking 21 | * 22 | * This adapter delegates to the backend service and converts the response to NextResponse 23 | */ 24 | export async function callTool( 25 | client: Client | undefined, 26 | serverName: string, 27 | toolName: string, 28 | args: Record, 29 | timeout?: number 30 | ): Promise { 31 | log.debug('Delegating callTool to backend service'); 32 | 33 | const result = await mcpService.callTool(serverName, toolName, args, timeout); 34 | 35 | // Convert MCPServiceResponse to NextResponse 36 | return NextResponse.json( 37 | result, 38 | { status: result.statusCode || (result.success ? 200 : 500) } 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/app/api/mcp/tools.ts: -------------------------------------------------------------------------------- 1 | import { createLogger } from '@/utils/logger'; 2 | import { Client } from '@modelcontextprotocol/sdk/client/index.js'; 3 | import { NextResponse } from 'next/server'; 4 | import { MCPToolResponse as ToolResponse } from '@/shared/types/mcp'; 5 | 6 | // Import from adapter 7 | import { listServerTools as listToolsAdapter, callTool as callToolAdapter } from './tools-adapter'; 8 | 9 | const log = createLogger('app/api/mcp/tools'); 10 | 11 | /** 12 | * List tools available from an MCP server 13 | * 14 | * This function delegates to the adapter 15 | */ 16 | export async function listServerTools(client: Client | undefined, serverName: string): Promise<{ tools: ToolResponse[], error?: string }> { 17 | log.debug('Delegating to adapter'); 18 | return listToolsAdapter(client, serverName); 19 | } 20 | 21 | /** 22 | * Call a tool on an MCP server with support for progress tracking 23 | * 24 | * This function delegates to the adapter 25 | */ 26 | export async function callTool( 27 | client: Client | undefined, 28 | serverName: string, 29 | toolName: string, 30 | args: Record, 31 | timeout?: number 32 | ): Promise { 33 | log.debug('Delegating to adapter'); 34 | return callToolAdapter(client, serverName, toolName, args, timeout); 35 | } 36 | 37 | /** 38 | * Cancel a tool execution in progress 39 | * 40 | * This function is deprecated and should not be used directly. 41 | * Tool cancellation is now handled by the backend service. 42 | */ 43 | export async function cancelToolExecution(client: Client, requestId: string, reason: string): Promise { 44 | log.debug('This function is deprecated. Tool cancellation is now handled by the backend service'); 45 | // No-op - cancellation is now handled by the backend service 46 | } 47 | -------------------------------------------------------------------------------- /src/app/api/mcp/types.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { ToolSchema } from '@modelcontextprotocol/sdk/types.js'; 3 | import { StdioServerParameters } from '@modelcontextprotocol/sdk/client/stdio.js'; 4 | 5 | // Constants 6 | export const SERVER_DIR_PREFIX = 'mcp-servers'; 7 | export type MCPManagerConfig = { 8 | name: string; 9 | disabled: boolean; 10 | autoApprove: string[]; 11 | rootPath: string; 12 | env: Record 13 | _buildCommand: string; 14 | _installCommand: string; 15 | } 16 | // Types 17 | export type MCPStdioConfig = StdioServerParameters & MCPManagerConfig & { 18 | transport: 'stdio'; 19 | }; 20 | 21 | export type MCPWebSocketConfig = MCPManagerConfig & { // there is no WebSocketServerParameters so we cant include anything here 22 | transport: 'websocket'; 23 | websocketUrl: string; 24 | }; 25 | 26 | export type MCPServerConfig = MCPStdioConfig | MCPWebSocketConfig; 27 | 28 | export interface ServiceResponse { 29 | success: boolean; 30 | data?: T; 31 | error?: string; 32 | } 33 | 34 | // Using the official type from MCP SDK 35 | export type ToolResponse = z.infer; 36 | 37 | export interface ConnectionAttempt { 38 | requestId: string; 39 | timestamp: number; 40 | status: 'pending' | 'success' | 'failed'; 41 | error?: string; 42 | } 43 | -------------------------------------------------------------------------------- /src/app/api/model/backend-model-adapter.ts: -------------------------------------------------------------------------------- 1 | import { createLogger } from '@/utils/logger'; 2 | import { Model } from '@/shared/types/model'; 3 | import * as modelAdapter from './frontend-model-adapter'; 4 | 5 | // Create a logger instance for this file 6 | const log = createLogger('app/api/model/backed-model-adapter'); 7 | 8 | /** 9 | * Server-side adapter for model operations 10 | * This adapter is used by server components to access model data 11 | * while maintaining the clean architecture pattern 12 | */ 13 | 14 | /** 15 | * Load all models 16 | * This adapter delegates to the model adapter 17 | */ 18 | export async function loadModels(): Promise { 19 | log.debug('loadModels: Delegating to model adapter'); 20 | const result = await modelAdapter.loadModels(); 21 | 22 | if (!result.success || !result.models) { 23 | log.warn('loadModels: Failed to load models', { error: result.error }); 24 | return []; 25 | } 26 | 27 | return result.models; 28 | } 29 | 30 | /** 31 | * Get a model by ID 32 | * This adapter delegates to the model adapter 33 | */ 34 | export async function getModel(modelId: string): Promise { 35 | log.debug(`getModel: Delegating to model adapter for model ID: ${modelId}`); 36 | const result = await modelAdapter.getModel(modelId); 37 | 38 | if (!result.success || !result.model) { 39 | log.warn(`getModel: Failed to get model ${modelId}`, { error: result.error }); 40 | return null; 41 | } 42 | 43 | return result.model; 44 | } 45 | -------------------------------------------------------------------------------- /src/app/api/model/backend-provider-adapter.ts: -------------------------------------------------------------------------------- 1 | import { createLogger } from '@/utils/logger'; 2 | import { NormalizedModel } from '@/shared/types/model/response'; 3 | import { modelService } from '@/backend/services/model'; 4 | 5 | // Create a logger instance for this file 6 | const log = createLogger('app/api/model/backend-provider-adapter'); 7 | 8 | /** 9 | * Fetch models from a provider 10 | * This adapter delegates to the backend service 11 | * @param baseUrl The base URL of the provider 12 | * @param modelId Optional model ID for existing models 13 | */ 14 | export async function fetchProviderModels( 15 | baseUrl: string, 16 | modelId?: string 17 | ): Promise { 18 | log.debug(`fetchProviderModels: Delegating to backend service for baseUrl: ${baseUrl}`); 19 | try { 20 | return await modelService.fetchProviderModels(baseUrl, modelId); 21 | } catch (error) { 22 | log.warn(`fetchProviderModels: Error fetching models for ${baseUrl}:`, error); 23 | // Return empty array instead of throwing to avoid UI errors 24 | return []; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/api/model/provider/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from 'next/server'; 2 | import { createLogger } from '@/utils/logger'; 3 | import { fetchProviderModels } from '../backend-provider-adapter'; 4 | 5 | const log = createLogger('app/api/model/provider/route'); 6 | 7 | export async function POST(request: NextRequest) { 8 | try { 9 | const { baseUrl, modelId } = await request.json(); 10 | 11 | if (!baseUrl) { 12 | return new Response(JSON.stringify({ error: 'Base URL is required' }), { 13 | status: 400, 14 | headers: { 'Content-Type': 'application/json' }, 15 | }); 16 | } 17 | 18 | if (!modelId) { 19 | return new Response(JSON.stringify({ error: 'Model ID is required' }), { 20 | status: 400, 21 | headers: { 'Content-Type': 'application/json' }, 22 | }); 23 | } 24 | 25 | // Fetch available models from the provider using the adapter 26 | // The adapter will delegate to the backend service which handles API key resolution and decryption 27 | const models = await fetchProviderModels(baseUrl, modelId); 28 | 29 | return new Response(JSON.stringify({ models }), { 30 | status: 200, 31 | headers: { 'Content-Type': 'application/json' }, 32 | }); 33 | } catch (error) { 34 | log.error('Error handling provider models request', error); 35 | return new Response(JSON.stringify({ error: 'Internal server error' }), { 36 | status: 500, 37 | headers: { 'Content-Type': 'application/json' }, 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/chat/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from 'react'; 4 | import { Box, Typography, Container } from '@mui/material'; 5 | import Chat from '@/frontend/components/Chat'; 6 | import ClientOnly from '@/frontend/components/ClientOnly'; 7 | import { createLogger } from '@/utils/logger'; 8 | 9 | const log = createLogger('app/chat/page'); 10 | 11 | export default function ChatPage() { 12 | log.debug('Rendering ChatPage'); 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | /* Import XYFlow styles globally */ 2 | @import '@xyflow/react/dist/style.css'; 3 | 4 | :root { 5 | /* Light theme variables (default) */ 6 | --background: #FFFFFF; 7 | --foreground: #2C3E50; 8 | --paper-background: #F5F6FA; 9 | --text-secondary: #7F8C8D; 10 | } 11 | 12 | :root.dark-theme { 13 | /* Dark theme variables */ 14 | --background: #1a1a1a; 15 | --foreground: #f0f0f0; 16 | --paper-background: #2a2a2a; 17 | --text-secondary: #aaaaaa; 18 | } 19 | 20 | body { 21 | color: var(--foreground); 22 | background: var(--background); 23 | font-family: Arial, Helvetica, sans-serif; 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | /* Global styles that were previously provided by Tailwind */ 29 | .sr-only { 30 | position: absolute; 31 | width: 1px; 32 | height: 1px; 33 | padding: 0; 34 | margin: -1px; 35 | overflow: hidden; 36 | clip: rect(0, 0, 0, 0); 37 | white-space: nowrap; 38 | border-width: 0; 39 | } 40 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Geist, Geist_Mono } from "next/font/google"; 3 | import "./globals.css"; 4 | import { createLogger } from '@/utils/logger'; 5 | import AppWrapper from "@/frontend/components/AppWrapper"; 6 | 7 | const log = createLogger('app/layout'); 8 | 9 | const geistSans = Geist({ 10 | variable: "--font-geist-sans", 11 | subsets: ["latin"], 12 | }); 13 | 14 | const geistMono = Geist_Mono({ 15 | variable: "--font-geist-mono", 16 | subsets: ["latin"], 17 | }); 18 | 19 | export const metadata: Metadata = { 20 | title: "FLUJO", 21 | description: "A browser-based application for managing models, MCP servers, flows and chat interactions", 22 | }; 23 | 24 | 25 | export default function RootLayout({ 26 | children, 27 | }: Readonly<{ 28 | children: React.ReactNode; 29 | }>) { 30 | log.debug('Rendering RootLayout'); 31 | return ( 32 | 33 | 36 | 37 | {children} 38 | 39 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/app/mcp/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Box } from '@mui/material'; 4 | import MCPManager from '@/frontend/components/mcp'; 5 | import { createLogger } from '@/utils/logger'; 6 | 7 | const log = createLogger('app/mcp/page'); 8 | 9 | export default function MCPPage() { 10 | log.debug('Rendering MCPPage'); 11 | return ( 12 | 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/app/models/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Box, Typography, Button } from '@mui/material'; 4 | import { useEffect } from 'react'; 5 | import { createLogger } from '@/utils/logger'; 6 | 7 | const log = createLogger('app/models/error'); 8 | 9 | export default function Error({ 10 | error, 11 | reset, 12 | }: { 13 | error: Error & { digest?: string }; 14 | reset: () => void; 15 | }) { 16 | useEffect(() => { 17 | log.error('Error in models page:', { error: error.message, digest: error.digest }); 18 | }, [error]); 19 | 20 | return ( 21 | 29 | 30 | Error loading models 31 | 32 | 33 | {error.message || 'Something went wrong while loading the models.'} 34 | 35 | 36 | 42 | 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/app/models/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Box, CircularProgress } from '@mui/material'; 2 | 3 | export default function Loading() { 4 | return ( 5 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/app/models/page.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react'; 2 | import { Box, Typography } from '@mui/material'; 3 | import { createLogger } from '@/utils/logger'; 4 | import * as serverAdapter from '@/app/api/model/backend-model-adapter'; 5 | import ModelClient from './ModelClient'; 6 | import Spinner from '@/frontend/components/shared/Spinner'; 7 | 8 | const log = createLogger('app/models/page'); 9 | 10 | export const dynamic = 'force-dynamic'; // Ensure dynamic rendering 11 | 12 | // Async server component 13 | async function ModelsPage() { 14 | log.debug('Rendering ModelsPage'); 15 | 16 | try { 17 | // Fetch models on the server using the server adapter 18 | const models = await serverAdapter.loadModels(); 19 | log.debug('Models loaded successfully', { count: models.length }); 20 | 21 | return ( 22 | 23 | 33 | Models 34 | 35 | 36 | }> 37 | 38 | 39 | 40 | 41 | ); 42 | } catch (error) { 43 | log.error('Error loading models:', error); 44 | throw error; // This will be caught by the error boundary 45 | } 46 | } 47 | 48 | export default ModelsPage; 49 | -------------------------------------------------------------------------------- /src/app/settings/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Box } from '@mui/material'; 4 | import Settings from '@/frontend/components/Settings'; 5 | import { createLogger } from '@/utils/logger'; 6 | 7 | const log = createLogger('app/settings/page'); 8 | 9 | export default function SettingsPage() { 10 | log.debug('Rendering SettingsPage'); 11 | return ( 12 | 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/app/v1/api/tags/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { loadItem } from '@/utils/storage/backend'; 3 | import { StorageKey } from '@/shared/types/storage'; 4 | import { Flow } from '@/frontend/types/flow/flow'; 5 | import { createLogger } from '@/utils/logger'; 6 | 7 | const log = createLogger('app/v1/api/tags/route'); 8 | 9 | export async function GET() { 10 | try { 11 | log.info('Fetching all flows for tags endpoint (Ollama format)'); 12 | 13 | // Load all flows directly from storage 14 | const flows = await loadItem(StorageKey.FLOWS, []); 15 | log.debug('Flows loaded successfully', { count: flows.length }); 16 | 17 | // Transform flows into the Ollama format 18 | const models = flows.map(flow => ({ 19 | name: `flow-${flow.name}`, 20 | modified_at: new Date().toISOString(), 21 | size: 0, 22 | digest: "", 23 | details: { 24 | format: "", 25 | family: "", 26 | families: null, 27 | parameter_size: "", 28 | quantization_level: "" 29 | } 30 | })); 31 | log.debug('Transformed flows into Ollama format', { modelCount: models.length }); 32 | 33 | // Return the models in the Ollama format 34 | log.info('Returning models in Ollama format'); 35 | return NextResponse.json({ 36 | models: models 37 | }); 38 | } catch (error) { 39 | log.error('Error fetching models', error); 40 | return NextResponse.json( 41 | { 42 | error: { 43 | message: 'Failed to fetch models', 44 | type: 'internal_error', 45 | code: 'internal_error' 46 | } 47 | }, 48 | { status: 500 } 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/app/v1/models/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { loadItem } from '@/utils/storage/backend'; 3 | import { StorageKey } from '@/shared/types/storage'; 4 | import { Flow } from '@/frontend/types/flow/flow'; 5 | import { createLogger } from '@/utils/logger'; 6 | 7 | const log = createLogger('app/v1/models/route'); 8 | 9 | export async function GET() { 10 | try { 11 | log.info('Fetching all flows for models endpoint'); 12 | 13 | // Load all flows directly from storage 14 | const flows = await loadItem(StorageKey.FLOWS, []); 15 | log.debug('Flows loaded successfully', { count: flows.length }); 16 | 17 | // Transform flows into the required format 18 | const models = flows.map(flow => ({ 19 | id: `flow-${flow.name}`, 20 | object: 'model' 21 | })); 22 | log.debug('Transformed flows into models', { modelCount: models.length }); 23 | 24 | // Return the models in the OpenAI format 25 | log.info('Returning models in OpenAI format'); 26 | return NextResponse.json({ 27 | object: 'list', 28 | data: models 29 | }); 30 | } catch (error) { 31 | log.error('Error fetching models', error); 32 | return NextResponse.json( 33 | { 34 | error: { 35 | message: 'Failed to fetch models', 36 | type: 'internal_error', 37 | code: 'internal_error' 38 | } 39 | }, 40 | { status: 500 } 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/backend/execution/flow/FlowExecutor.ts.backup: -------------------------------------------------------------------------------- 1 | // pip install pocketflowframework 2 | import { Flow as PocketFlow } from 'pocketflowframework'; 3 | import { flowService } from '@/backend/services/flow'; 4 | import { FlowConverter } from './FlowConverter'; 5 | import { createLogger } from '@/utils/logger'; 6 | 7 | // Create a logger instance for this file 8 | const log = createLogger('backend/execution/flow/FlowExecutor'); 9 | 10 | export class FlowExecutor { 11 | /** 12 | * Execute a flow by name 13 | */ 14 | static async executeFlow(flowName: string, initialState: any = {}): Promise { 15 | log.info(`Executing flow: ${flowName}`, { 16 | initialStateKeys: Object.keys(initialState) 17 | }); 18 | 19 | // Load the flow from storage 20 | log.debug('Loading flows from storage'); 21 | const flows = await flowService.loadFlows(); 22 | log.debug(`Loaded ${flows.length} flows from storage`); 23 | 24 | const reactFlow = flows.find(flow => flow.name === flowName); 25 | 26 | if (!reactFlow) { 27 | log.error(`Flow not found: ${flowName}`); 28 | throw new Error(`Flow not found: ${flowName}`); 29 | } 30 | 31 | log.info(`Found flow: ${flowName}`, { 32 | flowId: reactFlow.id, 33 | nodeCount: reactFlow.nodes.length, 34 | edgeCount: reactFlow.edges.length 35 | }); 36 | 37 | // Convert to Pocket Flow 38 | log.debug(`Converting flow to Pocket Flow: ${flowName}`); 39 | const pocketFlow = FlowConverter.convert(reactFlow) as PocketFlow; 40 | log.debug('Flow conversion completed'); 41 | 42 | // Create shared state 43 | const sharedState = { 44 | ...initialState, 45 | flowName, 46 | startTime: Date.now(), 47 | nodeExecutionTracker: [] // Track execution of each node 48 | }; 49 | 50 | log.info('Starting flow execution', { 51 | flowName, 52 | sharedStateKeys: Object.keys(sharedState) 53 | }); 54 | 55 | // Execute the flow 56 | try { 57 | await pocketFlow.run(sharedState); 58 | log.info('Flow execution completed successfully', { 59 | flowName, 60 | executionTime: Date.now() - sharedState.startTime, 61 | messagesCount: sharedState.messages?.length || 0 62 | }); 63 | } catch (error) { 64 | log.error('Error during flow execution', { 65 | flowName, 66 | error: error instanceof Error ? error.message : String(error) 67 | }); 68 | throw error; 69 | } 70 | 71 | // Return the final state 72 | const result = { 73 | result: sharedState.lastResponse || "Flow execution completed", 74 | messages: sharedState.messages || [], 75 | executionTime: Date.now() - sharedState.startTime, 76 | nodeExecutionTracker: sharedState.nodeExecutionTracker || [] // Include tracking information 77 | }; 78 | 79 | log.debug('Returning flow execution result', { 80 | resultLength: result.result?.length || 0, 81 | messagesCount: result.messages.length, 82 | executionTime: result.executionTime 83 | }); 84 | 85 | return result; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/backend/execution/flow/errorFactory.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FlowError, 3 | ModelError, 4 | ToolError, 5 | NodeError, 6 | MCPError 7 | } from './errors'; 8 | 9 | export const createModelError = ( 10 | code: string, 11 | message: string, 12 | modelId: string, 13 | requestId?: string, 14 | details?: Record 15 | ): ModelError => ({ 16 | type: 'model', 17 | code, 18 | message, 19 | modelId, 20 | requestId, 21 | details 22 | }); 23 | 24 | export const createToolError = ( 25 | code: string, 26 | message: string, 27 | toolName: string, 28 | toolArgs?: Record, 29 | details?: Record 30 | ): ToolError => ({ 31 | type: 'tool', 32 | code, 33 | message, 34 | toolName, 35 | toolArgs, 36 | details 37 | }); 38 | 39 | export const createNodeError = ( 40 | code: string, 41 | message: string, 42 | nodeId: string, 43 | nodeType: string, 44 | details?: Record 45 | ): NodeError => ({ 46 | type: 'node', 47 | code, 48 | message, 49 | nodeId, 50 | nodeType, 51 | details 52 | }); 53 | 54 | export const createMCPError = ( 55 | code: string, 56 | message: string, 57 | serverName: string, 58 | operation: string, 59 | details?: Record 60 | ): MCPError => ({ 61 | type: 'mcp', 62 | code, 63 | message, 64 | serverName, 65 | operation, 66 | details 67 | }); 68 | -------------------------------------------------------------------------------- /src/backend/execution/flow/errors.ts: -------------------------------------------------------------------------------- 1 | // Base error interface 2 | export interface FlowError { 3 | code: string; 4 | message: string; 5 | details?: Record; 6 | } 7 | 8 | // Model-related errors 9 | export interface ModelError extends FlowError { 10 | type: 'model'; 11 | modelId: string; 12 | requestId?: string; 13 | } 14 | 15 | // Tool-related errors 16 | export interface ToolError extends FlowError { 17 | type: 'tool'; 18 | toolName: string; 19 | toolArgs?: Record; 20 | } 21 | 22 | // Node-related errors 23 | export interface NodeError extends FlowError { 24 | type: 'node'; 25 | nodeId: string; 26 | nodeType: string; 27 | } 28 | 29 | // MCP-related errors 30 | export interface MCPError extends FlowError { 31 | type: 'mcp'; 32 | serverName: string; 33 | operation: string; 34 | } 35 | 36 | // Union type for all errors 37 | export type ExecutionError = ModelError | ToolError | NodeError | MCPError; 38 | 39 | // Result type for operations that can fail 40 | export type Result = 41 | | { success: true; value: T } 42 | | { success: false; error: ExecutionError }; 43 | -------------------------------------------------------------------------------- /src/backend/execution/flow/index.ts: -------------------------------------------------------------------------------- 1 | export * from './FlowConverter'; 2 | export * from './FlowExecutor'; 3 | export * from './nodes'; 4 | -------------------------------------------------------------------------------- /src/backend/execution/flow/nodes/index.ts: -------------------------------------------------------------------------------- 1 | export * from './StartNode'; 2 | export * from './ProcessNode'; 3 | export * from './MCPNode'; 4 | export * from './FinishNode'; 5 | -------------------------------------------------------------------------------- /src/backend/execution/flow/types/mcpHandler.ts: -------------------------------------------------------------------------------- 1 | import { ToolDefinition } from '../types'; 2 | 3 | // Input for MCP execution 4 | export interface MCPExecutionInput { 5 | mcpServer: string; 6 | enabledTools: string[]; 7 | mcpEnv?: Record; 8 | } 9 | 10 | // Result of MCP execution 11 | export interface MCPExecutionResult { 12 | server: string; 13 | tools: ToolDefinition[]; 14 | enabledTools: string[]; 15 | } 16 | -------------------------------------------------------------------------------- /src/backend/execution/flow/types/modelHandler.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from 'openai'; 2 | import { 3 | ToolDefinition, 4 | ToolCallInfo 5 | } from '../types'; 6 | import { FlujoChatMessage } from '@/shared/types/chat'; // Correct import path 7 | 8 | // Input for model call 9 | export interface ModelCallInput { 10 | modelId: string; 11 | prompt: string; 12 | messages: FlujoChatMessage[]; // Use FlujoChatMessage 13 | tools?: OpenAI.ChatCompletionTool[]; 14 | iteration: number; 15 | maxIterations: number; 16 | nodeName: string; // Name of the process node for display purposes 17 | nodeId: string; // ID of the process node 18 | } 19 | 20 | // Result of model call 21 | export interface ModelCallResult { 22 | content?: string; 23 | messages: FlujoChatMessage[]; // Use FlujoChatMessage 24 | toolCalls?: ToolCallInfo[]; 25 | fullResponse?: OpenAI.ChatCompletion; 26 | } 27 | 28 | // Tool call processing input 29 | export interface ToolCallProcessingInput { 30 | toolCalls: OpenAI.ChatCompletionMessageToolCall[]; 31 | content?: string; 32 | } 33 | 34 | // Tool call processing result 35 | export interface ToolCallProcessingResult { 36 | toolCallMessages: FlujoChatMessage[]; // Use FlujoChatMessage 37 | processedToolCalls: ToolCallInfo[]; 38 | } 39 | 40 | // Ensure the file is treated as a module 41 | export {}; 42 | -------------------------------------------------------------------------------- /src/backend/execution/flow/types/toolHandler.ts: -------------------------------------------------------------------------------- 1 | import { ToolDefinition, MCPNodeReference } from '../types'; 2 | import OpenAI from 'openai'; 3 | 4 | // Input for tool preparation 5 | export interface ToolPreparationInput { 6 | availableTools: ToolDefinition[]; 7 | } 8 | 9 | // Result of tool preparation 10 | export interface ToolPreparationResult { 11 | tools: OpenAI.ChatCompletionTool[]; 12 | } 13 | 14 | // Input for MCP node processing 15 | export interface MCPNodeProcessingInput { 16 | mcpNodes: MCPNodeReference[]; 17 | } 18 | 19 | // Result of MCP node processing 20 | export interface MCPNodeProcessingResult { 21 | availableTools: ToolDefinition[]; 22 | } 23 | -------------------------------------------------------------------------------- /src/backend/types/index.ts: -------------------------------------------------------------------------------- 1 | // export * from './mcp'; 2 | // todo remove file -------------------------------------------------------------------------------- /src/backend/utils/PromptRenderer.test.ts: -------------------------------------------------------------------------------- 1 | import { promptRenderer } from './PromptRenderer'; 2 | 3 | /** 4 | * Example usage of the PromptRenderer utility 5 | */ 6 | async function testPromptRenderer() { 7 | try { 8 | // Example 1: Render a complete prompt 9 | const completePrompt = await promptRenderer.renderPrompt('flow-123', 'node-456'); 10 | console.log('Complete Prompt:'); 11 | console.log(completePrompt); 12 | console.log('\n---\n'); 13 | 14 | // Example 2: Render with raw tool pills (no resolution) 15 | const rawPrompt = await promptRenderer.renderPrompt('flow-123', 'node-456', { 16 | renderMode: 'raw' 17 | }); 18 | console.log('Raw Prompt (with tool pills):'); 19 | console.log(rawPrompt); 20 | console.log('\n---\n'); 21 | 22 | // Example 3: Include conversation history placeholder 23 | const promptWithHistory = await promptRenderer.renderPrompt('flow-123', 'node-456', { 24 | includeConversationHistory: true 25 | }); 26 | console.log('Prompt with Conversation History:'); 27 | console.log(promptWithHistory); 28 | } catch (error) { 29 | console.error('Error testing PromptRenderer:', error); 30 | } 31 | } 32 | 33 | // Uncomment to run the test 34 | // testPromptRenderer(); 35 | 36 | /** 37 | * Example of how to use the PromptRenderer in a real application 38 | */ 39 | export async function renderNodePrompt(flowId: string, nodeId: string): Promise { 40 | try { 41 | return await promptRenderer.renderPrompt(flowId, nodeId, { 42 | renderMode: 'rendered', 43 | includeConversationHistory: false 44 | }); 45 | } catch (error) { 46 | console.error(`Error rendering prompt for node ${nodeId} in flow ${flowId}:`, error); 47 | return `Error rendering prompt: ${error instanceof Error ? error.message : 'Unknown error'}`; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/config/features.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Feature flags for the application 3 | * 4 | * This file contains feature flags that can be used to enable or disable 5 | * specific features of the application. 6 | */ 7 | export const FEATURES = { 8 | /** 9 | * Controls the application's logging level 10 | * Possible values: 11 | * - -1: VERBOSE (most verbose) 12 | * - 0: DEBUG 13 | * - 1: INFO 14 | * - 2: WARN 15 | * - 3: ERROR (least verbose) 16 | * 17 | * Only log messages with a level greater than or equal to this value will be displayed 18 | */ 19 | LOG_LEVEL: 1, // VERBOSE level for debugging 20 | 21 | /** 22 | * Controls whether tool calls are included in the response 23 | * When set to true, tool calls will be included in the response 24 | * When set to false, tool calls will be processed but not included in the response 25 | */ 26 | INCLUDE_TOOL_CALLS_IN_RESPONSE: true, 27 | 28 | /** 29 | * Controls whether the execution tracker is enabled 30 | * When true, node execution history will be tracked in sharedState.trackingInfo.nodeExecutionTracker 31 | * When false, the nodeExecutionTracker array will not be created or updated 32 | */ 33 | ENABLE_EXECUTION_TRACKER: false, // Enabled by default 34 | }; 35 | -------------------------------------------------------------------------------- /src/frontend/components/AppWrapper.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { Suspense } from 'react'; 4 | import dynamic from 'next/dynamic'; 5 | import { createLogger } from '@/utils/logger'; 6 | 7 | const log = createLogger('frontend/components/AppWrapper'); 8 | 9 | // Dynamically import components with loading fallbacks 10 | const ThemeProvider = dynamic(() => import('../contexts/ThemeContext').then(mod => mod.ThemeProvider), { 11 | ssr: false, 12 | loading: () =>
Loading theme...
13 | }); 14 | 15 | const StorageProvider = dynamic(() => import('../contexts/StorageContext').then(mod => mod.StorageProvider), { 16 | ssr: false, 17 | loading: () =>
Loading storage...
18 | }); 19 | 20 | const Navigation = dynamic(() => import("./Navigation"), { 21 | ssr: false, 22 | loading: () =>
Loading navigation...
23 | }); 24 | 25 | const EncryptionAuthDialog = dynamic(() => import("./EncryptionAuthDialog"), { 26 | ssr: false, 27 | loading: () => null 28 | }); 29 | 30 | // Lazily load the TransformersPreloader only in the browser 31 | const TransformersPreloader = dynamic( 32 | () => import('../services/transcription/client').then(mod => mod.TransformersPreloader), 33 | { 34 | ssr: false, 35 | loading: () => null 36 | } 37 | ); 38 | 39 | // Error boundary component to catch chunk loading errors 40 | class ErrorBoundary extends React.Component< 41 | { children: React.ReactNode }, 42 | { hasError: boolean } 43 | > { 44 | constructor(props: { children: React.ReactNode }) { 45 | super(props); 46 | this.state = { hasError: false }; 47 | } 48 | 49 | static getDerivedStateFromError() { 50 | return { hasError: true }; 51 | } 52 | 53 | componentDidCatch(error: any) { 54 | log.error('AppWrapper error boundary caught an error:', error); 55 | } 56 | 57 | render() { 58 | if (this.state.hasError) { 59 | return ( 60 |
61 |

Something went wrong loading the application.

62 |

Please try refreshing the page.

63 | 77 |
78 | ); 79 | } 80 | 81 | return this.props.children; 82 | } 83 | } 84 | 85 | interface AppWrapperProps { 86 | children: React.ReactNode; 87 | } 88 | 89 | export default function AppWrapper({ children }: AppWrapperProps) { 90 | log.debug('Rendering AppWrapper'); 91 | return ( 92 | 93 | Loading application...}> 94 | 95 | 96 | Loading navigation...}> 97 | 98 | 99 | 100 |
101 | {children} 102 |
103 |
104 |
105 |
106 |
107 | ); 108 | } 109 | -------------------------------------------------------------------------------- /src/frontend/components/ClientOnly.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState, ReactNode } from 'react'; 4 | import { createLogger } from '@/utils/logger'; 5 | 6 | const log = createLogger('frontend/components/ClientOnly'); 7 | 8 | interface ClientOnlyProps { 9 | children: ReactNode; 10 | } 11 | 12 | /** 13 | * Component that only renders its children on the client side. 14 | * This helps prevent hydration mismatches for components that use browser-only APIs. 15 | */ 16 | export default function ClientOnly({ children }: ClientOnlyProps) { 17 | const [isMounted, setIsMounted] = useState(false); 18 | 19 | useEffect(() => { 20 | log.debug('ClientOnly component mounted'); 21 | setIsMounted(true); 22 | return () => { 23 | log.debug('ClientOnly component unmounted'); 24 | }; 25 | }, []); 26 | 27 | if (!isMounted) { 28 | log.debug('ClientOnly component rendering null (not yet mounted)'); 29 | return null; 30 | } 31 | 32 | log.debug('ClientOnly component rendering children'); 33 | return <>{children}; 34 | } 35 | -------------------------------------------------------------------------------- /src/frontend/components/Flow/FlowDashboard/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import FlowDashboard from './FlowDashboard'; 4 | import FlowCard, { FlowCardSkeleton } from './FlowCard'; 5 | 6 | export { FlowDashboard, FlowCard, FlowCardSkeleton }; 7 | export default FlowDashboard; 8 | -------------------------------------------------------------------------------- /src/frontend/components/Flow/FlowManager/FlowBuilder/Canvas/components/CanvasControls.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Background, Controls, MiniMap } from '@xyflow/react'; 3 | import { useTheme } from '@mui/material/styles'; 4 | 5 | interface CanvasControlsProps { 6 | showMiniMap?: boolean; 7 | showControls?: boolean; 8 | showBackground?: boolean; 9 | } 10 | 11 | /** 12 | * Component that wraps ReactFlow's Background, Controls, and MiniMap components 13 | */ 14 | export const CanvasControls: React.FC = ({ 15 | showMiniMap = true, 16 | showControls = true, 17 | showBackground = true, 18 | }) => { 19 | const theme = useTheme(); 20 | const isDarkMode = theme.palette.mode === 'dark'; 21 | 22 | return ( 23 | <> 24 | {showBackground && ( 25 | 30 | )} 31 | {showControls && } 32 | {showMiniMap && ( 33 | 37 | )} 38 | 39 | ); 40 | }; 41 | 42 | export default CanvasControls; 43 | -------------------------------------------------------------------------------- /src/frontend/components/Flow/FlowManager/FlowBuilder/Canvas/components/CanvasToolbar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Panel } from '@xyflow/react'; 3 | import { Button, Tooltip } from '@mui/material'; 4 | import { styled } from '@mui/material/styles'; 5 | import ZoomInIcon from '@mui/icons-material/ZoomIn'; 6 | import ZoomOutIcon from '@mui/icons-material/ZoomOut'; 7 | import FitScreenIcon from '@mui/icons-material/FitScreen'; 8 | 9 | const ToolbarButton = styled(Button)(({ theme }) => ({ 10 | minWidth: '36px', 11 | padding: '6px', 12 | margin: '0 4px', 13 | backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.02)', 14 | '&:hover': { 15 | backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.05)', 16 | } 17 | })); 18 | 19 | interface CanvasToolbarProps { 20 | flowContainerRef: React.RefObject; 21 | } 22 | 23 | /** 24 | * Toolbar component for the Canvas with zoom controls 25 | */ 26 | export const CanvasToolbar: React.FC = ({ flowContainerRef }) => { 27 | return ( 28 | 29 |
30 | 31 | flowContainerRef.current?.querySelector('.react-flow__controls-button:first-child')?.click()} 35 | > 36 | 37 | 38 | 39 | 40 | flowContainerRef.current?.querySelector('.react-flow__controls-button:nth-child(2)')?.click()} 44 | > 45 | 46 | 47 | 48 | 49 | flowContainerRef.current?.querySelector('.react-flow__controls-button:nth-child(3)')?.click()} 53 | > 54 | 55 | 56 | 57 |
58 |
59 | ); 60 | }; 61 | 62 | export default CanvasToolbar; 63 | -------------------------------------------------------------------------------- /src/frontend/components/Flow/FlowManager/FlowBuilder/Canvas/hooks/useCanvasState.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { useNodesState, useEdgesState, Edge } from '@xyflow/react'; 3 | import { FlowNode } from '@/frontend/types/flow/flow'; 4 | import { createLogger } from '@/utils/logger'; 5 | 6 | // Create a logger instance for this file 7 | const log = createLogger('components/flow/FlowBuilder/Canvas/hooks/useCanvasState.ts'); 8 | 9 | /** 10 | * Custom hook to manage canvas state (nodes and edges) 11 | * @param initialNodes Initial nodes array 12 | * @param initialEdges Initial edges array 13 | * @returns Object containing state and state management functions 14 | */ 15 | export function useCanvasState(initialNodes: FlowNode[], initialEdges: Edge[]) { 16 | // Use ReactFlow's state hooks 17 | const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); 18 | const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); 19 | 20 | // Track the last added edge for notifying parent 21 | const [lastAddedEdge, setLastAddedEdge] = useState(null); 22 | 23 | // Log when lastAddedEdge changes 24 | useEffect(() => { 25 | if (lastAddedEdge) { 26 | log.debug(`useCanvasState: Last added edge set - ${lastAddedEdge.id} from ${lastAddedEdge.source} to ${lastAddedEdge.target}`); 27 | } 28 | }, [lastAddedEdge]); 29 | 30 | // Sync with parent component when initialNodes change 31 | useEffect(() => { 32 | if (initialNodes !== nodes) { 33 | log.debug(`useCanvasState: Syncing ${initialNodes.length} nodes from parent component`); 34 | setNodes(initialNodes); 35 | } 36 | }, [initialNodes, setNodes, nodes]); 37 | 38 | // Sync with parent component when initialEdges change 39 | useEffect(() => { 40 | if (initialEdges !== edges) { 41 | log.debug(`useCanvasState: Syncing ${initialEdges.length} edges from parent component`); 42 | setEdges(initialEdges); 43 | } 44 | }, [initialEdges, setEdges, edges]); 45 | 46 | return { 47 | nodes, 48 | edges, 49 | setNodes, 50 | setEdges, 51 | onNodesChange, 52 | onEdgesChange, 53 | lastAddedEdge, 54 | setLastAddedEdge 55 | }; 56 | } 57 | 58 | export default useCanvasState; 59 | -------------------------------------------------------------------------------- /src/frontend/components/Flow/FlowManager/FlowBuilder/Canvas/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Canvas } from './Canvas'; 2 | export * from './types'; 3 | -------------------------------------------------------------------------------- /src/frontend/components/Flow/FlowManager/FlowBuilder/Canvas/types.ts: -------------------------------------------------------------------------------- 1 | import { FlowNode, NodeType } from '@/frontend/types/flow/flow'; 2 | import { 3 | Edge, 4 | NodeChange, 5 | EdgeChange, 6 | ReactFlowInstance, 7 | MarkerType 8 | } from '@xyflow/react'; 9 | 10 | export interface CanvasProps { 11 | initialNodes?: FlowNode[]; 12 | initialEdges?: Edge[]; 13 | onNodesChange?: (changes: NodeChange[]) => void; 14 | onEdgesChange?: (changes: EdgeChange[]) => void; 15 | onDrop?: (event: React.DragEvent) => void; 16 | onDragOver?: (event: React.DragEvent) => void; 17 | onInit?: (reactFlowInstance: ReactFlowInstance) => void; 18 | reactFlowWrapper?: React.RefObject; 19 | onEditNode?: (node: FlowNode) => void; 20 | } 21 | 22 | export interface EditNodeEventDetail { 23 | nodeId: string; 24 | } 25 | 26 | export interface NodeSelectionModalProps { 27 | open: boolean; 28 | position: { x: number; y: number } | null; 29 | onClose: () => void; 30 | onSelectNodeType: (nodeType: NodeType, position: { x: number; y: number }) => void; 31 | sourceNodeType?: NodeType; 32 | sourceHandleId?: string; 33 | } 34 | 35 | export interface ContextMenuState { 36 | open: boolean; 37 | position: { x: number; y: number }; 38 | nodeId?: string; 39 | edgeId?: string; 40 | } 41 | 42 | export interface SelectedElementsState { 43 | nodes: string[]; 44 | edges: string[]; 45 | } 46 | 47 | // Constants 48 | export const MIN_DISTANCE = 150; 49 | 50 | // Default edge options 51 | export const defaultEdgeOptions = { 52 | type: 'custom', 53 | animated: true, 54 | style: { stroke: '#555', strokeWidth: 2 }, 55 | markerEnd: { 56 | type: MarkerType.ArrowClosed, 57 | width: 20, 58 | height: 20, 59 | color: '#555', 60 | }, 61 | }; 62 | 63 | // MCP edge options - bi-directional arrows without animation 64 | export const mcpEdgeOptions = { 65 | type: 'mcpEdge', 66 | animated: false, 67 | style: { stroke: '#1976d2', strokeWidth: 2 }, 68 | markerEnd: { 69 | type: MarkerType.ArrowClosed, 70 | width: 20, 71 | height: 20, 72 | color: '#1976d2', 73 | }, 74 | markerStart: { 75 | type: MarkerType.ArrowClosed, 76 | width: 20, 77 | height: 20, 78 | color: '#1976d2', 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /src/frontend/components/Flow/FlowManager/FlowBuilder/Canvas/utils/nodeUtils.ts: -------------------------------------------------------------------------------- 1 | import { FlowNode } from '@/frontend/types/flow/flow'; 2 | import { NodeChange, EdgeChange } from '@xyflow/react'; 3 | 4 | /** 5 | * Generates changes for deleting nodes and their connected edges 6 | * @param selectedNodeIds Array of selected node IDs 7 | * @param selectedEdgeIds Array of selected edge IDs 8 | * @param nodes Array of flow nodes 9 | * @param edges Array of edges 10 | * @returns Object containing node and edge changes 11 | */ 12 | export function getDeleteChanges( 13 | selectedNodeIds: string[], 14 | selectedEdgeIds: string[], 15 | nodes: FlowNode[], 16 | edges: any[] 17 | ): { nodeChanges: NodeChange[], edgeChanges: EdgeChange[] } { 18 | // Filter out Start nodes - they cannot be deleted 19 | const deletableNodeIds = selectedNodeIds.filter(nodeId => { 20 | const node = nodes.find(n => n.id === nodeId); 21 | return node && node.type !== 'start'; // Only include nodes that are not Start nodes 22 | }); 23 | 24 | // Create node changes for deletion 25 | const nodeChanges: NodeChange[] = deletableNodeIds.map(nodeId => ({ 26 | type: 'remove', 27 | id: nodeId, 28 | })); 29 | 30 | // Find all edges connected to the selected nodes 31 | const connectedEdges = edges.filter(edge => 32 | selectedNodeIds.includes(edge.source) || 33 | selectedNodeIds.includes(edge.target) 34 | ); 35 | 36 | // Create edge changes for deletion (both selected edges and connected edges) 37 | const edgeChanges: EdgeChange[] = [ 38 | ...selectedEdgeIds.map(edgeId => ({ 39 | type: 'remove' as const, 40 | id: edgeId, 41 | })), 42 | ...connectedEdges 43 | .filter(edge => !selectedEdgeIds.includes(edge.id)) // Avoid duplicates 44 | .map(edge => ({ 45 | type: 'remove' as const, 46 | id: edge.id, 47 | })) 48 | ]; 49 | 50 | return { nodeChanges, edgeChanges }; 51 | } 52 | 53 | /** 54 | * Finds a node by its ID 55 | * @param nodeId Node ID to find 56 | * @param nodes Array of flow nodes 57 | * @returns The found node or undefined 58 | */ 59 | export function findNodeById(nodeId: string, nodes: FlowNode[]): FlowNode | undefined { 60 | return nodes.find(node => node.id === nodeId); 61 | } 62 | 63 | /** 64 | * Checks if a node can be deleted 65 | * @param nodeId Node ID to check 66 | * @param nodes Array of flow nodes 67 | * @returns Boolean indicating if the node can be deleted 68 | */ 69 | export function canDeleteNode(nodeId: string, nodes: FlowNode[]): boolean { 70 | const node = findNodeById(nodeId, nodes); 71 | // Start nodes cannot be deleted 72 | return node ? node.type !== 'start' : false; 73 | } 74 | -------------------------------------------------------------------------------- /src/frontend/components/Flow/FlowManager/FlowBuilder/CustomEdges/CustomEdge.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { FC } from 'react'; 4 | import { 5 | EdgeProps, 6 | getSmoothStepPath, 7 | BaseEdge, 8 | EdgeLabelRenderer, 9 | Position 10 | } from '@xyflow/react'; 11 | import { styled, useTheme } from '@mui/material/styles'; 12 | 13 | const EdgeButton = styled('button')(({ theme }) => ({ 14 | background: theme.palette.background.paper, 15 | border: `1px solid ${theme.palette.divider}`, 16 | cursor: 'pointer', 17 | borderRadius: '50%', 18 | fontSize: '10px', 19 | width: '20px', 20 | height: '20px', 21 | display: 'flex', 22 | justifyContent: 'center', 23 | alignItems: 'center', 24 | color: theme.palette.text.primary, 25 | boxShadow: theme.palette.mode === 'dark' 26 | ? '0 2px 4px rgba(0,0,0,0.3)' 27 | : '0 2px 4px rgba(0,0,0,0.1)', 28 | '&:hover': { 29 | boxShadow: theme.palette.mode === 'dark' 30 | ? '0 2px 6px rgba(0,0,0,0.4)' 31 | : '0 2px 6px rgba(0,0,0,0.2)', 32 | background: theme.palette.mode === 'dark' 33 | ? theme.palette.action.hover 34 | : theme.palette.background.paper, 35 | } 36 | })); 37 | 38 | const EdgePath = styled(BaseEdge)(({ theme }) => ({ 39 | '&.animated': { 40 | strokeDasharray: 5, 41 | animation: 'flowPathAnimation 0.5s infinite linear', 42 | }, 43 | '&.temp': { 44 | strokeDasharray: '5,5', 45 | strokeOpacity: 0.5, 46 | }, 47 | '@keyframes flowPathAnimation': { 48 | '0%': { 49 | strokeDashoffset: 10, 50 | }, 51 | '100%': { 52 | strokeDashoffset: 0, 53 | }, 54 | } 55 | })); 56 | 57 | const CustomEdge: FC = ({ 58 | id, 59 | sourceX, 60 | sourceY, 61 | targetX, 62 | targetY, 63 | sourcePosition, 64 | targetPosition, 65 | style = {}, 66 | markerEnd, 67 | data, 68 | selected 69 | }) => { 70 | // Default values for edge path 71 | const [edgePath, labelX, labelY] = getSmoothStepPath({ 72 | sourceX, 73 | sourceY, 74 | sourcePosition: sourcePosition || Position.Bottom, 75 | targetX, 76 | targetY, 77 | targetPosition: targetPosition || Position.Top, 78 | borderRadius: 16 79 | }); 80 | 81 | const theme = useTheme(); 82 | 83 | // Default edge style 84 | const edgeStyle = { 85 | ...style, 86 | strokeWidth: selected ? 3 : 2, 87 | stroke: selected 88 | ? theme.palette.primary.main 89 | : theme.palette.text.secondary, 90 | }; 91 | 92 | return ( 93 | <> 94 | 101 | 102 |
111 | × 112 |
113 |
114 | 115 | ); 116 | }; 117 | 118 | export default CustomEdge; 119 | -------------------------------------------------------------------------------- /src/frontend/components/Flow/FlowManager/FlowBuilder/CustomEdges/MCPEdge.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { FC } from 'react'; 4 | import { 5 | EdgeProps, 6 | getSmoothStepPath, 7 | BaseEdge, 8 | EdgeLabelRenderer, 9 | Position, 10 | MarkerType 11 | } from '@xyflow/react'; 12 | import { styled, useTheme } from '@mui/material/styles'; 13 | 14 | const EdgeButton = styled('button')(({ theme }) => ({ 15 | background: theme.palette.background.paper, 16 | border: `1px solid ${theme.palette.divider}`, 17 | cursor: 'pointer', 18 | borderRadius: '50%', 19 | fontSize: '10px', 20 | width: '20px', 21 | height: '20px', 22 | display: 'flex', 23 | justifyContent: 'center', 24 | alignItems: 'center', 25 | color: theme.palette.text.primary, 26 | boxShadow: theme.palette.mode === 'dark' 27 | ? '0 2px 4px rgba(0,0,0,0.3)' 28 | : '0 2px 4px rgba(0,0,0,0.1)', 29 | '&:hover': { 30 | boxShadow: theme.palette.mode === 'dark' 31 | ? '0 2px 6px rgba(0,0,0,0.4)' 32 | : '0 2px 6px rgba(0,0,0,0.2)', 33 | background: theme.palette.mode === 'dark' 34 | ? theme.palette.action.hover 35 | : theme.palette.background.paper, 36 | } 37 | })); 38 | 39 | const EdgePath = styled(BaseEdge)(({ theme }) => ({ 40 | '&.animated': { 41 | strokeDasharray: 5, 42 | animation: 'flowPathAnimation 0.5s infinite linear', 43 | }, 44 | '@keyframes flowPathAnimation': { 45 | '0%': { 46 | strokeDashoffset: 10, 47 | }, 48 | '100%': { 49 | strokeDashoffset: 0, 50 | }, 51 | } 52 | })); 53 | 54 | const MCPEdge: FC = ({ 55 | id, 56 | sourceX, 57 | sourceY, 58 | targetX, 59 | targetY, 60 | sourcePosition, 61 | targetPosition, 62 | style = {}, 63 | markerEnd, 64 | data, 65 | selected 66 | }) => { 67 | // Default values for edge path 68 | const [edgePath, labelX, labelY] = getSmoothStepPath({ 69 | sourceX, 70 | sourceY, 71 | sourcePosition: sourcePosition || Position.Left, 72 | targetX, 73 | targetY, 74 | targetPosition: targetPosition || Position.Right, 75 | borderRadius: 16 76 | }); 77 | 78 | const theme = useTheme(); 79 | 80 | // MCP edge style - using theme colors 81 | const edgeStyle = { 82 | ...style, 83 | strokeWidth: selected ? 3 : 2, 84 | stroke: selected 85 | ? theme.palette.info.light 86 | : theme.palette.info.main, // Use theme info color for MCP connections 87 | }; 88 | 89 | return ( 90 | <> 91 | 98 | 99 |
108 | × 109 |
110 |
111 | 112 | ); 113 | }; 114 | 115 | export default MCPEdge; 116 | -------------------------------------------------------------------------------- /src/frontend/components/Flow/FlowManager/FlowBuilder/CustomEdges/index.ts: -------------------------------------------------------------------------------- 1 | import CustomEdge from './CustomEdge'; 2 | import MCPEdge from './MCPEdge'; 3 | 4 | export { CustomEdge, MCPEdge }; 5 | export default CustomEdge; 6 | -------------------------------------------------------------------------------- /src/frontend/components/Flow/FlowManager/FlowBuilder/CustomNodes/BaseNode.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { Handle, Position, NodeProps } from '@xyflow/react'; 3 | import { styled } from '@mui/material/styles'; 4 | import { Paper, Typography } from '@mui/material'; 5 | 6 | const NodeContainer = styled(Paper)(({ theme }) => ({ 7 | padding: theme.spacing(1), 8 | minWidth: '150px', 9 | borderRadius: '8px', 10 | backgroundColor: theme.palette.background.paper, 11 | border: `1px solid ${theme.palette.divider}`, 12 | })); 13 | 14 | const BaseNode = ({ data }: NodeProps) => { 15 | return ( 16 | 17 | 18 | 19 | {data.label as string} 20 | 21 | 22 | 23 | ); 24 | }; 25 | 26 | export default memo(BaseNode); 27 | -------------------------------------------------------------------------------- /src/frontend/components/Flow/FlowManager/FlowBuilder/Modals/ProcessNodePropertiesModal/NodeConfiguration.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TextField, Typography, Box } from '@mui/material'; 3 | 4 | interface NodeConfigurationProps { 5 | nodeData: { 6 | label: string; 7 | description?: string; 8 | } | null; 9 | setNodeData: (data: any) => void; 10 | } 11 | 12 | const NodeConfiguration: React.FC = ({ nodeData, setNodeData }) => { 13 | if (!nodeData) return null; 14 | 15 | return ( 16 | 17 | 18 | Node Configuration 19 | 20 | 21 | setNodeData({ ...nodeData, label: e.target.value })} 26 | margin="normal" 27 | /> 28 | 29 | setNodeData({ ...nodeData, description: e.target.value })} 34 | margin="normal" 35 | multiline 36 | rows={2} 37 | helperText="This description will be displayed on the node" 38 | /> 39 | 40 | ); 41 | }; 42 | 43 | export default NodeConfiguration; 44 | -------------------------------------------------------------------------------- /src/frontend/components/Flow/FlowManager/FlowBuilder/Modals/ProcessNodePropertiesModal/NodeProperties.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TextField, FormControl, InputLabel, Select, MenuItem, FormControlLabel, Switch, Typography, Box } from '@mui/material'; 3 | import { PropertyDefinition } from './types'; 4 | 5 | interface NodePropertiesProps { 6 | nodeData: { 7 | properties: Record; 8 | } | null; 9 | handlePropertyChange: (key: string, value: any) => void; 10 | properties: PropertyDefinition[]; 11 | } 12 | 13 | const NodeProperties: React.FC = ({ nodeData, handlePropertyChange, properties }) => { 14 | const renderField = (property: PropertyDefinition) => { 15 | if (!nodeData) return null; 16 | 17 | const value = nodeData.properties?.[property.key] ?? ''; 18 | 19 | switch (property.type) { 20 | case 'text': 21 | return ( 22 | handlePropertyChange(property.key, e.target.value)} 30 | margin="normal" 31 | /> 32 | ); 33 | case 'number': 34 | return ( 35 | handlePropertyChange(property.key, Number(e.target.value))} 47 | margin="normal" 48 | /> 49 | ); 50 | case 'select': 51 | return ( 52 | 53 | {property.label} 54 | 65 | 66 | ); 67 | case 'boolean': 68 | return ( 69 | handlePropertyChange(property.key, e.target.checked)} 75 | /> 76 | } 77 | label={property.label} 78 | sx={{ my: 1 }} 79 | /> 80 | ); 81 | default: 82 | return null; 83 | } 84 | }; 85 | 86 | return ( 87 | <> 88 | {properties.length > 0 && ( 89 | 90 | 91 | Node Properties 92 | 93 | {properties.map(property => renderField(property))} 94 | 95 | )} 96 | 97 | ); 98 | }; 99 | 100 | export default NodeProperties; 101 | -------------------------------------------------------------------------------- /src/frontend/components/Flow/FlowManager/FlowBuilder/Modals/ProcessNodePropertiesModal/ServerTools/index.tsx: -------------------------------------------------------------------------------- 1 | import ServerTools from './ServerTools'; 2 | import AgentTools from './AgentTools'; 3 | 4 | export { 5 | ServerTools, 6 | AgentTools 7 | }; 8 | 9 | // Also export ServerTools as default for backward compatibility 10 | export default ServerTools; 11 | -------------------------------------------------------------------------------- /src/frontend/components/Flow/FlowManager/FlowBuilder/Modals/ProcessNodePropertiesModal/hooks/useNodeData.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback } from 'react'; 2 | import { FlowNode } from '@/frontend/types/flow/flow'; 3 | 4 | const useNodeData = (node: FlowNode | null) => { 5 | const [nodeData, setNodeData] = useState<{ 6 | id: string; // Add id property 7 | label: string; 8 | type: string; 9 | description?: string; 10 | properties: Record; 11 | } | null>(null); 12 | 13 | useEffect(() => { 14 | if (node) { 15 | setNodeData({ 16 | id: node.id, // Include the node ID 17 | ...node.data, 18 | properties: { ...node.data.properties } 19 | }); 20 | } 21 | }, [node]); 22 | 23 | const handlePropertyChange = useCallback((key: string, value: any) => { 24 | setNodeData((prev) => { 25 | if (!prev) return null; 26 | return { 27 | ...prev, 28 | properties: { 29 | ...prev.properties, 30 | [key]: value, 31 | }, 32 | }; 33 | }); 34 | }, []); 35 | 36 | return { nodeData, setNodeData, handlePropertyChange }; 37 | }; 38 | 39 | export default useNodeData; 40 | -------------------------------------------------------------------------------- /src/frontend/components/Flow/FlowManager/FlowBuilder/Modals/ProcessNodePropertiesModal/types.ts: -------------------------------------------------------------------------------- 1 | import { FlowNode } from '@/frontend/types/flow/flow'; 2 | import { Edge } from '@xyflow/react'; 3 | import { Model as SharedModel } from '@/shared/types/model'; 4 | 5 | // Re-export the shared Model type 6 | export type Model = SharedModel; 7 | 8 | export interface ProcessNodePropertiesModalProps { 9 | open: boolean; 10 | node: FlowNode | null; 11 | onClose: () => void; 12 | onSave: (nodeId: string, data: ProcessNodeData) => void; 13 | flowEdges?: Edge[]; 14 | flowNodes?: FlowNode[]; 15 | flowId?: string; // Added flowId property 16 | } 17 | 18 | export interface ProcessNodeData { 19 | label: string; 20 | type: string; 21 | description?: string; 22 | properties: Record; 23 | } 24 | 25 | export interface PropertyDefinition { 26 | key: string; 27 | label: string; 28 | type: 'text' | 'number' | 'select' | 'boolean'; 29 | multiline?: boolean; 30 | rows?: number; 31 | min?: number; 32 | max?: number; 33 | step?: number; 34 | options?: string[]; 35 | } 36 | -------------------------------------------------------------------------------- /src/frontend/components/Flow/FlowManager/FlowBuilder/Modals/ProcessNodePropertiesModal/utils.ts: -------------------------------------------------------------------------------- 1 | import { Edge } from '@xyflow/react'; 2 | import { PropertyDefinition } from './types'; 3 | 4 | // No properties since we removed operation and enabled 5 | export const getNodeProperties = (): PropertyDefinition[] => []; 6 | 7 | // Find MCP nodes connected to this Process node 8 | export const findConnectedMCPNodes = (nodeId: string, allEdges: Edge[]) => { 9 | return allEdges 10 | .filter(edge => 11 | (edge.source === nodeId && edge.data?.edgeType === 'mcp') || 12 | (edge.target === nodeId && edge.data?.edgeType === 'mcp') 13 | ) 14 | .map(edge => edge.source === nodeId ? edge.target : edge.source); 15 | }; 16 | -------------------------------------------------------------------------------- /src/frontend/components/Flow/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mario-andreschak/FLUJO/1e6a2d6a77f012a15162419eeb7f9d07fa95d060/src/frontend/components/Flow/index.ts -------------------------------------------------------------------------------- /src/frontend/components/Navigation/index.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { AppBar, Box, IconButton, Toolbar, Typography, useTheme as useMuiTheme } from '@mui/material'; 3 | import { useTheme } from '@/frontend/contexts/ThemeContext'; 4 | import { createLogger } from '@/utils/logger'; 5 | 6 | const log = createLogger('frontend/components/Navigation'); 7 | import Brightness4Icon from '@mui/icons-material/Brightness4'; 8 | import Brightness7Icon from '@mui/icons-material/Brightness7'; 9 | import Link from 'next/link'; 10 | import { usePathname } from 'next/navigation'; 11 | 12 | const navItems = [ 13 | { name: 'Models', path: '/models' }, 14 | { name: 'MCP', path: '/mcp' }, 15 | { name: 'Flows', path: '/flows' }, 16 | { name: 'Chat', path: '/chat' }, 17 | { name: 'Settings', path: '/settings' }, 18 | ]; 19 | 20 | export default function Navigation() { 21 | const { toggleTheme, isDarkMode } = useTheme(); 22 | const muiTheme = useMuiTheme(); 23 | const pathname = usePathname(); 24 | 25 | log.debug(`Rendering Navigation component with pathname: ${pathname}`); 26 | 27 | return ( 28 | 29 | 30 | 42 | FLUJO 43 | 44 | 45 | 46 | {navItems.map((item) => ( 47 | 60 | {item.name} 61 | 62 | ))} 63 | 64 | 65 | { 67 | log.debug(`Theme toggle clicked, current mode: ${isDarkMode ? 'dark' : 'light'}`); 68 | toggleTheme(); 69 | }} 70 | color="inherit" 71 | > 72 | {isDarkMode ? : } 73 | 74 | 75 | 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /src/frontend/components/Settings/GlobalEnvSettings.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useState, useEffect } from 'react'; 4 | import { 5 | Box, 6 | Typography, 7 | Alert, 8 | } from '@mui/material'; 9 | import { useStorage } from '@/frontend/contexts/StorageContext'; 10 | import EnvEditor from '../mcp/MCPEnvManager/EnvEditor'; 11 | import { MASKED_STRING } from '@/shared/types/constants'; 12 | 13 | export default function GlobalEnvSettings() { 14 | const { globalEnvVars, setGlobalEnvVars, deleteGlobalEnvVar } = useStorage(); 15 | const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); 16 | 17 | const handleSave = async (vars: Record) => { 18 | try { 19 | // Filter out secret variables that have the masked value 20 | const filteredVars = Object.fromEntries( 21 | Object.entries(vars).filter(([key, value]) => { 22 | return typeof value === 'string' || !value.metadata.isSecret || value.value !== MASKED_STRING; 23 | }) 24 | ); 25 | 26 | await setGlobalEnvVars(filteredVars); 27 | setMessage({ 28 | type: 'success', 29 | text: 'Global environment variables updated successfully', 30 | }); 31 | 32 | // Clear message after 3 seconds 33 | setTimeout(() => { 34 | setMessage(null); 35 | }, 3000); 36 | } catch (error) { 37 | setMessage({ 38 | type: 'error', 39 | text: 'Failed to update global environment variables', 40 | }); 41 | } 42 | }; 43 | 44 | return ( 45 | 46 | 47 | Define global environment variables that can be bound to MCP servers. This allows you to manage API keys and other sensitive information in one place. 48 | 49 | 50 | {message && ( 51 | 52 | {message.text} 53 | 54 | )} 55 | 56 | 62 | 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /src/frontend/components/mcp/MCPEnvManager/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import { Box, Paper, Typography } from '@mui/material'; 5 | import EnvEditor from './EnvEditor'; 6 | import { useServerStatus } from '@/frontend/hooks/useServerStatus'; 7 | import { createLogger } from '@/utils/logger'; 8 | 9 | const log = createLogger('frontend/components/mcp/MCPEnvManager'); 10 | 11 | interface EnvManagerProps { 12 | serverName: string | null; 13 | } 14 | 15 | const EnvManager: React.FC = ({ serverName }) => { 16 | const { servers, saveEnv, toggleServer } = useServerStatus(); 17 | 18 | // Find the selected server 19 | const selectedServer = serverName 20 | ? servers.find(server => server.name === serverName) 21 | : null; 22 | 23 | // Handle saving environment variables 24 | const handleSaveEnv = async (env: Record) => { 25 | if (!serverName) return; 26 | 27 | log.debug(`Saving environment variables for server: ${serverName}`); 28 | 29 | // Pass the complete environment structure with metadata to saveEnv 30 | await saveEnv(serverName, env); 31 | }; 32 | 33 | // Handle server restart after env variable changes 34 | const handleServerRestart = async (serverName: string) => { 35 | log.debug(`Restarting server after env variable changes: ${serverName}`); 36 | 37 | // Disable the server 38 | await toggleServer(serverName, false); 39 | 40 | // Re-enable the server immediately (no delay) 41 | await toggleServer(serverName, true); 42 | 43 | log.info(`Server ${serverName} restarted after env variable changes`); 44 | }; 45 | 46 | // If no server is selected, show a message 47 | if (!serverName || !selectedServer) { 48 | return ( 49 | theme.palette.mode === 'dark' ? '#3a3a3a' : '#e5e7eb' 56 | }} 57 | > 58 | 59 | Environment Variables 60 | 61 | 62 | Please select a server to manage environment variables. 63 | 64 | 65 | ); 66 | } 67 | 68 | return ( 69 | 70 | 76 | 77 | ); 78 | }; 79 | 80 | export default EnvManager; 81 | -------------------------------------------------------------------------------- /src/frontend/components/mcp/MCPServerManager/Modals/ServerModal/tabs/GitHubTab/GitHubActions.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import { RepoInfo } from '../../types'; 5 | import { Box, Button } from '@mui/material'; 6 | 7 | interface GitHubActionsProps { 8 | showCloneButton: boolean; 9 | isCloning: boolean; 10 | cloneCompleted: boolean; 11 | repoInfo: RepoInfo | null; 12 | repoExists?: boolean; 13 | onClone: (forceClone?: boolean) => Promise; 14 | } 15 | 16 | const GitHubActions: React.FC = ({ 17 | showCloneButton, 18 | isCloning, 19 | cloneCompleted, 20 | repoInfo, 21 | repoExists, 22 | onClone 23 | }) => { 24 | if (!showCloneButton) return null; 25 | 26 | // Handle regular clone (no force) 27 | const handleClone = () => { 28 | onClone(false); 29 | }; 30 | 31 | // Handle force clone (re-clone) 32 | const handleForceClone = () => { 33 | onClone(true); 34 | }; 35 | 36 | return ( 37 | 38 | {repoExists && ( 39 | 47 | )} 48 | 56 | 57 | ); 58 | }; 59 | 60 | export default GitHubActions; 61 | -------------------------------------------------------------------------------- /src/frontend/components/mcp/MCPServerManager/Modals/ServerModal/tabs/LocalServerTab/ConsoleOutput.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import VisibilityIcon from '@mui/icons-material/Visibility'; 5 | import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; 6 | import { 7 | Box, 8 | IconButton, 9 | Paper, 10 | Typography, 11 | useTheme 12 | } from '@mui/material'; 13 | 14 | interface ConsoleOutputProps { 15 | output: string; 16 | isVisible: boolean; 17 | toggleVisibility: () => void; 18 | title?: string; 19 | } 20 | 21 | const ConsoleOutput: React.FC = ({ 22 | output, 23 | isVisible, 24 | toggleVisibility, 25 | title = 'Command Output' 26 | }) => { 27 | const theme = useTheme(); 28 | 29 | return ( 30 | 39 | 40 | {title} 41 | 48 | {isVisible ? : } 49 | 50 | 51 | 52 | 63 | {output ? ( 64 | 65 | {output} 66 | 67 | ) : ( 68 | 69 | No output to display 70 | 71 | )} 72 | 73 | 74 | ); 75 | }; 76 | 77 | export default ConsoleOutput; 78 | -------------------------------------------------------------------------------- /src/frontend/components/mcp/MCPServerManager/Modals/ServerModal/tabs/LocalServerTab/LocalServerForm.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import FolderIcon from '@mui/icons-material/Folder'; 5 | import { 6 | Box, 7 | IconButton, 8 | InputAdornment, 9 | Stack, 10 | TextField, 11 | Typography 12 | } from '@mui/material'; 13 | 14 | interface LocalServerFormProps { 15 | name: string; 16 | setName: (name: string) => void; 17 | rootPath: string; 18 | setRootPath: (rootPath: string) => void; 19 | onRootPathSelect: () => void; 20 | } 21 | 22 | const LocalServerForm: React.FC = ({ 23 | name, 24 | setName, 25 | rootPath, 26 | setRootPath, 27 | onRootPathSelect 28 | }) => { 29 | return ( 30 | 31 | 32 | 33 | Server Name 34 | 35 | setName(e.target.value)} 40 | placeholder="my-mcp-server" 41 | variant="outlined" 42 | required 43 | /> 44 | 45 | 46 | 47 | 48 | MCP Server Root Path 49 | 50 | setRootPath(e.target.value)} 55 | placeholder="/path/to/server/root" 56 | variant="outlined" 57 | InputProps={{ 58 | endAdornment: ( 59 | 60 | 64 | 65 | 66 | 67 | ), 68 | }} 69 | /> 70 | 71 | 72 | ); 73 | }; 74 | 75 | export default LocalServerForm; 76 | -------------------------------------------------------------------------------- /src/frontend/components/mcp/MCPServerManager/Modals/ServerModal/tabs/LocalServerTab/components/BuildSection.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import BuildTools from '../BuildTools'; 5 | import SectionHeader from './SectionHeader'; 6 | 7 | interface BuildSectionProps { 8 | installCommand: string; 9 | setInstallCommand: (command: string) => void; 10 | buildCommand: string; 11 | setBuildCommand: (command: string) => void; 12 | onInstall: () => Promise; 13 | onBuild: () => Promise; 14 | isInstalling: boolean; 15 | isBuilding: boolean; 16 | installCompleted: boolean; 17 | buildCompleted: boolean; 18 | isExpanded: boolean; 19 | toggleSection: () => void; 20 | } 21 | 22 | const BuildSection: React.FC = ({ 23 | installCommand, 24 | setInstallCommand, 25 | buildCommand, 26 | setBuildCommand, 27 | onInstall, 28 | onBuild, 29 | isInstalling, 30 | isBuilding, 31 | installCompleted, 32 | buildCompleted, 33 | isExpanded, 34 | toggleSection 35 | }) => { 36 | // Determine section status based on build/install state 37 | const getSectionStatus = () => { 38 | if (installCompleted && buildCompleted) { 39 | return 'success'; 40 | } else if (installCompleted || buildCompleted) { 41 | return 'warning'; 42 | } else if (isInstalling || isBuilding) { 43 | return 'loading'; 44 | } 45 | return 'default'; 46 | }; 47 | 48 | return ( 49 |
54 | 60 | 61 | {isExpanded && ( 62 | 74 | )} 75 |
76 | ); 77 | }; 78 | 79 | export default BuildSection; 80 | -------------------------------------------------------------------------------- /src/frontend/components/mcp/MCPServerManager/Modals/ServerModal/tabs/LocalServerTab/components/ConsoleToggle.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import VisibilityIcon from '@mui/icons-material/Visibility'; 5 | import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; 6 | 7 | interface ConsoleToggleProps { 8 | isVisible: boolean; 9 | toggleVisibility: () => void; 10 | } 11 | 12 | const ConsoleToggle: React.FC = ({ 13 | isVisible, 14 | toggleVisibility 15 | }) => { 16 | return ( 17 | 35 | ); 36 | }; 37 | 38 | export default ConsoleToggle; 39 | -------------------------------------------------------------------------------- /src/frontend/components/mcp/MCPServerManager/Modals/ServerModal/tabs/LocalServerTab/components/DefineServerSection.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import { MCPServerConfig } from '@/shared/types/mcp/mcp'; 5 | import LocalServerForm from '../LocalServerForm'; 6 | import SectionHeader from './SectionHeader'; 7 | 8 | interface DefineServerSectionProps { 9 | localConfig: MCPServerConfig; 10 | setLocalConfig: (config: MCPServerConfig) => void; 11 | isExpanded: boolean; 12 | toggleSection: () => void; 13 | onRootPathSelect: () => void; 14 | } 15 | 16 | const DefineServerSection: React.FC = ({ 17 | localConfig, 18 | setLocalConfig, 19 | isExpanded, 20 | toggleSection, 21 | onRootPathSelect 22 | }) => { 23 | // Determine section status based on form completion 24 | const getSectionStatus = () => { 25 | if (!localConfig.name || !localConfig.rootPath) { 26 | return 'error'; 27 | } 28 | return 'default'; 29 | }; 30 | 31 | return ( 32 |
33 | 39 | 40 | {isExpanded && ( 41 |
42 | setLocalConfig({ ...localConfig, name })} 45 | rootPath={localConfig.rootPath || ''} 46 | setRootPath={(rootPath) => setLocalConfig({ ...localConfig, rootPath })} 47 | onRootPathSelect={onRootPathSelect} 48 | /> 49 |
50 | )} 51 |
52 | ); 53 | }; 54 | 55 | export default DefineServerSection; 56 | -------------------------------------------------------------------------------- /src/frontend/components/mcp/MCPServerManager/Modals/ServerModal/tabs/LocalServerTab/components/SectionHeader.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | 5 | interface SectionHeaderProps { 6 | title: string; 7 | isExpanded: boolean; 8 | onToggle: () => void; 9 | status?: 'default' | 'error' | 'success' | 'warning' | 'loading'; 10 | rightContent?: React.ReactNode; 11 | } 12 | 13 | const SectionHeader: React.FC = ({ 14 | title, 15 | isExpanded, 16 | onToggle, 17 | status = 'default', 18 | rightContent 19 | }) => { 20 | // Determine text color based on status 21 | const getTextColorClass = () => { 22 | switch (status) { 23 | case 'error': 24 | return 'text-red-600 dark:text-red-400'; 25 | case 'success': 26 | return 'text-green-600 dark:text-green-400'; 27 | case 'warning': 28 | return 'text-orange-600 dark:text-orange-400'; 29 | case 'loading': 30 | return 'text-blue-600 dark:text-blue-400'; 31 | default: 32 | return 'text-gray-700 dark:text-gray-300'; 33 | } 34 | }; 35 | 36 | return ( 37 |
38 |
39 | 47 |

{title}

48 |
49 | {rightContent && ( 50 |
51 | {rightContent} 52 |
53 | )} 54 |
55 | ); 56 | }; 57 | 58 | export default SectionHeader; 59 | -------------------------------------------------------------------------------- /src/frontend/components/mcp/MCPServerManager/Modals/ServerModal/tabs/LocalServerTab/hooks/useConsoleOutput.ts: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState } from 'react'; 4 | 5 | export const useConsoleOutput = () => { 6 | const [consoleOutput, setConsoleOutput] = useState(''); 7 | const [isConsoleVisible, setIsConsoleVisible] = useState(false); 8 | const [consoleTitle, setConsoleTitle] = useState('Command Output'); 9 | 10 | const toggleConsoleVisibility = () => { 11 | setIsConsoleVisible(prev => !prev); 12 | }; 13 | 14 | const appendToConsole = (text: string) => { 15 | setConsoleOutput(prev => prev + text); 16 | }; 17 | 18 | const clearConsole = () => { 19 | setConsoleOutput(''); 20 | }; 21 | 22 | const updateConsole = (text: string | ((prev: string) => string)) => { 23 | setConsoleOutput(text); 24 | }; 25 | 26 | return { 27 | consoleOutput, 28 | isConsoleVisible, 29 | consoleTitle, 30 | setConsoleTitle, 31 | toggleConsoleVisibility, 32 | setIsConsoleVisible, 33 | appendToConsole, 34 | clearConsole, 35 | updateConsole 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /src/frontend/components/mcp/MCPServerManager/Modals/ServerModal/tabs/SmitheryTab.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import { TabProps } from '../types'; 5 | 6 | const SmitheryTab: React.FC = () => { 7 | return ( 8 |
9 |

10 | Smithery CLI integration coming soon... 11 |

12 |
13 | ); 14 | }; 15 | 16 | export default SmitheryTab; 17 | -------------------------------------------------------------------------------- /src/frontend/components/mcp/MCPServerManager/Modals/ServerModal/types.ts: -------------------------------------------------------------------------------- 1 | import { MCPServerConfig } from '@/utils/mcp'; 2 | 3 | export interface ServerModalProps { 4 | isOpen: boolean; 5 | onClose: () => void; 6 | onAdd: (config: MCPServerConfig) => void; 7 | initialConfig?: MCPServerConfig | null; 8 | onUpdate?: (config: MCPServerConfig) => void; 9 | onRestartAfterUpdate?: (serverName: string) => void; 10 | } 11 | 12 | export interface MessageState { 13 | type: 'success' | 'error' | 'warning'; 14 | text: string; 15 | } 16 | 17 | export interface RepoInfo { 18 | owner: string; 19 | repo: string; 20 | valid: boolean; 21 | contents?: any; 22 | } 23 | 24 | export interface TabProps { 25 | initialConfig?: MCPServerConfig | null; 26 | onAdd: (config: MCPServerConfig) => void; 27 | onUpdate?: (config: MCPServerConfig) => void; 28 | onClose: () => void; 29 | onRestartAfterUpdate?: (serverName: string) => void; 30 | setActiveTab?: (tab: 'github' | 'local' | 'smithery' | 'reference' | 'docker') => void; 31 | } 32 | -------------------------------------------------------------------------------- /src/frontend/components/mcp/MCPServerManager/Modals/ServerModal/utils/buildUtils.ts: -------------------------------------------------------------------------------- 1 | import { MessageState } from '../types'; 2 | 3 | 4 | export const installDependencies = async ( 5 | serverPath: string, 6 | installCommand: string 7 | ): Promise<{ 8 | success: boolean; 9 | message: MessageState; 10 | output?: string; 11 | }> => { 12 | try { 13 | 14 | // Call the server-side git API to install dependencies 15 | const response = await fetch('/api/git', { 16 | method: 'POST', 17 | headers: { 18 | 'Content-Type': 'application/json', 19 | }, 20 | body: JSON.stringify({ 21 | action: 'install', 22 | savePath: serverPath, 23 | installCommand: installCommand 24 | }), 25 | }); 26 | 27 | const result = await response.json(); 28 | 29 | if (!response.ok) { 30 | throw new Error(result.error || 'Failed to install dependencies'); 31 | } 32 | 33 | return { 34 | success: true, 35 | message: { 36 | type: 'success', 37 | text: `Dependencies installed successfully. You can now build the server.` 38 | }, 39 | output: result.commandOutput || 'No output was returned from the installation process.' 40 | }; 41 | } catch (error) { 42 | console.error('Error installing dependencies:', error); 43 | 44 | return { 45 | success: false, 46 | message: { 47 | type: 'error', 48 | text: `Error installing dependencies: ${(error as Error).message || 'Unknown error'}. You can still try to build the server.` 49 | }, 50 | output: error instanceof Response ? await error.text() : (error as any)?.message || 'Unknown error' 51 | }; 52 | } 53 | }; 54 | 55 | export const buildServer = async ( 56 | serverPath: string, 57 | buildCommand: string 58 | ): Promise<{ 59 | success: boolean; 60 | message: MessageState; 61 | output?: string; 62 | }> => { 63 | try { 64 | // Call the server-side git API to build the repository 65 | const response = await fetch('/api/git', { 66 | method: 'POST', 67 | headers: { 68 | 'Content-Type': 'application/json', 69 | }, 70 | body: JSON.stringify({ 71 | action: 'build', 72 | savePath: serverPath, 73 | buildCommand: buildCommand 74 | }), 75 | }); 76 | 77 | const result = await response.json(); 78 | 79 | if (!response.ok) { 80 | throw new Error(result.error || 'Failed to build repository'); 81 | } 82 | 83 | return { 84 | success: true, 85 | message: { 86 | type: 'success', 87 | text: `Server built successfully.` 88 | }, 89 | output: result.commandOutput || 'No output was returned from the build process.' 90 | }; 91 | } catch (error) { 92 | console.error('Error building server:', error); 93 | 94 | return { 95 | success: false, 96 | message: { 97 | type: 'error', 98 | text: `Error building server: ${(error as Error).message || 'Unknown error'}.` 99 | }, 100 | output: error instanceof Response ? await error.text() : (error as any)?.message || 'Unknown error' 101 | }; 102 | } 103 | }; 104 | -------------------------------------------------------------------------------- /src/frontend/components/mcp/MCPServerManager/Modals/ServerModal/utils/configDetection.ts: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { MessageState } from '../types'; 4 | import { parseRepositoryConfig } from '@/utils/mcp/configparse'; 5 | import { MCPServerConfig } from '@/shared/types/mcp/mcp'; 6 | import { createLogger } from '@/utils/logger'; 7 | 8 | const log = createLogger('frontend/components/mcp/MCPServerManager/Modals/ServerModal/utils/configDetection'); 9 | 10 | /** 11 | * Detect and parse configuration from a cloned repository 12 | */ 13 | export async function detectRepositoryConfig( 14 | repoPath: string, 15 | repoName: string, 16 | owner?: string 17 | ): Promise<{ 18 | config: Partial; 19 | message: MessageState; 20 | success: boolean; 21 | language?: string; 22 | }> { 23 | try { 24 | log.debug(`Detecting configuration for repository: ${repoPath}`); 25 | 26 | // Parse repository configuration 27 | const result = await parseRepositoryConfig({ 28 | repoPath, 29 | repoName, 30 | owner 31 | }); 32 | 33 | if (result.detected && result.config) { 34 | log.debug(`Configuration detected for ${repoPath}`, { language: result.language }); 35 | 36 | return { 37 | config: result.config, 38 | message: result.message || { 39 | type: 'success', 40 | text: `Configuration detected successfully.` 41 | }, 42 | success: true, 43 | language: result.language 44 | }; 45 | } else { 46 | log.debug(`No configuration detected for ${repoPath}`); 47 | 48 | // Return a default configuration with a warning message 49 | return { 50 | config: { 51 | name: repoName, 52 | transport: 'stdio', 53 | command: '', 54 | args: [], 55 | env: {}, 56 | disabled: false, 57 | autoApprove: [], 58 | rootPath: repoPath, 59 | _buildCommand: '', 60 | _installCommand: '', 61 | }, 62 | message: result.message || { 63 | type: 'warning', 64 | text: 'Could not detect repository configuration. Please configure manually.' 65 | }, 66 | success: false, 67 | language: result.language 68 | }; 69 | } 70 | } catch (error) { 71 | log.error(`Error detecting configuration for ${repoPath}:`, error); 72 | 73 | // Return a default configuration with an error message 74 | return { 75 | config: { 76 | name: repoName, 77 | transport: 'stdio', 78 | command: '', 79 | args: [], 80 | env: {}, 81 | disabled: false, 82 | autoApprove: [], 83 | rootPath: repoPath, 84 | _buildCommand: '', 85 | _installCommand: '', 86 | }, 87 | message: { 88 | type: 'error', 89 | text: `Error detecting configuration: ${error instanceof Error ? error.message : 'Unknown error'}` 90 | }, 91 | success: false 92 | }; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/frontend/components/mcp/MCPServerManager/Modals/ServerModal/utils/errorHandling.ts: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { MessageState } from '../types'; 4 | import { createLogger } from '@/utils/logger'; 5 | 6 | const log = createLogger('frontend/components/mcp/MCPServerManager/Modals/ServerModal/utils/errorHandling'); 7 | 8 | /** 9 | * Create a user-friendly error message for configuration detection failures 10 | */ 11 | export function createConfigDetectionErrorMessage(error: unknown): MessageState { 12 | log.error('Configuration detection error:', error); 13 | 14 | let errorMessage = 'Failed to detect repository configuration.'; 15 | 16 | if (error instanceof Error) { 17 | errorMessage = `${errorMessage} ${error.message}`; 18 | } else if (typeof error === 'string') { 19 | errorMessage = `${errorMessage} ${error}`; 20 | } 21 | 22 | return { 23 | type: 'error', 24 | text: errorMessage 25 | }; 26 | } 27 | 28 | /** 29 | * Create a user-friendly error message for repository cloning failures 30 | */ 31 | export function createCloneErrorMessage(error: unknown): MessageState { 32 | log.error('Repository cloning error:', error); 33 | 34 | let errorMessage = 'Failed to clone repository.'; 35 | 36 | if (error instanceof Error) { 37 | errorMessage = `${errorMessage} ${error.message}`; 38 | } else if (typeof error === 'string') { 39 | errorMessage = `${errorMessage} ${error}`; 40 | } 41 | 42 | return { 43 | type: 'error', 44 | text: errorMessage 45 | }; 46 | } 47 | 48 | /** 49 | * Create a user-friendly warning message for empty configuration 50 | */ 51 | export function createEmptyConfigWarningMessage(language?: string): MessageState { 52 | const languageText = language ? ` for ${language}` : ''; 53 | 54 | return { 55 | type: 'warning', 56 | text: `No configuration detected${languageText}. Please configure manually.` 57 | }; 58 | } 59 | 60 | /** 61 | * Create a user-friendly success message for configuration detection 62 | */ 63 | export function createConfigDetectionSuccessMessage(language?: string): MessageState { 64 | const languageText = language ? ` for ${language}` : ''; 65 | 66 | return { 67 | type: 'success', 68 | text: `Configuration detected successfully${languageText}.` 69 | }; 70 | } 71 | -------------------------------------------------------------------------------- /src/frontend/components/mcp/MCPServerManager/ServerList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ServerCard from './ServerCard'; 3 | import Spinner from '@/frontend/components/shared/Spinner'; 4 | import { MCPServerConfig, MCPServerState } from '@/shared/types/'; 5 | import { createLogger } from '@/utils/logger'; 6 | import { Grid, Box, Typography, Paper } from '@mui/material'; 7 | 8 | const log = createLogger('frontend/components/mcp/MCPServerManager/ServerList'); 9 | 10 | interface ServerListProps { 11 | servers: MCPServerState[]; 12 | isLoading: boolean; 13 | loadError: string | null; 14 | onServerSelect: (serverName: string) => void; 15 | onServerToggle: (serverName: string, enabled: boolean) => void; 16 | onServerRetry: (serverName: string) => void; 17 | onServerDelete: (serverName: string) => void; 18 | onServerEdit: (server: MCPServerConfig) => void; 19 | } 20 | 21 | const ServerList: React.FC = ({ 22 | servers, 23 | isLoading, 24 | loadError, 25 | onServerSelect, 26 | onServerToggle, 27 | onServerRetry, 28 | onServerDelete, 29 | onServerEdit, 30 | }) => { 31 | log.debug('Rendering ServerList', { 32 | serverCount: servers.length, 33 | isLoading, 34 | hasError: !!loadError 35 | }); 36 | 37 | if (isLoading) { 38 | log.debug('Servers are loading'); 39 | return ( 40 | 47 | 48 | 49 | Loading servers... 50 | 51 | 52 | ); 53 | } 54 | 55 | if (loadError) { 56 | log.warn('Error loading servers:', loadError); 57 | return ( 58 | 59 | {loadError} 60 | 61 | ); 62 | } 63 | 64 | if (servers.length === 0) { 65 | return ( 66 | 67 | 68 | No servers configured. Click "Add Server" to get started. 69 | 70 | 71 | ); 72 | } 73 | 74 | return ( 75 | 76 | {servers.map((server) => ( 77 | 78 | onServerToggle(server.name, enabled)} 84 | onRetry={() => onServerRetry(server.name)} 85 | onDelete={() => onServerDelete(server.name)} 86 | onClick={() => onServerSelect(server.name)} 87 | onEdit={() => onServerEdit(server)} 88 | error={server.error} 89 | stderrOutput={server.stderrOutput} 90 | containerName={server.containerName} 91 | /> 92 | 93 | ))} 94 | 95 | ); 96 | }; 97 | 98 | export default ServerList; 99 | -------------------------------------------------------------------------------- /src/frontend/components/mcp/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useState } from 'react'; 4 | import { Box, Container } from '@mui/material'; 5 | import ServerManager from './MCPServerManager'; 6 | import ToolManager from './MCPToolManager'; 7 | import EnvManager from './MCPEnvManager'; 8 | import { createLogger } from '@/utils/logger'; 9 | 10 | const log = createLogger('frontend/components/mcp'); 11 | 12 | const MCPManager: React.FC = () => { 13 | const [selectedServer, setSelectedServer] = useState(null); 14 | const [isServerModalOpen, setIsServerModalOpen] = useState(false); 15 | 16 | const handleServerSelect = (serverName: string) => { 17 | log.debug(`Selected server: ${serverName}`); 18 | setSelectedServer(serverName); 19 | }; 20 | 21 | const handleServerModalToggle = (isOpen: boolean) => { 22 | log.debug(`Server modal ${isOpen ? 'opened' : 'closed'}`); 23 | setIsServerModalOpen(isOpen); 24 | }; 25 | 26 | return ( 27 | 28 | {/* Server Management Section */} 29 | 33 | 34 | {/* Tool Testing Section - Hide when server modal is open */} 35 | {selectedServer && !isServerModalOpen && ( 36 | 37 | 38 | 39 | )} 40 | 41 | {/* Environment Variables Section - Hide when server modal is open */} 42 | {selectedServer && !isServerModalOpen && ( 43 | 44 | 45 | 46 | )} 47 | 48 | ); 49 | }; 50 | 51 | export default MCPManager; 52 | -------------------------------------------------------------------------------- /src/frontend/components/models/list/ModelCard.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from 'react'; 4 | import { 5 | Card, 6 | CardContent, 7 | CardActions, 8 | Typography, 9 | IconButton, 10 | Box, 11 | Tooltip, 12 | } from '@mui/material'; 13 | import EditIcon from '@mui/icons-material/Edit'; 14 | import DeleteIcon from '@mui/icons-material/Delete'; 15 | import KeyIcon from '@mui/icons-material/Key'; 16 | import { Model } from '@/shared/types'; 17 | 18 | export interface ModelCardProps { 19 | model: Model; 20 | onEdit: () => void; 21 | onDelete: () => void; 22 | } 23 | 24 | export const ModelCard = ({ model, onEdit, onDelete }: ModelCardProps) => { 25 | return ( 26 | 27 | 28 | 29 | {model.displayName || model.name} 30 | 31 | 43 | {model.description} 44 | 45 | 46 | 47 | 48 | API Key: •••••••• 49 | 50 | 51 | {model.baseUrl && ( 52 | 53 | 54 | Base URL: {model.baseUrl} 55 | 56 | 57 | )} 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | ); 69 | }; 70 | 71 | export default ModelCard; 72 | -------------------------------------------------------------------------------- /src/frontend/components/models/list/ModelList.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from 'react'; 4 | import { Grid, CircularProgress, Box } from '@mui/material'; 5 | import ModelCard from './ModelCard'; 6 | import { Model } from '@/shared/types'; 7 | import { ModelResult } from '@/frontend/services/model'; 8 | import { createLogger } from '@/utils/logger'; 9 | 10 | const log = createLogger('frontend/components/models/list/ModelList'); 11 | 12 | interface ModelListProps { 13 | models: Model[]; 14 | isLoading: boolean; 15 | onAdd: () => void; 16 | onUpdate: (model: Model) => Promise; 17 | onDelete: (id: string) => Promise; 18 | } 19 | 20 | export const ModelList = ({ models, isLoading, onAdd, onUpdate, onDelete }: ModelListProps) => { 21 | if (isLoading) { 22 | return ( 23 | 24 | 25 | 26 | ); 27 | } 28 | 29 | const handleUpdate = async (model: Model): Promise => { 30 | const result = await onUpdate(model); 31 | if (!result.success) { 32 | log.error('Failed to update model in ModelList', result.error); 33 | // Consider displaying an error message to the user here, 34 | // perhaps using a state variable to show an alert. 35 | } 36 | } 37 | 38 | return ( 39 | 40 | {!models || models.length === 0 ? ( 41 | 42 | 43 | No models found 44 | 45 | 46 | ) : ( 47 | models.map((model) => ( 48 | 49 | handleUpdate(model)} 52 | onDelete={() => onDelete(model.id)} 53 | /> 54 | 55 | )) 56 | )} 57 | 58 | ); 59 | }; 60 | 61 | export default ModelList; 62 | -------------------------------------------------------------------------------- /src/frontend/components/shared/PromptBuilder/promptBuilder.css: -------------------------------------------------------------------------------- 1 | /* Slate editor styles */ 2 | .slate-editor-container { 3 | font-size: 18px; 4 | line-height: 1.6; 5 | } 6 | 7 | .slate-editor { 8 | min-height: 100%; 9 | outline: none; 10 | } 11 | 12 | /* Tool reference wrapper and container styles */ 13 | .tool-reference-wrapper { 14 | position: relative; 15 | display: inline; 16 | } 17 | 18 | .tool-reference-container { 19 | position: relative; 20 | display: inline-flex; 21 | align-items: center; 22 | background-color: #e3f2fd; 23 | border-radius: 4px; 24 | margin: 0 2px; 25 | border: 1px solid #bbdefb; 26 | user-select: none; 27 | white-space: nowrap; 28 | } 29 | 30 | /* Handoff tool reference container styles */ 31 | .tool-reference-container.handoff { 32 | background-color: rgba(0, 0, 0, 0.04); 33 | border: 1px solid rgba(0, 0, 0, 0.1); 34 | } 35 | 36 | /* Tool reference text */ 37 | .tool-reference { 38 | color: #1976d2; 39 | padding: 2px 6px; 40 | font-size: 0.9em; 41 | font-family: monospace; 42 | } 43 | 44 | /* Handoff tool reference text */ 45 | .tool-reference.handoff { 46 | color: rgba(0, 0, 0, 0.7); 47 | } 48 | 49 | /* Delete button */ 50 | .tool-reference-delete { 51 | display: flex; 52 | align-items: center; 53 | justify-content: center; 54 | width: 16px; 55 | height: 16px; 56 | border-radius: 50%; 57 | background-color: rgba(0, 0, 0, 0.1); 58 | color: #1976d2; 59 | font-size: 12px; 60 | cursor: pointer; 61 | margin-right: 4px; 62 | border: none; 63 | padding: 0; 64 | } 65 | 66 | /* Handoff tool delete button */ 67 | .tool-reference-delete.handoff { 68 | color: rgba(0, 0, 0, 0.6); 69 | } 70 | 71 | .tool-reference-delete:hover { 72 | background-color: rgba(0, 0, 0, 0.2); 73 | } 74 | 75 | /* Preview mode styles */ 76 | .preview-container { 77 | font-size: 18px; 78 | line-height: 1.6; 79 | } 80 | 81 | .preview-content p { 82 | margin-bottom: 1em; 83 | } 84 | 85 | /* Tool reference preview styles */ 86 | .tool-reference-preview { 87 | display: inline-block; 88 | background-color: #f5f5f5; 89 | border-radius: 4px; 90 | padding: 4px 8px; 91 | margin: 0 2px; 92 | font-size: 0.95em; 93 | line-height: 1.4; 94 | border: 1px solid #e0e0e0; 95 | } 96 | 97 | /* Handoff tool reference preview styles */ 98 | .tool-reference-preview.handoff { 99 | background-color: rgba(0, 0, 0, 0.04); 100 | border: 1px solid rgba(0, 0, 0, 0.1); 101 | color: rgba(0, 0, 0, 0.7); 102 | } 103 | 104 | .tool-reference-preview.loading { 105 | background-color: #fff8e1; 106 | border-color: #ffe082; 107 | color: #ff8f00; 108 | } 109 | 110 | .tool-reference-preview.not-found { 111 | background-color: #ffebee; 112 | border-color: #ffcdd2; 113 | color: #c62828; 114 | } 115 | 116 | /* Code formatting */ 117 | code { 118 | font-family: 'Courier New', Courier, monospace; 119 | font-size: 0.9em; 120 | background-color: #f5f5f5; 121 | padding: 2px 4px; 122 | border-radius: 3px; 123 | } 124 | 125 | code.tool-reference { 126 | background-color: #e3f2fd; 127 | color: #1976d2; 128 | border: 1px solid #bbdefb; 129 | } 130 | 131 | code.tool-reference.handoff { 132 | background-color: rgba(0, 0, 0, 0.04); 133 | color: rgba(0, 0, 0, 0.7); 134 | border: 1px solid rgba(0, 0, 0, 0.1); 135 | } 136 | -------------------------------------------------------------------------------- /src/frontend/components/shared/Spinner.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import { CircularProgress, Box, SxProps, Theme } from '@mui/material'; 5 | 6 | interface SpinnerProps { 7 | size?: 'small' | 'medium' | 'large'; 8 | color?: 'primary' | 'secondary' | 'white'; 9 | className?: string; 10 | sx?: SxProps; 11 | } 12 | 13 | /** 14 | * A reusable spinner component for loading states 15 | */ 16 | const Spinner: React.FC = ({ 17 | size = 'medium', 18 | color = 'primary', 19 | className = '', 20 | sx = {} 21 | }) => { 22 | // Determine size in pixels 23 | const sizeInPx = { 24 | small: 16, 25 | medium: 24, 26 | large: 36 27 | }[size]; 28 | 29 | // Determine color for MUI 30 | const muiColor = color === 'white' ? 'inherit' : color; 31 | 32 | // Custom styles for white color 33 | const customSx = color === 'white' ? { 34 | color: '#fff', 35 | ...sx 36 | } : sx; 37 | 38 | return ( 39 | 49 | 71 | ); 72 | }; 73 | 74 | export default Spinner; 75 | -------------------------------------------------------------------------------- /src/frontend/contexts/index.ts: -------------------------------------------------------------------------------- 1 | export interface ThemeContextType { 2 | isDarkMode: boolean; 3 | toggleTheme: () => void; 4 | } 5 | 6 | export interface StorageContextType { 7 | setKey: (key: string) => Promise; 8 | changeKey: (oldKey: string, newKey: string) => Promise; 9 | verifyKey: (key: string) => Promise; 10 | isEncryptionInitialized: () => Promise; 11 | isUserEncryptionEnabled: () => Promise; 12 | isLoading: boolean; 13 | } 14 | -------------------------------------------------------------------------------- /src/frontend/hooks/useKeyPress.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect } from 'react'; 4 | import { createLogger } from '@/utils/logger'; 5 | 6 | const log = createLogger('frontend/hooks/useKeyPress'); 7 | 8 | export function useKeyPress(targetKey: string): boolean { 9 | log.debug(`Initializing useKeyPress hook for key: ${targetKey}`); 10 | // State for keeping track of whether key is pressed 11 | const [keyPressed, setKeyPressed] = useState(false); 12 | 13 | // If pressed key is our target key then set to true 14 | const downHandler = ({ key }: KeyboardEvent): void => { 15 | if (key === targetKey) { 16 | log.debug(`Key pressed: ${key}`); 17 | setKeyPressed(true); 18 | } 19 | }; 20 | 21 | // If released key is our target key then set to false 22 | const upHandler = ({ key }: KeyboardEvent): void => { 23 | if (key === targetKey) { 24 | log.debug(`Key released: ${key}`); 25 | setKeyPressed(false); 26 | } 27 | }; 28 | 29 | // Add event listeners 30 | useEffect(() => { 31 | log.debug(`Setting up event listeners for key: ${targetKey}`); 32 | window.addEventListener('keydown', downHandler); 33 | window.addEventListener('keyup', upHandler); 34 | 35 | // Remove event listeners on cleanup 36 | return () => { 37 | log.debug(`Cleaning up event listeners for key: ${targetKey}`); 38 | window.removeEventListener('keydown', downHandler); 39 | window.removeEventListener('keyup', upHandler); 40 | }; 41 | }, [targetKey]); // Only re-run if targetKey changes 42 | 43 | return keyPressed; 44 | } 45 | 46 | export default useKeyPress; 47 | -------------------------------------------------------------------------------- /src/frontend/services/transcription/client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | // This file allows us to explicitly mark the transformers library imports as client-side only 4 | // and ensures proper module loading 5 | 6 | import React, { useEffect, useState } from 'react'; 7 | import { createLogger } from '@/utils/logger'; 8 | 9 | const log = createLogger('frontend/services/transcription/client'); 10 | 11 | export function useTransformersAvailability() { 12 | const [isAvailable, setIsAvailable] = useState(null); 13 | const [isLoading, setIsLoading] = useState(false); 14 | const [error, setError] = useState(null); 15 | 16 | useEffect(() => { 17 | // Only run in browser 18 | if (typeof window === 'undefined') return; 19 | 20 | async function checkAvailability() { 21 | setIsLoading(true); 22 | try { 23 | const { pipeline } = await import('@xenova/transformers'); 24 | setIsAvailable(true); 25 | log.debug('Transformers library is available'); 26 | } catch (err) { 27 | log.error('Error loading transformers library:', err); 28 | setError(`Failed to load transformers library: ${err}`); 29 | setIsAvailable(false); 30 | } finally { 31 | setIsLoading(false); 32 | } 33 | } 34 | 35 | checkAvailability(); 36 | }, []); 37 | 38 | return { isAvailable, isLoading, error }; 39 | } 40 | 41 | // Can be used to preload the transformers library 42 | export function TransformersPreloader() { 43 | const { isAvailable, isLoading, error } = useTransformersAvailability(); 44 | 45 | return ( 46 | <> 47 | {/* This component doesn't render anything visible, 48 | it just ensures the library is loaded */} 49 | 50 | ); 51 | } -------------------------------------------------------------------------------- /src/frontend/services/transcription/index.ts: -------------------------------------------------------------------------------- 1 | import { createLogger } from '@/utils/logger'; 2 | import { transcribeWithWebSpeech, checkWebSpeechSupport } from './webSpeech'; 3 | 4 | const log = createLogger('frontend/services/transcription'); 5 | 6 | export interface TranscriptionOptions { 7 | onProgress?: (progress: number) => void; 8 | onStatusChange?: (status: string) => void; 9 | language?: string; 10 | } 11 | 12 | export interface TranscriptionResult { 13 | text: string; 14 | success: boolean; 15 | error?: string; 16 | engine?: 'webspeech'; 17 | } 18 | 19 | /** 20 | * Transcribes audio data to text using Web Speech API 21 | */ 22 | // Check if Web Speech API is supported 23 | export const isSpeechSupported = checkWebSpeechSupport; 24 | 25 | /** 26 | * Use this function to check if speech recognition is available 27 | * on the current browser before attempting transcription 28 | */ 29 | export function checkSpeechSupport() { 30 | return checkWebSpeechSupport(); 31 | } 32 | 33 | export async function transcribe( 34 | audioBlob: Blob, 35 | options: TranscriptionOptions = {} 36 | ): Promise { 37 | // Check if we're in a browser environment 38 | if (typeof window === 'undefined') { 39 | return { 40 | text: 'Speech recognition is only available in browser environments', 41 | success: false, 42 | error: 'Server-side transcription is not supported' 43 | }; 44 | } 45 | 46 | const { 47 | onProgress, 48 | onStatusChange, 49 | language 50 | } = options; 51 | 52 | try { 53 | log.debug('Starting transcription service'); 54 | 55 | if (onStatusChange) { 56 | onStatusChange('Initializing transcription...'); 57 | } 58 | 59 | // Check if Web Speech API is supported 60 | const webSpeechStatus = checkWebSpeechSupport(); 61 | if (!webSpeechStatus.supported) { 62 | throw new Error('Web Speech API is not supported in this browser'); 63 | } 64 | 65 | if (onStatusChange) { 66 | onStatusChange('Using browser speech recognition...'); 67 | } 68 | 69 | // Using Web Speech API 70 | const text = await transcribeWithWebSpeech(audioBlob, { 71 | language, 72 | onInterimResult: (interim) => { 73 | if (onStatusChange) { 74 | onStatusChange(`Transcribing: ${interim}`); 75 | } 76 | 77 | if (onProgress) { 78 | // Simulate progress for Web Speech API (doesn't have real progress) 79 | onProgress(50); 80 | } 81 | } 82 | }); 83 | 84 | if (onStatusChange) { 85 | onStatusChange('Transcription completed'); 86 | } 87 | 88 | log.debug('Web Speech API transcription completed successfully', { textLength: text.length }); 89 | 90 | return { 91 | text, 92 | success: true, 93 | engine: 'webspeech' 94 | }; 95 | } catch (error) { 96 | log.error('Transcription failed', { error }); 97 | 98 | if (onStatusChange) { 99 | onStatusChange('Transcription failed'); 100 | } 101 | 102 | return { 103 | text: 'Transcription failed. Please check if your browser supports speech recognition.', 104 | success: false, 105 | error: error instanceof Error ? error.message : String(error) 106 | }; 107 | } 108 | } -------------------------------------------------------------------------------- /src/frontend/types/flow/custom-events.d.ts: -------------------------------------------------------------------------------- 1 | // Custom event type definitions 2 | export interface EditNodeEventDetail { 3 | nodeId: string; 4 | } 5 | 6 | interface EditNodeEvent extends CustomEvent { 7 | detail: EditNodeEventDetail; 8 | } 9 | 10 | declare global { 11 | interface DocumentEventMap { 12 | 'editNode': EditNodeEvent; 13 | } 14 | } 15 | 16 | export {}; 17 | -------------------------------------------------------------------------------- /src/frontend/types/flow/flow.ts: -------------------------------------------------------------------------------- 1 | import { Node, Edge } from '@xyflow/react'; 2 | 3 | export interface FlowNode extends Node { 4 | data: { 5 | label: string; 6 | type: string; 7 | description?: string; 8 | properties?: Record; 9 | }; 10 | selected?: boolean; 11 | } 12 | 13 | export interface Flow { 14 | id: string; 15 | name: string; 16 | nodes: FlowNode[]; 17 | edges: Edge[]; 18 | input?: NodeType; 19 | } 20 | 21 | export type NodeType = 'start' | 'process' | 'finish' | 'mcp'; 22 | 23 | export interface FlowContextType { 24 | flows: Flow[]; 25 | selectedFlow: Flow | null; 26 | addFlow: (flow: Flow) => void; 27 | updateFlow: (flow: Flow) => void; 28 | deleteFlow: (id: string) => void; 29 | selectFlow: (id: string) => void; 30 | } 31 | -------------------------------------------------------------------------------- /src/frontend/types/model/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mario-andreschak/FLUJO/1e6a2d6a77f012a15162419eeb7f9d07fa95d060/src/frontend/types/model/index.ts -------------------------------------------------------------------------------- /src/frontend/utils/README.md: -------------------------------------------------------------------------------- 1 | # Frontend Utilities 2 | 3 | This directory contains utility functions and hooks for the frontend. 4 | 5 | ## Theme Utilities 6 | 7 | The theme utilities provide a consistent way to handle theme-aware styling across the application. 8 | 9 | ### Available Utilities 10 | 11 | - `useThemeUtils()`: A hook that provides theme-aware utility functions 12 | - `getThemeValue(lightValue, darkValue)`: Returns the appropriate value based on the current theme 13 | - `isDarkMode`: Boolean indicating if dark mode is currently active 14 | 15 | - `getCssVar(variableName)`: Returns a CSS variable reference (e.g., `var(--background)`) 16 | 17 | - `applyThemeStyles(element, isDarkMode)`: Applies theme-specific styles to an HTML element 18 | 19 | ### Usage Examples 20 | 21 | ```tsx 22 | // Using useThemeUtils 23 | import { useThemeUtils } from '@/frontend/utils'; 24 | 25 | function MyComponent() { 26 | const { getThemeValue, isDarkMode } = useThemeUtils(); 27 | 28 | // Get a theme-specific value 29 | const backgroundColor = getThemeValue('#FFFFFF', '#2C3E50'); 30 | 31 | return ( 32 |
33 | {isDarkMode ? 'Dark Mode' : 'Light Mode'} 34 |
35 | ); 36 | } 37 | 38 | // Using getCssVar 39 | import { getCssVar } from '@/frontend/utils'; 40 | 41 | function AnotherComponent() { 42 | return ( 43 |
47 | Using CSS Variables 48 |
49 | ); 50 | } 51 | ``` 52 | 53 | ## Theme Context 54 | 55 | For direct access to the theme state and toggle function, you can use the `useTheme` hook from the ThemeContext: 56 | 57 | ```tsx 58 | import { useTheme } from '@/frontend/contexts/ThemeContext'; 59 | 60 | function ThemeToggleButton() { 61 | const { isDarkMode, toggleTheme } = useTheme(); 62 | 63 | return ( 64 | 67 | ); 68 | } 69 | ``` 70 | 71 | ## CSS Variables 72 | 73 | The application defines the following CSS variables for theme-aware styling: 74 | 75 | ### Light Theme (default) 76 | ```css 77 | --background: #FFFFFF; 78 | --foreground: #2C3E50; 79 | --paper-background: #F5F6FA; 80 | --text-secondary: #7F8C8D; 81 | ``` 82 | 83 | ### Dark Theme 84 | ```css 85 | --background: #2C3E50; 86 | --foreground: #ECF0F1; 87 | --paper-background: #34495E; 88 | --text-secondary: #BDC3C7; 89 | ``` 90 | 91 | These variables are automatically applied based on the current theme. 92 | -------------------------------------------------------------------------------- /src/frontend/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './theme'; 2 | -------------------------------------------------------------------------------- /src/frontend/utils/muiTheme.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { createTheme, Theme } from '@mui/material/styles'; 4 | import { themeColors } from './theme'; 5 | import { PaletteMode } from '@mui/material'; 6 | 7 | /** 8 | * Create a MUI theme based on the current mode (light/dark) 9 | * @param mode The current theme mode ('light' or 'dark') 10 | * @returns A configured MUI theme 11 | */ 12 | export function createAppTheme(mode: PaletteMode): Theme { 13 | const colors = mode === 'dark' ? themeColors.dark : themeColors.light; 14 | 15 | return createTheme({ 16 | palette: { 17 | mode, 18 | primary: { 19 | main: '#007bff', 20 | }, 21 | secondary: { 22 | main: '#6c757d', 23 | }, 24 | error: { 25 | main: mode === 'dark' ? '#f87171' : '#dc2626', 26 | light: mode === 'dark' ? '#5a3333' : '#fecaca', 27 | dark: mode === 'dark' ? '#3a2222' : '#b91c1c', 28 | }, 29 | warning: { 30 | main: '#f59e0b', 31 | }, 32 | info: { 33 | main: '#3b82f6', 34 | }, 35 | success: { 36 | main: mode === 'dark' ? '#4ade80' : '#16a34a', 37 | }, 38 | background: { 39 | default: colors.background, 40 | paper: colors.paperBackground, 41 | }, 42 | text: { 43 | primary: colors.foreground, 44 | secondary: colors.textSecondary, 45 | }, 46 | }, 47 | typography: { 48 | fontFamily: 'var(--font-geist-sans), Arial, sans-serif', 49 | h1: { 50 | fontWeight: 700, 51 | }, 52 | h2: { 53 | fontWeight: 700, 54 | }, 55 | h3: { 56 | fontWeight: 600, 57 | }, 58 | h4: { 59 | fontWeight: 600, 60 | }, 61 | h5: { 62 | fontWeight: 600, 63 | }, 64 | h6: { 65 | fontWeight: 600, 66 | }, 67 | }, 68 | components: { 69 | MuiButton: { 70 | styleOverrides: { 71 | root: { 72 | textTransform: 'none', 73 | borderRadius: '0.375rem', 74 | }, 75 | }, 76 | }, 77 | MuiPaper: { 78 | styleOverrides: { 79 | root: { 80 | borderRadius: '0.5rem', 81 | }, 82 | }, 83 | }, 84 | MuiTextField: { 85 | styleOverrides: { 86 | root: { 87 | '& .MuiOutlinedInput-root': { 88 | borderRadius: '0.375rem', 89 | }, 90 | }, 91 | }, 92 | }, 93 | MuiCard: { 94 | styleOverrides: { 95 | root: { 96 | borderRadius: '0.5rem', 97 | boxShadow: mode === 'dark' 98 | ? '0 4px 6px -1px rgba(0, 0, 0, 0.2), 0 2px 4px -1px rgba(0, 0, 0, 0.1)' 99 | : '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)', 100 | }, 101 | }, 102 | }, 103 | }, 104 | }); 105 | } 106 | 107 | /** 108 | * Hook to get the current MUI theme based on the app's theme context 109 | * This should be used in a ThemeProvider component 110 | */ 111 | export function getThemeOptions(mode: PaletteMode): Theme { 112 | return createAppTheme(mode); 113 | } 114 | -------------------------------------------------------------------------------- /src/shared/types/chat.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Defines the structure for the metadata object sent with chat completion requests, 3 | * particularly when using Flujo features. 4 | */ 5 | export interface ChatCompletionMetadata { 6 | /** 7 | * Indicates if the request is part of a Flujo execution. 8 | * Expected value: "true" 9 | */ 10 | flujo?: "true"; 11 | 12 | /** 13 | * The ID of the conversation this request belongs to, allowing state resumption. 14 | */ 15 | conversationId?: string; 16 | 17 | /** 18 | * Indicates if tool calls within a Flujo execution require user approval before proceeding. 19 | * Expected value: "true" 20 | */ 21 | requireApproval?: "true"; 22 | 23 | /** 24 | * Indicates if the request should be executed in debug mode (step-by-step). 25 | * Expected value: "true" 26 | */ 27 | flujodebug?: "true"; 28 | 29 | /** 30 | * The ID of the process node to start execution from. 31 | * Used when editing messages to resume execution from a specific node. 32 | */ 33 | processNodeId?: string; 34 | } 35 | 36 | import OpenAI from 'openai'; 37 | 38 | /** 39 | * Extends OpenAI's chat completion message parameter type to include additional fields 40 | * needed for Flujo's chat functionality. 41 | */ 42 | export type FlujoChatMessage = OpenAI.ChatCompletionMessageParam & { 43 | /** Unique identifier for the message */ 44 | id: string; 45 | 46 | /** Timestamp in milliseconds since epoch when the message was created/added */ 47 | timestamp: number; 48 | 49 | /** Flag to indicate if the message should be excluded from processing */ 50 | disabled?: boolean; 51 | 52 | /** The ID of the process node that generated or handled this message */ 53 | processNodeId?: string; 54 | }; 55 | -------------------------------------------------------------------------------- /src/shared/types/constants.ts: -------------------------------------------------------------------------------- 1 | export const MASKED_STRING = 'masked:********'; 2 | 3 | export const ToolCallDefaultPatternJSON = '{"tool": "TOOL_NAME", "parameters": {"PARAM_NAME1":"PARAM_VALUE1$", "$PARAM_NAME2":"$PARAM_VALUE2$", "...": "..." }}' 4 | export const ToolCallDefaultPatternXML = 'PARAM_VALUE1PARAM_VALUE1' 5 | 6 | export const ReasoningDefaultPatternJSON = '{"think": "THINK_TEXT"}' 7 | export const ReasoningDefaultPatternXML = 'THINK_TEXT' 8 | 9 | export const ReasoningDefaultPattern = ReasoningDefaultPatternJSON 10 | export const ToolCallDefaultPattern = ToolCallDefaultPatternJSON 11 | 12 | 13 | export const xmlFindPattern = '<([\w-]+)>(?:.+)<\/(\{1})>' -------------------------------------------------------------------------------- /src/shared/types/flow/flow.ts: -------------------------------------------------------------------------------- 1 | import { Node, Edge } from '@xyflow/react'; 2 | 3 | export interface HistoryEntry { 4 | nodes: FlowNode[]; 5 | edges: Edge[]; 6 | } 7 | 8 | export interface FlowNode extends Node { 9 | data: { 10 | label: string; 11 | type: string; 12 | description?: string; 13 | properties?: Record; 14 | }; 15 | selected?: boolean; 16 | } 17 | 18 | export interface Flow { 19 | id: string; 20 | name: string; 21 | nodes: FlowNode[]; 22 | edges: Edge[]; 23 | input?: NodeType; 24 | } 25 | 26 | export type NodeType = 'start' | 'process' | 'finish' | 'mcp'; 27 | 28 | export interface FlowContextType { 29 | flows: Flow[]; 30 | selectedFlow: Flow | null; 31 | addFlow: (flow: Flow) => void; 32 | updateFlow: (flow: Flow) => void; 33 | deleteFlow: (id: string) => void; 34 | selectFlow: (id: string) => void; 35 | } 36 | -------------------------------------------------------------------------------- /src/shared/types/flow/index.ts: -------------------------------------------------------------------------------- 1 | export * from './flow'; 2 | export * from './response'; 3 | -------------------------------------------------------------------------------- /src/shared/types/flow/response.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from 'openai'; 2 | import { Flow } from './flow'; 3 | 4 | /** 5 | * Base response interface for flow service operations 6 | */ 7 | export interface FlowServiceResponse { 8 | success: boolean; 9 | error?: string; 10 | } 11 | 12 | /** 13 | * Response interface for operations that return a flow 14 | */ 15 | export interface FlowOperationResponse extends FlowServiceResponse { 16 | flow?: Flow; 17 | } 18 | 19 | /** 20 | * Response interface for operations that return a list of flows 21 | */ 22 | export interface FlowListResponse extends FlowServiceResponse { 23 | flows?: Flow[]; 24 | } 25 | 26 | // /** 27 | // * OpenAI-compatible message format 28 | // */ 29 | // export interface ChatCompletionMessage { 30 | // role: string; 31 | // content: string; 32 | // name?: string; 33 | // tool_calls?: ToolCall[]; 34 | // } 35 | 36 | // /** 37 | // * Tool call structure for OpenAI messages 38 | // */ 39 | // export interface ToolCall { 40 | // id: string; 41 | // function: { 42 | // name: string; 43 | // arguments: string; 44 | // }; 45 | // } 46 | 47 | /** 48 | * Model response information 49 | */ 50 | export interface ModelResponse { 51 | success: boolean; 52 | content?: string; 53 | error?: string; 54 | errorDetails?: Record; 55 | fullResponse?: OpenAI.ChatCompletionStoreMessage; 56 | } 57 | 58 | /** 59 | * Error result with success flag 60 | */ 61 | export interface ErrorResult { 62 | success: false; 63 | error: string; 64 | errorDetails?: Record; 65 | } 66 | 67 | /** 68 | * Success result with success flag 69 | */ 70 | export interface SuccessResult { 71 | success: true; 72 | [key: string]: unknown; 73 | } 74 | 75 | /** 76 | * Message result 77 | */ 78 | export interface MessageResult { 79 | message: string; 80 | [key: string]: unknown; 81 | } 82 | 83 | /** 84 | * Node execution tracker entry 85 | */ 86 | export interface NodeExecutionTrackerEntry { 87 | nodeType: string; 88 | timestamp: string; 89 | // Properties for different node types 90 | error?: string; 91 | errorDetails?: Record; 92 | content?: string; 93 | toolCallsCount?: number; 94 | toolName?: string; 95 | toolArgs?: Record; 96 | result?: string; 97 | nodeId?: string; 98 | nodeName?: string; 99 | modelDisplayName?: string; 100 | modelTechnicalName?: string; 101 | allowedTools?: string; 102 | } 103 | 104 | /** 105 | * Response interface for flow execution operations 106 | */ 107 | export interface FlowExecutionResponse extends FlowServiceResponse { 108 | result?: string | ErrorResult | SuccessResult | MessageResult; 109 | messages: OpenAI.ChatCompletionMessageParam[]; 110 | executionTime: number; 111 | nodeExecutionTracker: NodeExecutionTrackerEntry[]; 112 | // Additional properties used in chatCompletionService 113 | retryAttempts?: number; 114 | modelResponse?: ModelResponse; 115 | toolCalls?: Array<{ 116 | name: string; 117 | args: Record; 118 | id: string; 119 | result: string; 120 | }>; 121 | // Conversation ID for stateful execution 122 | conversationId?: string; 123 | } 124 | -------------------------------------------------------------------------------- /src/shared/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mcp/mcp'; 2 | export * from './storage'; 3 | export * from './flow/flow'; 4 | export * from './model/model'; 5 | export * from './chat'; // Add export for chat types 6 | -------------------------------------------------------------------------------- /src/shared/types/mcp/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mcp'; -------------------------------------------------------------------------------- /src/shared/types/mcp/mcp.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { ToolSchema } from '@modelcontextprotocol/sdk/types.js'; 3 | import { StdioServerParameters } from '@modelcontextprotocol/sdk/client/stdio.js'; 4 | import { SSEClientTransportOptions } from '@modelcontextprotocol/sdk/client/sse.js'; 5 | import { StreamableHTTPClientTransportOptions } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; 6 | 7 | // Constants 8 | export const SERVER_DIR_PREFIX = 'mcp-servers'; 9 | 10 | // Types 11 | export type EnvVarValue = string | { 12 | value: string; 13 | metadata: { 14 | isSecret: boolean 15 | } 16 | }; 17 | 18 | export type MCPManagerConfig = { 19 | name: string; 20 | disabled: boolean; 21 | autoApprove: string[]; 22 | rootPath: string; 23 | env: Record 24 | _buildCommand: string; 25 | _installCommand: string; 26 | } 27 | 28 | export type MCPStdioConfig = StdioServerParameters & MCPManagerConfig & { 29 | transport: 'stdio'; 30 | }; 31 | 32 | export type MCPSSEConfig = SSEClientTransportOptions & MCPManagerConfig & { 33 | transport: 'sse'; 34 | serverUrl: string 35 | }; 36 | 37 | export type MCPStreamableConfig = StreamableHTTPClientTransportOptions & MCPManagerConfig & { 38 | transport: 'streamable'; 39 | serverUrl: string 40 | }; 41 | 42 | export type MCPWebSocketConfig = MCPManagerConfig & { 43 | transport: 'websocket'; 44 | websocketUrl: string; 45 | }; 46 | 47 | export type MCPDockerConfig = MCPManagerConfig & { 48 | transport: 'docker'; 49 | image: string; // Docker image name (e.g., 'ghcr.io/github/github-mcp-server') 50 | containerName?: string; // Optional custom container name 51 | transportMethod: 'stdio' | 'websocket'; // How to communicate with the container 52 | websocketPort?: number; // Port for websocket if using websocket transport 53 | volumes?: string[]; // Optional volume mounts 54 | networkMode?: string; // Optional network mode 55 | extraArgs?: string[]; // Additional docker run arguments 56 | }; 57 | 58 | export type MCPServerConfig = MCPStdioConfig | MCPWebSocketConfig | MCPDockerConfig | MCPSSEConfig | MCPStreamableConfig; 59 | 60 | export interface MCPServiceResponse { 61 | success: boolean; 62 | data?: T; 63 | error?: string; 64 | statusCode?: number; 65 | progressToken?: string; 66 | errorType?: string; 67 | toolName?: string; 68 | timeout?: number; 69 | } 70 | 71 | // Using the official type from MCP SDK 72 | export type MCPToolResponse = z.infer; 73 | 74 | export interface MCPConnectionAttempt { 75 | requestId: string; 76 | timestamp: number; 77 | status: 'pending' | 'success' | 'failed'; 78 | error?: string; 79 | } 80 | 81 | // Define ServerState as an intersection type 82 | export type MCPServerState = MCPServerConfig & { 83 | status: 'connected' | 'disconnected' | 'error' | 'connecting' | 'initialization'; 84 | tools: Array<{ 85 | name: string; 86 | description: string; 87 | inputSchema: Record; 88 | }>; 89 | error?: string; 90 | stderrOutput?: string; 91 | containerName?: string; // Docker container name (auto-generated or custom) 92 | }; 93 | -------------------------------------------------------------------------------- /src/shared/types/model/index.ts: -------------------------------------------------------------------------------- 1 | export * from './model'; 2 | export * from './response'; 3 | export * from './provider'; 4 | -------------------------------------------------------------------------------- /src/shared/types/model/model.ts: -------------------------------------------------------------------------------- 1 | import { ModelProvider } from './provider'; 2 | 3 | export interface Model { 4 | id: string; 5 | name: string; 6 | displayName?: string; 7 | description?: string; 8 | ApiKey: string; 9 | baseUrl?: string; 10 | provider?: ModelProvider; 11 | promptTemplate?: string; 12 | // New fields 13 | reasoningSchema?: string; 14 | temperature?: string; 15 | functionCallingSchema?: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/shared/types/model/provider.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Supported model providers 3 | */ 4 | export type ModelProvider = 5 | | 'openai' 6 | | 'openrouter' 7 | | 'anthropic' 8 | | 'gemini' 9 | | 'mistral' 10 | | 'xai' 11 | | 'ollama'; 12 | 13 | /** 14 | * Provider information mapping 15 | */ 16 | export interface ProviderInfo { 17 | id: ModelProvider; 18 | label: string; 19 | baseUrl: string; 20 | } 21 | 22 | /** 23 | * Map of providers with their display labels and base URLs 24 | */ 25 | export const PROVIDER_INFO: Record> = { 26 | openai: { 27 | label: 'OpenAI', 28 | baseUrl: 'https://api.openai.com/v1' 29 | }, 30 | openrouter: { 31 | label: 'OpenRouter', 32 | baseUrl: 'https://openrouter.ai/api/v1' 33 | }, 34 | xai: { 35 | label: 'X.ai', 36 | baseUrl: 'https://api.x.ai/v1' 37 | }, 38 | gemini: { 39 | label: 'Gemini', 40 | baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai/' 41 | }, 42 | anthropic: { 43 | label: 'Anthropic', 44 | baseUrl: 'https://api.anthropic.com/v1/' 45 | }, 46 | mistral: { 47 | label: 'Mistral', 48 | baseUrl: 'https://api.mistral.ai/v1' 49 | }, 50 | ollama: { 51 | label: 'Ollama', 52 | baseUrl: 'http://localhost:11434/v1' 53 | } 54 | }; 55 | 56 | /** 57 | * Helper function to get all providers as an array 58 | */ 59 | export function getProvidersArray(): ProviderInfo[] { 60 | return Object.entries(PROVIDER_INFO).map(([id, info]) => ({ 61 | id: id as ModelProvider, 62 | ...info 63 | })); 64 | } 65 | -------------------------------------------------------------------------------- /src/shared/types/model/response.ts: -------------------------------------------------------------------------------- 1 | import { Model } from './model'; 2 | import OpenAI from 'openai'; 3 | 4 | /** 5 | * Base response interface for model service operations 6 | */ 7 | export interface ModelServiceResponse { 8 | success: boolean; 9 | error?: string; 10 | } 11 | 12 | /** 13 | * Response for operations that return a list of models 14 | */ 15 | export interface ModelListResponse extends ModelServiceResponse { 16 | models?: Model[]; 17 | } 18 | 19 | /** 20 | * Response for operations that return a single model 21 | */ 22 | export interface ModelOperationResponse extends ModelServiceResponse { 23 | model?: Model; 24 | } 25 | 26 | /** 27 | * Response for completion generation operations 28 | * Aligned with OpenAI's response format 29 | */ 30 | export interface CompletionResponse extends ModelServiceResponse { 31 | content?: string; 32 | fullResponse?: OpenAI.ChatCompletion; // Use OpenAI type instead of any 33 | toolCalls?: Array<{ 34 | name: string; 35 | args: Record; // More specific type than any 36 | id: string; 37 | result: string; 38 | }>; 39 | newMessages?: OpenAI.ChatCompletionMessageParam[]; // Use OpenAI type 40 | errorDetails?: { 41 | message: string; 42 | name?: string; 43 | type?: string; // Added to match OpenAI error format 44 | code?: string; // Added to match OpenAI error format 45 | param?: string; // Added to match OpenAI error format 46 | status?: number; // HTTP status code 47 | stack?: string; 48 | }; 49 | } 50 | 51 | /** 52 | * Interface for normalized model data from providers 53 | */ 54 | export interface NormalizedModel { 55 | id: string; 56 | name: string; 57 | description?: string; 58 | } 59 | -------------------------------------------------------------------------------- /src/shared/types/storage/index.ts: -------------------------------------------------------------------------------- 1 | export * from './storage'; -------------------------------------------------------------------------------- /src/shared/types/storage/storage.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Enum for storage keys used in the application 3 | */ 4 | export enum StorageKey { 5 | MODELS = 'models', 6 | FLOWS = 'flows', 7 | CHAT_HISTORY = 'history', 8 | THEME = 'theme', 9 | ENCRYPTION_KEY = 'encryption_key', 10 | MCP_SERVERS = 'mcp_servers', 11 | GLOBAL_ENV_VARS = 'global_env_vars', 12 | CURRENT_CONVERSATION_ID = 'current_conversation_id', 13 | SELECTED_FLOW_ID = 'selected_flow_id', 14 | SPEECH_SETTINGS = 'speech_settings' 15 | } 16 | 17 | export const StorageKeys = { 18 | MODELS: StorageKey.MODELS, 19 | FLOWS: StorageKey.FLOWS, 20 | CHAT_HISTORY: StorageKey.CHAT_HISTORY, 21 | THEME: StorageKey.THEME, 22 | ENCRYPTION_KEY: StorageKey.ENCRYPTION_KEY, 23 | MCP_SERVERS: StorageKey.MCP_SERVERS, 24 | GLOBAL_ENV_VARS: StorageKey.GLOBAL_ENV_VARS, 25 | CURRENT_CONVERSATION_ID: StorageKey.CURRENT_CONVERSATION_ID, 26 | SELECTED_FLOW_ID: StorageKey.SELECTED_FLOW_ID, 27 | SPEECH_SETTINGS: StorageKey.SPEECH_SETTINGS, 28 | } as const; 29 | 30 | /** 31 | * Speech recognition settings interface 32 | */ 33 | export interface SpeechSettings { 34 | enabled: boolean; 35 | language?: string; 36 | } 37 | 38 | /** 39 | * Settings interface containing all application settings 40 | */ 41 | export interface Settings { 42 | speech: SpeechSettings; 43 | } 44 | -------------------------------------------------------------------------------- /src/utils/encryption/index.ts: -------------------------------------------------------------------------------- 1 | import { createLogger } from '@/utils/logger'; 2 | 3 | // Create a logger instance for this file 4 | const log = createLogger('utils/encryption/index'); 5 | 6 | // Re-export the secure encryption functions 7 | export { 8 | encryptWithPassword as encrypt, 9 | decryptWithPassword as decrypt, 10 | initializeEncryption, 11 | initializeDefaultEncryption, 12 | changeEncryptionPassword, 13 | verifyPassword, 14 | isEncryptionInitialized, 15 | isUserEncryptionEnabled, 16 | getEncryptionType 17 | } from './secure'; 18 | 19 | // Compatibility functions for API key encryption 20 | export async function encryptApiKey(value: string, key?: string): Promise { 21 | log.debug('encryptApiKey: Entering method'); 22 | try { 23 | const { encryptWithPassword, initializeDefaultEncryption, isEncryptionInitialized } = await import('./secure'); 24 | 25 | // Ensure encryption is initialized 26 | const initialized = await isEncryptionInitialized(); 27 | if (!initialized) { 28 | await initializeDefaultEncryption(); 29 | } 30 | 31 | const result = await encryptWithPassword(value, key); 32 | if (result === null) { 33 | throw new Error('Encryption failed'); 34 | } 35 | 36 | return result; 37 | } catch (error) { 38 | log.error('encryptApiKey: Failed to encrypt API key:', error); 39 | // Instead of returning plain text, prefix with 'encrypted:' to indicate it should be encrypted 40 | // This will help identify values that failed encryption but should be encrypted 41 | return `encrypted_failed:${value}`; 42 | } 43 | } 44 | 45 | export async function decryptApiKey(encryptedValue: string, key?: string): Promise { 46 | log.debug('decryptApiKey: Entering method'); 47 | try { 48 | // Check if this is a global variable reference 49 | if (encryptedValue && encryptedValue.startsWith('${global:')) { 50 | return encryptedValue; // Return as is, it will be resolved at runtime 51 | } 52 | 53 | // Check if this is a failed encryption marker 54 | if (encryptedValue && encryptedValue.startsWith('encrypted_failed:')) { 55 | // Return asterisks for security 56 | return '********'; 57 | } 58 | 59 | const { decryptWithPassword } = await import('./secure'); 60 | const result = await decryptWithPassword(encryptedValue, key); 61 | 62 | if (result === null) { 63 | // Return the encrypted value for UI display 64 | return '********'; 65 | } 66 | 67 | return result; 68 | } catch (error) { 69 | log.error('decryptApiKey: Failed to decrypt API key:', error); 70 | // Return asterisks for security instead of the original encrypted value 71 | return '********'; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/utils/logger/README.md: -------------------------------------------------------------------------------- 1 | # Logger Utility 2 | 3 | This directory contains a logging utility for consistent logging across the application. 4 | 5 | ## Features 6 | 7 | - Consistent log format with timestamps and file paths 8 | - Multiple log levels (VERBOSE, DEBUG, INFO, WARN, ERROR) 9 | - Support for logging objects and primitive values 10 | - File-specific logger instances with pre-configured paths 11 | - Normalized file paths for consistent logging 12 | - Per-file log level override capability 13 | 14 | ## Usage 15 | 16 | ### Basic Usage 17 | 18 | ```typescript 19 | import { createLogger } from '@/utils/logger'; 20 | 21 | // Create a logger instance for this file 22 | const log = createLogger('path/to/component'); 23 | 24 | // Basic logging 25 | log.verbose('Extremely detailed message'); 26 | log.debug('Debug message'); 27 | log.info('Info message'); 28 | log.warn('Warning message'); 29 | log.error('Error message'); 30 | 31 | // Logging with data 32 | log.debug('Debug message with data', { key: 'value' }); 33 | ``` 34 | 35 | ### Using with Log Level Override 36 | 37 | ```typescript 38 | import { createLogger, LOG_LEVEL } from '@/utils/logger'; 39 | 40 | // Create a logger instance with a custom log level 41 | const log = createLogger('path/to/component', LOG_LEVEL.VERBOSE); 42 | 43 | // This will log even if the global log level is higher 44 | log.verbose('Verbose message that will be shown regardless of global setting'); 45 | log.debug('Debug message'); 46 | 47 | // You can create different loggers with different levels in the same file 48 | const criticalLogger = createLogger('path/to/component/critical', LOG_LEVEL.ERROR); 49 | criticalLogger.error('This error will always be logged'); 50 | ``` 51 | 52 | ## Log Levels 53 | 54 | The logger supports the following log levels: 55 | 56 | - `VERBOSE` (-1): Extremely detailed information for in-depth debugging 57 | - `DEBUG` (0): Detailed information for debugging purposes 58 | - `INFO` (1): General information about application operation 59 | - `WARN` (2): Warning messages that don't prevent the application from working 60 | - `ERROR` (3): Error messages that may prevent the application from working correctly 61 | 62 | The current log level is set in the `features.ts` file. Only messages with a level greater than or equal to the current log level will be displayed. 63 | 64 | ### Overriding Log Level Per File 65 | 66 | You can override the global log level when creating a logger instance: 67 | 68 | ```typescript 69 | import { createLogger, LOG_LEVEL } from '@/utils/logger'; 70 | 71 | // Create a logger with a custom log level 72 | const log = createLogger('path/to/component', LOG_LEVEL.VERBOSE); 73 | 74 | // This will log even if the global log level is higher 75 | log.verbose('Verbose message'); 76 | ``` 77 | 78 | This allows for more granular control over logging in specific parts of your application. 79 | 80 | ## Best Practices 81 | 82 | 1. **Use createLogger**: Create a logger instance at the top of each file with the file's path. 83 | 2. **Be Consistent**: Use the same path format across the application. 84 | 3. **Log Appropriately**: Use the appropriate log level for each message. 85 | 4. **Include Context**: Include relevant context in log messages to make them more useful. 86 | 5. **Avoid Sensitive Data**: Don't log sensitive data like passwords or tokens. 87 | -------------------------------------------------------------------------------- /src/utils/logger/index.ts: -------------------------------------------------------------------------------- 1 | export * from './logger'; 2 | -------------------------------------------------------------------------------- /src/utils/mcp/configparse/types.ts: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { MessageState } from '@/frontend/components/mcp/MCPServerManager/Modals/ServerModal/types'; 4 | import { MCPServerConfig } from '@/shared/types/mcp/mcp'; 5 | 6 | /** 7 | * Result of parsing configuration from a repository 8 | */ 9 | export interface ConfigParseResult { 10 | detected: boolean; 11 | language?: 'typescript' | 'python' | 'java' | 'kotlin' | 'unknown'; 12 | installCommand?: string; 13 | buildCommand?: string; 14 | runCommand?: string; 15 | args?: string[]; 16 | env?: Record; 17 | message?: MessageState; 18 | config?: Partial; 19 | } 20 | 21 | /** 22 | * Options for parsing configuration 23 | */ 24 | export interface ConfigParseOptions { 25 | repoPath: string; 26 | repoName: string; 27 | owner?: string; 28 | } 29 | 30 | /** 31 | * File existence check result 32 | */ 33 | export interface FileExistsResult { 34 | exists: boolean; 35 | content?: string; 36 | } 37 | -------------------------------------------------------------------------------- /src/utils/mcp/configparse/utils.ts: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { FileExistsResult } from './types'; 4 | import { createLogger } from '@/utils/logger'; 5 | 6 | const log = createLogger('utils/mcp/configparse/utils'); 7 | 8 | /** 9 | * Check if a file exists in the repository and optionally read its content 10 | * @param repoPath Path to the repository 11 | * @param filePath Path to the file relative to the repository root 12 | * @param readContent Whether to read the file content if it exists 13 | */ 14 | export async function checkFileExists( 15 | repoPath: string, 16 | filePath: string, 17 | readContent: boolean = false 18 | ): Promise { 19 | try { 20 | log.debug(`Checking if file exists: ${repoPath}/${filePath}`); 21 | 22 | // Construct the path - avoid double slashes if repoPath already ends with a slash 23 | const fullPath = repoPath.endsWith('/') || repoPath.endsWith('\\') 24 | ? `${repoPath}${filePath}` 25 | : `${repoPath}/${filePath}`; 26 | 27 | log.debug(`Constructed full path: ${fullPath}`); 28 | 29 | // Call the server-side API to check if the file exists 30 | const response = await fetch('/api/git', { 31 | method: 'POST', 32 | headers: { 33 | 'Content-Type': 'application/json', 34 | }, 35 | body: JSON.stringify({ 36 | action: 'readFile', 37 | savePath: fullPath, 38 | }), 39 | }); 40 | 41 | if (!response.ok) { 42 | log.debug(`File does not exist: ${repoPath}/${filePath}`); 43 | return { exists: false }; 44 | } 45 | 46 | const result = await response.json(); 47 | 48 | if (!result.content && readContent) { 49 | log.debug(`File exists but is empty: ${repoPath}/${filePath}`); 50 | return { exists: true, content: '' }; 51 | } 52 | 53 | log.debug(`File exists: ${repoPath}/${filePath}`); 54 | return { 55 | exists: true, 56 | content: readContent ? result.content : undefined 57 | }; 58 | } catch (error) { 59 | log.error(`Error checking if file exists: ${repoPath}/${filePath}`, error); 60 | return { exists: false }; 61 | } 62 | } 63 | 64 | /** 65 | * Read a file from the repository 66 | * @param repoPath Path to the repository 67 | * @param filePath Path to the file relative to the repository root 68 | */ 69 | export async function readFile( 70 | repoPath: string, 71 | filePath: string 72 | ): Promise { 73 | try { 74 | const result = await checkFileExists(repoPath, filePath, true); 75 | return result.exists && result.content ? result.content : null; 76 | } catch (error) { 77 | log.error(`Error reading file: ${repoPath}/${filePath}`, error); 78 | return null; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/utils/mcp/index.ts: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | export * from './types'; 4 | export * from './processPathLikeArgument'; 5 | export * from './parseServerConfig'; 6 | export * from './parseServerConfigFromClipboard'; 7 | export * from './configparse'; 8 | -------------------------------------------------------------------------------- /src/utils/mcp/parseServerConfigFromClipboard.ts: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ParsedServerConfig } from "./types"; 4 | import { parseServerConfig } from "./parseServerConfig"; 5 | import { createLogger } from '@/utils/logger'; 6 | 7 | // Create a logger instance for this file 8 | const log = createLogger('utils/mcp/parseServerConfigFromClipboard'); 9 | 10 | /** 11 | * Parse clipboard content to extract MCP server configuration 12 | * @param parseEnvVars Whether to parse environment variables (default: true) 13 | * @param serverName Optional server name to use for path processing 14 | */ 15 | export async function parseServerConfigFromClipboard(parseEnvVars: boolean = true, serverName?: string): Promise { 16 | log.debug('parseServerConfigFromClipboard: Entering method', { parseEnvVars, serverName }); 17 | try { 18 | const clipboardText = await navigator.clipboard.readText(); 19 | return parseServerConfig(clipboardText, parseEnvVars, serverName); 20 | } catch (error) { 21 | log.error('parseServerConfigFromClipboard: Failed to read clipboard:', error); 22 | return { 23 | config: {}, 24 | message: { 25 | type: 'error', 26 | text: 'Failed to read clipboard content.' 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/mcp/processPathLikeArgument.ts: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { createLogger } from '@/utils/logger'; 4 | 5 | // Create a logger instance for this file 6 | const log = createLogger('utils/mcp/processPathLikeArgument'); 7 | 8 | /** 9 | * Process path-like arguments to handle special path patterns 10 | * @param arg The argument string to process 11 | * @param serverName Optional server name to strip from the path 12 | * @returns The processed argument string 13 | */ 14 | export function processPathLikeArgument(arg: string, serverName?: string): string { 15 | log.debug('processPathLikeArgument: Entering method', { arg, serverName }); 16 | // Skip processing if the argument doesn't look like a path 17 | if (!arg || typeof arg !== 'string' || (!arg.includes('/') && !arg.includes('\\'))) { 18 | return arg; 19 | } 20 | 21 | log.debug(`processPathLikeArgument: Processing argument: ${arg}`); 22 | 23 | // Define patterns to match path indicators 24 | const pathPatterns = [ 25 | '/path/to', 26 | 'PATH_TO', 27 | 'path/to', 28 | 'PATH/TO', 29 | '/PATH/TO', 30 | '/PATH_TO', 31 | 'path_to' 32 | ]; 33 | 34 | let result = arg; 35 | 36 | // Check if the argument contains any of the path patterns 37 | for (const pattern of pathPatterns) { 38 | const index = result.indexOf(pattern); 39 | if (index !== -1) { 40 | // Strip everything from the beginning until the end of the pattern 41 | result = result.substring(index + pattern.length); 42 | log.debug(`processPathLikeArgument: Stripped path pattern "${pattern}": ${result}`); 43 | break; 44 | } 45 | } 46 | 47 | // Use the provided server name to strip from the path 48 | if (serverName && result.includes(serverName)) { 49 | // More robust check - handle cases where server name appears in path 50 | const serverPattern = new RegExp(`(^|[\\/\\\\])${serverName}([\\/\\\\]|$)`); 51 | if (serverPattern.test(result)) { 52 | result = result.replace(serverPattern, '$1$2'); 53 | log.debug(`processPathLikeArgument: Removed server name "${serverName}": ${result}`); 54 | 55 | // Clean up any double slashes that might have been created 56 | result = result.replace(/\/\//g, '/'); 57 | } 58 | } 59 | 60 | // If the argument now starts with a /, remove that 61 | if (result.startsWith('/') || result.startsWith('\\')) { 62 | result = result.substring(1); 63 | log.debug(`processPathLikeArgument: Removed leading slash: ${result}`); 64 | } 65 | 66 | // If it's completely empty now, fill with "." 67 | if (!result) { 68 | result = '.'; 69 | log.debug(`processPathLikeArgument: Empty result, replaced with "."`); 70 | } 71 | 72 | log.debug(`processPathLikeArgument: Final result: ${result}`); 73 | return result; 74 | } 75 | -------------------------------------------------------------------------------- /src/utils/mcp/types.ts: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { MCPServerConfig } from "@/shared/types/mcp"; 4 | 5 | export type { MCPServerConfig }; 6 | 7 | export interface ParsedServerConfig { 8 | config: Partial; 9 | message: { type: 'success' | 'error' | 'warning'; text: string } | null; 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/shared.ts: -------------------------------------------------------------------------------- 1 | // Re-export everything from the new structure 2 | // This maintains backward compatibility with existing imports 3 | export * from './shared/index'; 4 | -------------------------------------------------------------------------------- /src/utils/shared/common.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * List of keywords that identify secret environment variables 3 | */ 4 | export const SECRET_ENV_KEYWORDS = ['key', 'secret', 'token', 'password']; 5 | 6 | /** 7 | * Check if an environment variable key should be treated as secret 8 | * @param key The environment variable key to check 9 | * @returns True if the key contains any of the secret keywords 10 | */ 11 | export const isSecretEnvVar = (key: string): boolean => 12 | SECRET_ENV_KEYWORDS.some(keyword => key.toLowerCase().includes(keyword)); 13 | 14 | 15 | export const toolNameInternalRegex = /_-_-_([\w-^}]+)_-_-_([\w-^}]+)/g; 16 | 17 | // Construct the new regex using the source of the first one 18 | // Note the double backslashes needed to escape special characters for the RegExp constructor 19 | export const toolBindingRegex = new RegExp(`\\$\\{${toolNameInternalRegex.source}\\}`, 'g'); 20 | -------------------------------------------------------------------------------- /src/utils/shared/index.ts: -------------------------------------------------------------------------------- 1 | // Export client-safe utilities 2 | export * from './common'; 3 | 4 | // Export server-only utilities 5 | // These will only be available in server components 6 | export * from '../../backend/utils/resolveGlobalVars'; 7 | -------------------------------------------------------------------------------- /src/utils/storage/index.ts: -------------------------------------------------------------------------------- 1 | export * from '../../shared/types/storage'; 2 | 3 | // Only export client functions in a client context 4 | export { 5 | saveItem, 6 | loadItem, 7 | clearItem, 8 | useLocalStorage, 9 | setEncryptionKey, 10 | getEncryptionKey, 11 | } from './frontend'; 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules", "mcp-servers/**/*"] 27 | } 28 | --------------------------------------------------------------------------------