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/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; // Wait for 15 seconds of silence before syncing
24
- const MCP_POLL_INTERVAL_MS = 18 * 1000; // Poll external tools every 18 seconds
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 = ['https://thrust.web.app', 'http://localhost:3000'];
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
- const response = await fetch(`${API_URL}/api/projects/${projectId}/thrusts/active`, {
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
- activeThrust: data.length > 0 ? data[0] : null
160
+ prd: data.prd,
161
+ activeThrust: data.thrust,
162
+ timeline: data.timeline
122
163
  });
123
- broadcastLocalLog('mcp', `🔗 [Context Sync] External client requested project state.`);
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
- // Add the query result to the buffer and reset the 15-second countdown
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 if Unity is temporarily closed
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 to prevent infinite debounce loops
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
  }