everydaycats commited on
Commit
c298bcf
·
verified ·
1 Parent(s): 6c3dd57

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +32 -276
app.js CHANGED
@@ -1,297 +1,53 @@
1
  import express from 'express';
2
- import { createServer } from 'http';
3
- import { WebSocketServer, WebSocket } from 'ws';
4
  import cors from 'cors';
5
- import jwt from 'jsonwebtoken';
6
- import { createClient } from '@supabase/supabase-js';
7
 
8
- const PORT = 7860;
9
- const SUPABASE_URL = process.env.SUPABASE_URL;
10
- const SUPABASE_KEY = process.env.SUPABASE_SERVICE_KEY;
11
- const CORE_URL = process.env.CORE_URL || "http://localhost:7862";
12
-
13
- if (!SUPABASE_URL) { console.error("❌ Config Missing"); process.exit(1); }
14
 
 
15
  const app = express();
16
- const server = createServer(app);
17
- const wss = new WebSocketServer({ noServer: true });
18
- const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
19
-
20
- const clients = new Map(); // UserId -> Set<WebSocket>
21
 
22
  app.use(cors());
23
- app.use(express.json({ limit: '50mb' }));
24
-
25
- app.get('/', (req, res) => res.send('Gateway Active'));
26
-
27
- // Internal Notification Webhook
28
- app.post('/internal/notify', (req, res) => {
29
- const { user_id, type, message } = req.body;
30
- if (clients.has(user_id)) {
31
- clients.get(user_id).forEach(ws => {
32
- if (ws.readyState === WebSocket.OPEN) {
33
- ws.send(JSON.stringify({ type, message }));
34
- // Ensure UI reloads on significant notifications
35
- if (type === 'toast') ws.send(JSON.stringify({ type: 'reload_project' }));
36
- }
37
- });
38
- return res.json({ success: true });
39
- }
40
- res.json({ success: false });
41
- });
42
-
43
- // NEW: Internal MCP Query Webhook (Routes AI requests down to the local daemon)
44
- app.post('/internal/mcp_query', (req, res) => {
45
- const { user_id, lead_id, payload } = req.body;
46
- if (clients.has(user_id)) {
47
- clients.get(user_id).forEach(ws => {
48
- if (ws.readyState === WebSocket.OPEN) {
49
- // Forward the exact payload to the daemon
50
- ws.send(JSON.stringify({ type: 'mcp_query', payload }));
51
- }
52
- });
53
- return res.json({ success: true });
54
- }
55
- res.json({ success: false, message: "User daemon not connected" });
56
- });
57
 
58
- // Verify JWT against DB Secret
59
- async function verifyThrustToken(token) {
60
- const decoded = jwt.decode(token);
61
- if (!decoded || !decoded.sid) return null;
62
 
63
- const { data: session } = await supabase
64
- .from('user_sessions')
65
- .select('session_secret')
66
- .eq('id', decoded.sid)
67
- .single();
68
-
69
- if (!session) return null;
70
 
 
 
 
 
71
  try {
72
- return jwt.verify(token, session.session_secret);
73
- } catch (e) {
74
- return null;
75
- }
76
- }
77
-
78
- // --- FETCH PAGINATED PROJECTS VIA JWT ---
79
- app.get('/api/projects', async (req, res) => {
80
- const authHeader = req.headers.authorization;
81
- if (!authHeader?.startsWith('Bearer ')) return res.status(401).json({ error: 'Unauthorized' });
82
-
83
- const token = authHeader.split(' ')[1];
84
- const decoded = await verifyThrustToken(token);
85
-
86
- if (!decoded || !decoded.uid) return res.status(403).json({ error: 'Invalid Token' });
87
-
88
- // Pagination Logic (Default: 9 items per page for a 3x3 grid)
89
- const page = parseInt(req.query.page) || 1;
90
- const limit = parseInt(req.query.limit) || 9;
91
- const start = (page - 1) * limit;
92
- const end = start + limit - 1;
93
-
94
- const { data, count, error } = await supabase
95
- .from('leads')
96
- .select('*', { count: 'exact' })
97
- .eq('user_id', decoded.uid)
98
- .order('created_at', { ascending: false })
99
- .range(start, end);
100
-
101
- if (error) return res.status(500).json({ error: error.message });
102
-
103
- res.json({
104
- projects: data,
105
- total: count,
106
- page: page,
107
- totalPages: Math.ceil(count / limit) || 1
108
- });
109
- });
110
-
111
- // --- FETCH ACTIVE THRUST & TASKS (For backward compatibility) ---
112
- app.get('/api/projects/:projectId/thrusts/active', async (req, res) => {
113
- const authHeader = req.headers.authorization;
114
- if (!authHeader?.startsWith('Bearer ')) return res.status(401).json({ error: 'Unauthorized' });
115
-
116
- const token = authHeader.split(' ')[1];
117
- const decoded = await verifyThrustToken(token);
118
- if (!decoded || !decoded.uid) return res.status(403).json({ error: 'Invalid Token' });
119
-
120
- const { projectId } = req.params;
121
-
122
- const { data, error } = await supabase
123
- .from('thrusts')
124
- .select('*, thrust_tasks(*)')
125
- .eq('lead_id', projectId)
126
- .eq('status', 'active')
127
- .order('created_at', { ascending: false })
128
- .limit(1);
129
-
130
- if (error) return res.status(500).json({ error: error.message });
131
- res.json(data);
132
- });
133
-
134
- // --- NEW: COMBINED MCP CONTEXT FETCH ---
135
- app.get('/api/projects/:projectId/mcp-context', async (req, res) => {
136
- const authHeader = req.headers.authorization;
137
- if (!authHeader?.startsWith('Bearer ')) return res.status(401).json({ error: 'Unauthorized' });
138
-
139
- const token = authHeader.split(' ')[1];
140
- const decoded = await verifyThrustToken(token);
141
- if (!decoded || !decoded.uid) return res.status(403).json({ error: 'Invalid Token' });
142
-
143
- const { projectId } = req.params;
144
- const { prd, thrust, timeline } = req.query;
145
-
146
- let result = {};
147
-
148
- try {
149
- if (prd === 'true') {
150
- const { data } = await supabase.from('leads').select('requirements_doc').eq('id', projectId).single();
151
- result.prd = data?.requirements_doc || null;
152
- }
153
-
154
- if (thrust === 'true') {
155
- const { data } = await supabase.from('thrusts').select('*, tasks:thrust_tasks(*)').eq('lead_id', projectId).eq('status', 'active').order('created_at', { ascending: false }).limit(1).single();
156
- result.thrust = data || null;
157
- }
158
-
159
- if (timeline === 'true') {
160
- const { data } = await supabase.from('timeline_events').select('*').eq('lead_id', projectId).order('created_at', { ascending: false }).limit(20);
161
- result.timeline = data || [];
162
- }
163
-
164
  res.json(result);
165
- } catch (error) {
166
- res.status(500).json({ error: error.message });
167
  }
168
  });
169
 
170
- // --- MARK TASK COMPLETE & LOG TO TIMELINE ---
171
- app.post('/api/projects/:projectId/tasks/:taskId/complete', async (req, res) => {
172
- const authHeader = req.headers.authorization;
173
- if (!authHeader?.startsWith('Bearer ')) return res.status(401).json({ error: 'Unauthorized' });
174
-
175
- const token = authHeader.split(' ')[1];
176
- const decoded = await verifyThrustToken(token);
177
- if (!decoded || !decoded.uid) return res.status(403).json({ error: 'Invalid Token' });
178
-
179
- const { projectId, taskId } = req.params;
180
- const { taskTitle } = req.body;
181
 
182
  try {
183
- // 1. Update Task Status
184
- const { error: updateError } = await supabase
185
- .from('thrust_tasks')
186
- .update({ is_completed: true, status: 'done' })
187
- .eq('id', taskId);
188
-
189
- if (updateError) throw updateError;
190
-
191
- // 2. Add to Timeline so AI sees it on next context sync
192
- const { error: timelineError } = await supabase
193
- .from('timeline_events')
194
- .insert({
195
- lead_id: projectId,
196
- title: "Task Completed",
197
- description: `User manually completed: ${taskTitle || 'a task'}`,
198
- type: "chore"
199
- });
200
-
201
- if (timelineError) throw timelineError;
202
-
203
- // 3. Notify the local WebSockets to show a toast and reload!
204
- if (clients.has(decoded.uid)) {
205
- clients.get(decoded.uid).forEach(ws => {
206
- if (ws.readyState === WebSocket.OPEN) {
207
- ws.send(JSON.stringify({ type: 'toast', message: `✅ Task Completed: ${taskTitle || 'a task'}` }));
208
- ws.send(JSON.stringify({ type: 'reload_project' }));
209
- }
210
- });
211
- }
212
-
213
- res.json({ success: true });
214
- } catch (error) {
215
- console.error("Task Completion Error:", error.message);
216
- res.status(500).json({ error: error.message });
217
  }
218
  });
219
 
220
- // WS Upgrade
221
- server.on('upgrade', async (request, socket, head) => {
222
- const url = new URL(request.url, `http://${request.headers.host}`);
223
- const token = url.searchParams.get('token');
224
-
225
- if (!token) { socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); socket.destroy(); return; }
226
-
227
- const decodedData = await verifyThrustToken(token);
228
-
229
- if (!decodedData) { socket.write('HTTP/1.1 403 Forbidden\r\n\r\n'); socket.destroy(); return; }
230
-
231
- wss.handleUpgrade(request, socket, head, (ws) => {
232
- wss.emit('connection', ws, request, decodedData.uid);
233
- });
234
- });
235
-
236
- // WS Logic
237
- wss.on('connection', (ws, req, userId) => {
238
- if (!clients.has(userId)) clients.set(userId, new Set());
239
- clients.get(userId).add(ws);
240
-
241
- ws.isAlive = true;
242
- ws.on('pong', () => { ws.isAlive = true; });
243
-
244
- ws.on('message', async (message) => {
245
- try {
246
- const data = JSON.parse(message.toString());
247
-
248
- // 1. CHAT PROMPTS & OVERRIDES
249
- if (data.type === 'prompt') {
250
- ws.send(JSON.stringify({ type: 'status', status: 'thinking' }));
251
- const response = await fetch(`${CORE_URL}/process`, {
252
- method: 'POST', headers: { 'Content-Type': 'application/json' },
253
- body: JSON.stringify({ userId, projectId: data.projectId, prompt: data.content, context: data.context, task_type: 'chat' })
254
- });
255
- if (!response.ok) throw new Error("Core API Failed");
256
- const result = await response.json();
257
- ws.send(JSON.stringify({ type: 'response', text: result.text, should_reload: result.should_reload, usage: result.usage }));
258
- }
259
-
260
- // 2. BACKGROUND CONTEXT SYNC (SMART DEBOUNCED)
261
- if (data.type === 'context_sync') {
262
- const payloadData = data.data;
263
- const currentTime = new Date().toLocaleString(); // Anchor time for the AI
264
-
265
- const formattedPrompt = `[WORKSPACE UPDATE - CURRENT TIME: ${currentTime}]\n\nActivity Log (Recent Activity):\n${payloadData.buffer}\n\nGit Diffs (Modified Files):\n${payloadData.diffs}\n\nNew Untracked Files:\n${payloadData.new_files}`;
266
-
267
- await fetch(`${CORE_URL}/process`, {
268
- method: 'POST', headers: { 'Content-Type': 'application/json' },
269
- body: JSON.stringify({
270
- userId: userId,
271
- projectId: data.projectId,
272
- prompt: formattedPrompt,
273
- images: payloadData.images,
274
- task_type: 'log_ingestion'
275
- })
276
- });
277
- }
278
-
279
- } catch (e) {
280
- console.error("WS Error", e);
281
- }
282
- });
283
-
284
- ws.on('close', () => {
285
- if (clients.has(userId)) clients.get(userId).delete(ws);
286
- });
287
- });
288
-
289
- setInterval(() => {
290
- wss.clients.forEach((ws) => {
291
- if (ws.isAlive === false) return ws.terminate();
292
- ws.isAlive = false;
293
- ws.ping();
294
- });
295
- }, 30000);
296
 
297
- server.listen(PORT, () => console.log(`🚀 Gateway on ${PORT}`));
 
1
  import express from 'express';
 
 
2
  import cors from 'cors';
3
+ import dotenv from 'dotenv';
 
4
 
5
+ // Import your modular apps here
6
+ import trendCatRouter from './apps/trend_cat.js';
7
+ import { generateCompletion, streamCompletion } from './ai_engine.js';
 
 
 
8
 
9
+ dotenv.config();
10
  const app = express();
11
+ const PORT = process.env.PORT || 7860;
 
 
 
 
12
 
13
  app.use(cors());
14
+ app.use(express.json({ limit: '50mb' }));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
+ // Mount the App-Specific Routes
17
+ app.use('/api/trendcat', trendCatRouter);
18
+ // app.use('/api/drcat', drCatRouter); // Future app
19
+ // app.use('/api/chefcat', chefCatRouter); // Future app
20
 
 
 
 
 
 
 
 
21
 
22
+ // --- LEGACY/GENERIC ENDPOINTS (for fast testing without making a new router) ---
23
+ app.post('/api/generate', async (req, res) => {
24
+ const { model, prompt, system_prompt, images } = req.body;
25
+ console.log(`[TRAFFIC] Generic generation request for ${model}`);
26
  try {
27
+ const result = await generateCompletion({ model, prompt, system_prompt, images });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  res.json(result);
29
+ } catch (err) {
30
+ res.status(500).json({ success: false, error: err.message });
31
  }
32
  });
33
 
34
+ app.post('/api/stream', async (req, res) => {
35
+ const { model, prompt, system_prompt, images } = req.body;
36
+ console.log(`[TRAFFIC] Generic stream request for ${model}`);
37
+
38
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
39
+ res.setHeader('Transfer-Encoding', 'chunked');
40
+ res.setHeader('X-Accel-Buffering', 'no');
41
+ res.flushHeaders();
 
 
 
42
 
43
  try {
44
+ await streamCompletion({ model, prompt, system_prompt, images, res });
45
+ } catch (err) {
46
+ res.write(`ERROR: ${err.message}`);
47
+ res.end();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  }
49
  });
50
 
51
+ app.get('/', async (req, res) => { res.json({ success: true, ecosystem: "Everyday Cats Backend" }); });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
+ app.listen(PORT, '0.0.0.0', () => console.log(`😻 Everyday Cats Ecosystem live on port ${PORT}`));