File size: 16,813 Bytes
63870b6
 
2112cf0
 
e9e20ec
691c050
b641cdb
 
 
 
 
 
 
 
 
 
 
e56b182
 
475cd2c
dcbe0f5
b641cdb
 
 
 
 
 
 
 
 
dcbe0f5
 
b641cdb
 
 
 
 
dcbe0f5
 
b641cdb
dcbe0f5
b641cdb
 
 
 
dcbe0f5
b641cdb
dcbe0f5
b641cdb
 
dcbe0f5
b641cdb
dcbe0f5
b641cdb
dcbe0f5
b641cdb
 
 
 
8629b61
 
 
 
 
b641cdb
 
 
da7a53a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f4cb457
da7a53a
 
 
f4cb457
24116f5
f4cb457
 
da7a53a
f4cb457
da7a53a
f4cb457
 
da7a53a
 
f4cb457
 
 
e56b182
f4cb457
 
 
 
 
da7a53a
 
 
e56b182
475cd2c
 
 
 
 
 
 
e56b182
475cd2c
da7a53a
e56b182
 
da7a53a
 
 
 
e56b182
 
 
da7a53a
 
 
 
 
e56b182
da7a53a
e56b182
 
 
 
 
 
da7a53a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e56b182
f4cb457
 
e56b182
f4cb457
da7a53a
 
 
 
 
e56b182
da7a53a
 
 
 
 
 
e56b182
 
da7a53a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e56b182
 
 
da7a53a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e56b182
 
da7a53a
 
 
 
e56b182
 
24116f5
da7a53a
 
 
 
 
 
 
 
 
 
 
e56b182
da7a53a
e56b182
 
 
b641cdb
8629b61
 
 
 
 
 
 
dcbe0f5
e56b182
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
import express from 'express';
import cors from 'cors';
import fs from 'fs';
import path from 'path';
import { createClient } from '@supabase/supabase-js';

const PORT = 7860;
const SUPABASE_URL = process.env.SUPABASE_URL;
const SUPABASE_KEY = process.env.SUPABASE_SERVICE_KEY;
const REMOTE_SERVER_URL = process.env.REMOTE_AI_URL || "http://localhost:11434"; 
const FRONT_URL = process.env.FRONT_URL;
const CRON_REGISTRY_URL = process.env.CRON_REGISTRY_URL || "http://localhost:7861"; 
const CRON_SECRET = process.env.CRON_SECRET || "default_secret";

const SMART_MODEL_ID = "claude"; 
const FAST_MODEL_ID = "gpt-5-mini"; 

// The Utility Server that handles the Email Dispatching
const UTILITY_SERVER_URL = process.env.UTILITY_SERVER_URL || "https://lu5zfciin5mk34fowavmhz7dt40pkhhg.lambda-url.us-east-1.on.aws";

if (!SUPABASE_URL || !SUPABASE_KEY) process.exit(1);

const app = express();
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);

app.use(express.json({ limit: '50mb' }));
app.use(cors());

let prompts = {}; 
try {
    prompts = JSON.parse(fs.readFileSync(path.resolve('./prompts.json'), 'utf8'));
} catch (e) { process.exit(1); }

const activeProjects = new Map();

const StateManager = {
    getHistory: async (projectId) => {
        if (activeProjects.has(projectId)) return activeProjects.get(projectId).history;
        const { data: chunks } = await supabase.from('message_chunks').select('*').eq('project_id', projectId).order('chunk_index', { ascending: false }).limit(10);
        const fullHistory = (chunks || []).reverse().flatMap(c => c.payload || []);
        activeProjects.set(projectId, { history: fullHistory, isFrozen: false });
        return fullHistory;
    },
    addHistory: async (projectId, role, text) => {
        const newMessage = { role, parts: [{ text }] };
        if (activeProjects.has(projectId)) activeProjects.get(projectId).history.push(newMessage);
        try {
            const { data: latestChunk } = await supabase.from('message_chunks').select('id, chunk_index, payload').eq('project_id', projectId).order('chunk_index', { ascending: false }).limit(1).single();
            const currentPayload = (latestChunk?.payload) || [];
            if (latestChunk && currentPayload.length < 20) {
                await supabase.from('message_chunks').update({ payload: [...currentPayload, newMessage] }).eq('id', latestChunk.id);
            } else {
                await supabase.from('message_chunks').insert({ project_id: projectId, lead_id: projectId, chunk_index: (latestChunk?.chunk_index ?? -1) + 1, payload: [newMessage] });
            }
        } catch (e) {}
    },
    setFrozen: async (projectId, status) => {
        if (activeProjects.has(projectId)) activeProjects.get(projectId).isFrozen = status;
        await supabase.from('leads').update({ is_frozen: status }).eq('id', projectId);
    },
    isFrozen: async (projectId) => {
        if (activeProjects.has(projectId)) return activeProjects.get(projectId).isFrozen;
        const { data } = await supabase.from('leads').select('is_frozen').eq('id', projectId).single();
        return data?.is_frozen || false;
    }
};

const callAI = async (history, input, contextData, images, systemPrompt, projectContext, modelId) => {
    let contextStr = "";
    try { contextStr = JSON.stringify(contextData, null, 2); } catch {}
    const recentHistory = history.slice(-10).map(m => `${m.role === 'model' ? 'Assistant' : 'User'}: ${m.parts?.[0]?.text || ""}`).join('\n');
    const fullPrompt = `System: ${systemPrompt}\n\n${projectContext}\n\n[HISTORY]:\n${recentHistory}\n\n[CONTEXT]: ${contextStr}\n\nUser: ${input}\nAssistant:`;

    try {
        const response = await fetch(`${REMOTE_SERVER_URL}/api/generate`, {
            method: 'POST', headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ model: modelId, prompt: fullPrompt, system_prompt: systemPrompt, images: images || [] })
        });
        const result = await response.json();
        return { text: result.data || result.text || "", usage: result.usage };
    } catch (e) { return { text: "<notification>AI Unreachable</notification>", usage: {} }; }
};

function extractCommands(text) {
    const commands = [];
    const parse = (regex, type, isJson = true) => {
        let match;
        regex.lastIndex = 0;
        while ((match = regex.exec(text)) !== null) {
            let rawPayload = match[1].trim();
            try {
                commands.push({ type, payload: isJson ? JSON.parse(rawPayload) : rawPayload });
            } catch (e) { 
                if (type === 'create_thrust') commands.push({ type, payload: { title: "System Thrust", markdown_content: rawPayload, tasks: [] } });
            }
        }
    };

    parse(/<thrust_create>([\s\S]*?)<\/thrust_create>/gi, 'create_thrust');
    parse(/<timeline_log>([\s\S]*?)<\/timeline_log>/gi, 'log_timeline');
    parse(/<notification>([\s\S]*?)<\/notification>/gi, 'notification', false);
    parse(/<update_requirements>([\s\S]*?)<\/update_requirements>/gi, 'update_requirements', false);
    parse(/<schedule_briefing>([\s\S]*?)<\/schedule_briefing>/gi, 'schedule_briefing');
    parse(/<freeze_project>([\s\S]*?)<\/freeze_project>/gi, 'freeze_project', false); 
    parse(/<thrust_complete>([\s\S]*?)<\/thrust_complete>/gi, 'thrust_complete', false); 
    parse(/<complete_task>([\s\S]*?)<\/complete_task>/gi, 'complete_task', false); 

    return commands;
}

// --- BULLETPROOF CRON MATH ---
async function registerMorningCron(projectId, offset) {
    const now = new Date();
    const target = new Date(now);
    
    const targetUtcMinutes = (5 * 60) - (offset * 60); 
    target.setUTCHours(0, targetUtcMinutes, 0, 0); 
    
    if (target <= now) target.setDate(target.getDate() + 1); 

    const delayMs = target.getTime() - now.getTime();
    console.log(`⏰ Briefing Scheduled for ${projectId}. Local 6AM occurs in ${(delayMs/1000/60/60).toFixed(2)} hours.`);

    fetch(`${CRON_REGISTRY_URL}/register`, {
        method: 'POST', headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ 
            secret: CRON_SECRET, 
            jobId: `briefing_${projectId}`, 
            intervalMs: 86400000, 
            initialDelay: delayMs, 
            webhookUrl: `https://everydaytok-thrust-core-server.hf.space/automated-briefing`, 
            leadId: projectId, 
            payload: { projectId, timezoneOffset: offset } 
        })
    }).catch(()=>{});
}

// --- EMAIL DISPATCHER HELPER ---
async function dispatchEmail(userId, projectId, projectName, briefingId, markdownContent) {
    console.log(`📧 Dispatching email to Utility Server for Project: ${projectName}`);
    fetch(`${UTILITY_SERVER_URL}/api/email/send-briefing`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ userId, projectId, projectName, briefingId, markdownContent })
    }).catch(e => console.error("Email Dispatch Error:", e.message));
}

async function executeCommands(userId, projectId, commands) {
    // TRACK THE NEW THRUST SO WE CAN EMAIL IT LATER
    let flags = { shouldReload: false, thrustComplete: false, newThrustId: null, newThrustMarkdown: null };

    for (const cmd of commands) {
        try {
            if (cmd.type === 'create_thrust') {
                // Wipe old thrust
                await supabase.from('thrusts').delete().eq('lead_id', projectId);

                const { data: thrust } = await supabase.from('thrusts').insert({ lead_id: projectId, title: cmd.payload.title, markdown_content: cmd.payload.markdown_content, status: 'active' }).select().single();
                if (thrust && cmd.payload.tasks && cmd.payload.tasks.length > 0) {
                    const tasks = cmd.payload.tasks.map(t => ({ thrust_id: thrust.id, title: t }));
                    await supabase.from('thrust_tasks').insert(tasks);
                }
                
                flags.shouldReload = true;

                // Capture data for email dispatch
                if (thrust) {
                    flags.newThrustId = thrust.id;
                    flags.newThrustMarkdown = cmd.payload.markdown_content;
                }
            }

            if (cmd.type === 'log_timeline') {
                await supabase.from('timeline_events').insert({ lead_id: projectId, title: cmd.payload.title, description: cmd.payload.description, type: (cmd.payload.type || 'system').toLowerCase() });
                flags.shouldReload = true;
            }

            if (cmd.type === 'update_requirements') {
                await supabase.from('leads').update({ requirements_doc: cmd.payload }).eq('id', projectId);
                flags.shouldReload = true;
            }

            if (cmd.type === 'schedule_briefing') {
                await registerMorningCron(projectId, cmd.payload.timezone_offset || 0);
            }

            if (cmd.type === 'complete_task') {
                const { data: active } = await supabase.from('thrusts').select('id').eq('lead_id', projectId).eq('status', 'active').single();
                if (active) {
                    await supabase.from('thrust_tasks').update({ status: 'done' }).eq('thrust_id', active.id).ilike('title', `%${cmd.payload}%`);
                    flags.shouldReload = true;
                }
            }

            if (cmd.type === 'thrust_complete') {
                const { data: active } = await supabase.from('thrusts').select('id').eq('lead_id', projectId).eq('status', 'active').single();
                if (active) {
                    await supabase.from('thrusts').update({ status: 'completed' }).eq('id', active.id);
                    await supabase.from('thrust_tasks').delete().eq('thrust_id', active.id);
                    flags.thrustComplete = true;
                }
            }

            if (cmd.type === 'freeze_project') {
                const isFrozen = cmd.payload === 'true';
                await StateManager.setFrozen(projectId, isFrozen);
            }

            if (cmd.type === 'notification' && FRONT_URL) {
                fetch(`${FRONT_URL}/internal/notify`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id: userId, type: 'toast', message: cmd.payload }) }).catch(() => {});
            }

        } catch (e) {}
    }
    return flags;
}

app.post('/init-project', async (req, res) => {
    // Note: timezoneOffset added here to capture frontend's data
    const { userId, name, description, localPath, timezoneOffset = 0 } = req.body;
    const { data: lead } = await supabase.from('leads').insert({ user_id: userId, name, description, local_path: localPath, status: 'active', requirements_doc: "Init..." }).select().single();
    res.json({ success: true, leadId: lead.id });

    setImmediate(async () => {
        try {
            const initInput = `PROJECT: ${name}\nDESC: ${description}\nUSER TIMEZONE OFFSET: ${timezoneOffset}\nTask: Init PRD, First Thrust, Schedule Morning Briefing.`;
            const aiResult = await callAI([], initInput, {}, [], prompts.init_system_prompt, "", SMART_MODEL_ID);
            aiResult.text += `\n<notification>Project '${name}' initialized successfully!</notification>`;
            await StateManager.addHistory(lead.id, 'user', initInput);
            await StateManager.addHistory(lead.id, 'model', aiResult.text);
            const cmds = extractCommands(aiResult.text);
            await executeCommands(userId, lead.id, cmds);
            
            // NO EMAIL DISPATCH HERE - strictly for morning briefings!
        } catch (err) {}
    });
});

app.post('/process', async (req, res) => {
    const { userId, projectId, prompt, context, images, task_type = 'chat' } = req.body;
    if (task_type === 'chat') await StateManager.setFrozen(projectId, false);

    let selectedModel = (task_type === 'log_ingestion') ? FAST_MODEL_ID : SMART_MODEL_ID;
    let sysPrompt = (task_type === 'log_ingestion') ? prompts.log_analyst_prompt : prompts.director_system_prompt;

    try {
        const { data: lead } = await supabase.from('leads').select('requirements_doc').eq('id', projectId).single();
        const { data: activeThrust } = await supabase.from('thrusts').select('title, tasks:thrust_tasks(title, status)').eq('lead_id', projectId).eq('status', 'active').order('created_at', { ascending: false }).limit(1).single();
        const { data: timeline } = await supabase.from('timeline_events').select('title, type, description, created_at').eq('lead_id', projectId).order('created_at', { ascending: false }).limit(10); 

        const projectContext = `[PRD]: ${lead?.requirements_doc?.substring(0, 3000)}...\n[CURRENT THRUST]: ${activeThrust ? JSON.stringify(activeThrust) : "None"}\n[RECENT TIMELINE]: ${JSON.stringify(timeline || [])}`;
        const history = await StateManager.getHistory(projectId);
        
        let aiResult = await callAI(history, prompt, context, images, sysPrompt, projectContext, selectedModel);
        let cmds = extractCommands(aiResult.text);
        let flags = await executeCommands(userId, projectId, cmds);

        if (flags.thrustComplete && task_type === 'log_ingestion') {
            const escalationPrompt = "The previous thrust is complete based on logs. Generate the next Thrust immediately to keep momentum.";
            const smartResult = await callAI(history, escalationPrompt, context, [], prompts.director_system_prompt, projectContext, SMART_MODEL_ID);
            aiResult.text += `\n\n[DIRECTOR INTERVENTION]:\n${smartResult.text}`;
            const smartCmds = extractCommands(smartResult.text);
            await executeCommands(userId, projectId, smartCmds);
            
            // NO EMAIL DISPATCH HERE - strictly for morning briefings!

            await StateManager.addHistory(projectId, 'model', aiResult.text);
        } else {
            await StateManager.addHistory(projectId, 'model', aiResult.text);
        }

        const cleanText = aiResult.text.replace(/<[^>]+>[\s\S]*?<\/[^>]+>/g, '').trim();
        res.json({ text: cleanText, should_reload: flags.shouldReload });
    } catch (e) { res.status(500).json({ error: "Processing Error" }); }
});

app.post('/automated-briefing', async (req, res) => {
    const { projectId } = req.body;

    try {
        const isFrozen = await StateManager.isFrozen(projectId);
        const { data: lastThrust } = await supabase.from('thrusts').select('created_at').eq('lead_id', projectId).order('created_at', { ascending: false }).limit(1).single();

        const lastThrustDate = lastThrust ? new Date(lastThrust.created_at).getDate() : 0;
        const today = new Date();
        const todayDate = today.getDate();

        if (isFrozen) return res.json({ status: "skipped_frozen" });
        if (lastThrustDate === todayDate) return res.json({ status: "skipped_exists" });

        const currentDateString = today.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
        
        const prompt = `[SYSTEM TIME: ${currentDateString} at 5:00 AM]\nIt is morning. Generate today's Morning Briefing (New Thrust). Look at the RECENT TIMELINE to see what was accomplished yesterday. Adopt a highly conversational, proactive tone in the markdown (e.g., 'Morning! You finished X yesterday. Today, the priority is Y.'). If the project has been idle for days, use <freeze_project>true</freeze_project>.`;
        
        const { data: lead } = await supabase.from('leads').select('*').eq('id', projectId).single();
        const { data: timeline } = await supabase.from('timeline_events').select('*').eq('lead_id', projectId).order('created_at', { ascending: false }).limit(5);

        const projectContext = `[PRD]: ${lead.requirements_doc}\n[RECENT TIMELINE]: ${JSON.stringify(timeline)}`;
        const history = await StateManager.getHistory(projectId);
        
        const aiResult = await callAI(history, prompt, {}, [], prompts.director_system_prompt, projectContext, SMART_MODEL_ID);
        
        await StateManager.addHistory(projectId, 'model', aiResult.text);
        const cmds = extractCommands(aiResult.text);
        const flags = await executeCommands(lead.user_id, projectId, cmds);

        // 👉 EMAIL IS DISPATCHED ONLY HERE
        if (flags.newThrustId) {
            await dispatchEmail(lead.user_id, projectId, lead.name, flags.newThrustId, flags.newThrustMarkdown);
        }

        await StateManager.setFrozen(projectId, true); 
        res.json({ success: true });

    } catch (e) { res.status(500).json({ error: e.message }); }
});

app.get('/', async (req, res) => res.status(200).json({ status: "Alive" }));
app.listen(PORT, () => console.log(`✅ Core Online: ${PORT}`));