The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"name": "eek-Go v3",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "coding-agent",
"responseMode": "lastNode",
"options": {}
},
"id": "wh-trigger",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
250,
300
]
},
{
"parameters": {
"jsCode": "const query = $json.query || {};\nconst body = $json.body || $json;\nconst message = query.message || body.message || body.goal || '';\nconst projectId = query.project_id || body.project_id || ('proj-' + Date.now());\nconst reference_url = query.reference_url || body.reference_url || null;\nconst image_data = body.image_data || null;\nreturn [{ json: { message, project_id: projectId, operation: 'init', goal: message, reference_url, image_data } }];"
},
"id": "extract-input",
"name": "Extract Input",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
450,
300
]
},
{
"parameters": {
"method": "GET",
"url": "={{ ($env.FILE_API_URL || 'http://file-api:3456') + '/projects/' + $json.project_id + '/files-content' }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "={{ 'Bearer ' + ($env.FILE_API_TOKEN || '') }}"
}
]
},
"options": {}
},
"id": "fetch-project-files",
"name": "Fetch Project Files",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
850,
300
],
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "// MODIFIED: Absorbs Research: Build Project Context\n// Caches _allFileContents, _researchDocs, _projectFileList in static data\n// so Phase 2 chunks don't need to re-fetch or re-compute.\nconst memory = $('Extract Input').first().json;\nconst message = $('Extract Input').first().json.message;\nconst allFiles = $json.files || [];\nconst staticData = $getWorkflowStaticData('global');\n\n// Cache all file contents for Phase 2\n// Reset execution-scoped static data from prior runs\nstaticData._buildAttempt = 0;\nstaticData._buildResult = {};\nstaticData._reviewResult = {};\nstaticData.p2Results = [];\nstaticData.allTasks = [];\nstaticData._pipelineReport = '';\n\n// Extract project memory (if it exists) \u2014 pipeline-managed, not a code file\nconst memoryFile = allFiles.find(f => f.path === 'memory.md');\nconst projectMemory = memoryFile ? memoryFile.content : '';\nstaticData._projectMemory = projectMemory;\n\n// Filter memory.md from file list so planner/coder can't modify it\nconst codeFiles = allFiles.filter(f => f.path !== 'memory.md');\nstaticData._allFileContents = codeFiles;\n\nconst fileList = codeFiles.map(f => f.path);\n\n// Build API surface (exports from each file)\nconst apiSurface = [];\nfor (const f of allFiles) {\n if (!f.path.match(/\\.(ts|tsx|js|jsx)$/) || f.path.includes('node_modules')) continue;\n const lines = (f.content || '').split('\\n');\n const exports = [];\n for (const line of lines) {\n const trimmed = line.trim();\n if (/^export\\s+default\\s/.test(trimmed)) {\n exports.push(trimmed.replace(/\\{[\\s\\S]*$/, '{...}').substring(0, 120));\n } else if (/^export\\s+(const|let|var|function|class|interface|type|enum)\\s/.test(trimmed)) {\n exports.push(trimmed.replace(/\\{[\\s\\S]*$/, '{...}').replace(/=>[\\s\\S]*$/, '=> ...').substring(0, 150));\n } else if (/^export\\s+\\{/.test(trimmed)) {\n exports.push(trimmed.substring(0, 150));\n }\n }\n if (exports.length > 0) {\n apiSurface.push(`## ${f.path}\\n${exports.join('\\n')}`);\n }\n}\n\n// Extract dependencies from package.json\nlet dependencies = {};\nlet devDependencies = {};\nlet depNames = [];\nfor (const f of allFiles) {\n if (f.path === 'package.json' || f.path.endsWith('/package.json')) {\n try {\n const pkg = JSON.parse(f.content);\n dependencies = { ...dependencies, ...(pkg.dependencies || {}) };\n devDependencies = { ...devDependencies, ...(pkg.devDependencies || {}) };\n depNames = [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.devDependencies || {})];\n } catch(e) {}\n }\n}\n\n// Build research docs string\nconst depList = Object.entries(dependencies).map(([k,v]) => ` ${k}: ${v}`).join('\\n');\nconst devDepList = Object.entries(devDependencies).map(([k,v]) => ` ${k}: ${v}`).join('\\n');\nlet researchDocs = '';\nif (depList || devDepList) {\n researchDocs += '## Dependency Manifest\\nOnly use packages listed here.\\n';\n if (depList) researchDocs += `dependencies:\\n${depList}\\n`;\n if (devDepList) researchDocs += `devDependencies:\\n${devDepList}\\n`;\n}\nif (apiSurface.length > 0) {\n researchDocs += '\\n## Project API Surface\\nMatch these import/export signatures exactly when importing from project files.\\n';\n researchDocs += apiSurface.join('\\n\\n');\n}\n\n// Cache for all downstream phases\nstaticData._researchDocs = researchDocs;\nstaticData._projectFileList = fileList;\nstaticData._projectApiSummary = apiSurface.join('\\n');\nstaticData._projectDeps = depNames;\n\n// Cache planner input in static data so Build Request can read it\n// (Load Model HTTP response replaces $json, so downstream can't use $json)\nstaticData._plannerInput = {\n message,\n project_goal: memory.goal || message,\n project_id: memory.project_id,\n existing_files: fileList,\n api_summary: apiSurface.join('\\n'),\n installed_packages: depNames,\n _scrapeData: memory._scrapeData || null,\n image_data: memory.image_data || null,\n project_memory: projectMemory\n};\n\nreturn [{ json: staticData._plannerInput }];"
},
"id": "prepare-planner",
"name": "Prepare Planner Input",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1050,
300
]
},
{
"parameters": {
"jsCode": "// Read planner input from static data (Load Model HTTP response replaces $json)\nconst staticData = $getWorkflowStaticData('global');\nconst plannerInput = staticData._plannerInput || {};\nconst message = plannerInput.message || '';\nconst existingFiles = plannerInput.existing_files || [];\nconst apiSummary = plannerInput.api_summary || '';\nconst installedPkgs = plannerInput.installed_packages || [];\nconst scrapeData = plannerInput._scrapeData;\n\nlet projectContext = '';\nif (existingFiles.length > 0) {\n projectContext += `\\n\\nEXISTING PROJECT FILES (only reference files from this list):\\n${existingFiles.join('\\n')}\\n`;\n}\nif (apiSummary) {\n projectContext += `\\nAPI SURFACE (exports from each file):\\n${apiSummary}\\n`;\n}\nif (installedPkgs.length > 0) {\n projectContext += `\\nINSTALLED PACKAGES (only reference packages from this list):\\n${installedPkgs.join(', ')}\\n`;\n}\n\n// Inject project memory for continuity across runs\nconst projectMemory = plannerInput.project_memory || '';\nif (projectMemory) {\n projectContext += '\\n\\nPROJECT MEMORY (context from previous pipeline runs \u2014 use for continuity):\\n' + projectMemory.substring(0, 4000) + '\\n';\n}\n\nconst plannerPrompt = `You are a Senior Software Architect and UI/UX Design Expert. Your job is to break a coding request into MULTIPLE structured tasks with detailed visual specifications.\n\nDESIGN PRINCIPLES (apply to ALL task descriptions):\n- Main interaction element must be centered and dominant (40-60% of viewport height)\n- Use vertical single-column layouts for apps/games \u2014 never side-by-side grids unless explicitly requested\n- Content hierarchy: hero/main action at top \u2192 stats/feedback \u2192 secondary actions (shop, settings) at bottom\n- Mobile-first: everything fits in 100vh, no page-level scrollbars, internal scrolling only for lists\n- Dark backgrounds with bright accent colors. Use gradients for depth (purple-900 to black, not flat colors)\n- Every click/tap must produce visible feedback (scale animation, color flash, particle effect)\n- Numbers should animate when they change (bounce, pulse, scale). Use tabular-nums for counters\n- Cards: rounded-xl (12-16px), subtle shadows, semi-transparent backgrounds with backdrop-blur\n- Buttons: press animation (scale 0.95), hover glow, disabled state with reduced opacity\n- Color-code affordability: gold/amber = available, red/gray = locked, green = owned/success\n- Typography: big bold numbers (text-4xl+), clear hierarchy via weight not just size\n- Spacing: generous padding, never cramped. Mobile touch targets minimum 44px\n\nWHEN A REFERENCE IMAGE IS PROVIDED:\n- Describe the EXACT layout structure you see: column vs row, spacing ratios, component sizes\n- Specify exact colors, gradients, border styles, and shadows visible in the image\n- Note the visual hierarchy: what's biggest, what's brightest, what draws the eye first\n- Describe animations or interactive states implied by the design (hover effects, active states)\n- Include specific dimensions: \"toilet button should be 150px diameter\" not just \"large button\"\n- The coder CANNOT see the image \u2014 your description is their ONLY reference Each task will be handled by a separate coder with 128K context who can write up to 6 files at once.\\n\\nOutput a JSON object with ONE field:\\n\\n\"tasks\": array of task objects. IMPORTANT: You MUST create multiple tasks. Group files by concern (e.g. styles in one task, frontend components in another, config in another). Each task has:\\n- task_id: string like \"TASK-001\"\\n- description: detailed actionable description of what to implement, including specific requirements, color values, component names, API endpoints, and behavior. The coder cannot see the original request \u2014 your description is all they get.\\n- files: array of exact file paths to create or modify. List EVERY file this task needs \u2014 the coder can ONLY write files listed here. For existing projects, prefer modifying files from the EXISTING PROJECT FILES list. For new projects or missing functionality, CREATE new file paths as needed.\\n- dependencies: array of task_id strings this task depends on (empty if none)\\n- complexity: \"low\", \"medium\", or \"high\"\n- needs_concept: boolean \u2014 set to true if this task involves UI/visual work that has NO reference image from the user. Set to false if the user provided a reference image that covers this task's visual design, or if the task is purely logic/config with no visual component.\\n\\nRULES:\\n- Always split work across multiple tasks by concern: styles, components, routes, config, etc.\\n- Each task can touch up to 12-15 files. The coder has 128K context and currently uses less than 2% of it \u2014 give it MORE work per task. Aim for 2-3 LARGE tasks instead of many small ones. A single task can contain an entire feature (components + hooks + styles + config). Fewer tasks = faster pipeline\\n- The description must be SELF-CONTAINED \u2014 include ALL details the coder needs\\n- For existing projects, reference files from the EXISTING PROJECT FILES list when modifying. You MAY create new files that do not exist yet.\n- CRITICAL: For new projects (empty file list), you MUST include a setup task that creates: package.json (with ALL dependencies), index.html, vite.config.js (or next.config.js), tsconfig.json (if TypeScript), tailwind.config.js + postcss.config.js (if using Tailwind), and the main entry point (src/main.jsx or app/layout.tsx). The project MUST be buildable.\\n- Detect the project type from the files and packages. If the project is a React/Vite frontend (no Express or backend framework in INSTALLED PACKAGES), focus all tasks on frontend files. Only create server/middleware/API handler tasks if Express or a similar backend framework is in INSTALLED PACKAGES.\\n- In each task description, specify the EXACT import/export style each file should use (named vs default) based on the API SURFACE above.\\n- NEVER create a task whose only purpose is deleting files. The coder cannot delete files \u2014 it can only create or modify. If dead files exist, ignore them. They do not affect the build.\n- The FIRST task should always include project configuration files (package.json, config files, index.html) if they do not already exist. The project MUST compile and run after all tasks complete.\n- NEVER include memory.md in any task's file list \u2014 it is managed by the pipeline\n- Output ONLY the raw JSON object: {\"tasks\": [...]}\\n- No markdown, no explanation \u2014 ONLY the JSON with tasks`;\n\nlet messageContent;\nif (scrapeData && scrapeData.screenshot_b64) {\n const cssTokensStr = JSON.stringify(scrapeData.css_tokens, null, 2);\n const domStr = scrapeData.dom_summary || '';\n messageContent = [\n { type: 'image_url', image_url: { url: `data:image/png;base64,${scrapeData.screenshot_b64}` } },\n { type: 'text', text: `${plannerPrompt}\\n\\nREFERENCE DESIGN (screenshot above):\\nCSS Tokens:\\n${cssTokensStr}\\n\\nDOM Structure:\\n${domStr}\\n\\nRequest: ${message}${projectContext}\\n\\n(Match the visual design, color palette, typography, and layout from the screenshot)` }\n ];\n} else if (plannerInput.image_data) {\n const userImage = plannerInput.image_data;\n messageContent = [\n { type: 'image_url', image_url: { url: 'data:image/png;base64,' + userImage } },\n { type: 'text', text: plannerPrompt + '\\n\\nREFERENCE IMAGE (uploaded by user \u2014 replicate this design):\\n\\nRequest: ' + message + projectContext }\n ];\n} else {\n messageContent = `${plannerPrompt}\\n\\nRequest: ${message}${projectContext}`;\n}\n\nreturn [{\n json: {\n model: $env.PLANNER_MODEL || 'qwen3.5-27b@q4_k_m',\n messages: [{ role: 'user', content: messageContent }],\n temperature: 1.0,\n top_p: 0.95,\n top_k: 20,\n min_p: 0.0,\n presence_penalty: 0.0,\n max_tokens: parseInt($env.PLANNER_MAX_TOKENS) || 32768,\n chat_template_kwargs: { enable_thinking: true, max_thinking_tokens: 4096 }\n }\n}];"
},
"id": "planner-build",
"name": "Planner: Build Request",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1250,
300
]
},
{
"parameters": {
"jsCode": "const raw = $json.choices[0].message.content || $json.choices[0].message.reasoning_content || '';\nlet content = raw.replace(/<think>[\\s\\S]*?<\\/think>/g, '').trim();\nif (content.includes('</think>')) content = content.split('</think>').pop().trim();\ncontent = content.replace(/<\\/?task>/g, '').trim();\ncontent = content.replace(/\\n+(?:Reasoning|Note|Explanation):[\\s\\S]*/i, '').trim();\ncontent = content.replace(/^```(?:json)?\\n?/, '').replace(/\\n?```$/, '').trim();\n\nlet tasks, planDocument = '';\ntry {\n const parsed = JSON.parse(content);\n if (Array.isArray(parsed)) {\n tasks = parsed;\n } else {\n tasks = parsed.tasks || [parsed];\n planDocument = parsed.plan_document || '';\n }\n} catch (e) {\n // Truncated JSON recovery: extract complete task objects\n const tasksMatch = content.match(/\"tasks\"\\s*:\\s*\\[/);\n if (tasksMatch) {\n const arrStart = content.indexOf('[', tasksMatch.index);\n let bracketDepth = 0;\n let lastCompleteObj = -1;\n for (let i = arrStart; i < content.length; i++) {\n if (content[i] === '{') bracketDepth++;\n if (content[i] === '}') {\n bracketDepth--;\n if (bracketDepth === 0) lastCompleteObj = i;\n }\n }\n if (lastCompleteObj > arrStart) {\n const recoveredArr = content.substring(arrStart, lastCompleteObj + 1) + ']';\n try {\n tasks = JSON.parse(recoveredArr);\n } catch (e2) {\n tasks = [{ task_id: 'TASK-FALLBACK-' + Date.now(), description: content, files: [], dependencies: [], complexity: 'high' }];\n }\n } else {\n tasks = [{ task_id: 'TASK-FALLBACK-' + Date.now(), description: content, files: [], dependencies: [], complexity: 'high' }];\n }\n } else {\n tasks = [{ task_id: 'TASK-FALLBACK-' + Date.now(), description: content, files: [], dependencies: [], complexity: 'high' }];\n }\n}\n\ntasks = tasks.map(t => ({\n ...t,\n files: t.files || [],\n dependencies: t.dependencies || [],\n complexity: t.complexity || 'medium'\n}));\n\nreturn [{ json: { tasks, plan_document: planDocument } }];"
},
"id": "planner-parse",
"name": "Planner: Parse Response",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1650,
300
]
},
{
"id": "research-fetch-docs",
"name": "Research: Fetch Docs",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2050,
300
],
"parameters": {
"jsCode": "// Research: Fetch library docs \u2014 Context7 \u2192 Exa fallback \u2192 Magic UI\nconst http = require('http');\nconst https = require('https');\nconst staticData = $getWorkflowStaticData('global');\nconst allFiles = staticData._allFileContents || [];\nconst tasks = $json.tasks || [];\n\n// \u2500\u2500\u2500 1. Extract libraries from package.json + task text \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst libs = new Set();\nconst pkgFile = allFiles.find(f => f.path === 'package.json');\nif (pkgFile) {\n try {\n const pkg = JSON.parse(pkgFile.content);\n for (const dep of Object.keys(pkg.dependencies || {})) libs.add(dep);\n for (const dep of Object.keys(pkg.devDependencies || {})) libs.add(dep);\n } catch(e) {}\n}\nconst taskText = tasks.map(t => t.description || '').join(' ').toLowerCase();\nconst knownLibs = ['react', 'next', 'nextjs', 'vite', 'tailwindcss', 'tailwind', 'express', 'prisma', 'framer-motion', 'heroui', 'zustand', 'zod', 'three', 'react-three', 'r3f', 'drei', '@react-three/fiber', '@react-three/drei'];\nfor (const lib of knownLibs) {\n if (taskText.includes(lib)) {\n const mapped = {'nextjs':'next','tailwind':'tailwindcss','react-three':'@react-three/fiber','r3f':'@react-three/fiber','drei':'@react-three/drei'};\n libs.add(mapped[lib] || lib);\n }\n}\nif (allFiles.some(f => f.path.match(/\\.(jsx|tsx)$/))) libs.add('react');\nconst skipList = new Set(['autoprefixer', 'postcss', 'typescript', 'vite', '@vitejs/plugin-react', '@types/react', '@types/node', '@types/react-dom', 'eslint', 'prettier', 'react', 'react-dom', 'tailwindcss', 'tailwind', '@fontsource/inter', '@fontsource/fira-code']);\nconst toFetch = [...libs].filter(l => !skipList.has(l)).slice(0, 7);\n\nif (toFetch.length === 0) {\n staticData._researchDocs = '';\n return [{ json: $json }];\n}\n\n// \u2500\u2500\u2500 2. MCP helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction mcpRequest(sessionId, method, params) {\n return new Promise((resolve, reject) => {\n const body = JSON.stringify({ jsonrpc: '2.0', method, params, id: Date.now() });\n const req = http.request({\n hostname: 'docky', port: 8811, path: '/mcp', method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json, text/event-stream',\n 'Content-Length': Buffer.byteLength(body),\n ...(sessionId ? { 'Mcp-Session-Id': sessionId } : {})\n }\n }, res => {\n let data = '';\n res.on('data', c => data += c);\n res.on('end', () => resolve({ data, sessionId: res.headers['mcp-session-id'] }));\n });\n req.on('error', reject);\n req.setTimeout(20000, () => { req.destroy(); reject(new Error('timeout')); });\n req.write(body); req.end();\n });\n}\n\n// Exa MCP helper (HTTPS to mcp.exa.ai)\nfunction exaRequest(sessionId, method, params) {\n return new Promise((resolve, reject) => {\n const body = JSON.stringify({ jsonrpc: '2.0', method, params, id: Date.now() });\n const headers = {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json, text/event-stream',\n 'Content-Length': Buffer.byteLength(body),\n };\n if (sessionId) headers['Mcp-Session-Id'] = sessionId;\n const req = https.request({\n hostname: 'mcp.exa.ai', port: 443, path: '/mcp', method: 'POST', headers\n }, res => {\n let data = '';\n if (!sessionId && res.headers['mcp-session-id']) {\n sessionId = res.headers['mcp-session-id'];\n }\n res.on('data', c => data += c);\n res.on('end', () => resolve({ data, sessionId }));\n });\n req.on('error', reject);\n req.setTimeout(30000, () => { req.destroy(); reject(new Error('exa timeout')); });\n req.write(body); req.end();\n });\n}\n\nfunction parseSSE(raw) {\n for (const line of raw.split('\\n')) {\n if (line.startsWith('data: ') || line.startsWith('data:')) {\n const payload = line.startsWith('data: ') ? line.slice(6) : line.slice(5);\n try { return JSON.parse(payload); } catch(e) {}\n }\n }\n try { return JSON.parse(raw); } catch(e) {}\n return {};\n}\n\nconst docs = [];\nconst errors = [];\nconst docSources = {}; // track source per library\nlet docSource = 'none';\n\n// \u2500\u2500\u2500 3. Try Context7 first \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\ntry {\n const init = await mcpRequest(null, 'initialize', {\n protocolVersion: '2024-11-05', capabilities: {},\n clientInfo: { name: 'eek-go-research', version: '1.0' }\n });\n const sid = init.sessionId;\n if (!sid) throw new Error('No MCP session ID');\n\n for (const lib of toFetch) {\n try {\n const resolve = await mcpRequest(sid, 'tools/call', {\n name: 'resolve-library-id', arguments: { libraryName: lib }\n });\n const resolveData = parseSSE(resolve.data);\n const resolveText = (resolveData.result?.content || []).map(c => c.text || '').join('');\n if (resolveText.includes('Failed to retrieve')) { errors.push(lib + ': ctx7 failed'); continue; }\n const idMatch = resolveText.match(/Context7-compatible library ID:\\s*(\\/[\\w.-]+\\/[\\w.-]+)/);\n if (!idMatch) { errors.push(lib + ': no ID'); continue; }\n\n const docsResp = await mcpRequest(sid, 'tools/call', {\n name: 'get-library-docs',\n arguments: { context7CompatibleLibraryID: idMatch[1], tokens: 5000, topic: 'setup components hooks API examples' }\n });\n const docsData = parseSSE(docsResp.data);\n const docText = (docsData.result?.content || []).map(c => c.text || '').join('');\n if (docText.length > 100 && !docText.includes('Failed to retrieve')) {\n docs.push('## ' + lib + '\\n' + docText.substring(0, 8000));\n docSources[lib] = 'Context7';\n docSource = 'context7';\n }\n } catch(e) { errors.push(lib + ': ' + e.message); }\n }\n} catch(e) {\n errors.push('ctx7 init: ' + e.message);\n}\n\n\n// \u2500\u2500\u2500 3.5. GSAP MCP docs \u2014 if project uses GSAP \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nif (libs.has('gsap')) {\n try {\n const gsapInit = await mcpRequest(null, 'initialize', {\n protocolVersion: '2024-11-05', capabilities: {},\n clientInfo: { name: 'eek-go-gsap', version: '1.0' }\n });\n const gsapSid = gsapInit.sessionId;\n if (gsapSid) {\n // Get GSAP API docs for key APIs\n const gsapApis = ['ScrollTrigger', 'gsap.to', 'gsap.from', 'gsap.timeline'];\n const gsapDocs = [];\n for (const api of gsapApis) {\n try {\n const gsapResp = await mcpRequest(gsapSid, 'tools/call', {\n name: 'get_gsap_api_expert',\n arguments: { api_element: api, level: 'intermediate' }\n });\n const gsapData = parseSSE(gsapResp.data);\n const gsapText = (gsapData.result?.content || []).map(c => c.text || '').join('');\n if (gsapText.length > 100 && !gsapText.includes('Error')) {\n gsapDocs.push('### ' + api + '\\n' + gsapText.substring(0, 3000));\n }\n } catch(e) { errors.push('gsap-api-' + api + ': ' + e.message); }\n }\n if (gsapDocs.length > 0) {\n docs.push('## GSAP API Reference (from GSAP Master MCP)\\n' + gsapDocs.join('\\n\\n'));\n for (const api of gsapApis) docSources['gsap:' + api] = 'GSAP MCP';\n docSource = docSource || 'gsap-mcp';\n }\n }\n } catch(e) {\n errors.push('gsap-mcp: ' + e.message);\n }\n}\n\n// \u2500\u2500\u2500 4. Exa fallback \u2014 if Context7 returned 0 docs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nif (docs.length === 0) {\n try {\n const exaInit = await exaRequest(null, 'initialize', {\n protocolVersion: '2024-11-05', capabilities: {},\n clientInfo: { name: 'eek-go-research', version: '1.0' }\n });\n const exaSid = exaInit.sessionId;\n\n // Build TARGETED queries from task descriptions, not generic library names\n const mainLibs = toFetch.filter(l => !l.startsWith('@types'));\n const queries = [];\n \n // Extract specific features/APIs mentioned in tasks\n const allTaskText = tasks.map(t => (t.description || '')).join(' ');\n \n for (const lib of mainLibs.slice(0, 4)) {\n // Find what the task actually needs from this library\n const libLower = lib.toLowerCase();\n const taskWords = allTaskText.split(/[\\s.,;:]+/).filter(w => w.length > 3);\n \n // Look for API names, component names, or feature keywords near the library mention\n const relevantTerms = [];\n const taskLower = allTaskText.toLowerCase();\n const libIdx = taskLower.indexOf(libLower);\n if (libIdx >= 0) {\n // Get ~200 chars around the library mention for context\n const nearby = allTaskText.substring(Math.max(0, libIdx - 100), Math.min(allTaskText.length, libIdx + 200));\n // Extract capitalized words (likely API names) and quoted strings\n const apiNames = nearby.match(/[A-Z][a-zA-Z]+(?:Router|Provider|Context|Hook|Plugin|Trigger|Effect|Animation)?/g) || [];\n relevantTerms.push(...apiNames.slice(0, 3));\n }\n \n // Build a specific query\n const specifics = relevantTerms.length > 0 ? ' ' + relevantTerms.join(' ') : '';\n queries.push(lib + specifics + ' usage examples API reference');\n }\n \n // If no libs to fetch but tasks mention specific tech, search for that\n if (queries.length === 0 && allTaskText.length > 50) {\n const techMentions = allTaskText.match(/(?:GSAP|ScrollTrigger|framer.motion|react.router|BrowserRouter|createBrowserRouter|useEffect|useState)/gi) || [];\n if (techMentions.length > 0) {\n queries.push(techMentions.slice(0, 3).join(' ') + ' usage examples documentation');\n }\n }\n\n for (const query of queries.slice(0, 3)) {\n try {\n const resp = await exaRequest(exaSid, 'tools/call', {\n name: 'get_code_context_exa',\n arguments: { query, num_results: 2 }\n });\n const respData = parseSSE(resp.data);\n const text = (respData.result?.content || []).map(c => c.text || '').join('');\n if (text.length > 200) {\n // Trim to 6K chars per query to keep total context reasonable\n docs.push('## Docs: ' + query.split(' API ')[0] + '\\n' + text.substring(0, 6000));\n docSources[query.split(' ')[0]] = 'Exa';\n docSource = 'exa';\n }\n } catch(e) { errors.push('exa: ' + e.message); }\n }\n } catch(e) {\n errors.push('exa init: ' + e.message);\n }\n}\n\n// \u2500\u2500\u2500 5. Design guide (always included) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst designGuide = `## UI/UX Design Principles for Web Applications\n\n### Layout\n- Main interaction element must be centered and visually dominant (40-60% of viewport)\n- Use vertical single-column layouts for game/app UIs \u2014 never side-by-side grids unless it's a dashboard\n- Content hierarchy: hero/main action \u2192 stats/feedback \u2192 secondary actions (shop, settings)\n- Mobile-first: everything should work on a 375px wide screen and scale up\n- Scrollable secondary content (shops, lists) should never push the main interaction off-screen\n\n### Visual Feedback\n- Every user interaction (click, purchase, hover) MUST produce visible feedback\n- Number changes should animate (bounce, scale pulse, color flash)\n- Buttons: press animation (scale 0.95), hover glow/lift, disabled state with reduced opacity\n- Success actions: green flash, checkmark, particle burst\n- Use CSS transitions (200-300ms) on all interactive elements\n\n### Color & Contrast\n- Dark backgrounds with bright accent colors for maximum contrast\n- Use gradients over flat colors for depth (e.g., purple-900 to indigo-950)\n- Interactive elements should be the brightest items on screen\n- Text must have sufficient contrast \u2014 white/yellow on dark, with text-shadow for readability\n\n### Typography\n- Big, bold numbers for scores/stats (text-4xl to text-6xl)\n- Clear hierarchy: title (bold, large) \u2192 subtitle (medium) \u2192 body (regular, smaller)\n- Monospace or tabular numbers for counters that change frequently\n\n### Animation\n- Idle animations (slow pulse, float, rotate) make the UI feel alive\n- Click animations should be fast (100-200ms) and snappy\n- Spawn/death animations for appearing/disappearing elements\n- Use CSS will-change on animated elements for performance\n\n### Cards & Containers\n- Rounded corners (border-radius: 12-16px)\n- Subtle shadows for depth (shadow-lg, shadow-xl)\n- Semi-transparent backgrounds with backdrop-blur for overlay panels\n- Hover: lift (translateY -2 to -4px) + shadow increase`;\n\n// \u2500\u2500\u2500 6. Magic UI component examples \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet magicDocs = '';\ntry {\n const uiKeywords = [];\n const taskDescs = tasks.map(t => (t.description || '').toLowerCase()).join(' ');\n const uiPatterns = ['button', 'card', 'shop', 'form', 'input', 'modal', 'dialog', 'nav', 'header', 'footer', 'sidebar', 'menu', 'table', 'list', 'grid', 'dashboard', 'counter', 'score', 'game', 'animation', 'toggle', 'dropdown'];\n for (const p of uiPatterns) {\n if (taskDescs.includes(p)) uiKeywords.push(p);\n }\n if (uiKeywords.length > 0) {\n const magicInit = await new Promise((resolve, reject) => {\n const body = JSON.stringify({ jsonrpc: '2.0', method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'eek-go-magic', version: '1.0' } }, id: Date.now() });\n const req = http.request({\n hostname: 'docky', port: 8811, path: '/mcp', method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Accept': 'application/json, text/event-stream', 'Content-Length': Buffer.byteLength(body) }\n }, res => {\n let data = ''; res.on('data', c => data += c);\n res.on('end', () => resolve({ data, sessionId: res.headers['mcp-session-id'] }));\n });\n req.on('error', reject);\n req.setTimeout(10000, () => { req.destroy(); reject(new Error('timeout')); });\n req.write(body); req.end();\n });\n const magicSid = magicInit.sessionId;\n if (magicSid) {\n const searchTerms = uiKeywords.slice(0, 3).map(k => k + ' component');\n for (const query of searchTerms) {\n try {\n const inspResp = await mcpRequest(magicSid, 'tools/call', {\n name: '21st_magic_component_inspiration',\n arguments: { message: 'I need a modern ' + query + ' for a web app', searchQuery: query }\n });\n const inspData = parseSSE(inspResp.data);\n const inspText = (inspData.result?.content || []).map(c => c.text || '').join('');\n if (inspText.length > 200) {\n try {\n const components = JSON.parse(inspText);\n if (Array.isArray(components)) {\n const examples = components.slice(0, 2).map(c => {\n const code = c.demoCode || c.code || '';\n const name = c.demoName || c.name || query;\n return '### ' + name + '\\n```tsx\\n' + code + '\\n```';\n }).join('\\n\\n');\n if (examples.length > 100) magicDocs += '\\n\\n## Magic UI: ' + query + '\\n' + examples;\n }\n } catch(e) {\n if (inspText.length > 100) magicDocs += '\\n\\n## Magic UI: ' + query + '\\n' + inspText.substring(0, 4000);\n }\n }\n } catch(e) {}\n }\n }\n }\n} catch(e) {}\n\nif (magicDocs) docs.push('## UI Component Examples (from 21st.dev Magic)\\n' + magicDocs);\n\n// \u2500\u2500\u2500 7. Assemble final research docs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst sourceLabel = docSource === 'exa' ? 'Exa Web Search' : docSource === 'context7' ? 'Context7' : 'Design Guide Only';\nstaticData._researchDocs = docs.length > 0\n ? designGuide + '\\n\\n---\\n\\n## Library Documentation (from ' + sourceLabel + ')\\n\\n' + docs.join('\\n\\n---\\n\\n')\n : designGuide;\n\n// \u2500\u2500\u2500 8. Callback to Forge \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\ntry {\n const projectId = $('Extract Input').first().json.project_id || staticData._currentProjectId || 'unknown';\n const cbBody = JSON.stringify({\n event: 'research_complete', project_id: projectId,\n data: { libraries_fetched: toFetch, doc_count: docs.length, doc_chars: docs.reduce((s,d) => s+d.length, 0), source: docSource, sources: docSources, errors: errors.length ? errors : undefined }\n });\n const req = http.request({\n hostname: 'forge', port: 3500, path: '/api/status-callback', method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'Content-Length': Buffer.byteLength(cbBody) }\n });\n req.on('error', () => {});\n req.write(cbBody); req.end();\n} catch(e) {}\n\nreturn [{ json: $json }];\n"
}
},
{
"parameters": {
"jsCode": "// MODIFIED: Absorbs Split Into Chunks. Builds flat (task, chunk) queue.\n// Queue is passed through data pipeline (not static data) to avoid stale reads after HTTP nodes.\nconst tasks = $json.tasks || [];\nconst planDocument = $json.plan_document || '';\nconst plannerInput = $('Prepare Planner Input').first().json;\nconst projectId = plannerInput.project_id;\nconst projectGoal = plannerInput.project_goal;\n\n// Topological sort\nconst taskMap = new Map();\ntasks.forEach(t => taskMap.set(t.task_id, t));\nconst inDegree = new Map();\nconst dependents = new Map();\ntasks.forEach(t => {\n inDegree.set(t.task_id, 0);\n dependents.set(t.task_id, []);\n});\ntasks.forEach(t => {\n const deps = (t.dependencies || []).filter(d => taskMap.has(d));\n inDegree.set(t.task_id, deps.length);\n deps.forEach(d => dependents.get(d).push(t.task_id));\n});\nconst topoQueue = [];\ntasks.forEach(t => {\n if (inDegree.get(t.task_id) === 0) topoQueue.push(t.task_id);\n});\nconst sorted = [];\nwhile (topoQueue.length > 0) {\n const id = topoQueue.shift();\n sorted.push(taskMap.get(id));\n for (const dep of (dependents.get(id) || [])) {\n inDegree.set(dep, inDegree.get(dep) - 1);\n if (inDegree.get(dep) === 0) topoQueue.push(dep);\n }\n}\nif (sorted.length < tasks.length) {\n const sortedIds = new Set(sorted.map(t => t.task_id));\n tasks.forEach(t => { if (!sortedIds.has(t.task_id)) sorted.push(t); });\n}\n\n// Flatten into (task, chunk) queue \u2014 2 files per chunk\nconst chunkSize = 15;\nconst queue = [];\nfor (const task of sorted) {\n const taskFiles = task.files || [];\n const chunks = [];\n for (let i = 0; i < taskFiles.length; i += chunkSize) {\n chunks.push(taskFiles.slice(i, i + chunkSize));\n }\n if (taskFiles.length === 0) continue; // skip tasks with no files assigned\n for (const chunkFiles of chunks) {\n queue.push({\n task,\n chunk_files: chunkFiles,\n project_id: projectId,\n project_goal: projectGoal,\n plan_document: planDocument\n });\n }\n}\n\nconst staticData = $getWorkflowStaticData('global');\nstaticData.p2Results = [];\nstaticData.allTasks = sorted;\nstaticData._queueTotal = queue.length;\nstaticData._queueDone = 0;\n\nif (queue.length === 0) {\n return [{ json: { task: { task_id: 'NONE', description: 'No tasks', files: [] }, chunk_files: [], project_id: projectId, project_goal: projectGoal, plan_document: planDocument, _p2Queue: [] } }];\n}\n// Pass remaining queue items through data pipeline instead of static data\nconst first = queue[0];\nfirst._p2Queue = queue.slice(1);\nreturn [{ json: first }];"
},
"id": "spread-tasks",
"name": "Spread Tasks",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1850,
300
]
},
{
"parameters": {
"jsCode": "// P2: Stash Context \u2014 passes through all fields including _p2Queue for loop control\nconst staticData = $getWorkflowStaticData('global');\nstaticData._currentProjectId = $json.project_id;\nstaticData._currentTaskId = ($json.task || {}).task_id || '';\nstaticData._currentTask = $json.task || null;\nstaticData._currentProjectGoal = $json.project_goal || '';\nstaticData._currentQueue = $json._p2Queue || []; // snapshot queue for this iteration\nreturn [{ json: {\n task: $json.task,\n chunk_files: $json.chunk_files,\n project_id: $json.project_id,\n project_goal: $json.project_goal,\n plan_document: $json.plan_document,\n _p2Queue: $json._p2Queue || []\n} }];"
},
"id": "p2-stash",
"name": "P2: Stash Context",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2050,
300
]
},
{
"parameters": {
"jsCode": "// Builds coder input \u2014 task files in full, other files as compact summaries\nconst staticData = $getWorkflowStaticData('global');\nconst stashed = $json;\nconst taskFiles = (stashed.task || {}).files || stashed.chunk_files || [];\nconst allFiles = staticData._allFileContents || [];\n\n// Full content for files in THIS task + closely related files (imports)\nconst taskFileSet = new Set(taskFiles.map(f => typeof f === 'string' ? f : f.path));\n\n// Also include files that task files import from\nfor (const file of allFiles) {\n if (taskFileSet.has(file.path)) {\n // Find imports in this file\n const importMatches = (file.content || '').matchAll(/from\\s+['\"]([^'\"]+)['\"]/g);\n for (const m of importMatches) {\n const importPath = m[1];\n if (importPath.startsWith('.')) {\n // Resolve relative import to find the actual file\n const dir = file.path.replace(/\\/[^\\/]+$/, '');\n const resolved = importPath.replace(/^\\.\\//,'').replace(/^\\.\\.\\//, '');\n for (const f of allFiles) {\n if (f.path.includes(resolved) || f.path.replace(/\\.(ts|tsx|js|jsx)$/, '').endsWith(resolved)) {\n taskFileSet.add(f.path);\n }\n }\n }\n }\n }\n}\n\n// Always include package.json and main config files (small, critical for context)\nconst alwaysInclude = ['package.json', 'tsconfig.json', 'vite.config.js', 'tailwind.config.js', 'postcss.config.js', 'index.html'];\nfor (const f of alwaysInclude) {\n const match = allFiles.find(a => a.path === f || a.path.endsWith('/' + f));\n if (match) taskFileSet.add(match.path);\n}\n\n// Split: full content for task files, compact for everything else\nconst fullFiles = [];\nconst summaryFiles = [];\nfor (const file of allFiles) {\n if (file.path.includes('node_modules')) continue;\n if (taskFileSet.has(file.path)) {\n fullFiles.push(file);\n } else if (file.path.match(/\\.(ts|tsx|js|jsx|css|json|html)$/)) {\n // Compact summary: path + first line (exports) + size\n const firstLines = (file.content || '').split('\\n').slice(0, 3).join('\\n');\n const exports = (file.content || '').match(/export\\s+(default\\s+)?(?:function|const|class)\\s+(\\w+)/g) || [];\n summaryFiles.push({\n path: file.path,\n summary: exports.join(', ') || firstLines.substring(0, 100),\n size: (file.content || '').length\n });\n }\n}\n\nconst existingFiles = fullFiles;\n\nconst task = { ...(stashed.task || {}) };\nif (taskFiles.length > 0) task.files = taskFiles;\n\n// Extract design tokens from reference HTML files (e.g., Stitch concepts)\nconst http = require('http');\nconst projectId = stashed.project_id;\nlet designSystem = staticData._stitchDesignSystem || '';\nlet referenceStyles = staticData._stitchStyles || '';\n\n// Only fetch if not already cached this run AND task involves UI/visual work\nconst taskDesc = ((stashed.task || {}).description || '').toLowerCase();\nconst isVisualTask = taskDesc.includes('design') || taskDesc.includes('style') || taskDesc.includes('layout') || taskDesc.includes('ui') || taskDesc.includes('color') || taskDesc.includes('component') || taskDesc.includes('css') || taskDesc.includes('tailwind') || taskDesc.includes('visual') || taskDesc.includes('mockup') || taskDesc.includes('reference');\n\nif (!designSystem && isVisualTask && projectId) {\n try {\n // Fetch reference files list\n const refFiles = await new Promise((resolve) => {\n const req = http.request({\n hostname: 'file-api', port: 3456, method: 'GET',\n path: '/projects/' + projectId + '/files',\n headers: { 'Authorization': 'Bearer ' + ($env.FILE_API_TOKEN || '') },\n timeout: 10000\n }, res => {\n let d = ''; res.on('data', c => d += c);\n res.on('end', () => { try { resolve(JSON.parse(d).files || []); } catch { resolve([]); } });\n });\n req.on('error', () => resolve([]));\n req.end();\n });\n\n // Find HTML files in references/\n const htmlRefs = refFiles.filter(f => f.path.startsWith('references/') && f.path.endsWith('.html'));\n \n for (const ref of htmlRefs.slice(0, 2)) {\n const content = ref.content || '';\n if (content.length < 100) continue;\n \n // Extract Tailwind config\n const configMatch = content.match(/tailwind\\.config\\s*=\\s*(\\{[\\s\\S]*?\\})\\s*<\\/script>/);\n if (configMatch && !designSystem) {\n designSystem = configMatch[1].substring(0, 4000);\n staticData._stitchDesignSystem = designSystem;\n \n // Extract common component patterns from HTML and generate @apply classes\n const classPatterns = [];\n \n // Find repeated class combinations in the HTML (buttons, cards, badges, etc.)\n const classMatches = content.match(/class=\"([^\"]{30,})\"/g) || [];\n const classCounts = {};\n for (const match of classMatches) {\n const classes = match.replace('class=\"', '').replace('\"', '').trim();\n // Normalize whitespace\n const normalized = classes.replace(/\\s+/g, ' ').trim();\n classCounts[normalized] = (classCounts[normalized] || 0) + 1;\n }\n \n // Find patterns used 2+ times \u2014 these are component-worthy\n const componentClasses = [];\n for (const [classes, count] of Object.entries(classCounts)) {\n if (count >= 2 && classes.length > 20 && classes.length < 200) {\n // Generate a semantic name from the classes\n let name = 'component';\n if (classes.includes('rounded-full') && classes.includes('px-')) name = 'pill';\n else if (classes.includes('rounded-') && classes.includes('border')) name = 'card';\n else if (classes.includes('font-bold') && classes.includes('text-')) name = 'heading';\n else if (classes.includes('flex') && classes.includes('items-center')) name = 'row';\n else if (classes.includes('grid')) name = 'grid';\n else if (classes.includes('btn') || (classes.includes('cursor-pointer') && classes.includes('px-'))) name = 'btn';\n else if (classes.includes('backdrop-blur')) name = 'glass';\n \n componentClasses.push({ name: name + '-' + componentClasses.length, classes, count });\n }\n }\n \n if (componentClasses.length > 0) {\n // Generate @apply CSS\n let applyCSS = '@layer components {\\n';\n for (const comp of componentClasses.slice(0, 15)) {\n applyCSS += ' .' + comp.name + ' {\\n @apply ' + comp.classes.substring(0, 150) + ';\\n }\\n';\n }\n applyCSS += '}';\n staticData._stitchComponentClasses = applyCSS;\n }\n }\n \n // Extract style blocks\n const styleMatches = content.match(/<style>([\\s\\S]*?)<\\/style>/g);\n if (styleMatches && !referenceStyles) {\n referenceStyles = styleMatches.join('\\n').substring(0, 2000);\n staticData._stitchStyles = referenceStyles;\n }\n }\n } catch(e) {}\n}\n\nreturn [{ json: {\n task,\n existing_files: existingFiles,\n other_files_summary: summaryFiles,\n plan_document: stashed.plan_document || '',\n project_goal: stashed.project_goal || '',\n project_id: stashed.project_id,\n research_docs: staticData._researchDocs || '',\n design_system: designSystem,\n reference_styles: referenceStyles\n} }];"
},
"id": "p2-build-input",
"name": "P2: Build Code Input",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2250,
300
]
},
{
"parameters": {
"jsCode": "// CW: Prepare Message \u2014 restructured for instruction priority\n// Order: Task FIRST \u2192 Assets \u2192 Design System \u2192 Images \u2192 Files \u2192 Research (last)\nconst input = $json;\nconst planDocument = input.plan_document || '';\n\nlet taskDescription = input.task?.description || input.description || '';\nif (!taskDescription && input.task?.raw_output) {\n const raw = input.task.raw_output;\n const cleaned = raw.replace(/<\\/?task>/g, '').replace(/\\n+(?:Reasoning|Note):[\\s\\S]*/i, '').trim();\n try {\n const parsed = JSON.parse(cleaned);\n const list = Array.isArray(parsed) ? parsed : (parsed.tasks || []);\n taskDescription = list.map(t => `${t.task_id}: ${t.description}`).join('\\n\\n');\n } catch(e) { taskDescription = raw; }\n}\n\nconst taskFiles = (input.task || {}).files || [];\nconst filesConstraint = taskFiles.length > 0\n ? '\\n\\nFILES TO MODIFY (only output these files):\\n' + taskFiles.join('\\n')\n : '';\n\nconst existingFiles = input.existing_files || [];\nconst otherFilesSummary = input.other_files_summary || [];\nconst existingFilesSection = existingFiles.length > 0\n ? '\\n\\nEXISTING PROJECT FILES:\\n' +\n (() => { let total = 0; const MAX = 40000; return existingFiles.filter(f => { const size = f.path.length + (f.content||'').length + 20; if (total + size > MAX) return false; total += size; return true; }).map(f => `### ${f.path}\\n\\`\\`\\`\\n${f.content}\\n\\`\\`\\``).join('\\n\\n'); })() + (otherFilesSummary.length > 0 ? '\\n\\nOTHER PROJECT FILES (not shown in full):\\n' + otherFilesSummary.map(f => `- ${f.path} (${f.size} chars): ${f.summary}`).join('\\n') : '')\n : '';\n\nconst researchDocs = input.research_docs || '';\n\n// Detect image assets in public/assets/\nconst assetFiles = existingFiles\n .filter(f => f.path.match(/^public\\/assets\\/.*\\.(png|jpg|jpeg|svg|gif|webp)$/i))\n .map(f => f.path);\nconst assetWarning = assetFiles.length > 0\n ? '\\n\\nAVAILABLE ASSETS (use <img src> for these, do NOT recreate them):\\n' +\n assetFiles.map(a => `- ${a} \u2192 <img src=\"/${a.replace('public/', '')}\" />`).join('\\n')\n : '';\n\n// Design system tokens from reference HTML\nconst designSystem = input.design_system || $getWorkflowStaticData('global')._stitchDesignSystem || '';\nconst stitchStyles = input.reference_styles || $getWorkflowStaticData('global')._stitchStyles || '';\n\n// Load reference images\nconst http = require('http');\nconst projectId = input.project_id || $('Extract Input').first().json.project_id || '';\nconst FILE_API = ($env.FILE_API_URL || 'http://file-api:3456').replace('http://', '');\nconst [apiHost, apiPort] = FILE_API.split(':');\nconst refImages = await new Promise((resolve) => {\n const req = http.request({\n hostname: apiHost, port: parseInt(apiPort) || 3456,\n path: `/projects/${projectId}/references`,\n method: 'GET',\n headers: { 'Authorization': 'Bearer ' + ($env.FILE_API_TOKEN || '') },\n timeout: 10000\n }, (res) => {\n let data = '';\n res.on('data', d => data += d);\n res.on('end', () => { try { resolve(JSON.parse(data).references || []); } catch { resolve([]); } });\n });\n req.on('error', () => resolve([]));\n req.on('timeout', () => { req.destroy(); resolve([]); });\n req.end();\n});\n\nconst webhookImage = $('Extract Input').first().json.image_data || null;\nconst taskId = (input.task || {}).task_id || '';\nconst taskConcepts = $getWorkflowStaticData('global')._taskConcepts || {};\nconst taskConcept = taskConcepts[taskId] || null;\n\n// \u2500\u2500 TASK-SPECIFIC MCP DOCS (fetch targeted docs for this task) \u2500\u2500\nconst taskLower = taskDescription.toLowerCase();\nif (taskLower.includes('gsap') || taskLower.includes('scrolltrigger') || taskLower.includes('animation') || taskLower.includes('scroll')) {\n try {\n const http = require('http');\n const apis = [];\n if (taskLower.includes('scrolltrigger') || taskLower.includes('scroll')) apis.push('ScrollTrigger');\n if (taskLower.includes('timeline')) apis.push('gsap.timeline');\n if (taskLower.includes('gsap.from') || taskLower.includes('opacity')) apis.push('gsap.from');\n if (apis.length === 0) apis.push('ScrollTrigger', 'gsap.to');\n \n for (const api of apis) {\n try {\n const mcpBody = JSON.stringify({ jsonrpc: '2.0', method: 'tools/call', params: { name: 'get_gsap_api_expert', arguments: { api_element: api, level: 'intermediate' } }, id: Date.now() });\n const initBody = JSON.stringify({ jsonrpc: '2.0', method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'eek-coder', version: '1.0' } }, id: Date.now() });\n \n // Init session\n const sid = await new Promise((resolve) => {\n const req = http.request({ hostname: 'docky', port: 8811, path: '/mcp', method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Accept': 'application/json, text/event-stream', 'Content-Length': Buffer.byteLength(initBody) }\n }, res => {\n resolve(res.headers['mcp-session-id'] || '');\n res.resume();\n });\n req.on('error', () => resolve(''));\n req.setTimeout(5000, () => { req.destroy(); resolve(''); });\n req.write(initBody); req.end();\n });\n \n if (!sid) continue;\n \n const result = await new Promise((resolve) => {\n const req = http.request({ hostname: 'docky', port: 8811, path: '/mcp', method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Accept': 'application/json, text/event-stream', 'Content-Length': Buffer.byteLength(mcpBody), 'Mcp-Session-Id': sid }\n }, res => {\n let data = ''; res.on('data', c => data += c);\n res.on('end', () => {\n for (const line of data.split('\\n')) {\n if (line.startsWith('data: ') || line.startsWith('data:')) {\n try { const d = JSON.parse(line.replace(/^data:\\s?/, '')); \n const text = (d.result?.content || []).map(c => c.text || '').join('');\n if (text.length > 100 && !text.includes('Error')) resolve(text);\n else resolve('');\n } catch { resolve(''); }\n return;\n }\n }\n resolve('');\n });\n });\n req.on('error', () => resolve(''));\n req.setTimeout(10000, () => { req.destroy(); resolve(''); });\n req.write(mcpBody); req.end();\n });\n \n if (result) {\n researchDocs = (researchDocs ? researchDocs + '\\n\\n' : '') + '## GSAP: ' + api + '\\n' + result.substring(0, 3000);\n }\n } catch {}\n }\n } catch {}\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// BUILD THE PROMPT \u2014 instruction-first order\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n// SYSTEM: Short, focused rules only\nconst systemMessage = `You are a Senior Full-Stack Developer. Output ONLY code file blocks.\n\nENGINEERING PRINCIPLES:\n- Fix root causes, never work around errors or suppress them\n- Keep it simple \u2014 write the minimum code needed, no over-engineering\n- Don't add features, abstractions, or utilities that weren't asked for\n- Match existing patterns \u2014 if the project uses X style, follow X style\n- One component/function = one responsibility\n- Name things clearly \u2014 if you need a comment to explain a variable name, rename it\n- No dead code, no commented-out code, no console.logs left behind\n- Handle errors at system boundaries (user input, API calls), trust internal code\n- Prefer modifying existing files over creating new ones\n- DRY \u2014 but three similar lines are better than a premature abstraction\n\nFORMAT \u2014 two modes depending on whether the file exists:\n\nFOR EXISTING FILES \u2014 use SEARCH/REPLACE blocks (targeted edits, not full rewrites):\nFILE: path/to/file.ext\n<<<<<<< SEARCH\n[exact lines to find \u2014 copy from the existing file precisely]\n=======\n[replacement lines]\n>>>>>>> REPLACE\n\nYou can have multiple SEARCH/REPLACE blocks per file. Each block changes one section.\nThe SEARCH text must match the existing file EXACTLY \u2014 same whitespace, same indentation.\nInclude enough context lines (3-5) around the change to make the match unique.\n\nFOR NEW FILES (not in existing project) \u2014 use full content:\n### path/to/file.ext\n\\`\\`\\`ext\n[complete file content]\n\\`\\`\\`\n\nRULES:\n- For EXISTING files: use SEARCH/REPLACE blocks. For NEW files: output complete content\n- Only output files listed in FILES TO MODIFY\n- No explanations, no prose \u2014 ONLY file blocks\n- Files with JSX MUST use .tsx extension\n- NEVER remove @tailwind directives from CSS files\n- NEVER remove existing imports that are still used\n- When modifying a file, preserve everything that works \u2014 only change what the task asks for\n- NEVER rewrite a file from scratch. Keep the existing structure, imports, hooks, and component logic intact. Only modify the specific lines related to the task\n- If a file uses useGame(), keep useGame(). If it imports poop.png, keep poop.png. If it has a specific component structure, keep that structure`;\n\n// USER: Task first, then supporting context\nconst content = [];\n\n// \u2500\u2500 1. TASK DESCRIPTION (highest priority \u2014 read this first) \u2500\u2500\ncontent.push({ type: 'text', text: `YOUR TASK:\\n${taskDescription}${assetWarning}${filesConstraint}` });\n\n// \u2500\u2500 1.5 PROJECT MEMORY (continuity from previous runs) \u2500\u2500\nconst projectMemory = $getWorkflowStaticData('global')._projectMemory || '';\nif (projectMemory) {\n content.push({ type: 'text', text: 'PROJECT CONTEXT (from previous pipeline runs \u2014 do NOT create or modify memory.md):\\n' + projectMemory.substring(0, 3000) });\n}\n\n// \u2500\u2500 2. DESIGN SYSTEM (if available \u2014 use these tokens) \u2500\u2500\nif (designSystem) {\n content.push({ type: 'text', text: 'DESIGN SYSTEM (follow these design principles, color palette, font choices, and spacing patterns):\\n' + designSystem.substring(0, 3000) });\n}\nif (stitchStyles) {\n content.push({ type: 'text', text: 'REFERENCE CSS PATTERNS:\\n' + stitchStyles.substring(0, 1500) });\n}\nconst componentClasses = $getWorkflowStaticData('global')._stitchComponentClasses || '';\nif (componentClasses) {\n content.push({ type: 'text', text: 'COMPONENT CLASSES (add these to your CSS file and use the semantic class names instead of repeating utility classes):\\n' + componentClasses });\n}\n\n// \u2500\u2500 3. REFERENCE IMAGES (visual target) \u2500\u2500\nif (refImages.length > 0) {\n for (const ref of refImages) {\n content.push({ type: 'image_url', image_url: { url: 'data:image/png;base64,' + ref.base64 } });\n const label = ref.filename.replace(/\\.(png|jpg|jpeg|gif|webp)$/i, '').replace(/[_-]/g, ' ');\n content.push({ type: 'text', text: 'REFERENCE (' + label + '):' });\n }\n} else if (webhookImage) {\n content.push({ type: 'image_url', image_url: { url: 'data:image/png;base64,' + webhookImage } });\n content.push({ type: 'text', text: 'REFERENCE DESIGN:' });\n}\nif (taskConcept) {\n content.push({ type: 'image_url', image_url: { url: 'data:image/png;base64,' + taskConcept } });\n content.push({ type: 'text', text: 'TASK CONCEPT (visual guide for this task):' });\n}\n\n// \u2500\u2500 4. EXISTING FILES (context for the coder) \u2500\u2500\nif (existingFilesSection) {\n content.push({ type: 'text', text: existingFilesSection });\n}\n\n// \u2500\u2500 5. ARCHITECTURE + RESEARCH (lowest priority \u2014 background context) \u2500\u2500\nif (planDocument) {\n content.push({ type: 'text', text: 'ARCHITECTURE:\\n' + planDocument.substring(0, 3000) });\n}\nif (researchDocs) {\n content.push({ type: 'text', text: 'LIBRARY DOCS:\\n' + researchDocs.substring(0, 6000) });\n}\n\nconst messageContent = content.length > 1 ? content : content[0].text;\n\nreturn [{ json: {\n model: $env.CODER_MODEL || 'qwen3.5-27b@q4_k_m',\n messages: [\n { role: 'system', content: systemMessage },\n { role: 'user', content: messageContent }\n ],\n temperature: 0.6,\n chat_template_kwargs: { enable_thinking: false },\n top_p: 0.95,\n top_k: 20,\n min_p: 0.0,\n presence_penalty: 0.0,\n max_tokens: parseInt($env.CODER_MAX_TOKENS) || 32768\n} }];"
},
"id": "cw-prepare",
"name": "CW: Prepare Message",
"type": "n8n-nodes-base.code",
"typeVers
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
eek-Go v3. Uses httpRequest. Webhook trigger; 55 nodes.
Source: https://github.com/eekanti/eek.GO/blob/16654b8428e6d4ada43b42faf3aef2c5e46521a8/workflows/eek-go-v3.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
This n8n template provides enterprise-level version control for your workflows using GitHub integration. Stop losing hours to broken workflows and manual exports – get proper commit history, visual di
This flow creates dummy files for every item added in your *Arrs (Radarr/Sonarr) with the tag .
This workflow receives webhook requests from a content calendar and uses the X API v2 to publish text posts, threads, image/video posts, and polls, as well as delete existing posts and run a credentia
This workflow acts as a central API gateway for all technical indicator agents in the Binance Spot Market Quant AI system. It listens for incoming webhook requests and dynamically routes them to the c
Sign PDF documents with legally-compliant digital signatures using X.509 certificates. Supports multiple PAdES signature levels (B, T, LT, LTA) with optional visible stamps.