thrust-cli 1.0.12 → 1.0.13

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/utils/daemon.js +46 -49
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thrust-cli",
3
- "version": "1.0.12",
3
+ "version": "1.0.13",
4
4
  "description": "The local agent for Thrust AI Director",
5
5
  "type": "module",
6
6
  "homepage": "https://thrust.web.app",
package/utils/daemon.js CHANGED
@@ -20,8 +20,8 @@ const API_URL = GATEWAY_URL.replace('ws://', 'http://').replace('wss://', 'https
20
20
  const AUTH_PROXY_URL = "https://everydaycats-thrust-auth-server.hf.space";
21
21
 
22
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)
23
+ const INACTIVITY_DELAY_MS = 15 * 1000; // Wait for 15 seconds of silence across ALL inputs before sending
24
+ const MCP_POLL_INTERVAL_MS = 18 * 1000; // Poll external tools every 18 seconds
25
25
 
26
26
  let currentWatcher = null;
27
27
  let inactivityTimer = null;
@@ -59,6 +59,18 @@ function isBinaryData(buffer) {
59
59
  return false;
60
60
  }
61
61
 
62
+ // --- CENTRAL DEBOUNCER ---
63
+ // Called by local file changes AND MCP events. It resets the 15s timer.
64
+ function triggerDebouncedSync() {
65
+ const activeProject = getActiveProject();
66
+ if (!activeProject || !activeProject.path) return;
67
+
68
+ if (inactivityTimer) clearTimeout(inactivityTimer);
69
+ inactivityTimer = setTimeout(() => {
70
+ syncContext(activeProject.path);
71
+ }, INACTIVITY_DELAY_MS);
72
+ }
73
+
62
74
  export async function startDaemon(preferredPort) {
63
75
  const actualPort = await findAvailablePort(preferredPort);
64
76
  const app = express();
@@ -67,7 +79,7 @@ export async function startDaemon(preferredPort) {
67
79
  const origin = req.header('Origin');
68
80
  const allowedWebApps = ['https://thrust.web.app', 'http://localhost:3000'];
69
81
  if (req.path.startsWith('/api/mcp')) {
70
- callback(null, { origin: true });
82
+ callback(null, { origin: true });
71
83
  } else if (req.path === '/api/auth/callback') {
72
84
  if (!origin || allowedWebApps.includes(origin) || origin === `http://localhost:${actualPort}`) {
73
85
  callback(null, { origin: true, credentials: true });
@@ -102,7 +114,7 @@ export async function startDaemon(preferredPort) {
102
114
  headers: { 'Authorization': `Bearer ${token}` }
103
115
  });
104
116
  const data = await response.json();
105
-
117
+
106
118
  res.json({
107
119
  projectId,
108
120
  projectPath: config.leads[projectId].path,
@@ -114,30 +126,26 @@ export async function startDaemon(preferredPort) {
114
126
  }
115
127
  });
116
128
 
117
- app.post('/api/mcp/timeline', async (req, res) => {
129
+ app.post('/api/mcp/timeline', (req, res) => {
118
130
  const { source, action_type, description, requires_code_sync } = req.body;
119
-
131
+
120
132
  if (!source || !description) {
121
133
  return res.status(400).json({ error: "Malformed MCP payload." });
122
134
  }
123
135
 
124
136
  broadcastLocalLog('mcp', `🔗 [${source} Pushed Event] ${action_type}: ${description}`);
125
137
 
138
+ // Add to buffer and reset the 15-second countdown
126
139
  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
+ triggerDebouncedSync();
127
141
 
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." });
142
+ res.json({ success: true, message: "Timeline event ingested to buffer." });
135
143
  });
136
144
 
137
145
  // ==========================================
138
146
  // MCP CLIENT CONFIGURATION (FEED-IN PULL)
139
147
  // ==========================================
140
-
148
+
141
149
  app.get('/api/mcp/servers', (req, res) => {
142
150
  res.json(getConfig().mcpServers || []);
143
151
  });
@@ -145,13 +153,12 @@ export async function startDaemon(preferredPort) {
145
153
  app.post('/api/mcp/servers', (req, res) => {
146
154
  const { name, url } = req.body;
147
155
  if (!name || !url) return res.status(400).json({ error: "Missing name or url" });
148
-
156
+
149
157
  const config = getConfig();
150
158
  if (!config.mcpServers) config.mcpServers = [];
151
159
  config.mcpServers.push({ name, url, type: 'http' });
152
160
  saveConfig(config);
153
-
154
- // Trigger an immediate poll when a new service is added
161
+
155
162
  pollExternalMCPServers();
156
163
  res.json({ success: true });
157
164
  });
@@ -170,23 +177,20 @@ export async function startDaemon(preferredPort) {
170
177
  // EXTERNAL MCP DIRECT QUERY (AI OR MANUAL)
171
178
  // ==========================================
172
179
  app.post('/api/mcp/query', async (req, res) => {
180
+ const { serverName, toolName, targetArg } = req.body;
181
+ const config = getConfig();
173
182
 
174
- const { serverName, toolName, targetArg } = req.body;
175
- const config = getConfig();
176
-
177
- // Find the requested server by name (e.g., "Unity")
178
183
  const server = config.mcpServers?.find(s => s.name.toLowerCase() === serverName.toLowerCase());
179
-
184
+
180
185
  if (!server) return res.status(404).json({ error: `MCP Server '${serverName}' not found.` });
181
186
 
182
187
  try {
183
- // Build the JSON-RPC Payload
184
188
  const payload = {
185
189
  jsonrpc: "2.0",
186
190
  method: "tools/call",
187
- params: {
188
- name: toolName,
189
- arguments: targetArg ? { target: targetArg } : {}
191
+ params: {
192
+ name: toolName,
193
+ arguments: targetArg ? { target: targetArg } : {}
190
194
  },
191
195
  id: Date.now()
192
196
  };
@@ -198,15 +202,15 @@ export async function startDaemon(preferredPort) {
198
202
  });
199
203
 
200
204
  if (!response.ok) throw new Error("Server responded with error");
201
-
205
+
202
206
  const data = await response.json();
203
207
  const resultText = data.result?.content?.[0]?.text || "No data returned.";
204
-
205
- // Log it locally so the user sees the system thinking
208
+
206
209
  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
210
+
211
+ // Add the query result to the buffer and reset the 15-second countdown
209
212
  fileActivityBuffer += `\n[${new Date().toLocaleTimeString()}] [MCP DIRECT QUERY RESULT] Source: ${serverName} | Tool: ${toolName} | Target: ${targetArg || 'none'} \nResult:\n${resultText}\n`;
213
+ triggerDebouncedSync();
210
214
 
211
215
  res.json({ success: true, data: resultText });
212
216
 
@@ -284,6 +288,7 @@ export async function startDaemon(preferredPort) {
284
288
  if (!response.ok) throw new Error(data.error);
285
289
 
286
290
  if (config.leads[projectId]?.path) {
291
+ // Instantly sync when a task is manually completed to update cloud state fast
287
292
  if (inactivityTimer) clearTimeout(inactivityTimer);
288
293
  syncContext(config.leads[projectId].path);
289
294
  }
@@ -370,10 +375,10 @@ export async function startDaemon(preferredPort) {
370
375
  saveConfig(config);
371
376
 
372
377
  await startWatching(folderPath);
373
-
374
- if (inactivityTimer) clearTimeout(inactivityTimer);
375
- syncContext(folderPath);
376
-
378
+
379
+ // Initial sync on link
380
+ triggerDebouncedSync();
381
+
377
382
  res.json({ success: true });
378
383
  });
379
384
 
@@ -452,7 +457,6 @@ async function pollExternalMCPServers() {
452
457
 
453
458
  for (const server of config.mcpServers) {
454
459
  try {
455
- // Send standard MCP JSON-RPC payload asking for recent activity
456
460
  const res = await fetch(server.url, {
457
461
  method: 'POST',
458
462
  headers: { 'Content-Type': 'application/json' },
@@ -466,11 +470,9 @@ async function pollExternalMCPServers() {
466
470
 
467
471
  if (res.ok) {
468
472
  const data = await res.json();
469
-
470
- // Parse standard MCP response { result: { content: [{ text: "..." }] } }
471
473
  if (data.result && data.result.content && data.result.content.length > 0) {
472
474
  const updateText = data.result.content[0].text;
473
-
475
+
474
476
  if (updateText && updateText.trim() !== "" && updateText.trim() !== "No new activity") {
475
477
  fileActivityBuffer += `\n[${new Date().toLocaleTimeString()}] [MCP POLL] Source: ${server.name} | Update: ${updateText}\n`;
476
478
  broadcastLocalLog('mcp', `🔗 Pulled new data from ${server.name}`);
@@ -479,16 +481,13 @@ async function pollExternalMCPServers() {
479
481
  }
480
482
  }
481
483
  } catch (e) {
482
- broadcastLocalLog('error', `⚠️ Failed to poll MCP: ${server.name}`);
484
+ // Silently fail to avoid spamming the logs if Unity is temporarily closed
483
485
  }
484
486
  }
485
487
 
486
488
  if (hasNewData) {
487
- const activeProject = getActiveProject();
488
- if (activeProject && activeProject.path) {
489
- if (inactivityTimer) clearTimeout(inactivityTimer);
490
- await syncContext(activeProject.path);
491
- }
489
+ // Add to buffer and reset the 15-second countdown
490
+ triggerDebouncedSync();
492
491
  }
493
492
  }
494
493
 
@@ -515,10 +514,8 @@ async function startWatching(projectPath) {
515
514
  fileActivityBuffer += `[${new Date().toLocaleTimeString()}] ${event.toUpperCase()}: ${relativePath}\n`;
516
515
  broadcastLocalLog('watch', `[${event.toUpperCase()}] ${relativePath}`);
517
516
 
518
- if (inactivityTimer) clearTimeout(inactivityTimer);
519
- inactivityTimer = setTimeout(() => {
520
- syncContext(projectPath);
521
- }, INACTIVITY_DELAY_MS);
517
+ // Reset the 15-second countdown
518
+ triggerDebouncedSync();
522
519
  });
523
520
  } catch (err) {}
524
521
  }