${title}
\nFrontend + Backend scaffolded by OmniDev
\n/* eslint-disable @typescript-eslint/no-explicit-any */ import { NextRequest, NextResponse } from "next/server"; import { GoogleGenAI } from "@google/genai"; import JSON5 from "json5"; import { InferenceClient } from "@huggingface/inference"; import type { AugmentRequest, AugmentResponse, FileUpdate } from "@/types"; const SYS = `You are Omni Engine, an autonomous code evolution system. INPUTS: - project context (tree + critical files) - a user instruction OUTPUT: - STRICT JSON ONLY. No prose. No code fences. No leading or trailing text. - Prefer this object shape: { "ok": true, "summary": string, "logs": string[], "files": [ { "path": string, "action": "add"|"update"|"delete", "content"?: string, "note"?: string } ] } - If you cannot produce an object, return a top-level JSON array of the same file objects (files array). RULES: - Paths must be POSIX-like and rooted from repo (e.g., /frontend/src/App.tsx) - For add/update include full file content (runnable) - Keep changes consistent and compilable for the chosen language/framework - DO NOT include markdown fences or explanation outside JSON `; function cleanText(raw: string): string { let t = raw || ""; t = t.replace(/```[a-zA-Z]*\n?/g, ""); t = t.replace(/```/g, ""); return t.trim(); } function tryParseAny(jsonish: string): any { // Try strict JSON first try { return JSON.parse(jsonish); } catch { // Relaxed JSON5 parse (handles trailing commas, etc.) try { // Prefer full string return JSON5.parse(jsonish); } catch { // Try to extract object const sObj = jsonish.indexOf("{"); const eObj = jsonish.lastIndexOf("}"); if (sObj >= 0 && eObj > sObj) { const candidate = jsonish.slice(sObj, eObj + 1); try { return JSON.parse(candidate); } catch {} try { return JSON5.parse(candidate); } catch {} } // Try to extract array const sArr = jsonish.indexOf("["); const eArr = jsonish.lastIndexOf("]"); if (sArr >= 0 && eArr > sArr) { const candidate = jsonish.slice(sArr, eArr + 1); try { return JSON.parse(candidate); } catch {} try { return JSON5.parse(candidate); } catch {} } throw new Error("Model did not return valid JSON"); } } } function findJsonCandidates(text: string, maxCandidates = 8): string[] { const candidates: string[] = []; const cleaned = cleanText(text); // 1) Code-fenced JSON blocks (already stripped in cleanText, so skip) // 2) Balanced scanning for objects and arrays const openers = ['{', '['] as const; for (let start = 0; start < cleaned.length; start++) { const ch = cleaned[start]; if (!openers.includes(ch as any)) continue; const isArray = ch === '['; const closer = isArray ? ']' : '}'; let depth = 0; let inStr: null | '"' | "'" = null; let prev = ''; for (let i = start; i < cleaned.length; i++) { const c = cleaned[i]; if (inStr) { if (c === inStr && prev !== '\\') inStr = null as any; } else { if (c === '"' || c === "'") inStr = c as any; else if (c === ch) depth++; else if (c === closer) depth--; if (depth === 0) { const slice = cleaned.slice(start, i + 1).trim(); if ((isArray && slice.startsWith('[') && slice.endsWith(']')) || (!isArray && slice.startsWith('{') && slice.endsWith('}'))) { candidates.push(slice); if (candidates.length >= maxCandidates) return candidates; } break; } } prev = c; } } return candidates; } export async function POST(req: NextRequest) { try { const body = (await req.json()) as AugmentRequest; const { context, instruction, language = "javascript", framework = "express-react", response_type = "file_updates", model, provider } = body || {} as AugmentRequest; if (!context || !instruction) { return NextResponse.json({ ok: false, message: "Missing context or instruction" } as AugmentResponse, { status: 400 }); } const userPrompt = `Project Context:\n${context}\n\nInstruction:\n${instruction}\n\nTarget:\n- language: ${language}\n- framework: ${framework}\n- response_type: ${response_type}`; let text = ""; if ((provider || "").toLowerCase() === "google" || (model || "").toLowerCase().startsWith("gemini-")) { const apiKey = process.env.GEMINI_API_KEY; if (!apiKey) return NextResponse.json({ ok: false, message: "Missing GEMINI_API_KEY" } as AugmentResponse, { status: 500 }); const ai = new GoogleGenAI({ apiKey }); const res = await ai.models.generateContent({ model: model || "gemini-2.5-flash", contents: [ { role: "user", parts: [{ text: SYS }] }, { role: "user", parts: [{ text: userPrompt }] }, ], config: { maxOutputTokens: 4096 }, } as any); text = ((res as any)?.response?.text && (res as any).response.text()) || (res as any)?.text || ((res as any)?.candidates?.[0]?.content?.parts?.map((p: any) => p?.text || "").join("") || ""); // Fallback: single user message with combined system + prompt if (!text || !String(text).trim()) { const res2 = await ai.models.generateContent({ model: model || "gemini-2.5-flash", contents: [ { role: 'user', parts: [{ text: `${SYS}\n\n${userPrompt}` }] } ], config: { maxOutputTokens: 4096 }, } as any); text = ((res2 as any)?.response?.text && (res2 as any).response.text()) || (res2 as any)?.text || ((res2 as any)?.candidates?.[0]?.content?.parts?.map((p: any) => p?.text || "").join("") || ""); } } else { const token = process.env.HF_TOKEN || process.env.DEFAULT_HF_TOKEN; const client = new InferenceClient(token); const res = await client.chatCompletion({ model: model || "deepseek-ai/DeepSeek-V3.1", provider: (provider as any) || undefined, messages: [ { role: "system", content: SYS }, { role: "user", content: userPrompt }, ], }); text = res.choices?.[0]?.message?.content || ""; } const cleaned = cleanText(text); if (!cleaned) { const title = instruction.match(/Title:\s*(.*)/)?.[1] || "OmniDev App"; return NextResponse.json({ ok: true, files: generateFallbackFiles(framework, language, title) } as AugmentResponse, { status: 200 }); } let json: any; let parseErr: any = null; try { json = tryParseAny(cleaned); } catch (e: any) { parseErr = e; // Try candidates extracted by balanced scanning const cands = findJsonCandidates(cleaned, 12); for (const c of cands) { try { json = tryParseAny(c); parseErr = null; break; } catch {} } // Last resort: attempt to coerce by simple replacements if (parseErr) { try { const coerced = cleaned .replace(/(\w+)\s*:/g, '"$1":') // quote keys .replace(/'([^']*)'/g, '"$1"'); // single to double quotes json = tryParseAny(coerced); parseErr = null; } catch {} } // REPAIR PASS via Gemini: convert arbitrary text to strict JSON per schema if (parseErr) { try { const apiKey = process.env.GEMINI_API_KEY; if (apiKey) { const ai = new GoogleGenAI({ apiKey }); const repairSys = `You are a JSON repair tool. Convert the user's content into STRICT JSON matching either: { ok, files[] } or just files[]. files[] is an array of { path, action, content?, note? }. No prose, no code fences.`; const resR = await ai.models.generateContent({ model: "gemini-2.5-flash", contents: [ { role: 'user', parts: [{ text: repairSys }] }, { role: 'user', parts: [{ text: cleaned }] }, ], config: { maxOutputTokens: 2048 }, } as any); const repaired = ((resR as any)?.response?.text && (resR as any).response.text()) || (resR as any)?.text || ((resR as any)?.candidates?.[0]?.content?.parts?.map((p: any) => p?.text || "").join("") || ""); const repairedClean = cleanText(repaired || ""); if (repairedClean) { json = tryParseAny(repairedClean); parseErr = null; } } } catch {} } if (parseErr) { // As a last-resort, generate a minimal runnable scaffold so the flow never breaks const files = generateFallbackFiles(framework, language, (instruction.match(/Title:\s*(.*)/)?.[1] || "OmniDev App")); return NextResponse.json({ ok: true, files } as AugmentResponse, { status: 200 }); } } // Minimal validation if (json && json.ok && Array.isArray(json.files)) { return NextResponse.json(json as AugmentResponse, { status: 200 }); } // If model returned array at root, assume it's files if (Array.isArray(json)) { return NextResponse.json({ ok: true, files: json } as AugmentResponse, { status: 200 }); } // If the object contains files nested under any key, lift it if (json && typeof json === 'object') { const stack: any[] = [json]; while (stack.length) { const cur = stack.pop(); if (cur && typeof cur === 'object') { if (Array.isArray(cur.files)) { return NextResponse.json({ ok: true, files: cur.files, summary: cur.summary, logs: cur.logs } as AugmentResponse, { status: 200 }); } for (const k of Object.keys(cur)) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const v: any = (cur as any)[k]; if (v && typeof v === 'object') stack.push(v); } } } } // Shape still unexpected: provide runnable fallback const files = generateFallbackFiles(framework, language, (instruction.match(/Title:\s*(.*)/)?.[1] || "OmniDev App")); return NextResponse.json({ ok: true, files } as AugmentResponse, { status: 200 }); } catch (e: any) { return NextResponse.json({ ok: false, message: e?.message || "Internal error" } as AugmentResponse, { status: 500 }); } } function generateFallbackFiles(framework: string, language: string, title: string): FileUpdate[] { const isTs = language.toLowerCase().includes('ts'); if (framework?.startsWith('express')) { const server = `import express from 'express';\nimport cors from 'cors';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst app = express();\napp.use(cors());\nconst distPath = path.join(__dirname, '..', 'frontend', 'dist');\napp.use(express.static(distPath));\napp.get('/health', (_, res) => res.json({ ok: true }));\napp.get('*', (req, res) => { try { res.sendFile(path.join(distPath, 'index.html')); } catch { res.send('OK'); } });\nconst PORT = process.env.PORT || 3000;\napp.listen(PORT, () => console.log('Server listening on', PORT));\n`; const indexHtml = `\n\n
\n\n\nFrontend + Backend scaffolded by OmniDev
\nNext.js scaffold by OmniDev