mcp-swarm 1.1.5 → 1.1.6

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/CHANGELOG.md CHANGED
@@ -9,6 +9,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ---
11
11
 
12
+ ## [1.1.6] - 2026-02-10
13
+
14
+ ### What's New
15
+
16
+ #### 🏗️ Hub Architecture Refactoring
17
+ - **Modular services** — Hub refactored from 846-line monolith into clean modules: `types.ts`, `services/events.ts`, `services/tasks.ts`, `services/agents.ts`
18
+ - **Thin entrypoint** — `index.ts` now delegates to services, making the codebase maintainable
19
+ - **Legacy cleanup** — Removed `smartTools.legacy.ts` (144KB dead code)
20
+
21
+ #### 📊 Dashboard 2.0
22
+ - **Chart.js graphs** — Bar chart for tasks over 24h, doughnut chart for agent activity
23
+ - **Pulse Timeline** — Live heartbeat visualization of all connected agents
24
+ - **WebSocket updates** — Replaced `meta http-equiv="refresh"` with WebSocket for real-time updates
25
+ - **Global Swarm Control** — Stop/Resume entire swarm directly from dashboard via Hub API
26
+
27
+ #### 🔒 API Security
28
+ - **X-Swarm-Secret middleware** — All `/api/*` endpoints validate `X-Swarm-Secret` header when `SWARM_AUTH_TOKEN` is set
29
+ - **Rate Limiting** — Built-in 100 requests/IP/minute limiter with `429 Too Many Requests` response
30
+
31
+ #### 🧪 E2E Testing
32
+ - **Full lifecycle test** — Hub → Task → Claim → Release → Lock → Unlock → Stop → Resume
33
+ - **Rate limit test** — Validates the 429 protection works correctly
34
+ - **Vitest-based** — Consistent with existing test suite
35
+
36
+ #### 🦙 Optional Ollama Integration
37
+ - **Local LLM support** — `swarm_booster` now supports `ollama_generate` task type for complex operations
38
+ - **Cost savings** — Use local Ollama models (codellama:7b) instead of expensive API calls
39
+ - **Fully optional** — Without `ollamaUrl` in config, everything works exactly as before
40
+ - **Smart detection** — `can_boost` detects refactoring/optimization tasks when Ollama is available
41
+
42
+ ---
43
+
12
44
  ## [1.1.5] - 2026-02-09
13
45
 
14
46
  ### What's New
package/CHANGELOG.ru.md CHANGED
@@ -9,6 +9,38 @@
9
9
 
10
10
  ---
11
11
 
12
+ ## [1.1.6] - 2026-02-10
13
+
14
+ ### Что нового
15
+
16
+ #### 🏗️ Рефакторинг архитектуры Hub
17
+ - **Модульные сервисы** — Hub рефакторнут из 846-строчного монолита в чистые модули: `types.ts`, `services/events.ts`, `services/tasks.ts`, `services/agents.ts`
18
+ - **Тонкий entrypoint** — `index.ts` теперь делегирует сервисам, код стал поддерживаемым
19
+ - **Удаление legacy** — Удалён `smartTools.legacy.ts` (144KB мёртвого кода)
20
+
21
+ #### 📊 Dashboard 2.0
22
+ - **Chart.js графики** — Bar chart задач за 24ч, doughnut chart активности агентов
23
+ - **Pulse Timeline** — Визуализация heartbeat всех подключённых агентов в реальном времени
24
+ - **WebSocket обновления** — Заменён `meta http-equiv="refresh"` на WebSocket для live-обновлений
25
+ - **Глобальный Swarm Control** — Stop/Resume всего роя прямо из дашборда через Hub API
26
+
27
+ #### 🔒 Безопасность API
28
+ - **X-Swarm-Secret middleware** — Все `/api/*` эндпоинты проверяют заголовок `X-Swarm-Secret` при установке `SWARM_AUTH_TOKEN`
29
+ - **Rate Limiting** — Встроенный лимитер 100 запросов/IP/минуту с ответом `429 Too Many Requests`
30
+
31
+ #### 🧪 E2E Тестирование
32
+ - **Полный lifecycle тест** — Hub → Task → Claim → Release → Lock → Unlock → Stop → Resume
33
+ - **Тест rate limiting** — Проверка корректности защиты 429
34
+ - **На базе Vitest** — Консистентно с существующим тест-сьютом
35
+
36
+ #### 🦙 Опциональная интеграция Ollama
37
+ - **Поддержка локальных LLM** — `swarm_booster` теперь поддерживает тип задач `ollama_generate` для сложных операций
38
+ - **Экономия** — Используйте локальные модели Ollama (codellama:7b) вместо дорогих API-вызовов
39
+ - **Полностью опционально** — Без `ollamaUrl` в конфиге всё работает как раньше
40
+ - **Умное определение** — `can_boost` определяет задачи рефакторинга/оптимизации при доступном Ollama
41
+
42
+ ---
43
+
12
44
  ## [1.1.5] - 2026-02-09
13
45
 
14
46
  ### Что нового
package/README.md CHANGED
@@ -12,9 +12,9 @@
12
12
  <img src="./assets/banner.png" alt="MCP Swarm Banner" width="800" />
13
13
  </p>
14
14
 
15
- # 🐝 MCP Swarm v1.1.5 — Universal AI Agent Coordination Platform
15
+ # 🐝 MCP Swarm v1.1.6 — Universal AI Agent Coordination Platform
16
16
 
17
- > 🐝 **v1.1.5Interactive Telegram Bot:** Create tasks from chat, AI intent matching (RU/EN), push notifications from Hub, Mini App dashboard, inline task management. All **26 Smart Tools** via Remote Bridge. Update: `npm install -g mcp-swarm@latest`
17
+ > 🐝 **v1.1.6Architecture & Security:** Hub refactored into modular services, Dashboard 2.0 with Chart.js graphs and WebSocket live updates, API security (X-Swarm-Secret + Rate Limiting), E2E tests, optional Ollama integration for swarm_booster. Update: `npm install -g mcp-swarm@latest`
18
18
 
19
19
  **MCP Swarm** is a global "nervous system" for your AI assistants. It turns separate agents (Claude, Cursor, Windsurf, OpenCode) into a coordinated team that can work on massive projects without conflicts or context loss.
20
20
 
package/README.ru.md CHANGED
@@ -12,9 +12,9 @@
12
12
  <img src="./assets/banner.png" alt="MCP Swarm Banner" width="800" />
13
13
  </p>
14
14
 
15
- # 🐝 MCP Swarm v1.1.5 — Универсальная Платформа Координации AI-Агентов
15
+ # 🐝 MCP Swarm v1.1.6 — Универсальная Платформа Координации AI-Агентов
16
16
 
17
- > 🐝 **v1.1.5Интерактивный Telegram Bot:** Создание задач из чата, AI-распознавание намерений (РУ/EN), push-уведомления от Hub, Mini App дашборд, inline-управление задачами. Все **26 Smart Tools** через Remote Bridge. Обновитесь: `npm install -g mcp-swarm@latest`
17
+ > 🐝 **v1.1.6Архитектура и Безопасность:** Hub рефакторнут в модульные сервисы, Dashboard 2.0 с Chart.js графиками и WebSocket обновлениями, API безопасность (X-Swarm-Secret + Rate Limiting), E2E тесты, опциональная интеграция Ollama для swarm_booster. Обновитесь: `npm install -g mcp-swarm@latest`
18
18
 
19
19
  **MCP Swarm** — это глобальная «нервная система» для ваших AI-помощников. Она превращает разрозненных агентов (Claude, Cursor, Windsurf, OpenCode) в слаженную команду, способную работать над огромными проектами без конфликтов и потери контекста.
20
20
 
package/dist/dashboard.js CHANGED
@@ -1,25 +1,32 @@
1
1
  /**
2
- * MCP Swarm — Dashboard HTML Generator
2
+ * MCP Swarm — Dashboard 2.0 HTML Generator
3
3
  *
4
- * Generates the interactive web dashboard HTML for the companion control server.
5
- * Extracted from companion.ts for better maintainability.
4
+ * Enhanced dashboard with:
5
+ * - Chart.js graphs (tasks 24h, cost per agent)
6
+ * - Pulse timeline (agent heartbeats)
7
+ * - Global Stop/Resume (via Hub API)
8
+ * - WebSocket live updates (replaces meta-refresh)
6
9
  */
7
10
  export function renderDashboard(data) {
8
- const { agentName, role, isOrchestrator, paused, stop, bridgeConnected, projectId, uptimeStr, pid, logFilePath } = data;
11
+ const { agentName, role, isOrchestrator, paused, stop, bridgeConnected, projectId, uptimeStr, pid, logFilePath, hubUrl } = data;
9
12
  return `<!DOCTYPE html>
10
13
  <html lang="en">
11
14
  <head>
12
15
  <meta charset="UTF-8">
13
16
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
14
- <meta http-equiv="refresh" content="5">
15
- <title>🐝 MCP Swarm — Dashboard</title>
17
+ <title>🐝 MCP Swarm — Dashboard 2.0</title>
18
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
16
19
  <style>
17
20
  * { margin: 0; padding: 0; box-sizing: border-box; }
18
21
  body { font-family: 'Segoe UI', system-ui, sans-serif; background: #0d1117; color: #e6edf3; min-height: 100vh; padding: 2rem; }
19
- .container { max-width: 960px; margin: 0 auto; }
22
+ .container { max-width: 1100px; margin: 0 auto; }
20
23
  h1 { font-size: 2rem; margin-bottom: 0.5rem; }
21
24
  h1 span { color: #58a6ff; }
22
25
  .subtitle { color: #8b949e; margin-bottom: 2rem; font-size: 0.95rem; }
26
+ .live-dot { display: inline-block; width: 8px; height: 8px; background: #3fb950; border-radius: 50%; margin-left: 8px; animation: pulse-dot 2s infinite; }
27
+ @keyframes pulse-dot { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
28
+
29
+ /* Cards */
23
30
  .cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
24
31
  .card { background: #161b22; border: 1px solid #30363d; border-radius: 12px; padding: 1.2rem; transition: border-color 0.2s; }
25
32
  .card:hover { border-color: #58a6ff44; }
@@ -34,6 +41,8 @@ export function renderDashboard(data) {
34
41
  .badge.running { background: #23863633; color: #3fb950; }
35
42
  .badge.paused { background: #d2992233; color: #d29922; }
36
43
  .badge.stopped { background: #f8514933; color: #f85149; }
44
+
45
+ /* Controls */
37
46
  .controls { display: flex; gap: 0.6rem; margin-bottom: 2rem; flex-wrap: wrap; }
38
47
  .btn { padding: 0.5rem 1.2rem; border: 1px solid #30363d; border-radius: 8px; background: #161b22; color: #e6edf3; cursor: pointer; font-size: 0.85rem; font-weight: 500; transition: all 0.15s; }
39
48
  .btn:hover { background: #1f2937; border-color: #58a6ff; transform: translateY(-1px); }
@@ -44,8 +53,26 @@ export function renderDashboard(data) {
44
53
  .btn.warn:hover { background: #d2992222; }
45
54
  .btn.ok { border-color: #3fb950; color: #3fb950; }
46
55
  .btn.ok:hover { background: #3fb95022; }
47
- .toast { position: fixed; bottom: 2rem; right: 2rem; background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 0.8rem 1.2rem; color: #e6edf3; font-size: 0.85rem; opacity: 0; transition: opacity 0.3s; z-index: 99; pointer-events: none; }
48
- .toast.show { opacity: 1; }
56
+
57
+ /* Charts */
58
+ .charts { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 2rem; }
59
+ .chart-box { background: #161b22; border: 1px solid #30363d; border-radius: 12px; padding: 1.2rem; }
60
+ .chart-box h3 { color: #c9d1d9; font-size: 0.9rem; margin-bottom: 0.8rem; }
61
+
62
+ /* Pulse Timeline */
63
+ .timeline { background: #161b22; border: 1px solid #30363d; border-radius: 12px; padding: 1.2rem; margin-bottom: 2rem; }
64
+ .timeline h3 { color: #c9d1d9; font-size: 0.9rem; margin-bottom: 0.8rem; }
65
+ .timeline-items { display: flex; flex-direction: column; gap: 0.4rem; }
66
+ .timeline-item { display: flex; align-items: center; gap: 0.8rem; padding: 0.4rem 0.6rem; border-radius: 6px; background: #0d1117; }
67
+ .timeline-item .dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
68
+ .timeline-item .dot.active { background: #3fb950; box-shadow: 0 0 6px #3fb95066; }
69
+ .timeline-item .dot.idle { background: #d29922; }
70
+ .timeline-item .dot.offline { background: #484f58; }
71
+ .timeline-item .agent-name { font-weight: 600; font-size: 0.85rem; min-width: 120px; }
72
+ .timeline-item .agent-info { color: #8b949e; font-size: 0.8rem; }
73
+ .timeline-item .agent-time { color: #484f58; font-size: 0.75rem; margin-left: auto; }
74
+
75
+ /* Endpoints */
49
76
  .endpoints { margin-top: 1rem; }
50
77
  .endpoints h2 { font-size: 1.1rem; margin-bottom: 0.8rem; color: #c9d1d9; }
51
78
  .ep-list { list-style: none; }
@@ -53,30 +80,39 @@ export function renderDashboard(data) {
53
80
  .ep-list .method { color: #3fb950; min-width: 3.5rem; font-weight: 600; }
54
81
  .ep-list .path { color: #58a6ff; }
55
82
  .ep-list .desc { color: #8b949e; margin-left: auto; }
83
+
84
+ .toast { position: fixed; bottom: 2rem; right: 2rem; background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 0.8rem 1.2rem; color: #e6edf3; font-size: 0.85rem; opacity: 0; transition: opacity 0.3s; z-index: 99; pointer-events: none; }
85
+ .toast.show { opacity: 1; }
56
86
  .footer { margin-top: 2rem; color: #484f58; font-size: 0.8rem; text-align: center; }
57
87
  .footer a { color: #58a6ff; text-decoration: none; }
88
+
89
+ @media (max-width: 768px) {
90
+ .charts { grid-template-columns: 1fr; }
91
+ .cards { grid-template-columns: repeat(2, 1fr); }
92
+ }
58
93
  </style>
59
94
  </head>
60
95
  <body>
61
96
  <div class="container">
62
- <h1>🐝 MCP <span>Swarm</span></h1>
63
- <p class="subtitle">Companion Dashboard — auto-refreshes every 5s</p>
97
+ <h1>🐝 MCP <span>Swarm</span> <span class="live-dot" id="live-dot" title="Live updates"></span></h1>
98
+ <p class="subtitle">Dashboard 2.0 Live WebSocket Updates</p>
99
+
64
100
  <div class="cards">
65
101
  <div class="card">
66
102
  <h3>Agent</h3>
67
- <div class="value blue">${agentName}</div>
103
+ <div class="value blue" id="agent-name">${agentName}</div>
68
104
  </div>
69
105
  <div class="card">
70
106
  <h3>Role</h3>
71
- <div class="value"><span class="badge ${isOrchestrator ? 'orch' : 'exec'}">${role.toUpperCase()}</span></div>
107
+ <div class="value"><span class="badge ${isOrchestrator ? 'orch' : 'exec'}" id="agent-role">${role.toUpperCase()}</span></div>
72
108
  </div>
73
109
  <div class="card">
74
110
  <h3>Status</h3>
75
- <div class="value"><span class="badge ${stop ? 'stopped' : paused ? 'paused' : 'running'}">${stop ? '⏹ STOPPED' : paused ? '⏸ PAUSED' : '▶ RUNNING'}</span></div>
111
+ <div class="value"><span class="badge ${stop ? 'stopped' : paused ? 'paused' : 'running'}" id="agent-status">${stop ? '⏹ STOPPED' : paused ? '⏸ PAUSED' : '▶ RUNNING'}</span></div>
76
112
  </div>
77
113
  <div class="card">
78
114
  <h3>Bridge</h3>
79
- <div class="value ${bridgeConnected ? 'green' : 'yellow'}">${bridgeConnected ? '🌉 Connected' : '⚠ Off'}</div>
115
+ <div class="value ${bridgeConnected ? 'green' : 'yellow'}" id="bridge-status">${bridgeConnected ? '🌉 Connected' : '⚠ Off'}</div>
80
116
  </div>
81
117
  <div class="card">
82
118
  <h3>Project</h3>
@@ -84,7 +120,7 @@ export function renderDashboard(data) {
84
120
  </div>
85
121
  <div class="card">
86
122
  <h3>Uptime</h3>
87
- <div class="value green">${uptimeStr}</div>
123
+ <div class="value green" id="uptime">${uptimeStr}</div>
88
124
  </div>
89
125
  <div class="card">
90
126
  <h3>PID</h3>
@@ -95,11 +131,41 @@ export function renderDashboard(data) {
95
131
  <div class="value" style="font-size:0.7rem;word-break:break-all;color:#8b949e;">${logFilePath}</div>
96
132
  </div>
97
133
  </div>
134
+
135
+ <!-- Controls -->
98
136
  <div class="controls">
99
- <button class="btn ${paused ? 'ok' : 'warn'}" onclick="action('${paused ? 'resume' : 'pause'}')">${paused ? '▶ Resume' : '⏸ Pause'}</button>
137
+ <button class="btn ${paused ? 'ok' : 'warn'}" onclick="action('${paused ? 'resume' : 'pause'}')" id="btn-pause">${paused ? '▶ Resume' : '⏸ Pause'}</button>
100
138
  <button class="btn danger" onclick="if(confirm('Shutdown companion?')) action('stop')">⏹ Shutdown</button>
139
+ <button class="btn ok" id="btn-swarm-resume" onclick="swarmControl('resume')" style="display:${stop ? 'inline-block' : 'none'}">🐝 Resume Swarm</button>
140
+ <button class="btn danger" id="btn-swarm-stop" onclick="if(confirm('Stop entire swarm?')) swarmControl('stop')" style="display:${stop ? 'none' : 'inline-block'}">🐝 Stop Swarm</button>
101
141
  <button class="btn" onclick="copyId()">📋 Copy Project ID</button>
102
142
  </div>
143
+
144
+ <!-- Charts -->
145
+ <div class="charts">
146
+ <div class="chart-box">
147
+ <h3>📊 Tasks Last 24h</h3>
148
+ <canvas id="tasksChart" height="200"></canvas>
149
+ </div>
150
+ <div class="chart-box">
151
+ <h3>🤖 Agents Activity</h3>
152
+ <canvas id="agentsChart" height="200"></canvas>
153
+ </div>
154
+ </div>
155
+
156
+ <!-- Pulse Timeline -->
157
+ <div class="timeline">
158
+ <h3>💓 Agent Pulse Timeline</h3>
159
+ <div class="timeline-items" id="pulse-timeline">
160
+ <div class="timeline-item">
161
+ <div class="dot active"></div>
162
+ <span class="agent-name">${agentName}</span>
163
+ <span class="agent-info">${role} • ${bridgeConnected ? 'bridge connected' : 'local only'}</span>
164
+ <span class="agent-time">now</span>
165
+ </div>
166
+ </div>
167
+ </div>
168
+
103
169
  <div class="endpoints">
104
170
  <h2>📡 API Endpoints</h2>
105
171
  <ul class="ep-list">
@@ -113,17 +179,20 @@ export function renderDashboard(data) {
113
179
  </ul>
114
180
  </div>
115
181
  <div class="footer">
116
- MCP Swarm v1.1 • <a href="https://github.com/AbdrAbdr/MCP-Swarm" target="_blank">GitHub</a> • <a href="https://www.npmjs.com/package/mcp-swarm" target="_blank">npm</a>
182
+ MCP Swarm v1.1.6 • <a href="https://github.com/AbdrAbdr/MCP-Swarm" target="_blank">GitHub</a> • <a href="https://www.npmjs.com/package/mcp-swarm" target="_blank">npm</a>
117
183
  </div>
118
184
  </div>
119
185
  <div class="toast" id="toast"></div>
120
186
  <script>
187
+ // ==================== Toast ====================
121
188
  function toast(msg) {
122
189
  const t = document.getElementById('toast');
123
190
  t.textContent = msg;
124
191
  t.classList.add('show');
125
192
  setTimeout(() => t.classList.remove('show'), 2500);
126
193
  }
194
+
195
+ // ==================== Local Companion Actions ====================
127
196
  async function action(act) {
128
197
  try {
129
198
  const r = await fetch('/' + act, { method: 'POST' });
@@ -132,10 +201,172 @@ export function renderDashboard(data) {
132
201
  if (act !== 'stop') setTimeout(() => location.reload(), 500);
133
202
  } catch(e) { toast('Error: ' + e.message); }
134
203
  }
204
+
205
+ // ==================== Global Swarm Control via Hub ====================
206
+ async function swarmControl(act) {
207
+ try {
208
+ const r = await fetch('/hub/' + act, { method: 'POST' });
209
+ const j = await r.json();
210
+ toast(j.ok ? 'Swarm ' + act + ' OK ✅' : 'Error: ' + (j.message || 'unknown'));
211
+ setTimeout(() => location.reload(), 500);
212
+ } catch(e) { toast('Error: ' + e.message); }
213
+ }
214
+
135
215
  function copyId() {
136
216
  const id = document.getElementById('project-id').textContent;
137
217
  navigator.clipboard.writeText(id).then(() => toast('Copied: ' + id)).catch(() => toast('Copy failed'));
138
218
  }
219
+
220
+ // ==================== Charts ====================
221
+ const chartDefaults = {
222
+ responsive: true,
223
+ maintainAspectRatio: false,
224
+ plugins: { legend: { labels: { color: '#8b949e', font: { size: 11 } } } },
225
+ scales: {
226
+ x: { ticks: { color: '#484f58' }, grid: { color: '#21262d' } },
227
+ y: { ticks: { color: '#484f58' }, grid: { color: '#21262d' }, beginAtZero: true }
228
+ }
229
+ };
230
+
231
+ // Tasks Chart (placeholder data — will be filled by WebSocket)
232
+ const tasksCtx = document.getElementById('tasksChart').getContext('2d');
233
+ const tasksChart = new Chart(tasksCtx, {
234
+ type: 'bar',
235
+ data: {
236
+ labels: ['6h ago', '5h', '4h', '3h', '2h', '1h', 'Now'],
237
+ datasets: [{
238
+ label: 'Created',
239
+ data: [0, 0, 0, 0, 0, 0, 0],
240
+ backgroundColor: '#58a6ff66',
241
+ borderColor: '#58a6ff',
242
+ borderWidth: 1,
243
+ borderRadius: 4,
244
+ }, {
245
+ label: 'Completed',
246
+ data: [0, 0, 0, 0, 0, 0, 0],
247
+ backgroundColor: '#3fb95066',
248
+ borderColor: '#3fb950',
249
+ borderWidth: 1,
250
+ borderRadius: 4,
251
+ }]
252
+ },
253
+ options: chartDefaults
254
+ });
255
+
256
+ // Agents Activity (pie/doughnut)
257
+ const agentsCtx = document.getElementById('agentsChart').getContext('2d');
258
+ const agentsChart = new Chart(agentsCtx, {
259
+ type: 'doughnut',
260
+ data: {
261
+ labels: ['Active', 'Idle', 'Offline'],
262
+ datasets: [{
263
+ data: [1, 0, 0],
264
+ backgroundColor: ['#3fb950', '#d29922', '#484f58'],
265
+ borderColor: '#161b22',
266
+ borderWidth: 3,
267
+ }]
268
+ },
269
+ options: {
270
+ responsive: true,
271
+ maintainAspectRatio: false,
272
+ plugins: {
273
+ legend: { position: 'bottom', labels: { color: '#8b949e', font: { size: 11 }, padding: 12 } }
274
+ }
275
+ }
276
+ });
277
+
278
+ // ==================== WebSocket Live Updates ====================
279
+ let ws = null;
280
+ function connectWS() {
281
+ try {
282
+ const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
283
+ ws = new WebSocket(proto + '//' + location.host + '/ws');
284
+ ws.onmessage = (evt) => {
285
+ try {
286
+ const msg = JSON.parse(evt.data);
287
+ handleWSMessage(msg);
288
+ } catch {}
289
+ };
290
+ ws.onclose = () => { setTimeout(connectWS, 3000); };
291
+ ws.onerror = () => { ws.close(); };
292
+ document.getElementById('live-dot').style.background = '#3fb950';
293
+ } catch {
294
+ // Fallback to polling if WebSocket not available
295
+ setInterval(async () => {
296
+ try {
297
+ const r = await fetch('/status');
298
+ const data = await r.json();
299
+ updateDashboard(data);
300
+ } catch {}
301
+ }, 5000);
302
+ }
303
+ }
304
+
305
+ function handleWSMessage(msg) {
306
+ if (msg.kind === 'status_update') {
307
+ updateDashboard(msg);
308
+ }
309
+ if (msg.kind === 'pulse_update') {
310
+ updateTimeline(msg);
311
+ }
312
+ if (msg.kind === 'task_created' || msg.kind === 'task_completed') {
313
+ toast('📋 ' + msg.kind.replace('_', ' ') + ': ' + (msg.title || msg.taskId));
314
+ }
315
+ if (msg.kind === 'swarm_stopped') {
316
+ toast('⏹ Swarm Stopped');
317
+ document.getElementById('btn-swarm-stop').style.display = 'none';
318
+ document.getElementById('btn-swarm-resume').style.display = 'inline-block';
319
+ }
320
+ if (msg.kind === 'swarm_resumed') {
321
+ toast('▶ Swarm Resumed');
322
+ document.getElementById('btn-swarm-stop').style.display = 'inline-block';
323
+ document.getElementById('btn-swarm-resume').style.display = 'none';
324
+ }
325
+ }
326
+
327
+ function updateDashboard(data) {
328
+ if (data.agentName) document.getElementById('agent-name').textContent = data.agentName;
329
+ if (data.uptimeStr) document.getElementById('uptime').textContent = data.uptimeStr;
330
+ }
331
+
332
+ function updateTimeline(pulse) {
333
+ const timeline = document.getElementById('pulse-timeline');
334
+ const existingItem = timeline.querySelector('[data-agent="' + pulse.agent + '"]');
335
+ const statusClass = pulse.status === 'active' ? 'active' : pulse.status === 'idle' ? 'idle' : 'offline';
336
+ const ago = Math.round((Date.now() - (pulse.lastUpdate || Date.now())) / 1000);
337
+ const timeStr = ago < 60 ? ago + 's ago' : Math.round(ago / 60) + 'm ago';
338
+
339
+ const html = '<div class="dot ' + statusClass + '"></div>' +
340
+ '<span class="agent-name">' + pulse.agent + '</span>' +
341
+ '<span class="agent-info">' + (pulse.platform || '') + ' • ' + (pulse.currentTask || 'idle') + '</span>' +
342
+ '<span class="agent-time">' + timeStr + '</span>';
343
+
344
+ if (existingItem) {
345
+ existingItem.innerHTML = html;
346
+ } else {
347
+ const item = document.createElement('div');
348
+ item.className = 'timeline-item';
349
+ item.setAttribute('data-agent', pulse.agent);
350
+ item.innerHTML = html;
351
+ timeline.appendChild(item);
352
+ }
353
+ }
354
+
355
+ // Initialize WebSocket
356
+ connectWS();
357
+
358
+ // Fetch initial chart data
359
+ fetch('/status').then(r => r.json()).then(data => {
360
+ if (data.chartData) {
361
+ tasksChart.data.datasets[0].data = data.chartData.created || [0,0,0,0,0,0,0];
362
+ tasksChart.data.datasets[1].data = data.chartData.completed || [0,0,0,0,0,0,0];
363
+ tasksChart.update();
364
+ }
365
+ if (data.agentStats) {
366
+ agentsChart.data.datasets[0].data = [data.agentStats.active || 1, data.agentStats.idle || 0, data.agentStats.offline || 0];
367
+ agentsChart.update();
368
+ }
369
+ }).catch(() => {});
139
370
  </script>
140
371
  </body>
141
372
  </html>`;
@@ -1 +1 @@
1
- {"version":3,"file":"dashboard.js","sourceRoot":"","sources":["../src/dashboard.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAeH,MAAM,UAAU,eAAe,CAAC,IAAmB;IAC/C,MAAM,EACF,SAAS,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,EAAE,IAAI,EAC7C,eAAe,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,EAAE,WAAW,EAC1D,GAAG,IAAI,CAAC;IAET,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kCA0DuB,SAAS;;;;gDAIK,cAAc,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,KAAK,IAAI,CAAC,WAAW,EAAE;;;;gDAIvD,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,KAAK,IAAI,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,WAAW;;;;4BAI/H,eAAe,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,KAAK,eAAe,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,OAAO;;;;4FAInB,SAAS;;;;mCAIlE,SAAS;;;;qDAIS,GAAG;;;;0FAIkC,WAAW;;;;2BAI1E,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,sBAAsB,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,OAAO,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QA0ChI,CAAC;AACT,CAAC"}
1
+ {"version":3,"file":"dashboard.js","sourceRoot":"","sources":["../src/dashboard.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAgBH,MAAM,UAAU,eAAe,CAAC,IAAmB;IACjD,MAAM,EACJ,SAAS,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,EAAE,IAAI,EAC7C,eAAe,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,EAAE,WAAW,EACvD,MAAM,EACP,GAAG,IAAI,CAAC;IAET,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kDA2FyC,SAAS;;;;gDAIX,cAAc,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,qBAAqB,IAAI,CAAC,WAAW,EAAE;;;;gDAIvE,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,uBAAuB,IAAI,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,WAAW;;;;4BAIjJ,eAAe,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,wBAAwB,eAAe,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,OAAO;;;;4FAItC,SAAS;;;;+CAItD,SAAS;;;;qDAIH,GAAG;;;;0FAIkC,WAAW;;;;;;2BAM1E,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,sBAAsB,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,sBAAsB,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS;;qGAElD,IAAI,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,MAAM;uIACI,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,cAAc;;;;;;;;;;;;;;;;;;;;;;qCAsBhI,SAAS;qCACT,IAAI,MAAM,eAAe,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAiN1F,CAAC;AACT,CAAC"}
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Agent Booster — Fast Local Execution for Simple Tasks
3
3
  *
4
- * MCP Swarm v0.9.6
4
+ * MCP Swarm v1.1.6
5
5
  *
6
6
  * Executes trivial tasks locally without calling LLM APIs:
7
7
  * - Rename variables/functions
@@ -36,6 +36,8 @@ const DEFAULT_CONFIG = {
36
36
  backupBeforeChange: true,
37
37
  dryRun: false,
38
38
  estimatedLLMCostPerTask: 0.01, // $0.01 per simple task
39
+ ollamaUrl: undefined, // Not set by default — fully optional
40
+ ollamaModel: "codellama:7b",
39
41
  };
40
42
  // Estimated LLM time for comparison (ms)
41
43
  const ESTIMATED_LLM_TIME_MS = 3000;
@@ -534,6 +536,71 @@ async function executeExtractConstant(content, task) {
534
536
  lines: affectedLines,
535
537
  };
536
538
  }
539
+ // ============ OLLAMA INTEGRATION (OPTIONAL) ============
540
+ /**
541
+ * Execute a complex task via local Ollama LLM
542
+ * Only works if ollamaUrl is set in booster config
543
+ */
544
+ async function executeOllamaGenerate(content, task, config) {
545
+ if (!config.ollamaUrl) {
546
+ throw new Error("Ollama is not configured. Set ollamaUrl in booster config:\n" +
547
+ " swarm_booster({ action: 'set_config', config: { ollamaUrl: 'http://localhost:11434' } })");
548
+ }
549
+ const prompt = task.prompt || `Modify this code:\n\n${content}`;
550
+ const model = task.model || config.ollamaModel || "codellama:7b";
551
+ try {
552
+ const response = await fetch(`${config.ollamaUrl}/api/generate`, {
553
+ method: "POST",
554
+ headers: { "Content-Type": "application/json" },
555
+ body: JSON.stringify({
556
+ model,
557
+ prompt: `You are a code assistant. Given the following code, apply the requested change and return ONLY the modified code without explanation.\n\nCode:\n\`\`\`\n${content}\n\`\`\`\n\nRequest: ${prompt}\n\nModified code:`,
558
+ stream: false,
559
+ options: {
560
+ temperature: 0.1,
561
+ num_predict: 4096,
562
+ },
563
+ }),
564
+ signal: AbortSignal.timeout(60_000),
565
+ });
566
+ if (!response.ok) {
567
+ const err = await response.text();
568
+ throw new Error(`Ollama error (${response.status}): ${err}`);
569
+ }
570
+ const data = await response.json();
571
+ let newContent = data.response || "";
572
+ // Clean up ollama response — extract code from markdown blocks if present
573
+ const codeBlockMatch = newContent.match(/```(?:\w+)?\n([\s\S]*?)```/);
574
+ if (codeBlockMatch) {
575
+ newContent = codeBlockMatch[1].trim();
576
+ }
577
+ if (!newContent || newContent.trim() === content.trim()) {
578
+ return { content, changes: 0, lines: [] };
579
+ }
580
+ // Simple diff: count changed lines
581
+ const oldLines = content.split("\n");
582
+ const newLines = newContent.split("\n");
583
+ const changedLines = [];
584
+ const maxLen = Math.max(oldLines.length, newLines.length);
585
+ for (let i = 0; i < maxLen; i++) {
586
+ if ((oldLines[i] || "") !== (newLines[i] || "")) {
587
+ changedLines.push(i + 1);
588
+ }
589
+ }
590
+ return {
591
+ content: newContent,
592
+ changes: changedLines.length,
593
+ lines: changedLines,
594
+ };
595
+ }
596
+ catch (err) {
597
+ if (err instanceof Error && err.message.includes("Ollama error")) {
598
+ throw err;
599
+ }
600
+ throw new Error(`Cannot connect to Ollama at ${config.ollamaUrl}. ` +
601
+ `Make sure Ollama is running: ollama serve`);
602
+ }
603
+ }
537
604
  // ============ MAIN EXECUTOR ============
538
605
  /**
539
606
  * Execute a booster task
@@ -618,6 +685,9 @@ export async function executeTask(input) {
618
685
  case "extract_constant":
619
686
  result = await executeExtractConstant(originalContent, input.task);
620
687
  break;
688
+ case "ollama_generate":
689
+ result = await executeOllamaGenerate(originalContent, input.task, config);
690
+ break;
621
691
  default:
622
692
  throw new Error(`Unsupported task type: ${input.task.type}`);
623
693
  }
@@ -824,12 +894,33 @@ export async function canBoost(input) {
824
894
  };
825
895
  }
826
896
  }
897
+ // Check if Ollama is available for more complex tasks
898
+ const repoRoot = await getRepoRoot(input.repoPath);
899
+ const config = await loadConfig(repoRoot);
900
+ if (config.ollamaUrl) {
901
+ // With Ollama, we can handle more complex tasks
902
+ const complexPatterns = [
903
+ /refactor/i, /optimize/i, /simplify/i, /generate/i,
904
+ /explain/i, /document/i, /review/i, /suggest/i,
905
+ ];
906
+ if (complexPatterns.some(p => p.test(text))) {
907
+ return {
908
+ canBoost: true,
909
+ taskType: "ollama_generate",
910
+ confidence: 0.7,
911
+ suggestedParams: { prompt: input.description },
912
+ reason: `Ollama available — can handle complex task via local LLM (${config.ollamaModel || 'codellama:7b'})`,
913
+ };
914
+ }
915
+ }
827
916
  return {
828
917
  canBoost: false,
829
918
  taskType: null,
830
919
  confidence: 0,
831
920
  suggestedParams: {},
832
- reason: "Task does not match any boostable pattern",
921
+ reason: config.ollamaUrl
922
+ ? "Task does not match any boostable pattern (Ollama available for complex tasks)"
923
+ : "Task does not match any boostable pattern (tip: set ollamaUrl in config for complex tasks)",
833
924
  };
834
925
  }
835
926
  // ============ API ============
@@ -892,6 +983,7 @@ export function getSupportedTypes() {
892
983
  { type: "sort_imports", description: "Sort imports alphabetically", requiredParams: [] },
893
984
  { type: "add_export", description: "Add export to declaration", requiredParams: ["variableName"] },
894
985
  { type: "extract_constant", description: "Extract value to constant", requiredParams: ["searchText", "variableName"] },
986
+ { type: "ollama_generate", description: "[Optional] Use local Ollama LLM for complex tasks (requires OLLAMA_URL)", requiredParams: ["prompt"] },
895
987
  ];
896
988
  }
897
989
  export async function handleBoosterTool(input) {