everydaytok commited on
Commit
b788227
·
verified ·
1 Parent(s): 01e24e5

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +49 -56
app.js CHANGED
@@ -7,17 +7,17 @@ import { createClient } from '@supabase/supabase-js';
7
  // ==========================================
8
  // 1. CONFIGURATION & SETUP
9
  // ==========================================
10
- const PORT = 7860;
11
  const SUPABASE_URL = process.env.SUPABASE_URL;
12
  const SUPABASE_KEY = process.env.SUPABASE_SERVICE_KEY;
13
- const REMOTE_SERVER_URL = process.env.REMOTE_AI_URL || "http://localhost:11434"; // Defaulting to local Ollama/LMStudio if env missing
14
  const FRONT_URL = process.env.FRONT_URL;
15
- const CRON_REGISTRY_URL = process.env.CRON_REGISTRY_URL || "http://localhost:7861"; // URL of the Cron Server
16
  const CRON_SECRET = process.env.CRON_SECRET || "default_secret";
17
 
18
  // --- MODEL CONFIGURATION ---
19
- const SMART_MODEL_ID = "claude";
20
- const FAST_MODEL_ID = "gpt-5-mini";
21
 
22
  if (!SUPABASE_URL || !SUPABASE_KEY) {
23
  console.error("FATAL: Missing Supabase URL or Service Key.");
@@ -34,7 +34,6 @@ app.use(cors());
34
  // 2. PROMPT MANAGEMENT
35
  // ==========================================
36
  let prompts = {};
37
- // Load synchronously on boot
38
  try {
39
  const promptsPath = path.resolve('./prompts.json');
40
  if (fs.existsSync(promptsPath)) {
@@ -72,7 +71,7 @@ const StateManager = {
72
  if (error) return [];
73
  const fullHistory = (chunks || []).reverse().flatMap(c => c.payload || []);
74
 
75
- // Initialize Cache with default frozen state (false) if not known, will update from DB later
76
  activeProjects.set(projectId, { history: fullHistory, lastActive: Date.now(), isFrozen: false });
77
  return fullHistory;
78
  },
@@ -122,7 +121,6 @@ const StateManager = {
122
  } catch (e) { console.error("History Sync Error", e); }
123
  },
124
 
125
- // Check frozen state from DB
126
  isFrozen: async (projectId) => {
127
  if (activeProjects.has(projectId)) return activeProjects.get(projectId).isFrozen;
128
  const { data } = await supabase.from('leads').select('is_frozen').eq('id', projectId).single();
@@ -140,7 +138,6 @@ const StateManager = {
140
  // ==========================================
141
 
142
  const callAI = async (history, input, contextData, systemPrompt, projectContext, modelId) => {
143
- // Construct Context
144
  let contextStr = "";
145
  try { contextStr = JSON.stringify(contextData, null, 2); } catch {}
146
 
@@ -166,7 +163,7 @@ const callAI = async (history, input, contextData, systemPrompt, projectContext,
166
  };
167
 
168
  // ==========================================
169
- // 5. LOGIC & COMMANDS
170
  // ==========================================
171
 
172
  function extractCommands(text) {
@@ -186,25 +183,26 @@ function extractCommands(text) {
186
  parse(/<notification>([\s\S]*?)<\/notification>/g, 'notification', false);
187
  parse(/<update_requirements>([\s\S]*?)<\/update_requirements>/g, 'update_requirements', false);
188
  parse(/<schedule_briefing>([\s\S]*?)<\/schedule_briefing>/g, 'schedule_briefing');
189
- parse(/<freeze_project>([\s\S]*?)<\/freeze_project>/g, 'freeze_project', false); // expecting "true"
190
- parse(/<thrust_complete>([\s\S]*?)<\/thrust_complete>/g, 'thrust_complete', false); // expecting "true"
191
 
192
  return commands;
193
  }
194
 
195
- // Register Cron Helper
196
  async function registerMorningCron(projectId, offset) {
197
- // 1. Calculate time until next 6 AM
198
  const now = new Date();
199
  const target = new Date(now);
200
 
201
- // Adjust logic: Find 6 AM in user's timezone relative to UTC
202
- // Simple approach: Calculate ms delay
203
- target.setUTCHours(6 - offset, 0, 0, 0);
 
 
 
204
 
205
  if (target <= now) {
206
- // If 6AM passed today, schedule for tomorrow
207
- target.setDate(target.getDate() + 1);
208
  }
209
 
210
  const delayMs = target.getTime() - now.getTime();
@@ -212,14 +210,6 @@ async function registerMorningCron(projectId, offset) {
212
 
213
  console.log(`⏰ Scheduling Briefing for ${projectId}. Next run in ${(delayMs/1000/60).toFixed(1)} mins.`);
214
 
215
- // 2. Call Cron Registry (One time set for specific start, then interval)
216
- // Note: The simple registry provided uses setInterval. To sync to 6AM exactly,
217
- // we register a job that fires in 'delayMs', which then executes logic and re-registers itself
218
- // OR simply use 24h interval starting NOW if close enough.
219
-
220
- // Better strategy for this specific "Morning" requirement with a simple setInterval registry:
221
- // We register a job with delayMs. The payload tells the Core "Setup Daily Loop".
222
-
223
  await fetch(`${CRON_REGISTRY_URL}/register`, {
224
  method: 'POST',
225
  headers: { 'Content-Type': 'application/json' },
@@ -227,7 +217,8 @@ async function registerMorningCron(projectId, offset) {
227
  secret: CRON_SECRET,
228
  jobId: `briefing_${projectId}`,
229
  intervalMs: intervalMs,
230
- webhookUrl: `http://localhost:${PORT}/automated-briefing`, // Self-call
 
231
  payload: { projectId, timezoneOffset: offset }
232
  })
233
  }).catch(e => console.error("Cron Reg Error", e));
@@ -282,7 +273,7 @@ async function executeCommands(userId, projectId, commands) {
282
  }
283
 
284
  if (cmd.type === 'thrust_complete') {
285
- flags.thrustComplete = true; // Signals Core to trigger Smart Model
286
  }
287
 
288
  if (cmd.type === 'notification' && FRONT_URL) {
@@ -306,14 +297,12 @@ async function executeCommands(userId, projectId, commands) {
306
  app.post('/init-project', async (req, res) => {
307
  const { userId, name, description, localPath } = req.body;
308
 
309
- // Create DB Entry
310
  const { data: lead } = await supabase.from('leads').insert({
311
  user_id: userId, name, description, local_path: localPath, status: 'active', requirements_doc: "Init..."
312
  }).select().single();
313
 
314
  res.json({ success: true, leadId: lead.id });
315
 
316
- // Background Process
317
  setImmediate(async () => {
318
  const initInput = `PROJECT: ${name}\nDESC: ${description}\nTask: Init PRD, First Thrust, Schedule Morning Briefing.`;
319
  const aiResult = await callAI([], initInput, {}, prompts.init_system_prompt, "", SMART_MODEL_ID);
@@ -333,13 +322,12 @@ app.post('/process', async (req, res) => {
333
  // 1. Unfreeze if user interacts
334
  if (task_type === 'chat') await StateManager.setFrozen(projectId, false);
335
 
336
- // 2. Select Model
337
- // Logic: Log ingestion uses Fast model. If Fast model says "Thrust Complete", we switch to Smart.
338
  let selectedModel = (task_type === 'log_ingestion') ? FAST_MODEL_ID : SMART_MODEL_ID;
339
  let sysPrompt = (task_type === 'log_ingestion') ? prompts.log_analyst_prompt : prompts.director_system_prompt;
340
 
341
  try {
342
- // Fetch Context (PRD + Current Thrust)
343
  const { data: lead } = await supabase.from('leads').select('requirements_doc').eq('id', projectId).single();
344
  const { data: activeThrust } = await supabase.from('thrusts')
345
  .select('title, tasks:thrust_tasks(title, status)')
@@ -348,46 +336,52 @@ app.post('/process', async (req, res) => {
348
  .order('created_at', { ascending: false })
349
  .limit(1)
350
  .single();
 
 
 
 
 
 
 
351
 
352
  const projectContext = `
353
- [PRD]: ${lead?.requirements_doc?.substring(0, 2000)}...
354
- [CURRENT THRUST]: ${activeThrust ? JSON.stringify(activeThrust) : "None"}
 
 
 
 
 
355
  `;
356
 
357
  const history = await StateManager.getHistory(projectId);
358
  if (task_type === 'chat') await StateManager.addHistory(projectId, 'user', prompt);
359
 
360
- // 3. AI Execution
361
  let aiResult = await callAI(history, prompt, context, sysPrompt, projectContext, selectedModel);
362
-
363
- // 4. Command Execution
364
  let cmds = extractCommands(aiResult.text);
365
  let flags = await executeCommands(userId, projectId, cmds);
366
 
367
  // 5. AUTO-ESCALATION (Fast -> Smart)
368
- // If Log Analyst says thrust is done, we immediately call the Smart Director to generate the next one.
369
  if (flags.thrustComplete && task_type === 'log_ingestion') {
370
  console.log(`🚀 Thrust Complete detected by Analyst. Escalating to Director...`);
371
 
372
  const escalationPrompt = "The previous thrust is complete based on logs. Generate the next Thrust immediately to keep momentum.";
373
- // Call Smart Model (No history of logs, just the context that it's done)
374
  const smartResult = await callAI(history, escalationPrompt, context, prompts.director_system_prompt, projectContext, SMART_MODEL_ID);
375
 
376
- // Append Smart response
377
  aiResult.text += `\n\n[DIRECTOR INTERVENTION]:\n${smartResult.text}`;
378
 
379
  const smartCmds = extractCommands(smartResult.text);
380
  await executeCommands(userId, projectId, smartCmds);
381
 
382
- // Save history
383
  await StateManager.addHistory(projectId, 'model', aiResult.text);
384
  } else {
385
  await StateManager.addHistory(projectId, 'model', aiResult.text);
386
  }
387
 
388
- // Clean output
389
  const cleanText = aiResult.text.replace(/<[^>]+>[\s\S]*?<\/[^>]+>/g, '').trim();
390
-
391
  res.json({ text: cleanText, should_reload: flags.shouldReload });
392
 
393
  } catch (e) {
@@ -402,10 +396,8 @@ app.post('/automated-briefing', async (req, res) => {
402
  console.log(`☀️ Morning Briefing Triggered: ${projectId}`);
403
 
404
  try {
405
- // 1. Check Freeze State
406
  const isFrozen = await StateManager.isFrozen(projectId);
407
 
408
- // Check when the last thrust was made. If made today, skip.
409
  const { data: lastThrust } = await supabase.from('thrusts')
410
  .select('created_at')
411
  .eq('lead_id', projectId)
@@ -416,7 +408,7 @@ app.post('/automated-briefing', async (req, res) => {
416
  const lastThrustDate = lastThrust ? new Date(lastThrust.created_at).getDate() : 0;
417
  const todayDate = new Date().getDate();
418
 
419
- // LOGIC: If frozen, DO NOT generate. If thrust made today, DO NOT generate.
420
  if (isFrozen) {
421
  console.log(`Skipping briefing: Project Frozen.`);
422
  return res.json({ status: "skipped_frozen" });
@@ -426,24 +418,25 @@ app.post('/automated-briefing', async (req, res) => {
426
  return res.json({ status: "skipped_exists" });
427
  }
428
 
429
- // 2. Generate Briefing
430
  const prompt = "It is 6 AM. Generate the Morning Briefing (New Thrust). If the project has been idle, use <freeze_project>true</freeze_project> after this.";
431
 
432
- // Use Process endpoint logic internally
433
- // We create a fake "request" to process
434
- // Ideally refactor logic out, but for now calling AI directly:
435
  const { data: lead } = await supabase.from('leads').select('*').eq('id', projectId).single();
 
 
 
 
436
  const history = await StateManager.getHistory(projectId);
437
 
438
- const aiResult = await callAI(history, prompt, {}, prompts.director_system_prompt, `[PRD]: ${lead.requirements_doc}`, SMART_MODEL_ID);
439
 
440
- // 3. Save & Execute
441
  await StateManager.addHistory(projectId, 'model', aiResult.text);
442
  const cmds = extractCommands(aiResult.text);
443
  await executeCommands(lead.user_id, projectId, cmds);
444
 
445
- // 4. Force Freeze (Morning briefings lock the project until user returns)
446
- // This ensures we don't pile up briefings on an abandoned project.
447
  await StateManager.setFrozen(projectId, true);
448
 
449
  res.json({ success: true });
 
7
  // ==========================================
8
  // 1. CONFIGURATION & SETUP
9
  // ==========================================
10
+ const PORT = 7862;
11
  const SUPABASE_URL = process.env.SUPABASE_URL;
12
  const SUPABASE_KEY = process.env.SUPABASE_SERVICE_KEY;
13
+ const REMOTE_SERVER_URL = process.env.REMOTE_AI_URL || "http://localhost:11434"; // Defaulting to local if env missing
14
  const FRONT_URL = process.env.FRONT_URL;
15
+ const CRON_REGISTRY_URL = process.env.CRON_REGISTRY_URL || "http://localhost:7861";
16
  const CRON_SECRET = process.env.CRON_SECRET || "default_secret";
17
 
18
  // --- MODEL CONFIGURATION ---
19
+ const SMART_MODEL_ID = "gpt-4-turbo";
20
+ const FAST_MODEL_ID = "gpt-3.5-turbo";
21
 
22
  if (!SUPABASE_URL || !SUPABASE_KEY) {
23
  console.error("FATAL: Missing Supabase URL or Service Key.");
 
34
  // 2. PROMPT MANAGEMENT
35
  // ==========================================
36
  let prompts = {};
 
37
  try {
38
  const promptsPath = path.resolve('./prompts.json');
39
  if (fs.existsSync(promptsPath)) {
 
71
  if (error) return [];
72
  const fullHistory = (chunks || []).reverse().flatMap(c => c.payload || []);
73
 
74
+ // Initialize Cache with default frozen state (false)
75
  activeProjects.set(projectId, { history: fullHistory, lastActive: Date.now(), isFrozen: false });
76
  return fullHistory;
77
  },
 
121
  } catch (e) { console.error("History Sync Error", e); }
122
  },
123
 
 
124
  isFrozen: async (projectId) => {
125
  if (activeProjects.has(projectId)) return activeProjects.get(projectId).isFrozen;
126
  const { data } = await supabase.from('leads').select('is_frozen').eq('id', projectId).single();
 
138
  // ==========================================
139
 
140
  const callAI = async (history, input, contextData, systemPrompt, projectContext, modelId) => {
 
141
  let contextStr = "";
142
  try { contextStr = JSON.stringify(contextData, null, 2); } catch {}
143
 
 
163
  };
164
 
165
  // ==========================================
166
+ // 5. COMMAND & CRON LOGIC
167
  // ==========================================
168
 
169
  function extractCommands(text) {
 
183
  parse(/<notification>([\s\S]*?)<\/notification>/g, 'notification', false);
184
  parse(/<update_requirements>([\s\S]*?)<\/update_requirements>/g, 'update_requirements', false);
185
  parse(/<schedule_briefing>([\s\S]*?)<\/schedule_briefing>/g, 'schedule_briefing');
186
+ parse(/<freeze_project>([\s\S]*?)<\/freeze_project>/g, 'freeze_project', false);
187
+ parse(/<thrust_complete>([\s\S]*?)<\/thrust_complete>/g, 'thrust_complete', false);
188
 
189
  return commands;
190
  }
191
 
192
+ // Logic: Calculate delay until 6AM user time, register Cron with that initial delay
193
  async function registerMorningCron(projectId, offset) {
 
194
  const now = new Date();
195
  const target = new Date(now);
196
 
197
+ // Logic: 6 AM relative to user offset.
198
+ // E.g., Offset -5 (EST). UTC is 11 AM when EST is 6 AM.
199
+ // Target UTC hour = 6 - offset.
200
+ const targetUtcHour = 6 - offset;
201
+
202
+ target.setUTCHours(targetUtcHour, 0, 0, 0);
203
 
204
  if (target <= now) {
205
+ target.setDate(target.getDate() + 1); // Move to tomorrow
 
206
  }
207
 
208
  const delayMs = target.getTime() - now.getTime();
 
210
 
211
  console.log(`⏰ Scheduling Briefing for ${projectId}. Next run in ${(delayMs/1000/60).toFixed(1)} mins.`);
212
 
 
 
 
 
 
 
 
 
213
  await fetch(`${CRON_REGISTRY_URL}/register`, {
214
  method: 'POST',
215
  headers: { 'Content-Type': 'application/json' },
 
217
  secret: CRON_SECRET,
218
  jobId: `briefing_${projectId}`,
219
  intervalMs: intervalMs,
220
+ initialDelay: delayMs, // Send delay to Cron Server
221
+ webhookUrl: `http://localhost:${PORT}/automated-briefing`,
222
  payload: { projectId, timezoneOffset: offset }
223
  })
224
  }).catch(e => console.error("Cron Reg Error", e));
 
273
  }
274
 
275
  if (cmd.type === 'thrust_complete') {
276
+ flags.thrustComplete = true; // Signal for escalation
277
  }
278
 
279
  if (cmd.type === 'notification' && FRONT_URL) {
 
297
  app.post('/init-project', async (req, res) => {
298
  const { userId, name, description, localPath } = req.body;
299
 
 
300
  const { data: lead } = await supabase.from('leads').insert({
301
  user_id: userId, name, description, local_path: localPath, status: 'active', requirements_doc: "Init..."
302
  }).select().single();
303
 
304
  res.json({ success: true, leadId: lead.id });
305
 
 
306
  setImmediate(async () => {
307
  const initInput = `PROJECT: ${name}\nDESC: ${description}\nTask: Init PRD, First Thrust, Schedule Morning Briefing.`;
308
  const aiResult = await callAI([], initInput, {}, prompts.init_system_prompt, "", SMART_MODEL_ID);
 
322
  // 1. Unfreeze if user interacts
323
  if (task_type === 'chat') await StateManager.setFrozen(projectId, false);
324
 
325
+ // 2. Select Model (Log Ingestion = Fast, High Level = Smart)
 
326
  let selectedModel = (task_type === 'log_ingestion') ? FAST_MODEL_ID : SMART_MODEL_ID;
327
  let sysPrompt = (task_type === 'log_ingestion') ? prompts.log_analyst_prompt : prompts.director_system_prompt;
328
 
329
  try {
330
+ // 3. Build Context (God View: PRD + Timeline + Active Thrust)
331
  const { data: lead } = await supabase.from('leads').select('requirements_doc').eq('id', projectId).single();
332
  const { data: activeThrust } = await supabase.from('thrusts')
333
  .select('title, tasks:thrust_tasks(title, status)')
 
336
  .order('created_at', { ascending: false })
337
  .limit(1)
338
  .single();
339
+
340
+ // FETCH TIMELINE (Restored functionality)
341
+ const { data: timeline } = await supabase.from('timeline_events')
342
+ .select('title, type, description, created_at')
343
+ .eq('lead_id', projectId)
344
+ .order('created_at', { ascending: false })
345
+ .limit(10); // Look at last 10 events for context
346
 
347
  const projectContext = `
348
+ [PRD]: ${lead?.requirements_doc?.substring(0, 3000)}...
349
+
350
+ [CURRENT THRUST]:
351
+ ${activeThrust ? JSON.stringify(activeThrust, null, 2) : "None"}
352
+
353
+ [RECENT TIMELINE]:
354
+ ${JSON.stringify(timeline || [], null, 2)}
355
  `;
356
 
357
  const history = await StateManager.getHistory(projectId);
358
  if (task_type === 'chat') await StateManager.addHistory(projectId, 'user', prompt);
359
 
360
+ // 4. AI Execution
361
  let aiResult = await callAI(history, prompt, context, sysPrompt, projectContext, selectedModel);
 
 
362
  let cmds = extractCommands(aiResult.text);
363
  let flags = await executeCommands(userId, projectId, cmds);
364
 
365
  // 5. AUTO-ESCALATION (Fast -> Smart)
366
+ // Logic: Analyst determines thrust is done via logs -> Triggers Director to set new Aim.
367
  if (flags.thrustComplete && task_type === 'log_ingestion') {
368
  console.log(`🚀 Thrust Complete detected by Analyst. Escalating to Director...`);
369
 
370
  const escalationPrompt = "The previous thrust is complete based on logs. Generate the next Thrust immediately to keep momentum.";
371
+ // Call Smart Model
372
  const smartResult = await callAI(history, escalationPrompt, context, prompts.director_system_prompt, projectContext, SMART_MODEL_ID);
373
 
 
374
  aiResult.text += `\n\n[DIRECTOR INTERVENTION]:\n${smartResult.text}`;
375
 
376
  const smartCmds = extractCommands(smartResult.text);
377
  await executeCommands(userId, projectId, smartCmds);
378
 
 
379
  await StateManager.addHistory(projectId, 'model', aiResult.text);
380
  } else {
381
  await StateManager.addHistory(projectId, 'model', aiResult.text);
382
  }
383
 
 
384
  const cleanText = aiResult.text.replace(/<[^>]+>[\s\S]*?<\/[^>]+>/g, '').trim();
 
385
  res.json({ text: cleanText, should_reload: flags.shouldReload });
386
 
387
  } catch (e) {
 
396
  console.log(`☀️ Morning Briefing Triggered: ${projectId}`);
397
 
398
  try {
 
399
  const isFrozen = await StateManager.isFrozen(projectId);
400
 
 
401
  const { data: lastThrust } = await supabase.from('thrusts')
402
  .select('created_at')
403
  .eq('lead_id', projectId)
 
408
  const lastThrustDate = lastThrust ? new Date(lastThrust.created_at).getDate() : 0;
409
  const todayDate = new Date().getDate();
410
 
411
+ // Prevent generating if project is dead (frozen) or if generated recently
412
  if (isFrozen) {
413
  console.log(`Skipping briefing: Project Frozen.`);
414
  return res.json({ status: "skipped_frozen" });
 
418
  return res.json({ status: "skipped_exists" });
419
  }
420
 
421
+ // Generate Briefing
422
  const prompt = "It is 6 AM. Generate the Morning Briefing (New Thrust). If the project has been idle, use <freeze_project>true</freeze_project> after this.";
423
 
424
+ // Manually build context context for the automated call
 
 
425
  const { data: lead } = await supabase.from('leads').select('*').eq('id', projectId).single();
426
+ const { data: timeline } = await supabase.from('timeline_events')
427
+ .select('*').eq('lead_id', projectId).order('created_at', { ascending: false }).limit(5);
428
+
429
+ const projectContext = `[PRD]: ${lead.requirements_doc}\n[TIMELINE]: ${JSON.stringify(timeline)}`;
430
  const history = await StateManager.getHistory(projectId);
431
 
432
+ const aiResult = await callAI(history, prompt, {}, prompts.director_system_prompt, projectContext, SMART_MODEL_ID);
433
 
 
434
  await StateManager.addHistory(projectId, 'model', aiResult.text);
435
  const cmds = extractCommands(aiResult.text);
436
  await executeCommands(lead.user_id, projectId, cmds);
437
 
438
+ // Morning briefings set the project to Frozen state until user returns
439
+ // This prevents the AI from generating 30 briefings for an abandoned project
440
  await StateManager.setFrozen(projectId, true);
441
 
442
  res.json({ success: true });