bosun 0.37.0 → 0.37.2
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/.env.example +4 -1
- package/agent-tool-config.mjs +338 -0
- package/bosun-skills.mjs +59 -4
- package/bosun.schema.json +1 -1
- package/desktop/launch.mjs +18 -0
- package/desktop/main.mjs +52 -13
- package/fleet-coordinator.mjs +34 -1
- package/kanban-adapter.mjs +30 -3
- package/library-manager.mjs +66 -0
- package/maintenance.mjs +30 -5
- package/monitor.mjs +56 -0
- package/package.json +4 -1
- package/setup-web-server.mjs +73 -12
- package/setup.mjs +3 -3
- package/ui/app.js +40 -3
- package/ui/components/session-list.js +25 -7
- package/ui/components/workspace-switcher.js +48 -1
- package/ui/demo.html +176 -0
- package/ui/modules/mic-track-registry.js +83 -0
- package/ui/modules/settings-schema.js +4 -1
- package/ui/modules/state.js +25 -0
- package/ui/modules/streaming.js +1 -1
- package/ui/modules/voice-barge-in.js +27 -0
- package/ui/modules/voice-client-sdk.js +268 -42
- package/ui/modules/voice-client.js +665 -61
- package/ui/modules/voice-overlay.js +829 -47
- package/ui/setup.html +151 -9
- package/ui/styles.css +258 -0
- package/ui/tabs/chat.js +11 -0
- package/ui/tabs/library.js +890 -15
- package/ui/tabs/settings.js +51 -11
- package/ui/tabs/telemetry.js +327 -105
- package/ui/tabs/workflows.js +86 -0
- package/ui-server.mjs +1201 -107
- package/voice-action-dispatcher.mjs +81 -0
- package/voice-agents-sdk.mjs +2 -2
- package/voice-relay.mjs +131 -14
- package/voice-tools.mjs +475 -9
- package/workflow-engine.mjs +54 -0
- package/workflow-nodes.mjs +177 -28
- package/workflow-templates/github.mjs +205 -94
- package/workflow-templates/task-batch.mjs +247 -0
- package/workflow-templates.mjs +15 -0
package/ui/tabs/library.js
CHANGED
|
@@ -153,6 +153,79 @@ const LIBRARY_STYLES = `
|
|
|
153
153
|
.library-stat { min-width: 64px; }
|
|
154
154
|
.library-stat-val { font-size: 1.3em; }
|
|
155
155
|
}
|
|
156
|
+
|
|
157
|
+
/* ── MCP Marketplace ────────────────────────────────── */
|
|
158
|
+
.mcp-section { display: flex; flex-direction: column; gap: 12px; }
|
|
159
|
+
.mcp-section-header { display: flex; align-items: center; gap: 8px; }
|
|
160
|
+
.mcp-section-header h3 { margin: 0; font-size: 1em; flex: 1; }
|
|
161
|
+
.mcp-catalog-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr)); gap: 10px; }
|
|
162
|
+
.mcp-card { background: var(--bg-card, #1a1a2e); border: 1px solid var(--border, #333);
|
|
163
|
+
border-radius: 10px; padding: 14px; display: flex; flex-direction: column; gap: 8px;
|
|
164
|
+
transition: border-color 0.15s; }
|
|
165
|
+
.mcp-card:hover { border-color: var(--accent, #58a6ff); }
|
|
166
|
+
.mcp-card-header { display: flex; align-items: center; gap: 10px; }
|
|
167
|
+
.mcp-card-name { font-weight: 600; font-size: 0.92em; color: var(--text-primary, #eee); flex: 1; }
|
|
168
|
+
.mcp-card-desc { font-size: 0.8em; color: var(--text-secondary, #aaa); line-height: 1.4; }
|
|
169
|
+
.mcp-card-tags { display: flex; gap: 4px; flex-wrap: wrap; }
|
|
170
|
+
.mcp-card-tag { font-size: 0.72em; padding: 2px 7px; border-radius: 8px;
|
|
171
|
+
background: rgba(88,166,255,0.1); color: var(--accent, #58a6ff); }
|
|
172
|
+
.mcp-card-actions { display: flex; gap: 6px; margin-top: auto; }
|
|
173
|
+
.mcp-card-actions button { padding: 5px 14px; border-radius: 7px; border: 1px solid var(--border, #333);
|
|
174
|
+
background: transparent; color: var(--text-secondary, #aaa); cursor: pointer; font-size: 0.82em;
|
|
175
|
+
transition: all 0.15s; }
|
|
176
|
+
.mcp-card-actions button:hover { border-color: var(--accent, #58a6ff); color: var(--text-primary, #eee); }
|
|
177
|
+
.mcp-card-actions .btn-install { background: var(--accent, #58a6ff); color: #fff; border-color: var(--accent, #58a6ff); }
|
|
178
|
+
.mcp-card-actions .btn-install:hover { filter: brightness(1.1); }
|
|
179
|
+
.mcp-card-actions .btn-installed { background: rgba(63,185,80,0.15); color: #3fb950; border-color: rgba(63,185,80,0.3); cursor: default; }
|
|
180
|
+
.mcp-card-actions .btn-uninstall { color: #d73a49; border-color: rgba(215,58,73,0.3); }
|
|
181
|
+
.mcp-card-actions .btn-uninstall:hover { background: rgba(215,58,73,0.1); }
|
|
182
|
+
|
|
183
|
+
.mcp-env-editor { display: flex; flex-direction: column; gap: 6px; margin-top: 8px;
|
|
184
|
+
padding: 10px; background: var(--bg-input, #0d1117); border-radius: 8px; border: 1px solid var(--border, #333); }
|
|
185
|
+
.mcp-env-row { display: flex; gap: 6px; align-items: center; }
|
|
186
|
+
.mcp-env-key { font-size: 0.78em; font-family: monospace; color: var(--accent, #58a6ff); min-width: 140px; }
|
|
187
|
+
.mcp-env-input { flex: 1; padding: 4px 8px; border-radius: 6px; border: 1px solid var(--border, #333);
|
|
188
|
+
background: var(--bg-card, #1a1a2e); color: var(--text-primary, #eee); font-size: 0.82em; font-family: monospace; }
|
|
189
|
+
|
|
190
|
+
/* Custom MCP form */
|
|
191
|
+
.mcp-custom-form { display: flex; flex-direction: column; gap: 10px; padding: 14px;
|
|
192
|
+
background: var(--bg-card, #1a1a2e); border: 1px solid var(--border, #333); border-radius: 10px; }
|
|
193
|
+
.mcp-custom-form label { display: flex; flex-direction: column; gap: 3px; font-size: 0.82em;
|
|
194
|
+
color: var(--text-secondary, #aaa); }
|
|
195
|
+
.mcp-custom-form input, .mcp-custom-form select { padding: 6px 10px; border-radius: 7px;
|
|
196
|
+
border: 1px solid var(--border, #333); background: var(--bg-input, #0d1117);
|
|
197
|
+
color: var(--text-primary, #eee); font-size: 0.85em; }
|
|
198
|
+
|
|
199
|
+
/* ── Tool Configuration ────────────────────────────── */
|
|
200
|
+
.tool-config-section { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; }
|
|
201
|
+
.tool-config-header { display: flex; align-items: center; gap: 8px; padding-bottom: 8px;
|
|
202
|
+
border-bottom: 1px solid var(--border, #333); }
|
|
203
|
+
.tool-config-header h4 { margin: 0; font-size: 0.92em; flex: 1; color: var(--text-primary, #eee); }
|
|
204
|
+
.tool-config-group { display: flex; flex-direction: column; gap: 2px; }
|
|
205
|
+
.tool-config-group-label { font-size: 0.78em; color: var(--text-secondary, #aaa); text-transform: uppercase;
|
|
206
|
+
letter-spacing: 0.04em; margin: 8px 0 4px; font-weight: 600; }
|
|
207
|
+
.tool-config-item { display: flex; align-items: center; gap: 10px; padding: 8px 10px;
|
|
208
|
+
border-radius: 8px; transition: background 0.1s; }
|
|
209
|
+
.tool-config-item:hover { background: rgba(255,255,255,0.03); }
|
|
210
|
+
.tool-config-item-icon { font-size: 1.1em; width: 24px; text-align: center; flex-shrink: 0; }
|
|
211
|
+
.tool-config-item-icon svg { width: 16px; height: 16px; vertical-align: middle; }
|
|
212
|
+
.tool-config-item-info { flex: 1; min-width: 0; }
|
|
213
|
+
.tool-config-item-name { font-size: 0.88em; font-weight: 500; color: var(--text-primary, #eee); }
|
|
214
|
+
.tool-config-item-desc { font-size: 0.76em; color: var(--text-secondary, #aaa); }
|
|
215
|
+
.tool-config-toggle { flex-shrink: 0; }
|
|
216
|
+
|
|
217
|
+
/* ── Agent Detail with Tools ──────────────────────── */
|
|
218
|
+
.agent-tools-section { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--border, #333); }
|
|
219
|
+
.agent-tools-tabs { display: flex; gap: 6px; margin-bottom: 10px; }
|
|
220
|
+
.agent-tools-tab { padding: 5px 14px; border-radius: 14px; border: 1px solid var(--border, #333);
|
|
221
|
+
background: transparent; color: var(--text-secondary, #aaa); cursor: pointer; font-size: 0.82em;
|
|
222
|
+
transition: all 0.15s; }
|
|
223
|
+
.agent-tools-tab:hover { border-color: var(--accent, #58a6ff); }
|
|
224
|
+
.agent-tools-tab.active { background: var(--accent, #58a6ff); color: #fff; border-color: var(--accent, #58a6ff); }
|
|
225
|
+
|
|
226
|
+
@media (min-width: 1000px) {
|
|
227
|
+
.mcp-catalog-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
228
|
+
}
|
|
156
229
|
`;
|
|
157
230
|
|
|
158
231
|
let stylesInjected = false;
|
|
@@ -172,8 +245,11 @@ const entries = signal([]);
|
|
|
172
245
|
const scopes = signal([]);
|
|
173
246
|
const isLoading = signal(false);
|
|
174
247
|
const initialized = signal(false);
|
|
175
|
-
const filterType = signal("all"); // "all" | "prompt" | "agent" | "skill"
|
|
248
|
+
const filterType = signal("all"); // "all" | "prompt" | "agent" | "skill" | "mcp"
|
|
176
249
|
const searchQuery = signal("");
|
|
250
|
+
const mcpCatalog = signal([]);
|
|
251
|
+
const mcpInstalled = signal([]);
|
|
252
|
+
const mcpCatalogLoaded = signal(false);
|
|
177
253
|
|
|
178
254
|
/* ═══════════════════════════════════════════════════════════════
|
|
179
255
|
* API Helpers
|
|
@@ -230,13 +306,144 @@ async function testProfileMatch(title) {
|
|
|
230
306
|
return res?.data || null;
|
|
231
307
|
}
|
|
232
308
|
|
|
309
|
+
/* ── MCP API Helpers ─ */
|
|
310
|
+
|
|
311
|
+
async function fetchMcpCatalog() {
|
|
312
|
+
const res = await apiFetch("/api/mcp/catalog");
|
|
313
|
+
return res?.data || [];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function fetchMcpInstalled() {
|
|
317
|
+
const res = await apiFetch("/api/mcp/installed");
|
|
318
|
+
return res?.data || [];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function installMcp(idOrDef, envOverrides) {
|
|
322
|
+
const body = typeof idOrDef === "string"
|
|
323
|
+
? { id: idOrDef, envOverrides }
|
|
324
|
+
: { serverDef: idOrDef, envOverrides };
|
|
325
|
+
return apiFetch("/api/mcp/install", {
|
|
326
|
+
method: "POST",
|
|
327
|
+
headers: { "Content-Type": "application/json" },
|
|
328
|
+
body: JSON.stringify(body),
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function uninstallMcp(id) {
|
|
333
|
+
return apiFetch("/api/mcp/uninstall", {
|
|
334
|
+
method: "POST",
|
|
335
|
+
headers: { "Content-Type": "application/json" },
|
|
336
|
+
body: JSON.stringify({ id }),
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function configureMcpEnv(id, env) {
|
|
341
|
+
return apiFetch("/api/mcp/configure", {
|
|
342
|
+
method: "POST",
|
|
343
|
+
headers: { "Content-Type": "application/json" },
|
|
344
|
+
body: JSON.stringify({ id, env }),
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/* ── Agent Tool Config API Helpers ─ */
|
|
349
|
+
|
|
350
|
+
async function fetchAvailableTools() {
|
|
351
|
+
const res = await apiFetch("/api/agent-tools/available");
|
|
352
|
+
return res?.data || { builtinTools: [], bosunTools: [], mcpServers: [] };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function fetchAgentToolConfig(agentId) {
|
|
356
|
+
const res = await apiFetch(`/api/agent-tools/config?agentId=${encodeURIComponent(agentId)}`);
|
|
357
|
+
return res?.data || { builtinTools: [], bosunTools: [], mcpServers: [], enabledTools: null };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function saveAgentToolConfig(agentId, config) {
|
|
361
|
+
return apiFetch("/api/agent-tools/config", {
|
|
362
|
+
method: "POST",
|
|
363
|
+
headers: { "Content-Type": "application/json" },
|
|
364
|
+
body: JSON.stringify({ agentId, ...config }),
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function fetchBuiltinToolDefaults() {
|
|
369
|
+
const res = await apiFetch("/api/agent-tools/defaults");
|
|
370
|
+
return res?.data?.builtinTools || [];
|
|
371
|
+
}
|
|
372
|
+
|
|
233
373
|
/* ═══════════════════════════════════════════════════════════════
|
|
234
374
|
* Icons per type
|
|
235
375
|
* ═══════════════════════════════════════════════════════════════ */
|
|
236
376
|
|
|
237
|
-
const TYPE_ICONS = { prompt: ":edit:", agent: ":bot:", skill: ":cpu:" };
|
|
238
|
-
const TYPE_LABELS = { prompt: "Prompt", agent: "Agent Profile", skill: "Skill" };
|
|
239
|
-
const TYPE_COLORS = { prompt: "#58a6ff", agent: "#af7bff", skill: "#3fb950" };
|
|
377
|
+
const TYPE_ICONS = { prompt: ":edit:", agent: ":bot:", skill: ":cpu:", mcp: ":plug:" };
|
|
378
|
+
const TYPE_LABELS = { prompt: "Prompt", agent: "Agent Profile", skill: "Skill", mcp: "MCP Server" };
|
|
379
|
+
const TYPE_COLORS = { prompt: "#58a6ff", agent: "#af7bff", skill: "#3fb950", mcp: "#f59e0b" };
|
|
380
|
+
const AGENT_TYPE_OPTIONS = Object.freeze([
|
|
381
|
+
{ value: "voice", label: "Voice" },
|
|
382
|
+
{ value: "task", label: "Task" },
|
|
383
|
+
{ value: "chat", label: "Chat" },
|
|
384
|
+
]);
|
|
385
|
+
|
|
386
|
+
function normalizeAgentType(rawType) {
|
|
387
|
+
const value = String(rawType || "").trim().toLowerCase();
|
|
388
|
+
if (value === "voice" || value === "task" || value === "chat") return value;
|
|
389
|
+
return "task";
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function inferAgentTypeFromEntry(entry, parsedContent) {
|
|
393
|
+
const explicit = normalizeAgentType(parsedContent?.agentType);
|
|
394
|
+
if (parsedContent?.agentType) return explicit;
|
|
395
|
+
if (parsedContent?.voiceAgent === true) return "voice";
|
|
396
|
+
const id = String(entry?.id || "").trim().toLowerCase();
|
|
397
|
+
const tags = Array.isArray(entry?.tags)
|
|
398
|
+
? entry.tags.map((tag) => String(tag || "").trim().toLowerCase())
|
|
399
|
+
: [];
|
|
400
|
+
if (id.startsWith("voice-agent")) return "voice";
|
|
401
|
+
if (tags.includes("voice") || tags.includes("audio-agent") || tags.includes("realtime")) return "voice";
|
|
402
|
+
return "task";
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const AUDIO_AGENT_TEMPLATES = Object.freeze({
|
|
406
|
+
female: {
|
|
407
|
+
type: "agent",
|
|
408
|
+
name: "Voice Agent (Female)",
|
|
409
|
+
description: "Conversational voice specialist with concise guidance and call-friendly pacing.",
|
|
410
|
+
tags: "voice,audio-agent,female,realtime",
|
|
411
|
+
scope: "global",
|
|
412
|
+
content: JSON.stringify({
|
|
413
|
+
name: "Voice Agent (Female)",
|
|
414
|
+
description: "Conversational voice specialist with concise guidance and call-friendly pacing.",
|
|
415
|
+
titlePatterns: ["\\bvoice\\b", "\\bcall\\b", "\\bmeeting\\b", "\\bassistant\\b"],
|
|
416
|
+
scopes: ["voice", "assistant"],
|
|
417
|
+
model: null,
|
|
418
|
+
promptOverride: null,
|
|
419
|
+
skills: ["concise-voice-guidance", "conversation-memory"],
|
|
420
|
+
agentType: "voice",
|
|
421
|
+
voiceAgent: true,
|
|
422
|
+
voicePersona: "female",
|
|
423
|
+
voiceInstructions: "You are Nova, a female voice agent. Be concise, warm, and practical. Use tools for facts and execution. Keep spoken responses short and clear.",
|
|
424
|
+
}, null, 2),
|
|
425
|
+
},
|
|
426
|
+
male: {
|
|
427
|
+
type: "agent",
|
|
428
|
+
name: "Voice Agent (Male)",
|
|
429
|
+
description: "Operational voice specialist focused on diagnostics and execution.",
|
|
430
|
+
tags: "voice,audio-agent,male,realtime",
|
|
431
|
+
scope: "global",
|
|
432
|
+
content: JSON.stringify({
|
|
433
|
+
name: "Voice Agent (Male)",
|
|
434
|
+
description: "Operational voice specialist focused on diagnostics and execution.",
|
|
435
|
+
titlePatterns: ["\\bvoice\\b", "\\bcall\\b", "\\bmeeting\\b", "\\bassistant\\b"],
|
|
436
|
+
scopes: ["voice", "assistant"],
|
|
437
|
+
model: null,
|
|
438
|
+
promptOverride: null,
|
|
439
|
+
skills: ["ops-diagnostics", "task-execution"],
|
|
440
|
+
agentType: "voice",
|
|
441
|
+
voiceAgent: true,
|
|
442
|
+
voicePersona: "male",
|
|
443
|
+
voiceInstructions: "You are Atlas, a male voice agent. Be direct and execution-oriented. Prefer actionable status updates. Use tools proactively for diagnostics.",
|
|
444
|
+
}, null, 2),
|
|
445
|
+
},
|
|
446
|
+
});
|
|
240
447
|
|
|
241
448
|
/* ═══════════════════════════════════════════════════════════════
|
|
242
449
|
* Sub-components
|
|
@@ -244,7 +451,7 @@ const TYPE_COLORS = { prompt: "#58a6ff", agent: "#af7bff", skill: "#3fb950" };
|
|
|
244
451
|
|
|
245
452
|
function LibraryStats() {
|
|
246
453
|
const all = entries.value;
|
|
247
|
-
const counts = { prompt: 0, agent: 0, skill: 0 };
|
|
454
|
+
const counts = { prompt: 0, agent: 0, skill: 0, mcp: 0 };
|
|
248
455
|
for (const e of all) { if (counts[e.type] !== undefined) counts[e.type]++; }
|
|
249
456
|
return html`
|
|
250
457
|
<div class="library-stats">
|
|
@@ -264,6 +471,10 @@ function LibraryStats() {
|
|
|
264
471
|
<div class="library-stat-val" style="color: ${TYPE_COLORS.skill}">${counts.skill}</div>
|
|
265
472
|
<div class="library-stat-lbl">${iconText(`${TYPE_ICONS.skill} Skills`)}</div>
|
|
266
473
|
</div>
|
|
474
|
+
<div class="library-stat">
|
|
475
|
+
<div class="library-stat-val" style="color: ${TYPE_COLORS.mcp}">${counts.mcp}</div>
|
|
476
|
+
<div class="library-stat-lbl">${iconText(`${TYPE_ICONS.mcp} MCP`)}</div>
|
|
477
|
+
</div>
|
|
267
478
|
</div>
|
|
268
479
|
`;
|
|
269
480
|
}
|
|
@@ -274,6 +485,7 @@ function TypePills() {
|
|
|
274
485
|
{ id: "prompt", label: `${TYPE_ICONS.prompt} Prompts` },
|
|
275
486
|
{ id: "agent", label: `${TYPE_ICONS.agent} Agents` },
|
|
276
487
|
{ id: "skill", label: `${TYPE_ICONS.skill} Skills` },
|
|
488
|
+
{ id: "mcp", label: `${TYPE_ICONS.mcp} MCP Servers` },
|
|
277
489
|
];
|
|
278
490
|
return html`
|
|
279
491
|
<div class="library-type-pills">
|
|
@@ -309,6 +521,9 @@ function LibraryCard({ entry, onSelect }) {
|
|
|
309
521
|
<div class="library-card-desc">${entry.description}</div>
|
|
310
522
|
`}
|
|
311
523
|
<div class="library-card-meta">
|
|
524
|
+
${entry.type === "agent" && entry.agentType && html`
|
|
525
|
+
<span class="library-card-tag">${String(entry.agentType).toUpperCase()}</span>
|
|
526
|
+
`}
|
|
312
527
|
${(entry.tags || []).slice(0, 5).map((tag) => html`
|
|
313
528
|
<span class="library-card-tag" key=${tag}>${tag}</span>
|
|
314
529
|
`)}
|
|
@@ -331,7 +546,8 @@ function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
|
|
|
331
546
|
description: entry?.description || "",
|
|
332
547
|
tags: (entry?.tags || []).join(", "),
|
|
333
548
|
scope: entry?.scope || "global",
|
|
334
|
-
|
|
549
|
+
agentType: inferAgentTypeFromEntry(entry, null),
|
|
550
|
+
content: typeof entry?.content === "string" ? entry.content : "",
|
|
335
551
|
};
|
|
336
552
|
const [form, setForm] = useState(initialFormSnapshot);
|
|
337
553
|
const [baseline, setBaseline] = useState(initialFormSnapshot);
|
|
@@ -351,6 +567,7 @@ function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
|
|
|
351
567
|
description: entry?.description || "",
|
|
352
568
|
tags: (entry?.tags || []).join(", "),
|
|
353
569
|
scope: entry?.scope || "global",
|
|
570
|
+
agentType: inferAgentTypeFromEntry(entry, null),
|
|
354
571
|
content: "",
|
|
355
572
|
};
|
|
356
573
|
setForm(next);
|
|
@@ -368,8 +585,13 @@ function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
|
|
|
368
585
|
if (cancelled) return;
|
|
369
586
|
let contentStr = detail?.content ?? "";
|
|
370
587
|
if (typeof contentStr === "object") contentStr = JSON.stringify(contentStr, null, 2);
|
|
588
|
+
const parsed = detail?.content && typeof detail.content === "object" ? detail.content : null;
|
|
371
589
|
setForm((f) => {
|
|
372
|
-
const next = {
|
|
590
|
+
const next = {
|
|
591
|
+
...f,
|
|
592
|
+
content: contentStr,
|
|
593
|
+
agentType: inferAgentTypeFromEntry(detail || entry, parsed),
|
|
594
|
+
};
|
|
373
595
|
setBaseline(next);
|
|
374
596
|
return next;
|
|
375
597
|
});
|
|
@@ -406,7 +628,19 @@ function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
|
|
|
406
628
|
const tags = form.tags.split(/[,\s]+/).map((t) => t.trim().toLowerCase()).filter(Boolean);
|
|
407
629
|
let content = form.content;
|
|
408
630
|
if (form.type === "agent") {
|
|
409
|
-
try {
|
|
631
|
+
try {
|
|
632
|
+
content = JSON.parse(content);
|
|
633
|
+
} catch {
|
|
634
|
+
showToast("Agent profile content must be valid JSON", "error");
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
const agentType = normalizeAgentType(form.agentType);
|
|
638
|
+
content.agentType = agentType;
|
|
639
|
+
if (agentType === "voice") {
|
|
640
|
+
content.voiceAgent = true;
|
|
641
|
+
} else if (content.voiceAgent === true) {
|
|
642
|
+
content.voiceAgent = false;
|
|
643
|
+
}
|
|
410
644
|
}
|
|
411
645
|
const res = await saveEntry({
|
|
412
646
|
id: form.id || undefined,
|
|
@@ -466,6 +700,7 @@ function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
|
|
|
466
700
|
model: null,
|
|
467
701
|
promptOverride: null,
|
|
468
702
|
skills: [],
|
|
703
|
+
agentType: "task",
|
|
469
704
|
tags: [],
|
|
470
705
|
}, null, 2)
|
|
471
706
|
: "# Skill Title\n\n## Purpose\nDescribe what this skill teaches agents.\n\n## Instructions\n...";
|
|
@@ -490,6 +725,7 @@ function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
|
|
|
490
725
|
<option value="prompt">Prompt</option>
|
|
491
726
|
<option value="agent">Agent Profile</option>
|
|
492
727
|
<option value="skill">Skill</option>
|
|
728
|
+
<option value="mcp">MCP Server</option>
|
|
493
729
|
</select>
|
|
494
730
|
</label>
|
|
495
731
|
`}
|
|
@@ -515,6 +751,16 @@ function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
|
|
|
515
751
|
<option value="workspace">Workspace</option>
|
|
516
752
|
</select>
|
|
517
753
|
</label>
|
|
754
|
+
${form.type === "agent" && html`
|
|
755
|
+
<label>
|
|
756
|
+
Agent Type
|
|
757
|
+
<select value=${normalizeAgentType(form.agentType)} onChange=${updateField("agentType")}>
|
|
758
|
+
${AGENT_TYPE_OPTIONS.map((opt) => html`
|
|
759
|
+
<option key=${opt.value} value=${opt.value}>${opt.label}</option>
|
|
760
|
+
`)}
|
|
761
|
+
</select>
|
|
762
|
+
</label>
|
|
763
|
+
`}
|
|
518
764
|
<label>
|
|
519
765
|
Content
|
|
520
766
|
${loadingContent
|
|
@@ -526,10 +772,17 @@ function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
|
|
|
526
772
|
</label>
|
|
527
773
|
<div style="font-size:0.78em;color:var(--text-tertiary,#666);margin-top:-8px;">
|
|
528
774
|
${form.type === "prompt" ? "Use {{VARIABLE_NAME}} for template variables. Reference in workflows as {{prompt:name}}."
|
|
529
|
-
|
|
775
|
+
: form.type === "agent" ? "JSON format. Referenced in workflows as {{agent:name}}."
|
|
776
|
+
: form.type === "mcp" ? "MCP server configuration. Managed via the MCP Servers panel."
|
|
530
777
|
: "Markdown format. Referenced in workflows as {{skill:name}}."}
|
|
531
778
|
</div>
|
|
532
779
|
|
|
780
|
+
${/* ── Agent Tool Configuration Section ── */
|
|
781
|
+
!isNew && form.type === "agent" && entry?.id && html`
|
|
782
|
+
<${AgentToolConfigurator} agentId=${entry.id} agentName=${entry.name || form.name} />
|
|
783
|
+
`
|
|
784
|
+
}
|
|
785
|
+
|
|
533
786
|
<div class="library-actions">
|
|
534
787
|
${!isNew && html`
|
|
535
788
|
<button class="btn-danger" onClick=${() => setConfirmDelete(true)} disabled=${loading}>
|
|
@@ -571,6 +824,598 @@ function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
|
|
|
571
824
|
`;
|
|
572
825
|
}
|
|
573
826
|
|
|
827
|
+
/* ─ Agent Tool Configurator ─────────────────────────────── */
|
|
828
|
+
|
|
829
|
+
function AgentToolConfigurator({ agentId, agentName }) {
|
|
830
|
+
const [toolsTab, setToolsTab] = useState("builtin"); // "builtin" | "bosun" | "mcp"
|
|
831
|
+
const [tools, setTools] = useState({ builtinTools: [], bosunTools: [], mcpServers: [], enabledTools: null });
|
|
832
|
+
const [installed, setInstalled] = useState([]);
|
|
833
|
+
const [loading, setLoading] = useState(true);
|
|
834
|
+
const [saving, setSaving] = useState(false);
|
|
835
|
+
|
|
836
|
+
const loadConfig = useCallback(async () => {
|
|
837
|
+
setLoading(true);
|
|
838
|
+
try {
|
|
839
|
+
const [effective, inst] = await Promise.all([
|
|
840
|
+
fetchAgentToolConfig(agentId),
|
|
841
|
+
fetchMcpInstalled(),
|
|
842
|
+
]);
|
|
843
|
+
setTools(effective);
|
|
844
|
+
setInstalled(inst);
|
|
845
|
+
} catch (err) {
|
|
846
|
+
showToast("Failed to load tool config: " + err.message, "error");
|
|
847
|
+
}
|
|
848
|
+
setLoading(false);
|
|
849
|
+
}, [agentId]);
|
|
850
|
+
|
|
851
|
+
useEffect(() => { loadConfig(); }, [agentId]);
|
|
852
|
+
|
|
853
|
+
const toggleBuiltinTool = useCallback(async (toolId, enabled) => {
|
|
854
|
+
const current = tools.builtinTools || [];
|
|
855
|
+
const disabledList = current.filter((t) => !t.enabled).map((t) => t.id);
|
|
856
|
+
const newDisabled = enabled
|
|
857
|
+
? disabledList.filter((id) => id !== toolId)
|
|
858
|
+
: [...disabledList, toolId];
|
|
859
|
+
const currentEnabledTools = Array.isArray(tools.enabledTools)
|
|
860
|
+
? tools.enabledTools.map((id) => String(id || "").trim()).filter(Boolean)
|
|
861
|
+
: null;
|
|
862
|
+
const nextEnabledTools = currentEnabledTools
|
|
863
|
+
? (() => {
|
|
864
|
+
const set = new Set(currentEnabledTools);
|
|
865
|
+
if (enabled) set.add(toolId);
|
|
866
|
+
else set.delete(toolId);
|
|
867
|
+
return [...set];
|
|
868
|
+
})()
|
|
869
|
+
: undefined;
|
|
870
|
+
setSaving(true);
|
|
871
|
+
try {
|
|
872
|
+
await saveAgentToolConfig(agentId, {
|
|
873
|
+
disabledBuiltinTools: newDisabled,
|
|
874
|
+
...(nextEnabledTools !== undefined ? { enabledTools: nextEnabledTools } : {}),
|
|
875
|
+
});
|
|
876
|
+
setTools((prev) => ({
|
|
877
|
+
...prev,
|
|
878
|
+
...(nextEnabledTools !== undefined ? { enabledTools: nextEnabledTools } : {}),
|
|
879
|
+
builtinTools: prev.builtinTools.map((t) =>
|
|
880
|
+
t.id === toolId ? { ...t, enabled } : t
|
|
881
|
+
),
|
|
882
|
+
}));
|
|
883
|
+
} catch (err) {
|
|
884
|
+
showToast("Failed to save: " + err.message, "error");
|
|
885
|
+
}
|
|
886
|
+
setSaving(false);
|
|
887
|
+
}, [agentId, tools]);
|
|
888
|
+
|
|
889
|
+
const toggleBosunTool = useCallback(async (toolId, enabled) => {
|
|
890
|
+
const bosunIds = (tools.bosunTools || []).map((tool) => String(tool?.id || "").trim()).filter(Boolean);
|
|
891
|
+
const bosunIdSet = new Set(bosunIds);
|
|
892
|
+
const currentEnabledTools = Array.isArray(tools.enabledTools)
|
|
893
|
+
? tools.enabledTools.map((id) => String(id || "").trim()).filter(Boolean)
|
|
894
|
+
: [];
|
|
895
|
+
const currentSet = new Set(currentEnabledTools);
|
|
896
|
+
const hasBosunAllowlist = bosunIds.some((id) => currentSet.has(id));
|
|
897
|
+
const nextBosunSet = hasBosunAllowlist
|
|
898
|
+
? new Set(currentEnabledTools.filter((id) => bosunIdSet.has(id)))
|
|
899
|
+
: new Set(bosunIds);
|
|
900
|
+
if (enabled) nextBosunSet.add(toolId);
|
|
901
|
+
else nextBosunSet.delete(toolId);
|
|
902
|
+
const preserved = currentEnabledTools.filter((id) => !bosunIdSet.has(id));
|
|
903
|
+
const nextEnabledTools = [...new Set([...preserved, ...nextBosunSet])];
|
|
904
|
+
setSaving(true);
|
|
905
|
+
try {
|
|
906
|
+
await saveAgentToolConfig(agentId, { enabledTools: nextEnabledTools });
|
|
907
|
+
setTools((prev) => ({
|
|
908
|
+
...prev,
|
|
909
|
+
enabledTools: nextEnabledTools,
|
|
910
|
+
}));
|
|
911
|
+
} catch (err) {
|
|
912
|
+
showToast("Failed to save: " + err.message, "error");
|
|
913
|
+
}
|
|
914
|
+
setSaving(false);
|
|
915
|
+
}, [agentId, tools]);
|
|
916
|
+
|
|
917
|
+
const toggleMcpServer = useCallback(async (serverId, enabled) => {
|
|
918
|
+
const currentMcp = tools.mcpServers || [];
|
|
919
|
+
const newMcp = enabled
|
|
920
|
+
? [...new Set([...currentMcp, serverId])]
|
|
921
|
+
: currentMcp.filter((id) => id !== serverId);
|
|
922
|
+
setSaving(true);
|
|
923
|
+
try {
|
|
924
|
+
await saveAgentToolConfig(agentId, { enabledMcpServers: newMcp });
|
|
925
|
+
setTools((prev) => ({ ...prev, mcpServers: newMcp }));
|
|
926
|
+
} catch (err) {
|
|
927
|
+
showToast("Failed to save: " + err.message, "error");
|
|
928
|
+
}
|
|
929
|
+
setSaving(false);
|
|
930
|
+
}, [agentId, tools]);
|
|
931
|
+
|
|
932
|
+
const enabledMcpSet = new Set(tools.mcpServers || []);
|
|
933
|
+
const bosunTools = Array.isArray(tools.bosunTools) ? tools.bosunTools : [];
|
|
934
|
+
const bosunToolIds = bosunTools.map((tool) => String(tool?.id || "").trim()).filter(Boolean);
|
|
935
|
+
const rawEnabledTools = Array.isArray(tools.enabledTools)
|
|
936
|
+
? tools.enabledTools.map((id) => String(id || "").trim()).filter(Boolean)
|
|
937
|
+
: null;
|
|
938
|
+
const hasBosunAllowlist = Boolean(rawEnabledTools && rawEnabledTools.some((id) => bosunToolIds.includes(id)));
|
|
939
|
+
const enabledBosunSet = new Set(
|
|
940
|
+
hasBosunAllowlist
|
|
941
|
+
? rawEnabledTools.filter((id) => bosunToolIds.includes(id))
|
|
942
|
+
: bosunToolIds,
|
|
943
|
+
);
|
|
944
|
+
|
|
945
|
+
if (loading) {
|
|
946
|
+
return html`<div class="agent-tools-section">
|
|
947
|
+
<div style="text-align:center;padding:16px;"><${Spinner} size=${16} /> Loading tools...</div>
|
|
948
|
+
</div>`;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
return html`
|
|
952
|
+
<div class="agent-tools-section">
|
|
953
|
+
<div class="tool-config-header">
|
|
954
|
+
<h4>${iconText(":settings: Tools & MCP Servers")}</h4>
|
|
955
|
+
${saving && html`<${Spinner} size=${12} />`}
|
|
956
|
+
</div>
|
|
957
|
+
|
|
958
|
+
<div class="agent-tools-tabs">
|
|
959
|
+
<button class=${`agent-tools-tab ${toolsTab === "builtin" ? "active" : ""}`}
|
|
960
|
+
onClick=${() => setToolsTab("builtin")}>
|
|
961
|
+
${iconText(":cpu: Built-in Tools")} (${(tools.builtinTools || []).filter((t) => t.enabled).length}/${(tools.builtinTools || []).length})
|
|
962
|
+
</button>
|
|
963
|
+
<button class=${`agent-tools-tab ${toolsTab === "bosun" ? "active" : ""}`}
|
|
964
|
+
onClick=${() => setToolsTab("bosun")}>
|
|
965
|
+
${iconText(":zap: Bosun Tools")} (${enabledBosunSet.size}/${bosunTools.length})
|
|
966
|
+
</button>
|
|
967
|
+
<button class=${`agent-tools-tab ${toolsTab === "mcp" ? "active" : ""}`}
|
|
968
|
+
onClick=${() => setToolsTab("mcp")}>
|
|
969
|
+
${iconText(":plug: MCP Servers")} (${enabledMcpSet.size}/${installed.length})
|
|
970
|
+
</button>
|
|
971
|
+
</div>
|
|
972
|
+
|
|
973
|
+
${toolsTab === "builtin" && html`
|
|
974
|
+
<div class="tool-config-group">
|
|
975
|
+
${(tools.builtinTools || []).map((tool) => html`
|
|
976
|
+
<div class="tool-config-item" key=${tool.id}>
|
|
977
|
+
<span class="tool-config-item-icon">${resolveIcon(tool.icon) || iconText(tool.icon || ":cpu:")}</span>
|
|
978
|
+
<div class="tool-config-item-info">
|
|
979
|
+
<div class="tool-config-item-name">${tool.name}</div>
|
|
980
|
+
<div class="tool-config-item-desc">${tool.description}</div>
|
|
981
|
+
</div>
|
|
982
|
+
<div class="tool-config-toggle">
|
|
983
|
+
<${Toggle}
|
|
984
|
+
checked=${tool.enabled}
|
|
985
|
+
onChange=${(val) => toggleBuiltinTool(tool.id, val)}
|
|
986
|
+
/>
|
|
987
|
+
</div>
|
|
988
|
+
</div>
|
|
989
|
+
`)}
|
|
990
|
+
</div>
|
|
991
|
+
`}
|
|
992
|
+
|
|
993
|
+
${toolsTab === "bosun" && html`
|
|
994
|
+
<details open class="tool-config-group">
|
|
995
|
+
<summary class="tool-config-group-label">
|
|
996
|
+
Runtime Voice Tools (collapsible)
|
|
997
|
+
</summary>
|
|
998
|
+
${bosunTools.length === 0 && html`
|
|
999
|
+
<div style="padding:12px;text-align:center;color:var(--text-secondary);font-size:0.85em;">
|
|
1000
|
+
No Bosun runtime tools were discovered.
|
|
1001
|
+
</div>
|
|
1002
|
+
`}
|
|
1003
|
+
${bosunTools.map((tool) => html`
|
|
1004
|
+
<div class="tool-config-item" key=${tool.id}>
|
|
1005
|
+
<span class="tool-config-item-icon">${resolveIcon(":zap:") || iconText(":zap:")}</span>
|
|
1006
|
+
<div class="tool-config-item-info">
|
|
1007
|
+
<div class="tool-config-item-name">${tool.name}</div>
|
|
1008
|
+
<div class="tool-config-item-desc">${tool.description || "Bosun runtime tool"}</div>
|
|
1009
|
+
</div>
|
|
1010
|
+
<div class="tool-config-toggle">
|
|
1011
|
+
<${Toggle}
|
|
1012
|
+
checked=${enabledBosunSet.has(tool.id)}
|
|
1013
|
+
onChange=${(val) => toggleBosunTool(tool.id, val)}
|
|
1014
|
+
/>
|
|
1015
|
+
</div>
|
|
1016
|
+
</div>
|
|
1017
|
+
`)}
|
|
1018
|
+
</details>
|
|
1019
|
+
`}
|
|
1020
|
+
|
|
1021
|
+
${toolsTab === "mcp" && html`
|
|
1022
|
+
<div class="tool-config-group">
|
|
1023
|
+
${installed.length === 0 && html`
|
|
1024
|
+
<div style="padding:12px;text-align:center;color:var(--text-secondary);font-size:0.85em;">
|
|
1025
|
+
No MCP servers installed. Use the MCP Servers tab to install from the marketplace.
|
|
1026
|
+
</div>
|
|
1027
|
+
`}
|
|
1028
|
+
${installed.map((srv) => html`
|
|
1029
|
+
<div class="tool-config-item" key=${srv.id}>
|
|
1030
|
+
<span class="tool-config-item-icon">${resolveIcon(":plug:") || iconText(":plug:")}</span>
|
|
1031
|
+
<div class="tool-config-item-info">
|
|
1032
|
+
<div class="tool-config-item-name">${srv.name}</div>
|
|
1033
|
+
<div class="tool-config-item-desc">${srv.description || `Transport: ${srv.meta?.transport || "stdio"}`}</div>
|
|
1034
|
+
</div>
|
|
1035
|
+
<div class="tool-config-toggle">
|
|
1036
|
+
<${Toggle}
|
|
1037
|
+
checked=${enabledMcpSet.has(srv.id)}
|
|
1038
|
+
onChange=${(val) => toggleMcpServer(srv.id, val)}
|
|
1039
|
+
/>
|
|
1040
|
+
</div>
|
|
1041
|
+
</div>
|
|
1042
|
+
`)}
|
|
1043
|
+
</div>
|
|
1044
|
+
`}
|
|
1045
|
+
</div>
|
|
1046
|
+
`;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/* ─ MCP Marketplace / Catalog ─────────────────────────────── */
|
|
1050
|
+
|
|
1051
|
+
function McpMarketplace({ onInstalled }) {
|
|
1052
|
+
const [catalog, setCatalog] = useState([]);
|
|
1053
|
+
const [installed, setInstalled] = useState([]);
|
|
1054
|
+
const [loading, setLoading] = useState(true);
|
|
1055
|
+
const [installing, setInstalling] = useState(null);
|
|
1056
|
+
const [uninstalling, setUninstalling] = useState(null);
|
|
1057
|
+
const [configuring, setConfiguring] = useState(null);
|
|
1058
|
+
const [envEdits, setEnvEdits] = useState({});
|
|
1059
|
+
const [showCustom, setShowCustom] = useState(false);
|
|
1060
|
+
const [marketplaceSearch, setMarketplaceSearch] = useState("");
|
|
1061
|
+
|
|
1062
|
+
const loadData = useCallback(async () => {
|
|
1063
|
+
setLoading(true);
|
|
1064
|
+
try {
|
|
1065
|
+
const [cat, inst] = await Promise.all([
|
|
1066
|
+
fetchMcpCatalog(),
|
|
1067
|
+
fetchMcpInstalled(),
|
|
1068
|
+
]);
|
|
1069
|
+
setCatalog(cat);
|
|
1070
|
+
setInstalled(inst);
|
|
1071
|
+
mcpCatalog.value = cat;
|
|
1072
|
+
mcpInstalled.value = inst;
|
|
1073
|
+
} catch (err) {
|
|
1074
|
+
showToast("Failed to load MCP data: " + err.message, "error");
|
|
1075
|
+
}
|
|
1076
|
+
setLoading(false);
|
|
1077
|
+
}, []);
|
|
1078
|
+
|
|
1079
|
+
useEffect(() => { loadData(); }, []);
|
|
1080
|
+
|
|
1081
|
+
const installedIds = new Set(installed.map((s) => s.id));
|
|
1082
|
+
|
|
1083
|
+
const handleInstall = useCallback(async (catalogId) => {
|
|
1084
|
+
setInstalling(catalogId);
|
|
1085
|
+
try {
|
|
1086
|
+
const envVars = envEdits[catalogId] || {};
|
|
1087
|
+
const res = await installMcp(catalogId, envVars);
|
|
1088
|
+
if (res?.ok) {
|
|
1089
|
+
showToast(`Installed ${catalogId}`, "success");
|
|
1090
|
+
await loadData();
|
|
1091
|
+
onInstalled?.();
|
|
1092
|
+
} else {
|
|
1093
|
+
showToast(res?.error || "Install failed", "error");
|
|
1094
|
+
}
|
|
1095
|
+
} catch (err) {
|
|
1096
|
+
showToast("Install failed: " + err.message, "error");
|
|
1097
|
+
}
|
|
1098
|
+
setInstalling(null);
|
|
1099
|
+
}, [envEdits, loadData, onInstalled]);
|
|
1100
|
+
|
|
1101
|
+
const handleUninstall = useCallback(async (id) => {
|
|
1102
|
+
setUninstalling(id);
|
|
1103
|
+
try {
|
|
1104
|
+
const res = await uninstallMcp(id);
|
|
1105
|
+
if (res?.ok) {
|
|
1106
|
+
showToast(`Uninstalled ${id}`, "success");
|
|
1107
|
+
await loadData();
|
|
1108
|
+
onInstalled?.();
|
|
1109
|
+
} else {
|
|
1110
|
+
showToast(res?.error || "Uninstall failed", "error");
|
|
1111
|
+
}
|
|
1112
|
+
} catch (err) {
|
|
1113
|
+
showToast("Uninstall failed: " + err.message, "error");
|
|
1114
|
+
}
|
|
1115
|
+
setUninstalling(null);
|
|
1116
|
+
}, [loadData, onInstalled]);
|
|
1117
|
+
|
|
1118
|
+
const handleConfigure = useCallback(async (id) => {
|
|
1119
|
+
const env = envEdits[id] || {};
|
|
1120
|
+
try {
|
|
1121
|
+
const res = await configureMcpEnv(id, env);
|
|
1122
|
+
if (res?.ok) {
|
|
1123
|
+
showToast(`Updated ${id} configuration`, "success");
|
|
1124
|
+
setConfiguring(null);
|
|
1125
|
+
} else {
|
|
1126
|
+
showToast(res?.error || "Update failed", "error");
|
|
1127
|
+
}
|
|
1128
|
+
} catch (err) {
|
|
1129
|
+
showToast("Update failed: " + err.message, "error");
|
|
1130
|
+
}
|
|
1131
|
+
}, [envEdits]);
|
|
1132
|
+
|
|
1133
|
+
const handleCustomInstall = useCallback(async (def) => {
|
|
1134
|
+
setInstalling("custom");
|
|
1135
|
+
try {
|
|
1136
|
+
const res = await installMcp(def);
|
|
1137
|
+
if (res?.ok) {
|
|
1138
|
+
showToast(`Installed custom MCP server: ${def.name}`, "success");
|
|
1139
|
+
setShowCustom(false);
|
|
1140
|
+
await loadData();
|
|
1141
|
+
onInstalled?.();
|
|
1142
|
+
} else {
|
|
1143
|
+
showToast(res?.error || "Install failed", "error");
|
|
1144
|
+
}
|
|
1145
|
+
} catch (err) {
|
|
1146
|
+
showToast("Install failed: " + err.message, "error");
|
|
1147
|
+
}
|
|
1148
|
+
setInstalling(null);
|
|
1149
|
+
}, [loadData, onInstalled]);
|
|
1150
|
+
|
|
1151
|
+
const updateEnv = useCallback((serverId, key, value) => {
|
|
1152
|
+
setEnvEdits((prev) => ({
|
|
1153
|
+
...prev,
|
|
1154
|
+
[serverId]: { ...(prev[serverId] || {}), [key]: value },
|
|
1155
|
+
}));
|
|
1156
|
+
}, []);
|
|
1157
|
+
|
|
1158
|
+
const filteredCatalog = useMemo(() => {
|
|
1159
|
+
if (!marketplaceSearch.trim()) return catalog;
|
|
1160
|
+
const q = marketplaceSearch.toLowerCase();
|
|
1161
|
+
return catalog.filter(
|
|
1162
|
+
(s) => s.name.toLowerCase().includes(q) ||
|
|
1163
|
+
s.description.toLowerCase().includes(q) ||
|
|
1164
|
+
(s.tags || []).some((t) => t.toLowerCase().includes(q)),
|
|
1165
|
+
);
|
|
1166
|
+
}, [catalog, marketplaceSearch]);
|
|
1167
|
+
|
|
1168
|
+
if (loading) {
|
|
1169
|
+
return html`<div style="text-align:center;padding:40px;"><${Spinner} /> Loading MCP marketplace...</div>`;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
return html`
|
|
1173
|
+
<div class="mcp-section">
|
|
1174
|
+
<!-- Installed section -->
|
|
1175
|
+
${installed.length > 0 && html`
|
|
1176
|
+
<div class="mcp-section-header">
|
|
1177
|
+
<h3>${iconText(":check: Installed")} (${installed.length})</h3>
|
|
1178
|
+
</div>
|
|
1179
|
+
<div class="mcp-catalog-grid">
|
|
1180
|
+
${installed.map((srv) => html`
|
|
1181
|
+
<div class="mcp-card" key=${srv.id}>
|
|
1182
|
+
<div class="mcp-card-header">
|
|
1183
|
+
<span class="mcp-card-name">${srv.name}</span>
|
|
1184
|
+
<${Badge} text="Installed" status="success" />
|
|
1185
|
+
</div>
|
|
1186
|
+
<div class="mcp-card-desc">${srv.description}</div>
|
|
1187
|
+
<div class="mcp-card-tags">
|
|
1188
|
+
${(srv.tags || []).map((t) => html`<span class="mcp-card-tag" key=${t}>${t}</span>`)}
|
|
1189
|
+
</div>
|
|
1190
|
+
<div class="mcp-card-actions">
|
|
1191
|
+
<button class="btn-uninstall"
|
|
1192
|
+
onClick=${() => handleUninstall(srv.id)}
|
|
1193
|
+
disabled=${uninstalling === srv.id}>
|
|
1194
|
+
${uninstalling === srv.id ? html`<${Spinner} size=${12} />` : "Uninstall"}
|
|
1195
|
+
</button>
|
|
1196
|
+
<button onClick=${() => setConfiguring(configuring === srv.id ? null : srv.id)}>
|
|
1197
|
+
${iconText(":settings: Configure")}
|
|
1198
|
+
</button>
|
|
1199
|
+
</div>
|
|
1200
|
+
${configuring === srv.id && html`
|
|
1201
|
+
<div class="mcp-env-editor">
|
|
1202
|
+
<div style="font-size:0.78em;color:var(--text-secondary);margin-bottom:4px;">
|
|
1203
|
+
Environment Variables
|
|
1204
|
+
</div>
|
|
1205
|
+
${Object.entries(srv.meta?.env || {}).map(([key, val]) => html`
|
|
1206
|
+
<div class="mcp-env-row" key=${key}>
|
|
1207
|
+
<span class="mcp-env-key">${key}</span>
|
|
1208
|
+
<input class="mcp-env-input"
|
|
1209
|
+
type=${key.toLowerCase().includes("key") || key.toLowerCase().includes("token") || key.toLowerCase().includes("secret") ? "password" : "text"}
|
|
1210
|
+
value=${envEdits[srv.id]?.[key] ?? val}
|
|
1211
|
+
placeholder="Enter value..."
|
|
1212
|
+
onInput=${(e) => updateEnv(srv.id, key, e.target.value)} />
|
|
1213
|
+
</div>
|
|
1214
|
+
`)}
|
|
1215
|
+
${Object.keys(srv.meta?.env || {}).length === 0 && html`
|
|
1216
|
+
<div style="font-size:0.82em;color:var(--text-tertiary,#666);">No environment variables required.</div>
|
|
1217
|
+
`}
|
|
1218
|
+
<div style="display:flex;gap:6px;justify-content:flex-end;margin-top:6px;">
|
|
1219
|
+
<button class="library-type-pill" onClick=${() => setConfiguring(null)}>Cancel</button>
|
|
1220
|
+
<button class="library-type-pill active" onClick=${() => handleConfigure(srv.id)}>
|
|
1221
|
+
${iconText(":check: Save")}
|
|
1222
|
+
</button>
|
|
1223
|
+
</div>
|
|
1224
|
+
</div>
|
|
1225
|
+
`}
|
|
1226
|
+
</div>
|
|
1227
|
+
`)}
|
|
1228
|
+
</div>
|
|
1229
|
+
`}
|
|
1230
|
+
|
|
1231
|
+
<!-- Marketplace Catalog -->
|
|
1232
|
+
<div class="mcp-section-header">
|
|
1233
|
+
<h3>${iconText(":shopping: MCP Marketplace")}</h3>
|
|
1234
|
+
<button class="library-type-pill" onClick=${() => setShowCustom(!showCustom)}>
|
|
1235
|
+
${iconText("➕ Custom Server")}
|
|
1236
|
+
</button>
|
|
1237
|
+
</div>
|
|
1238
|
+
|
|
1239
|
+
<div style="margin-bottom:8px;">
|
|
1240
|
+
<${SearchInput}
|
|
1241
|
+
value=${marketplaceSearch}
|
|
1242
|
+
onChange=${setMarketplaceSearch}
|
|
1243
|
+
placeholder="Search marketplace (GitHub, Playwright, Exa, etc.)..." />
|
|
1244
|
+
</div>
|
|
1245
|
+
|
|
1246
|
+
${showCustom && html`
|
|
1247
|
+
<${McpCustomInstallForm} onInstall=${handleCustomInstall} installing=${installing === "custom"} />
|
|
1248
|
+
`}
|
|
1249
|
+
|
|
1250
|
+
<div class="mcp-catalog-grid">
|
|
1251
|
+
${filteredCatalog.map((srv) => {
|
|
1252
|
+
const isInstalled = installedIds.has(srv.id);
|
|
1253
|
+
const hasEnv = srv.env && Object.keys(srv.env).length > 0;
|
|
1254
|
+
const isInstalling = installing === srv.id;
|
|
1255
|
+
return html`
|
|
1256
|
+
<div class="mcp-card" key=${srv.id}>
|
|
1257
|
+
<div class="mcp-card-header">
|
|
1258
|
+
<span class="mcp-card-name">${srv.name}</span>
|
|
1259
|
+
${isInstalled && html`<${Badge} text="Installed" status="success" />`}
|
|
1260
|
+
</div>
|
|
1261
|
+
<div class="mcp-card-desc">${srv.description}</div>
|
|
1262
|
+
<div class="mcp-card-tags">
|
|
1263
|
+
${(srv.tags || []).map((t) => html`<span class="mcp-card-tag" key=${t}>${t}</span>`)}
|
|
1264
|
+
<span class="mcp-card-tag" style="background:rgba(255,255,255,0.05);color:var(--text-tertiary,#666);">
|
|
1265
|
+
${srv.transport}
|
|
1266
|
+
</span>
|
|
1267
|
+
</div>
|
|
1268
|
+
${hasEnv && !isInstalled && html`
|
|
1269
|
+
<div class="mcp-env-editor">
|
|
1270
|
+
${Object.entries(srv.env).map(([key, val]) => html`
|
|
1271
|
+
<div class="mcp-env-row" key=${key}>
|
|
1272
|
+
<span class="mcp-env-key">${key}</span>
|
|
1273
|
+
<input class="mcp-env-input"
|
|
1274
|
+
type=${key.toLowerCase().includes("key") || key.toLowerCase().includes("token") || key.toLowerCase().includes("secret") ? "password" : "text"}
|
|
1275
|
+
value=${envEdits[srv.id]?.[key] ?? ""}
|
|
1276
|
+
placeholder=${val || "Enter value..."}
|
|
1277
|
+
onInput=${(e) => updateEnv(srv.id, key, e.target.value)} />
|
|
1278
|
+
</div>
|
|
1279
|
+
`)}
|
|
1280
|
+
</div>
|
|
1281
|
+
`}
|
|
1282
|
+
${srv.homepage && html`
|
|
1283
|
+
<div style="font-size:0.75em;">
|
|
1284
|
+
<a href=${srv.homepage} target="_blank" rel="noopener"
|
|
1285
|
+
style="color:var(--accent,#58a6ff);text-decoration:none;">
|
|
1286
|
+
${iconText(":link: Documentation")}
|
|
1287
|
+
</a>
|
|
1288
|
+
</div>
|
|
1289
|
+
`}
|
|
1290
|
+
<div class="mcp-card-actions">
|
|
1291
|
+
${isInstalled
|
|
1292
|
+
? html`<button class="btn-installed" disabled>✓ Installed</button>`
|
|
1293
|
+
: html`
|
|
1294
|
+
<button class="btn-install"
|
|
1295
|
+
onClick=${() => handleInstall(srv.id)}
|
|
1296
|
+
disabled=${isInstalling}>
|
|
1297
|
+
${isInstalling ? html`<${Spinner} size=${12} />` : iconText(":download: Install")}
|
|
1298
|
+
</button>
|
|
1299
|
+
`
|
|
1300
|
+
}
|
|
1301
|
+
</div>
|
|
1302
|
+
</div>
|
|
1303
|
+
`;
|
|
1304
|
+
})}
|
|
1305
|
+
</div>
|
|
1306
|
+
|
|
1307
|
+
${filteredCatalog.length === 0 && html`
|
|
1308
|
+
<div style="text-align:center;padding:20px;color:var(--text-secondary);font-size:0.9em;">
|
|
1309
|
+
${marketplaceSearch ? "No MCP servers match your search." : "No catalog entries available."}
|
|
1310
|
+
</div>
|
|
1311
|
+
`}
|
|
1312
|
+
</div>
|
|
1313
|
+
`;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
/* ─ Custom MCP Install Form ───────────────────────────────── */
|
|
1317
|
+
|
|
1318
|
+
function McpCustomInstallForm({ onInstall, installing }) {
|
|
1319
|
+
const [form, setForm] = useState({
|
|
1320
|
+
name: "",
|
|
1321
|
+
description: "",
|
|
1322
|
+
transport: "stdio",
|
|
1323
|
+
command: "npx",
|
|
1324
|
+
args: "",
|
|
1325
|
+
url: "",
|
|
1326
|
+
tags: "",
|
|
1327
|
+
envKeys: "",
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
const updateField = (key) => (e) => setForm((f) => ({ ...f, [key]: e.target.value }));
|
|
1331
|
+
|
|
1332
|
+
const handleSubmit = useCallback(() => {
|
|
1333
|
+
if (!form.name.trim()) {
|
|
1334
|
+
showToast("Server name is required", "error");
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
const def = {
|
|
1338
|
+
name: form.name.trim(),
|
|
1339
|
+
description: form.description.trim(),
|
|
1340
|
+
transport: form.transport,
|
|
1341
|
+
tags: form.tags.split(/[,\s]+/).filter(Boolean),
|
|
1342
|
+
};
|
|
1343
|
+
if (form.transport === "stdio") {
|
|
1344
|
+
def.command = form.command.trim() || "npx";
|
|
1345
|
+
def.args = form.args.split(/\s+/).filter(Boolean);
|
|
1346
|
+
} else {
|
|
1347
|
+
def.url = form.url.trim();
|
|
1348
|
+
}
|
|
1349
|
+
// Parse env keys
|
|
1350
|
+
if (form.envKeys.trim()) {
|
|
1351
|
+
def.env = {};
|
|
1352
|
+
for (const key of form.envKeys.split(/[,\s]+/).filter(Boolean)) {
|
|
1353
|
+
def.env[key] = "";
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
onInstall(def);
|
|
1357
|
+
}, [form, onInstall]);
|
|
1358
|
+
|
|
1359
|
+
return html`
|
|
1360
|
+
<div class="mcp-custom-form">
|
|
1361
|
+
<div style="font-size:0.88em;font-weight:600;color:var(--text-primary);">
|
|
1362
|
+
${iconText("➕ Install Custom MCP Server")}
|
|
1363
|
+
</div>
|
|
1364
|
+
<label>
|
|
1365
|
+
Name *
|
|
1366
|
+
<input type="text" value=${form.name} onInput=${updateField("name")}
|
|
1367
|
+
placeholder="e.g. My Custom Server" />
|
|
1368
|
+
</label>
|
|
1369
|
+
<label>
|
|
1370
|
+
Description
|
|
1371
|
+
<input type="text" value=${form.description} onInput=${updateField("description")}
|
|
1372
|
+
placeholder="Brief description" />
|
|
1373
|
+
</label>
|
|
1374
|
+
<label>
|
|
1375
|
+
Transport
|
|
1376
|
+
<select value=${form.transport} onChange=${updateField("transport")}>
|
|
1377
|
+
<option value="stdio">stdio (command + args)</option>
|
|
1378
|
+
<option value="url">URL (HTTP/SSE endpoint)</option>
|
|
1379
|
+
</select>
|
|
1380
|
+
</label>
|
|
1381
|
+
${form.transport === "stdio" && html`
|
|
1382
|
+
<label>
|
|
1383
|
+
Command
|
|
1384
|
+
<input type="text" value=${form.command} onInput=${updateField("command")}
|
|
1385
|
+
placeholder="npx" />
|
|
1386
|
+
</label>
|
|
1387
|
+
<label>
|
|
1388
|
+
Arguments (space-separated)
|
|
1389
|
+
<input type="text" value=${form.args} onInput=${updateField("args")}
|
|
1390
|
+
placeholder="-y @scope/mcp-server" />
|
|
1391
|
+
</label>
|
|
1392
|
+
`}
|
|
1393
|
+
${form.transport === "url" && html`
|
|
1394
|
+
<label>
|
|
1395
|
+
URL
|
|
1396
|
+
<input type="text" value=${form.url} onInput=${updateField("url")}
|
|
1397
|
+
placeholder="https://example.com/mcp" />
|
|
1398
|
+
</label>
|
|
1399
|
+
`}
|
|
1400
|
+
<label>
|
|
1401
|
+
Tags (comma-separated)
|
|
1402
|
+
<input type="text" value=${form.tags} onInput=${updateField("tags")}
|
|
1403
|
+
placeholder="custom, tools" />
|
|
1404
|
+
</label>
|
|
1405
|
+
<label>
|
|
1406
|
+
Environment Variable Keys (comma-separated, values set after install)
|
|
1407
|
+
<input type="text" value=${form.envKeys} onInput=${updateField("envKeys")}
|
|
1408
|
+
placeholder="API_KEY, SECRET_TOKEN" />
|
|
1409
|
+
</label>
|
|
1410
|
+
<div class="library-actions">
|
|
1411
|
+
<button class="btn-primary" onClick=${handleSubmit} disabled=${installing}>
|
|
1412
|
+
${installing ? html`<${Spinner} size=${14} />` : iconText(":download: Install")}
|
|
1413
|
+
</button>
|
|
1414
|
+
</div>
|
|
1415
|
+
</div>
|
|
1416
|
+
`;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
574
1419
|
/* ─ Scope Detector Panel ─────────────────────────────────── */
|
|
575
1420
|
|
|
576
1421
|
function ScopeDetector() {
|
|
@@ -691,6 +1536,17 @@ export function LibraryTab() {
|
|
|
691
1536
|
|
|
692
1537
|
useEffect(() => { loadEntries(); }, [filterType.value]);
|
|
693
1538
|
|
|
1539
|
+
useEffect(() => {
|
|
1540
|
+
const onWorkspaceSwitched = () => {
|
|
1541
|
+
setEditing(null);
|
|
1542
|
+
loadEntries();
|
|
1543
|
+
};
|
|
1544
|
+
window.addEventListener("ve:workspace-switched", onWorkspaceSwitched);
|
|
1545
|
+
return () => {
|
|
1546
|
+
window.removeEventListener("ve:workspace-switched", onWorkspaceSwitched);
|
|
1547
|
+
};
|
|
1548
|
+
}, [loadEntries]);
|
|
1549
|
+
|
|
694
1550
|
// Debounced search
|
|
695
1551
|
const searchTimer = useRef(null);
|
|
696
1552
|
const handleSearch = useCallback((value) => {
|
|
@@ -732,6 +1588,13 @@ export function LibraryTab() {
|
|
|
732
1588
|
setEditing(entry);
|
|
733
1589
|
}, []);
|
|
734
1590
|
|
|
1591
|
+
const handleCreateAudioAgent = useCallback((templateKey) => {
|
|
1592
|
+
const template = AUDIO_AGENT_TEMPLATES[templateKey];
|
|
1593
|
+
if (!template) return;
|
|
1594
|
+
haptic("light");
|
|
1595
|
+
setEditing({ ...template });
|
|
1596
|
+
}, []);
|
|
1597
|
+
|
|
735
1598
|
const handleSaved = useCallback(() => {
|
|
736
1599
|
setEditing(null);
|
|
737
1600
|
loadEntries();
|
|
@@ -755,6 +1618,12 @@ export function LibraryTab() {
|
|
|
755
1618
|
<div class="library-root">
|
|
756
1619
|
<div class="library-header">
|
|
757
1620
|
<h2>${iconText(":book: Library")}</h2>
|
|
1621
|
+
<button class="library-type-pill" onClick=${() => handleCreateAudioAgent("female")}>
|
|
1622
|
+
${iconText(":mic: New Female Audio Agent")}
|
|
1623
|
+
</button>
|
|
1624
|
+
<button class="library-type-pill" onClick=${() => handleCreateAudioAgent("male")}>
|
|
1625
|
+
${iconText(":mic: New Male Audio Agent")}
|
|
1626
|
+
</button>
|
|
758
1627
|
<button class="library-type-pill" onClick=${handleRebuild}
|
|
759
1628
|
title="Rescan directories and rebuild manifest">
|
|
760
1629
|
${iconText(":refresh: Rebuild")}
|
|
@@ -779,19 +1648,25 @@ export function LibraryTab() {
|
|
|
779
1648
|
<${SearchInput}
|
|
780
1649
|
value=${searchQuery.value}
|
|
781
1650
|
onChange=${handleSearch}
|
|
782
|
-
placeholder="Search prompts, agents, skills..." />
|
|
1651
|
+
placeholder="Search prompts, agents, skills, MCP servers..." />
|
|
783
1652
|
</div>
|
|
784
1653
|
<${TypePills} />
|
|
785
1654
|
</div>
|
|
786
1655
|
|
|
787
|
-
|
|
788
|
-
|
|
1656
|
+
${filterType.value !== "mcp" && html`<${ProfileMatcher} />`}
|
|
1657
|
+
${filterType.value !== "mcp" && html`<${ScopeDetector} />`}
|
|
1658
|
+
|
|
1659
|
+
${/* ── MCP Marketplace View ── */
|
|
1660
|
+
filterType.value === "mcp" && html`
|
|
1661
|
+
<${McpMarketplace} onInstalled=${loadEntries} />
|
|
1662
|
+
`
|
|
1663
|
+
}
|
|
789
1664
|
|
|
790
|
-
${loading && html`
|
|
1665
|
+
${filterType.value !== "mcp" && loading && html`
|
|
791
1666
|
<div style="text-align:center;padding:40px;"><${Spinner} /> Loading library...</div>
|
|
792
1667
|
`}
|
|
793
1668
|
|
|
794
|
-
${!loading && displayed.length === 0 && initialized.value && html`
|
|
1669
|
+
${filterType.value !== "mcp" && !loading && displayed.length === 0 && initialized.value && html`
|
|
795
1670
|
<${EmptyState}
|
|
796
1671
|
icon="book"
|
|
797
1672
|
title="No resources found"
|
|
@@ -803,7 +1678,7 @@ export function LibraryTab() {
|
|
|
803
1678
|
: { label: "➕ New Resource", onClick: () => setEditing({}) }} />
|
|
804
1679
|
`}
|
|
805
1680
|
|
|
806
|
-
${!loading && displayed.length > 0 && html`
|
|
1681
|
+
${filterType.value !== "mcp" && !loading && displayed.length > 0 && html`
|
|
807
1682
|
<div class="library-grid">
|
|
808
1683
|
${displayed.map((e) => html`
|
|
809
1684
|
<${LibraryCard} key=${e.id} entry=${e} onSelect=${handleSelect} />
|