thrust-cli 1.0.15 → 1.0.17

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
@@ -21,8 +21,8 @@ const API_URL = GATEWAY_URL.replace('ws://', 'http://').replace('wss://', 'https
21
21
  const AUTH_PROXY_URL = "https://everydaycats-thrust-auth-server.hf.space";
22
22
 
23
23
  // --- DEBOUNCE & POLLING TIMERS ---
24
- const INACTIVITY_DELAY_MS = 15 * 1000; // Wait for 15 seconds of silence before syncing
25
- 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;
26
26
 
27
27
  let currentWatcher = null;
28
28
  let inactivityTimer = null;
@@ -89,10 +89,10 @@ async function fetchInitialMCPContext(server) {
89
89
  const data = await res.json();
90
90
  if (data.result && data.result.content && data.result.content.length > 0) {
91
91
  const stateText = data.result.content[0].text;
92
-
92
+
93
93
  fileActivityBuffer += `\n[${new Date().toLocaleTimeString()}][MCP BOOT STATE] Source: ${server.name} | Current Context:\n${stateText}\n`;
94
94
  broadcastLocalLog('mcp', `📥 Fetched initial project state from ${server.name}`);
95
-
95
+
96
96
  triggerDebouncedSync();
97
97
  }
98
98
  }
@@ -106,7 +106,7 @@ export async function startDaemon(preferredPort) {
106
106
 
107
107
  const app = express();
108
108
 
109
- attachExternalBridges(app);
109
+ attachExternalBridges(app);
110
110
 
111
111
  const corsOptionsDelegate = (req, callback) => {
112
112
  const origin = req.header('Origin');
@@ -127,7 +127,7 @@ export async function startDaemon(preferredPort) {
127
127
  app.use(cors(corsOptionsDelegate));
128
128
  app.use(express.json());
129
129
 
130
- const frontendPath = path.join(__dirname, '..', 'frontend');
130
+ const frontendPath = path.join(__dirname, '..', 'build');
131
131
 
132
132
  // ==========================================
133
133
  // MCP SERVER ENDPOINTS (FEED-IN PUSH)
@@ -138,12 +138,16 @@ export async function startDaemon(preferredPort) {
138
138
  const token = config.auth?.token;
139
139
  const projectId = config.activeLeadId;
140
140
 
141
+ const prd = req.query.prd || 'true';
142
+ const thrust = req.query.thrust || 'true';
143
+ const timeline = req.query.timeline || 'true';
144
+
141
145
  if (!token || !projectId) {
142
146
  return res.status(401).json({ error: "Thrust agent not linked or authenticated." });
143
147
  }
144
148
 
145
149
  try {
146
- const response = await fetch(`${API_URL}/api/projects/${projectId}/thrusts/active`, {
150
+ const response = await fetch(`${API_URL}/api/projects/${projectId}/mcp-context?prd=${prd}&thrust=${thrust}&timeline=${timeline}`, {
147
151
  headers: { 'Authorization': `Bearer ${token}` }
148
152
  });
149
153
  const data = await response.json();
@@ -151,10 +155,11 @@ export async function startDaemon(preferredPort) {
151
155
  res.json({
152
156
  projectId,
153
157
  projectPath: config.leads[projectId].path,
154
- activeThrust: data.length > 0 ? data[0] : null
155
- //timeline
158
+ prd: data.prd,
159
+ activeThrust: data.thrust,
160
+ timeline: data.timeline
156
161
  });
157
- broadcastLocalLog('mcp', `🔗 [Context Sync] External client requested project state.`);
162
+ broadcastLocalLog('mcp', `🔗 [Context Sync] AI Client requested project state.`);
158
163
  } catch (e) {
159
164
  res.status(502).json({ error: "Failed to fetch context." });
160
165
  }
@@ -193,7 +198,7 @@ export async function startDaemon(preferredPort) {
193
198
  saveConfig(config);
194
199
 
195
200
  pollExternalMCPServers();
196
- fetchInitialMCPContext({ name, url, type: 'http' }); // Fetch state immediately upon linking
201
+ fetchInitialMCPContext({ name, url, type: 'http' });
197
202
  res.json({ success: true });
198
203
  });
199
204
 
@@ -299,7 +304,8 @@ export async function startDaemon(preferredPort) {
299
304
  }
300
305
  });
301
306
 
302
- app.post('/api/tasks/complete', async (req, res) => {
307
+
308
+ app.post('/api/tasks/complete', async (req, res) => {
303
309
  const { taskId, taskTitle } = req.body;
304
310
  const config = getConfig();
305
311
  const token = config.auth?.token;
@@ -320,6 +326,10 @@ export async function startDaemon(preferredPort) {
320
326
  const data = await response.json();
321
327
  if (!response.ok) throw new Error(data.error);
322
328
 
329
+ // 👉 NEW: Force the buffer to have content so syncContext doesn't abort!
330
+ // This tells the AI exactly what you just checked off manually.
331
+ fileActivityBuffer += `\n[${new Date().toLocaleTimeString()}] [USER ACTION] Manually marked task as complete: "${taskTitle}"\n`;
332
+
323
333
  if (config.leads[projectId]?.path) {
324
334
  if (inactivityTimer) clearTimeout(inactivityTimer);
325
335
  syncContext(config.leads[projectId].path);
@@ -528,13 +538,11 @@ async function startWatching(projectPath) {
528
538
 
529
539
  fileActivityBuffer = "";
530
540
 
531
- // Fetch initial MCP states immediately upon watching a project
532
541
  const config = getConfig();
533
542
  if (config.mcpServers && config.mcpServers.length > 0) {
534
543
  config.mcpServers.forEach(server => fetchInitialMCPContext(server));
535
544
  }
536
545
 
537
- // Start the active polling loop for external services
538
546
  if (mcpPollTimer) clearInterval(mcpPollTimer);
539
547
  mcpPollTimer = setInterval(pollExternalMCPServers, MCP_POLL_INTERVAL_MS);
540
548
 
@@ -542,11 +550,37 @@ async function startWatching(projectPath) {
542
550
  ignored:[
543
551
  /(^|[\/\\])\../,
544
552
  '**/node_modules/**', '**/dist/**', '**/build/**',
545
- // Ignore noisy Unity cache & build folders
546
553
  '**/Library/**', '**/Temp/**', '**/Logs/**', '**/obj/**', '**/ProjectSettings/**'
547
554
  ],
548
555
  persistent: true,
549
- ignoreInitial: true
556
+ ignoreInitial: true,
557
+ ignorePermissionErrors: true // NEW: Ignores restricted Android/Termux subfolders safely
558
+ });
559
+
560
+ // NEW: FATAL ERROR HANDLER
561
+ // Catches EACCES constraints before Node.js crashes and forces a UI unlink
562
+ currentWatcher.on('error', async (error) => {
563
+ const isPermissionError = error.code === 'EACCES' || error.code === 'EPERM';
564
+ const msg = isPermissionError
565
+ ? `Permission Denied: Cannot watch folder. Access Denied by OS.`
566
+ : `Watcher Error: ${error.message}`;
567
+
568
+ broadcastLocalLog('error', msg);
569
+
570
+ // Force unlink the bad folder
571
+ const cfg = getConfig();
572
+ cfg.activeLeadId = null;
573
+ saveConfig(cfg);
574
+
575
+ if (inactivityTimer) { clearTimeout(inactivityTimer); inactivityTimer = null; }
576
+ if (mcpPollTimer) { clearInterval(mcpPollTimer); mcpPollTimer = null; }
577
+ if (currentWatcher) {
578
+ await currentWatcher.close().catch(() => {});
579
+ currentWatcher = null;
580
+ }
581
+
582
+ // Signal the frontend to drop the user back to the setup wizard
583
+ broadcastLocalLog('force_unlink', 'Folder access denied. Project unlinked safely.');
550
584
  });
551
585
 
552
586
  currentWatcher.on('all', (event, filePath) => {
@@ -556,7 +590,9 @@ async function startWatching(projectPath) {
556
590
 
557
591
  triggerDebouncedSync();
558
592
  });
559
- } catch (err) {}
593
+ } catch (err) {
594
+ broadcastLocalLog('error', `Failed to initialize watcher: ${err.message}`);
595
+ }
560
596
  }
561
597
 
562
598
  function connectWebSocket() {
@@ -581,12 +617,11 @@ function connectWebSocket() {
581
617
  globalWs.on('message', async (data) => {
582
618
  try {
583
619
  const msg = JSON.parse(data.toString());
584
-
585
- // NEW: Handle dynamic MCP Queries pushed from the AI Director
620
+
586
621
  if (msg.type === 'mcp_query' && msg.payload) {
587
622
  const { serverName, toolName, targetArg } = msg.payload;
588
623
  const targetServer = getConfig().mcpServers?.find(s => s.name.toLowerCase() === serverName.toLowerCase());
589
-
624
+
590
625
  if (targetServer) {
591
626
  broadcastLocalLog('mcp', `🤖 AI requested data from ${serverName} (${toolName})...`);
592
627
  try {
@@ -605,7 +640,7 @@ function connectWebSocket() {
605
640
  }
606
641
  }
607
642
  }
608
-
643
+
609
644
  if (msg.type === 'toast' || msg.type === 'response') {
610
645
  broadcastLocalLog('ai', `🔔 [AI]: ${msg.message || msg.text}`);
611
646
  }