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 CHANGED
@@ -1,3 +1,4 @@
1
1
  **tt-package-demo**
2
2
 
3
3
  A demo package for Total TypeScript.
4
+ # thrust-cli
@@ -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>
package/index.js CHANGED
@@ -8,10 +8,14 @@ import { fileURLToPath } from 'url';
8
8
  import { startDaemon } from './utils/daemon.js';
9
9
  import { getConfig, saveConfig } from './utils/config.js';
10
10
 
11
+ // --- MCP Imports ---
12
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
+ import { server } from "./mcps/projectMcpServer.js";
14
+
11
15
  const __filename = fileURLToPath(import.meta.url);
12
16
  const __dirname = path.dirname(__filename);
13
17
 
14
- let packageJson = { version: "1.0.0" };
18
+ let packageJson = { version: "1.0.15" };
15
19
  try {
16
20
  packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
17
21
  } catch (e) {
@@ -29,7 +33,7 @@ program
29
33
  console.log('🚀 Booting Thrust Agent...');
30
34
 
31
35
  await setupNativeStartup();
32
-
36
+
33
37
  startDaemon(parseInt(options.port, 10));
34
38
  });
35
39
 
@@ -52,10 +56,30 @@ program
52
56
  config.activeLeadId = leadId;
53
57
 
54
58
  saveConfig(config);
55
- console.log(`✅ Successfully linked Lead [${leadId}] to ${absolutePath}`);
59
+ console.log(`✅ Successfully linked Lead[${leadId}] to ${absolutePath}`);
56
60
  console.log(`Type 'thrust' to begin tracking.`);
57
61
  });
58
62
 
63
+ // --- NEW MCP COMMAND ---
64
+ program
65
+ .command('mcp')
66
+ .description('Start the Thrust MCP Server over Stdio for AI clients (Claude, VS Code, Cursor)')
67
+ .action(async () => {
68
+ try {
69
+ // IMPORTANT: We must use console.error here.
70
+ // console.log prints to stdout, which corrupts the JSON-RPC stream the AI expects.
71
+ console.error('Starting Thrust MCP Server over Stdio...');
72
+
73
+ const transport = new StdioServerTransport();
74
+ await server.connect(transport);
75
+
76
+ console.error('Thrust MCP Server is ready and listening.');
77
+ } catch (error) {
78
+ console.error('MCP Server crashed:', error);
79
+ process.exit(1);
80
+ }
81
+ });
82
+
59
83
  program.parse(process.argv);
60
84
 
61
85
  // --- NATIVE, CROSS-PLATFORM OS STARTUP LOGIC ---
@@ -71,7 +95,7 @@ async function setupNativeStartup() {
71
95
  try {
72
96
  const startupFolder = path.join(os.homedir(), 'AppData', 'Roaming', 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup');
73
97
  const batFilePath = path.join(startupFolder, 'thrust-agent.bat');
74
-
98
+
75
99
  // Clean batch script that simply calls the global `thrust` command
76
100
  const batContent = `@echo off\ntitle Thrust Local Agent\necho Starting Thrust...\ntimeout /t 2 /nobreak > NUL\nthrust\npause`;
77
101
 
@@ -81,14 +105,14 @@ async function setupNativeStartup() {
81
105
  } catch (err) {
82
106
  console.log(`⚠️ Failed to create Windows startup script: ${err.message}`);
83
107
  }
84
- }
85
-
108
+ }
109
+
86
110
  // 3. macOS - Requires absolute paths due to empty launchd $PATH
87
111
  else if (process.platform === 'darwin') {
88
112
  try {
89
113
  const launchAgentsDir = path.join(os.homedir(), 'Library', 'LaunchAgents');
90
114
  const plistPath = path.join(launchAgentsDir, 'com.thrust.agent.plist');
91
-
115
+
92
116
  const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
93
117
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
94
118
  <plist version="1.0">
@@ -109,7 +133,7 @@ async function setupNativeStartup() {
109
133
 
110
134
  if (!fs.existsSync(launchAgentsDir)) fs.mkdirSync(launchAgentsDir, { recursive: true });
111
135
  fs.writeFileSync(plistPath, plistContent, 'utf8');
112
-
136
+
113
137
  import('child_process').then(({ exec }) => {
114
138
  exec(`launchctl load -w "${plistPath}"`, () => {});
115
139
  });
@@ -117,8 +141,8 @@ async function setupNativeStartup() {
117
141
  } catch (err) {
118
142
  console.log(`⚠️ Failed to create macOS LaunchAgent: ${err.message}`);
119
143
  }
120
- }
121
-
144
+ }
145
+
122
146
  // 4. Linux - Requires absolute paths for FreeDesktop compliance
123
147
  else if (process.platform === 'linux') {
124
148
  try {
@@ -129,7 +153,7 @@ async function setupNativeStartup() {
129
153
 
130
154
  const autostartDir = path.join(os.homedir(), '.config', 'autostart');
131
155
  const desktopPath = path.join(autostartDir, 'thrust-agent.desktop');
132
-
156
+
133
157
  const desktopContent = `[Desktop Entry]
134
158
  Type=Application
135
159
  Exec="${process.execPath}" "${__filename}"