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.
@@ -0,0 +1,321 @@
1
+ import { WebSocket, WebSocketServer } from 'ws';
2
+ import chokidar from 'chokidar';
3
+ import simpleGit from 'simple-git';
4
+ import express from 'express';
5
+ import open from 'open';
6
+ import path from 'path';
7
+ import net from 'net';
8
+ import fs from 'fs';
9
+ import os from 'os';
10
+ import cors from 'cors';
11
+ import { exec } from 'child_process';
12
+ import { fileURLToPath } from 'url';
13
+ import { getActiveProject, getConfig, saveConfig } from './config.js';
14
+
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = path.dirname(__filename);
17
+
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://'); const AUTH_PROXY_URL = "https://everydaycats-thrust-auth-server.hf.space";
20
+
21
+ let currentWatcher = null;
22
+ let syncTimeout = null;
23
+ let globalWs = null;
24
+ let localWss = null;
25
+ let wsRetryLogged = false;
26
+
27
+ const findAvailablePort = (startPort) => {
28
+ return new Promise((resolve) => {
29
+ const server = net.createServer();
30
+ server.unref();
31
+ server.on('error', () => resolve(findAvailablePort(startPort + 1)));
32
+ server.listen(startPort, () => {
33
+ server.close(() => resolve(startPort));
34
+ });
35
+ });
36
+ };
37
+
38
+ function broadcastLocalLog(type, message) {
39
+ if (!localWss) return;
40
+ const payload = JSON.stringify({ type, message });
41
+ localWss.clients.forEach(client => {
42
+ if (client.readyState === WebSocket.OPEN) {
43
+ client.send(payload);
44
+ }
45
+ });
46
+ }
47
+
48
+ export async function startDaemon(preferredPort) {
49
+ const actualPort = await findAvailablePort(preferredPort);
50
+ const app = express();
51
+
52
+ const corsOptionsDelegate = (req, callback) => {
53
+ const origin = req.header('Origin');
54
+ const allowedWebApps = ['https://thrust.web.app', 'http://localhost:3000'];
55
+ if (req.path.startsWith('/MCP')) {
56
+ callback(null, { origin: true });
57
+ } else if (req.path === '/api/auth/callback') {
58
+ if (!origin || allowedWebApps.includes(origin) || origin === `http://localhost:${actualPort}`) {
59
+ callback(null, { origin: true, credentials: true });
60
+ } else {
61
+ callback(new Error('CORS blocked auth callback'), { origin: false });
62
+ }
63
+ } else {
64
+ callback(null, { origin: `http://localhost:${actualPort}` });
65
+ }
66
+ };
67
+
68
+ app.use(cors(corsOptionsDelegate));
69
+ app.use(express.json());
70
+
71
+ const frontendPath = path.join(__dirname, '..', 'frontend');
72
+
73
+ app.post('/MCP', (req, res) => {
74
+ const { method } = req.body;
75
+ broadcastLocalLog('system', `šŸ¤– [MCP] Call intercepted: ${method}`);
76
+ res.json({ success: true, message: "MCP Endpoint Active" });
77
+ });
78
+
79
+ app.get('/api/status', (req, res) => {
80
+ const config = getConfig();
81
+ res.json({ auth: config.auth, activeProject: getActiveProject() });
82
+ });
83
+
84
+ app.get('/api/cloud-projects', async (req, res) => {
85
+ const config = getConfig();
86
+ const token = config.auth?.token;
87
+ if (!token) return res.status(401).json({ error: "Not authenticated" });
88
+
89
+ const page = req.query.page || 1;
90
+ try {
91
+ const response = await fetch(`${API_URL}/api/projects?page=${page}&limit=9`, {
92
+ headers: { 'Authorization': `Bearer ${token}` }
93
+ });
94
+ const data = await response.json();
95
+ if (!response.ok) throw new Error(data.error);
96
+ res.json(data);
97
+ } catch (e) {
98
+ res.status(502).json({ error: "Failed to fetch from Gateway." });
99
+ }
100
+ });
101
+
102
+ app.post('/api/auth/callback', async (req, res) => {
103
+ // --- NEW: Capture email from payload ---
104
+ const { tempKey, token, email } = req.body;
105
+ try {
106
+ let finalToken = token;
107
+ if (tempKey && !token) {
108
+ const response = await fetch(`${AUTH_PROXY_URL}/redeem`, {
109
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key: tempKey })
110
+ });
111
+ const data = await response.json();
112
+ if (!data.token) return res.status(401).json({ error: "Invalid Key" });
113
+ finalToken = data.token;
114
+ }
115
+
116
+ if (finalToken) {
117
+ const config = getConfig();
118
+ // --- NEW: Save email to config ---
119
+ config.auth = { token: finalToken, email: email || "Authenticated User" };
120
+ saveConfig(config);
121
+ broadcastLocalLog('success', 'āœ… Authentication Successful!');
122
+ connectWebSocket();
123
+ return res.json({ success: true });
124
+ } else {
125
+ return res.status(400).json({ error: "No valid auth data provided" });
126
+ }
127
+ } catch (e) {
128
+ res.status(502).json({ error: "Auth Processing Failed" });
129
+ }
130
+ });
131
+
132
+ app.post('/api/auth/logout', async (req, res) => {
133
+ const config = getConfig();
134
+ const token = config.auth?.token;
135
+ const projectId = config.activeLeadId;
136
+
137
+ if (token && projectId) {
138
+ fetch(`${API_URL}/nullify`, {
139
+ method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ projectId })
140
+ }).catch(() => {});
141
+ }
142
+
143
+ config.auth = { token: null, email: null };
144
+ config.activeLeadId = null;
145
+ saveConfig(config);
146
+
147
+ if (globalWs) { globalWs.close(); globalWs = null; }
148
+ if (currentWatcher) { await currentWatcher.close(); currentWatcher = null; }
149
+
150
+ broadcastLocalLog('error', 'šŸ›‘ Logged out. Session cleared.');
151
+ res.json({ success: true });
152
+ });
153
+
154
+ app.get('/api/explore', (req, res) => {
155
+ try {
156
+ let targetPath = req.query.path || os.homedir();
157
+ targetPath = path.resolve(targetPath);
158
+ if (!fs.existsSync(targetPath)) return res.status(404).json({ error: "Directory not found" });
159
+
160
+ const items = fs.readdirSync(targetPath, { withFileTypes: true });
161
+ const directories = items.filter(item => item.isDirectory() && !item.name.startsWith('.')).map(item => item.name).sort();
162
+ const parentPath = path.dirname(targetPath);
163
+
164
+ res.json({ currentPath: targetPath, parentPath: targetPath === parentPath ? null : parentPath, directories: directories });
165
+ } catch (error) { res.status(403).json({ error: "Permission denied." }); }
166
+ });
167
+
168
+ app.post('/api/link', async (req, res) => {
169
+ const { leadId, folderPath } = req.body;
170
+ if (!fs.existsSync(folderPath)) return res.status(400).json({ error: "Folder path not found." });
171
+
172
+ const config = getConfig();
173
+ config.leads[leadId] = { path: folderPath, linkedAt: new Date().toISOString() };
174
+ config.activeLeadId = leadId;
175
+ saveConfig(config);
176
+
177
+ await startWatching(folderPath);
178
+ syncContext(folderPath);
179
+ res.json({ success: true });
180
+ });
181
+
182
+ app.post('/api/unlink', async (req, res) => {
183
+ const config = getConfig();
184
+ config.activeLeadId = null;
185
+ saveConfig(config);
186
+
187
+ if (currentWatcher) {
188
+ await currentWatcher.close();
189
+ currentWatcher = null;
190
+ broadcastLocalLog('system', 'šŸ›‘ Stopped watching.');
191
+ }
192
+ res.json({ success: true });
193
+ });
194
+
195
+ if (fs.existsSync(frontendPath)) {
196
+ app.use(express.static(frontendPath));
197
+ app.get(/.*$/, (req, res) => res.sendFile(path.join(frontendPath, 'index.html')));
198
+ }
199
+
200
+ const server = app.listen(actualPort, () => {
201
+ const url = `http://localhost:${actualPort}`;
202
+ console.log(`\n========================================`);
203
+ console.log(`šŸš€ Thrust Dashboard is live at: ${url}`);
204
+ console.log(`========================================\n`);
205
+ safeOpenBrowser(url);
206
+ });
207
+
208
+ localWss = new WebSocketServer({ server });
209
+ localWss.on('connection', (ws) => {
210
+ ws.send(JSON.stringify({ type: 'system', message: '🟢 Local UI Connected.' }));
211
+ ws.on('message', (message) => {
212
+ try {
213
+ const data = JSON.parse(message.toString());
214
+ if (data.type === 'frontend_prompt') {
215
+ if (globalWs && globalWs.readyState === WebSocket.OPEN) {
216
+ globalWs.send(JSON.stringify({ type: 'prompt', content: data.payload, projectId: getActiveProject()?.id }));
217
+ broadcastLocalLog('sync', `Prompt sent to Cloud Director...`);
218
+ } else {
219
+ broadcastLocalLog('error', `āš ļø Cannot send: Cloud Gateway is offline.`);
220
+ }
221
+ }
222
+ } catch (err) {}
223
+ });
224
+ });
225
+
226
+ const config = getConfig();
227
+ if (config.auth && config.auth.token) {
228
+ connectWebSocket();
229
+ const initialProject = getActiveProject();
230
+ if (initialProject?.path) {
231
+ startWatching(initialProject.path);
232
+ syncContext(initialProject.path);
233
+ }
234
+ } else {
235
+ console.log("\nāš ļø Agent is not authenticated. Awaiting connection from Web Dashboard...");
236
+ }
237
+ }
238
+
239
+ async function safeOpenBrowser(url) {
240
+ try {
241
+ if (process.env.TERMUX_VERSION) exec(`termux-open-url ${url}`, () => {});
242
+ else if (!process.env.DISPLAY && process.platform === 'linux') {}
243
+ else { await open(url); }
244
+ } catch (e) {}
245
+ }
246
+
247
+ async function startWatching(projectPath) {
248
+ try {
249
+ if (currentWatcher) await currentWatcher.close();
250
+ broadcastLocalLog('system', `šŸ‘ļø Started watching: ${projectPath}`);
251
+
252
+ currentWatcher = chokidar.watch(projectPath, {
253
+ ignored: [/(^|[\/\\])\../, '**/node_modules/**'],
254
+ persistent: true,
255
+ ignoreInitial: true
256
+ });
257
+
258
+ currentWatcher.on('all', (event, filePath) => {
259
+ const relativePath = path.relative(projectPath, filePath);
260
+ broadcastLocalLog('watch', `[${event.toUpperCase()}] ${relativePath}`);
261
+ clearTimeout(syncTimeout);
262
+ syncTimeout = setTimeout(() => syncContext(projectPath), 3000);
263
+ });
264
+ } catch (err) {}
265
+ }
266
+
267
+ function connectWebSocket() {
268
+ const config = getConfig();
269
+ if (!config.auth || !config.auth.token) return;
270
+
271
+ try {
272
+ if (globalWs) globalWs.close();
273
+ globalWs = new WebSocket(`${GATEWAY_URL}?token=${config.auth.token}`);
274
+
275
+ globalWs.on('error', (err) => {
276
+ if (err.code === 'ECONNREFUSED' && !wsRetryLogged) {
277
+ wsRetryLogged = true;
278
+ }
279
+ });
280
+
281
+ globalWs.on('open', () => {
282
+ broadcastLocalLog('success', '🟢 Connected to Cloud Gateway.');
283
+ wsRetryLogged = false;
284
+ const activeProj = getActiveProject();
285
+ if (activeProj) syncContext(activeProj.path);
286
+ });
287
+
288
+ globalWs.on('message', (data) => {
289
+ try {
290
+ const msg = JSON.parse(data.toString());
291
+ if (msg.type === 'toast' || msg.type === 'response') {
292
+ broadcastLocalLog('ai', `šŸ”” [AI]: ${msg.message || msg.text}`);
293
+ }
294
+ if (msg.type === 'status') broadcastLocalLog('system', `šŸ¤– AI Status: ${msg.status}...`);
295
+ } catch (err) {}
296
+ });
297
+
298
+ globalWs.on('close', () => {
299
+ if (!wsRetryLogged) {
300
+ broadcastLocalLog('error', 'šŸ”“ Disconnected from Cloud Gateway. Retrying...');
301
+ wsRetryLogged = true;
302
+ }
303
+ if (getConfig().auth?.token) setTimeout(connectWebSocket, 5000);
304
+ });
305
+ } catch (err) { setTimeout(connectWebSocket, 5000); }
306
+ }
307
+
308
+ async function syncContext(projectPath) {
309
+ if (!globalWs || globalWs.readyState !== WebSocket.OPEN) return;
310
+ try {
311
+ const git = simpleGit(projectPath);
312
+ const isRepo = await git.checkIsRepo().catch(() => false);
313
+ if (!isRepo) return;
314
+
315
+ const status = await git.status();
316
+ const diff = await git.diff();
317
+
318
+ globalWs.send(JSON.stringify({ type: "context_sync", data: { files_changed: status.modified, diffs: diff } }));
319
+ broadcastLocalLog('sync', `āœ… Git state diff synced to AI.`);
320
+ } catch (e) {}
321
+ }