kalhdrawi's picture
Reupload OmniDev clean version
c89a5a0
raw
history blame
14.1 kB
/* 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 = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n<title>${title}</title>\n<script src=\"https://cdn.tailwindcss.com\"></script>\n</head>\n<body class=\"bg-neutral-950 text-white\">\n<div id=\"root\"></div>\n<script type=\"module\" src=\"/src/main.${isTs ? 'tsx' : 'jsx'}\"></script>\n</body>\n</html>`;
const main = `import React from 'react';\nimport { createRoot } from 'react-dom/client';\nimport App from './App';\ncreateRoot(document.getElementById('root')).render(<App />);\n`;
const app = `export default function App(){\n return (<main className=\"min-h-screen grid place-content-center\">\n <h1 className=\"text-3xl font-bold\">${title}</h1>\n <p className=\"text-neutral-400\">Frontend + Backend scaffolded by OmniDev</p>\n </main>);\n}\n`;
const fePkg = { name: "frontend", private: true, scripts: { dev: "vite", build: "vite build", preview: "vite preview" }, dependencies: { react: "^18.3.1", "react-dom": "^18.3.1" }, devDependencies: { vite: "^5.4.0" } };
const bePkg = { name: "backend", private: true, type: "module", scripts: { start: "node server.js" }, dependencies: { express: "^4.19.0", cors: "^2.8.5" } };
const files: FileUpdate[] = [
{ path: "/backend/server.js", action: "add", content: server },
{ path: "/backend/package.json", action: "add", content: JSON.stringify(bePkg, null, 2) },
{ path: "/frontend/index.html", action: "add", content: indexHtml },
{ path: `/frontend/src/main.${isTs ? 'tsx' : 'jsx'}`, action: "add", content: main },
{ path: `/frontend/src/App.${isTs ? 'tsx' : 'jsx'}`, action: "add", content: app },
{ path: "/frontend/package.json", action: "add", content: JSON.stringify(fePkg, null, 2) },
{ path: "/README.md", action: "add", content: `# ${title}\n\nGenerated by OmniDev scaffold.` },
];
return files;
}
if (framework?.startsWith('next')) {
const page = `export default function Page(){ return (<main style={{minHeight:'100vh',display:'grid',placeContent:'center'}}><h1>${title}</h1><p>Next.js scaffold by OmniDev</p></main>); }`;
const api = `export async function GET(){ return Response.json({ ok: true }); }`;
const pkg = { name: "next-app", private: true, scripts: { dev: "next dev", build: "next build", start: "next start" }, dependencies: { next: "^15.0.0", react: "^18.3.1", "react-dom": "^18.3.1" } };
return [
{ path: "/app/page.tsx", action: "add", content: page },
{ path: "/app/api/health/route.ts", action: "add", content: api },
{ path: "/package.json", action: "add", content: JSON.stringify(pkg, null, 2) },
{ path: "/README.md", action: "add", content: `# ${title}\n\nGenerated by OmniDev scaffold.` },
];
}
return [ { path: "/README.md", action: "add", content: `# ${title}\n\nOmniDev scaffold.` } ];
}