thrust-cli 1.0.9 → 1.0.11

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,267 @@ 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
+ }
99
+
100
+ try {
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
+ });
116
+
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
+ }
123
+
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
+ // CORE DASHBOARD API
171
+ // ==========================================
172
+
173
+ app.get('/api/status', (req, res) => {
174
+ const config = getConfig();
175
+ res.json({ auth: config.auth, activeProject: getActiveProject() });
176
+ });
177
+
178
+ app.get('/api/cloud-projects', async (req, res) => {
179
+ const config = getConfig();
180
+ const token = config.auth?.token;
181
+ if (!token) return res.status(401).json({ error: "Not authenticated" });
182
+
183
+ const page = req.query.page || 1;
184
+ try {
185
+ const response = await fetch(`${API_URL}/api/projects?page=${page}&limit=9`, {
186
+ headers: { 'Authorization': `Bearer ${token}` }
187
+ });
188
+ const data = await response.json();
189
+ if (!response.ok) throw new Error(data.error);
190
+ res.json(data);
191
+ } catch (e) {
192
+ res.status(502).json({ error: "Failed to fetch from Gateway." });
193
+ }
194
+ });
195
+
196
+ app.get('/api/thrusts/active', async (req, res) => {
197
+ const config = getConfig();
198
+ const token = config.auth?.token;
199
+ const projectId = config.activeLeadId;
200
+
201
+ if (!token || !projectId) return res.status(401).json({ error: "Not ready" });
202
+
203
+ try {
204
+ const response = await fetch(`${API_URL}/api/projects/${projectId}/thrusts/active`, {
205
+ headers: { 'Authorization': `Bearer ${token}` }
206
+ });
207
+ const data = await response.json();
208
+ if (!response.ok) throw new Error(data.error);
209
+ res.json(data);
210
+ } catch (e) {
211
+ res.status(502).json({ error: "Failed to fetch current directive." });
212
+ }
213
+ });
214
+
215
+ app.post('/api/tasks/complete', async (req, res) => {
216
+ const { taskId, taskTitle } = req.body;
217
+ const config = getConfig();
218
+ const token = config.auth?.token;
219
+ const projectId = config.activeLeadId;
220
+
221
+ if (!token || !projectId) return res.status(401).json({ error: "Not authenticated" });
222
+
223
+ try {
224
+ const response = await fetch(`${API_URL}/api/projects/${projectId}/tasks/${taskId}/complete`, {
225
+ method: 'POST',
226
+ headers: {
227
+ 'Authorization': `Bearer ${token}`,
228
+ 'Content-Type': 'application/json'
229
+ },
230
+ body: JSON.stringify({ taskTitle })
231
+ });
232
+
233
+ const data = await response.json();
234
+ if (!response.ok) throw new Error(data.error);
235
+
236
+ if (config.leads[projectId]?.path) {
237
+ if (inactivityTimer) clearTimeout(inactivityTimer);
238
+ syncContext(config.leads[projectId].path);
239
+ }
240
+
241
+ res.json(data);
242
+ } catch (e) {
243
+ res.status(502).json({ error: "Failed to sync task completion to cloud." });
244
+ }
245
+ });
246
+
247
+ app.post('/api/auth/callback', async (req, res) => {
248
+ const { tempKey, token, email } = req.body;
249
+ try {
250
+ let finalToken = token;
251
+ if (tempKey && !token) {
252
+ const response = await fetch(`${AUTH_PROXY_URL}/redeem`, {
253
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key: tempKey })
254
+ });
255
+ const data = await response.json();
256
+ if (!data.token) return res.status(401).json({ error: "Invalid Key" });
257
+ finalToken = data.token;
258
+ }
259
+
260
+ if (finalToken) {
261
+ const config = getConfig();
262
+ config.auth = { token: finalToken, email: email || "Authenticated User" };
263
+ saveConfig(config);
264
+ broadcastLocalLog('success', 'āœ… Authentication Successful!');
265
+ connectWebSocket();
266
+ return res.json({ success: true });
267
+ } else {
268
+ return res.status(400).json({ error: "No valid auth data provided" });
269
+ }
270
+ } catch (e) {
271
+ res.status(502).json({ error: "Auth Processing Failed" });
272
+ }
273
+ });
274
+
275
+ app.post('/api/auth/logout', async (req, res) => {
276
+ const config = getConfig();
277
+ const token = config.auth?.token;
278
+ const projectId = config.activeLeadId;
279
+
280
+ if (token && projectId) {
281
+ fetch(`${API_URL}/nullify`, {
282
+ method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ projectId })
283
+ }).catch(() => {});
284
+ }
285
+
286
+ config.auth = { token: null, email: null };
287
+ config.activeLeadId = null;
288
+ saveConfig(config);
289
+
290
+ if (globalWs) { globalWs.close(); globalWs = null; }
291
+ if (currentWatcher) { await currentWatcher.close(); currentWatcher = null; }
292
+ if (inactivityTimer) { clearTimeout(inactivityTimer); inactivityTimer = null; }
293
+ if (mcpPollTimer) { clearInterval(mcpPollTimer); mcpPollTimer = null; }
294
+
295
+ broadcastLocalLog('error', 'šŸ›‘ Logged out. Session cleared.');
296
+ res.json({ success: true });
297
+ });
61
298
 
62
299
  app.get('/api/explore', (req, res) => {
63
300
  try {
64
301
  let targetPath = req.query.path || os.homedir();
65
302
  targetPath = path.resolve(targetPath);
66
-
67
- if (!fs.existsSync(targetPath)) {
68
- return res.status(404).json({ error: "Directory not found" });
69
- }
303
+ if (!fs.existsSync(targetPath)) return res.status(404).json({ error: "Directory not found" });
70
304
 
71
305
  const items = fs.readdirSync(targetPath, { withFileTypes: true });
72
-
73
- const directories = items
74
- .filter(item => item.isDirectory() && !item.name.startsWith('.'))
75
- .map(item => item.name)
76
- .sort();
77
-
306
+ const directories = items.filter(item => item.isDirectory() && !item.name.startsWith('.')).map(item => item.name).sort();
78
307
  const parentPath = path.dirname(targetPath);
79
308
 
80
- res.json({
81
- currentPath: targetPath,
82
- parentPath: targetPath === parentPath ? null : parentPath,
83
- directories: directories
84
- });
85
- } catch (error) {
86
- res.status(403).json({ error: "Permission denied." });
87
- }
309
+ res.json({ currentPath: targetPath, parentPath: targetPath === parentPath ? null : parentPath, directories: directories });
310
+ } catch (error) { res.status(403).json({ error: "Permission denied." }); }
88
311
  });
89
312
 
90
313
  app.post('/api/link', async (req, res) => {
91
314
  const { leadId, folderPath } = req.body;
92
- if (!fs.existsSync(folderPath)) {
93
- return res.status(400).json({ error: "Folder path not found." });
94
- }
315
+ if (!fs.existsSync(folderPath)) return res.status(400).json({ error: "Folder path not found." });
95
316
 
96
317
  const config = getConfig();
97
318
  config.leads[leadId] = { path: folderPath, linkedAt: new Date().toISOString() };
@@ -99,6 +320,10 @@ export async function startDaemon(preferredPort) {
99
320
  saveConfig(config);
100
321
 
101
322
  await startWatching(folderPath);
323
+
324
+ if (inactivityTimer) clearTimeout(inactivityTimer);
325
+ syncContext(folderPath);
326
+
102
327
  res.json({ success: true });
103
328
  });
104
329
 
@@ -110,9 +335,10 @@ export async function startDaemon(preferredPort) {
110
335
  if (currentWatcher) {
111
336
  await currentWatcher.close();
112
337
  currentWatcher = null;
338
+ if (inactivityTimer) { clearTimeout(inactivityTimer); inactivityTimer = null; }
339
+ if (mcpPollTimer) { clearInterval(mcpPollTimer); mcpPollTimer = null; }
113
340
  broadcastLocalLog('system', 'šŸ›‘ Stopped watching.');
114
341
  }
115
-
116
342
  res.json({ success: true });
117
343
  });
118
344
 
@@ -130,141 +356,224 @@ export async function startDaemon(preferredPort) {
130
356
  });
131
357
 
132
358
  localWss = new WebSocketServer({ server });
133
-
134
359
  localWss.on('connection', (ws) => {
135
- ws.send(JSON.stringify({ type: 'system', message: '🟢 Local WebSocket connected.' }));
360
+ ws.send(JSON.stringify({ type: 'system', message: '🟢 Local UI Connected.' }));
136
361
  ws.on('message', (message) => {
137
362
  try {
138
363
  const data = JSON.parse(message.toString());
139
364
  if (data.type === 'frontend_prompt') {
140
- console.log(`\nšŸ’¬ [LOCAL UI]: ${data.payload}`);
141
- broadcastLocalLog('ai', `Daemon received prompt: "${data.payload}"`);
365
+ if (globalWs && globalWs.readyState === WebSocket.OPEN) {
366
+ globalWs.send(JSON.stringify({ type: 'prompt', content: data.payload, projectId: getActiveProject()?.id }));
367
+ broadcastLocalLog('sync', `Prompt sent to Cloud Director...`);
368
+ } else {
369
+ broadcastLocalLog('error', `āš ļø Cannot send: Cloud Gateway is offline.`);
370
+ }
142
371
  }
143
372
  } catch (err) {}
144
373
  });
145
374
  });
146
375
 
147
- connectWebSocket();
148
- const initialProject = getActiveProject();
149
- if (initialProject?.path) {
150
- startWatching(initialProject.path);
376
+ const config = getConfig();
377
+ if (config.auth && config.auth.token) {
378
+ connectWebSocket();
379
+ const initialProject = getActiveProject();
380
+ if (initialProject?.path) {
381
+ startWatching(initialProject.path);
382
+ }
383
+ } else {
384
+ console.log("\nāš ļø Agent is not authenticated. Awaiting connection from Web Dashboard...");
151
385
  }
152
386
  }
153
387
 
154
- // --- INTELLIGENT BROWSER OPENER ---
155
388
  async function safeOpenBrowser(url) {
156
389
  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.");
160
- });
161
- } else if (!process.env.DISPLAY && process.platform === 'linux') {
162
- console.log("šŸ‘‰ [Headless] Please open the URL manually in a browser.");
163
- } else {
164
- const chromeName = process.platform === 'darwin' ? 'google chrome' :
165
- process.platform === 'win32' ? 'chrome' : 'google-chrome';
166
-
167
- console.log("🌐 Attempting to launch dashboard in App Mode...");
168
-
169
- try {
170
- // ATTEMPT 1: Try Chrome App Mode
171
- const browserProcess = await open(url, {
172
- app: { name: chromeName, arguments: [`--app=${url}`, '--window-size=1000,800'] }
173
- });
390
+ if (process.env.TERMUX_VERSION) exec(`termux-open-url ${url}`, () => {});
391
+ else if (!process.env.DISPLAY && process.platform === 'linux') {}
392
+ else { await open(url); }
393
+ } catch (e) {}
394
+ }
174
395
 
175
- // Catch asynchronous spawn failures (e.g., Windows when Chrome isn't in PATH)
176
- browserProcess.on('error', async () => {
177
- console.log("šŸ‘‰ Chrome App Mode failed. Falling back to default browser...");
178
- await open(url).catch(()=>{});
179
- });
396
+ // --- EXTERNAL MCP POLLING LOGIC ---
397
+ async function pollExternalMCPServers() {
398
+ const config = getConfig();
399
+ if (!config.mcpServers || config.mcpServers.length === 0) return;
180
400
 
181
- } catch (chromeErr) {
182
- // Catch synchronous promise rejections (e.g., Mac/Linux when Chrome is missing)
183
- console.log("šŸ‘‰ Chrome App Mode unavailable. Falling back to default browser...");
184
- await open(url);
401
+ let hasNewData = false;
402
+
403
+ for (const server of config.mcpServers) {
404
+ try {
405
+ // Send standard MCP JSON-RPC payload asking for recent activity
406
+ const res = await fetch(server.url, {
407
+ method: 'POST',
408
+ headers: { 'Content-Type': 'application/json' },
409
+ body: JSON.stringify({
410
+ jsonrpc: "2.0",
411
+ method: "tools/call",
412
+ params: { name: "get_recent_activity", arguments: {} },
413
+ id: Date.now()
414
+ })
415
+ });
416
+
417
+ if (res.ok) {
418
+ const data = await res.json();
419
+
420
+ // Parse standard MCP response { result: { content: [{ text: "..." }] } }
421
+ if (data.result && data.result.content && data.result.content.length > 0) {
422
+ const updateText = data.result.content[0].text;
423
+
424
+ if (updateText && updateText.trim() !== "" && updateText.trim() !== "No new activity") {
425
+ fileActivityBuffer += `\n[${new Date().toLocaleTimeString()}] [MCP POLL] Source: ${server.name} | Update: ${updateText}\n`;
426
+ broadcastLocalLog('mcp', `šŸ”— Pulled new data from ${server.name}`);
427
+ hasNewData = true;
428
+ }
429
+ }
185
430
  }
431
+ } catch (e) {
432
+ broadcastLocalLog('error', `āš ļø Failed to poll MCP: ${server.name}`);
433
+ }
434
+ }
435
+
436
+ if (hasNewData) {
437
+ const activeProject = getActiveProject();
438
+ if (activeProject && activeProject.path) {
439
+ if (inactivityTimer) clearTimeout(inactivityTimer);
440
+ await syncContext(activeProject.path);
186
441
  }
187
- } catch (e) {
188
- console.log("\nšŸ‘‰ Please open the URL above manually.\n");
189
442
  }
190
443
  }
191
444
 
445
+ // --- CORE FILE WATCHING LOGIC ---
192
446
  async function startWatching(projectPath) {
193
447
  try {
194
448
  if (currentWatcher) await currentWatcher.close();
195
-
196
- console.log(`šŸ‘ļø Watching: ${projectPath}`);
197
449
  broadcastLocalLog('system', `šŸ‘ļø Started watching: ${projectPath}`);
198
-
450
+
451
+ fileActivityBuffer = "";
452
+
453
+ // Start the active polling loop for external services
454
+ if (mcpPollTimer) clearInterval(mcpPollTimer);
455
+ mcpPollTimer = setInterval(pollExternalMCPServers, MCP_POLL_INTERVAL_MS);
456
+
199
457
  currentWatcher = chokidar.watch(projectPath, {
200
- ignored: [/(^|[\/\\])\../, '**/node_modules/**'],
458
+ ignored: [/(^|[\/\\])\../, '**/node_modules/**', '**/dist/**', '**/build/**'],
201
459
  persistent: true,
202
460
  ignoreInitial: true
203
461
  });
204
462
 
205
463
  currentWatcher.on('all', (event, filePath) => {
206
464
  const relativePath = path.relative(projectPath, filePath);
465
+ fileActivityBuffer += `[${new Date().toLocaleTimeString()}] ${event.toUpperCase()}: ${relativePath}\n`;
207
466
  broadcastLocalLog('watch', `[${event.toUpperCase()}] ${relativePath}`);
208
- clearTimeout(syncTimeout);
209
- syncTimeout = setTimeout(() => syncContext(projectPath), 3000);
467
+
468
+ if (inactivityTimer) clearTimeout(inactivityTimer);
469
+ inactivityTimer = setTimeout(() => {
470
+ syncContext(projectPath);
471
+ }, INACTIVITY_DELAY_MS);
210
472
  });
211
- } catch (err) {
212
- console.error(`āŒ Failed: ${err.message}`);
213
- }
473
+ } catch (err) {}
214
474
  }
215
475
 
216
476
  function connectWebSocket() {
477
+ const config = getConfig();
478
+ if (!config.auth || !config.auth.token) return;
479
+
217
480
  try {
218
- globalWs = new WebSocket(`${GATEWAY_URL}?token=YOUR_TOKEN_HERE`);
481
+ if (globalWs) globalWs.close();
482
+ globalWs = new WebSocket(`${GATEWAY_URL}?token=${config.auth.token}`);
219
483
 
220
484
  globalWs.on('error', (err) => {
221
- if (err.code === 'ECONNREFUSED') {
222
- if (!wsRetryLogged) {
223
- console.log('šŸ”“ Cloud Gateway offline (ECONNREFUSED). Will retry silently...');
224
- wsRetryLogged = true;
225
- }
226
- } else {
227
- console.error(`āš ļø Cloud Gateway Error: ${err.message}`);
485
+ if (err.code === 'ECONNREFUSED' && !wsRetryLogged) {
486
+ wsRetryLogged = true;
228
487
  }
229
488
  });
230
489
 
231
490
  globalWs.on('open', () => {
232
- console.log('🟢 Connected to Cloud Gateway.');
233
- wsRetryLogged = false;
234
- const activeProj = getActiveProject();
235
- if (activeProj) syncContext(activeProj.path);
491
+ broadcastLocalLog('success', '🟢 Connected to Cloud Gateway.');
492
+ wsRetryLogged = false;
236
493
  });
237
-
494
+
238
495
  globalWs.on('message', (data) => {
239
496
  try {
240
497
  const msg = JSON.parse(data.toString());
241
- if (msg.type === 'toast') {
242
- console.log(`\nšŸ”” [DIRECTOR]: ${msg.message}`);
243
- broadcastLocalLog('ai', `šŸ”” [AI DIRECTOR] ${msg.message}`);
498
+ if (msg.type === 'toast' || msg.type === 'response') {
499
+ broadcastLocalLog('ai', `šŸ”” [AI]: ${msg.message || msg.text}`);
244
500
  }
501
+ if (msg.type === 'status') broadcastLocalLog('system', `šŸ¤– AI Status: ${msg.status}...`);
502
+ if (msg.should_reload || msg.type === 'reload_project') broadcastLocalLog('reload', `fetch_thrusts`);
245
503
  } catch (err) {}
246
504
  });
247
-
505
+
248
506
  globalWs.on('close', () => {
249
507
  if (!wsRetryLogged) {
250
- console.log('šŸ”“ Disconnected from Cloud Gateway. Retrying silently...');
508
+ broadcastLocalLog('error', 'šŸ”“ Disconnected from Cloud Gateway. Retrying...');
251
509
  wsRetryLogged = true;
252
510
  }
253
- setTimeout(connectWebSocket, 5000);
511
+ if (getConfig().auth?.token) setTimeout(connectWebSocket, 5000);
254
512
  });
255
-
256
- } catch (err) {
257
- setTimeout(connectWebSocket, 5000);
258
- }
513
+ } catch (err) { setTimeout(connectWebSocket, 5000); }
259
514
  }
260
515
 
261
516
  async function syncContext(projectPath) {
262
517
  if (!globalWs || globalWs.readyState !== WebSocket.OPEN) return;
518
+
519
+ const git = simpleGit(projectPath);
520
+ const isRepo = await git.checkIsRepo().catch(() => false);
521
+ if (!isRepo) return;
522
+
263
523
  try {
264
- const git = simpleGit(projectPath);
265
524
  const status = await git.status();
266
525
  const diff = await git.diff();
267
- globalWs.send(JSON.stringify({ type: "context_sync", data: { files_changed: status.modified, diffs: diff } }));
268
- broadcastLocalLog('sync', `āœ… Git context synced to AI.`);
269
- } catch (e) {}
526
+
527
+ if (!fileActivityBuffer.trim() && !diff) return;
528
+
529
+ let newFilesData = "";
530
+ let imagesData = [];
531
+
532
+ const binaryExts =[
533
+ '.png', '.jpg', '.jpeg', '.webp', '.gif', '.svg', '.ico',
534
+ '.pdf', '.zip', '.tar', '.gz', '.mp4', '.mp3', '.wav',
535
+ '.exe', '.dll', '.so', '.dylib',
536
+ '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.bin'
537
+ ];
538
+
539
+ for (const file of status.not_added) {
540
+ const ext = path.extname(file).toLowerCase();
541
+ const fullPath = path.join(projectPath, file);
542
+ if (!fs.existsSync(fullPath)) continue;
543
+
544
+ const stat = fs.statSync(fullPath);
545
+ if (stat.isDirectory() || stat.size > 5 * 1024 * 1024) continue;
546
+
547
+ const fileData = fs.readFileSync(fullPath);
548
+ const isBinary = binaryExts.includes(ext) || isBinaryData(fileData);
549
+
550
+ if (isBinary) {
551
+ const base64 = fileData.toString('base64');
552
+ const mime = ['.png','.jpg','.jpeg','.webp','.gif'].includes(ext)
553
+ ? `image/${ext.replace('.','')}`
554
+ : 'application/octet-stream';
555
+ imagesData.push(`data:${mime};base64,${base64}`);
556
+ } else {
557
+ const content = fileData.toString('utf8');
558
+ newFilesData += `\n--- NEW FILE: ${file} (Scraped at ${new Date().toLocaleTimeString()}) ---\n${content.substring(0, 10000)}\n`;
559
+ }
560
+ }
561
+
562
+ globalWs.send(JSON.stringify({
563
+ type: "context_sync",
564
+ projectId: getActiveProject().id,
565
+ data: {
566
+ buffer: fileActivityBuffer,
567
+ diffs: diff,
568
+ new_files: newFilesData,
569
+ images: imagesData
570
+ }
571
+ }));
572
+
573
+ broadcastLocalLog('sync', `āœ… Context Batch synced to AI.`);
574
+ fileActivityBuffer = "";
575
+
576
+ } catch (e) {
577
+ console.error("Sync error:", e);
578
+ }
270
579
  }