Pepguy commited on
Commit
ef9a0f0
·
verified ·
1 Parent(s): 43cc6fc

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +119 -335
app.js CHANGED
@@ -1,6 +1,4 @@
1
- // server.js
2
- // ESM Node (package.json should contain "type": "module")
3
- // Install: npm install express body-parser cors @google/genai
4
  // Run: GEMINI_API_KEY=your_key node server.js
5
 
6
  import express from "express";
@@ -20,23 +18,19 @@ const app = express();
20
  app.use(cors());
21
  app.use(bodyParser.json({ limit: "2mb" }));
22
 
23
- /**
24
- * In-memory store (prototype). For production, replace with persistent storage.
25
- */
26
  const projects = new Map();
27
 
28
  const simpleId = (prefix = "") => prefix + Math.random().toString(36).slice(2, 9);
29
 
30
  /**
31
- * Action vocabulary — keep plugin and server in sync.
32
- * We also accept some common synonyms from the model and map them to canonical allowed actions.
 
33
  */
34
  const VALID_ACTIONS = [
35
  "create_part",
36
  "create_model",
37
- "create_meshpart",
38
- "create_complex_mesh",
39
- "create_union",
40
  "create_script",
41
  "attach_script",
42
  "edit_script",
@@ -45,432 +39,222 @@ const VALID_ACTIONS = [
45
  "rotate_to",
46
  "set_parent",
47
  "delete",
48
- "apply_texture",
49
  "weld",
50
- "unweld",
51
- "create_rig",
52
- "apply_animation",
53
- "feedback_request",
54
- "list_workspace",
55
  "finish"
56
  ];
57
 
58
  /**
59
- * Map of alternate action names the model might output -> canonical action.
60
- * Add more mappings here if you see similar synonyms from the model.
61
  */
62
  const ACTION_SYNONYMS = {
63
- // common variants
64
- "add_bone": "create_rig",
65
- "add_bones": "create_rig",
66
- "create_bone": "create_rig",
67
- "attach_script_to": "attach_script",
68
- "modify_script": "edit_script",
69
  "move": "move_to",
 
70
  "rotate": "rotate_to",
71
- "set_parent_to": "set_parent",
72
- // geometry synonyms
73
- "union": "create_union",
74
- "make_union": "create_union",
75
- "mesh_part": "create_meshpart",
76
  "add_script": "create_script",
77
  "insert_script": "create_script",
 
78
  "append_script": "attach_script",
79
- "add_animation": "apply_animation"
 
 
 
 
 
 
 
 
 
 
80
  };
81
 
82
- /**
83
- * Validate/prepare model output tasks (ensure allowed actions, add ids).
84
- * Also maps synonyms to canonical actions (records the originalAction in the returned object).
85
- */
86
  function validateAndPrepareTasks(arr) {
87
  if (!Array.isArray(arr)) throw new Error("Model output must be a JSON array of tasks.");
88
  return arr.map((t) => {
89
  if (!t || typeof t !== "object") throw new Error("Each task must be an object.");
90
  if (!t.action) throw new Error("Each task must have an action field.");
91
 
92
- // Normalize action: trim and coerce to string
93
  let actionRaw = String(t.action).trim();
94
  const actionLower = actionRaw.toLowerCase();
95
 
96
- // Map synonyms (case-insensitive)
97
  let canonicalAction = null;
98
- // First try exact match against VALID_ACTIONS (case-sensitive)
99
  if (VALID_ACTIONS.includes(actionRaw)) canonicalAction = actionRaw;
100
- // Then try case-insensitive match
 
101
  if (!canonicalAction) {
102
  canonicalAction = VALID_ACTIONS.find(a => a.toLowerCase() === actionLower) || null;
103
  }
104
- // Then try synonyms map
 
105
  if (!canonicalAction && ACTION_SYNONYMS[actionLower]) {
106
  canonicalAction = ACTION_SYNONYMS[actionLower];
107
  }
108
 
109
  if (!canonicalAction) {
110
- // Not recognized include the offending action in the error to help debugging
111
- throw new Error(`Invalid action from model: ${actionRaw}`);
 
112
  }
113
 
114
  const id = t.id || simpleId("cmd_");
115
  const params = t.params && typeof t.params === "object" ? t.params : {};
116
- // Attach originalAction for traceability in history/logs
117
- return { id, action: canonicalAction, originalAction: actionRaw, params, target_ref: t.target_ref || null };
 
 
 
 
 
 
118
  });
119
  }
120
 
121
- /**
122
- * generateTasks(project)
123
- * Uses Gemini to convert description + project state into an ordered JSON array of tasks.
124
- */
125
  async function generateTasks(project) {
126
- // Summarize project state (concise)
127
- const historySample = (project.history || []).slice(-20).map(h => ({
128
- commandId: h.commandId,
129
  status: h.status,
130
- message: h.message ? (typeof h.message === "string" ? (h.message.length > 120 ? h.message.slice(0, 117) + "..." : h.message) : null) : null,
131
- originalAction: h.originalAction || null,
132
- target_ref: h.target_ref || null,
133
- ts: h.ts
134
  }));
135
- const refsKeys = Object.keys(project._refs || {}).slice(0, 200);
136
- const queuedCount = project.commandQueue ? project.commandQueue.length : 0;
137
- const tasksCount = project.tasks ? project.tasks.length : 0;
138
  const status = project.status || "unknown";
139
 
140
- const stateSummary = {
141
- projectId: project.id,
142
- status,
143
- queuedCount,
144
- tasksCount,
145
- recentHistory: historySample,
146
- refsSummary: refsKeys
147
- };
148
-
149
  const systemPrompt = `
150
- You are a precise assistant that converts a high-level Roblox project description into an ordered JSON array of low-level editor tasks. Be spatially aware: reason about coordinates (x,y,z), sizes, rotations (euler degrees as {x,y,z}), orientations, and parent/child relationships. For every object you create, assign a stable "target_ref" string (short alphanumeric) that later tasks can reference.
151
-
152
- Return ONLY valid JSON — a single array, no commentary, no extra text.
153
-
154
- Allowed actions (use exactly these): ${VALID_ACTIONS.join(", ")}.
155
-
156
- Task object shape:
157
- {
158
- "action": "<action>",
159
- "params": { ... }, // numbers, strings, booleans, objects with numeric x,y,z, etc.
160
- "target_ref": "<optional-ref>" // for create_* actions include this for reference
161
- }
162
-
163
- Important rules:
164
- - Spatial parameters use explicit numeric fields: position: { x, y, z }, size: { x, y, z }, rotation: { x, y, z } (degrees).
165
- - When referencing an earlier-created object, use params.target_ref exactly as the ref string you returned earlier.
166
- - Do not reference Roblox-specific paths (like Workspace.Model.Part).
167
- - Use only the allowed actions listed above (exactly those names).
168
- - If you need the plugin to evaluate the scene and return properties (for feedback), use actions "feedback_request" or "list_workspace".
169
- - End the sequence with an action "finish" when the project is complete.
170
-
171
- EXISTING PROJECT STATE (read-only; use to decide next tasks):
172
- ${JSON.stringify(stateSummary, null, 2)}
173
-
174
- Now produce tasks for this project:
175
- ${JSON.stringify({ id: project.id, description: project.description })}
176
  `;
177
 
178
  const resp = await ai.models.generateContent({
179
- model: "gemini-2.5-flash",
180
  contents: systemPrompt,
181
- temperature: 0.0,
182
- maxOutputTokens: 2000
183
  });
184
 
185
- const text = resp?.text ?? (resp?.data?.[0]?.text ?? "");
 
186
  let arr;
187
  try {
188
- arr = JSON.parse(text);
 
 
189
  } catch (err) {
190
- // Try to extract substring
191
- const start = text.indexOf("[");
192
- const end = text.lastIndexOf("]");
193
- if (start >= 0 && end > start) {
194
- arr = JSON.parse(text.slice(start, end + 1));
195
- } else {
196
- throw new Error("Could not parse JSON from model output: " + text.slice(0, 1000));
197
- }
198
  }
199
 
200
  return validateAndPrepareTasks(arr);
201
  }
202
 
203
- /**
204
- * Create project endpoint
205
- * Body: { description: string, projectId?: string }
206
- */
207
  app.post("/projects", async (req, res) => {
208
  try {
209
- const description = req.body.description;
210
- if (!description || typeof description !== "string") {
211
- return res.status(400).json({ error: "description string required in body" });
212
- }
213
  const projectId = req.body.projectId || simpleId("proj_");
214
- if (projects.has(projectId)) return res.status(400).json({ error: "projectId already exists" });
215
-
216
  const project = {
217
  id: projectId,
218
- description,
219
  status: "queued",
220
  tasks: [],
221
  commandQueue: [],
222
  pendingResults: new Map(),
223
- running: false,
224
- history: [], // { commandId, status, message, extra, target_ref, originalAction, ts }
225
- _refs: {} // mapping: target_ref -> { commandId, extra, ts }
226
  };
227
-
228
  projects.set(projectId, project);
229
-
230
- // Start background orchestrator (non-blocking)
231
- runProject(project).catch((err) => {
232
- project.status = "error";
233
- console.error("runProject error:", err);
234
- });
235
-
236
  return res.status(202).json({ projectId, status: "accepted" });
237
  } catch (err) {
238
- console.error("projects POST error", err);
239
  return res.status(500).json({ error: err.message });
240
  }
241
  });
242
 
243
- /**
244
- * Tweak / prompt endpoint — includes project context so the model knows what has been done.
245
- * Body: { prompt: string }
246
- */
247
  app.post("/projects/:projectId/prompt", async (req, res) => {
248
  const project = projects.get(req.params.projectId);
249
  if (!project) return res.status(404).json({ error: "project not found" });
 
250
  const prompt = req.body.prompt;
251
- if (!prompt || typeof prompt !== "string") return res.status(400).json({ error: "prompt required" });
252
-
 
 
253
  try {
254
- const contextSummary = {
255
- id: project.id,
256
- status: project.status,
257
- queuedCount: project.commandQueue.length,
258
- tasksCount: project.tasks.length,
259
- recentHistory: (project.history || []).slice(-50).map(h => ({ commandId: h.commandId, status: h.status, target_ref: h.target_ref, originalAction: h.originalAction })),
260
- refsKeys: Object.keys(project._refs || {}).slice(0, 200)
261
- };
262
-
263
- const pseudoProject = {
264
- id: project.id,
265
- description: `${project.description}\n\n---\nContextSummary: ${JSON.stringify(contextSummary)}\n\nUserTweak: ${prompt}`
266
- };
267
-
268
- const newTasks = await generateTasks(pseudoProject);
269
-
270
- // Append tasks to the project's task list
271
  project.tasks.push(...newTasks);
272
-
273
- return res.json({ appended: newTasks.length, tasks: newTasks });
274
- } catch (err) {
275
- console.error("prompt endpoint error:", err);
276
- return res.status(500).json({ error: err.message });
277
  }
278
  });
279
 
280
- /**
281
- * Direct model endpoint for manual calls (returns Gemini text)
282
- * Body: { prompt: string }
283
- */
284
- app.post("/model", async (req, res) => {
285
- try {
286
- const prompt = req.body.prompt;
287
- if (!prompt || typeof prompt !== "string") return res.status(400).json({ error: "prompt required" });
288
-
289
- const resp = await ai.models.generateContent({
290
- model: "gemini-2.5-flash",
291
- contents: prompt,
292
- temperature: 0.0,
293
- maxOutputTokens: 1500
294
- });
295
-
296
- return res.json({ text: resp?.text ?? "" });
297
- } catch (err) {
298
- console.error("/model error", err);
299
- return res.status(500).json({ error: err.message });
300
- }
301
- });
302
-
303
- /**
304
- * Plugin polling: GET next command or 204 if none
305
- */
306
  app.get("/projects/:projectId/next", (req, res) => {
307
  const project = projects.get(req.params.projectId);
308
  if (!project) return res.status(404).json({ error: "project not found" });
309
-
310
  if (project.commandQueue.length > 0) {
311
- const cmd = project.commandQueue.shift();
312
- return res.json(cmd);
313
  }
314
  return res.status(204).end();
315
  });
316
 
317
- /**
318
- * Plugin posts execution result. This records history and resolves waiters.
319
- * Body: { commandId, status: "ok"|"error", message?: string, extra?: any, target_ref?: string }
320
- */
321
  app.post("/projects/:projectId/result", (req, res) => {
322
  const project = projects.get(req.params.projectId);
323
  if (!project) return res.status(404).json({ error: "project not found" });
324
-
325
- const { commandId, status, message, extra, target_ref } = req.body;
326
- if (!commandId || !status) return res.status(400).json({ error: "commandId and status required" });
327
-
328
- // Record in history
329
- const entry = {
330
- commandId,
331
- status,
332
- message: message || null,
333
- extra: extra || null,
334
- target_ref: target_ref || null,
335
- originalAction: null, // we'll try to attach if we can find the task in history/tasks
336
- ts: Date.now()
337
- };
338
-
339
- // Attempt to find the originalAction from project's tasks or prior mapping
340
- // (task may have been dispatched earlier; search tasks and history)
341
- const possibleTask = (project.tasks || []).find(t => t.id === commandId) || (project.commandQueue || []).find(t => t.id === commandId);
342
- if (possibleTask && possibleTask.originalAction) entry.originalAction = possibleTask.originalAction;
343
-
344
- project.history = project.history || [];
345
- project.history.push(entry);
346
-
347
- // Map target_ref if present and OK
348
- if (target_ref && status === "ok") {
349
- project._refs = project._refs || {};
350
- project._refs[target_ref] = { commandId, extra, ts: Date.now() };
351
- }
352
-
353
- // Resolve pending waiter if present
354
- const waiter = project.pendingResults.get(commandId);
355
- if (waiter) {
356
  project.pendingResults.delete(commandId);
357
- waiter.resolve({ status, message, extra, target_ref });
358
- return res.json({ ok: true });
359
  }
360
-
361
- // Otherwise accept and return
362
- return res.json({ ok: true, note: "recorded result (no pending waiter)" });
363
- });
364
-
365
- /**
366
- * Status endpoint
367
- */
368
- app.get("/status/:projectId", (req, res) => {
369
- const project = projects.get(req.params.projectId);
370
- if (!project) return res.status(404).json({ error: "project not found" });
371
- return res.json({
372
- id: project.id,
373
- status: project.status,
374
- queuedCommands: project.commandQueue.length,
375
- totalTasks: project.tasks.length,
376
- refs: project._refs || {},
377
- historyLength: (project.history || []).length
378
- });
379
  });
380
 
381
- /**
382
- * Simple list of projects (debug)
383
- */
384
- app.get("/projects", (req, res) => {
385
- const list = Array.from(projects.values()).map((p) => ({
386
- id: p.id,
387
- description: p.description,
388
- status: p.status,
389
- queuedCommands: p.commandQueue.length,
390
- tasks: p.tasks.length
391
- }));
392
- return res.json(list);
393
- });
394
-
395
- /**
396
- * Orchestrator per project
397
- * - If no tasks: generate from description
398
- * - Iterate tasks, push to commandQueue, wait for plugin result, then continue
399
- */
400
  async function runProject(project) {
401
- if (project.running) return;
402
- project.running = true;
403
- project.status = "running";
404
-
405
- try {
406
- // If no tasks, generate initial plan
407
- if (!project.tasks || project.tasks.length === 0) {
408
- project.tasks = await generateTasks(project);
409
- }
410
-
411
- // Iterate tasks
412
- for (const task of project.tasks) {
413
- // push to queue
414
- project.commandQueue.push(task);
415
-
416
- // record that we dispatched this task (for easier traceability)
417
- project.history = project.history || [];
418
- project.history.push({
419
- commandId: task.id,
420
- status: "dispatched",
421
- message: null,
422
- extra: null,
423
- target_ref: task.target_ref || null,
424
- originalAction: task.originalAction || null,
425
- ts: Date.now()
426
- });
427
-
428
- // Wait for plugin result for this task
429
- const result = await new Promise((resolve, reject) => {
430
- const timeoutMs = 3 * 60 * 1000; // 3 minutes
431
- let timedOut = false;
432
- const timeout = setTimeout(() => {
433
- timedOut = true;
434
- project.pendingResults.delete(task.id);
435
- reject(new Error("Command timed out: " + task.id));
436
- }, timeoutMs);
437
-
438
- project.pendingResults.set(task.id, {
439
- resolve: (payload) => {
440
- if (timedOut) return;
441
- clearTimeout(timeout);
442
- resolve(payload);
443
- },
444
- reject: (err) => {
445
- if (timedOut) return;
446
- clearTimeout(timeout);
447
- reject(err);
448
- }
449
- });
450
- });
451
-
452
- // If failure, mark and stop
453
- if (result.status !== "ok") {
454
- project.status = "failed";
455
- throw new Error(`Command ${task.id} failed: ${result.message ?? "no message"}`);
456
- }
457
-
458
- // If task had target_ref & plugin returned target_ref, map it
459
- if (task.target_ref && result.target_ref) {
460
- project._refs = project._refs || {};
461
- project._refs[task.target_ref] = result.target_ref;
462
- }
463
- }
464
-
465
- project.status = "complete";
466
- } catch (err) {
467
- console.error("Project run error for", project.id, err);
468
- if (project.status !== "failed") project.status = "error";
469
- } finally {
470
- project.running = false;
471
  }
 
 
 
472
  }
473
 
474
- app.listen(PORT, () => {
475
- console.log(`BloxBuddy backend listening on port ${PORT}`);
476
- });
 
1
+ // server.js - ALIGNED WITH PLUGIN
 
 
2
  // Run: GEMINI_API_KEY=your_key node server.js
3
 
4
  import express from "express";
 
18
  app.use(cors());
19
  app.use(bodyParser.json({ limit: "2mb" }));
20
 
 
 
 
21
  const projects = new Map();
22
 
23
  const simpleId = (prefix = "") => prefix + Math.random().toString(36).slice(2, 9);
24
 
25
  /**
26
+ * STRICT VOCABULARY
27
+ * Only actions that exist in the Plugin's ACTION_HANDLERS are allowed here.
28
+ * Removed: create_union, create_rig, apply_animation, unweld, apply_texture
29
  */
30
  const VALID_ACTIONS = [
31
  "create_part",
32
  "create_model",
33
+ "create_meshpart",
 
 
34
  "create_script",
35
  "attach_script",
36
  "edit_script",
 
39
  "rotate_to",
40
  "set_parent",
41
  "delete",
 
42
  "weld",
43
+ "list_workspace", // Plugin calls this workspaceSnapshot
 
 
 
 
44
  "finish"
45
  ];
46
 
47
  /**
48
+ * SYNONYM MAPPING
49
+ * Maps what the AI *wants* to say to what the Plugin *can* do.
50
  */
51
  const ACTION_SYNONYMS = {
52
+ // Map complex mesh requests to simple meshparts
53
+ "create_complex_mesh": "create_meshpart",
54
+ "load_mesh": "create_meshpart",
55
+
56
+ // Map common movement terms
 
57
  "move": "move_to",
58
+ "position": "move_to",
59
  "rotate": "rotate_to",
60
+ "set_rotation": "rotate_to",
61
+
62
+ // Scripting synonyms
 
 
63
  "add_script": "create_script",
64
  "insert_script": "create_script",
65
+ "modify_script": "edit_script",
66
  "append_script": "attach_script",
67
+
68
+ // Hierarchy
69
+ "group": "create_model",
70
+ "parent": "set_parent",
71
+ "set_parent_to": "set_parent",
72
+ "join": "weld", -- "Join" usually implies welding in this simple context
73
+
74
+ // Unsupported fallbacks (Safety)
75
+ "union": "create_model", -- Fallback: Group them instead of Unioning (prevents crash)
76
+ "create_union": "create_model",
77
+ "create_rig": "create_model" -- Fallback: Just make a model container
78
  };
79
 
 
 
 
 
80
  function validateAndPrepareTasks(arr) {
81
  if (!Array.isArray(arr)) throw new Error("Model output must be a JSON array of tasks.");
82
  return arr.map((t) => {
83
  if (!t || typeof t !== "object") throw new Error("Each task must be an object.");
84
  if (!t.action) throw new Error("Each task must have an action field.");
85
 
 
86
  let actionRaw = String(t.action).trim();
87
  const actionLower = actionRaw.toLowerCase();
88
 
 
89
  let canonicalAction = null;
 
90
  if (VALID_ACTIONS.includes(actionRaw)) canonicalAction = actionRaw;
91
+
92
+ // Case-insensitive check
93
  if (!canonicalAction) {
94
  canonicalAction = VALID_ACTIONS.find(a => a.toLowerCase() === actionLower) || null;
95
  }
96
+
97
+ // Synonym check
98
  if (!canonicalAction && ACTION_SYNONYMS[actionLower]) {
99
  canonicalAction = ACTION_SYNONYMS[actionLower];
100
  }
101
 
102
  if (!canonicalAction) {
103
+ // If AI hallucinates an action, we force it to a safe fallback or error
104
+ // Instead of crashing, let's log it and skip or error.
105
+ throw new Error(`Invalid action '${actionRaw}'. Supported: ${VALID_ACTIONS.join(", ")}`);
106
  }
107
 
108
  const id = t.id || simpleId("cmd_");
109
  const params = t.params && typeof t.params === "object" ? t.params : {};
110
+
111
+ return {
112
+ id,
113
+ action: canonicalAction,
114
+ originalAction: actionRaw,
115
+ params,
116
+ target_ref: t.target_ref || null
117
+ };
118
  });
119
  }
120
 
 
 
 
 
121
  async function generateTasks(project) {
122
+ // Summarize project state
123
+ const historySample = (project.history || []).slice(-15).map(h => ({
 
124
  status: h.status,
125
+ action: h.originalAction || "unknown",
126
+ msg: h.message ? h.message.slice(0, 50) : "ok"
 
 
127
  }));
128
+
 
 
129
  const status = project.status || "unknown";
130
 
131
+ // We explicitly tell the AI about the Registry logic in the prompt
 
 
 
 
 
 
 
 
132
  const systemPrompt = `
133
+ You are a Roblox Studio Assistant. Convert user descriptions into JSON tasks.
134
+
135
+ AVAILABLE ACTIONS:
136
+ ${VALID_ACTIONS.join(", ")}
137
+
138
+ RULES:
139
+ 1. **References:** When creating an object, ALWAYS assign a unique "target_ref" (e.g., "ref_wall_1").
140
+ 2. **Chaining:** When moving/modifying that object later, use "target_ref": "ref_wall_1" in the params.
141
+ 3. **Hierarchy:** To put a Part inside a Model:
142
+ - Task 1: create_model (target_ref: "ref_grp")
143
+ - Task 2: create_part (target_ref: "ref_part")
144
+ - Task 3: set_parent (target_ref: "ref_part", params: { parent_ref: "ref_grp" })
145
+ 4. **Math:** Use numbers for positions. Roblox Y is Up.
146
+ 5. **Output:** Return ONLY a JSON Array. No markdown.
147
+
148
+ Current Context:
149
+ ${JSON.stringify({ history: historySample, status }, null, 2)}
150
+
151
+ User Request:
152
+ ${JSON.stringify({ description: project.description })}
 
 
 
 
 
 
153
  `;
154
 
155
  const resp = await ai.models.generateContent({
156
+ model: "gemini-2.0-flash", // Updated to latest flash if available, or use 1.5-flash
157
  contents: systemPrompt,
158
+ generationConfig: { responseMimeType: "application/json" } // Force JSON mode
 
159
  });
160
 
161
+ const text = resp?.text ? resp.text() : (resp?.response?.text() ?? "");
162
+
163
  let arr;
164
  try {
165
+ // Clean markdown code blocks if present
166
+ const cleanText = text.replace(/```json/g, "").replace(/```/g, "").trim();
167
+ arr = JSON.parse(cleanText);
168
  } catch (err) {
169
+ console.error("Model Output:", text);
170
+ throw new Error("Failed to parse JSON from AI.");
 
 
 
 
 
 
171
  }
172
 
173
  return validateAndPrepareTasks(arr);
174
  }
175
 
176
+ // --- EXPRESS ROUTES (Same as before, simplified) ---
177
+
 
 
178
  app.post("/projects", async (req, res) => {
179
  try {
 
 
 
 
180
  const projectId = req.body.projectId || simpleId("proj_");
 
 
181
  const project = {
182
  id: projectId,
183
+ description: req.body.description || "New Project",
184
  status: "queued",
185
  tasks: [],
186
  commandQueue: [],
187
  pendingResults: new Map(),
188
+ history: [],
189
+ _refs: {}
 
190
  };
 
191
  projects.set(projectId, project);
192
+ runProject(project); // Background run
 
 
 
 
 
 
193
  return res.status(202).json({ projectId, status: "accepted" });
194
  } catch (err) {
 
195
  return res.status(500).json({ error: err.message });
196
  }
197
  });
198
 
 
 
 
 
199
  app.post("/projects/:projectId/prompt", async (req, res) => {
200
  const project = projects.get(req.params.projectId);
201
  if (!project) return res.status(404).json({ error: "project not found" });
202
+
203
  const prompt = req.body.prompt;
204
+ // Append new prompt to description acts as "memory" for this simple prototype
205
+ project.description += `\nUser Update: ${prompt}`;
206
+
207
+ // Re-run generation logic for the new prompt
208
  try {
209
+ const newTasks = await generateTasks({ ...project, description: prompt }); // Only send the *new* prompt as the task trigger
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  project.tasks.push(...newTasks);
211
+ project.commandQueue.push(...newTasks); // Push directly to queue if already running
212
+ return res.json({ tasks: newTasks });
213
+ } catch(e) {
214
+ return res.status(500).json({error: e.message});
 
215
  }
216
  });
217
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  app.get("/projects/:projectId/next", (req, res) => {
219
  const project = projects.get(req.params.projectId);
220
  if (!project) return res.status(404).json({ error: "project not found" });
 
221
  if (project.commandQueue.length > 0) {
222
+ return res.json(project.commandQueue.shift());
 
223
  }
224
  return res.status(204).end();
225
  });
226
 
 
 
 
 
227
  app.post("/projects/:projectId/result", (req, res) => {
228
  const project = projects.get(req.params.projectId);
229
  if (!project) return res.status(404).json({ error: "project not found" });
230
+
231
+ const { commandId, status, message, target_ref } = req.body;
232
+
233
+ // Log history
234
+ project.history.push({ commandId, status, message, timestamp: Date.now() });
235
+
236
+ // Resolve waiting promise (if runProject is waiting)
237
+ if (project.pendingResults.has(commandId)) {
238
+ project.pendingResults.get(commandId).resolve({ status, message, target_ref });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  project.pendingResults.delete(commandId);
 
 
240
  }
241
+
242
+ return res.json({ ok: true });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  });
244
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  async function runProject(project) {
246
+ if (project.tasks.length === 0 && project.description) {
247
+ try {
248
+ project.tasks = await generateTasks(project);
249
+ project.commandQueue.push(...project.tasks);
250
+ } catch (e) {
251
+ console.error("Gen Error:", e);
252
+ return;
253
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  }
255
+
256
+ // In this simplified version, we just let the /next endpoint drain the queue.
257
+ // We don't strictly "block" here, making it more robust for connection drops.
258
  }
259
 
260
+ app.listen(PORT, () => console.log(`BloxBuddy Brain running on ${PORT}`));