everydaytok commited on
Commit
e9e20ec
·
verified ·
1 Parent(s): 893fe0d

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +135 -1276
app.js CHANGED
@@ -1,1310 +1,169 @@
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 crypto from "crypto";
8
- import dotenv from 'dotenv';
9
 
10
- dotenv.config();
 
 
 
 
 
11
 
12
- // Initialize Database
13
- initDB();
14
- const supabase = StateManager.getSupabaseClient();
15
 
 
16
  const app = express();
17
- const PORT = process.env.PORT || 7860;
 
18
 
 
19
  app.use(cors());
20
- app.use(express.json({ limit: '50mb' }));
21
 
22
- // --- BILLING CONSTANTS ---
23
- const MIN_BASIC_REQUIRED = 100;
24
- const MIN_DIAMOND_REQUIRED = 100;
25
-
26
- // --- STATUS PHASES ---
27
- const WORKER_PHASES = [
28
- "Worker: Sorting...",
29
- "Worker: Analyzing Request...",
30
- "Worker: Reading Context...",
31
- "Worker: Checking Hierarchy...",
32
- "Worker: Planning Logic...",
33
- "Worker: Preparing Environment...",
34
- "Worker: Thinking..."
35
- ];
36
-
37
- const PM_PHASES = [
38
- "Manager: Sorting...",
39
- "Manager: Reviewing Request...",
40
- "Manager: Analyzing Project Structure...",
41
- "Manager: Consulting Guidelines...",
42
- "Manager: Formulating Strategy...",
43
- "Manager: Delegating Tasks...",
44
- "Manager: Thinking..."
45
- ];
46
-
47
- // --- HELPER FUNCTIONS ---
48
- function startStatusLoop(projectId, type = 'worker') {
49
- const phases = type === 'pm' ? PM_PHASES : WORKER_PHASES;
50
- let index = 0;
51
-
52
- // Set the initial state immediately
53
- StateManager.setStatus(projectId, phases[0]);
54
-
55
- const interval = setInterval(() => {
56
- if (index < phases.length - 1) {
57
- index++;
58
- StateManager.setStatus(projectId, phases[index]);
59
- } else {
60
- // Stop the loop once we reach the last entry
61
- clearInterval(interval);
62
- }
63
- }, 3500);
64
-
65
- return () => clearInterval(interval);
66
- };
67
-
68
- function extractWorkerPrompt(text) {
69
- if (!text || typeof text !== 'string') return null;
70
- const match = text.match(/WORKER_PROMPT:\s*(.*)/s);
71
- return match ? match[1].trim() : null;
72
- }
73
-
74
- function extractPMQuestion(text) {
75
- if (!text || typeof text !== 'string') return null;
76
- const match = text.match(/\[ASK_PM:\s*(.*?)\]/s);
77
- return match ? match[1].trim() : null;
78
- }
79
-
80
- function extractImagePrompt(text) {
81
- if (!text || typeof text !== 'string') return null;
82
- const match = text.match(/\[GENERATE_IMAGE:\s*(.*?)\]/s);
83
- return match ? match[1].trim() : null;
84
- }
85
-
86
- function extractRouteToPM(text) {
87
- if (!text || typeof text !== 'string') return null;
88
- const match = text.match(/\[ROUTE_TO_PM:\s*(.*?)\]/s);
89
- return match ? match[1].trim() : null;
90
- }
91
-
92
- function formatContext({ hierarchyContext, scriptContext, logContext }) {
93
- let parts = [];
94
-
95
- if (hierarchyContext) {
96
- parts.push(`[HIERARCHY_VIEW]\n${hierarchyContext}`);
97
- }
98
-
99
- if (scriptContext && scriptContext.targetName) {
100
- // Enforce code block formatting for the script content
101
- const source = scriptContext.scriptSource || "(Empty Script)";
102
- parts.push(`[ACTIVE_SCRIPT: ${scriptContext.targetName}]\n\`\`\`lua\n${source}\n\`\`\``);
103
- }
104
-
105
- if (logContext && logContext.logs) {
106
- parts.push(`[OUTPUT_LOGS]\n${logContext.logs}`);
107
- }
108
-
109
- if (parts.length === 0) return "";
110
-
111
- return `\n\n=== STUDIO CONTEXT ===\n${parts.join("\n\n")}\n======================\n`;
112
- }
113
-
114
- const validateRequest = (req, res, next) => {
115
- if (req.path.includes('/admin/cleanup')) return next();
116
- const { userId } = req.body;
117
- if (!userId && (req.path.includes('/onboarding') || req.path.includes('/new') || req.path.includes('/project'))) {
118
- return res.status(400).json({ error: "Missing userId" });
119
- }
120
- next();
121
- };
122
-
123
- // --- BILLING LOGIC ---
124
-
125
- async function checkMinimumCredits(userId, type = 'basic') {
126
- if (!supabase) return;
127
- const { data } = await supabase.from('users').select('credits').eq('id', userId).single();
128
- if (!data) return;
129
-
130
- const credits = data.credits?.[type] || 0;
131
- const req = type === 'diamond' ? MIN_DIAMOND_REQUIRED : MIN_BASIC_REQUIRED;
132
-
133
- if (credits < req) {
134
- throw new Error(`Insufficient ${type} credits. Required: ${req}, Available: ${credits}`);
135
- }
136
- }
137
-
138
- async function deductUserCredits(userId, amount, type = 'basic') {
139
- const deduction = Math.floor(parseInt(amount, 10));
140
- if (!supabase || !deduction || deduction <= 0) return;
141
-
142
- try {
143
- const { data } = await supabase.from('users').select('credits').eq('id', userId).single();
144
- if (!data) return;
145
-
146
- const currentCredits = data.credits || { basic: 0, diamond: 0 };
147
- const currentVal = currentCredits[type] || 0;
148
- const newVal = Math.max(0, currentVal - deduction);
149
-
150
- const updatedCredits = { ...currentCredits, [type]: newVal };
151
-
152
- await supabase.from('users').update({ credits: updatedCredits }).eq('id', userId);
153
- console.log(`[Credits] User ${userId} | -${deduction} ${type} | Remaining: ${newVal}`);
154
- } catch (err) {
155
- console.error("Deduction failed", err);
156
- }
157
- }
158
-
159
- // --- CORE LOGIC: Background Initialization ---
160
-
161
- async function runBackgroundInitialization(projectId, userId, description) {
162
- if (StateManager.isLocked(projectId)) {
163
- console.log(`[Init Guard] Project ${projectId} is already initializing. Skipping.`);
164
- return;
165
- }
166
-
167
- StateManager.lock(projectId);
168
- console.log(`[Background] Starting initialization for ${projectId}`);
169
-
170
- let diamondUsage = 0;
171
- let basicUsage = 0;
172
-
173
- try {
174
- await StateManager.getProject(projectId);
175
-
176
- await StateManager.updateProject(projectId, {
177
- status: "working",
178
- gdd: "",
179
- failureCount: 0
180
- });
181
-
182
- const pmHistory = [];
183
-
184
- // --- Step 1: Generate GDD (PM) ---
185
- let stopStatus = startStatusLoop(projectId, 'pm');
186
- const gddPrompt = `Create a comprehensive Game Design Document (GDD) for: ${description}`;
187
- const gddResult = await AIEngine.callPM(pmHistory, gddPrompt);
188
- stopStatus();
189
-
190
- if (!gddResult?.text) throw new Error("PM failed to generate GDD");
191
-
192
- const gddText = gddResult.text;
193
- diamondUsage += (gddResult.usage?.totalTokenCount || 0);
194
-
195
- await StateManager.addHistory(projectId, 'pm', 'user', gddPrompt);
196
- await StateManager.addHistory(projectId, 'pm', 'model', gddText);
197
-
198
- pmHistory.push({ role: 'user', parts: [{ text: gddPrompt }] });
199
- pmHistory.push({ role: 'model', parts: [{ text: gddText }] });
200
-
201
- // --- Step 2: Generate Tasks (PM) ---
202
- stopStatus = startStatusLoop(projectId, 'pm');
203
- const taskPrompt = "Based on the GDD, generate the first technical milestone.\nOutput format:\nTASK_NAME: <Name>\nWORKER_PROMPT: <Specific, isolated instructions for the worker>";
204
- const taskResult = await AIEngine.callPM(pmHistory, taskPrompt);
205
- stopStatus();
206
-
207
- if (!taskResult?.text) throw new Error("PM failed to generate Task");
208
-
209
- const taskText = taskResult.text;
210
- diamondUsage += (taskResult.usage?.totalTokenCount || 0);
211
-
212
- await StateManager.addHistory(projectId, 'pm', 'user', taskPrompt);
213
- await StateManager.addHistory(projectId, 'pm', 'model', taskText);
214
-
215
- // --- Step 3: Initialize Worker ---
216
- const initialWorkerInstruction = extractWorkerPrompt(taskText) || `Initialize structure for: ${description}`;
217
- const initialWorkerPrompt = `CONTEXT: New Project. \nINSTRUCTION: ${initialWorkerInstruction}`;
218
-
219
- stopStatus = startStatusLoop(projectId, 'worker');
220
-
221
- let workerTextAccumulated = "";
222
-
223
- const workerResult = await AIEngine.callWorkerStream(
224
- [],
225
- initialWorkerPrompt,
226
- (thought) => {
227
- stopStatus();
228
- StateManager.setStatus(projectId, "Worker: Thinking...");
229
- StateManager.appendSnapshotOnly(projectId, thought);
230
- },
231
- (chunk) => {
232
- stopStatus();
233
- StateManager.setStatus(projectId, "Worker: Coding...");
234
- workerTextAccumulated += chunk;
235
- StateManager.appendStream(projectId, chunk);
236
- }
237
- );
238
- stopStatus();
239
-
240
- basicUsage += (workerResult.usage?.totalTokenCount || 0);
241
-
242
- if (!workerTextAccumulated && !workerResult.text) throw new Error("Worker failed to initialize");
243
- const finalWorkerText = workerResult.text || workerTextAccumulated;
244
-
245
- await StateManager.addHistory(projectId, 'worker', 'user', initialWorkerPrompt);
246
- await StateManager.addHistory(projectId, 'worker', 'model', finalWorkerText);
247
-
248
- await StateManager.updateProject(projectId, {
249
- gdd: gddText,
250
- status: "idle"
251
- });
252
-
253
- await processAndQueueResponse(projectId, finalWorkerText, userId);
254
- console.log(`[Background] Init complete for ${projectId}`);
255
-
256
- } catch (err) {
257
- console.error(`[Background] Init Error for ${projectId}:`, err.message);
258
- await StateManager.updateProject(projectId, { status: "error" });
259
- } finally {
260
- StateManager.unlock(projectId);
261
- if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
262
- if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
263
- }
264
- }
265
-
266
- // --- CORE LOGIC: Async Feedback Loop ---
267
-
268
- async function runAsyncFeedback(projectId, userId, fullInput, images = []) {
269
- let diamondUsage = 0;
270
- let basicUsage = 0;
271
-
272
- try {
273
- console.log(`[${projectId}] Feedback received. Persisting state immediately.`);
274
-
275
- await Promise.all([
276
- StateManager.updateProject(projectId, { status: "working" }),
277
- StateManager.addHistory(projectId, 'worker', 'user', fullInput)
278
- ]);
279
-
280
- const project = await StateManager.getProject(projectId);
281
-
282
- StateManager.clearSnapshot(projectId);
283
- let stopStatus = startStatusLoop(projectId, 'worker');
284
-
285
- let firstTurnResponse = "";
286
- let thoughtText = "";
287
-
288
- const currentWorkerHistory = [...(project.workerHistory || [])];
289
-
290
- // 2. First Turn (Worker Execution)
291
- const workerResult = await AIEngine.callWorkerStream(
292
- currentWorkerHistory,
293
- fullInput,
294
- (thought) => {
295
- stopStatus();
296
- thoughtText += thought;
297
- StateManager.setStatus(projectId, "Worker: Thinking...");
298
- StateManager.appendSnapshotOnly(projectId, thought);
299
- },
300
- (chunk) => {
301
- stopStatus();
302
- firstTurnResponse += chunk;
303
- StateManager.setStatus(projectId, "Worker: Coding...");
304
- StateManager.appendStream(projectId, chunk);
305
- },
306
- images
307
- );
308
- stopStatus();
309
- basicUsage += (workerResult.usage?.totalTokenCount || 0);
310
- firstTurnResponse = workerResult.text || firstTurnResponse;
311
-
312
- // 3. Analyze Output (Delegation/Questions)
313
- const routeTask = extractRouteToPM(firstTurnResponse);
314
- const pmQuestion = extractPMQuestion(firstTurnResponse);
315
- let finalResponseToSave = firstTurnResponse;
316
-
317
- if (routeTask || pmQuestion) {
318
- await StateManager.addHistory(projectId, 'worker', 'model', firstTurnResponse);
319
- currentWorkerHistory.push({ role: 'model', parts: [{ text: firstTurnResponse }] });
320
-
321
- let pmPrompt = "";
322
- let pmContextPrefix = "";
323
-
324
- if (routeTask) {
325
- console.log(`[${projectId}] Routing task to PM...`);
326
- pmPrompt = `[ROUTED TASK]: ${routeTask}\nWrite the backend/logic. Then use WORKER_PROMPT: <task> to delegate.`;
327
- pmContextPrefix = "[PM DELEGATION]:";
328
- } else {
329
- console.log(`[${projectId}] Worker consulting PM...`);
330
- pmPrompt = `[WORKER QUESTION]: ${pmQuestion}\nProvide a technical answer or code snippet.`;
331
- pmContextPrefix = "[PM ANSWER]:";
332
  }
333
-
334
- // 4. Run PM (Diamond Logic)
335
- StateManager.clearSnapshot(projectId);
336
- stopStatus = startStatusLoop(projectId, 'pm');
337
-
338
- let pmResponseText = "";
339
-
340
- const pmResult = await AIEngine.callPMStream(
341
- project.pmHistory,
342
- pmPrompt,
343
- (thought) => {
344
- stopStatus();
345
- StateManager.setStatus(projectId, "Manager: Thinking...");
346
- StateManager.appendSnapshotOnly(projectId, thought);
347
- },
348
- (chunk) => {
349
- stopStatus();
350
- StateManager.setStatus(projectId, "Manager: Architecture...");
351
- pmResponseText += chunk;
352
- StateManager.appendSnapshotOnly(projectId, chunk);
353
- }
354
- );
355
- stopStatus();
356
- diamondUsage += (pmResult.usage?.totalTokenCount || 0);
357
- pmResponseText = pmResult.text || pmResponseText;
358
-
359
- await StateManager.addHistory(projectId, 'pm', 'user', pmPrompt);
360
- await StateManager.addHistory(projectId, 'pm', 'model', pmResponseText);
361
-
362
- // --- EXECUTE PM CODE ---
363
- await processAndQueueResponse(projectId, pmResponseText, userId);
364
-
365
- // 5. Run Worker Continuation
366
- const nextInstruction = extractWorkerPrompt(pmResponseText) || pmResponseText;
367
- const workerContinuationPrompt = `${pmContextPrefix} ${nextInstruction}\n\nBased on this, continue the task and output the code.`;
368
-
369
- await StateManager.addHistory(projectId, 'worker', 'user', workerContinuationPrompt);
370
- currentWorkerHistory.push({ role: 'user', parts: [{ text: workerContinuationPrompt }] });
371
-
372
- StateManager.clearSnapshot(projectId);
373
- stopStatus = startStatusLoop(projectId, 'worker');
374
-
375
- let secondTurnResponse = "";
376
-
377
- const secondWorkerResult = await AIEngine.callWorkerStream(
378
- currentWorkerHistory,
379
- workerContinuationPrompt,
380
- (thought) => {
381
- stopStatus();
382
- StateManager.setStatus(projectId, "Worker: Applying Fix...");
383
- StateManager.appendSnapshotOnly(projectId, thought);
384
- },
385
- (chunk) => {
386
- stopStatus();
387
- StateManager.setStatus(projectId, "Worker: Finalizing...");
388
- secondTurnResponse += chunk;
389
- StateManager.appendStream(projectId, chunk);
390
- }
391
- );
392
- stopStatus();
393
-
394
- basicUsage += (secondWorkerResult.usage?.totalTokenCount || 0);
395
- secondTurnResponse = secondWorkerResult.text || secondTurnResponse;
396
-
397
- await StateManager.addHistory(projectId, 'worker', 'model', secondTurnResponse);
398
- finalResponseToSave = secondTurnResponse;
399
-
400
- } else {
401
- // No delegation, just save the first response
402
- await StateManager.addHistory(projectId, 'worker', 'model', firstTurnResponse);
403
  }
404
 
405
- // --- STATUS RESET ---
406
- StateManager.setStatus(projectId, "Idle");
407
-
408
- // 6. Final Execution & Cleanup
409
- await processAndQueueResponse(projectId, finalResponseToSave, userId);
410
-
411
- await StateManager.updateProject(projectId, { status: "idle" });
412
-
413
- } catch (err) {
414
- console.error("Async Feedback Error:", err);
415
- StateManager.setStatus(projectId, "Error: " + err.message);
416
- await StateManager.updateProject(projectId, { status: "error" });
417
- } finally {
418
- if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
419
- if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
420
- }
421
- }
422
-
423
- async function processAndQueueResponse(projectId, rawResponse, userId) {
424
- if (!rawResponse) return;
425
-
426
- // Image Generation
427
- const imgPrompt = extractImagePrompt(rawResponse);
428
- if (imgPrompt) {
429
- try {
430
- const imgResult = await AIEngine.generateImage(imgPrompt);
431
- if (imgResult?.image) {
432
- if (userId) await deductUserCredits(userId, 1000, 'basic'); // Flat fee
433
- await StateManager.queueCommand(projectId, { type: "CREATE_ASSET", payload: imgResult.image });
434
- }
435
- } catch (e) {
436
- console.error("Image gen failed", e);
437
- }
438
- }
439
-
440
- // Code Execution
441
- const codeMatch = rawResponse.match(/```(?:lua|luau)?([\s\S]*?)```/i);
442
- if (codeMatch) {
443
- await StateManager.queueCommand(projectId, { type: "EXECUTE", payload: codeMatch[1].trim() });
444
- }
445
-
446
- // Explicit Read Commands (Parsing what the AI wrote)
447
- await StateManager.queueCommand(projectId, rawResponse);
448
- }
449
-
450
- // --- EXPRESS ROUTES ---
451
-
452
- app.post('/onboarding/analyze', validateRequest, async (req, res) => {
453
- const { description, userId } = req.body;
454
- if (!description) return res.status(400).json({ error: "Description required" });
455
-
456
- try {
457
- try {
458
- await checkMinimumCredits(userId, 'basic');
459
- } catch (creditErr) {
460
- return res.json({ success: false, insufficient: true, error: creditErr.message });
461
- }
462
-
463
- const result = await AIEngine.generateEntryQuestions(description);
464
-
465
- if (result.usage?.totalTokenCount > 0) {
466
- await deductUserCredits(userId, result.usage.totalTokenCount, 'basic');
467
- }
468
-
469
- if (result.status === "REJECTED") {
470
- return res.json({ rejected: true, reason: result.reason || "Idea violates TOS." });
471
- }
472
-
473
- res.json({ questions: result.questions });
474
- } catch (err) {
475
- res.status(500).json({ error: err.message });
476
- }
477
- });
478
-
479
- app.post('/onboarding/create', validateRequest, async (req, res) => {
480
- const { userId, description, answers } = req.body;
481
-
482
- try {
483
-
484
- try {
485
- await checkMinimumCredits(userId, 'basic');
486
- await checkMinimumCredits(userId, 'diamond');
487
- } catch (creditErr) {
488
- return res.json({ success: false, insufficient: true, error: creditErr.message });
489
- }
490
-
491
- const randomHex = (n = 6) => crypto.randomBytes(Math.ceil(n/2)).toString("hex").slice(0, n);
492
- const projectId = `proj_${Date.now()}_${randomHex(7)}`;
493
-
494
- const gradingResult = await AIEngine.gradeProject(description, answers);
495
-
496
- if (gradingResult.usage?.totalTokenCount) {
497
- await deductUserCredits(userId, gradingResult.usage.totalTokenCount, 'basic');
498
- }
499
-
500
- const isFailure = gradingResult.feasibility < 30 || gradingResult.rating === 'F';
501
-
502
- if (!isFailure) {
503
- const { error: insertError } = await supabase.from('projects').insert({
504
- id: projectId,
505
- user_id: userId,
506
- info: {
507
- title: gradingResult.title || "Untitled",
508
- stats: gradingResult,
509
- description,
510
- answers,
511
- status: "initializing"
512
- }
513
  });
514
-
515
- if (insertError) {
516
- console.error("Failed to insert project:", insertError.message);
517
- return res.status(500).json({ error: "Database save failed" });
518
- }
519
-
520
- runBackgroundInitialization(projectId, userId, description);
521
- }
522
-
523
- res.json({
524
- status: 200,
525
- success: !isFailure,
526
- projectId,
527
- stats: gradingResult,
528
- title: gradingResult.title || "Untitled"
529
- });
530
-
531
- } catch (err) {
532
- console.error("Create Error:", err);
533
- res.status(500).json({ error: err.message });
534
- }
535
- });
536
-
537
- app.post('/project/feedback', async (req, res) => {
538
- const { userId, projectId, prompt, hierarchyContext, scriptContext, logContext, images } = req.body;
539
-
540
- try {
541
- const project = await StateManager.getProject(projectId);
542
- if (!project || project.userId !== userId) return res.status(403).json({ error: "Auth Error" });
543
-
544
- try {
545
- await checkMinimumCredits(userId, 'basic');
546
- } catch (creditErr) {
547
- return res.json({ success: false, insufficient: true, error: creditErr.message });
548
- }
549
-
550
- // Pass the context objects into formatContext
551
- const formattedContext = formatContext({ hierarchyContext, scriptContext, logContext });
552
-
553
- // Append to user prompt
554
- const fullInput = `USER: ${prompt || "Automatic Feedback"}${formattedContext}`;
555
-
556
- // Trigger Async
557
- runAsyncFeedback(projectId, userId, fullInput, images || []);
558
-
559
- res.json({ success: true, message: "Processing started" });
560
-
561
- } catch (err) {
562
- console.error("Feedback Error:", err);
563
- await StateManager.updateProject(projectId, { status: "error" });
564
- res.status(500).json({ error: "Feedback process failed" });
565
- }
566
- });
567
-
568
- app.post('/project/ping', async (req, res) => {
569
- const { projectId, userId, isFrontend } = req.body;
570
- if (!projectId || !userId) return res.status(400).json({ error: "Missing IDs" });
571
-
572
- const project = await StateManager.getProject(projectId);
573
- if (!project || project.userId !== userId) return res.json({ action: "IDLE" });
574
-
575
- // 1. BASE RESPONSE (Visuals)
576
- const response = {
577
- action: "IDLE",
578
- status: StateManager.getStatus(projectId), // Will now return "Idle" by default
579
- snapshot: StateManager.getSnapshot(projectId)
580
- };
581
-
582
- // 2. EXECUTOR LOGIC (Plugin Only)
583
- if (!isFrontend) {
584
- const command = await StateManager.popCommand(projectId);
585
-
586
- if (command) {
587
- response.action = command.type;
588
- response.target = command.payload;
589
- response.code = command.type === 'EXECUTE' ? command.payload : null;
590
  }
591
- }
592
-
593
- res.json(response);
594
- });
595
-
596
-
597
- app.post('/human/override', validateRequest, async (req, res) => {
598
- const { projectId, instruction, userId } = req.body;
599
- try {
600
- await checkMinimumCredits(userId, 'basic');
601
- const project = await StateManager.getProject(projectId);
602
-
603
- res.json({ success: true });
604
-
605
- const overrideMsg = `[SYSTEM OVERRIDE]: ${instruction}`;
606
-
607
- let overrideText = "";
608
- let stopStatus = startStatusLoop(projectId, 'worker');
609
-
610
- const workerResult = await AIEngine.callWorkerStream(
611
- project.workerHistory,
612
- overrideMsg,
613
- (thought) => {
614
- stopStatus();
615
- StateManager.setStatus(projectId, "Worker: Thinking...");
616
- StateManager.appendSnapshotOnly(projectId, thought);
617
- },
618
- (chunk) => {
619
- stopStatus();
620
- StateManager.setStatus(projectId, "Worker: Coding...");
621
- overrideText += chunk;
622
- StateManager.appendStream(projectId, chunk);
623
- }
624
- );
625
- stopStatus();
626
-
627
- await StateManager.addHistory(projectId, 'worker', 'user', overrideMsg);
628
- await StateManager.addHistory(projectId, 'worker', 'model', workerResult.text || overrideText);
629
- await processAndQueueResponse(projectId, workerResult.text || overrideText, userId);
630
-
631
- const usage = workerResult.usage?.totalTokenCount || 0;
632
- if (usage > 0) await deductUserCredits(userId, usage, 'basic');
633
-
634
- } catch (err) {
635
- console.error("Override failed", err);
636
- }
637
- });
638
 
639
- app.get('/admin/cleanup', async (req, res) => {
640
- try {
641
- const removed = StateManager.cleanupMemory ? StateManager.cleanupMemory() : 0;
642
- res.json({ success: true, removedCount: removed });
643
- } catch (err) {
644
- res.status(500).json({ error: "Cleanup failed" });
645
- }
646
- });
647
-
648
- app.listen(PORT, () => {
649
- console.log(`AI Backend Running on ${PORT} (Supabase + Billing Edition)`);
650
- });
651
-
652
- /* import express from 'express';
653
- import bodyParser from 'body-parser';
654
- import cors from 'cors';
655
- import { StateManager, initDB } from './stateManager.js';
656
- import { AIEngine } from './aiEngine.js';
657
- import fs from 'fs';
658
- import crypto from "crypto";
659
- import dotenv from 'dotenv';
660
-
661
- dotenv.config();
662
-
663
- // Initialize Database
664
- initDB();
665
- const supabase = StateManager.getSupabaseClient();
666
-
667
- const app = express();
668
- const PORT = process.env.PORT || 7860;
669
-
670
- app.use(cors());
671
- app.use(express.json({ limit: '50mb' }));
672
-
673
- // --- BILLING CONSTANTS ---
674
- const MIN_BASIC_REQUIRED = 100;
675
- const MIN_DIAMOND_REQUIRED = 100;
676
-
677
- // --- STATUS PHASES ---
678
- const WORKER_PHASES = [
679
- "Worker: Sorting...",
680
- "Worker: Analyzing Request...",
681
- "Worker: Reading Context...",
682
- "Worker: Checking Hierarchy...",
683
- "Worker: Planning Logic...",
684
- "Worker: Preparing Environment...",
685
- "Worker: Thinking..."
686
- ];
687
-
688
- const PM_PHASES = [
689
- "Manager: Sorting...",
690
- "Manager: Reviewing Request...",
691
- "Manager: Analyzing Project Structure...",
692
- "Manager: Consulting Guidelines...",
693
- "Manager: Formulating Strategy...",
694
- "Manager: Delegating Tasks...",
695
- "Manager: Thinking..."
696
- ];
697
-
698
- // --- HELPER FUNCTIONS ---
699
- function startStatusLoop(projectId, type = 'worker') {
700
- const phases = type === 'pm' ? PM_PHASES : WORKER_PHASES;
701
- let index = 0;
702
-
703
- // Set the initial state immediately
704
- StateManager.setStatus(projectId, phases[0]);
705
-
706
- const interval = setInterval(() => {
707
- if (index < phases.length - 1) {
708
- index++;
709
- StateManager.setStatus(projectId, phases[index]);
710
- } else {
711
- // Stop the loop once we reach the last entry
712
- clearInterval(interval);
713
  }
714
- }, 3500); // 3.5 second delay as requested
715
-
716
- // Return a cleanup function in case the component unmounts early
717
- return () => clearInterval(interval);
718
- };
719
-
720
-
721
- function extractWorkerPrompt(text) {
722
- if (!text || typeof text !== 'string') return null;
723
- const match = text.match(/WORKER_PROMPT:\s*(.*)/s);
724
- return match ? match[1].trim() : null;
725
- }
726
-
727
- function extractPMQuestion(text) {
728
- if (!text || typeof text !== 'string') return null;
729
- const match = text.match(/\[ASK_PM:\s*(.*?)\]/s);
730
- return match ? match[1].trim() : null;
731
- }
732
-
733
- function extractImagePrompt(text) {
734
- if (!text || typeof text !== 'string') return null;
735
- const match = text.match(/\[GENERATE_IMAGE:\s*(.*?)\]/s);
736
- return match ? match[1].trim() : null;
737
- }
738
-
739
- function extractRouteToPM(text) {
740
- if (!text || typeof text !== 'string') return null;
741
- const match = text.match(/\[ROUTE_TO_PM:\s*(.*?)\]/s);
742
- return match ? match[1].trim() : null;
743
- }
744
-
745
- function formatContext({ hierarchyContext, scriptContext, logContext }) {
746
- let out = "";
747
- if (scriptContext) out += `\n[TARGET SCRIPT]: ${scriptContext.targetName}\n[SOURCE PREVIEW]: ${scriptContext.scriptSource}`;
748
- if (logContext) out += `\n[LAST LOGS]: ${logContext.logs}`;
749
- if (hierarchyContext) out += `\n[Hierarchy Context]: ${hierarchyContext}`;
750
- return out;
751
- }
752
-
753
- const validateRequest = (req, res, next) => {
754
- if (req.path.includes('/admin/cleanup')) return next();
755
- const { userId } = req.body;
756
- if (!userId && (req.path.includes('/onboarding') || req.path.includes('/new') || req.path.includes('/project'))) {
757
- return res.status(400).json({ error: "Missing userId" });
758
- }
759
- next();
760
- };
761
-
762
- // --- BILLING LOGIC ---
763
-
764
- async function checkMinimumCredits(userId, type = 'basic') {
765
- if (!supabase) return;
766
- const { data } = await supabase.from('users').select('credits').eq('id', userId).single();
767
- if (!data) return;
768
-
769
- const credits = data.credits?.[type] || 0;
770
- const req = type === 'diamond' ? MIN_DIAMOND_REQUIRED : MIN_BASIC_REQUIRED;
771
-
772
- if (credits < req) {
773
- throw new Error(`Insufficient ${type} credits. Required: ${req}, Available: ${credits}`);
774
  }
 
775
  }
776
 
777
- async function deductUserCredits(userId, amount, type = 'basic') {
778
- const deduction = Math.floor(parseInt(amount, 10));
779
- if (!supabase || !deduction || deduction <= 0) return;
780
 
781
- try {
782
- const { data } = await supabase.from('users').select('credits').eq('id', userId).single();
783
- if (!data) return;
784
-
785
- const currentCredits = data.credits || { basic: 0, diamond: 0 };
786
- const currentVal = currentCredits[type] || 0;
787
- const newVal = Math.max(0, currentVal - deduction);
788
-
789
- const updatedCredits = { ...currentCredits, [type]: newVal };
790
-
791
- await supabase.from('users').update({ credits: updatedCredits }).eq('id', userId);
792
- console.log(`[Credits] User ${userId} | -${deduction} ${type} | Remaining: ${newVal}`);
793
- } catch (err) {
794
- console.error("Deduction failed", err);
795
- }
796
- }
797
 
798
- // --- CORE LOGIC: Background Initialization ---
 
799
 
800
- async function runBackgroundInitialization(projectId, userId, description) {
801
- if (StateManager.isLocked(projectId)) {
802
- console.log(`[Init Guard] Project ${projectId} is already initializing. Skipping.`);
803
- return;
804
- }
805
-
806
- StateManager.lock(projectId);
807
- console.log(`[Background] Starting initialization for ${projectId}`);
808
-
809
- let diamondUsage = 0;
810
- let basicUsage = 0;
811
 
812
  try {
813
- await StateManager.getProject(projectId);
814
-
815
- await StateManager.updateProject(projectId, {
816
- status: "working",
817
- gdd: "",
818
- failureCount: 0
819
- });
820
-
821
- const pmHistory = [];
822
-
823
- // --- Step 1: Generate GDD (PM) ---
824
- let stopStatus = startStatusLoop(projectId, 'pm');
825
- const gddPrompt = `Create a comprehensive Game Design Document (GDD) for: ${description}`;
826
- const gddResult = await AIEngine.callPM(pmHistory, gddPrompt);
827
- stopStatus();
828
-
829
- if (!gddResult?.text) throw new Error("PM failed to generate GDD");
830
-
831
- const gddText = gddResult.text;
832
- diamondUsage += (gddResult.usage?.totalTokenCount || 0);
833
-
834
- await StateManager.addHistory(projectId, 'pm', 'user', gddPrompt);
835
- await StateManager.addHistory(projectId, 'pm', 'model', gddText);
836
-
837
- pmHistory.push({ role: 'user', parts: [{ text: gddPrompt }] });
838
- pmHistory.push({ role: 'model', parts: [{ text: gddText }] });
839
-
840
- // --- Step 2: Generate Tasks (PM) ---
841
- stopStatus = startStatusLoop(projectId, 'pm');
842
- const taskPrompt = "Based on the GDD, generate the first technical milestone.\nOutput format:\nTASK_NAME: <Name>\nWORKER_PROMPT: <Specific, isolated instructions for the worker>";
843
- const taskResult = await AIEngine.callPM(pmHistory, taskPrompt);
844
- stopStatus();
845
-
846
- if (!taskResult?.text) throw new Error("PM failed to generate Task");
847
-
848
- const taskText = taskResult.text;
849
- diamondUsage += (taskResult.usage?.totalTokenCount || 0);
850
-
851
- await StateManager.addHistory(projectId, 'pm', 'user', taskPrompt);
852
- await StateManager.addHistory(projectId, 'pm', 'model', taskText);
853
-
854
- // --- Step 3: Initialize Worker ---
855
- const initialWorkerInstruction = extractWorkerPrompt(taskText) || `Initialize structure for: ${description}`;
856
- const initialWorkerPrompt = `CONTEXT: New Project. \nINSTRUCTION: ${initialWorkerInstruction}`;
857
-
858
- stopStatus = startStatusLoop(projectId, 'worker');
859
-
860
- let workerTextAccumulated = "";
861
-
862
- const workerResult = await AIEngine.callWorkerStream(
863
- [],
864
- initialWorkerPrompt,
865
- (thought) => {
866
- stopStatus();
867
- StateManager.setStatus(projectId, "Worker: Thinking...");
868
- StateManager.appendSnapshotOnly(projectId, thought);
869
- },
870
- (chunk) => {
871
- stopStatus();
872
- StateManager.setStatus(projectId, "Worker: Coding...");
873
- workerTextAccumulated += chunk;
874
- StateManager.appendStream(projectId, chunk);
875
- }
876
- );
877
- stopStatus();
878
-
879
- basicUsage += (workerResult.usage?.totalTokenCount || 0);
880
-
881
- if (!workerTextAccumulated && !workerResult.text) throw new Error("Worker failed to initialize");
882
- const finalWorkerText = workerResult.text || workerTextAccumulated;
883
-
884
- await StateManager.addHistory(projectId, 'worker', 'user', initialWorkerPrompt);
885
- await StateManager.addHistory(projectId, 'worker', 'model', finalWorkerText);
886
-
887
- await StateManager.updateProject(projectId, {
888
- gdd: gddText,
889
- status: "idle"
890
  });
891
 
892
- await processAndQueueResponse(projectId, finalWorkerText, userId);
893
- console.log(`[Background] Init complete for ${projectId}`);
894
-
895
- } catch (err) {
896
- console.error(`[Background] Init Error for ${projectId}:`, err.message);
897
- await StateManager.updateProject(projectId, { status: "error" });
898
- } finally {
899
- StateManager.unlock(projectId);
900
- if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
901
- if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
902
- }
903
- }
904
-
905
- // --- CORE LOGIC: Async Feedback Loop ---
906
-
907
- async function runAsyncFeedback(projectId, userId, fullInput, images = []) {
908
- let diamondUsage = 0;
909
- let basicUsage = 0;
910
-
911
- try {
912
- console.log(`[${projectId}] Feedback received. Persisting state immediately.`);
913
-
914
- // 1. Instant Persistence: Saves status and message BEFORE processing
915
- // This ensures if user reloads, the state is preserved
916
- await Promise.all([
917
- StateManager.updateProject(projectId, { status: "working" }),
918
- StateManager.addHistory(projectId, 'worker', 'user', fullInput)
919
- ]);
920
-
921
- const project = await StateManager.getProject(projectId);
922
-
923
- StateManager.clearSnapshot(projectId);
924
- let stopStatus = startStatusLoop(projectId, 'worker');
925
-
926
- let firstTurnResponse = "";
927
- let thoughtText = "";
928
-
929
- // Clone history to prevent mutation issues
930
- // Note: project.workerHistory includes the message we just saved above
931
- const currentWorkerHistory = [...(project.workerHistory || [])];
932
-
933
- // 2. First Turn (Worker Execution)
934
- const workerResult = await AIEngine.callWorkerStream(
935
- currentWorkerHistory,
936
- fullInput,
937
- (thought) => {
938
- stopStatus();
939
- thoughtText += thought;
940
- StateManager.setStatus(projectId, "Worker: Thinking...");
941
- StateManager.appendSnapshotOnly(projectId, thought);
942
- },
943
- (chunk) => {
944
- stopStatus();
945
- firstTurnResponse += chunk;
946
- StateManager.setStatus(projectId, "Worker: Coding...");
947
- StateManager.appendStream(projectId, chunk);
948
- },
949
- images
950
- );
951
- stopStatus();
952
- basicUsage += (workerResult.usage?.totalTokenCount || 0);
953
- firstTurnResponse = workerResult.text || firstTurnResponse;
954
-
955
- // 3. Analyze Output (Delegation/Questions)
956
- const routeTask = extractRouteToPM(firstTurnResponse);
957
- const pmQuestion = extractPMQuestion(firstTurnResponse);
958
- let finalResponseToSave = firstTurnResponse;
959
-
960
- if (routeTask || pmQuestion) {
961
- // Save preliminary response to DB so it doesn't get lost
962
- await StateManager.addHistory(projectId, 'worker', 'model', firstTurnResponse);
963
- currentWorkerHistory.push({ role: 'model', parts: [{ text: firstTurnResponse }] });
964
-
965
- let pmPrompt = "";
966
- let pmContextPrefix = "";
967
-
968
- if (routeTask) {
969
- console.log(`[${projectId}] Routing task to PM...`);
970
- pmPrompt = `[ROUTED TASK]: ${routeTask}\nWrite the backend/logic. Then use WORKER_PROMPT: <task> to delegate.`;
971
- pmContextPrefix = "[PM DELEGATION]:";
972
- } else {
973
- console.log(`[${projectId}] Worker consulting PM...`);
974
- pmPrompt = `[WORKER QUESTION]: ${pmQuestion}\nProvide a technical answer or code snippet.`;
975
- pmContextPrefix = "[PM ANSWER]:";
976
- }
977
-
978
- // 4. Run PM (Diamond Logic)
979
- StateManager.clearSnapshot(projectId);
980
- stopStatus = startStatusLoop(projectId, 'pm');
981
-
982
- let pmResponseText = "";
983
-
984
- const pmResult = await AIEngine.callPMStream(
985
- project.pmHistory,
986
- pmPrompt,
987
- (thought) => {
988
- stopStatus();
989
- StateManager.setStatus(projectId, "Manager: Thinking...");
990
- StateManager.appendSnapshotOnly(projectId, thought);
991
- },
992
- (chunk) => {
993
- stopStatus();
994
- StateManager.setStatus(projectId, "Manager: Architecture...");
995
- pmResponseText += chunk;
996
- StateManager.appendSnapshotOnly(projectId, chunk);
997
- }
998
- );
999
- stopStatus();
1000
- diamondUsage += (pmResult.usage?.totalTokenCount || 0);
1001
- pmResponseText = pmResult.text || pmResponseText;
1002
-
1003
- await StateManager.addHistory(projectId, 'pm', 'user', pmPrompt);
1004
- await StateManager.addHistory(projectId, 'pm', 'model', pmResponseText);
1005
 
1006
- // --- EXECUTE PM CODE ---
1007
- // If the PM wrote code (e.g. ServerScriptService logic), execute it immediately
1008
- await processAndQueueResponse(projectId, pmResponseText, userId);
1009
 
1010
- // 5. Run Worker Continuation
1011
- const nextInstruction = extractWorkerPrompt(pmResponseText) || pmResponseText;
1012
- const workerContinuationPrompt = `${pmContextPrefix} ${nextInstruction}\n\nBased on this, continue the task and output the code.`;
 
 
 
1013
 
1014
- // Add system prompt to history
1015
- await StateManager.addHistory(projectId, 'worker', 'user', workerContinuationPrompt);
1016
- currentWorkerHistory.push({ role: 'user', parts: [{ text: workerContinuationPrompt }] });
1017
-
1018
- StateManager.clearSnapshot(projectId);
1019
- stopStatus = startStatusLoop(projectId, 'worker');
1020
-
1021
- let secondTurnResponse = "";
1022
-
1023
- const secondWorkerResult = await AIEngine.callWorkerStream(
1024
- currentWorkerHistory,
1025
- workerContinuationPrompt,
1026
- (thought) => {
1027
- stopStatus();
1028
- StateManager.setStatus(projectId, "Worker: Applying Fix...");
1029
- StateManager.appendSnapshotOnly(projectId, thought);
1030
- },
1031
- (chunk) => {
1032
- stopStatus();
1033
- StateManager.setStatus(projectId, "Worker: Finalizing...");
1034
- secondTurnResponse += chunk;
1035
- StateManager.appendStream(projectId, chunk);
1036
- }
1037
- );
1038
- stopStatus();
1039
-
1040
- // Deduct usage for second turn
1041
- basicUsage += (secondWorkerResult.usage?.totalTokenCount || 0);
1042
- secondTurnResponse = secondWorkerResult.text || secondTurnResponse;
1043
-
1044
- await StateManager.addHistory(projectId, 'worker', 'model', secondTurnResponse);
1045
- finalResponseToSave = secondTurnResponse;
1046
-
1047
- } else {
1048
- // No delegation, just save the first response
1049
- await StateManager.addHistory(projectId, 'worker', 'model', firstTurnResponse);
1050
- }
1051
-
1052
- StateManager.setStatus(projectId, "Idle");
1053
-
1054
- // 6. Final Execution & Cleanup
1055
- await processAndQueueResponse(projectId, finalResponseToSave, userId);
1056
-
1057
- // Ensure status is idle (this also updates timestamps)
1058
- await StateManager.updateProject(projectId, { status: "idle" });
1059
-
1060
- } catch (err) {
1061
- console.error("Async Feedback Error:", err);
1062
- StateManager.setStatus(projectId, "Error: " + err.message);
1063
- await StateManager.updateProject(projectId, { status: "error" });
1064
- } finally {
1065
- // Billing
1066
- if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
1067
- if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
1068
- }
1069
- }
1070
-
1071
- async function processAndQueueResponse(projectId, rawResponse, userId) {
1072
- if (!rawResponse) return;
1073
-
1074
- // Image Generation
1075
- const imgPrompt = extractImagePrompt(rawResponse);
1076
- if (imgPrompt) {
1077
- try {
1078
- const imgResult = await AIEngine.generateImage(imgPrompt);
1079
- if (imgResult?.image) {
1080
- if (userId) await deductUserCredits(userId, 1000, 'basic'); // Flat fee
1081
- await StateManager.queueCommand(projectId, { type: "CREATE_ASSET", payload: imgResult.image });
1082
- }
1083
- } catch (e) {
1084
- console.error("Image gen failed", e);
1085
- }
1086
- }
1087
-
1088
- // Code Execution
1089
- // Regex looks for lua/luau code blocks to execute
1090
- const codeMatch = rawResponse.match(/```(?:lua|luau)?([\s\S]*?)```/i);
1091
-
1092
- if (codeMatch) {
1093
- await StateManager.queueCommand(projectId, { type: "EXECUTE", payload: codeMatch[1].trim() });
1094
- } else {
1095
- // If there's no code block but response is non-empty, maybe check if it's pure code?
1096
- // For safety, we usually only execute if wrapped in blocks.
1097
- // But if you want to support raw text execution:
1098
- // await StateManager.queueCommand(projectId, { type: "EXECUTE", payload: rawResponse });
1099
- }
1100
- }
1101
-
1102
- // --- EXPRESS ROUTES ---
1103
-
1104
- app.post('/onboarding/analyze', validateRequest, async (req, res) => {
1105
- const { description, userId } = req.body;
1106
- if (!description) return res.status(400).json({ error: "Description required" });
1107
-
1108
- try {
1109
- // await checkMinimumCredits(userId, 'basic');
1110
- try {
1111
- await checkMinimumCredits(userId, 'basic');
1112
- } catch (creditErr) {
1113
- return res.json({ success: false, insufficient: true, error: creditErr.message });
1114
- }
1115
-
1116
- const result = await AIEngine.generateEntryQuestions(description);
1117
-
1118
- if (result.usage?.totalTokenCount > 0) {
1119
- await deductUserCredits(userId, result.usage.totalTokenCount, 'basic');
1120
- }
1121
-
1122
- if (result.status === "REJECTED") {
1123
- return res.json({ rejected: true, reason: result.reason || "Idea violates TOS." });
1124
- }
1125
-
1126
- res.json({ questions: result.questions });
1127
- } catch (err) {
1128
- res.status(500).json({ error: err.message });
1129
- }
1130
- });
1131
-
1132
- app.post('/onboarding/create', validateRequest, async (req, res) => {
1133
- const { userId, description, answers } = req.body;
1134
-
1135
- try {
1136
-
1137
- try {
1138
- await checkMinimumCredits(userId, 'basic');
1139
- await checkMinimumCredits(userId, 'diamond');
1140
- } catch (creditErr) {
1141
- return res.json({ success: false, insufficient: true, error: creditErr.message });
1142
- }
1143
-
1144
- const randomHex = (n = 6) => crypto.randomBytes(Math.ceil(n/2)).toString("hex").slice(0, n);
1145
- const projectId = `proj_${Date.now()}_${randomHex(7)}`;
1146
-
1147
- const gradingResult = await AIEngine.gradeProject(description, answers);
1148
-
1149
- if (gradingResult.usage?.totalTokenCount) {
1150
- await deductUserCredits(userId, gradingResult.usage.totalTokenCount, 'basic');
1151
- }
1152
-
1153
- const isFailure = gradingResult.feasibility < 30 || gradingResult.rating === 'F';
1154
-
1155
- if (!isFailure) {
1156
- const { error: insertError } = await supabase.from('projects').insert({
1157
- id: projectId,
1158
- user_id: userId,
1159
- info: {
1160
- title: gradingResult.title || "Untitled",
1161
- stats: gradingResult,
1162
- description,
1163
- answers,
1164
- status: "initializing"
1165
- }
1166
- });
1167
-
1168
- if (insertError) {
1169
- console.error("Failed to insert project:", insertError.message);
1170
- return res.status(500).json({ error: "Database save failed" });
1171
- }
1172
-
1173
- runBackgroundInitialization(projectId, userId, description);
1174
- }
1175
-
1176
- res.json({
1177
- status: 200,
1178
- success: !isFailure,
1179
- projectId,
1180
- stats: gradingResult,
1181
- title: gradingResult.title || "Untitled"
1182
  });
1183
 
1184
- } catch (err) {
1185
- console.error("Create Error:", err);
1186
- res.status(500).json({ error: err.message });
1187
- }
1188
- });
1189
-
1190
- app.post('/project/feedback', async (req, res) => {
1191
- const { userId, projectId, prompt, hierarchyContext, scriptContext, logContext, images } = req.body;
1192
-
1193
- try {
1194
- const project = await StateManager.getProject(projectId);
1195
- if (!project || project.userId !== userId) return res.status(403).json({ error: "Auth Error" });
1196
-
1197
- // --- FIXED CREDIT CHECK ---
1198
- // Catch credit errors here to return a soft 200 OK so frontend banner shows
1199
- try {
1200
- await checkMinimumCredits(userId, 'basic');
1201
- } catch (creditErr) {
1202
- return res.json({ success: false, insufficient: true, error: creditErr.message });
1203
- }
1204
-
1205
- const context = formatContext({ hierarchyContext, scriptContext, logContext });
1206
- const fullInput = `USER: ${prompt || "Automatic Feedback"}${context}`;
1207
-
1208
- // Trigger Async
1209
- runAsyncFeedback(projectId, userId, fullInput, images || []);
1210
-
1211
- res.json({ success: true, message: "Processing started" });
1212
-
1213
- } catch (err) {
1214
- console.error("Feedback Error:", err);
1215
- await StateManager.updateProject(projectId, { status: "error" });
1216
- res.status(500).json({ error: "Feedback process failed" });
1217
- }
1218
- });
1219
-
1220
-
1221
- app.post('/project/ping', async (req, res) => {
1222
- const { projectId, userId, isFrontend } = req.body;
1223
- if (!projectId || !userId) return res.status(400).json({ error: "Missing IDs" });
1224
-
1225
- const project = await StateManager.getProject(projectId);
1226
- if (!project || project.userId !== userId) return res.json({ action: "IDLE" });
1227
-
1228
- // 1. BASE RESPONSE (Visuals)
1229
- // Both Frontend and Plugin need this to show "Thinking..." or text generation
1230
- const response = {
1231
- action: "IDLE",
1232
- status: StateManager.getStatus(projectId),
1233
- snapshot: StateManager.getSnapshot(projectId)
1234
- };
1235
-
1236
- // 2. EXECUTOR LOGIC (Plugin Only)
1237
- // Only pop commands if this is NOT the passive frontend viewer
1238
- if (!isFrontend) {
1239
- const command = await StateManager.popCommand(projectId);
1240
-
1241
- // We don't need 'streamBuffers' anymore since we use 'snapshot' for visuals.
1242
- // But if you have legacy stream logic, we can leave it or remove it.
1243
- // For this hybrid model, snapshot is king.
1244
-
1245
- if (command) {
1246
- response.action = command.type;
1247
- response.target = command.payload;
1248
- response.code = command.type === 'EXECUTE' ? command.payload : null;
1249
- }
1250
- }
1251
-
1252
- res.json(response);
1253
- });
1254
-
1255
-
1256
- app.post('/human/override', validateRequest, async (req, res) => {
1257
- const { projectId, instruction, userId } = req.body;
1258
- try {
1259
- await checkMinimumCredits(userId, 'basic');
1260
- const project = await StateManager.getProject(projectId);
1261
-
1262
- res.json({ success: true });
1263
-
1264
- const overrideMsg = `[SYSTEM OVERRIDE]: ${instruction}`;
1265
-
1266
- let overrideText = "";
1267
- let stopStatus = startStatusLoop(projectId, 'worker');
1268
-
1269
- const workerResult = await AIEngine.callWorkerStream(
1270
- project.workerHistory,
1271
- overrideMsg,
1272
- (thought) => {
1273
- stopStatus();
1274
- StateManager.setStatus(projectId, "Worker: Thinking...");
1275
- StateManager.appendSnapshotOnly(projectId, thought);
1276
- },
1277
- (chunk) => {
1278
- stopStatus();
1279
- StateManager.setStatus(projectId, "Worker: Coding...");
1280
- overrideText += chunk;
1281
- StateManager.appendStream(projectId, chunk);
1282
- }
1283
- );
1284
- stopStatus();
1285
-
1286
- await StateManager.addHistory(projectId, 'worker', 'user', overrideMsg);
1287
- await StateManager.addHistory(projectId, 'worker', 'model', workerResult.text || overrideText);
1288
- await processAndQueueResponse(projectId, workerResult.text || overrideText, userId);
1289
-
1290
- const usage = workerResult.usage?.totalTokenCount || 0;
1291
- if (usage > 0) await deductUserCredits(userId, usage, 'basic');
1292
-
1293
- } catch (err) {
1294
- console.error("Override failed", err);
1295
  }
1296
  });
1297
 
1298
- app.get('/admin/cleanup', async (req, res) => {
1299
- try {
1300
- const removed = StateManager.cleanupMemory ? StateManager.cleanupMemory() : 0;
1301
- res.json({ success: true, removedCount: removed });
1302
- } catch (err) {
1303
- res.status(500).json({ error: "Cleanup failed" });
1304
- }
1305
- });
1306
-
1307
- app.listen(PORT, () => {
1308
- console.log(`AI Backend Running on ${PORT} (Supabase + Billing Edition)`);
1309
- });
1310
- */
 
1
  import express from 'express';
 
2
  import cors from 'cors';
3
+ import { createClient } from '@supabase/supabase-js';
4
+ import Anthropic from '@anthropic-ai/sdk';
 
 
 
5
 
6
+ // --- CONFIG ---
7
+ const PORT = 7860;
8
+ const ANTHROPIC_KEY = process.env.ANTHROPIC_API_KEY;
9
+ const SUPABASE_URL = process.env.SUPABASE_URL;
10
+ const SUPABASE_KEY = process.env.SUPABASE_SERVICE_KEY;
11
+ const FRONT_URL = process.env.FRONT_URL; // e.g. https://my-front-server.hf.space
12
 
13
+ if (!ANTHROPIC_KEY) { console.error("❌ Missing Anthropic Key"); process.exit(1); }
 
 
14
 
15
+ // --- STATE ---
16
  const app = express();
17
+ const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
18
+ const anthropic = new Anthropic({ apiKey: ANTHROPIC_KEY });
19
 
20
+ app.use(express.json({ limit: '10mb' })); // Allow large contexts (files)
21
  app.use(cors());
 
22
 
23
+ // --- PROMPTS (Embedded) ---
24
+ const SYSTEM_PROMPT = `
25
+ You are NOW THRUST, an elite AI Project Director. Your goal is to maintain momentum.
26
+ You analyze user context (Git diffs, VS Code state) and give direct, actionable orders.
27
+
28
+ OUTPUT FORMAT:
29
+ 1. Speak directly to the user. Be concise.
30
+ 2. If you need to create a new "Thrust" (Morning Briefing/Task List), use this XML:
31
+ <thrust_create>
32
+ {
33
+ "title": "Title of Briefing",
34
+ "markdown_content": "# Briefing\nDetails...",
35
+ "tasks": ["Task 1", "Task 2"]
36
+ }
37
+ </thrust_create>
38
+
39
+ 3. If you need to log an achievement to the Timeline, use this XML:
40
+ <timeline_log>
41
+ {
42
+ "title": "Achievement Name",
43
+ "description": "What was done",
44
+ "type": "feature",
45
+ "tag": "BACKEND"
46
+ }
47
+ </timeline_log>
48
+
49
+ 4. If you need to send a toast notification to the desktop, use:
50
+ <notification>Message here</notification>
51
+
52
+ Do not output JSON markdown blocks. Output raw XML tags mixed with natural language.
53
+ `;
54
+
55
+ // --- LOGIC ---
56
+
57
+ function extractCommands(text) {
58
+ const commands = [];
59
+
60
+ // Extract Thrusts
61
+ const thrustRegex = /<thrust_create>([\s\S]*?)<\/thrust_create>/g;
62
+ let match;
63
+ while ((match = thrustRegex.exec(text)) !== null) {
64
+ try { commands.push({ type: 'create_thrust', payload: JSON.parse(match[1]) }); } catch (e) { console.error("JSON Parse Error", e); }
65
+ }
66
+
67
+ // Extract Timeline Logs
68
+ const timeRegex = /<timeline_log>([\s\S]*?)<\/timeline_log>/g;
69
+ while ((match = timeRegex.exec(text)) !== null) {
70
+ try { commands.push({ type: 'log_timeline', payload: JSON.parse(match[1]) }); } catch (e) { console.error("JSON Parse Error", e); }
71
+ }
72
+
73
+ // Extract Notifications
74
+ const notifyRegex = /<notification>([\s\S]*?)<\/notification>/g;
75
+ while ((match = notifyRegex.exec(text)) !== null) {
76
+ commands.push({ type: 'notification', payload: match[1] });
77
+ }
78
+
79
+ return commands;
80
+ }
81
+
82
+ async function executeCommands(userId, projectId, commands) {
83
+ let shouldReload = false;
84
+
85
+ for (const cmd of commands) {
86
+ if (cmd.type === 'create_thrust') {
87
+ // 1. Create Thrust
88
+ const { data: thrust, error } = await supabase.from('thrusts').insert({
89
+ lead_id: projectId, // Assuming projectId maps to Lead ID
90
+ title: cmd.payload.title,
91
+ markdown_content: cmd.payload.markdown_content,
92
+ status: 'active'
93
+ }).select().single();
94
+
95
+ if (!error && thrust) {
96
+ // 2. Create Tasks
97
+ const tasks = cmd.payload.tasks.map(t => ({ thrust_id: thrust.id, title: t }));
98
+ await supabase.from('thrust_tasks').insert(tasks);
99
+ shouldReload = true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  }
102
 
103
+ if (cmd.type === 'log_timeline') {
104
+ await supabase.from('timeline_events').insert({
105
+ lead_id: projectId,
106
+ title: cmd.payload.title,
107
+ description: cmd.payload.description,
108
+ type: cmd.payload.type.toLowerCase(), // feature, ui, etc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  });
110
+ shouldReload = true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
+ if (cmd.type === 'notification' && FRONT_URL) {
114
+ // Fire and forget notification to front server
115
+ fetch(`${FRONT_URL}/internal/notify`, {
116
+ method: 'POST',
117
+ headers: { 'Content-Type': 'application/json' },
118
+ body: JSON.stringify({ user_id: userId, type: 'toast', message: cmd.payload })
119
+ }).catch(e => console.error("Failed to notify front", e));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  }
122
+ return shouldReload;
123
  }
124
 
125
+ // --- ENDPOINTS ---
 
 
126
 
127
+ app.get('/', (req, res) => res.send('Core Brain Active'));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
+ app.post('/process', async (req, res) => {
130
+ const { userId, projectId, prompt, context } = req.body;
131
 
132
+ console.log(`🧠 Processing for User: ${userId}`);
 
 
 
 
 
 
 
 
 
 
133
 
134
  try {
135
+ // 1. Call Anthropic
136
+ const msg = await anthropic.messages.create({
137
+ model: "claude-3-5-sonnet-20240620",
138
+ max_tokens: 2048,
139
+ system: SYSTEM_PROMPT,
140
+ messages: [
141
+ { role: "user", content: `CONTEXT:\n${JSON.stringify(context)}\n\nUSER PROMPT: ${prompt}` }
142
+ ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  });
144
 
145
+ const rawText = msg.content[0].text;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
 
147
+ // 2. Extract & Execute
148
+ const commands = extractCommands(rawText);
149
+ const shouldReload = await executeCommands(userId, projectId, commands);
150
 
151
+ // 3. Clean Text for Display (Remove XML tags)
152
+ const cleanText = rawText
153
+ .replace(/<thrust_create>[\s\S]*?<\/thrust_create>/g, '')
154
+ .replace(/<timeline_log>[\s\S]*?<\/timeline_log>/g, '')
155
+ .replace(/<notification>[\s\S]*?<\/notification>/g, '')
156
+ .trim();
157
 
158
+ res.json({
159
+ text: cleanText,
160
+ should_reload: shouldReload
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  });
162
 
163
+ } catch (e) {
164
+ console.error("Core Error:", e);
165
+ res.status(500).json({ error: e.message });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  }
167
  });
168
 
169
+ app.listen(PORT, () => console.log(`🧠 Core Server running on port ${PORT}`));