diff --git a/app/w/hooks/use-workflow-execution.ts b/app/w/hooks/use-workflow-execution.ts index 236ec7cef7..fa00d8a846 100644 --- a/app/w/hooks/use-workflow-execution.ts +++ b/app/w/hooks/use-workflow-execution.ts @@ -1,6 +1,7 @@ import { useCallback, useState } from 'react' import { v4 as uuidv4 } from 'uuid' import { useConsoleStore } from '@/stores/console/store' +import { useExecutionStore } from '@/stores/execution/store' import { useNotificationStore } from '@/stores/notifications/store' import { useEnvironmentStore } from '@/stores/settings/environment/store' import { useWorkflowRegistry } from '@/stores/workflow/registry/store' @@ -12,13 +13,13 @@ import { ExecutionResult } from '@/executor/types' import { Serializer } from '@/serializer' export function useWorkflowExecution() { - const [isExecuting, setIsExecuting] = useState(false) - const [executionResult, setExecutionResult] = useState(null) const { blocks, edges, loops } = useWorkflowStore() const { activeWorkflowId } = useWorkflowRegistry() const { addNotification } = useNotificationStore() const { toggleConsole, isOpen } = useConsoleStore() const { getAllVariables } = useEnvironmentStore() + const { isExecuting, setIsExecuting } = useExecutionStore() + const [executionResult, setExecutionResult] = useState(null) const persistLogs = async (logs: any[], executionId: string) => { try { @@ -91,7 +92,17 @@ export function useWorkflowExecution() { const executor = new Executor(workflow, currentBlockStates, envVarValues) const result = await executor.execute(activeWorkflowId) - // Prepare logs for persistence + // Set result and show notification immediately + setExecutionResult(result) + addNotification( + result.success ? 'console' : 'error', + result.success + ? 'Workflow completed successfully' + : `Workflow execution failed: ${result.error}`, + activeWorkflowId + ) + + // Prepare logs for persistence (moved after notification) const blockLogs = (result.logs || []).map((log) => ({ level: log.success ? 'info' : 'error', message: `Block ${log.blockName || log.blockId} (${log.blockType}): ${ @@ -116,30 +127,22 @@ export function useWorkflowExecution() { createdAt: new Date().toISOString(), }) - // Persist all logs + // Persist logs after notification await persistLogs(blockLogs, executionId) - - setExecutionResult(result) - - // Show execution result notification - addNotification( - result.success ? 'console' : 'error', - result.success - ? 'Workflow completed successfully' - : `Workflow execution failed: ${result.error}`, - activeWorkflowId - ) } catch (error: any) { console.error('Workflow Execution Error:', error) const errorMessage = error instanceof Error ? error.message : 'Unknown error' + + // Set error result and show notification immediately setExecutionResult({ success: false, output: { response: {} }, error: errorMessage, logs: [], }) + addNotification('error', `Workflow execution failed: ${errorMessage}`, activeWorkflowId) - // Persist error log + // Persist error log after notification await persistLogs( [ { @@ -151,8 +154,6 @@ export function useWorkflowExecution() { ], executionId ) - - addNotification('error', `Workflow execution failed: ${errorMessage}`, activeWorkflowId) } finally { setIsExecuting(false) } @@ -165,6 +166,8 @@ export function useWorkflowExecution() { isOpen, toggleConsole, getAllVariables, + isExecuting, + setIsExecuting, ]) return { isExecuting, executionResult, handleRunWorkflow } diff --git a/blocks/blocks/function.ts b/blocks/blocks/function.ts index ac736da326..a3f842916f 100644 --- a/blocks/blocks/function.ts +++ b/blocks/blocks/function.ts @@ -17,12 +17,11 @@ export const FunctionBlock: BlockConfig = { }, ], tools: { - access: ['function_execute'], + access: ['webcontainer_execute'], }, inputs: { code: { type: 'string', required: true }, timeout: { type: 'number', required: false }, - memoryLimit: { type: 'number', required: false }, }, outputs: { response: { diff --git a/executor/handlers.ts b/executor/handlers.ts index d52c9ddafb..6136967e51 100644 --- a/executor/handlers.ts +++ b/executor/handlers.ts @@ -460,17 +460,44 @@ export class FunctionBlockHandler implements BlockHandler { inputs: Record, context: ExecutionContext ): Promise { - const tool = getTool(block.config.tool) - if (!tool) { - throw new Error(`Tool not found: ${block.config.tool}`) + // Only try WebContainer in browser environment with direct execution + const isBrowser = typeof window !== 'undefined' + if (isBrowser && window.crossOriginIsolated) { + try { + // Dynamically import WebContainer to prevent server-side import + const { executeCode } = await import('@/lib/webcontainer') + + // Prepare code for execution + const codeContent = Array.isArray(inputs.code) + ? inputs.code.map((c: { content: string }) => c.content).join('\n') + : inputs.code + + // Execute directly in the browser + const result = await executeCode(codeContent, inputs, inputs.timeout || 5000) + + if (!result.success) { + console.warn(`WebContainer API execution failed: ${result.error}`) + throw new Error(result.error || `WebContainer execution failed with no error message`) + } + + return { response: result.output } + } catch (error: any) { + console.warn('WebContainer execution failed, falling back to VM:', error.message) + } } - const result = await executeTool(block.config.tool, inputs, true) - if (!result.success) { - throw new Error(result.error || `Function execution failed with no error message`) - } + // Fall back to VM execution if WebContainer fails or not available + try { + const vmResult = await executeTool('function_execute', inputs, true) - return { response: result.output } + if (!vmResult.success) { + throw new Error(vmResult.error || `Function execution failed with no error message`) + } + + return { response: vmResult.output } + } catch (vmError: any) { + throw new Error(`Function execution failed: ${vmError.message}`) + } } } diff --git a/lib/webcontainer.ts b/lib/webcontainer.ts new file mode 100644 index 0000000000..ba0ad08f8c --- /dev/null +++ b/lib/webcontainer.ts @@ -0,0 +1,323 @@ +import { WebContainer } from '@webcontainer/api' +import { auth } from '@webcontainer/api' + +// Singleton instance of WebContainer +let webcontainerInstance: WebContainer | null = null +let isBooting = false +let bootPromise: Promise | null = null + +/** + * Checks if the browser environment supports WebContainers + */ +function isCrossOriginIsolated(): boolean { + if (typeof window === 'undefined') return false + return !!window.crossOriginIsolated +} + +/** + * Check if we're running in the browser + */ +function isBrowser(): boolean { + return typeof window !== 'undefined' +} + +/** + * Initialize WebContainer authentication + */ +async function initializeAuth() { + if (!isBrowser()) return + + try { + await auth.init({ + clientId: process.env.WEBCONTAINER_CLIENT_ID || '', + scope: '', + }) + } catch (error) { + console.error('Failed to initialize WebContainer auth:', error) + throw error + } +} + +/** + * Initializes a WebContainer instance if one doesn't already exist + * @returns Promise + */ +export async function getWebContainer(): Promise { + // Only attempt WebContainer initialization in browser + if (!isBrowser()) { + throw new Error('WebContainer can only be initialized in browser environments') + } + + // Return existing instance if available + if (webcontainerInstance) { + return webcontainerInstance + } + + // Return existing boot promise if already booting + if (isBooting && bootPromise) { + return bootPromise + } + + // Check if cross-origin isolation is enabled + if (!isCrossOriginIsolated()) { + console.warn('Cross-Origin Isolation is not enabled. WebContainers require COOP/COEP headers.') + throw new Error( + 'WebContainers require cross-origin isolation. Please restart the server for changes to take effect.' + ) + } + + // Set flag and create boot promise + isBooting = true + bootPromise = (async () => { + try { + await initializeAuth() + + // Boot a new WebContainer instance + webcontainerInstance = await WebContainer.boot({ + // COEP configuration + coep: 'require-corp', + workdirName: 'sim-studio', + // Enable debugging + forwardPreviewErrors: true, + }) + + // Set up a basic project with a package.json + await setupBaseFileSystem(webcontainerInstance) + + return webcontainerInstance + } catch (error) { + console.error('WebContainer boot error:', error) + isBooting = false + bootPromise = null + throw error + } + })() + + return bootPromise +} + +/** + * Sets up the base file system for the WebContainer + */ +async function setupBaseFileSystem(container: WebContainer): Promise { + await container.mount({ + 'package.json': { + file: { + contents: JSON.stringify( + { + name: 'sim-studio-execution', + type: 'module', + version: '1.0.0', + description: 'Sim Studio Code Execution Environment', + dependencies: {}, + }, + null, + 2 + ), + }, + }, + 'README.md': { + file: { + contents: + '# Sim Studio Code Execution\n\nThis is a code execution environment for Sim Studio.', + }, + }, + }) +} + +/** + * Parse imports from code and install required packages + */ +async function installDependencies(container: WebContainer, code: string): Promise { + // Simple regex to find import statements + const importRegex = + /import\s+?(?:(?:(?:[\w*\s{},]*)\s+from\s+?)|)(?:(?:"(?:[^"]*)")|(?:'([^']*)'))[^;]*/g + const matches = code.match(importRegex) || [] + + const packages = new Set() + + // Extract package names from import statements + for (const match of matches) { + const packageMatch = match.match(/from\s+['"]([^@\s'"]+)/) + if (packageMatch && packageMatch[1] && !packageMatch[1].startsWith('.')) { + packages.add(packageMatch[1]) + } + } + + if (packages.size > 0) { + // Install each package + const installProcess = await container.spawn('npm', ['install', ...Array.from(packages)]) + const installExit = await installProcess.exit + if (installExit !== 0) { + throw new Error('Failed to install dependencies') + } + } +} + +/** + * Execute code in a WebContainer + */ +export async function executeCode( + code: string, + params: Record = {}, + timeout: number = 5000 +): Promise<{ + success: boolean + output: { + result: any + stdout: string + executionTime: number + } + error?: string +}> { + const startTime = Date.now() + let process: any = null + + try { + // Get or initialize WebContainer + const webcontainer = await getWebContainer() + + // Install any required dependencies + await installDependencies(webcontainer, code) + + // Split imports from the rest of the code + const importRegex = + /^import\s+?(?:(?:(?:[\w*\s{},]*)\s+from\s+?)|)(?:(?:"(?:[^"]*)")|(?:'([^']*)'))[^;]*;?\s*/gm + const imports = [] + let remainingCode = code + + let match + while ((match = importRegex.exec(code)) !== null) { + imports.push(match[0]) + remainingCode = remainingCode.replace(match[0], '') + } + + // Create the module file with proper structure + const moduleCode = ` + ${imports.join('\n')} + + // Access to params + const params = ${JSON.stringify(params)}; + + // Capture return values + let __result; + + // Execute code and capture result + __result = await (async () => { + ${remainingCode} + })(); + + // Print the result to stdout for capture + if (__result !== undefined) { + console.log('__RESULT_START__'); + console.log(JSON.stringify(__result)); + console.log('__RESULT_END__'); + } + + // Ensure process exits after completion + process.exit(0); + ` + + // Mount the file as an ES module + await webcontainer.fs.writeFile('/code.mjs', moduleCode) + + // Set up stdout capture + let stdout = '' + let result: any = null + let processCompleted = false + + // Run the code with Node.js in ES module mode + process = await webcontainer.spawn('node', ['code.mjs']) + + // Create a promise that resolves when we have our result + const resultPromise = new Promise((resolve, reject) => { + // Collect output + process.output.pipeTo( + new WritableStream({ + write(data) { + stdout += data + + // Check for result markers + if (stdout.includes('__RESULT_START__') && stdout.includes('__RESULT_END__')) { + try { + const resultStart = stdout.indexOf('__RESULT_START__') + '__RESULT_START__'.length + const resultEnd = stdout.indexOf('__RESULT_END__') + const resultJson = stdout.substring(resultStart, resultEnd).trim() + + if (resultJson) { + result = JSON.parse(resultJson) + } + + // Don't resolve yet - wait for process to exit + } catch (error) { + console.error('Failed to parse result', error) + reject(error) + } + } + }, + }) + ) + + // Listen for process exit + process.exit.then((code: number) => { + processCompleted = true + if (code === 0) { + resolve() + } else { + reject(new Error(`Process exited with code ${code}`)) + } + }) + }) + + // Add a timeout + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => { + if (!processCompleted && process) { + process.kill() + } + reject(new Error(`Execution timed out after ${timeout}ms`)) + }, timeout) + }) + + // Wait for either completion or timeout + await Promise.race([resultPromise, timeoutPromise]) + + const executionTime = Date.now() - startTime + + // Clean stdout of our result markers + const cleanedStdout = stdout + .replace(/(__RESULT_START__|__RESULT_END__)/g, '') + .replace(/\n{3,}/g, '\n\n') // Reduce multiple newlines + .trim() + + return { + success: true, + output: { + result, + stdout: cleanedStdout, + executionTime, + }, + } + } catch (error: any) { + // Ensure process is killed on error + if (process) { + try { + process.kill() + } catch (killError) { + console.error('Error killing process:', killError) + } + } + + const executionTime = Date.now() - startTime + + return { + success: false, + output: { + result: null, + stdout: '', + executionTime, + }, + error: error.message || 'Code execution failed', + } + } +} diff --git a/next.config.ts b/next.config.ts index 3fdc091b15..1e4fd9aec4 100644 --- a/next.config.ts +++ b/next.config.ts @@ -5,6 +5,41 @@ const nextConfig: NextConfig = { buildActivity: false, appIsrStatus: false, }, + images: { + domains: ['avatars.githubusercontent.com'], + }, + async headers() { + return [ + { + // API routes CORS headers + source: '/api/:path*', + headers: [ + { key: 'Access-Control-Allow-Credentials', value: 'true' }, + { key: 'Access-Control-Allow-Origin', value: 'https://localhost:3001' }, + { key: 'Access-Control-Allow-Methods', value: 'GET,POST,OPTIONS,PUT,DELETE' }, + { + key: 'Access-Control-Allow-Headers', + value: + 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version', + }, + ], + }, + { + // Cross-Origin Isolation headers (existing) + source: '/:path*', + headers: [ + { + key: 'Cross-Origin-Embedder-Policy', + value: 'require-corp', + }, + { + key: 'Cross-Origin-Opener-Policy', + value: 'same-origin', + }, + ], + }, + ] + }, } export default nextConfig diff --git a/package-lock.json b/package-lock.json index a0ecb4b49b..c27c9ac843 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.6", "@types/lodash.debounce": "^4.0.9", + "@webcontainer/api": "^1.5.1-internal.9", "better-auth": "^1.1.18", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -34,7 +35,7 @@ "drizzle-orm": "^0.39.3", "lodash.debounce": "^4.0.8", "lucide-react": "^0.469.0", - "next": "15.1.3", + "next": "^15.2.0", "openai": "^4.85.4", "postgres": "^3.4.5", "prismjs": "^1.29.0", @@ -1387,15 +1388,15 @@ "license": "MIT" }, "node_modules/@next/env": { - "version": "15.1.3", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.3.tgz", - "integrity": "sha512-Q1tXwQCGWyA3ehMph3VO+E6xFPHDKdHFYosadt0F78EObYxPio0S09H9UGYznDe6Wc8eLKLG89GqcFJJDiK5xw==", + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.0.tgz", + "integrity": "sha512-eMgJu1RBXxxqqnuRJQh5RozhskoNUDHBFybvi+Z+yK9qzKeG7dadhv/Vp1YooSZmCnegf7JxWuapV77necLZNA==", "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.1.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.3.tgz", - "integrity": "sha512-aZtmIh8jU89DZahXQt1La0f2EMPt/i7W+rG1sLtYJERsP7GRnNFghsciFpQcKHcGh4dUiyTB5C1X3Dde/Gw8gg==", + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.0.tgz", + "integrity": "sha512-rlp22GZwNJjFCyL7h5wz9vtpBVuCt3ZYjFWpEPBGzG712/uL1bbSkS675rVAUCRZ4hjoTJ26Q7IKhr5DfJrHDA==", "cpu": [ "arm64" ], @@ -1409,12 +1410,13 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.1.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.3.tgz", - "integrity": "sha512-aw8901rjkVBK5mbq5oV32IqkJg+CQa6aULNlN8zyCWSsePzEG3kpDkAFkkTOh3eJ0p95KbkLyWBzslQKamXsLA==", + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.0.tgz", + "integrity": "sha512-DiU85EqSHogCz80+sgsx90/ecygfCSGl5P3b4XDRVZpgujBm5lp4ts7YaHru7eVTyZMjHInzKr+w0/7+qDrvMA==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -1424,12 +1426,13 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.1.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.3.tgz", - "integrity": "sha512-YbdaYjyHa4fPK4GR4k2XgXV0p8vbU1SZh7vv6El4bl9N+ZSiMfbmqCuCuNU1Z4ebJMumafaz6UCC2zaJCsdzjw==", + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.0.tgz", + "integrity": "sha512-VnpoMaGukiNWVxeqKHwi8MN47yKGyki5q+7ql/7p/3ifuU2341i/gDwGK1rivk0pVYbdv5D8z63uu9yMw0QhpQ==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -1439,12 +1442,13 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.1.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.3.tgz", - "integrity": "sha512-qgH/aRj2xcr4BouwKG3XdqNu33SDadqbkqB6KaZZkozar857upxKakbRllpqZgWl/NDeSCBYPmUAZPBHZpbA0w==", + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.0.tgz", + "integrity": "sha512-ka97/ssYE5nPH4Qs+8bd8RlYeNeUVBhcnsNUmFM6VWEob4jfN9FTr0NBhXVi1XEJpj3cMfgSRW+LdE3SUZbPrw==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -1454,12 +1458,13 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.1.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.3.tgz", - "integrity": "sha512-uzafnTFwZCPN499fNVnS2xFME8WLC9y7PLRs/yqz5lz1X/ySoxfaK2Hbz74zYUdEg+iDZPd8KlsWaw9HKkLEVw==", + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.0.tgz", + "integrity": "sha512-zY1JduE4B3q0k2ZCE+DAF/1efjTXUsKP+VXRtrt/rJCTgDlUyyryx7aOgYXNc1d8gobys/Lof9P9ze8IyRDn7Q==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -1469,12 +1474,13 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.1.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.3.tgz", - "integrity": "sha512-el6GUFi4SiDYnMTTlJJFMU+GHvw0UIFnffP1qhurrN1qJV3BqaSRUjkDUgVV44T6zpw1Lc6u+yn0puDKHs+Sbw==", + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.0.tgz", + "integrity": "sha512-QqvLZpurBD46RhaVaVBepkVQzh8xtlUN00RlG4Iq1sBheNugamUNPuZEH1r9X1YGQo1KqAe1iiShF0acva3jHQ==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -1484,12 +1490,13 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.1.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.3.tgz", - "integrity": "sha512-6RxKjvnvVMM89giYGI1qye9ODsBQpHSHVo8vqA8xGhmRPZHDQUE4jcDbhBwK0GnFMqBnu+XMg3nYukNkmLOLWw==", + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.0.tgz", + "integrity": "sha512-ODZ0r9WMyylTHAN6pLtvUtQlGXBL9voljv6ujSlcsjOxhtXPI1Ag6AhZK0SE8hEpR1374WZZ5w33ChpJd5fsjw==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -1499,12 +1506,13 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.1.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.3.tgz", - "integrity": "sha512-VId/f5blObG7IodwC5Grf+aYP0O8Saz1/aeU3YcWqNdIUAmFQY3VEPKPaIzfv32F/clvanOb2K2BR5DtDs6XyQ==", + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.0.tgz", + "integrity": "sha512-8+4Z3Z7xa13NdUuUAcpVNA6o76lNPniBd9Xbo02bwXQXnZgFvEopwY2at5+z7yHl47X9qbZpvwatZ2BRo3EdZw==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -3711,6 +3719,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@webcontainer/api": { + "version": "1.5.1-internal.9", + "resolved": "https://registry.npmjs.org/@webcontainer/api/-/api-1.5.1-internal.9.tgz", + "integrity": "sha512-l5cR4pzGwe+ZnR+HFKtz5Rq6JntSqay5ivX/LHoaf4vmTv8lYbNK0FQDPsXSd6Ual6F95eofCUJkyExDqK7Ozw==", + "license": "MIT" + }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -8453,12 +8467,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "15.1.3", - "resolved": "https://registry.npmjs.org/next/-/next-15.1.3.tgz", - "integrity": "sha512-5igmb8N8AEhWDYzogcJvtcRDU6n4cMGtBklxKD4biYv4LXN8+awc/bbQ2IM2NQHdVPgJ6XumYXfo3hBtErg1DA==", + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/next/-/next-15.2.0.tgz", + "integrity": "sha512-VaiM7sZYX8KIAHBrRGSFytKknkrexNfGb8GlG6e93JqueCspuGte8i4ybn8z4ww1x3f2uzY4YpTaBEW4/hvsoQ==", "license": "MIT", "dependencies": { - "@next/env": "15.1.3", + "@next/env": "15.2.0", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", @@ -8473,14 +8487,14 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.1.3", - "@next/swc-darwin-x64": "15.1.3", - "@next/swc-linux-arm64-gnu": "15.1.3", - "@next/swc-linux-arm64-musl": "15.1.3", - "@next/swc-linux-x64-gnu": "15.1.3", - "@next/swc-linux-x64-musl": "15.1.3", - "@next/swc-win32-arm64-msvc": "15.1.3", - "@next/swc-win32-x64-msvc": "15.1.3", + "@next/swc-darwin-arm64": "15.2.0", + "@next/swc-darwin-x64": "15.2.0", + "@next/swc-linux-arm64-gnu": "15.2.0", + "@next/swc-linux-arm64-musl": "15.2.0", + "@next/swc-linux-x64-gnu": "15.2.0", + "@next/swc-linux-x64-musl": "15.2.0", + "@next/swc-win32-arm64-msvc": "15.2.0", + "@next/swc-win32-x64-msvc": "15.2.0", "sharp": "^0.33.5" }, "peerDependencies": { diff --git a/package.json b/package.json index 44799ccbfc..e02f17a539 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.6", "@types/lodash.debounce": "^4.0.9", + "@webcontainer/api": "^1.5.1-internal.9", "better-auth": "^1.1.18", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -43,7 +44,7 @@ "drizzle-orm": "^0.39.3", "lodash.debounce": "^4.0.8", "lucide-react": "^0.469.0", - "next": "15.1.3", + "next": "^15.2.0", "openai": "^4.85.4", "postgres": "^3.4.5", "prismjs": "^1.29.0", diff --git a/tools/function/webcontainer.ts b/tools/function/webcontainer.ts new file mode 100644 index 0000000000..3be366fe01 --- /dev/null +++ b/tools/function/webcontainer.ts @@ -0,0 +1,66 @@ +import { ToolConfig } from '../types' +import { CodeExecutionInput, CodeExecutionOutput } from './execute' + +const DEFAULT_TIMEOUT = 5000 // 5 seconds + +export const webcontainerExecuteTool: ToolConfig = { + id: 'webcontainer_execute', + name: 'WebContainer Execute', + description: + 'Execute JavaScript code in a secure WebContainer environment with Node.js runtime and full npm package support.', + version: '1.0.0', + + params: { + code: { + type: 'string', + required: true, + description: 'The code to execute', + }, + timeout: { + type: 'number', + required: false, + description: 'Execution timeout in milliseconds', + default: DEFAULT_TIMEOUT, + }, + }, + + request: { + url: '/api/webcontainer', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params: CodeExecutionInput) => { + const codeContent = Array.isArray(params.code) + ? params.code.map((c: { content: string }) => c.content).join('\n') + : params.code + + return { + code: codeContent, + timeout: params.timeout || DEFAULT_TIMEOUT, + } + }, + isInternalRoute: true, + }, + + transformResponse: async (response: Response): Promise => { + const result = await response.json() + + if (!response.ok || !result.success) { + throw new Error(result.error || 'WebContainer code execution failed') + } + + return { + success: true, + output: { + result: result.output.result, + stdout: result.output.stdout, + executionTime: result.output.executionTime, + }, + } + }, + + transformError: (error: any) => { + return error.message || 'WebContainer code execution failed' + }, +} diff --git a/tools/index.ts b/tools/index.ts index 2ab3b8197d..83ac9a6135 100644 --- a/tools/index.ts +++ b/tools/index.ts @@ -2,6 +2,7 @@ import { useCustomToolsStore } from '@/stores/custom-tools/store' import { visionTool as crewAIVision } from './crewai/vision' import { scrapeTool } from './firecrawl/scrape' import { functionExecuteTool as functionExecute } from './function/execute' +import { webcontainerExecuteTool as webcontainerExecute } from './function/webcontainer' import { commentTool } from './github/comment' import { prTool } from './github/pr' import { repoInfoTool } from './github/repo' @@ -39,6 +40,7 @@ export const tools: Record = { hubspot_contacts: hubspotContacts, salesforce_opportunities: salesforceOpportunities, function_execute: functionExecute, + webcontainer_execute: webcontainerExecute, crewai_vision: crewAIVision, firecrawl_scrape: scrapeTool, jina_readurl: readUrlTool,