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/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 +416 -107
- 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,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
|
|
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
|
-
// --- INTELLIGENT BROWSER OPENER ---
|
|
155
388
|
async function safeOpenBrowser(url) {
|
|
156
389
|
try {
|
|
157
|
-
if (process.env.TERMUX_VERSION) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
209
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
}
|