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