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/frontend/index.html +480 -343
- package/frontend/index2.html +468 -0
- package/package.json +1 -3
- package/utils/config.js +13 -3
- package/utils/daemon.js +418 -91
- package/utils/daemon2.js +321 -0
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 || "
|
|
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
|
|
22
|
-
let
|
|
23
|
-
let
|
|
24
|
-
let
|
|
25
|
-
let
|
|
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
|
|
53
|
-
origin
|
|
54
|
-
|
|
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
|
-
|
|
81
|
+
|
|
82
|
+
app.use(cors(corsOptionsDelegate));
|
|
83
|
+
app.use(express.json());
|
|
57
84
|
|
|
58
85
|
const frontendPath = path.join(__dirname, '..', 'frontend');
|
|
59
86
|
|
|
60
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
141
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
159
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
191
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
}
|