// App.js import express from 'express'; import bodyParser from 'body-parser'; import cors from 'cors'; import { StateManager, initDB } from './stateManager.js'; import { AIEngine } from './aiEngine.js'; import fs from 'fs'; import admin from 'firebase-admin'; import crypto from "crypto"; // --- FIREBASE SETUP --- let db = null; let firestore = null; let storage = null; try { if (process.env.FIREBASE_SERVICE_ACCOUNT_JSON) { const serviceAccount = JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT_JSON); // Specific bucket as requested const bucketName = `hollowpad-ai-default-rtdb.firebaseio.com` if (admin.apps.length === 0) { admin.initializeApp({ credential: admin.credential.cert(serviceAccount), databaseURL: "https://shago-web-default-rtdb.firebaseio.com", storageBucket: bucketName }); } db = admin.database(); firestore = admin.firestore(); storage = admin.storage(); initDB(db); console.log("🔥 Firebase Connected (RTDB, Firestore, Storage) & StateManager Linked"); } else { console.warn("⚠️ Memory-Only mode."); } } catch (e) { console.error("Firebase Init Error:", e); } const app = express(); const PORT = process.env.PORT || 7860; // Load prompts safely let sysPrompts = {}; try { sysPrompts = JSON.parse(fs.readFileSync('./prompts.json', 'utf8')); } catch (e) { console.error("Failed to load prompts.json:", e); } app.use(cors()); app.use(bodyParser.json({ limit: '50mb' })); // --- CREDIT CONFIGURATION --- const MIN_BASIC_REQUIRED = 50; const MIN_DIAMOND_REQUIRED = 50; const IMAGE_COST_BASIC = 1000; async function checkMinimumCredits(userId, type = 'basic') { if (!db) return; const snap = await db.ref(`users/${userId}/credits/${type}`).once('value'); const credits = snap.val() || 0; const required = type === 'diamond' ? MIN_DIAMOND_REQUIRED : MIN_BASIC_REQUIRED; if (credits < required) { throw new Error(`Insufficient ${type} credits. You have ${credits}, need minimum ${required} to proceed.`); } } async function deductUserCredits(userId, amount, type = 'basic') { if (!db || !amount || amount <= 0) return; try { const ref = db.ref(`users/${userId}/credits/${type}`); await ref.transaction((current_credits) => { const current = current_credits || 0; return Math.max(0, current - amount); }); console.log(`[Credits] Deducted ${amount} ${type} credits from User ${userId}`); } catch (err) { console.error(`[Credits] Failed to deduct ${amount} ${type} from ${userId}:`, err); } } // --- MIDDLEWARE --- const validateRequest = (req, res, next) => { if (req.path.includes('/admin/cleanup')) return next(); const { userId, projectId } = req.body; if (!userId && (req.path.includes('/onboarding') || req.path.includes('/new') || req.path.includes('/project'))) { return res.status(400).json({ error: "Missing userId" }); } next(); }; // --- REGEX HELPERS (FIXED) --- 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}`; if (logContext) out += `\n[LAST LOGS]: ${logContext.logs}`; if (hierarchyContext) out += `\n[Hierarchy Context]: ${hierarchyContext}`; return out; } function extractPMQuestion(text) { // Looks for [ASK_PM: ...] const match = text.match(/\[ASK_PM:\s*(.*?)\]/s); return match ? match[1].trim() : null; } function extractImagePrompt(text) { // Looks for [GENERATE_IMAGE: ...] const match = text.match(/\[GENERATE_IMAGE:\s*(.*?)\]/s); return match ? match[1].trim() : null; } function extractRouteToPM(text) { // Looks for [ROUTE_TO_PM: ...] const match = text.match(/\[ROUTE_TO_PM:\s*(.*?)\]/s); return match ? match[1].trim() : null; } // --- ADMIN ENDPOINTS --- app.get('/admin/cleanup', async (req, res) => { try { const removed = StateManager.cleanupMemory(); res.json({ success: true, removedCount: removed }); } catch (err) { res.status(500).json({ error: "Cleanup failed" }); } }); // --- ONBOARDING ENDPOINTS --- app.post('/onboarding/analyze', validateRequest, async (req, res) => { const { description, userId } = req.body; if (!description) return res.status(400).json({ error: "Description required" }); try { await checkMinimumCredits(userId, 'basic'); console.log(`[Onboarding] Analyzing idea...`); const result = await AIEngine.generateEntryQuestions(description); const usage = result.usage?.totalTokenCount || 0; if (usage > 0) await deductUserCredits(userId, usage, 'basic'); if (result.status === "REJECTED") { return res.json({ rejected: true, reason: result.reason || "Idea violates TOS." }); } res.json({ questions: result.questions }); } catch (err) { console.error(err); if (err.message && err.message.includes("Insufficient")) { return res.status(402).json({ error: err.message }); } res.status(500).json({ error: "Analysis failed" }); } }); app.post('/onboarding/create', validateRequest, async (req, res) => { const { userId, description, answers } = req.body; let basicTokens = 0; try { await checkMinimumCredits(userId, 'basic'); const randomHex = (n = 6) => crypto.randomBytes(Math.ceil(n/2)).toString("hex").slice(0, n); const projectId = `proj_${Date.now()}_${randomHex(7)}`; console.log(`[Onboarding] Grading Project ${projectId}...`); const gradingResult = await AIEngine.gradeProject(description, answers); basicTokens += (gradingResult.usage?.totalTokenCount || 0); const isFailure = gradingResult.feasibility < 30 || gradingResult.rating === 'F'; let thumbnailBase64 = null; let thumbnailUrl = null; // Save Data Logic if (!isFailure) { const memoryObject = { id: projectId, userId, title: gradingResult.title || "Untitled", description, answers, stats: gradingResult, thumbnail: thumbnailUrl, createdAt: Date.now(), status: "Idle", workerHistory: [], pmHistory: [], commandQueue: [], failureCount: 0 }; await StateManager.updateProject(projectId, memoryObject); } if (firestore && !isFailure) { await firestore.collection('projects').doc(projectId).set({ id: projectId, userId: userId, assets: thumbnailUrl ? [thumbnailUrl] : [], createdAt: admin.firestore.FieldValue.serverTimestamp() }); } if (db && !isFailure) { const updates = {}; updates[`projects/${projectId}/info`] = { id: projectId, userId, title: gradingResult.title || "Untitled", description, answers, stats: gradingResult, createdAt: Date.now(), status: "Idle" }; if (thumbnailUrl) updates[`projects/${projectId}/thumbnail`] = { url: thumbnailUrl }; updates[`projects/${projectId}/state`] = { workerHistory: [], pmHistory: [], commandQueue: [], failureCount: 0 }; await db.ref().update(updates); } if (basicTokens > 0) await deductUserCredits(userId, basicTokens, 'basic'); res.json({ status: 200, success: !isFailure, projectId, stats: gradingResult, title: gradingResult.title || "Untitled", thumbnail: thumbnailBase64 }); } catch (err) { console.error("Create Error:", err); if (err.message && err.message.includes("Insufficient")) { return res.status(402).json({ error: err.message }); } res.status(500).json({ error: "Creation failed" }); } }); // --- CORE ENDPOINTS --- async function runBackgroundInitialization(projectId, userId, description) { console.log(`[Background] Starting initialization for ${projectId}`); let diamondUsage = 0; let basicUsage = 0; try { const pmHistory = []; // 1. Generate GDD (PM -> Diamond) const gddPrompt = `Create a comprehensive GDD for: ${description}`; const gddResult = await AIEngine.callPM(pmHistory, gddPrompt); diamondUsage += (gddResult.usage?.totalTokenCount || 0); const gddText = gddResult.text; pmHistory.push({ role: 'user', parts: [{ text: gddPrompt }] }); pmHistory.push({ role: 'model', parts: [{ text: gddText }] }); // 2. Generate First Task (PM -> Diamond) const taskPrompt = "Based on the GDD, generate the first technical milestone.\nOutput format:\nTASK_NAME: \nWORKER_PROMPT: "; const taskResult = await AIEngine.callPM(pmHistory, taskPrompt); diamondUsage += (taskResult.usage?.totalTokenCount || 0); const taskText = taskResult.text; pmHistory.push({ role: 'user', parts: [{ text: taskPrompt }] }); pmHistory.push({ role: 'model', parts: [{ text: taskText }] }); // 3. Initialize Worker (Worker -> Basic) const initialWorkerInstruction = extractWorkerPrompt(taskText) || `Initialize structure for: ${description}`; const workerHistory = []; const initialWorkerPrompt = `CONTEXT: New Project. \nINSTRUCTION: ${initialWorkerInstruction}`; const workerResult = await AIEngine.callWorker(workerHistory, initialWorkerPrompt, []); basicUsage += (workerResult.usage?.totalTokenCount || 0); const workerText = workerResult.text; workerHistory.push({ role: 'user', parts: [{ text: initialWorkerPrompt }] }); workerHistory.push({ role: 'model', parts: [{ text: workerText }] }); // 4. Update Memory await StateManager.updateProject(projectId, { userId, pmHistory, workerHistory, gdd: gddText, failureCount: 0 }); // 5. Queue commands await processAndQueueResponse(projectId, workerText, userId); // 6. Update Status if(db) await db.ref(`projects/${projectId}/info`).update({ status: "IDLE", lastUpdated: Date.now() }); // 7. Deduct Accumulated Credits if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond'); if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic'); console.log(`[Background] Init complete. Diamond: ${diamondUsage}, Basic: ${basicUsage}`); } catch (err) { console.error(`[Background] Init Error for ${projectId}:`, err); if(db) await db.ref(`projects/${projectId}/info/status`).set("error"); } } app.post('/new/project', validateRequest, async (req, res) => { const { userId, projectId, description } = req.body; try { await checkMinimumCredits(userId, 'diamond'); await checkMinimumCredits(userId, 'basic'); if(db) db.ref(`projects/${projectId}/info/status`).set("initializing"); res.json({ success: true, message: "Project initialization started in background." }); runBackgroundInitialization(projectId, userId, description); } catch (err) { if (err.message && err.message.includes("Insufficient")) { return res.status(402).json({ error: err.message }); } res.status(500).json({ error: "Failed to start project" }); } }); // MAIN FEEDBACK LOOP app.post('/project/feedback', async (req, res) => { const { userId, projectId, prompt, hierarchyContext, scriptContext, logContext, taskComplete, images } = req.body; let diamondUsage = 0; let basicUsage = 0; try { const project = await StateManager.getProject(projectId); if (!project) return res.status(404).json({ error: "Project not found." }); if (project.userId !== userId) return res.status(403).json({ error: "Unauthorized" }); await checkMinimumCredits(userId, 'basic'); if(db) await db.ref(`projects/${projectId}/info/status`).set("working"); // SCENARIO 1: TASK COMPLETE (Worker asks PM for next job) 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 pmResult = await AIEngine.callPM(project.pmHistory, summary); diamondUsage += (pmResult.usage?.totalTokenCount || 0); const pmText = pmResult.text; project.pmHistory.push({ role: 'user', parts: [{ text: summary }] }); project.pmHistory.push({ role: 'model', parts: [{ text: pmText }] }); const nextInstruction = extractWorkerPrompt(pmText); if (!nextInstruction) { await StateManager.updateProject(projectId, { pmHistory: project.pmHistory }); if(db) await db.ref(`projects/${projectId}/info`).update({ status: "IDLE", lastUpdated: Date.now() }); if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond'); return res.json({ success: true, message: "No further tasks. Project Idle." }); } const newPrompt = `New Objective: ${nextInstruction}`; const workerResult = await AIEngine.callWorker(project.workerHistory, newPrompt, []); basicUsage += (workerResult.usage?.totalTokenCount || 0); const workerText = workerResult.text; project.workerHistory.push({ role: 'user', parts: [{ text: newPrompt }] }); project.workerHistory.push({ role: 'model', parts: [{ text: workerText }] }); await StateManager.updateProject(projectId, { pmHistory: project.pmHistory, workerHistory: project.workerHistory, failureCount: 0 }); StateManager.queueCommand(projectId, { type: "EXECUTE", payload: "warn('Starting Next Task...')" }); await processAndQueueResponse(projectId, workerText, userId); if(db) await db.ref(`projects/${projectId}/info`).update({ status: "working", lastUpdated: Date.now() }); if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond'); if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic'); return res.json({ success: true, message: "Next Task Assigned" }); } // SCENARIO 2: FAILURE ESCALATION if (project.failureCount > 3) { const pmPrompt = (sysPrompts.pm_guidance_prompt || "Analyze logs: {{LOGS}}").replace('{{LOGS}}', logContext?.logs); const pmResult = await AIEngine.callPM(project.pmHistory, pmPrompt); diamondUsage += (pmResult.usage?.totalTokenCount || 0); const pmVerdict = pmResult.text; if (pmVerdict.includes("[TERMINATE]")) { const fixInstruction = pmVerdict.replace("[TERMINATE]", "").trim(); const resetPrompt = `[SYSTEM]: Previous worker terminated. \nNew Objective: ${fixInstruction}`; // Clear History on terminate const resetHistory = []; const workerResult = await AIEngine.callWorker(resetHistory, resetPrompt, []); basicUsage += (workerResult.usage?.totalTokenCount || 0); resetHistory.push({ role: 'user', parts: [{ text: resetPrompt }] }); resetHistory.push({ role: 'model', parts: [{ text: workerResult.text }] }); await StateManager.updateProject(projectId, { workerHistory: resetHistory, failureCount: 0 }); StateManager.queueCommand(projectId, { type: "EXECUTE", payload: "print('SYSTEM: Worker Reset')" }); await processAndQueueResponse(projectId, workerResult.text, userId); if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond'); if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic'); return res.json({ success: true, message: "Worker Terminated." }); } else { const injection = `[PM GUIDANCE]: ${pmVerdict} \n\nApply this fix now.`; const workerResult = await AIEngine.callWorker(project.workerHistory, injection, []); basicUsage += (workerResult.usage?.totalTokenCount || 0); project.workerHistory.push({ role: 'user', parts: [{ text: injection }] }); project.workerHistory.push({ role: 'model', parts: [{ text: workerResult.text }] }); await StateManager.updateProject(projectId, { workerHistory: project.workerHistory, failureCount: 1 }); await processAndQueueResponse(projectId, workerResult.text, userId); if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond'); if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic'); return res.json({ success: true, message: "PM Guidance Applied." }); } } // SCENARIO 3: STANDARD INTERACTION (Worker is the Frontline) const fullInput = `USER: ${prompt || "Automatic Feedback"}` + formatContext({ hierarchyContext, scriptContext, logContext }); let workerResult = await AIEngine.callWorker(project.workerHistory, fullInput, images || []); basicUsage += (workerResult.usage?.totalTokenCount || 0); let responseText = workerResult.text; // INTERCEPTOR A: Worker routes complex task to PM const routeTask = extractRouteToPM(responseText); if (routeTask) { console.log(` 🔀 Task routed to PM: ${routeTask}`); // PM generates Backend Code (Diamond) const pmPrompt = `[ROUTED TASK]: ${routeTask}\nWrite the core server/backend logic yourself. Then use WORKER_PROMPT: to delegate the UI/Client aspects to the Worker.`; const pmResult = await AIEngine.callPM(project.pmHistory, pmPrompt); diamondUsage += (pmResult.usage?.totalTokenCount || 0); const pmText = pmResult.text; project.pmHistory.push({ role: 'user', parts: [{ text: pmPrompt }] }); project.pmHistory.push({ role: 'model', parts: [{ text: pmText }] }); // Queue PM's Backend Code to Studio await processAndQueueResponse(projectId, pmText, userId); // Did the PM give the Worker UI/Client homework? const nextInstruction = extractWorkerPrompt(pmText); if (nextInstruction) { const delegationPrompt = `[SYSTEM]: The PM has handled the backend logic. Now execute this UI/Client task: ${nextInstruction}`; const workerDelegationResult = await AIEngine.callWorker(project.workerHistory, delegationPrompt, []); basicUsage += (workerDelegationResult.usage?.totalTokenCount || 0); responseText = workerDelegationResult.text; project.workerHistory.push({ role: 'user', parts: [{ text: delegationPrompt }] }); project.workerHistory.push({ role: 'model', parts: [{ text: responseText }] }); } else { responseText = "System Note: The PM completed the core task without delegating client logic."; project.workerHistory.push({ role: 'user', parts: [{ text: "System: PM task complete." }] }); project.workerHistory.push({ role: 'model', parts: [{ text: responseText }] }); } } // INTERCEPTOR B: Worker asks PM a question else { const pmQuestion = extractPMQuestion(responseText); if (pmQuestion) { console.log(` ❓ Worker asked PM: ${pmQuestion}`); const pmConsultPrompt = `[WORKER CONSULTATION]: The Worker asks: "${pmQuestion}"\nProvide a technical answer to unblock them.`; const pmResult = await AIEngine.callPM(project.pmHistory, pmConsultPrompt); diamondUsage += (pmResult.usage?.totalTokenCount || 0); const pmAnswer = pmResult.text; project.pmHistory.push({ role: 'user', parts: [{ text: pmConsultPrompt }] }); project.pmHistory.push({ role: 'model', parts: [{ text: pmAnswer }] }); const injectionMsg = `[PM RESPONSE]: ${pmAnswer}`; project.workerHistory.push({ role: 'user', parts: [{ text: injectionMsg }] }); project.workerHistory.push({ role: 'model', parts: [{ text: responseText }] }); // Re-call Worker workerResult = await AIEngine.callWorker(project.workerHistory, injectionMsg, []); basicUsage += (workerResult.usage?.totalTokenCount || 0); responseText = workerResult.text; project.workerHistory.push({ role: 'user', parts: [{ text: injectionMsg }] }); project.workerHistory.push({ role: 'model', parts: [{ text: responseText }] }); } else { // Standard Worker Execution project.workerHistory.push({ role: 'user', parts: [{ text: fullInput }] }); project.workerHistory.push({ role: 'model', parts: [{ text: responseText }] }); } } // Save State & Queue Responses await StateManager.updateProject(projectId, { workerHistory: project.workerHistory, pmHistory: project.pmHistory, failureCount: project.failureCount }); if (!routeTask) { // Only queue the worker response if we didn't route to PM (if routed, PM already queued its code above) await processAndQueueResponse(projectId, responseText, userId); } if(db) await db.ref(`projects/${projectId}/info`).update({ status: "idle", lastUpdated: Date.now() }); // Final Billing if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond'); if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic'); res.json({ success: true }); } catch (err) { console.error("AI Error:", err); if (err.message && err.message.includes("Insufficient")) { return res.status(402).json({ error: err.message }); } res.status(500).json({ error: "AI Failed" }); } }); app.post('/project/ping', async (req, res) => { const { projectId, userId } = req.body; if (!projectId || !userId) return res.status(400).json({ error: "Missing ID fields" }); const project = await StateManager.getProject(projectId); if (!project) return res.status(404).json({ action: "IDLE", error: "Project not found" }); if (project.userId !== userId) return res.status(403).json({ error: "Unauthorized" }); 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" }); } }); app.post('/human/override', validateRequest, async (req, res) => { const { projectId, instruction, pruneHistory, userId } = req.body; let basicUsage = 0; try { await checkMinimumCredits(userId, 'basic'); const project = await StateManager.getProject(projectId); const overrideMsg = `[SYSTEM OVERRIDE]: ${instruction}`; if (pruneHistory && project.workerHistory.length >= 2) { project.workerHistory.pop(); project.workerHistory.pop(); } const workerResult = await AIEngine.callWorker(project.workerHistory, overrideMsg, []); basicUsage += (workerResult.usage?.totalTokenCount || 0); project.workerHistory.push({ role: 'user', parts: [{ text: overrideMsg }] }); project.workerHistory.push({ role: 'model', parts: [{ text: workerResult.text }] }); await StateManager.updateProject(projectId, { workerHistory: project.workerHistory }); await processAndQueueResponse(projectId, workerResult.text, userId); if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic'); res.json({ success: true }); } catch (err) { if (err.message && err.message.includes("Insufficient")) { return res.status(402).json({ error: err.message }); } res.status(500).json({ error: "Override Failed" }); } }); // Helper to handle Asset Parsing & Billing async function processAndQueueResponse(projectId, rawResponse, userId) { if (!rawResponse) return; const imgPrompt = extractImagePrompt(rawResponse); if (imgPrompt) { console.log(` 🎨 Generating Asset: ${imgPrompt}`); const imgResult = await AIEngine.generateImage(imgPrompt); if (imgResult && imgResult.image) { const imgTokens = imgResult.usage?.totalTokenCount || 0; const totalCost = imgTokens; if (userId && totalCost > 0) { await deductUserCredits(userId, totalCost, 'basic'); } await StateManager.queueCommand(projectId, { type: "CREATE_ASSET", payload: imgResult.image }); } } await StateManager.queueCommand(projectId, rawResponse); } app.listen(PORT, () => { console.log(`AI Backend Running on ${PORT}`); });