thrust-cli 1.0.10 → 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,123 +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
- // --- TRULY UNIVERSAL 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.");
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
+ }
395
+
396
+ // --- EXTERNAL MCP POLLING LOGIC ---
397
+ async function pollExternalMCPServers() {
398
+ const config = getConfig();
399
+ if (!config.mcpServers || config.mcpServers.length === 0) return;
400
+
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
+ })
160
415
  });
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);
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
+ }
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);
168
441
  }
169
- } catch (e) {
170
- console.log("\nšŸ‘‰ Please open the URL above manually.\n");
171
442
  }
172
443
  }
173
444
 
445
+ // --- CORE FILE WATCHING LOGIC ---
174
446
  async function startWatching(projectPath) {
175
447
  try {
176
448
  if (currentWatcher) await currentWatcher.close();
177
-
178
- console.log(`šŸ‘ļø Watching: ${projectPath}`);
179
449
  broadcastLocalLog('system', `šŸ‘ļø Started watching: ${projectPath}`);
180
-
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
+
181
457
  currentWatcher = chokidar.watch(projectPath, {
182
- ignored: [/(^|[\/\\])\../, '**/node_modules/**'],
458
+ ignored: [/(^|[\/\\])\../, '**/node_modules/**', '**/dist/**', '**/build/**'],
183
459
  persistent: true,
184
460
  ignoreInitial: true
185
461
  });
186
462
 
187
463
  currentWatcher.on('all', (event, filePath) => {
188
464
  const relativePath = path.relative(projectPath, filePath);
465
+ fileActivityBuffer += `[${new Date().toLocaleTimeString()}] ${event.toUpperCase()}: ${relativePath}\n`;
189
466
  broadcastLocalLog('watch', `[${event.toUpperCase()}] ${relativePath}`);
190
- clearTimeout(syncTimeout);
191
- syncTimeout = setTimeout(() => syncContext(projectPath), 3000);
467
+
468
+ if (inactivityTimer) clearTimeout(inactivityTimer);
469
+ inactivityTimer = setTimeout(() => {
470
+ syncContext(projectPath);
471
+ }, INACTIVITY_DELAY_MS);
192
472
  });
193
- } catch (err) {
194
- console.error(`āŒ Failed: ${err.message}`);
195
- }
473
+ } catch (err) {}
196
474
  }
197
475
 
198
476
  function connectWebSocket() {
477
+ const config = getConfig();
478
+ if (!config.auth || !config.auth.token) return;
479
+
199
480
  try {
200
- globalWs = new WebSocket(`${GATEWAY_URL}?token=YOUR_TOKEN_HERE`);
481
+ if (globalWs) globalWs.close();
482
+ globalWs = new WebSocket(`${GATEWAY_URL}?token=${config.auth.token}`);
201
483
 
202
484
  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}`);
485
+ if (err.code === 'ECONNREFUSED' && !wsRetryLogged) {
486
+ wsRetryLogged = true;
210
487
  }
211
488
  });
212
489
 
213
490
  globalWs.on('open', () => {
214
- console.log('🟢 Connected to Cloud Gateway.');
215
- wsRetryLogged = false;
216
- const activeProj = getActiveProject();
217
- if (activeProj) syncContext(activeProj.path);
491
+ broadcastLocalLog('success', '🟢 Connected to Cloud Gateway.');
492
+ wsRetryLogged = false;
218
493
  });
219
-
494
+
220
495
  globalWs.on('message', (data) => {
221
496
  try {
222
497
  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}`);
498
+ if (msg.type === 'toast' || msg.type === 'response') {
499
+ broadcastLocalLog('ai', `šŸ”” [AI]: ${msg.message || msg.text}`);
226
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`);
227
503
  } catch (err) {}
228
504
  });
229
-
505
+
230
506
  globalWs.on('close', () => {
231
507
  if (!wsRetryLogged) {
232
- console.log('šŸ”“ Disconnected from Cloud Gateway. Retrying silently...');
508
+ broadcastLocalLog('error', 'šŸ”“ Disconnected from Cloud Gateway. Retrying...');
233
509
  wsRetryLogged = true;
234
510
  }
235
- setTimeout(connectWebSocket, 5000);
511
+ if (getConfig().auth?.token) setTimeout(connectWebSocket, 5000);
236
512
  });
237
-
238
- } catch (err) {
239
- setTimeout(connectWebSocket, 5000);
240
- }
513
+ } catch (err) { setTimeout(connectWebSocket, 5000); }
241
514
  }
242
515
 
243
516
  async function syncContext(projectPath) {
244
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
+
245
523
  try {
246
- const git = simpleGit(projectPath);
247
524
  const status = await git.status();
248
525
  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) {}
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
+ }
252
579
  }