thrust-cli 1.0.10 → 1.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/utils/daemon.js CHANGED
@@ -1,4 +1,4 @@
1
- import { WebSocket, WebSocketServer } from 'ws';
1
+ import { WebSocket, WebSocketServer } from 'ws';
2
2
  import chokidar from 'chokidar';
3
3
  import simpleGit from 'simple-git';
4
4
  import express from 'express';
@@ -15,14 +15,21 @@ import { getActiveProject, getConfig, saveConfig } from './config.js';
15
15
  const __filename = fileURLToPath(import.meta.url);
16
16
  const __dirname = path.dirname(__filename);
17
17
 
18
- const GATEWAY_URL = process.env.FRONT_URL || "ws://localhost:7860";
18
+ const GATEWAY_URL = process.env.FRONT_URL || "wss://everydaycats-thrust-front-server.hf.space";
19
+ const API_URL = GATEWAY_URL.replace('ws://', 'http://').replace('wss://', 'https://');
20
+ const AUTH_PROXY_URL = "https://everydaycats-thrust-auth-server.hf.space";
21
+
22
+ // --- DEBOUNCE & POLLING TIMERS ---
23
+ const INACTIVITY_DELAY_MS = 15 * 1000; // 30 * 1000; // lets make it 15 seconds instead
24
+ const MCP_POLL_INTERVAL_MS = 18 * 1000; // 0.3 * 60 * 1000; // Poll external tools every 0.3 mins (18 seconds)
19
25
 
20
26
  let currentWatcher = null;
21
- let syncTimeout = null;
22
- let globalWs = null;
23
- let localWss = null;
24
- let wsRetryLogged = false;
25
- let eaccesWarningLogged = false;
27
+ let inactivityTimer = null;
28
+ let mcpPollTimer = null;
29
+ let fileActivityBuffer = "";
30
+ let globalWs = null;
31
+ let localWss = null;
32
+ let wsRetryLogged = false;
26
33
 
27
34
  const findAvailablePort = (startPort) => {
28
35
  return new Promise((resolve) => {
@@ -45,53 +52,317 @@ function broadcastLocalLog(type, message) {
45
52
  });
46
53
  }
47
54
 
55
+ function isBinaryData(buffer) {
56
+ for (let i = 0; i < Math.min(buffer.length, 512); i++) {
57
+ if (buffer[i] === 0) return true;
58
+ }
59
+ return false;
60
+ }
61
+
48
62
  export async function startDaemon(preferredPort) {
49
63
  const actualPort = await findAvailablePort(preferredPort);
50
64
  const app = express();
51
65
 
52
- const corsOptions = {
53
- origin: `http://localhost:${actualPort}`,
54
- optionsSuccessStatus: 200
66
+ const corsOptionsDelegate = (req, callback) => {
67
+ const origin = req.header('Origin');
68
+ const allowedWebApps = ['https://thrust.web.app', 'http://localhost:3000'];
69
+ if (req.path.startsWith('/api/mcp')) {
70
+ callback(null, { origin: true });
71
+ } else if (req.path === '/api/auth/callback') {
72
+ if (!origin || allowedWebApps.includes(origin) || origin === `http://localhost:${actualPort}`) {
73
+ callback(null, { origin: true, credentials: true });
74
+ } else {
75
+ callback(new Error('CORS blocked auth callback'), { origin: false });
76
+ }
77
+ } else {
78
+ callback(null, { origin: `http://localhost:${actualPort}` });
79
+ }
55
80
  };
56
- app.use('/api', cors(corsOptions), express.json());
81
+
82
+ app.use(cors(corsOptionsDelegate));
83
+ app.use(express.json());
57
84
 
58
85
  const frontendPath = path.join(__dirname, '..', 'frontend');
59
86
 
60
- app.get('/api/status', (req, res) => res.json({ activeProject: getActiveProject() }));
87
+ // ==========================================
88
+ // MCP SERVER ENDPOINTS (FEED-IN PUSH)
89
+ // ==========================================
90
+
91
+ app.get('/api/mcp/context', async (req, res) => {
92
+ const config = getConfig();
93
+ const token = config.auth?.token;
94
+ const projectId = config.activeLeadId;
95
+
96
+ if (!token || !projectId) {
97
+ return res.status(401).json({ error: "Thrust agent not linked or authenticated." });
98
+ }
61
99
 
62
- app.get('/api/explore', (req, res) => {
63
100
  try {
64
- let targetPath = req.query.path || os.homedir();
65
- targetPath = path.resolve(targetPath);
101
+ const response = await fetch(`${API_URL}/api/projects/${projectId}/thrusts/active`, {
102
+ headers: { 'Authorization': `Bearer ${token}` }
103
+ });
104
+ const data = await response.json();
105
+
106
+ res.json({
107
+ projectId,
108
+ projectPath: config.leads[projectId].path,
109
+ activeThrust: data.length > 0 ? data[0] : null
110
+ });
111
+ broadcastLocalLog('mcp', `šŸ”— [Context Sync] External client requested project state.`);
112
+ } catch (e) {
113
+ res.status(502).json({ error: "Failed to fetch context." });
114
+ }
115
+ });
66
116
 
67
- if (!fs.existsSync(targetPath)) {
68
- return res.status(404).json({ error: "Directory not found" });
69
- }
117
+ app.post('/api/mcp/timeline', async (req, res) => {
118
+ const { source, action_type, description, requires_code_sync } = req.body;
119
+
120
+ if (!source || !description) {
121
+ return res.status(400).json({ error: "Malformed MCP payload." });
122
+ }
70
123
 
71
- const items = fs.readdirSync(targetPath, { withFileTypes: true });
124
+ broadcastLocalLog('mcp', `šŸ”— [${source} Pushed Event] ${action_type}: ${description}`);
125
+
126
+ fileActivityBuffer += `\n[${new Date().toLocaleTimeString()}] [MCP EXT EVENT] Source: ${source} | Action: ${action_type} | Desc: ${description} | Needs Code Sync: ${requires_code_sync ? 'Yes' : 'No'}\n`;
127
+
128
+ const activeProject = getActiveProject();
129
+ if (activeProject && activeProject.path) {
130
+ if (inactivityTimer) clearTimeout(inactivityTimer);
131
+ await syncContext(activeProject.path);
132
+ }
133
+
134
+ res.json({ success: true, message: "Timeline event ingested and sync triggered." });
135
+ });
136
+
137
+ // ==========================================
138
+ // MCP CLIENT CONFIGURATION (FEED-IN PULL)
139
+ // ==========================================
140
+
141
+ app.get('/api/mcp/servers', (req, res) => {
142
+ res.json(getConfig().mcpServers || []);
143
+ });
144
+
145
+ app.post('/api/mcp/servers', (req, res) => {
146
+ const { name, url } = req.body;
147
+ if (!name || !url) return res.status(400).json({ error: "Missing name or url" });
148
+
149
+ const config = getConfig();
150
+ if (!config.mcpServers) config.mcpServers = [];
151
+ config.mcpServers.push({ name, url, type: 'http' });
152
+ saveConfig(config);
153
+
154
+ // Trigger an immediate poll when a new service is added
155
+ pollExternalMCPServers();
156
+ res.json({ success: true });
157
+ });
158
+
159
+ app.delete('/api/mcp/servers/:index', (req, res) => {
160
+ const config = getConfig();
161
+ const index = parseInt(req.params.index);
162
+ if (config.mcpServers && config.mcpServers[index]) {
163
+ config.mcpServers.splice(index, 1);
164
+ saveConfig(config);
165
+ }
166
+ res.json({ success: true });
167
+ });
168
+
169
+ // ==========================================
170
+ // EXTERNAL MCP DIRECT QUERY (AI OR MANUAL)
171
+ // ==========================================
172
+ app.post('/api/mcp/query', async (req, res) => {
173
+
174
+ const { serverName, toolName, targetArg } = req.body;
175
+ const config = getConfig();
176
+
177
+ // Find the requested server by name (e.g., "Unity")
178
+ const server = config.mcpServers?.find(s => s.name.toLowerCase() === serverName.toLowerCase());
179
+
180
+ if (!server) return res.status(404).json({ error: `MCP Server '${serverName}' not found.` });
181
+
182
+ try {
183
+ // Build the JSON-RPC Payload
184
+ const payload = {
185
+ jsonrpc: "2.0",
186
+ method: "tools/call",
187
+ params: {
188
+ name: toolName,
189
+ arguments: targetArg ? { target: targetArg } : {}
190
+ },
191
+ id: Date.now()
192
+ };
193
+
194
+ const response = await fetch(server.url, {
195
+ method: 'POST',
196
+ headers: { 'Content-Type': 'application/json' },
197
+ body: JSON.stringify(payload)
198
+ });
199
+
200
+ if (!response.ok) throw new Error("Server responded with error");
201
+
202
+ const data = await response.json();
203
+ const resultText = data.result?.content?.[0]?.text || "No data returned.";
72
204
 
73
- const directories = items
74
- .filter(item => item.isDirectory() && !item.name.startsWith('.'))
75
- .map(item => item.name)
76
- .sort();
205
+ // Log it locally so the user sees the system thinking
206
+ broadcastLocalLog('mcp', `⚔ Queried ${serverName} for ${toolName}.`);
207
+
208
+ // Inject this data directly into the activity buffer so the AI sees it on the next sync
209
+ fileActivityBuffer += `\n[${new Date().toLocaleTimeString()}] [MCP DIRECT QUERY RESULT] Source: ${serverName} | Tool: ${toolName} | Target: ${targetArg || 'none'} \nResult:\n${resultText}\n`;
77
210
 
78
- const parentPath = path.dirname(targetPath);
211
+ res.json({ success: true, data: resultText });
79
212
 
80
- res.json({
81
- currentPath: targetPath,
82
- parentPath: targetPath === parentPath ? null : parentPath,
83
- directories: directories
213
+ } catch (e) {
214
+ broadcastLocalLog('error', `āš ļø Failed to query MCP server: ${serverName}`);
215
+ res.status(500).json({ error: e.message });
216
+ }
217
+ });
218
+
219
+ // ==========================================
220
+ // CORE DASHBOARD API
221
+ // ==========================================
222
+
223
+ app.get('/api/status', (req, res) => {
224
+ const config = getConfig();
225
+ res.json({ auth: config.auth, activeProject: getActiveProject() });
226
+ });
227
+
228
+ app.get('/api/cloud-projects', async (req, res) => {
229
+ const config = getConfig();
230
+ const token = config.auth?.token;
231
+ if (!token) return res.status(401).json({ error: "Not authenticated" });
232
+
233
+ const page = req.query.page || 1;
234
+ try {
235
+ const response = await fetch(`${API_URL}/api/projects?page=${page}&limit=9`, {
236
+ headers: { 'Authorization': `Bearer ${token}` }
84
237
  });
85
- } catch (error) {
86
- res.status(403).json({ error: "Permission denied." });
238
+ const data = await response.json();
239
+ if (!response.ok) throw new Error(data.error);
240
+ res.json(data);
241
+ } catch (e) {
242
+ res.status(502).json({ error: "Failed to fetch from Gateway." });
87
243
  }
88
244
  });
89
245
 
246
+ app.get('/api/thrusts/active', async (req, res) => {
247
+ const config = getConfig();
248
+ const token = config.auth?.token;
249
+ const projectId = config.activeLeadId;
250
+
251
+ if (!token || !projectId) return res.status(401).json({ error: "Not ready" });
252
+
253
+ try {
254
+ const response = await fetch(`${API_URL}/api/projects/${projectId}/thrusts/active`, {
255
+ headers: { 'Authorization': `Bearer ${token}` }
256
+ });
257
+ const data = await response.json();
258
+ if (!response.ok) throw new Error(data.error);
259
+ res.json(data);
260
+ } catch (e) {
261
+ res.status(502).json({ error: "Failed to fetch current directive." });
262
+ }
263
+ });
264
+
265
+ app.post('/api/tasks/complete', async (req, res) => {
266
+ const { taskId, taskTitle } = req.body;
267
+ const config = getConfig();
268
+ const token = config.auth?.token;
269
+ const projectId = config.activeLeadId;
270
+
271
+ if (!token || !projectId) return res.status(401).json({ error: "Not authenticated" });
272
+
273
+ try {
274
+ const response = await fetch(`${API_URL}/api/projects/${projectId}/tasks/${taskId}/complete`, {
275
+ method: 'POST',
276
+ headers: {
277
+ 'Authorization': `Bearer ${token}`,
278
+ 'Content-Type': 'application/json'
279
+ },
280
+ body: JSON.stringify({ taskTitle })
281
+ });
282
+
283
+ const data = await response.json();
284
+ if (!response.ok) throw new Error(data.error);
285
+
286
+ if (config.leads[projectId]?.path) {
287
+ if (inactivityTimer) clearTimeout(inactivityTimer);
288
+ syncContext(config.leads[projectId].path);
289
+ }
290
+
291
+ res.json(data);
292
+ } catch (e) {
293
+ res.status(502).json({ error: "Failed to sync task completion to cloud." });
294
+ }
295
+ });
296
+
297
+ app.post('/api/auth/callback', async (req, res) => {
298
+ const { tempKey, token, email } = req.body;
299
+ try {
300
+ let finalToken = token;
301
+ if (tempKey && !token) {
302
+ const response = await fetch(`${AUTH_PROXY_URL}/redeem`, {
303
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key: tempKey })
304
+ });
305
+ const data = await response.json();
306
+ if (!data.token) return res.status(401).json({ error: "Invalid Key" });
307
+ finalToken = data.token;
308
+ }
309
+
310
+ if (finalToken) {
311
+ const config = getConfig();
312
+ config.auth = { token: finalToken, email: email || "Authenticated User" };
313
+ saveConfig(config);
314
+ broadcastLocalLog('success', 'āœ… Authentication Successful!');
315
+ connectWebSocket();
316
+ return res.json({ success: true });
317
+ } else {
318
+ return res.status(400).json({ error: "No valid auth data provided" });
319
+ }
320
+ } catch (e) {
321
+ res.status(502).json({ error: "Auth Processing Failed" });
322
+ }
323
+ });
324
+
325
+ app.post('/api/auth/logout', async (req, res) => {
326
+ const config = getConfig();
327
+ const token = config.auth?.token;
328
+ const projectId = config.activeLeadId;
329
+
330
+ if (token && projectId) {
331
+ fetch(`${API_URL}/nullify`, {
332
+ method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ projectId })
333
+ }).catch(() => {});
334
+ }
335
+
336
+ config.auth = { token: null, email: null };
337
+ config.activeLeadId = null;
338
+ saveConfig(config);
339
+
340
+ if (globalWs) { globalWs.close(); globalWs = null; }
341
+ if (currentWatcher) { await currentWatcher.close(); currentWatcher = null; }
342
+ if (inactivityTimer) { clearTimeout(inactivityTimer); inactivityTimer = null; }
343
+ if (mcpPollTimer) { clearInterval(mcpPollTimer); mcpPollTimer = null; }
344
+
345
+ broadcastLocalLog('error', 'šŸ›‘ Logged out. Session cleared.');
346
+ res.json({ success: true });
347
+ });
348
+
349
+ app.get('/api/explore', (req, res) => {
350
+ try {
351
+ let targetPath = req.query.path || os.homedir();
352
+ targetPath = path.resolve(targetPath);
353
+ if (!fs.existsSync(targetPath)) return res.status(404).json({ error: "Directory not found" });
354
+
355
+ const items = fs.readdirSync(targetPath, { withFileTypes: true });
356
+ const directories = items.filter(item => item.isDirectory() && !item.name.startsWith('.')).map(item => item.name).sort();
357
+ const parentPath = path.dirname(targetPath);
358
+
359
+ res.json({ currentPath: targetPath, parentPath: targetPath === parentPath ? null : parentPath, directories: directories });
360
+ } catch (error) { res.status(403).json({ error: "Permission denied." }); }
361
+ });
362
+
90
363
  app.post('/api/link', async (req, res) => {
91
364
  const { leadId, folderPath } = req.body;
92
- if (!fs.existsSync(folderPath)) {
93
- return res.status(400).json({ error: "Folder path not found." });
94
- }
365
+ if (!fs.existsSync(folderPath)) return res.status(400).json({ error: "Folder path not found." });
95
366
 
96
367
  const config = getConfig();
97
368
  config.leads[leadId] = { path: folderPath, linkedAt: new Date().toISOString() };
@@ -99,6 +370,10 @@ export async function startDaemon(preferredPort) {
99
370
  saveConfig(config);
100
371
 
101
372
  await startWatching(folderPath);
373
+
374
+ if (inactivityTimer) clearTimeout(inactivityTimer);
375
+ syncContext(folderPath);
376
+
102
377
  res.json({ success: true });
103
378
  });
104
379
 
@@ -110,9 +385,10 @@ export async function startDaemon(preferredPort) {
110
385
  if (currentWatcher) {
111
386
  await currentWatcher.close();
112
387
  currentWatcher = null;
388
+ if (inactivityTimer) { clearTimeout(inactivityTimer); inactivityTimer = null; }
389
+ if (mcpPollTimer) { clearInterval(mcpPollTimer); mcpPollTimer = null; }
113
390
  broadcastLocalLog('system', 'šŸ›‘ Stopped watching.');
114
391
  }
115
-
116
392
  res.json({ success: true });
117
393
  });
118
394
 
@@ -130,123 +406,224 @@ export async function startDaemon(preferredPort) {
130
406
  });
131
407
 
132
408
  localWss = new WebSocketServer({ server });
133
-
134
409
  localWss.on('connection', (ws) => {
135
- ws.send(JSON.stringify({ type: 'system', message: '🟢 Local WebSocket connected.' }));
410
+ ws.send(JSON.stringify({ type: 'system', message: '🟢 Local UI Connected.' }));
136
411
  ws.on('message', (message) => {
137
412
  try {
138
413
  const data = JSON.parse(message.toString());
139
414
  if (data.type === 'frontend_prompt') {
140
- console.log(`\nšŸ’¬ [LOCAL UI]: ${data.payload}`);
141
- broadcastLocalLog('ai', `Daemon received prompt: "${data.payload}"`);
415
+ if (globalWs && globalWs.readyState === WebSocket.OPEN) {
416
+ globalWs.send(JSON.stringify({ type: 'prompt', content: data.payload, projectId: getActiveProject()?.id }));
417
+ broadcastLocalLog('sync', `Prompt sent to Cloud Director...`);
418
+ } else {
419
+ broadcastLocalLog('error', `āš ļø Cannot send: Cloud Gateway is offline.`);
420
+ }
142
421
  }
143
422
  } catch (err) {}
144
423
  });
145
424
  });
146
425
 
147
- connectWebSocket();
148
- const initialProject = getActiveProject();
149
- if (initialProject?.path) {
150
- startWatching(initialProject.path);
426
+ const config = getConfig();
427
+ if (config.auth && config.auth.token) {
428
+ connectWebSocket();
429
+ const initialProject = getActiveProject();
430
+ if (initialProject?.path) {
431
+ startWatching(initialProject.path);
432
+ }
433
+ } else {
434
+ console.log("\nāš ļø Agent is not authenticated. Awaiting connection from Web Dashboard...");
151
435
  }
152
436
  }
153
437
 
154
- // --- TRULY UNIVERSAL BROWSER OPENER ---
155
438
  async function safeOpenBrowser(url) {
156
439
  try {
157
- if (process.env.TERMUX_VERSION) {
158
- exec(`termux-open-url ${url}`, () => {
159
- console.log("šŸ‘‰ Please open the URL manually in your mobile browser.");
440
+ if (process.env.TERMUX_VERSION) exec(`termux-open-url ${url}`, () => {});
441
+ else if (!process.env.DISPLAY && process.platform === 'linux') {}
442
+ else { await open(url); }
443
+ } catch (e) {}
444
+ }
445
+
446
+ // --- EXTERNAL MCP POLLING LOGIC ---
447
+ async function pollExternalMCPServers() {
448
+ const config = getConfig();
449
+ if (!config.mcpServers || config.mcpServers.length === 0) return;
450
+
451
+ let hasNewData = false;
452
+
453
+ for (const server of config.mcpServers) {
454
+ try {
455
+ // Send standard MCP JSON-RPC payload asking for recent activity
456
+ const res = await fetch(server.url, {
457
+ method: 'POST',
458
+ headers: { 'Content-Type': 'application/json' },
459
+ body: JSON.stringify({
460
+ jsonrpc: "2.0",
461
+ method: "tools/call",
462
+ params: { name: "get_recent_activity", arguments: {} },
463
+ id: Date.now()
464
+ })
160
465
  });
161
- } else if (!process.env.DISPLAY && process.platform === 'linux') {
162
- console.log("šŸ‘‰ [Headless] Please open the URL manually in a browser.");
163
- } else {
164
- console.log("🌐 Launching dashboard in your default browser...");
165
- // By stripping the Chromium-specific '--app' flags and arguments,
166
- // the OS will perfectly route this to Edge, Safari, Firefox, or Chrome.
167
- await open(url);
466
+
467
+ if (res.ok) {
468
+ const data = await res.json();
469
+
470
+ // Parse standard MCP response { result: { content: [{ text: "..." }] } }
471
+ if (data.result && data.result.content && data.result.content.length > 0) {
472
+ const updateText = data.result.content[0].text;
473
+
474
+ if (updateText && updateText.trim() !== "" && updateText.trim() !== "No new activity") {
475
+ fileActivityBuffer += `\n[${new Date().toLocaleTimeString()}] [MCP POLL] Source: ${server.name} | Update: ${updateText}\n`;
476
+ broadcastLocalLog('mcp', `šŸ”— Pulled new data from ${server.name}`);
477
+ hasNewData = true;
478
+ }
479
+ }
480
+ }
481
+ } catch (e) {
482
+ broadcastLocalLog('error', `āš ļø Failed to poll MCP: ${server.name}`);
483
+ }
484
+ }
485
+
486
+ if (hasNewData) {
487
+ const activeProject = getActiveProject();
488
+ if (activeProject && activeProject.path) {
489
+ if (inactivityTimer) clearTimeout(inactivityTimer);
490
+ await syncContext(activeProject.path);
168
491
  }
169
- } catch (e) {
170
- console.log("\nšŸ‘‰ Please open the URL above manually.\n");
171
492
  }
172
493
  }
173
494
 
495
+ // --- CORE FILE WATCHING LOGIC ---
174
496
  async function startWatching(projectPath) {
175
497
  try {
176
498
  if (currentWatcher) await currentWatcher.close();
177
-
178
- console.log(`šŸ‘ļø Watching: ${projectPath}`);
179
499
  broadcastLocalLog('system', `šŸ‘ļø Started watching: ${projectPath}`);
180
-
500
+
501
+ fileActivityBuffer = "";
502
+
503
+ // Start the active polling loop for external services
504
+ if (mcpPollTimer) clearInterval(mcpPollTimer);
505
+ mcpPollTimer = setInterval(pollExternalMCPServers, MCP_POLL_INTERVAL_MS);
506
+
181
507
  currentWatcher = chokidar.watch(projectPath, {
182
- ignored: [/(^|[\/\\])\../, '**/node_modules/**'],
508
+ ignored: [/(^|[\/\\])\../, '**/node_modules/**', '**/dist/**', '**/build/**'],
183
509
  persistent: true,
184
510
  ignoreInitial: true
185
511
  });
186
512
 
187
513
  currentWatcher.on('all', (event, filePath) => {
188
514
  const relativePath = path.relative(projectPath, filePath);
515
+ fileActivityBuffer += `[${new Date().toLocaleTimeString()}] ${event.toUpperCase()}: ${relativePath}\n`;
189
516
  broadcastLocalLog('watch', `[${event.toUpperCase()}] ${relativePath}`);
190
- clearTimeout(syncTimeout);
191
- syncTimeout = setTimeout(() => syncContext(projectPath), 3000);
517
+
518
+ if (inactivityTimer) clearTimeout(inactivityTimer);
519
+ inactivityTimer = setTimeout(() => {
520
+ syncContext(projectPath);
521
+ }, INACTIVITY_DELAY_MS);
192
522
  });
193
- } catch (err) {
194
- console.error(`āŒ Failed: ${err.message}`);
195
- }
523
+ } catch (err) {}
196
524
  }
197
525
 
198
526
  function connectWebSocket() {
527
+ const config = getConfig();
528
+ if (!config.auth || !config.auth.token) return;
529
+
199
530
  try {
200
- globalWs = new WebSocket(`${GATEWAY_URL}?token=YOUR_TOKEN_HERE`);
531
+ if (globalWs) globalWs.close();
532
+ globalWs = new WebSocket(`${GATEWAY_URL}?token=${config.auth.token}`);
201
533
 
202
534
  globalWs.on('error', (err) => {
203
- if (err.code === 'ECONNREFUSED') {
204
- if (!wsRetryLogged) {
205
- console.log('šŸ”“ Cloud Gateway offline (ECONNREFUSED). Will retry silently...');
206
- wsRetryLogged = true;
207
- }
208
- } else {
209
- console.error(`āš ļø Cloud Gateway Error: ${err.message}`);
535
+ if (err.code === 'ECONNREFUSED' && !wsRetryLogged) {
536
+ wsRetryLogged = true;
210
537
  }
211
538
  });
212
539
 
213
540
  globalWs.on('open', () => {
214
- console.log('🟢 Connected to Cloud Gateway.');
215
- wsRetryLogged = false;
216
- const activeProj = getActiveProject();
217
- if (activeProj) syncContext(activeProj.path);
541
+ broadcastLocalLog('success', '🟢 Connected to Cloud Gateway.');
542
+ wsRetryLogged = false;
218
543
  });
219
-
544
+
220
545
  globalWs.on('message', (data) => {
221
546
  try {
222
547
  const msg = JSON.parse(data.toString());
223
- if (msg.type === 'toast') {
224
- console.log(`\nšŸ”” [DIRECTOR]: ${msg.message}`);
225
- broadcastLocalLog('ai', `šŸ”” [AI DIRECTOR] ${msg.message}`);
548
+ if (msg.type === 'toast' || msg.type === 'response') {
549
+ broadcastLocalLog('ai', `šŸ”” [AI]: ${msg.message || msg.text}`);
226
550
  }
551
+ if (msg.type === 'status') broadcastLocalLog('system', `šŸ¤– AI Status: ${msg.status}...`);
552
+ if (msg.should_reload || msg.type === 'reload_project') broadcastLocalLog('reload', `fetch_thrusts`);
227
553
  } catch (err) {}
228
554
  });
229
-
555
+
230
556
  globalWs.on('close', () => {
231
557
  if (!wsRetryLogged) {
232
- console.log('šŸ”“ Disconnected from Cloud Gateway. Retrying silently...');
558
+ broadcastLocalLog('error', 'šŸ”“ Disconnected from Cloud Gateway. Retrying...');
233
559
  wsRetryLogged = true;
234
560
  }
235
- setTimeout(connectWebSocket, 5000);
561
+ if (getConfig().auth?.token) setTimeout(connectWebSocket, 5000);
236
562
  });
237
-
238
- } catch (err) {
239
- setTimeout(connectWebSocket, 5000);
240
- }
563
+ } catch (err) { setTimeout(connectWebSocket, 5000); }
241
564
  }
242
565
 
243
566
  async function syncContext(projectPath) {
244
567
  if (!globalWs || globalWs.readyState !== WebSocket.OPEN) return;
568
+
569
+ const git = simpleGit(projectPath);
570
+ const isRepo = await git.checkIsRepo().catch(() => false);
571
+ if (!isRepo) return;
572
+
245
573
  try {
246
- const git = simpleGit(projectPath);
247
574
  const status = await git.status();
248
575
  const diff = await git.diff();
249
- globalWs.send(JSON.stringify({ type: "context_sync", data: { files_changed: status.modified, diffs: diff } }));
250
- broadcastLocalLog('sync', `āœ… Git context synced to AI.`);
251
- } catch (e) {}
576
+
577
+ if (!fileActivityBuffer.trim() && !diff) return;
578
+
579
+ let newFilesData = "";
580
+ let imagesData = [];
581
+
582
+ const binaryExts =[
583
+ '.png', '.jpg', '.jpeg', '.webp', '.gif', '.svg', '.ico',
584
+ '.pdf', '.zip', '.tar', '.gz', '.mp4', '.mp3', '.wav',
585
+ '.exe', '.dll', '.so', '.dylib',
586
+ '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.bin'
587
+ ];
588
+
589
+ for (const file of status.not_added) {
590
+ const ext = path.extname(file).toLowerCase();
591
+ const fullPath = path.join(projectPath, file);
592
+ if (!fs.existsSync(fullPath)) continue;
593
+
594
+ const stat = fs.statSync(fullPath);
595
+ if (stat.isDirectory() || stat.size > 5 * 1024 * 1024) continue;
596
+
597
+ const fileData = fs.readFileSync(fullPath);
598
+ const isBinary = binaryExts.includes(ext) || isBinaryData(fileData);
599
+
600
+ if (isBinary) {
601
+ const base64 = fileData.toString('base64');
602
+ const mime = ['.png','.jpg','.jpeg','.webp','.gif'].includes(ext)
603
+ ? `image/${ext.replace('.','')}`
604
+ : 'application/octet-stream';
605
+ imagesData.push(`data:${mime};base64,${base64}`);
606
+ } else {
607
+ const content = fileData.toString('utf8');
608
+ newFilesData += `\n--- NEW FILE: ${file} (Scraped at ${new Date().toLocaleTimeString()}) ---\n${content.substring(0, 10000)}\n`;
609
+ }
610
+ }
611
+
612
+ globalWs.send(JSON.stringify({
613
+ type: "context_sync",
614
+ projectId: getActiveProject().id,
615
+ data: {
616
+ buffer: fileActivityBuffer,
617
+ diffs: diff,
618
+ new_files: newFilesData,
619
+ images: imagesData
620
+ }
621
+ }));
622
+
623
+ broadcastLocalLog('sync', `āœ… Context Batch synced to AI.`);
624
+ fileActivityBuffer = "";
625
+
626
+ } catch (e) {
627
+ console.error("Sync error:", e);
628
+ }
252
629
  }