thrust-cli 1.0.14 → 1.0.16
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/README.md +1 -0
- package/frontend/index.html +232 -37
- package/index.js +35 -11
- package/mcps/ExternalBridge.js +144 -0
- package/mcps/ThrustMCPBridge.cs +78 -24
- package/mcps/core.js +19 -0
- package/mcps/projectMcpServer.js +165 -0
- package/package.json +3 -1
- package/utils/daemon.js +90 -22
package/utils/daemon.js
CHANGED
|
@@ -11,6 +11,7 @@ import cors from 'cors';
|
|
|
11
11
|
import { exec } from 'child_process';
|
|
12
12
|
import { fileURLToPath } from 'url';
|
|
13
13
|
import { getActiveProject, getConfig, saveConfig } from './config.js';
|
|
14
|
+
import { attachExternalBridges } from '../mcps/ExternalBridge.js';
|
|
14
15
|
|
|
15
16
|
const __filename = fileURLToPath(import.meta.url);
|
|
16
17
|
const __dirname = path.dirname(__filename);
|
|
@@ -20,8 +21,8 @@ const API_URL = GATEWAY_URL.replace('ws://', 'http://').replace('wss://', 'https
|
|
|
20
21
|
const AUTH_PROXY_URL = "https://everydaycats-thrust-auth-server.hf.space";
|
|
21
22
|
|
|
22
23
|
// --- DEBOUNCE & POLLING TIMERS ---
|
|
23
|
-
const INACTIVITY_DELAY_MS = 15 * 1000;
|
|
24
|
-
const MCP_POLL_INTERVAL_MS = 18 * 1000;
|
|
24
|
+
const INACTIVITY_DELAY_MS = 15 * 1000;
|
|
25
|
+
const MCP_POLL_INTERVAL_MS = 18 * 1000;
|
|
25
26
|
|
|
26
27
|
let currentWatcher = null;
|
|
27
28
|
let inactivityTimer = null;
|
|
@@ -60,7 +61,6 @@ function isBinaryData(buffer) {
|
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
// --- CENTRAL DEBOUNCER ---
|
|
63
|
-
// Called by local file changes AND MCP events. It resets the 15s timer.
|
|
64
64
|
function triggerDebouncedSync() {
|
|
65
65
|
const activeProject = getActiveProject();
|
|
66
66
|
if (!activeProject || !activeProject.path) return;
|
|
@@ -71,13 +71,46 @@ function triggerDebouncedSync() {
|
|
|
71
71
|
}, INACTIVITY_DELAY_MS);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
// --- EXTERNAL MCP INITIALIZATION (ON BOOT / ON CONNECT) ---
|
|
75
|
+
async function fetchInitialMCPContext(server) {
|
|
76
|
+
try {
|
|
77
|
+
const res = await fetch(server.url, {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: { 'Content-Type': 'application/json' },
|
|
80
|
+
body: JSON.stringify({
|
|
81
|
+
jsonrpc: "2.0",
|
|
82
|
+
method: "tools/call",
|
|
83
|
+
params: { name: "get_initial_state", arguments: {} },
|
|
84
|
+
id: Date.now()
|
|
85
|
+
})
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (res.ok) {
|
|
89
|
+
const data = await res.json();
|
|
90
|
+
if (data.result && data.result.content && data.result.content.length > 0) {
|
|
91
|
+
const stateText = data.result.content[0].text;
|
|
92
|
+
|
|
93
|
+
fileActivityBuffer += `\n[${new Date().toLocaleTimeString()}][MCP BOOT STATE] Source: ${server.name} | Current Context:\n${stateText}\n`;
|
|
94
|
+
broadcastLocalLog('mcp', `📥 Fetched initial project state from ${server.name}`);
|
|
95
|
+
|
|
96
|
+
triggerDebouncedSync();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} catch (e) {
|
|
100
|
+
broadcastLocalLog('error', `⚠️ Failed to fetch initial state from ${server.name}. Is it running?`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
74
104
|
export async function startDaemon(preferredPort) {
|
|
75
105
|
const actualPort = await findAvailablePort(preferredPort);
|
|
106
|
+
|
|
76
107
|
const app = express();
|
|
77
108
|
|
|
109
|
+
attachExternalBridges(app);
|
|
110
|
+
|
|
78
111
|
const corsOptionsDelegate = (req, callback) => {
|
|
79
112
|
const origin = req.header('Origin');
|
|
80
|
-
const allowedWebApps =
|
|
113
|
+
const allowedWebApps =['https://thrust.web.app', 'http://localhost:3000'];
|
|
81
114
|
if (req.path.startsWith('/api/mcp')) {
|
|
82
115
|
callback(null, { origin: true });
|
|
83
116
|
} else if (req.path === '/api/auth/callback') {
|
|
@@ -105,12 +138,18 @@ export async function startDaemon(preferredPort) {
|
|
|
105
138
|
const token = config.auth?.token;
|
|
106
139
|
const projectId = config.activeLeadId;
|
|
107
140
|
|
|
141
|
+
// Extract optional parameters, default to true
|
|
142
|
+
const prd = req.query.prd || 'true';
|
|
143
|
+
const thrust = req.query.thrust || 'true';
|
|
144
|
+
const timeline = req.query.timeline || 'true';
|
|
145
|
+
|
|
108
146
|
if (!token || !projectId) {
|
|
109
147
|
return res.status(401).json({ error: "Thrust agent not linked or authenticated." });
|
|
110
148
|
}
|
|
111
149
|
|
|
112
150
|
try {
|
|
113
|
-
|
|
151
|
+
// Forward parameters to new combined gateway endpoint
|
|
152
|
+
const response = await fetch(`${API_URL}/api/projects/${projectId}/mcp-context?prd=${prd}&thrust=${thrust}&timeline=${timeline}`, {
|
|
114
153
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
115
154
|
});
|
|
116
155
|
const data = await response.json();
|
|
@@ -118,9 +157,11 @@ export async function startDaemon(preferredPort) {
|
|
|
118
157
|
res.json({
|
|
119
158
|
projectId,
|
|
120
159
|
projectPath: config.leads[projectId].path,
|
|
121
|
-
|
|
160
|
+
prd: data.prd,
|
|
161
|
+
activeThrust: data.thrust,
|
|
162
|
+
timeline: data.timeline
|
|
122
163
|
});
|
|
123
|
-
broadcastLocalLog('mcp', `🔗 [Context Sync]
|
|
164
|
+
broadcastLocalLog('mcp', `🔗 [Context Sync] AI Client requested project state.`);
|
|
124
165
|
} catch (e) {
|
|
125
166
|
res.status(502).json({ error: "Failed to fetch context." });
|
|
126
167
|
}
|
|
@@ -135,7 +176,6 @@ export async function startDaemon(preferredPort) {
|
|
|
135
176
|
|
|
136
177
|
broadcastLocalLog('mcp', `🔗 [${source} Pushed Event] ${action_type}: ${description}`);
|
|
137
178
|
|
|
138
|
-
// Add to buffer and reset the 15-second countdown
|
|
139
179
|
fileActivityBuffer += `\n[${new Date().toLocaleTimeString()}] [MCP EXT EVENT] Source: ${source} | Action: ${action_type} | Desc: ${description} | Needs Code Sync: ${requires_code_sync ? 'Yes' : 'No'}\n`;
|
|
140
180
|
triggerDebouncedSync();
|
|
141
181
|
|
|
@@ -147,7 +187,7 @@ export async function startDaemon(preferredPort) {
|
|
|
147
187
|
// ==========================================
|
|
148
188
|
|
|
149
189
|
app.get('/api/mcp/servers', (req, res) => {
|
|
150
|
-
res.json(getConfig().mcpServers ||
|
|
190
|
+
res.json(getConfig().mcpServers ||[]);
|
|
151
191
|
});
|
|
152
192
|
|
|
153
193
|
app.post('/api/mcp/servers', (req, res) => {
|
|
@@ -155,11 +195,12 @@ export async function startDaemon(preferredPort) {
|
|
|
155
195
|
if (!name || !url) return res.status(400).json({ error: "Missing name or url" });
|
|
156
196
|
|
|
157
197
|
const config = getConfig();
|
|
158
|
-
if (!config.mcpServers) config.mcpServers =
|
|
198
|
+
if (!config.mcpServers) config.mcpServers =[];
|
|
159
199
|
config.mcpServers.push({ name, url, type: 'http' });
|
|
160
200
|
saveConfig(config);
|
|
161
201
|
|
|
162
202
|
pollExternalMCPServers();
|
|
203
|
+
fetchInitialMCPContext({ name, url, type: 'http' });
|
|
163
204
|
res.json({ success: true });
|
|
164
205
|
});
|
|
165
206
|
|
|
@@ -208,8 +249,7 @@ export async function startDaemon(preferredPort) {
|
|
|
208
249
|
|
|
209
250
|
broadcastLocalLog('mcp', `⚡ Queried ${serverName} for ${toolName}.`);
|
|
210
251
|
|
|
211
|
-
|
|
212
|
-
fileActivityBuffer += `\n[${new Date().toLocaleTimeString()}] [MCP DIRECT QUERY RESULT] Source: ${serverName} | Tool: ${toolName} | Target: ${targetArg || 'none'} \nResult:\n${resultText}\n`;
|
|
252
|
+
fileActivityBuffer += `\n[${new Date().toLocaleTimeString()}][MCP DIRECT QUERY RESULT] Source: ${serverName} | Tool: ${toolName} | Target: ${targetArg || 'none'} \nResult:\n${resultText}\n`;
|
|
213
253
|
triggerDebouncedSync();
|
|
214
254
|
|
|
215
255
|
res.json({ success: true, data: resultText });
|
|
@@ -288,7 +328,6 @@ export async function startDaemon(preferredPort) {
|
|
|
288
328
|
if (!response.ok) throw new Error(data.error);
|
|
289
329
|
|
|
290
330
|
if (config.leads[projectId]?.path) {
|
|
291
|
-
// Instantly sync when a task is manually completed to update cloud state fast
|
|
292
331
|
if (inactivityTimer) clearTimeout(inactivityTimer);
|
|
293
332
|
syncContext(config.leads[projectId].path);
|
|
294
333
|
}
|
|
@@ -375,8 +414,6 @@ export async function startDaemon(preferredPort) {
|
|
|
375
414
|
saveConfig(config);
|
|
376
415
|
|
|
377
416
|
await startWatching(folderPath);
|
|
378
|
-
|
|
379
|
-
// Initial sync on link
|
|
380
417
|
triggerDebouncedSync();
|
|
381
418
|
|
|
382
419
|
res.json({ success: true });
|
|
@@ -481,7 +518,7 @@ async function pollExternalMCPServers() {
|
|
|
481
518
|
}
|
|
482
519
|
}
|
|
483
520
|
} catch (e) {
|
|
484
|
-
// Silently fail to avoid spamming the logs
|
|
521
|
+
// Silently fail to avoid spamming the logs
|
|
485
522
|
}
|
|
486
523
|
}
|
|
487
524
|
|
|
@@ -498,15 +535,21 @@ async function startWatching(projectPath) {
|
|
|
498
535
|
|
|
499
536
|
fileActivityBuffer = "";
|
|
500
537
|
|
|
538
|
+
// Fetch initial MCP states immediately upon watching a project
|
|
539
|
+
const config = getConfig();
|
|
540
|
+
if (config.mcpServers && config.mcpServers.length > 0) {
|
|
541
|
+
config.mcpServers.forEach(server => fetchInitialMCPContext(server));
|
|
542
|
+
}
|
|
543
|
+
|
|
501
544
|
// Start the active polling loop for external services
|
|
502
545
|
if (mcpPollTimer) clearInterval(mcpPollTimer);
|
|
503
546
|
mcpPollTimer = setInterval(pollExternalMCPServers, MCP_POLL_INTERVAL_MS);
|
|
504
547
|
|
|
505
548
|
currentWatcher = chokidar.watch(projectPath, {
|
|
506
|
-
ignored:
|
|
507
|
-
/(^|[\/\\])\../,
|
|
549
|
+
ignored:[
|
|
550
|
+
/(^|[\/\\])\../,
|
|
508
551
|
'**/node_modules/**', '**/dist/**', '**/build/**',
|
|
509
|
-
// Ignore noisy Unity cache & build folders
|
|
552
|
+
// Ignore noisy Unity cache & build folders
|
|
510
553
|
'**/Library/**', '**/Temp/**', '**/Logs/**', '**/obj/**', '**/ProjectSettings/**'
|
|
511
554
|
],
|
|
512
555
|
persistent: true,
|
|
@@ -542,9 +585,34 @@ function connectWebSocket() {
|
|
|
542
585
|
wsRetryLogged = false;
|
|
543
586
|
});
|
|
544
587
|
|
|
545
|
-
globalWs.on('message', (data) => {
|
|
588
|
+
globalWs.on('message', async (data) => {
|
|
546
589
|
try {
|
|
547
590
|
const msg = JSON.parse(data.toString());
|
|
591
|
+
|
|
592
|
+
// NEW: Handle dynamic MCP Queries pushed from the AI Director
|
|
593
|
+
if (msg.type === 'mcp_query' && msg.payload) {
|
|
594
|
+
const { serverName, toolName, targetArg } = msg.payload;
|
|
595
|
+
const targetServer = getConfig().mcpServers?.find(s => s.name.toLowerCase() === serverName.toLowerCase());
|
|
596
|
+
|
|
597
|
+
if (targetServer) {
|
|
598
|
+
broadcastLocalLog('mcp', `🤖 AI requested data from ${serverName} (${toolName})...`);
|
|
599
|
+
try {
|
|
600
|
+
const response = await fetch(targetServer.url, {
|
|
601
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
602
|
+
body: JSON.stringify({ jsonrpc: "2.0", method: "tools/call", params: { name: toolName, arguments: targetArg ? { target: targetArg } : {} }, id: Date.now() })
|
|
603
|
+
});
|
|
604
|
+
if (response.ok) {
|
|
605
|
+
const responseData = await response.json();
|
|
606
|
+
const resultText = responseData.result?.content?.[0]?.text || "No data returned.";
|
|
607
|
+
fileActivityBuffer += `\n[${new Date().toLocaleTimeString()}] [MCP AI QUERY RESULT] Source: ${serverName} | Tool: ${toolName} | Target: ${targetArg || 'none'} \nResult:\n${resultText}\n`;
|
|
608
|
+
triggerDebouncedSync();
|
|
609
|
+
}
|
|
610
|
+
} catch (e) {
|
|
611
|
+
broadcastLocalLog('error', `⚠️ AI failed to query MCP server: ${serverName}`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
548
616
|
if (msg.type === 'toast' || msg.type === 'response') {
|
|
549
617
|
broadcastLocalLog('ai', `🔔 [AI]: ${msg.message || msg.text}`);
|
|
550
618
|
}
|
|
@@ -573,7 +641,7 @@ async function syncContext(projectPath) {
|
|
|
573
641
|
|
|
574
642
|
let diff = "";
|
|
575
643
|
let newFilesData = "";
|
|
576
|
-
let imagesData =
|
|
644
|
+
let imagesData =[];
|
|
577
645
|
|
|
578
646
|
try {
|
|
579
647
|
const git = simpleGit(projectPath);
|
|
@@ -636,5 +704,5 @@ async function syncContext(projectPath) {
|
|
|
636
704
|
}));
|
|
637
705
|
|
|
638
706
|
broadcastLocalLog('sync', `✅ Context Batch synced to AI.`);
|
|
639
|
-
fileActivityBuffer = "";
|
|
707
|
+
fileActivityBuffer = "";
|
|
640
708
|
}
|