bosun 0.41.2 → 0.41.4

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.
Files changed (73) hide show
  1. package/.env.example +1 -1
  2. package/agent/agent-pool.mjs +9 -2
  3. package/agent/agent-prompt-catalog.mjs +971 -0
  4. package/agent/agent-prompts.mjs +2 -970
  5. package/agent/agent-supervisor.mjs +119 -6
  6. package/agent/autofix-git.mjs +33 -0
  7. package/agent/autofix-prompts.mjs +151 -0
  8. package/agent/autofix.mjs +11 -175
  9. package/agent/bosun-skills.mjs +3 -2
  10. package/bosun.config.example.json +17 -0
  11. package/bosun.schema.json +87 -188
  12. package/cli.mjs +34 -1
  13. package/config/config-doctor.mjs +5 -250
  14. package/config/config-file-names.mjs +5 -0
  15. package/config/config.mjs +89 -493
  16. package/config/executor-config.mjs +493 -0
  17. package/config/repo-root.mjs +1 -2
  18. package/config/workspace-health.mjs +242 -0
  19. package/git/git-safety.mjs +15 -0
  20. package/github/github-oauth-portal.mjs +46 -0
  21. package/infra/library-manager-utils.mjs +22 -0
  22. package/infra/library-manager-well-known-sources.mjs +578 -0
  23. package/infra/library-manager.mjs +512 -1030
  24. package/infra/monitor.mjs +35 -9
  25. package/infra/session-tracker.mjs +10 -7
  26. package/kanban/kanban-adapter.mjs +17 -1
  27. package/lib/codebase-audit-manifests.mjs +117 -0
  28. package/lib/codebase-audit.mjs +18 -115
  29. package/package.json +18 -3
  30. package/server/setup-web-server.mjs +58 -5
  31. package/server/ui-server.mjs +1394 -79
  32. package/shell/codex-config-file.mjs +178 -0
  33. package/shell/codex-config.mjs +538 -575
  34. package/task/task-cli.mjs +54 -3
  35. package/task/task-executor.mjs +143 -13
  36. package/task/task-store.mjs +409 -1
  37. package/telegram/telegram-bot.mjs +127 -0
  38. package/tools/apply-pr-suggestions.mjs +401 -0
  39. package/tools/syntax-check.mjs +28 -9
  40. package/ui/app.js +3 -14
  41. package/ui/components/kanban-board.js +227 -4
  42. package/ui/components/session-list.js +85 -5
  43. package/ui/demo-defaults.js +338 -84
  44. package/ui/demo.html +155 -0
  45. package/ui/modules/session-api.js +96 -0
  46. package/ui/modules/settings-schema.js +1 -2
  47. package/ui/modules/state.js +43 -3
  48. package/ui/setup.html +4 -5
  49. package/ui/styles/components.css +58 -4
  50. package/ui/tabs/agents.js +12 -15
  51. package/ui/tabs/control.js +1 -0
  52. package/ui/tabs/library.js +484 -22
  53. package/ui/tabs/manual-flows.js +105 -29
  54. package/ui/tabs/tasks.js +848 -141
  55. package/ui/tabs/telemetry.js +129 -11
  56. package/ui/tabs/workflow-canvas-utils.mjs +130 -0
  57. package/ui/tabs/workflows.js +293 -23
  58. package/voice/voice-tool-definitions.mjs +757 -0
  59. package/voice/voice-tools.mjs +34 -778
  60. package/workflow/manual-flow-audit.mjs +165 -0
  61. package/workflow/manual-flows.mjs +164 -259
  62. package/workflow/workflow-engine.mjs +147 -58
  63. package/workflow/workflow-nodes/definitions.mjs +1207 -0
  64. package/workflow/workflow-nodes/transforms.mjs +612 -0
  65. package/workflow/workflow-nodes.mjs +358 -63
  66. package/workflow/workflow-templates.mjs +313 -191
  67. package/workflow-templates/_helpers.mjs +154 -0
  68. package/workflow-templates/agents.mjs +61 -4
  69. package/workflow-templates/code-quality.mjs +7 -7
  70. package/workflow-templates/github.mjs +20 -10
  71. package/workflow-templates/task-batch.mjs +44 -11
  72. package/workflow-templates/task-lifecycle.mjs +31 -6
  73. package/workspace/worktree-manager.mjs +277 -3
@@ -238,6 +238,34 @@ const LIBRARY_STYLES = `
238
238
  }
239
239
  `;
240
240
 
241
+ function normalizeLibraryTaskStatus(status) {
242
+ return String(status || "").trim().toLowerCase();
243
+ }
244
+
245
+ export function isSelectableLibraryTask(task) {
246
+ const status = normalizeLibraryTaskStatus(task?.status);
247
+ return (
248
+ status === "draft" ||
249
+ status === "todo" ||
250
+ status === "backlog" ||
251
+ status === "planned" ||
252
+ status === "open" ||
253
+ status === "new" ||
254
+ status === ""
255
+ );
256
+ }
257
+
258
+ export function extractSelectableLibraryTasks(payload) {
259
+ const tasks = Array.isArray(payload?.tasks)
260
+ ? payload.tasks
261
+ : Array.isArray(payload?.data)
262
+ ? payload.data
263
+ : Array.isArray(payload)
264
+ ? payload
265
+ : [];
266
+ return tasks.filter(isSelectableLibraryTask).slice(0, 100);
267
+ }
268
+
241
269
  let stylesInjected = false;
242
270
  function injectStyles() {
243
271
  if (stylesInjected) return;
@@ -1530,6 +1558,39 @@ function ScopeDetector() {
1530
1558
  `;
1531
1559
  }
1532
1560
 
1561
+ /* ─ Workflow Step Resolver ─────────────────────────────────── */
1562
+
1563
+ async function resolveWorkflowSteps(title, description) {
1564
+ const steps = ["tdd", "implementation", "review"];
1565
+ const results = {};
1566
+ for (const step of steps) {
1567
+ const stepTitle = `${step}: ${title}`;
1568
+ try {
1569
+ const resp = await testProfileMatch({ title: stepTitle, description, topN: 1 });
1570
+ results[step] = resp;
1571
+ } catch {
1572
+ results[step] = { best: null, candidates: [], plan: null };
1573
+ }
1574
+ }
1575
+ return results;
1576
+ }
1577
+
1578
+ /* ─ Score Breakdown Definitions ────────────────────────────── */
1579
+
1580
+ const SIGNAL_DEFS = [
1581
+ { key: "titlePattern", label: "Title Pattern", max: 10, color: "#3b82f6" },
1582
+ { key: "scope", label: "Scope", max: 6, color: "#22c55e" },
1583
+ { key: "tags", label: "Tags", max: 6, color: "#eab308" },
1584
+ { key: "voice", label: "Voice", max: 3, color: "#a855f7" },
1585
+ { key: "paths", label: "Paths", max: 8, color: "#f97316" },
1586
+ { key: "repoCtx", label: "Repo Ctx", max: 6, color: "#14b8a6" },
1587
+ { key: "fileType", label: "File Type", max: 4, color: "#ec4899" },
1588
+ { key: "descMatch", label: "Desc Match", max: 8, color: "#06b6d4" },
1589
+ { key: "taskType", label: "Task Type", max: 5, color: "#6366f1" },
1590
+ ];
1591
+
1592
+ const MAX_SCORE_TOTAL = SIGNAL_DEFS.reduce((sum, s) => sum + s.max, 0);
1593
+
1533
1594
  /* ─ Profile Matcher Panel ─────────────────────────────────── */
1534
1595
 
1535
1596
  function ProfileMatcher() {
@@ -1542,6 +1603,9 @@ function ProfileMatcher() {
1542
1603
  const [importSkills, setImportSkills] = useState(true);
1543
1604
  const [importPrompts, setImportPrompts] = useState(true);
1544
1605
  const [importTools, setImportTools] = useState(true);
1606
+ const [expandedAlts, setExpandedAlts] = useState({});
1607
+ const [workflowSteps, setWorkflowSteps] = useState(null);
1608
+ const [workflowLoading, setWorkflowLoading] = useState(false);
1545
1609
 
1546
1610
  // ── Execution Plan state ──────────────────────────────────────────────
1547
1611
  const [execPlan, setExecPlan] = useState(null);
@@ -1558,11 +1622,10 @@ function ProfileMatcher() {
1558
1622
  useEffect(() => {
1559
1623
  setTaskListLoading(true);
1560
1624
  const wsParam = typeof window !== "undefined" && window.__bosunWorkspaceId ? `&workspace=${encodeURIComponent(window.__bosunWorkspaceId)}` : "";
1561
- fetch(`/api/tasks?status=todo,backlog,in-progress,blocked${wsParam}`)
1625
+ fetch(`/api/tasks?pageSize=100&status=library${wsParam}`)
1562
1626
  .then((r) => r.json())
1563
1627
  .then((data) => {
1564
- const tasks = Array.isArray(data?.tasks) ? data.tasks : Array.isArray(data) ? data : [];
1565
- setTaskList(tasks.slice(0, 100));
1628
+ setTaskList(extractSelectableLibraryTasks(data));
1566
1629
  })
1567
1630
  .catch(() => {})
1568
1631
  .finally(() => setTaskListLoading(false));
@@ -1677,27 +1740,140 @@ function ProfileMatcher() {
1677
1740
  <//>
1678
1741
  </div>
1679
1742
 
1680
- ${/* ── Library Profile Match Results ── */ ""}
1743
+ ${/* ── Library Profile Match Results (Rich Visualization) ── */ ""}
1681
1744
  ${best && html`
1682
- <div class="library-profile-match" style="margin-top:8px;">
1683
- <div class="library-profile-match-label">Best match:</div>
1684
- <div>
1685
- <span class="library-profile-match-name">${iconText(`${TYPE_ICONS.agent} ${best.name}`)}</span>
1686
- <span class="library-profile-match-score">score: ${best.score} | confidence: ${Math.round(Number(best.confidence || 0) * 100)}%</span>
1745
+ <div class="library-profile-match" style="margin-top:12px;padding:12px;border:1px solid var(--border,#333);border-radius:10px;background:var(--bg-card,rgba(255,255,255,0.03));">
1746
+ ${/* ── Header with name, score, confidence ── */ ""}
1747
+ <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
1748
+ <span style="font-weight:600;font-size:0.95em;">${iconText(`${TYPE_ICONS.agent} ${best.name}`)}</span>
1749
+ <${Chip} size="small" label=${`Score: ${best.score}/${MAX_SCORE_TOTAL}`} color="primary" variant="outlined" />
1750
+ <${Chip} size="small" label=${`${Math.round(Number(best.confidence || 0) * 100)}% confidence`}
1751
+ color=${Number(best.confidence || 0) >= 0.45 ? "success" : "warning"} variant="outlined" />
1687
1752
  </div>
1688
- <div style="font-size:0.8em;color:var(--text-secondary);margin-top:4px;">auto-trigger: ${auto.shouldAutoApply ? "eligible" : "not eligible"} (${auto.reason || "n/a"})</div>
1689
- ${best.description && html`
1690
- <div style="font-size:0.8em;color:var(--text-secondary);margin-top:4px;">${best.description}</div>
1691
- `}
1692
- ${Array.isArray(best.reasons) && best.reasons.length > 0 && html`
1693
- <div style="font-size:0.78em;color:var(--text-secondary);margin-top:4px;">reasons: ${best.reasons.join(", ")}</div>
1753
+ ${best.description && html`<div style="font-size:0.82em;color:var(--text-secondary);margin-top:6px;">${best.description}</div>`}
1754
+
1755
+ ${/* ── Auto-trigger status ── */ ""}
1756
+ <div style="display:flex;align-items:center;gap:6px;margin-top:8px;padding:6px 10px;border-radius:8px;background:${auto.shouldAutoApply ? "rgba(34,197,94,0.1)" : Number(best.confidence || 0) >= 0.35 ? "rgba(245,158,11,0.1)" : "rgba(239,68,68,0.1)"};">
1757
+ <span style="font-size:1.1em;">${auto.shouldAutoApply ? "✅" : Number(best.confidence || 0) >= 0.35 ? "⚠️" : "❌"}</span>
1758
+ <span style="font-size:0.82em;font-weight:500;color:${auto.shouldAutoApply ? "#22c55e" : Number(best.confidence || 0) >= 0.35 ? "#f59e0b" : "#ef4444"};">
1759
+ Auto-trigger: ${auto.shouldAutoApply ? "Eligible" : "Not eligible"}
1760
+ </span>
1761
+ <span style="font-size:0.75em;color:var(--text-secondary);">(${auto.reason || "n/a"})</span>
1762
+ </div>
1763
+
1764
+ ${/* ── Score Breakdown Bar ── */ ""}
1765
+ <div style="margin-top:10px;">
1766
+ <div style="font-size:0.78em;font-weight:600;color:var(--text-secondary);margin-bottom:4px;">Score Breakdown</div>
1767
+ <div style="display:flex;height:22px;border-radius:6px;overflow:hidden;background:rgba(255,255,255,0.05);border:1px solid var(--border,#333);">
1768
+ ${SIGNAL_DEFS.map((sig) => {
1769
+ const val = Number(best.breakdown?.[sig.key] || 0);
1770
+ const pct = (sig.max / MAX_SCORE_TOTAL) * 100;
1771
+ return html`
1772
+ <${Tooltip} title=${`${sig.label}: ${val}/${sig.max}`} key=${sig.key}>
1773
+ <div style="width:${pct}%;height:100%;position:relative;border-right:1px solid rgba(0,0,0,0.2);">
1774
+ <div style="position:absolute;bottom:0;left:0;right:0;height:${sig.max > 0 ? (val / sig.max) * 100 : 0}%;background:${sig.color};opacity:0.85;transition:height 0.3s;"></div>
1775
+ <div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:0.6em;color:#fff;white-space:nowrap;text-shadow:0 1px 2px rgba(0,0,0,0.6);z-index:1;">${val > 0 ? val : ""}</div>
1776
+ </div>
1777
+ <//>
1778
+ `;
1779
+ })}
1780
+ </div>
1781
+ <div style="display:flex;flex-wrap:wrap;gap:6px;margin-top:4px;">
1782
+ ${SIGNAL_DEFS.map((sig) => {
1783
+ const val = Number(best.breakdown?.[sig.key] || 0);
1784
+ return html`<span key=${sig.key} style="font-size:0.65em;display:flex;align-items:center;gap:3px;color:var(--text-secondary);">
1785
+ <span style="display:inline-block;width:8px;height:8px;border-radius:2px;background:${sig.color};opacity:${val > 0 ? 1 : 0.3};"></span>
1786
+ ${sig.label}
1787
+ </span>`;
1788
+ })}
1789
+ </div>
1790
+ </div>
1791
+
1792
+ ${/* ── Detected Task Types ── */ ""}
1793
+ ${result?.context?.detectedTaskTypes?.length > 0 && html`
1794
+ <div style="margin-top:8px;display:flex;align-items:center;gap:6px;flex-wrap:wrap;">
1795
+ <span style="font-size:0.78em;color:var(--text-secondary);font-weight:500;">Detected:</span>
1796
+ ${result.context.detectedTaskTypes.map((t) => html`
1797
+ <${Chip} key=${t} size="small" label=${t} variant="outlined"
1798
+ style=${{ fontSize: "0.72em", height: "20px" }} />
1799
+ `)}
1800
+ </div>
1694
1801
  `}
1802
+
1803
+ ${/* ── Skills Preview ── */ ""}
1695
1804
  ${plan && html`
1696
- <div style="font-size:0.78em;color:var(--text-secondary);margin-top:6px;">prompt: ${plan.prompt?.name || "none"} | skills: ${(plan.skillIds || []).slice(0, 4).join(", ") || "none"}</div>
1697
- <div style="font-size:0.78em;color:var(--text-secondary);margin-top:4px;">builtin tools: ${(plan.builtinToolIds || []).slice(0, 6).join(", ") || "none"} | MCP: ${(plan.enabledMcpServers || []).slice(0, 4).join(", ") || "none"}</div>
1805
+ <div style="margin-top:10px;padding:8px 10px;border:1px solid var(--border,#333);border-radius:8px;background:rgba(255,255,255,0.02);">
1806
+ <div style="font-size:0.8em;font-weight:600;margin-bottom:4px;">📚 Skills (${(plan.skillIds || []).length} resolved)</div>
1807
+ ${(plan.skillIds || []).length > 0 ? (plan.skillIds || []).map((s) => html`
1808
+ <div key=${s} style="font-size:0.78em;color:var(--text-secondary);padding:2px 0;"> ✓ ${s} (profile-skill)</div>
1809
+ `) : html`<div style="font-size:0.78em;color:var(--text-secondary);"> No skills resolved</div>`}
1810
+ <div style="font-size:0.78em;color:var(--text-secondary);margin-top:4px;">
1811
+ Prompt: ${plan.prompt?.name || "none"} · Tools: ${(plan.builtinToolIds || []).slice(0, 6).join(", ") || "none"} · MCP: ${(plan.enabledMcpServers || []).slice(0, 4).join(", ") || "none"}
1812
+ </div>
1813
+ </div>
1698
1814
  `}
1815
+
1816
+ ${/* ── Workflow Step Preview ── */ ""}
1817
+ <div style="margin-top:10px;">
1818
+ <div style="display:flex;align-items:center;gap:8px;">
1819
+ <span style="font-size:0.8em;font-weight:600;">Workflow Steps</span>
1820
+ <${Button} variant="text" size="small" onClick=${async () => {
1821
+ if (!title.trim()) return;
1822
+ setWorkflowLoading(true);
1823
+ try {
1824
+ const steps = await resolveWorkflowSteps(title.trim(), description.trim());
1825
+ setWorkflowSteps(steps);
1826
+ } catch { setWorkflowSteps(null); }
1827
+ setWorkflowLoading(false);
1828
+ }} disabled=${workflowLoading || !title.trim()}
1829
+ style=${{ fontSize: "0.72em", minWidth: 0, padding: "2px 8px" }}>
1830
+ ${workflowLoading ? html`<${Spinner} size=${12} />` : "Resolve"}
1831
+ <//>
1832
+ </div>
1833
+ ${workflowSteps && html`
1834
+ <div style="margin-top:4px;padding:8px 10px;border-left:3px solid var(--border,#555);font-size:0.78em;font-family:monospace;">
1835
+ ${Object.entries(workflowSteps).map(([step, stepResult], i, arr) => {
1836
+ const connector = i === 0 ? "┌" : i === arr.length - 1 ? "└" : "├";
1837
+ const stepBest = stepResult?.best;
1838
+ const stepPlan = stepResult?.plan;
1839
+ const skills = stepPlan?.skillIds?.slice(0, 3)?.join(", ") || "none";
1840
+ return html`<div key=${step} style="color:var(--text-secondary);padding:1px 0;">
1841
+ ${connector} <span style="text-transform:capitalize;font-weight:500;color:var(--text-primary);">${step}</span> → ${stepBest?.name || "No match"} + [${skills}]
1842
+ </div>`;
1843
+ })}
1844
+ </div>
1845
+ `}
1846
+ </div>
1847
+
1848
+ ${/* ── Alternatives as expandable cards ── */ ""}
1699
1849
  ${candidates.length > 1 && html`
1700
- <div style="font-size:0.78em;color:var(--text-secondary);margin-top:6px;">alternatives: ${candidates.slice(1, 4).map((c) => `${c.name} (${c.score})`).join(" | ")}</div>
1850
+ <div style="margin-top:10px;">
1851
+ <div style="font-size:0.8em;font-weight:600;color:var(--text-secondary);margin-bottom:6px;">Alternatives (${candidates.length - 1})</div>
1852
+ ${candidates.slice(1).map((c, i) => html`
1853
+ <div key=${c.id || i} style="margin-bottom:6px;border:1px solid var(--border,#333);border-radius:8px;overflow:hidden;background:rgba(255,255,255,0.02);">
1854
+ <div onClick=${() => setExpandedAlts((prev) => ({ ...prev, [i]: !prev[i] }))}
1855
+ style="display:flex;align-items:center;gap:8px;padding:6px 10px;cursor:pointer;user-select:none;">
1856
+ <span style="font-size:0.82em;font-weight:500;flex:1;">${iconText(`${TYPE_ICONS.agent} ${c.name}`)}</span>
1857
+ <div style="width:60px;height:6px;border-radius:3px;background:rgba(255,255,255,0.08);overflow:hidden;">
1858
+ <div style="height:100%;width:${Math.round(Number(c.confidence || 0) * 100)}%;background:#3b82f6;border-radius:3px;"></div>
1859
+ </div>
1860
+ <span style="font-size:0.72em;color:var(--text-secondary);min-width:40px;text-align:right;">${c.score}pts</span>
1861
+ <span style="font-size:0.7em;transform:${expandedAlts[i] ? "rotate(180deg)" : "none"};transition:transform 0.2s;">▼</span>
1862
+ </div>
1863
+ ${expandedAlts[i] && html`
1864
+ <div style="padding:6px 10px 8px;border-top:1px solid var(--border,#333);font-size:0.78em;color:var(--text-secondary);">
1865
+ <div>Confidence: ${Math.round(Number(c.confidence || 0) * 100)}%</div>
1866
+ ${c.description && html`<div style="margin-top:2px;">${c.description}</div>`}
1867
+ ${Array.isArray(c.reasons) && c.reasons.length > 0 && html`
1868
+ <div style="margin-top:4px;display:flex;flex-wrap:wrap;gap:4px;">
1869
+ ${c.reasons.map((r) => html`<${Chip} key=${r} size="small" label=${r} variant="outlined" style=${{ fontSize: "0.68em", height: "18px" }} />`)}
1870
+ </div>
1871
+ `}
1872
+ </div>
1873
+ `}
1874
+ </div>
1875
+ `)}
1876
+ </div>
1701
1877
  `}
1702
1878
  </div>
1703
1879
  `}
@@ -2245,9 +2421,295 @@ function AgentLibraryImporter({ onImported }) {
2245
2421
  `;
2246
2422
  }
2247
2423
 
2248
- /* ═══════════════════════════════════════════════════════════════
2249
- * Main Library Tab
2250
- * ═══════════════════════════════════════════════════════════════ */
2424
+ /* ─ Library Marketplace ───────────────────────────────────── */
2425
+
2426
+ const MARKETPLACE_CATEGORIES = [
2427
+ { id: "all", label: "All" },
2428
+ { id: "official", label: "Official" },
2429
+ { id: "community", label: "Community" },
2430
+ { id: "agents", label: "Agents" },
2431
+ { id: "skills", label: "Skills" },
2432
+ { id: "mcp", label: "MCP" },
2433
+ ];
2434
+
2435
+ function getTrustTier(source) {
2436
+ const tier = String(source?.trustTier || "").toLowerCase();
2437
+ if (tier === "official" || tier === "partner") return { label: "Official", color: "#22c55e", icon: "🏢" };
2438
+ if (tier === "community") return { label: "Community", color: "#3b82f6", icon: "👥" };
2439
+ return { label: "Unknown", color: "#6b7280", icon: "❔" };
2440
+ }
2441
+
2442
+ function LibraryMarketplace({ onImported }) {
2443
+ const [sources, setSources] = useState([]);
2444
+ const [loading, setLoading] = useState(true);
2445
+ const [searchText, setSearchText] = useState("");
2446
+ const [activeCategory, setActiveCategory] = useState("all");
2447
+ const [expandedSource, setExpandedSource] = useState(null);
2448
+ const [previewData, setPreviewData] = useState(null);
2449
+ const [previewSourceId, setPreviewSourceId] = useState(null);
2450
+ const [scanning, setScanning] = useState(null);
2451
+ const [importing, setImporting] = useState(null);
2452
+ const [importedSources, setImportedSources] = useState(new Set());
2453
+ const [customUrl, setCustomUrl] = useState("");
2454
+ const [customBranch, setCustomBranch] = useState("main");
2455
+ const [showCustom, setShowCustom] = useState(false);
2456
+
2457
+ useEffect(() => {
2458
+ let alive = true;
2459
+ setLoading(true);
2460
+ fetchLibrarySources()
2461
+ .then((data) => {
2462
+ if (!alive) return;
2463
+ setSources(Array.isArray(data) ? data : []);
2464
+ })
2465
+ .catch(() => {})
2466
+ .finally(() => setLoading(false));
2467
+ return () => { alive = false; };
2468
+ }, []);
2469
+
2470
+ const filteredSources = useMemo(() => {
2471
+ let list = [...sources];
2472
+ if (searchText.trim()) {
2473
+ const q = searchText.trim().toLowerCase();
2474
+ list = list.filter((s) => {
2475
+ const haystack = `${s.name || ""} ${s.description || ""} ${(s.focuses || []).join(" ")} ${s.id || ""}`.toLowerCase();
2476
+ return haystack.includes(q);
2477
+ });
2478
+ }
2479
+ if (activeCategory !== "all") {
2480
+ if (activeCategory === "official") {
2481
+ list = list.filter((s) => s.trustTier === "official" || s.trustTier === "partner");
2482
+ } else if (activeCategory === "community") {
2483
+ list = list.filter((s) => s.trustTier === "community" || !s.trustTier);
2484
+ } else if (activeCategory === "agents" || activeCategory === "skills" || activeCategory === "mcp") {
2485
+ list = list.filter((s) => {
2486
+ const focuses = (s.focuses || []).map((f) => f.toLowerCase());
2487
+ return focuses.includes(activeCategory) || String(s.description || "").toLowerCase().includes(activeCategory);
2488
+ });
2489
+ }
2490
+ }
2491
+ return list.sort((a, b) => (Number(b.estimatedPlugins || 0) - Number(a.estimatedPlugins || 0)) || String(a.name || "").localeCompare(String(b.name || "")));
2492
+ }, [sources, searchText, activeCategory]);
2493
+
2494
+ const doPreviewSource = useCallback(async (sourceId) => {
2495
+ setScanning(sourceId);
2496
+ try {
2497
+ const res = await previewLibrarySource({ sourceId });
2498
+ if (!res?.ok) throw new Error(res?.error || "Preview failed");
2499
+ setPreviewData(res?.data);
2500
+ setPreviewSourceId(sourceId);
2501
+ } catch (err) {
2502
+ showToast(`Preview failed: ${parseApiError(err)}`, "error");
2503
+ }
2504
+ setScanning(null);
2505
+ }, []);
2506
+
2507
+ const doImportSource = useCallback(async (sourceId, selectedPaths) => {
2508
+ setImporting(sourceId);
2509
+ try {
2510
+ const payload = {
2511
+ sourceId,
2512
+ importAgents: true,
2513
+ importSkills: true,
2514
+ importPrompts: true,
2515
+ importTools: true,
2516
+ includeEntries: selectedPaths,
2517
+ };
2518
+ const res = await importLibrarySource(payload);
2519
+ if (!res?.ok) throw new Error(res?.error || "Import failed");
2520
+ const count = Number(res?.data?.importedCount || 0);
2521
+ const byType = res?.data?.importedByType || {};
2522
+ const details = [
2523
+ `agents ${Number(byType?.agent || 0)}`,
2524
+ `prompts ${Number(byType?.prompt || 0)}`,
2525
+ `skills ${Number(byType?.skill || 0)}`,
2526
+ `tools ${Number(byType?.mcp || 0)}`,
2527
+ ].join(", ");
2528
+ showToast(`Imported ${count} entries (${details})`, "success");
2529
+ setImportedSources((prev) => new Set([...prev, sourceId]));
2530
+ setPreviewData(null);
2531
+ setPreviewSourceId(null);
2532
+ if (typeof onImported === "function") onImported();
2533
+ } catch (err) {
2534
+ showToast(`Import failed: ${parseApiError(err)}`, "error");
2535
+ }
2536
+ setImporting(null);
2537
+ }, [onImported]);
2538
+
2539
+ const doImportAll = useCallback(async (sourceId) => {
2540
+ setImporting(sourceId);
2541
+ try {
2542
+ const res = await importLibrarySource({
2543
+ sourceId,
2544
+ importAgents: true,
2545
+ importSkills: true,
2546
+ importPrompts: true,
2547
+ importTools: true,
2548
+ });
2549
+ if (!res?.ok) throw new Error(res?.error || "Import failed");
2550
+ const count = Number(res?.data?.importedCount || 0);
2551
+ showToast(`Imported ${count} entries from ${sourceId}`, "success");
2552
+ setImportedSources((prev) => new Set([...prev, sourceId]));
2553
+ if (typeof onImported === "function") onImported();
2554
+ } catch (err) {
2555
+ showToast(`Import failed: ${parseApiError(err)}`, "error");
2556
+ }
2557
+ setImporting(null);
2558
+ }, [onImported]);
2559
+
2560
+ const doCustomImport = useCallback(async () => {
2561
+ if (!customUrl.trim()) return;
2562
+ setScanning("custom");
2563
+ try {
2564
+ const res = await previewLibrarySource({ repoUrl: customUrl.trim(), branch: customBranch.trim() || "main" });
2565
+ if (!res?.ok) throw new Error(res?.error || "Preview failed");
2566
+ setPreviewData(res?.data);
2567
+ setPreviewSourceId("custom");
2568
+ } catch (err) {
2569
+ showToast(`Preview failed: ${parseApiError(err)}`, "error");
2570
+ }
2571
+ setScanning(null);
2572
+ }, [customUrl, customBranch]);
2573
+
2574
+ return html`
2575
+ <div style="margin-top:10px;padding:12px;border:1px solid var(--border,#333);border-radius:10px;">
2576
+ <div style="display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;margin-bottom:10px;">
2577
+ <div style="font-size:0.95em;font-weight:600;">${iconText(":package: Library Marketplace")}</div>
2578
+ <${Button} variant="text" size="small" onClick=${() => setShowCustom((v) => !v)}
2579
+ style=${{ fontSize: "0.75em", textTransform: "none" }}>
2580
+ ${showCustom ? "Hide Custom URL" : "Custom URL Import"}
2581
+ <//>
2582
+ </div>
2583
+
2584
+ ${/* ── Search Bar ── */ ""}
2585
+ <input type="text" value=${searchText} onInput=${(e) => setSearchText(e.currentTarget.value)}
2586
+ placeholder="Search sources, descriptions, focuses…"
2587
+ style="width:100%;padding:8px 12px;border-radius:8px;border:1px solid var(--border,#333);background:var(--bg-input,#0d1117);color:var(--text-primary,#eee);margin-bottom:8px;font-size:0.85em;" />
2588
+
2589
+ ${/* ── Category Filter Pills ── */ ""}
2590
+ <div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px;">
2591
+ ${MARKETPLACE_CATEGORIES.map((cat) => html`
2592
+ <${Chip} key=${cat.id} label=${cat.label} size="small"
2593
+ variant=${activeCategory === cat.id ? "filled" : "outlined"}
2594
+ color=${activeCategory === cat.id ? "primary" : "default"}
2595
+ onClick=${() => setActiveCategory(cat.id)}
2596
+ style=${{ cursor: "pointer", fontSize: "0.78em" }} />
2597
+ `)}
2598
+ </div>
2599
+
2600
+ ${/* ── Custom URL Import ── */ ""}
2601
+ ${showCustom && html`
2602
+ <div style="margin-bottom:10px;padding:10px;border:1px solid var(--border,#333);border-radius:8px;background:rgba(255,255,255,0.02);">
2603
+ <div style="font-size:0.82em;font-weight:500;margin-bottom:6px;">Custom Repository Import</div>
2604
+ <div style="display:grid;grid-template-columns:1fr auto;gap:8px;align-items:end;">
2605
+ <input type="text" value=${customUrl} onInput=${(e) => setCustomUrl(e.currentTarget.value)}
2606
+ placeholder="https://github.com/org/repo.git"
2607
+ style="width:100%;padding:6px 10px;border-radius:6px;border:1px solid var(--border,#333);background:var(--bg-input,#0d1117);color:var(--text-primary);font-size:0.82em;" />
2608
+ <input type="text" value=${customBranch} onInput=${(e) => setCustomBranch(e.currentTarget.value)}
2609
+ placeholder="main" style="width:80px;padding:6px 10px;border-radius:6px;border:1px solid var(--border,#333);background:var(--bg-input,#0d1117);color:var(--text-primary);font-size:0.82em;" />
2610
+ </div>
2611
+ <div style="margin-top:6px;">
2612
+ <${Button} variant="outlined" size="small" onClick=${doCustomImport} disabled=${scanning === "custom" || !customUrl.trim()}>
2613
+ ${scanning === "custom" ? html`<${Spinner} size=${14} /> Scanning…` : iconText(":mag: Preview")}
2614
+ <//>
2615
+ </div>
2616
+ </div>
2617
+ `}
2618
+
2619
+ ${/* ── Loading State ── */ ""}
2620
+ ${loading && html`
2621
+ <div style="text-align:center;padding:20px;">
2622
+ <${Spinner} size=${24} />
2623
+ <div style="font-size:0.82em;color:var(--text-secondary);margin-top:8px;">Loading marketplace sources…</div>
2624
+ </div>
2625
+ `}
2626
+
2627
+ ${/* ── Source Cards Grid ── */ ""}
2628
+ ${!loading && html`
2629
+ <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:10px;">
2630
+ ${filteredSources.map((source) => {
2631
+ const trust = getTrustTier(source);
2632
+ const trustScore = Number(source?.trust?.score || 0);
2633
+ const est = Number(source.estimatedPlugins || 0);
2634
+ const isImported = importedSources.has(source.id);
2635
+ const isScanning = scanning === source.id;
2636
+ const isImporting = importing === source.id;
2637
+ const isExpanded = expandedSource === source.id;
2638
+ const focuses = (source.focuses || []).slice(0, 4);
2639
+
2640
+ return html`
2641
+ <div key=${source.id} style="border:1px solid var(--border,#333);border-radius:10px;overflow:hidden;background:var(--bg-card,rgba(255,255,255,0.03));transition:border-color 0.2s;${isImported ? "border-color:#22c55e;" : ""}">
2642
+ <div style="padding:10px 12px;">
2643
+ ${/* ── Card Header ── */ ""}
2644
+ <div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-bottom:6px;">
2645
+ <span style="font-size:1em;">${trust.icon}</span>
2646
+ <span style="font-size:0.88em;font-weight:600;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${source.name}</span>
2647
+ <span style="font-size:0.68em;padding:2px 8px;border-radius:999px;background:${trust.color}22;color:${trust.color};font-weight:500;">${trust.label}</span>
2648
+ ${isImported && html`<span style="font-size:0.68em;padding:2px 8px;border-radius:999px;background:rgba(34,197,94,0.15);color:#22c55e;">✓ Imported</span>`}
2649
+ </div>
2650
+
2651
+ ${/* ── Stats and description ── */ ""}
2652
+ <div style="font-size:0.78em;color:var(--text-secondary);margin-bottom:4px;">
2653
+ ${est > 0 ? `~${est} plugins` : ""}${est > 0 && focuses.length > 0 ? " · " : ""}${focuses.join(", ")}
2654
+ </div>
2655
+ ${source.description && html`
2656
+ <div style="font-size:0.78em;color:var(--text-secondary);margin-bottom:6px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">${source.description}</div>
2657
+ `}
2658
+
2659
+ ${/* ── Trust Score Bar ── */ ""}
2660
+ <div style="display:flex;align-items:center;gap:6px;margin-bottom:8px;">
2661
+ <span style="font-size:0.72em;color:var(--text-secondary);min-width:55px;">Trust ${trustScore}/100</span>
2662
+ <div style="flex:1;height:4px;border-radius:2px;background:rgba(255,255,255,0.08);overflow:hidden;">
2663
+ <div style="height:100%;width:${trustScore}%;border-radius:2px;background:${trustScore >= 70 ? "#22c55e" : trustScore >= 40 ? "#f59e0b" : "#ef4444"};"></div>
2664
+ </div>
2665
+ </div>
2666
+
2667
+ ${/* ── Action Buttons ── */ ""}
2668
+ <div style="display:flex;gap:6px;flex-wrap:wrap;">
2669
+ <${Button} variant="outlined" size="small" onClick=${() => doPreviewSource(source.id)} disabled=${isScanning || isImporting || source.enabled === false}
2670
+ style=${{ fontSize: "0.72em", padding: "3px 10px", textTransform: "none" }}>
2671
+ ${isScanning ? html`<${Spinner} size=${12} />` : "Preview"}
2672
+ <//>
2673
+ <${Button} variant="outlined" size="small" onClick=${() => doImportAll(source.id)} disabled=${isScanning || isImporting || source.enabled === false}
2674
+ style=${{ fontSize: "0.72em", padding: "3px 10px", textTransform: "none" }}>
2675
+ ${isImporting ? html`<${Spinner} size=${12} />` : "Import All"}
2676
+ <//>
2677
+ </div>
2678
+
2679
+ ${/* ── Low trust warning ── */ ""}
2680
+ ${source?.trust?.lowTrust && source.enabled !== false ? html`
2681
+ <div style="font-size:0.72em;color:#f59e0b;margin-top:6px;padding:3px 6px;border-radius:4px;background:rgba(245,158,11,0.08);">⚠ Low trust — review before use</div>
2682
+ ` : null}
2683
+ ${source.enabled === false ? html`
2684
+ <div style="font-size:0.72em;color:#ef4444;margin-top:6px;padding:3px 6px;border-radius:4px;background:rgba(239,68,68,0.08);">Unavailable</div>
2685
+ ` : null}
2686
+ </div>
2687
+ </div>
2688
+ `;
2689
+ })}
2690
+ </div>
2691
+ ${filteredSources.length === 0 && html`
2692
+ <div style="text-align:center;padding:20px;color:var(--text-secondary);font-size:0.85em;">
2693
+ No sources match your search.
2694
+ </div>
2695
+ `}
2696
+ `}
2697
+
2698
+ ${/* ── Preview Modal ── */ ""}
2699
+ ${previewData ? html`
2700
+ <${ImportPreviewModal}
2701
+ candidates=${previewData.candidates}
2702
+ source=${previewData.source}
2703
+ duplicates=${previewData.duplicates}
2704
+ intraDuplicates=${previewData.intraDuplicates}
2705
+ onConfirm=${(selected) => doImportSource(previewSourceId, selected)}
2706
+ onClose=${() => { setPreviewData(null); setPreviewSourceId(null); }}
2707
+ loading=${importing != null}
2708
+ />
2709
+ ` : null}
2710
+ </div>
2711
+ `;
2712
+ }
2251
2713
 
2252
2714
  export function LibraryTab() {
2253
2715
  injectStyles();
@@ -2401,7 +2863,7 @@ export function LibraryTab() {
2401
2863
  </div>
2402
2864
 
2403
2865
  ${filterType.value !== "mcp" && html`<${ProfileMatcher} />`}
2404
- ${filterType.value !== "mcp" && html`<${AgentLibraryImporter} onImported=${loadEntries} />`}
2866
+ ${filterType.value !== "mcp" && html`<${LibraryMarketplace} onImported=${loadEntries} />`}
2405
2867
  ${filterType.value !== "mcp" && html`<${ScopeDetector} />`}
2406
2868
 
2407
2869
  ${/* ── MCP Marketplace View ── */