everydaytok's picture
Update app.js
bf29c52 verified
// 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: <Name>\nWORKER_PROMPT: <Specific, isolated instructions for the worker>";
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: <task> 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}`);
});