everydaytok commited on
Commit
abf20d4
·
verified ·
1 Parent(s): 1b4fc86

Update app2.js

Browse files
Files changed (1) hide show
  1. app2.js +497 -207
app2.js CHANGED
@@ -1,46 +1,140 @@
1
  import express from 'express';
2
  import bodyParser from 'body-parser';
3
  import cors from 'cors';
4
- import { StateManager } from './stateManager.js';
5
  import { AIEngine } from './aiEngine.js';
6
  import fs from 'fs';
7
  import admin from 'firebase-admin';
 
8
 
9
  // --- FIREBASE SETUP ---
10
- // Using environment variable injection as requested
11
  let db = null;
 
 
 
12
  try {
13
- if (process.env.FIREBASE_SERVICE_ACCOUNT_JSON && process.env.FIREBASE_SERVICE_ACCOUNT_JSON !== "") {
14
  const serviceAccount = JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT_JSON);
15
- admin.initializeApp({
16
- credential: admin.credential.cert(serviceAccount),
17
- // Make sure to replace this URL or add it to env variables as well
18
- databaseURL: "https://shago-web-default-rtdb.firebaseio.com"
19
- });
 
 
 
 
 
 
 
20
  db = admin.database();
21
- console.log("🔥 Firebase Connected");
 
 
 
 
22
  } else {
23
- console.warn("⚠️ No FIREBASE_SERVICE_ACCOUNT_JSON found. Running in Memory-Only mode.");
24
  }
25
- } catch (e) {
26
- console.error("Firebase Init Error:", e);
27
- }
28
 
29
  const app = express();
30
  const PORT = process.env.PORT || 7860;
31
 
32
- // Load Prompts for Server-Side Logic Checks
33
- const sysPrompts = JSON.parse(fs.readFileSync('./prompts.json', 'utf8'));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
  app.use(cors());
36
  app.use(bodyParser.json({ limit: '50mb' }));
37
 
38
- // --- HELPERS ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  const validateRequest = (req, res, next) => {
40
- if (req.path.includes('/onboarding')) return next();
41
 
42
  const { userId, projectId } = req.body;
43
- if (!userId || !projectId) return res.status(400).json({ error: "Missing ID" });
 
 
 
 
 
44
  next();
45
  };
46
 
@@ -51,9 +145,12 @@ function extractWorkerPrompt(text) {
51
 
52
  function formatContext({ hierarchyContext, scriptContext, logContext }) {
53
  let out = "";
54
- if (scriptContext) out += `\n[TARGET SCRIPT]: ${scriptContext.targetName}\n[SOURCE PREVIEW]: ${scriptContext.scriptSource?.substring(0, 1000)}...`;
 
55
  if (logContext) out += `\n[LAST LOGS]: ${logContext.logs}`;
56
- return out;
 
 
57
  }
58
 
59
  function extractPMQuestion(text) {
@@ -66,268 +163,431 @@ function extractImagePrompt(text) {
66
  return match ? match[1].trim() : null;
67
  }
68
 
 
 
 
 
 
 
 
 
 
 
 
69
  // --- ONBOARDING ENDPOINTS ---
70
- /**
71
- * 1. ANALYZE
72
- */
73
  app.post('/onboarding/analyze', validateRequest, async (req, res) => {
74
- const { description } = req.body;
 
75
  if (!description) return res.status(400).json({ error: "Description required" });
76
 
77
  try {
 
 
 
78
  console.log(`[Onboarding] Analyzing idea...`);
 
79
  const result = await AIEngine.generateEntryQuestions(description);
80
 
 
 
 
 
 
81
  if (result.status === "REJECTED") {
82
  return res.json({
83
  rejected: true,
84
- reason: result.reason || "Idea violates TOS or guidelines."
85
  });
86
  }
87
  res.json({ questions: result.questions });
 
88
  } catch (err) {
89
  console.error(err);
 
 
 
90
  res.status(500).json({ error: "Analysis failed" });
91
  }
92
  });
93
 
94
- /**
95
- * 2. CREATE
96
- */
97
  app.post('/onboarding/create', validateRequest, async (req, res) => {
98
  const { userId, description, answers } = req.body;
99
- const projectId = "proj_" + Date.now();
100
 
101
  try {
 
 
 
 
 
 
102
  console.log(`[Onboarding] Grading Project ${projectId}...`);
103
 
104
- // STEP 1: GRADE
105
- const grading = await AIEngine.gradeProject(description, answers);
106
-
107
- // STEP 2: CHECK FAIL CONDITIONS (Relaxed)
108
- // Only fail if Feasibility is extremely low (< 30) or Rating is F
109
- const isFailure = grading.feasibility < 30 || grading.rating === 'F';
110
 
111
  let thumbnailBase64 = null;
112
 
113
- if (isFailure) {
114
- console.log(`[Onboarding] Project Failed Grading (${grading.rating}). Skipping Image.`);
115
- } else {
116
  console.log(`[Onboarding] ✅ Passed. Generating Thumbnail...`);
 
117
 
118
- // --- CRITICAL FIX HERE ---
119
- // We MUST send the description so the AI knows what to draw.
120
- const imagePrompt = `Game Title: ${grading.title}. Core Concept: ${description}`;
121
- thumbnailBase64 = await AIEngine.generateImage(imagePrompt);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  }
123
 
124
- const projectData = {
125
- id: projectId,
126
- userId,
127
- title: grading.title || "Untitled Project",
128
- description,
129
- answers,
130
- stats: grading,
131
- thumbnail: thumbnailBase64, // ? `data:image/png;base64,${thumbnailBase64}` : null,
132
- createdAt: Date.now(),
133
- status: isFailure ? "rejected" : "initialized"
134
- };
135
-
136
- if (db) await db.ref(`projects/${projectId}`).set(projectData);
137
-
138
  if (!isFailure) {
139
- await StateManager.updateProject(projectId, {
140
- ...projectData,
 
 
 
 
 
 
 
 
141
  workerHistory: [],
142
  pmHistory: [],
 
143
  failureCount: 0
 
 
 
 
 
 
 
 
 
 
144
  });
145
  }
146
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  res.json({
 
148
  success: !isFailure,
149
  projectId,
150
- stats: grading,
151
- title: projectData.title,
152
- thumbnail: projectData.thumbnail
153
  });
154
 
155
  } catch (err) {
156
  console.error("Create Error:", err);
 
 
 
157
  res.status(500).json({ error: "Creation failed" });
158
  }
159
  });
160
 
 
161
 
162
- // --- CORE WORKFLOW ENDPOINTS ---
163
-
164
- /**
165
- * 3. INITIALIZE WORKSPACE (PM Generates GDD -> First Task)
166
- */
167
- app.post('/new/project', validateRequest, async (req, res) => {
168
- const { userId, projectId, description } = req.body;
169
 
170
  try {
171
  const pmHistory = [];
 
 
172
  const gddPrompt = `Create a comprehensive GDD for: ${description}`;
173
- const gddResponse = await AIEngine.callPM(pmHistory, gddPrompt);
174
 
 
 
 
175
  pmHistory.push({ role: 'user', parts: [{ text: gddPrompt }] });
176
- pmHistory.push({ role: 'model', parts: [{ text: gddResponse }] });
177
 
 
178
  const taskPrompt = "Based on the GDD, generate the first technical milestone.\nOutput format:\nTASK_NAME: <Name>\nWORKER_PROMPT: <Specific, isolated instructions for the worker>";
179
- const taskResponse = await AIEngine.callPM(pmHistory, taskPrompt);
 
 
 
180
 
181
  pmHistory.push({ role: 'user', parts: [{ text: taskPrompt }] });
182
- pmHistory.push({ role: 'model', parts: [{ text: taskResponse }] });
183
-
184
- const initialWorkerInstruction = extractWorkerPrompt(taskResponse) || `Initialize structure for: ${description}`;
185
 
 
 
186
  const workerHistory = [];
187
  const initialWorkerPrompt = `CONTEXT: New Project. \nINSTRUCTION: ${initialWorkerInstruction}`;
188
- const workerResponse = await AIEngine.callWorker(workerHistory, initialWorkerPrompt, []);
 
 
 
 
189
 
190
  workerHistory.push({ role: 'user', parts: [{ text: initialWorkerPrompt }] });
191
- workerHistory.push({ role: 'model', parts: [{ text: workerResponse }] });
192
 
193
- // Update State
194
  await StateManager.updateProject(projectId, {
195
  userId,
196
  pmHistory,
197
  workerHistory,
198
- gdd: gddResponse,
199
  failureCount: 0
200
  });
201
 
202
- // Queue Actions
203
- await processAndQueueResponse(projectId, workerResponse);
204
-
205
- res.json({ success: true, message: "Workspace Initialized", gddPreview: gddResponse.substring(0, 200) });
 
 
 
 
 
 
 
 
 
 
 
206
 
207
  } catch (err) {
208
- console.error("Init Error:", err);
209
- res.status(500).json({ error: "Initialization Failed" });
210
  }
211
- });
212
 
213
- /**
214
- * 4. FEEDBACK LOOP (The "Brain")
215
- */
216
- app.post('/project/feedback', async (req, res) => {
217
- const { projectId, prompt, hierarchyContext, scriptContext, logContext, taskComplete, images } = req.body;
218
 
219
- const project = await StateManager.getProject(projectId);
220
- if (!project) return res.status(404).json({ error: "Project not found." });
 
 
221
 
222
- // A. TASK COMPLETE -> SCOPE SWITCH
223
- if (taskComplete) {
224
- console.log(`[${projectId}] ✅ TASK COMPLETE.`);
225
-
226
- const summary = `Worker completed the previous task. Logs: ${logContext?.logs || "Clean"}. \nGenerate the NEXT task using 'WORKER_PROMPT:' format.`;
227
- const pmResponse = await AIEngine.callPM(project.pmHistory, summary);
228
 
229
- project.pmHistory.push({ role: 'user', parts: [{ text: summary }] });
230
- project.pmHistory.push({ role: 'model', parts: [{ text: pmResponse }] });
231
-
232
- const nextInstruction = extractWorkerPrompt(pmResponse);
233
- if (!nextInstruction) {
234
- await StateManager.updateProject(projectId, { pmHistory: project.pmHistory, status: "IDLE" });
235
- return res.json({ success: true, message: "No further tasks. Project Idle." });
236
- }
237
-
238
- console.log(`[${projectId}] 🧹 Nuking Worker for next task.`);
239
- const newWorkerHistory = [];
240
- const newPrompt = `New Objective: ${nextInstruction}`;
241
- const workerResponse = await AIEngine.callWorker(newWorkerHistory, newPrompt, []);
242
-
243
- newWorkerHistory.push({ role: 'user', parts: [{ text: newPrompt }] });
244
- newWorkerHistory.push({ role: 'model', parts: [{ text: workerResponse }] });
245
-
246
- await StateManager.updateProject(projectId, {
247
- pmHistory: project.pmHistory,
248
- workerHistory: newWorkerHistory,
249
- failureCount: 0
250
  });
251
 
252
- StateManager.queueCommand(projectId, { type: "EXECUTE", payload: "warn('Starting Next Task...')" });
253
- await processAndQueueResponse(projectId, workerResponse);
254
 
255
- return res.json({ success: true, message: "Next Task Assigned" });
256
- }
257
-
258
- // B. ERROR ESCALATION
259
- let isFailure = false;
260
- if (logContext?.logs) {
261
- const errs = ["Error", "Exception", "failed", "Stack Begin", "Infinite yield"];
262
- if (errs.some(k => logContext.logs.includes(k))) {
263
- isFailure = true;
264
- project.failureCount = (project.failureCount || 0) + 1;
265
  }
 
266
  }
 
267
 
268
- if (project.failureCount > 3) {
269
- console.log(`[${projectId}] 🚨 Escalating to PM...`);
270
- const pmPrompt = sysPrompts.pm_guidance_prompt.replace('{{LOGS}}', logContext?.logs);
271
- const pmVerdict = await AIEngine.callPM(project.pmHistory, pmPrompt);
 
 
 
 
 
 
272
 
273
- if (pmVerdict.includes("[TERMINATE]")) {
274
- const fixInstruction = pmVerdict.replace("[TERMINATE]", "").trim();
275
- const resetHistory = [];
276
- const resetPrompt = `[SYSTEM]: Previous worker terminated. \nNew Objective: ${fixInstruction}`;
277
- const workerResp = await AIEngine.callWorker(resetHistory, resetPrompt, []);
 
 
 
278
 
279
- resetHistory.push({ role: 'user', parts: [{ text: resetPrompt }] });
280
- resetHistory.push({ role: 'model', parts: [{ text: workerResp }] });
281
-
282
- await StateManager.updateProject(projectId, { workerHistory: resetHistory, failureCount: 0 });
283
- StateManager.queueCommand(projectId, { type: "EXECUTE", payload: "print('SYSTEM: Worker Reset')" });
284
- await processAndQueueResponse(projectId, workerResp);
285
 
286
- return res.json({ success: true, message: "Worker Terminated." });
287
- } else {
288
- const injection = `[PM GUIDANCE]: ${pmVerdict} \n\nApply this fix now.`;
289
- const workerResp = await AIEngine.callWorker(project.workerHistory, injection, []);
 
 
 
 
 
 
 
 
 
 
 
 
290
 
291
- project.workerHistory.push({ role: 'user', parts: [{ text: injection }] });
292
- project.workerHistory.push({ role: 'model', parts: [{ text: workerResp }] });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
 
294
- await StateManager.updateProject(projectId, { workerHistory: project.workerHistory, failureCount: 1 });
295
- await processAndQueueResponse(projectId, workerResp);
 
 
 
 
 
 
 
 
 
 
296
 
297
- return res.json({ success: true, message: "PM Guidance Applied." });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
298
  }
299
- }
300
 
301
- // C. STANDARD LOOP + CONSULTATION
302
- try {
303
  const fullInput = `USER: ${prompt || "Automatic Feedback"}` + formatContext({ hierarchyContext, scriptContext, logContext });
304
 
305
- let response = await AIEngine.callWorker(project.workerHistory, fullInput, images || []);
 
 
306
 
307
- // CHECK: Consultant Question
308
- const pmQuestion = extractPMQuestion(response);
309
  if (pmQuestion) {
310
- console.log(`[${projectId}] 🙋 Worker asking PM: "${pmQuestion}"`);
311
-
312
  const pmConsultPrompt = `[WORKER CONSULTATION]: The Worker asks: "${pmQuestion}"\nProvide a technical answer to unblock them.`;
313
- const pmAnswer = await AIEngine.callPM(project.pmHistory, pmConsultPrompt);
 
 
314
 
315
- // Update PM Context
316
  project.pmHistory.push({ role: 'user', parts: [{ text: pmConsultPrompt }] });
317
  project.pmHistory.push({ role: 'model', parts: [{ text: pmAnswer }] });
318
 
319
- // Feed Answer Back
320
  const injectionMsg = `[PM RESPONSE]: ${pmAnswer}`;
321
  project.workerHistory.push({ role: 'user', parts: [{ text: fullInput }] });
322
- project.workerHistory.push({ role: 'model', parts: [{ text: response }] });
323
 
324
- // Resume Worker
325
- response = await AIEngine.callWorker(project.workerHistory, injectionMsg, []);
 
 
 
326
  project.workerHistory.push({ role: 'user', parts: [{ text: injectionMsg }] });
327
- project.workerHistory.push({ role: 'model', parts: [{ text: response }] });
328
  } else {
329
  project.workerHistory.push({ role: 'user', parts: [{ text: fullInput }] });
330
- project.workerHistory.push({ role: 'model', parts: [{ text: response }] });
331
  }
332
 
333
  await StateManager.updateProject(projectId, {
@@ -336,20 +596,33 @@ app.post('/project/feedback', async (req, res) => {
336
  failureCount: project.failureCount
337
  });
338
 
339
- await processAndQueueResponse(projectId, response);
 
 
 
 
 
 
 
340
  res.json({ success: true });
341
 
342
  } catch (err) {
343
  console.error("AI Error:", err);
 
 
 
344
  res.status(500).json({ error: "AI Failed" });
345
  }
346
  });
347
 
348
- /**
349
- * 5. PING (Plugin Polling)
350
- */
351
  app.post('/project/ping', async (req, res) => {
352
- const { projectId } = req.body;
 
 
 
 
 
 
353
  const command = await StateManager.popCommand(projectId);
354
 
355
  if (command) {
@@ -367,48 +640,65 @@ app.post('/project/ping', async (req, res) => {
367
  }
368
  });
369
 
370
- /**
371
- * 6. MANUAL OVERRIDE
372
- */
373
  app.post('/human/override', validateRequest, async (req, res) => {
374
- const { projectId, instruction, pruneHistory } = req.body;
375
- const project = await StateManager.getProject(projectId);
376
-
377
- const overrideMsg = `[SYSTEM OVERRIDE]: ${instruction}`;
378
-
379
- if (pruneHistory && project.workerHistory.length >= 2) {
380
- project.workerHistory.pop();
381
- project.workerHistory.pop();
382
- }
383
 
384
- const response = await AIEngine.callWorker(project.workerHistory, overrideMsg, []);
385
- project.workerHistory.push({ role: 'user', parts: [{ text: overrideMsg }] });
386
- project.workerHistory.push({ role: 'model', parts: [{ text: response }] });
387
 
388
- await StateManager.updateProject(projectId, { workerHistory: project.workerHistory });
389
- await processAndQueueResponse(projectId, response);
390
- res.json({ success: true });
391
- });
392
 
 
 
 
 
393
 
394
- // --- RESPONSE PROCESSOR ---
395
- async function processAndQueueResponse(projectId, rawResponse) {
396
- // 1. Intercept Image Requests
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
397
  const imgPrompt = extractImagePrompt(rawResponse);
398
  if (imgPrompt) {
399
  console.log(`[${projectId}] 🎨 Generating Asset: ${imgPrompt}`);
400
- const base64Image = await AIEngine.generateImage(imgPrompt);
401
 
402
- if (base64Image) {
403
- // Queue asset creation for Plugin
404
- await StateManager.queueCommand(projectId, {
405
- type: "CREATE_ASSET",
406
- payload: base64Image
407
- });
 
 
 
 
 
 
 
 
408
  }
409
  }
410
-
411
- // 2. Queue the Raw Response (StateManager parses code)
412
  await StateManager.queueCommand(projectId, rawResponse);
413
  }
414
 
 
1
  import express from 'express';
2
  import bodyParser from 'body-parser';
3
  import cors from 'cors';
4
+ import { StateManager, initDB } from './stateManager.js';
5
  import { AIEngine } from './aiEngine.js';
6
  import fs from 'fs';
7
  import admin from 'firebase-admin';
8
+ import crypto from "crypto";
9
 
10
  // --- FIREBASE SETUP ---
 
11
  let db = null;
12
+ let firestore = null;
13
+ let storage = null;
14
+
15
  try {
16
+ if (process.env.FIREBASE_SERVICE_ACCOUNT_JSON) {
17
  const serviceAccount = JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT_JSON);
18
+
19
+ // Specific bucket as requested
20
+ const bucketName = `hollowpad-ai-default-rtdb.firebaseio.com` // `shago-web.firebasestorage.app`;
21
+
22
+ if (admin.apps.length === 0) {
23
+ admin.initializeApp({
24
+ credential: admin.credential.cert(serviceAccount),
25
+ databaseURL: "https://shago-web-default-rtdb.firebaseio.com",
26
+ storageBucket: bucketName
27
+ });
28
+ }
29
+
30
  db = admin.database();
31
+ firestore = admin.firestore();
32
+ storage = admin.storage();
33
+
34
+ initDB(db);
35
+ console.log("🔥 Firebase Connected (RTDB, Firestore, Storage) & StateManager Linked");
36
  } else {
37
+ console.warn("⚠️ Memory-Only mode.");
38
  }
39
+ } catch (e) { console.error("Firebase Init Error:", e); }
40
+
 
41
 
42
  const app = express();
43
  const PORT = process.env.PORT || 7860;
44
 
45
+ // ---
46
+ /*
47
+
48
+ app.use((req, res, next) => {
49
+ console.log('[REQ]', req.method, req.originalUrl);
50
+ console.log('[HEADERS]', req.headers);
51
+ console.log('[BODY]', req.body ?? '(unparseable or empty)');
52
+ next();
53
+ });
54
+
55
+ */
56
+
57
+ // ---
58
+
59
+
60
+ // Load prompts safely
61
+ let sysPrompts = {};
62
+ try {
63
+ sysPrompts = JSON.parse(fs.readFileSync('./prompts.json', 'utf8'));
64
+ } catch (e) {
65
+ console.error("Failed to load prompts.json:", e);
66
+ }
67
 
68
  app.use(cors());
69
  app.use(bodyParser.json({ limit: '50mb' }));
70
 
71
+ // --- CREDIT CONFIGURATION ---
72
+
73
+ // Minimums required to start a request
74
+ const MIN_BASIC_REQUIRED = 50;
75
+ const MIN_DIAMOND_REQUIRED = 50;
76
+
77
+ // Fixed cost for images (Basic credits)
78
+ const IMAGE_COST_BASIC = 1000;
79
+
80
+ /**
81
+ * Checks if user has enough credits of a specific type.
82
+ * @param {string} userId
83
+ * @param {'basic'|'diamond'} type
84
+ */
85
+ async function checkMinimumCredits(userId, type = 'basic') {
86
+ if (!db) return;
87
+
88
+ // Path: users/{uid}/credits/basic OR users/{uid}/credits/diamond
89
+ const snap = await db.ref(`users/${userId}/credits/${type}`).once('value');
90
+ const credits = snap.val() || 0;
91
+
92
+ const required = type === 'diamond' ? MIN_DIAMOND_REQUIRED : MIN_BASIC_REQUIRED;
93
+
94
+ if (credits < required) {
95
+
96
+ res.status(500).json({
97
+ insufficient: true,
98
+ error:`Insufficient ${type} credits. You have ${credits}, need minimum ${required} to proceed.`
99
+
100
+ });
101
+ // throw new Error(`Insufficient ${type} credits. You have ${credits}, need minimum ${required} to proceed.`);
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Deducts exact tokens from the user's specific wallet.
107
+ * @param {string} userId
108
+ * @param {number} amount
109
+ * @param {'basic'|'diamond'} type
110
+ */
111
+ async function deductUserCredits(userId, amount, type = 'basic') {
112
+ if (!db || !amount || amount <= 0) return;
113
+
114
+ try {
115
+ const ref = db.ref(`users/${userId}/credits/${type}`);
116
+ await ref.transaction((current_credits) => {
117
+ const current = current_credits || 0;
118
+ return Math.max(0, current - amount);
119
+ });
120
+ console.log(`[Credits] Deducted ${amount} ${type} credits from User ${userId}`);
121
+ } catch (err) {
122
+ console.error(`[Credits] Failed to deduct ${amount} ${type} from ${userId}:`, err);
123
+ }
124
+ }
125
+
126
+ // --- MIDDLEWARE ---
127
+
128
  const validateRequest = (req, res, next) => {
129
+ if (req.path.includes('/admin/cleanup')) return next();
130
 
131
  const { userId, projectId } = req.body;
132
+
133
+ // Ensure userId exists for credit checks
134
+ if (!userId && (req.path.includes('/onboarding') || req.path.includes('/new') || req.path.includes('/project'))) {
135
+ return res.status(400).json({ error: "Missing userId" });
136
+ }
137
+
138
  next();
139
  };
140
 
 
145
 
146
  function formatContext({ hierarchyContext, scriptContext, logContext }) {
147
  let out = "";
148
+ if (scriptContext) out += `\n[TARGET SCRIPT]: ${scriptContext.targetName}\n[SOURCE PREVIEW]: ${scriptContext.scriptSource}`;
149
+ // if (scriptContext) out += `\n[TARGET SCRIPT]: ${scriptContext.targetName}\n[SOURCE PREVIEW]: ${scriptContext.scriptSource?.substring(0, 1000)}...`;
150
  if (logContext) out += `\n[LAST LOGS]: ${logContext.logs}`;
151
+ // my own line
152
+ if (hierarchyContext) out += `\n[Hierarchy Context]: ${hierarchyContext}`;
153
+ return out;
154
  }
155
 
156
  function extractPMQuestion(text) {
 
163
  return match ? match[1].trim() : null;
164
  }
165
 
166
+ // --- ADMIN ENDPOINTS ---
167
+
168
+ app.get('/admin/cleanup', async (req, res) => {
169
+ try {
170
+ const removed = StateManager.cleanupMemory();
171
+ res.json({ success: true, removedCount: removed });
172
+ } catch (err) {
173
+ res.status(500).json({ error: "Cleanup failed" });
174
+ }
175
+ });
176
+
177
  // --- ONBOARDING ENDPOINTS ---
178
+
 
 
179
  app.post('/onboarding/analyze', validateRequest, async (req, res) => {
180
+ const { description, userId } = req.body;
181
+
182
  if (!description) return res.status(400).json({ error: "Description required" });
183
 
184
  try {
185
+ // Analyst uses BASIC models (Flash)
186
+ await checkMinimumCredits(userId, 'basic');
187
+
188
  console.log(`[Onboarding] Analyzing idea...`);
189
+
190
  const result = await AIEngine.generateEntryQuestions(description);
191
 
192
+ const usage = result.usage?.totalTokenCount || 0;
193
+
194
+ // Bill Basic
195
+ if (usage > 0) await deductUserCredits(userId, usage, 'basic');
196
+
197
  if (result.status === "REJECTED") {
198
  return res.json({
199
  rejected: true,
200
+ reason: result.reason || "Idea violates TOS."
201
  });
202
  }
203
  res.json({ questions: result.questions });
204
+
205
  } catch (err) {
206
  console.error(err);
207
+ if (err.message.includes("Insufficient")) {
208
+ return res.status(402).json({ error: err.message });
209
+ }
210
  res.status(500).json({ error: "Analysis failed" });
211
  }
212
  });
213
 
 
 
 
214
  app.post('/onboarding/create', validateRequest, async (req, res) => {
215
  const { userId, description, answers } = req.body;
216
+ let basicTokens = 0; // Create flow uses Grader (Basic) + Image (Basic)
217
 
218
  try {
219
+ // Pre-flight check
220
+ await checkMinimumCredits(userId, 'basic');
221
+
222
+ const randomHex = (n = 6) => crypto.randomBytes(Math.ceil(n/2)).toString("hex").slice(0, n);
223
+ const projectId = `proj_${Date.now()}_${randomHex(7)}`;
224
+
225
  console.log(`[Onboarding] Grading Project ${projectId}...`);
226
 
227
+ // 1. Grading (Basic)
228
+ const gradingResult = await AIEngine.gradeProject(description, answers);
229
+ basicTokens += (gradingResult.usage?.totalTokenCount || 0);
230
+
231
+ const isFailure = gradingResult.feasibility < 30 || gradingResult.rating === 'F';
 
232
 
233
  let thumbnailBase64 = null;
234
 
235
+ /*
236
+ * image generator
237
+ if (!isFailure) {
238
  console.log(`[Onboarding] ✅ Passed. Generating Thumbnail...`);
239
+ const imagePrompt = `Game Title: ${gradingResult.title}. Core Concept: ${description}`;
240
 
241
+ // 2. Image Generation (Basic)
242
+ const imgResult = await AIEngine.generateImage(imagePrompt);
243
+ if (imgResult) {
244
+ thumbnailBase64 = imgResult.image;
245
+ // Add token usage for text + fixed cost
246
+ basicTokens += (imgResult.usage || 0);
247
+ if (imgResult.image) basicTokens += IMAGE_COST_BASIC;
248
+ }
249
+ } */
250
+
251
+ // Upload Logic
252
+ let thumbnailUrl = null;
253
+ if (thumbnailBase64 && storage) {
254
+ try {
255
+ const base64Data = thumbnailBase64.replace(/^data:image\/\w+;base64,/, "");
256
+ const buffer = Buffer.from(base64Data, 'base64');
257
+ const bucket = storage.bucket();
258
+ const file = bucket.file(`${projectId}/thumbnail.png`);
259
+ await file.save(buffer, { metadata: { contentType: 'image/png' } });
260
+ await file.makePublic();
261
+ thumbnailUrl = `https://storage.googleapis.com/${bucket.name}/${projectId}/thumbnail.png`;
262
+ } catch (uploadErr) {
263
+ console.error("Storage Upload Failed:", uploadErr);
264
+ }
265
  }
266
 
267
+ const timestamp = Date.now();
268
+ const status = isFailure ? "rejected" : "Idle";
269
+
270
+ // Save Data Logic
 
 
 
 
 
 
 
 
 
 
271
  if (!isFailure) {
272
+ const memoryObject = {
273
+ id: projectId,
274
+ userId,
275
+ title: gradingResult.title || "Untitled",
276
+ description,
277
+ answers,
278
+ stats: gradingResult,
279
+ thumbnail: thumbnailUrl,
280
+ createdAt: timestamp,
281
+ status,
282
  workerHistory: [],
283
  pmHistory: [],
284
+ commandQueue: [],
285
  failureCount: 0
286
+ };
287
+ await StateManager.updateProject(projectId, memoryObject);
288
+ }
289
+
290
+ if (firestore && !isFailure) {
291
+ await firestore.collection('projects').doc(projectId).set({
292
+ id: projectId,
293
+ userId: userId,
294
+ assets: thumbnailUrl ? [thumbnailUrl] : [],
295
+ createdAt: admin.firestore.FieldValue.serverTimestamp()
296
  });
297
  }
298
 
299
+ if (db && !isFailure) {
300
+ const updates = {};
301
+ updates[`projects/${projectId}/info`] = {
302
+ id: projectId,
303
+ userId,
304
+ title: gradingResult.title || "Untitled",
305
+ description,
306
+ answers,
307
+ stats: gradingResult,
308
+ createdAt: timestamp,
309
+ status
310
+ };
311
+ if (thumbnailUrl) updates[`projects/${projectId}/thumbnail`] = { url: thumbnailUrl };
312
+ updates[`projects/${projectId}/state`] = {
313
+ workerHistory: [],
314
+ pmHistory: [],
315
+ commandQueue: [],
316
+ failureCount: 0
317
+ };
318
+ await db.ref().update(updates);
319
+ }
320
+
321
+ // Deduct Basic Credits
322
+ if (basicTokens > 0) await deductUserCredits(userId, basicTokens, 'basic');
323
+ console.log("sending");
324
  res.json({
325
+ status: 200,
326
  success: !isFailure,
327
  projectId,
328
+ stats: gradingResult,
329
+ title: gradingResult.title || "Untitled",
330
+ thumbnail: thumbnailBase64
331
  });
332
 
333
  } catch (err) {
334
  console.error("Create Error:", err);
335
+ if (err.message.includes("Insufficient")) {
336
+ return res.status(402).json({ error: err.message });
337
+ }
338
  res.status(500).json({ error: "Creation failed" });
339
  }
340
  });
341
 
342
+ // --- CORE ENDPOINTS ---
343
 
344
+ async function runBackgroundInitialization(projectId, userId, description) {
345
+ console.log(`[Background] Starting initialization for ${projectId}`);
346
+
347
+ // Separate Counters
348
+ let diamondUsage = 0; // For PM
349
+ let basicUsage = 0; // For Worker
 
350
 
351
  try {
352
  const pmHistory = [];
353
+
354
+ // 1. Generate GDD (PM -> Diamond)
355
  const gddPrompt = `Create a comprehensive GDD for: ${description}`;
356
+ const gddResult = await AIEngine.callPM(pmHistory, gddPrompt);
357
 
358
+ diamondUsage += (gddResult.usage?.totalTokenCount || 0);
359
+ const gddText = gddResult.text;
360
+
361
  pmHistory.push({ role: 'user', parts: [{ text: gddPrompt }] });
362
+ pmHistory.push({ role: 'model', parts: [{ text: gddText }] });
363
 
364
+ // 2. Generate First Task (PM -> Diamond)
365
  const taskPrompt = "Based on the GDD, generate the first technical milestone.\nOutput format:\nTASK_NAME: <Name>\nWORKER_PROMPT: <Specific, isolated instructions for the worker>";
366
+ const taskResult = await AIEngine.callPM(pmHistory, taskPrompt);
367
+
368
+ diamondUsage += (taskResult.usage?.totalTokenCount || 0);
369
+ const taskText = taskResult.text;
370
 
371
  pmHistory.push({ role: 'user', parts: [{ text: taskPrompt }] });
372
+ pmHistory.push({ role: 'model', parts: [{ text: taskText }] });
 
 
373
 
374
+ // 3. Initialize Worker (Worker -> Basic)
375
+ const initialWorkerInstruction = extractWorkerPrompt(taskText) || `Initialize structure for: ${description}`;
376
  const workerHistory = [];
377
  const initialWorkerPrompt = `CONTEXT: New Project. \nINSTRUCTION: ${initialWorkerInstruction}`;
378
+
379
+ const workerResult = await AIEngine.callWorker(workerHistory, initialWorkerPrompt, []);
380
+
381
+ basicUsage += (workerResult.usage?.totalTokenCount || 0);
382
+ const workerText = workerResult.text;
383
 
384
  workerHistory.push({ role: 'user', parts: [{ text: initialWorkerPrompt }] });
385
+ workerHistory.push({ role: 'model', parts: [{ text: workerText }] });
386
 
387
+ // 4. Update Memory
388
  await StateManager.updateProject(projectId, {
389
  userId,
390
  pmHistory,
391
  workerHistory,
392
+ gdd: gddText,
393
  failureCount: 0
394
  });
395
 
396
+ // 5. Queue commands (Handle assets + basic costs)
397
+ // We pass userId to deduct image costs (Basic) immediately inside this function
398
+ await processAndQueueResponse(projectId, workerText, userId);
399
+
400
+ // 6. Update Status
401
+ if(db) await db.ref(`projects/${projectId}/info`).update({
402
+ status: "IDLE",
403
+ lastUpdated: Date.now()
404
+ });
405
+
406
+ // 7. Deduct Accumulated Credits
407
+ if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
408
+ if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
409
+
410
+ console.log(`[Background] Init complete. Diamond: ${diamondUsage}, Basic: ${basicUsage}`);
411
 
412
  } catch (err) {
413
+ console.error(`[Background] Init Error for ${projectId}:`, err);
414
+ if(db) await db.ref(`projects/${projectId}/info/status`).set("error");
415
  }
416
+ }
417
 
418
+ app.post('/new/project', validateRequest, async (req, res) => {
419
+ const { userId, projectId, description } = req.body;
 
 
 
420
 
421
+ try {
422
+ // Init requires BOTH types to be safe
423
+ await checkMinimumCredits(userId, 'diamond');
424
+ await checkMinimumCredits(userId, 'basic');
425
 
426
+ if(db) db.ref(`projects/${projectId}/info/status`).set("initializing");
 
 
 
 
 
427
 
428
+ res.json({
429
+ success: true,
430
+ message: "Project initialization started in background."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
431
  });
432
 
433
+ runBackgroundInitialization(projectId, userId, description);
 
434
 
435
+ } catch (err) {
436
+ if (err.message.includes("Insufficient")) {
437
+ return res.status(402).json({ error: err.message });
 
 
 
 
 
 
 
438
  }
439
+ res.status(500).json({ error: "Failed to start project" });
440
  }
441
+ });
442
 
443
+ app.post('/project/feedback', async (req, res) => {
444
+ const { userId, projectId, prompt, hierarchyContext, scriptContext, logContext, taskComplete, images } = req.body;
445
+
446
+ let diamondUsage = 0;
447
+ let basicUsage = 0;
448
+
449
+ try {
450
+ const project = await StateManager.getProject(projectId);
451
+ if (!project) return res.status(404).json({ error: "Project not found." });
452
+ if (project.userId !== userId) return res.status(403).json({ error: "Unauthorized" });
453
 
454
+ // Basic Check (most interaction is worker)
455
+ await checkMinimumCredits(userId, 'basic');
456
+
457
+ if(db) await db.ref(`projects/${projectId}/info/status`).set("working");
458
+
459
+ if (taskComplete) {
460
+ console.log(`[${projectId}] ✅ TASK COMPLETE.`);
461
+ const summary = `Worker completed the previous task. Logs: ${logContext?.logs || "Clean"}. \nGenerate the NEXT task using 'WORKER_PROMPT:' format.`;
462
 
463
+ // PM Call -> Diamond
464
+ const pmResult = await AIEngine.callPM(project.pmHistory, summary);
465
+ diamondUsage += (pmResult.usage?.totalTokenCount || 0);
466
+ const pmText = pmResult.text;
 
 
467
 
468
+ project.pmHistory.push({ role: 'user', parts: [{ text: summary }] });
469
+ project.pmHistory.push({ role: 'model', parts: [{ text: pmText }] });
470
+
471
+ const nextInstruction = extractWorkerPrompt(pmText);
472
+ if (!nextInstruction) {
473
+ await StateManager.updateProject(projectId, { pmHistory: project.pmHistory, status: "IDLE" });
474
+ if(db) await db.ref(`projects/${projectId}/info`).update({ status: "IDLE", lastUpdated: Date.now() });
475
+
476
+ // Deduct what we used
477
+ if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
478
+
479
+ return res.json({ success: true, message: "No further tasks. Project Idle." });
480
+ }
481
+
482
+ const newWorkerHistory = [];
483
+ const newPrompt = `New Objective: ${nextInstruction}`;
484
 
485
+ // Worker Call -> Basic
486
+ const workerResult = await AIEngine.callWorker(newWorkerHistory, newPrompt, []);
487
+ basicUsage += (workerResult.usage?.totalTokenCount || 0);
488
+ const workerText = workerResult.text;
489
+
490
+ newWorkerHistory.push({ role: 'user', parts: [{ text: newPrompt }] });
491
+ newWorkerHistory.push({ role: 'model', parts: [{ text: workerText }] });
492
+
493
+ await StateManager.updateProject(projectId, {
494
+ pmHistory: project.pmHistory,
495
+ workerHistory: newWorkerHistory,
496
+ failureCount: 0
497
+ });
498
+
499
+ StateManager.queueCommand(projectId, { type: "EXECUTE", payload: "warn('Starting Next Task...')" });
500
+ await processAndQueueResponse(projectId, workerText, userId);
501
 
502
+ if(db) await db.ref(`projects/${projectId}/info`).update({ status: "working", lastUpdated: Date.now() });
503
+
504
+ // Final Bill
505
+ if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
506
+ if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
507
+
508
+ return res.json({ success: true, message: "Next Task Assigned" });
509
+ }
510
+
511
+ // Logic for Failure Handling (PM Guidance)
512
+ if (project.failureCount > 3) {
513
+ const pmPrompt = (sysPrompts.pm_guidance_prompt || "Analyze logs: {{LOGS}}").replace('{{LOGS}}', logContext?.logs);
514
 
515
+ // PM -> Diamond
516
+ const pmResult = await AIEngine.callPM(project.pmHistory, pmPrompt);
517
+ diamondUsage += (pmResult.usage?.totalTokenCount || 0);
518
+ const pmVerdict = pmResult.text;
519
+
520
+ if (pmVerdict.includes("[TERMINATE]")) {
521
+ const fixInstruction = pmVerdict.replace("[TERMINATE]", "").trim();
522
+ const resetHistory = [];
523
+ const resetPrompt = `[SYSTEM]: Previous worker terminated. \nNew Objective: ${fixInstruction}`;
524
+
525
+ // Worker -> Basic
526
+ const workerResult = await AIEngine.callWorker(resetHistory, resetPrompt, []);
527
+ basicUsage += (workerResult.usage?.totalTokenCount || 0);
528
+
529
+ resetHistory.push({ role: 'user', parts: [{ text: resetPrompt }] });
530
+ resetHistory.push({ role: 'model', parts: [{ text: workerResult.text }] });
531
+
532
+ await StateManager.updateProject(projectId, { workerHistory: resetHistory, failureCount: 0 });
533
+ StateManager.queueCommand(projectId, { type: "EXECUTE", payload: "print('SYSTEM: Worker Reset')" });
534
+ await processAndQueueResponse(projectId, workerResult.text, userId);
535
+
536
+ if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
537
+ if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
538
+ return res.json({ success: true, message: "Worker Terminated." });
539
+ } else {
540
+ const injection = `[PM GUIDANCE]: ${pmVerdict} \n\nApply this fix now.`;
541
+
542
+ // Worker -> Basic
543
+ const workerResult = await AIEngine.callWorker(project.workerHistory, injection, []);
544
+ basicUsage += (workerResult.usage?.totalTokenCount || 0);
545
+
546
+ project.workerHistory.push({ role: 'user', parts: [{ text: injection }] });
547
+ project.workerHistory.push({ role: 'model', parts: [{ text: workerResult.text }] });
548
+
549
+ await StateManager.updateProject(projectId, { workerHistory: project.workerHistory, failureCount: 1 });
550
+ await processAndQueueResponse(projectId, workerResult.text, userId);
551
+
552
+ if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
553
+ if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
554
+ return res.json({ success: true, message: "PM Guidance Applied." });
555
+ }
556
  }
 
557
 
558
+ // Standard Interaction (Worker Only = Basic)
 
559
  const fullInput = `USER: ${prompt || "Automatic Feedback"}` + formatContext({ hierarchyContext, scriptContext, logContext });
560
 
561
+ let workerResult = await AIEngine.callWorker(project.workerHistory, fullInput, images || []);
562
+ basicUsage += (workerResult.usage?.totalTokenCount || 0);
563
+ let responseText = workerResult.text;
564
 
565
+ // PM Consultation Check
566
+ const pmQuestion = extractPMQuestion(responseText);
567
  if (pmQuestion) {
568
+ // PM -> Diamond
 
569
  const pmConsultPrompt = `[WORKER CONSULTATION]: The Worker asks: "${pmQuestion}"\nProvide a technical answer to unblock them.`;
570
+ const pmResult = await AIEngine.callPM(project.pmHistory, pmConsultPrompt);
571
+ diamondUsage += (pmResult.usage?.totalTokenCount || 0);
572
+ const pmAnswer = pmResult.text;
573
 
 
574
  project.pmHistory.push({ role: 'user', parts: [{ text: pmConsultPrompt }] });
575
  project.pmHistory.push({ role: 'model', parts: [{ text: pmAnswer }] });
576
 
 
577
  const injectionMsg = `[PM RESPONSE]: ${pmAnswer}`;
578
  project.workerHistory.push({ role: 'user', parts: [{ text: fullInput }] });
579
+ project.workerHistory.push({ role: 'model', parts: [{ text: responseText }] });
580
 
581
+ // Re-call Worker -> Basic
582
+ workerResult = await AIEngine.callWorker(project.workerHistory, injectionMsg, []);
583
+ basicUsage += (workerResult.usage?.totalTokenCount || 0);
584
+ responseText = workerResult.text;
585
+
586
  project.workerHistory.push({ role: 'user', parts: [{ text: injectionMsg }] });
587
+ project.workerHistory.push({ role: 'model', parts: [{ text: responseText }] });
588
  } else {
589
  project.workerHistory.push({ role: 'user', parts: [{ text: fullInput }] });
590
+ project.workerHistory.push({ role: 'model', parts: [{ text: responseText }] });
591
  }
592
 
593
  await StateManager.updateProject(projectId, {
 
596
  failureCount: project.failureCount
597
  });
598
 
599
+ await processAndQueueResponse(projectId, responseText, userId);
600
+
601
+ if(db) await db.ref(`projects/${projectId}/info`).update({ status: "idle", lastUpdated: Date.now() });
602
+
603
+ // Final Billing
604
+ if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
605
+ if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
606
+
607
  res.json({ success: true });
608
 
609
  } catch (err) {
610
  console.error("AI Error:", err);
611
+ if (err.message.includes("Insufficient")) {
612
+ return res.status(402).json({ error: err.message });
613
+ }
614
  res.status(500).json({ error: "AI Failed" });
615
  }
616
  });
617
 
 
 
 
618
  app.post('/project/ping', async (req, res) => {
619
+ const { projectId, userId } = req.body;
620
+ if (!projectId || !userId) return res.status(400).json({ error: "Missing ID fields" });
621
+
622
+ const project = await StateManager.getProject(projectId);
623
+ if (!project) return res.status(404).json({ action: "IDLE", error: "Project not found" });
624
+ if (project.userId !== userId) return res.status(403).json({ error: "Unauthorized" });
625
+
626
  const command = await StateManager.popCommand(projectId);
627
 
628
  if (command) {
 
640
  }
641
  });
642
 
 
 
 
643
  app.post('/human/override', validateRequest, async (req, res) => {
644
+ const { projectId, instruction, pruneHistory, userId } = req.body;
645
+ let basicUsage = 0;
 
 
 
 
 
 
 
646
 
647
+ try {
648
+ await checkMinimumCredits(userId, 'basic');
 
649
 
650
+ const project = await StateManager.getProject(projectId);
651
+ const overrideMsg = `[SYSTEM OVERRIDE]: ${instruction}`;
 
 
652
 
653
+ if (pruneHistory && project.workerHistory.length >= 2) {
654
+ project.workerHistory.pop();
655
+ project.workerHistory.pop();
656
+ }
657
 
658
+ // Worker -> Basic
659
+ const workerResult = await AIEngine.callWorker(project.workerHistory, overrideMsg, []);
660
+ basicUsage += (workerResult.usage?.totalTokenCount || 0);
661
+
662
+ project.workerHistory.push({ role: 'user', parts: [{ text: overrideMsg }] });
663
+ project.workerHistory.push({ role: 'model', parts: [{ text: workerResult.text }] });
664
+
665
+ await StateManager.updateProject(projectId, { workerHistory: project.workerHistory });
666
+ await processAndQueueResponse(projectId, workerResult.text, userId);
667
+
668
+ if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
669
+
670
+ res.json({ success: true });
671
+ } catch (err) {
672
+ if (err.message.includes("Insufficient")) {
673
+ return res.status(402).json({ error: err.message });
674
+ }
675
+ res.status(500).json({ error: "Override Failed" });
676
+ }
677
+ });
678
+
679
+ // Helper to handle Asset Parsing & Billing (Basic Credits)
680
+ async function processAndQueueResponse(projectId, rawResponse, userId) {
681
  const imgPrompt = extractImagePrompt(rawResponse);
682
  if (imgPrompt) {
683
  console.log(`[${projectId}] 🎨 Generating Asset: ${imgPrompt}`);
 
684
 
685
+ // 1. Generate Image (returns { image, usage } or null)
686
+ const imgResult = await AIEngine.generateImage(imgPrompt);
687
+
688
+ if (imgResult && imgResult.image) {
689
+ // 2. Bill for image tokens immediately (Basic)
690
+ const imgTokens = imgResult.usage?.totalTokenCount || 0;
691
+ const totalCost = imgTokens; // imgTokens + IMAGE_COST_BASIC;
692
+
693
+ if (userId && totalCost > 0) {
694
+ await deductUserCredits(userId, totalCost, 'basic');
695
+ }
696
+
697
+ // 3. Queue creation command
698
+ await StateManager.queueCommand(projectId, { type: "CREATE_ASSET", payload: imgResult.image });
699
  }
700
  }
701
+ // Queue the raw text response for the frontend
 
702
  await StateManager.queueCommand(projectId, rawResponse);
703
  }
704