thrust-cli 1.0.15 → 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.
@@ -22,7 +22,7 @@
22
22
  --mcp: #06B6D4;
23
23
  }
24
24
 
25
- body { background-color: var(--bg-main); color: var(--text-main); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 0; }
25
+ body { background-color: var(--bg-main); color: var(--text-main); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 0; overflow-x: hidden; }
26
26
 
27
27
  .navbar { background-color: rgba(15, 15, 22, 0.8); backdrop-filter: blur(10px); border-bottom: 1px solid var(--border-color); padding: 1rem 2rem; display: flex; justify-content: space-between; align-items: center; position: sticky; top: 0; z-index: 100; }
28
28
  .nav-logo { font-size: 1.25rem; font-weight: 900; color: var(--primary); font-style: italic; letter-spacing: -0.5px; display: flex; align-items: center; gap: 8px;}
@@ -78,16 +78,25 @@
78
78
  .directive-text strong { color: var(--text-main); font-weight: 600; }
79
79
 
80
80
  .checklist-container { background-color: var(--bg-panel); border-top: 1px solid var(--border-color); padding: 1.5rem; }
81
- .task-item { display: flex; align-items: flex-start; gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--border-color); }
82
- .task-item:last-child { border-bottom: none; padding-bottom: 0; }
81
+
82
+ .task-item-wrapper { padding: 12px 0; border-bottom: 1px solid var(--border-color); }
83
+ .task-item-wrapper:last-child { border-bottom: none; padding-bottom: 0; }
84
+ .task-item { display: flex; align-items: flex-start; gap: 12px; }
83
85
  .task-checkbox { margin-top: 4px; width: 16px; height: 16px; cursor: pointer; accent-color: var(--primary); }
84
86
  .task-text { font-size: 0.95rem; color: var(--text-main); transition: color 0.3s; }
85
87
  .task-done { text-decoration: line-through; color: var(--text-muted); }
86
88
 
89
+ /* AI QUICK COMMENTS UI */
90
+ .quick-comments { display: flex; gap: 8px; flex-wrap: wrap; }
91
+ .quick-badge { background: rgba(255,255,255,0.05); border: 1px solid var(--border-color); color: var(--text-muted); font-size: 0.75rem; padding: 4px 10px; border-radius: 12px; cursor: pointer; transition: all 0.2s; font-family: inherit; font-weight: 500; }
92
+ .quick-badge:hover { background: rgba(139, 92, 246, 0.1); border-color: var(--primary); color: var(--primary); }
93
+ .task-input { flex: 1; background: var(--bg-hover); border: 1px solid var(--border-color); color: white; padding: 6px 12px; border-radius: 6px; font-size: 0.85rem; outline: none; }
94
+ .task-input:focus { border-color: var(--primary); }
95
+
87
96
  .input-group { margin-bottom: 1.5rem; display: flex; gap: 10px; flex-direction: column; }
88
97
  .input-group label { font-size: 0.85rem; font-weight: bold; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; }
89
- input[type="text"], input[type="url"] { flex-grow: 1; background-color: var(--bg-hover); color: var(--text-main); border: 1px solid var(--border-color); padding: 0.8rem; border-radius: 6px; font-size: 1rem; box-sizing: border-box; }
90
- input[type="text"]:focus, input[type="url"]:focus { outline: none; border-color: var(--primary); }
98
+ input[type="text"], input[type="url"], input[type="password"] { flex-grow: 1; background-color: var(--bg-hover); color: var(--text-main); border: 1px solid var(--border-color); padding: 0.8rem; border-radius: 6px; font-size: 1rem; box-sizing: border-box; }
99
+ input[type="text"]:focus, input[type="url"]:focus, input[type="password"]:focus { outline: none; border-color: var(--primary); }
91
100
 
92
101
  .projects-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 1rem; margin-bottom: 1.5rem; }
93
102
  .project-card { background-color: var(--bg-hover); border: 1px solid var(--border-color); border-radius: 12px; padding: 1rem; cursor: pointer; transition: all 0.2s; display: flex; flex-direction: column; }
@@ -113,15 +122,21 @@
113
122
  .btn-watch:hover { background-color: var(--primary-hover); }
114
123
  .explorer-footer { background-color: var(--bg-panel); padding: 0.75rem 1rem; border-top: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; }
115
124
 
116
- /* MCP LIST UI */
125
+ /* MCP LIST & TABS UI */
117
126
  .mcp-list { margin-bottom: 1.5rem; }
118
127
  .mcp-item { display: flex; justify-content: space-between; align-items: center; background-color: var(--bg-hover); border: 1px solid var(--border-color); padding: 0.8rem 1rem; border-radius: 8px; margin-bottom: 0.5rem; }
119
128
  .mcp-item span { font-weight: bold; color: var(--mcp); }
120
129
  .mcp-item small { color: var(--text-muted); font-family: monospace; margin-left: 10px; }
130
+
131
+ .mcp-tabs { display: flex; gap: 8px; margin-bottom: 12px; }
132
+ .mcp-tab { background: transparent; border: 1px solid var(--border-color); color: var(--text-muted); padding: 8px 16px; border-radius: 8px; cursor: pointer; font-size: 0.85rem; font-weight: bold; transition: all 0.2s; }
133
+ .mcp-tab:hover { background: rgba(255,255,255,0.05); }
134
+ .mcp-tab.active { background: rgba(139, 92, 246, 0.1); color: var(--primary); border-color: var(--primary); }
135
+
121
136
  .mcp-add-form { display: flex; gap: 10px; margin-bottom: 2rem; }
122
137
  .mcp-add-form input { margin: 0; }
123
138
 
124
- .terminal-container { background-color: var(--bg-panel); border: 1px solid var(--border-color); border-radius: 8px; display: flex; flex-direction: column; overflow: hidden; }
139
+ .terminal-container { background-color: var(--bg-panel); border: 1px solid var(--border-color); border-radius: 8px; display: flex; flex-direction: column; overflow: hidden; margin-bottom: 40px; }
125
140
  .log-box { height: 200px; overflow-y: auto; padding: 1rem; font-family: monospace; font-size: 0.85rem; color: var(--text-muted); }
126
141
  .log-box p { margin: 0 0 0.5rem 0; line-height: 1.4; word-break: break-all; }
127
142
  .log-time { color: var(--primary); margin-right: 8px; flex-shrink: 0; }
@@ -131,10 +146,22 @@
131
146
  .prompt-btn:hover { background-color: var(--primary-hover); }
132
147
 
133
148
  .hidden { display: none !important; }
149
+
150
+ /* TOAST NOTIFICATIONS */
151
+ #toast-container { position: fixed; bottom: 24px; right: 24px; z-index: 9999; display: flex; flex-direction: column; gap: 12px; }
152
+ .toast { padding: 14px 20px; border-radius: 10px; color: white; font-weight: 500; font-size: 0.95rem; box-shadow: 0 10px 30px rgba(0,0,0,0.5); transform: translateX(120%); transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); display: flex; align-items: center; gap: 12px; min-width: 250px; max-width: 350px; word-break: break-word; }
153
+ .toast.show { transform: translateX(0); }
154
+ .toast-info { background: var(--primary); border: 1px solid var(--primary-hover); }
155
+ .toast-success { background: var(--success); border: 1px solid #059669; }
156
+ .toast-error { background: var(--danger); border: 1px solid #DC2626; }
157
+ .toast-ai { background: #1A1A24; border: 1px solid var(--primary); }
134
158
  </style>
135
159
  </head>
136
160
  <body>
137
161
 
162
+ <!-- TOAST NOTIFICATION MOUNT -->
163
+ <div id="toast-container"></div>
164
+
138
165
  <nav class="navbar">
139
166
  <div class="nav-logo">⚡ THRUST</div>
140
167
  <div class="profile-wrapper" id="profile-wrapper">
@@ -227,12 +254,17 @@
227
254
 
228
255
  <!-- EXTERNAL MCP INTEGRATIONS -->
229
256
  <h2>3. External Integrations (MCP Clients)</h2>
230
- <p style="font-size: 0.85rem; color: var(--text-muted); margin-bottom: 1rem;">Connect external tools (Figma, Unity, Cursor) by providing their local HTTP MCP URL. The Agent will poll them automatically.</p>
257
+ <p style="font-size: 0.85rem; color: var(--text-muted); margin-bottom: 1rem;">Connect external tools like Figma, GitHub, or local MCPs. The Agent will poll them to maintain context momentum.</p>
231
258
  <div id="mcp-list" class="mcp-list"></div>
232
- <form class="mcp-add-form" onsubmit="addMCPServer(event)">
233
- <input type="text" id="mcp-name" placeholder="Tool Name (e.g., Unity)" required style="flex: 1;">
234
- <input type="url" id="mcp-url" placeholder="http://localhost:8081/mcp" required style="flex: 2;">
235
- <button type="submit" class="btn-primary" style="width: auto;">+ Link Tool</button>
259
+
260
+ <div class="mcp-tabs">
261
+ <button type="button" class="mcp-tab active" onclick="setMcpTab('local', this)">Standard MCP</button>
262
+ <button type="button" class="mcp-tab" onclick="setMcpTab('figma', this)">Figma Bridge</button>
263
+ <button type="button" class="mcp-tab" onclick="setMcpTab('github', this)">GitHub Bridge</button>
264
+ </div>
265
+
266
+ <form class="mcp-add-form" onsubmit="addMCPServer(event)" id="mcp-form">
267
+ <!-- Rendered Dynamically -->
236
268
  </form>
237
269
  </div>
238
270
 
@@ -252,6 +284,48 @@
252
284
  let currentFolderName = "";
253
285
  let localWs = null;
254
286
  let currentPage = 1;
287
+ let currentMcpType = 'local';
288
+
289
+ // --- AUDIO ENGINE ---
290
+ function playNotificationSound() {
291
+ try {
292
+ const ctx = new (window.AudioContext || window.webkitAudioContext)();
293
+ const osc = ctx.createOscillator();
294
+ const gain = ctx.createGain();
295
+ osc.connect(gain);
296
+ gain.connect(ctx.destination);
297
+ osc.type = 'sine';
298
+ osc.frequency.setValueAtTime(880, ctx.currentTime);
299
+ osc.frequency.exponentialRampToValueAtTime(1760, ctx.currentTime + 0.1);
300
+ gain.gain.setValueAtTime(0.05, ctx.currentTime);
301
+ gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.3);
302
+ osc.start();
303
+ osc.stop(ctx.currentTime + 0.3);
304
+ } catch (e) {} // Failsafe for auto-play policy blocks
305
+ }
306
+
307
+ // --- TOAST UI ---
308
+ function showToast(message, type = "info") {
309
+ playNotificationSound();
310
+ const container = document.getElementById('toast-container');
311
+ const toast = document.createElement('div');
312
+ toast.className = `toast toast-${type}`;
313
+
314
+ let icon = "💡";
315
+ if(type === 'success') icon = "✅";
316
+ if(type === 'error') icon = "❌";
317
+ if(type === 'ai') icon = "🤖";
318
+
319
+ toast.innerHTML = `<span style="font-size: 1.2rem;">${icon}</span> <span>${message}</span>`;
320
+ container.appendChild(toast);
321
+
322
+ setTimeout(() => toast.classList.add('show'), 10);
323
+
324
+ setTimeout(() => {
325
+ toast.classList.remove('show');
326
+ setTimeout(() => toast.remove(), 300);
327
+ }, 5000);
328
+ }
255
329
 
256
330
  function addLog(message, color = "var(--text-muted)") {
257
331
  const p = document.createElement('p');
@@ -289,34 +363,57 @@
289
363
  } catch(e) {}
290
364
  }
291
365
 
292
- async function completeTask(taskId, taskTitle, checkboxElem) {
366
+ // --- UPDATED COMPLETION LOGIC FOR QUICK REPLIES ---
367
+ async function completeTask(taskId, taskTitle, checkboxElem, comment = null) {
368
+ if (!checkboxElem) checkboxElem = document.getElementById(`cb-${taskId}`);
293
369
  checkboxElem.disabled = true;
370
+ checkboxElem.checked = true;
371
+
294
372
  const textSpan = checkboxElem.nextElementSibling;
295
- textSpan.classList.add('task-done');
373
+ if(textSpan) textSpan.classList.add('task-done');
374
+
375
+ // Hide the quick comment UI once completed
376
+ const qcContainer = document.getElementById(`qc-${taskId}`);
377
+ if (qcContainer) qcContainer.style.display = 'none';
378
+
379
+ // Extract comment from the input box if a badge wasn't explicitly clicked
380
+ let finalComment = comment;
381
+ if (finalComment === null || finalComment === undefined) {
382
+ const inputElem = document.getElementById(`input-${taskId}`);
383
+ if (inputElem && inputElem.value.trim() !== '') {
384
+ finalComment = inputElem.value.trim();
385
+ }
386
+ }
387
+
388
+ // Append comment for the AI timeline if it exists
389
+ const finalTitle = finalComment ? `${taskTitle} (Response: ${finalComment})` : taskTitle;
296
390
 
297
391
  try {
298
392
  const res = await fetch('/api/tasks/complete', {
299
393
  method: 'POST',
300
394
  headers: { 'Content-Type': 'application/json' },
301
- body: JSON.stringify({ taskId, taskTitle })
395
+ body: JSON.stringify({ taskId, taskTitle: finalTitle })
302
396
  });
303
397
 
304
398
  if (res.ok) {
305
- addLog(`✅ Task marked complete: ${taskTitle}`, 'var(--success)');
399
+ addLog(`✅ Task marked complete: ${finalTitle}`, 'var(--success)');
306
400
  } else {
307
401
  throw new Error("API Failed");
308
402
  }
309
403
  } catch(e) {
310
404
  checkboxElem.disabled = false;
311
- textSpan.classList.remove('task-done');
405
+ checkboxElem.checked = false;
406
+ if(textSpan) textSpan.classList.remove('task-done');
407
+ if (qcContainer) qcContainer.style.display = 'block';
312
408
  addLog(`❌ Failed to sync task completion.`, 'var(--danger)');
409
+ showToast("Failed to sync task", "error");
313
410
  }
314
411
  }
315
412
 
316
413
  function requestNewThrust() {
317
414
  const overridePrompt = `[SYSTEM OVERRIDE]: The user has requested the next Thrust. Evaluate the current Timeline, PRD, and recent Git diffs. Check if the previous Thrust tasks are completed. Generate a new <thrust_create> with the next 1-4 immediate steps to keep momentum going.`;
318
415
 
319
- if (localWs && localWs.readyState === 1) {
416
+ if (localWs && localWs.readyState === 1) {
320
417
  localWs.send(JSON.stringify({ type: 'frontend_prompt', payload: overridePrompt }));
321
418
  addLog(`⚡ Override triggered. Waking up AI Director...`, 'var(--warning)');
322
419
  } else {
@@ -366,16 +463,46 @@
366
463
 
367
464
  const tasksBox = document.getElementById('dir-tasks');
368
465
  tasksBox.innerHTML = '';
466
+
369
467
  if (thrust.thrust_tasks && thrust.thrust_tasks.length > 0) {
370
468
  thrust.thrust_tasks.forEach(task => {
371
- const isDone = task.is_completed;
469
+ const isDone = task.is_completed || task.status === 'done';
372
470
  const safeTitle = task.title.replace(/'/g, "\\'").replace(/"/g, '&quot;');
373
471
 
472
+ let commentsHtml = '';
473
+ if (!isDone) {
474
+ // NEW: Dynamically parse the options/placeholder generated by the AI
475
+ const hasOptions = task.options && task.options.length > 0;
476
+ const placeholder = task.placeholder || "Add a note or comment...";
477
+
478
+ if (hasOptions) {
479
+ let badgesHtml = task.options.map(qc => {
480
+ const safeQc = qc.replace(/'/g, "\\'");
481
+ return `<button type="button" class="quick-badge" onclick="document.getElementById('input-${task.id}').value = '${safeQc}'">${qc}</button>`;
482
+ }).join('');
483
+
484
+ commentsHtml = `
485
+ <div id="qc-${task.id}" style="margin-top: 8px; margin-left: 28px;">
486
+ <div class="quick-comments" style="margin-bottom: 8px;">
487
+ ${badgesHtml}
488
+ </div>
489
+ <div style="display: flex; gap: 8px; align-items: center;">
490
+ <input type="text" id="input-${task.id}" placeholder="${placeholder}" class="task-input">
491
+ <button class="btn-small btn-watch" onclick="completeTask('${task.id}', '${safeTitle}', document.getElementById('cb-${task.id}'), document.getElementById('input-${task.id}').value)">Complete</button>
492
+ </div>
493
+ </div>
494
+ `;
495
+ }
496
+ }
497
+
374
498
  tasksBox.innerHTML += `
375
- <div class="task-item">
376
- <input type="checkbox" class="task-checkbox"
377
- ${isDone ? 'checked disabled' : `onchange="completeTask('${task.id}', '${safeTitle}', this)"`}>
378
- <span class="task-text ${isDone ? 'task-done' : ''}">${task.title}</span>
499
+ <div class="task-item-wrapper">
500
+ <div class="task-item">
501
+ <input type="checkbox" id="cb-${task.id}" class="task-checkbox"
502
+ ${isDone ? 'checked disabled' : `onchange="completeTask('${task.id}', '${safeTitle}', this)"`}>
503
+ <span class="task-text ${isDone ? 'task-done' : ''}">${task.title}</span>
504
+ </div>
505
+ ${commentsHtml}
379
506
  </div>
380
507
  `;
381
508
  });
@@ -437,14 +564,41 @@
437
564
  document.getElementById('current-path').scrollIntoView({ behavior: 'smooth', block: 'center' });
438
565
  }
439
566
 
440
- /* MCP CLIENT MANAGEMENT */
567
+ /* DYNAMIC MCP CLIENT MANAGEMENT */
568
+ function setMcpTab(type, btn) {
569
+ document.querySelectorAll('.mcp-tab').forEach(b => b.classList.remove('active'));
570
+ btn.classList.add('active');
571
+ currentMcpType = type;
572
+ const form = document.getElementById('mcp-form');
573
+
574
+ if (type === 'local') {
575
+ form.innerHTML = `
576
+ <input type="text" id="mcp-name" placeholder="Tool Name (e.g., Unity, Cursor)" required style="flex: 1;">
577
+ <input type="url" id="mcp-url" placeholder="http://localhost:8081/mcp" required style="flex: 2;">
578
+ <button type="submit" class="btn-primary" style="width: auto;">+ Link Tool</button>
579
+ `;
580
+ } else if (type === 'figma') {
581
+ form.innerHTML = `
582
+ <input type="text" id="mcp-file" placeholder="Figma File Key" required style="flex: 1;">
583
+ <input type="password" id="mcp-token" placeholder="Figma Access Token" required style="flex: 2;">
584
+ <button type="submit" class="btn-primary" style="width: auto;">+ Link Figma</button>
585
+ `;
586
+ } else if (type === 'github') {
587
+ form.innerHTML = `
588
+ <input type="text" id="mcp-repo" placeholder="Repo (owner/repo)" required style="flex: 1;">
589
+ <input type="password" id="mcp-token" placeholder="GitHub PAT Token" required style="flex: 2;">
590
+ <button type="submit" class="btn-primary" style="width: auto;">+ Link GitHub</button>
591
+ `;
592
+ }
593
+ }
594
+
441
595
  async function fetchMCPServers() {
442
596
  try {
443
597
  const res = await fetch('/api/mcp/servers');
444
598
  const servers = await res.json();
445
599
  const list = document.getElementById('mcp-list');
446
600
  list.innerHTML = '';
447
-
601
+
448
602
  if (servers.length === 0) {
449
603
  list.innerHTML = `<div style="color: var(--text-muted); font-size: 0.85rem;">No external integrations linked.</div>`;
450
604
  return;
@@ -453,7 +607,7 @@
453
607
  servers.forEach((s, index) => {
454
608
  list.innerHTML += `
455
609
  <div class="mcp-item">
456
- <div><span>🔗 ${s.name}</span> <small>${s.url}</small></div>
610
+ <div><span>🔗 ${s.name}</span> <small>${s.url.substring(0, 40)}${s.url.length > 40 ? '...' : ''}</small></div>
457
611
  <button class="btn-outline-danger" onclick="removeMCPServer(${index})">Remove</button>
458
612
  </div>
459
613
  `;
@@ -463,9 +617,25 @@
463
617
 
464
618
  async function addMCPServer(e) {
465
619
  e.preventDefault();
466
- const name = document.getElementById('mcp-name').value;
467
- const url = document.getElementById('mcp-url').value;
468
-
620
+ let name = "";
621
+ let url = "";
622
+ const hostBase = `${window.location.protocol}//${window.location.host}`;
623
+
624
+ if (currentMcpType === 'local') {
625
+ name = document.getElementById('mcp-name').value;
626
+ url = document.getElementById('mcp-url').value;
627
+ } else if (currentMcpType === 'figma') {
628
+ const fileKey = document.getElementById('mcp-file').value;
629
+ const token = document.getElementById('mcp-token').value;
630
+ name = "Figma";
631
+ url = `${hostBase}/api/bridge/figma?fileKey=${encodeURIComponent(fileKey)}&token=${encodeURIComponent(token)}`;
632
+ } else if (currentMcpType === 'github') {
633
+ const repo = document.getElementById('mcp-repo').value;
634
+ const token = document.getElementById('mcp-token').value;
635
+ name = "GitHub";
636
+ url = `${hostBase}/api/bridge/github?repo=${encodeURIComponent(repo)}&token=${encodeURIComponent(token)}`;
637
+ }
638
+
469
639
  try {
470
640
  const res = await fetch('/api/mcp/servers', {
471
641
  method: 'POST',
@@ -473,18 +643,28 @@
473
643
  body: JSON.stringify({ name, url })
474
644
  });
475
645
  if (res.ok) {
476
- document.getElementById('mcp-name').value = '';
477
- document.getElementById('mcp-url').value = '';
646
+ // Reset inputs
647
+ if(document.getElementById('mcp-name')) document.getElementById('mcp-name').value = '';
648
+ if(document.getElementById('mcp-url')) document.getElementById('mcp-url').value = '';
649
+ if(document.getElementById('mcp-file')) document.getElementById('mcp-file').value = '';
650
+ if(document.getElementById('mcp-repo')) document.getElementById('mcp-repo').value = '';
651
+ if(document.getElementById('mcp-token')) document.getElementById('mcp-token').value = '';
652
+
478
653
  fetchMCPServers();
479
654
  addLog(`🔗 Linked External MCP: ${name}`, 'var(--mcp)');
655
+ showToast(`Successfully linked ${name} bridge!`, 'success');
480
656
  }
481
- } catch (err) { addLog('❌ Failed to link MCP.', 'var(--danger)'); }
657
+ } catch (err) {
658
+ addLog('❌ Failed to link MCP.', 'var(--danger)');
659
+ showToast('Failed to link integration.', 'error');
660
+ }
482
661
  }
483
662
 
484
663
  async function removeMCPServer(index) {
485
664
  try {
486
665
  await fetch(`/api/mcp/servers/${index}`, { method: 'DELETE' });
487
666
  fetchMCPServers();
667
+ showToast("Integration removed", "info");
488
668
  } catch (err) {}
489
669
  }
490
670
 
@@ -495,13 +675,24 @@
495
675
 
496
676
  localWs.onmessage = (event) => {
497
677
  const data = JSON.parse(event.data);
678
+
498
679
  if (data.type === 'success' && data.message.includes('Authentication Successful')) fetchStatus();
499
680
  else if (data.type === 'watch') addLog(`👀 ${data.message}`, 'var(--success)');
500
681
  else if (data.type === 'sync') addLog(data.message, 'var(--sync)');
501
- else if (data.type === 'ai') addLog(data.message, 'var(--warning)');
502
682
  else if (data.type === 'mcp') addLog(`${data.message}`, 'var(--mcp)');
503
- else if (data.type === 'success') addLog(data.message, 'var(--success)');
504
- else if (data.type === 'error') addLog(`❌ ${data.message}`, 'var(--danger)');
683
+ else if (data.type === 'success') {
684
+ addLog(data.message, 'var(--success)');
685
+ }
686
+ else if (data.type === 'error') {
687
+ addLog(`❌ ${data.message}`, 'var(--danger)');
688
+ showToast(data.message, 'error');
689
+ }
690
+ else if (data.type === 'ai') {
691
+ addLog(data.message, 'var(--warning)');
692
+ // Clean AI Prefix for the Toast
693
+ let cleanMsg = data.message.replace('🔔 [AI]: ', '');
694
+ showToast(cleanMsg, 'ai');
695
+ }
505
696
  else if (data.type === 'reload') {
506
697
  addLog('🔄 AI updated Directive. Fetching...', 'var(--primary)');
507
698
  fetchDirective();
@@ -538,7 +729,7 @@
538
729
 
539
730
  fetchCloudProjects();
540
731
  fetchDirective();
541
- fetchMCPServers(); // Load the MCP client list
732
+ fetchMCPServers();
542
733
  } else {
543
734
  document.getElementById('unauth-view').classList.remove('hidden');
544
735
  document.getElementById('auth-view').classList.add('hidden');
@@ -612,7 +803,10 @@
612
803
 
613
804
  try {
614
805
  const res = await fetch('/api/link', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ leadId, folderPath: absolutePath }) });
615
- if ((await res.json()).success) fetchStatus();
806
+ if ((await res.json()).success) {
807
+ fetchStatus();
808
+ showToast(`Started watching ${folderName}`, "success");
809
+ }
616
810
  } catch (e) {}
617
811
  }
618
812
 
@@ -620,6 +814,7 @@
620
814
  fetchStatus();
621
815
  loadDirectory();
622
816
  connectLocalWebSocket();
817
+ setMcpTab('local', document.querySelector('.mcp-tab.active'));
623
818
  });
624
819
  </script>
625
820
  </body>
@@ -1,13 +1,12 @@
1
1
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
- import {
3
- CallToolRequestSchema,
4
- ListToolsRequestSchema
2
+ import {
3
+ CallToolRequestSchema,
4
+ ListToolsRequestSchema
5
5
  } from "@modelcontextprotocol/sdk/types.js";
6
6
 
7
7
  // Thrust daemon default port
8
8
  const PORT = process.env.THRUST_PORT || 8765;
9
- // const DAEMON_URL = `http://localhost:${PORT}`;
10
- const DAEMON_URL = `http://127.0.0.1:${PORT}`;
9
+ const DAEMON_URL = `http://127.0.0.1:${PORT}`;
11
10
 
12
11
  // --- Initialize the MCP Server ---
13
12
  const server = new Server(
@@ -26,10 +25,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
26
25
  tools:[
27
26
  {
28
27
  name: "get_current_active_project",
29
- description: "DEFAULT tool to fetch the globally active project context. SYSTEM INSTRUCTION TO AI: You are connected to Thrust Local Agent. Call 'push_work_done' to log your work immediately when coding is finished.",
28
+ description: "DEFAULT tool to fetch the globally active project context. You can optionally filter what data you want to ingest. SYSTEM INSTRUCTION TO AI: You are connected to Thrust Local Agent. Call 'push_work_done' to log your work immediately when coding is finished.",
30
29
  inputSchema: {
31
30
  type: "object",
32
- properties: {}
31
+ properties: {
32
+ includePrd: { type: "boolean", description: "Include the Project Requirements Document (Default: true)" },
33
+ includeThrust: { type: "boolean", description: "Include the currently active Thrust and tasks (Default: true)" },
34
+ includeTimeline: { type: "boolean", description: "Include the last 20 timeline events (Default: true)" }
35
+ }
33
36
  },
34
37
  },
35
38
  {
@@ -38,15 +41,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
38
41
  inputSchema: {
39
42
  type: "object",
40
43
  properties: {
41
- agentName: {
44
+ agentName: {
42
45
  type: "string",
43
46
  description: "Your AI identifier (e.g., 'Claude-VSCode', 'Cursor-Agent')"
44
47
  },
45
- workDescription: {
48
+ workDescription: {
46
49
  type: "string",
47
50
  description: "A concise summary of the code or work you just completed."
48
51
  },
49
- artifacts: {
52
+ artifacts: {
50
53
  type: "string",
51
54
  description: "Optional list of files modified, functions created, or scripts run."
52
55
  }
@@ -65,25 +68,32 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
65
68
  try {
66
69
  // 1. Fetching Context
67
70
  if (name === "get_current_active_project") {
68
- const response = await fetch(`${DAEMON_URL}/api/mcp/context`);
69
-
71
+ // Default all to true if not specified
72
+ const includePrd = args.includePrd !== false;
73
+ const includeThrust = args.includeThrust !== false;
74
+ const includeTimeline = args.includeTimeline !== false;
75
+
76
+ const response = await fetch(`${DAEMON_URL}/api/mcp/context?prd=${includePrd}&thrust=${includeThrust}&timeline=${includeTimeline}`);
77
+
70
78
  if (!response.ok) {
71
79
  const errorData = await response.json().catch(() => ({}));
72
80
  throw new Error(errorData.error || "Thrust daemon returned an error. Ensure you are authenticated in the UI.");
73
81
  }
74
-
82
+
75
83
  const data = await response.json();
76
84
 
77
85
  return {
78
86
  content:[
79
- {
80
- type: "text",
87
+ {
88
+ type: "text",
81
89
  text: JSON.stringify({
82
90
  projectId: data.projectId,
83
91
  projectPath: data.projectPath,
92
+ prd: data.prd || "Not requested.",
84
93
  activeDirective: data.activeThrust || "No active cloud directive found. Proceed with standard file modifications.",
94
+ timeline: data.timeline || "Not requested.",
85
95
  systemInstruction: "You are editing files in the projectPath or something related to the project. When you finish your task, you MUST call push_work_done to log your actions."
86
- }, null, 2)
96
+ }, null, 2)
87
97
  }
88
98
  ]
89
99
  };
@@ -114,14 +124,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
114
124
  const errorData = await response.json().catch(() => ({}));
115
125
  throw new Error(errorData.error || "Failed to push work to Thrust daemon.");
116
126
  }
117
-
127
+
118
128
  const data = await response.json();
119
129
 
120
130
  return {
121
131
  content:[
122
- {
123
- type: "text",
124
- text: `SUCCESS: ${data.message} Work logged by ${agentName}. Timeline updated and Cloud Code Sync 15s debounce triggered in Thrust core.`
132
+ {
133
+ type: "text",
134
+ text: `SUCCESS: ${data.message} Work logged by ${agentName}. Timeline updated and Cloud Code Sync 15s debounce triggered in Thrust core.`
125
135
  }
126
136
  ]
127
137
  };
@@ -133,14 +143,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
133
143
  if (error.cause?.code === 'ECONNREFUSED' || error.message.includes('fetch failed')) {
134
144
  return {
135
145
  content:[
136
- {
137
- type: "text",
138
- text: `ERROR: The Thrust main daemon is not reachable at ${DAEMON_URL}. Please ensure you have run 'thrust' in your terminal and linked a project.`
146
+ {
147
+ type: "text",
148
+ text: `ERROR: The Thrust main daemon is not reachable at ${DAEMON_URL}. Please ensure you have run 'thrust' in your terminal and linked a project.`
139
149
  }
140
150
  ]
141
151
  };
142
152
  }
143
-
153
+
144
154
  return {
145
155
  content:[
146
156
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thrust-cli",
3
- "version": "1.0.15",
3
+ "version": "1.0.16",
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
@@ -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');
@@ -138,12 +138,18 @@ export async function startDaemon(preferredPort) {
138
138
  const token = config.auth?.token;
139
139
  const projectId = config.activeLeadId;
140
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
+
141
146
  if (!token || !projectId) {
142
147
  return res.status(401).json({ error: "Thrust agent not linked or authenticated." });
143
148
  }
144
149
 
145
150
  try {
146
- 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}`, {
147
153
  headers: { 'Authorization': `Bearer ${token}` }
148
154
  });
149
155
  const data = await response.json();
@@ -151,10 +157,11 @@ export async function startDaemon(preferredPort) {
151
157
  res.json({
152
158
  projectId,
153
159
  projectPath: config.leads[projectId].path,
154
- activeThrust: data.length > 0 ? data[0] : null
155
- //timeline
160
+ prd: data.prd,
161
+ activeThrust: data.thrust,
162
+ timeline: data.timeline
156
163
  });
157
- broadcastLocalLog('mcp', `🔗 [Context Sync] External client requested project state.`);
164
+ broadcastLocalLog('mcp', `🔗 [Context Sync] AI Client requested project state.`);
158
165
  } catch (e) {
159
166
  res.status(502).json({ error: "Failed to fetch context." });
160
167
  }
@@ -193,7 +200,7 @@ export async function startDaemon(preferredPort) {
193
200
  saveConfig(config);
194
201
 
195
202
  pollExternalMCPServers();
196
- fetchInitialMCPContext({ name, url, type: 'http' }); // Fetch state immediately upon linking
203
+ fetchInitialMCPContext({ name, url, type: 'http' });
197
204
  res.json({ success: true });
198
205
  });
199
206
 
@@ -581,12 +588,12 @@ function connectWebSocket() {
581
588
  globalWs.on('message', async (data) => {
582
589
  try {
583
590
  const msg = JSON.parse(data.toString());
584
-
591
+
585
592
  // NEW: Handle dynamic MCP Queries pushed from the AI Director
586
593
  if (msg.type === 'mcp_query' && msg.payload) {
587
594
  const { serverName, toolName, targetArg } = msg.payload;
588
595
  const targetServer = getConfig().mcpServers?.find(s => s.name.toLowerCase() === serverName.toLowerCase());
589
-
596
+
590
597
  if (targetServer) {
591
598
  broadcastLocalLog('mcp', `🤖 AI requested data from ${serverName} (${toolName})...`);
592
599
  try {
@@ -605,7 +612,7 @@ function connectWebSocket() {
605
612
  }
606
613
  }
607
614
  }
608
-
615
+
609
616
  if (msg.type === 'toast' || msg.type === 'response') {
610
617
  broadcastLocalLog('ai', `🔔 [AI]: ${msg.message || msg.text}`);
611
618
  }