import express from 'express'; import bodyParser from 'body-parser'; import cors from 'cors'; import { StateManager } from './stateManager.js'; import { AIEngine } from './aiEngine.js'; import fs from 'fs'; import admin from 'firebase-admin'; // --- FIREBASE SETUP --- // Using environment variable injection as requested let db = null; try { if (process.env.FIREBASE_SERVICE_ACCOUNT_JSON && process.env.FIREBASE_SERVICE_ACCOUNT_JSON !== "") { const serviceAccount = JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT_JSON); admin.initializeApp({ credential: admin.credential.cert(serviceAccount), // Make sure to replace this URL or add it to env variables as well databaseURL: "https://shago-web-default-rtdb.firebaseio.com" }); db = admin.database(); console.log("๐Ÿ”ฅ Firebase Connected"); } else { console.warn("โš ๏ธ No FIREBASE_SERVICE_ACCOUNT_JSON found. Running in Memory-Only mode."); } } catch (e) { console.error("Firebase Init Error:", e); } const app = express(); const PORT = process.env.PORT || 7860; // Load Prompts for Server-Side Logic Checks const sysPrompts = JSON.parse(fs.readFileSync('./prompts.json', 'utf8')); app.use(cors()); app.use(bodyParser.json({ limit: '50mb' })); // --- HELPERS --- const validateRequest = (req, res, next) => { if (req.path.includes('/onboarding')) return next(); const { userId, projectId } = req.body; if (!userId || !projectId) return res.status(400).json({ error: "Missing ID" }); next(); }; function extractWorkerPrompt(text) { const match = text.match(/WORKER_PROMPT:\s*(.*)/s); return match ? match[1].trim() : null; } function formatContext({ hierarchyContext, scriptContext, logContext }) { let out = ""; if (scriptContext) out += `\n[TARGET SCRIPT]: ${scriptContext.targetName}\n[SOURCE PREVIEW]: ${scriptContext.scriptSource?.substring(0, 1000)}...`; if (logContext) out += `\n[LAST LOGS]: ${logContext.logs}`; return out; } function extractPMQuestion(text) { const match = text.match(/\[ASK_PM:\s*(.*?)\]/s); return match ? match[1].trim() : null; } function extractImagePrompt(text) { const match = text.match(/\[GENERATE_IMAGE:\s*(.*?)\]/s); return match ? match[1].trim() : null; } // --- ONBOARDING ENDPOINTS --- /** * 1. ANALYZE */ app.post('/onboarding/analyze', validateRequest, async (req, res) => { const { description } = req.body; if (!description) return res.status(400).json({ error: "Description required" }); try { console.log(`[Onboarding] Analyzing idea...`); const result = await AIEngine.generateEntryQuestions(description); if (result.status === "REJECTED") { return res.json({ rejected: true, reason: result.reason || "Idea violates TOS or guidelines." }); } res.json({ questions: result.questions }); } catch (err) { console.error(err); res.status(500).json({ error: "Analysis failed" }); } }); /** * 2. CREATE */ app.post('/onboarding/create', validateRequest, async (req, res) => { const { userId, description, answers } = req.body; const projectId = "proj_" + Date.now(); try { console.log(`[Onboarding] Grading Project ${projectId}...`); // STEP 1: GRADE const grading = await AIEngine.gradeProject(description, answers); // STEP 2: CHECK FAIL CONDITIONS (Relaxed) // Only fail if Feasibility is extremely low (< 30) or Rating is F const isFailure = grading.feasibility < 30 || grading.rating === 'F'; let thumbnailBase64 = null; if (isFailure) { console.log(`[Onboarding] โŒ Project Failed Grading (${grading.rating}). Skipping Image.`); } else { console.log(`[Onboarding] โœ… Passed. Generating Thumbnail...`); // --- CRITICAL FIX HERE --- // We MUST send the description so the AI knows what to draw. const imagePrompt = `Game Title: ${grading.title}. Core Concept: ${description}`; thumbnailBase64 = await AIEngine.generateImage(imagePrompt); } const projectData = { id: projectId, userId, title: grading.title || "Untitled Project", description, answers, stats: grading, thumbnail: thumbnailBase64, // ? `data:image/png;base64,${thumbnailBase64}` : null, createdAt: Date.now(), status: isFailure ? "rejected" : "initialized" }; if (db) await db.ref(`projects/${projectId}`).set(projectData); if (!isFailure) { await StateManager.updateProject(projectId, { ...projectData, workerHistory: [], pmHistory: [], failureCount: 0 }); } res.json({ success: !isFailure, projectId, stats: grading, title: projectData.title, thumbnail: projectData.thumbnail }); } catch (err) { console.error("Create Error:", err); res.status(500).json({ error: "Creation failed" }); } }); // --- CORE WORKFLOW ENDPOINTS --- /** * 3. INITIALIZE WORKSPACE (PM Generates GDD -> First Task) */ app.post('/new/project', validateRequest, async (req, res) => { const { userId, projectId, description } = req.body; try { const pmHistory = []; const gddPrompt = `Create a comprehensive GDD for: ${description}`; const gddResponse = await AIEngine.callPM(pmHistory, gddPrompt); pmHistory.push({ role: 'user', parts: [{ text: gddPrompt }] }); pmHistory.push({ role: 'model', parts: [{ text: gddResponse }] }); const taskPrompt = "Based on the GDD, generate the first technical milestone.\nOutput format:\nTASK_NAME: \nWORKER_PROMPT: "; const taskResponse = await AIEngine.callPM(pmHistory, taskPrompt); pmHistory.push({ role: 'user', parts: [{ text: taskPrompt }] }); pmHistory.push({ role: 'model', parts: [{ text: taskResponse }] }); const initialWorkerInstruction = extractWorkerPrompt(taskResponse) || `Initialize structure for: ${description}`; const workerHistory = []; const initialWorkerPrompt = `CONTEXT: New Project. \nINSTRUCTION: ${initialWorkerInstruction}`; const workerResponse = await AIEngine.callWorker(workerHistory, initialWorkerPrompt, []); workerHistory.push({ role: 'user', parts: [{ text: initialWorkerPrompt }] }); workerHistory.push({ role: 'model', parts: [{ text: workerResponse }] }); // Update State await StateManager.updateProject(projectId, { userId, pmHistory, workerHistory, gdd: gddResponse, failureCount: 0 }); // Queue Actions await processAndQueueResponse(projectId, workerResponse); res.json({ success: true, message: "Workspace Initialized", gddPreview: gddResponse.substring(0, 200) }); } catch (err) { console.error("Init Error:", err); res.status(500).json({ error: "Initialization Failed" }); } }); /** * 4. FEEDBACK LOOP (The "Brain") */ app.post('/project/feedback', async (req, res) => { const { projectId, prompt, hierarchyContext, scriptContext, logContext, taskComplete, images } = req.body; const project = await StateManager.getProject(projectId); if (!project) return res.status(404).json({ error: "Project not found." }); // A. TASK COMPLETE -> SCOPE SWITCH if (taskComplete) { console.log(`[${projectId}] โœ… TASK COMPLETE.`); const summary = `Worker completed the previous task. Logs: ${logContext?.logs || "Clean"}. \nGenerate the NEXT task using 'WORKER_PROMPT:' format.`; const pmResponse = await AIEngine.callPM(project.pmHistory, summary); project.pmHistory.push({ role: 'user', parts: [{ text: summary }] }); project.pmHistory.push({ role: 'model', parts: [{ text: pmResponse }] }); const nextInstruction = extractWorkerPrompt(pmResponse); if (!nextInstruction) { await StateManager.updateProject(projectId, { pmHistory: project.pmHistory, status: "IDLE" }); return res.json({ success: true, message: "No further tasks. Project Idle." }); } console.log(`[${projectId}] ๐Ÿงน Nuking Worker for next task.`); const newWorkerHistory = []; const newPrompt = `New Objective: ${nextInstruction}`; const workerResponse = await AIEngine.callWorker(newWorkerHistory, newPrompt, []); newWorkerHistory.push({ role: 'user', parts: [{ text: newPrompt }] }); newWorkerHistory.push({ role: 'model', parts: [{ text: workerResponse }] }); await StateManager.updateProject(projectId, { pmHistory: project.pmHistory, workerHistory: newWorkerHistory, failureCount: 0 }); StateManager.queueCommand(projectId, { type: "EXECUTE", payload: "warn('Starting Next Task...')" }); await processAndQueueResponse(projectId, workerResponse); return res.json({ success: true, message: "Next Task Assigned" }); } // B. ERROR ESCALATION let isFailure = false; if (logContext?.logs) { const errs = ["Error", "Exception", "failed", "Stack Begin", "Infinite yield"]; if (errs.some(k => logContext.logs.includes(k))) { isFailure = true; project.failureCount = (project.failureCount || 0) + 1; } } if (project.failureCount > 3) { console.log(`[${projectId}] ๐Ÿšจ Escalating to PM...`); const pmPrompt = sysPrompts.pm_guidance_prompt.replace('{{LOGS}}', logContext?.logs); const pmVerdict = await AIEngine.callPM(project.pmHistory, pmPrompt); if (pmVerdict.includes("[TERMINATE]")) { const fixInstruction = pmVerdict.replace("[TERMINATE]", "").trim(); const resetHistory = []; const resetPrompt = `[SYSTEM]: Previous worker terminated. \nNew Objective: ${fixInstruction}`; const workerResp = await AIEngine.callWorker(resetHistory, resetPrompt, []); resetHistory.push({ role: 'user', parts: [{ text: resetPrompt }] }); resetHistory.push({ role: 'model', parts: [{ text: workerResp }] }); await StateManager.updateProject(projectId, { workerHistory: resetHistory, failureCount: 0 }); StateManager.queueCommand(projectId, { type: "EXECUTE", payload: "print('SYSTEM: Worker Reset')" }); await processAndQueueResponse(projectId, workerResp); return res.json({ success: true, message: "Worker Terminated." }); } else { const injection = `[PM GUIDANCE]: ${pmVerdict} \n\nApply this fix now.`; const workerResp = await AIEngine.callWorker(project.workerHistory, injection, []); project.workerHistory.push({ role: 'user', parts: [{ text: injection }] }); project.workerHistory.push({ role: 'model', parts: [{ text: workerResp }] }); await StateManager.updateProject(projectId, { workerHistory: project.workerHistory, failureCount: 1 }); await processAndQueueResponse(projectId, workerResp); return res.json({ success: true, message: "PM Guidance Applied." }); } } // C. STANDARD LOOP + CONSULTATION try { const fullInput = `USER: ${prompt || "Automatic Feedback"}` + formatContext({ hierarchyContext, scriptContext, logContext }); let response = await AIEngine.callWorker(project.workerHistory, fullInput, images || []); // CHECK: Consultant Question const pmQuestion = extractPMQuestion(response); if (pmQuestion) { console.log(`[${projectId}] ๐Ÿ™‹ Worker asking PM: "${pmQuestion}"`); const pmConsultPrompt = `[WORKER CONSULTATION]: The Worker asks: "${pmQuestion}"\nProvide a technical answer to unblock them.`; const pmAnswer = await AIEngine.callPM(project.pmHistory, pmConsultPrompt); // Update PM Context project.pmHistory.push({ role: 'user', parts: [{ text: pmConsultPrompt }] }); project.pmHistory.push({ role: 'model', parts: [{ text: pmAnswer }] }); // Feed Answer Back const injectionMsg = `[PM RESPONSE]: ${pmAnswer}`; project.workerHistory.push({ role: 'user', parts: [{ text: fullInput }] }); project.workerHistory.push({ role: 'model', parts: [{ text: response }] }); // Resume Worker response = await AIEngine.callWorker(project.workerHistory, injectionMsg, []); project.workerHistory.push({ role: 'user', parts: [{ text: injectionMsg }] }); project.workerHistory.push({ role: 'model', parts: [{ text: response }] }); } else { project.workerHistory.push({ role: 'user', parts: [{ text: fullInput }] }); project.workerHistory.push({ role: 'model', parts: [{ text: response }] }); } await StateManager.updateProject(projectId, { workerHistory: project.workerHistory, pmHistory: project.pmHistory, failureCount: project.failureCount }); await processAndQueueResponse(projectId, response); res.json({ success: true }); } catch (err) { console.error("AI Error:", err); res.status(500).json({ error: "AI Failed" }); } }); /** * 5. PING (Plugin Polling) */ app.post('/project/ping', async (req, res) => { const { projectId } = req.body; const command = await StateManager.popCommand(projectId); if (command) { if (command.payload === "CLEAR_CONSOLE") { res.json({ action: "CLEAR_LOGS" }); } else { res.json({ action: command.type, target: command.payload, code: command.type === 'EXECUTE' ? command.payload : null }); } } else { res.json({ action: "IDLE" }); } }); /** * 6. MANUAL OVERRIDE */ app.post('/human/override', validateRequest, async (req, res) => { const { projectId, instruction, pruneHistory } = req.body; const project = await StateManager.getProject(projectId); const overrideMsg = `[SYSTEM OVERRIDE]: ${instruction}`; if (pruneHistory && project.workerHistory.length >= 2) { project.workerHistory.pop(); project.workerHistory.pop(); } const response = await AIEngine.callWorker(project.workerHistory, overrideMsg, []); project.workerHistory.push({ role: 'user', parts: [{ text: overrideMsg }] }); project.workerHistory.push({ role: 'model', parts: [{ text: response }] }); await StateManager.updateProject(projectId, { workerHistory: project.workerHistory }); await processAndQueueResponse(projectId, response); res.json({ success: true }); }); // --- RESPONSE PROCESSOR --- async function processAndQueueResponse(projectId, rawResponse) { // 1. Intercept Image Requests const imgPrompt = extractImagePrompt(rawResponse); if (imgPrompt) { console.log(`[${projectId}] ๐ŸŽจ Generating Asset: ${imgPrompt}`); const base64Image = await AIEngine.generateImage(imgPrompt); if (base64Image) { // Queue asset creation for Plugin await StateManager.queueCommand(projectId, { type: "CREATE_ASSET", payload: base64Image }); } } // 2. Queue the Raw Response (StateManager parses code) await StateManager.queueCommand(projectId, rawResponse); } app.listen(PORT, () => { console.log(`AI Backend Running on ${PORT}`); });