bosun 0.41.2 → 0.41.3

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 (71) hide show
  1. package/.env.example +1 -1
  2. package/agent/agent-prompt-catalog.mjs +971 -0
  3. package/agent/agent-prompts.mjs +2 -970
  4. package/agent/agent-supervisor.mjs +6 -3
  5. package/agent/autofix-git.mjs +33 -0
  6. package/agent/autofix-prompts.mjs +151 -0
  7. package/agent/autofix.mjs +11 -175
  8. package/agent/bosun-skills.mjs +3 -2
  9. package/bosun.config.example.json +17 -0
  10. package/bosun.schema.json +87 -188
  11. package/cli.mjs +34 -1
  12. package/config/config-doctor.mjs +5 -250
  13. package/config/config-file-names.mjs +5 -0
  14. package/config/config.mjs +89 -493
  15. package/config/executor-config.mjs +493 -0
  16. package/config/repo-root.mjs +1 -2
  17. package/config/workspace-health.mjs +242 -0
  18. package/git/git-safety.mjs +15 -0
  19. package/github/github-oauth-portal.mjs +46 -0
  20. package/infra/library-manager-utils.mjs +22 -0
  21. package/infra/library-manager-well-known-sources.mjs +578 -0
  22. package/infra/library-manager.mjs +512 -1030
  23. package/infra/monitor.mjs +28 -9
  24. package/infra/session-tracker.mjs +10 -7
  25. package/kanban/kanban-adapter.mjs +17 -1
  26. package/lib/codebase-audit-manifests.mjs +117 -0
  27. package/lib/codebase-audit.mjs +18 -115
  28. package/package.json +18 -3
  29. package/server/ui-server.mjs +1194 -79
  30. package/shell/codex-config-file.mjs +178 -0
  31. package/shell/codex-config.mjs +538 -575
  32. package/task/task-cli.mjs +54 -3
  33. package/task/task-executor.mjs +143 -13
  34. package/task/task-store.mjs +409 -1
  35. package/telegram/telegram-bot.mjs +127 -0
  36. package/tools/apply-pr-suggestions.mjs +401 -0
  37. package/tools/syntax-check.mjs +21 -9
  38. package/ui/app.js +3 -14
  39. package/ui/components/kanban-board.js +227 -4
  40. package/ui/components/session-list.js +85 -5
  41. package/ui/demo-defaults.js +334 -80
  42. package/ui/demo.html +155 -0
  43. package/ui/modules/session-api.js +96 -0
  44. package/ui/modules/settings-schema.js +1 -2
  45. package/ui/modules/state.js +21 -3
  46. package/ui/setup.html +4 -5
  47. package/ui/styles/components.css +58 -4
  48. package/ui/tabs/agents.js +12 -15
  49. package/ui/tabs/control.js +1 -0
  50. package/ui/tabs/library.js +484 -22
  51. package/ui/tabs/manual-flows.js +105 -29
  52. package/ui/tabs/tasks.js +785 -140
  53. package/ui/tabs/telemetry.js +129 -11
  54. package/ui/tabs/workflow-canvas-utils.mjs +130 -0
  55. package/ui/tabs/workflows.js +293 -23
  56. package/voice/voice-tool-definitions.mjs +757 -0
  57. package/voice/voice-tools.mjs +34 -778
  58. package/workflow/manual-flow-audit.mjs +165 -0
  59. package/workflow/manual-flows.mjs +164 -259
  60. package/workflow/workflow-engine.mjs +147 -58
  61. package/workflow/workflow-nodes/definitions.mjs +1207 -0
  62. package/workflow/workflow-nodes/transforms.mjs +612 -0
  63. package/workflow/workflow-nodes.mjs +304 -52
  64. package/workflow/workflow-templates.mjs +313 -191
  65. package/workflow-templates/_helpers.mjs +154 -0
  66. package/workflow-templates/agents.mjs +61 -4
  67. package/workflow-templates/code-quality.mjs +7 -7
  68. package/workflow-templates/github.mjs +20 -10
  69. package/workflow-templates/task-batch.mjs +20 -9
  70. package/workflow-templates/task-lifecycle.mjs +31 -6
  71. package/workspace/worktree-manager.mjs +277 -3
@@ -20,6 +20,14 @@ import { resolve, basename, relative, extname, sep } from "node:path";
20
20
  import { execSync, spawnSync } from "node:child_process";
21
21
  import { homedir } from "node:os";
22
22
  import { getAgentToolConfig, getEffectiveTools } from "../agent/agent-tool-config.mjs";
23
+ import { nowISO, toStringArray, uniqueStrings } from "./library-manager-utils.mjs";
24
+ import {
25
+ WELL_KNOWN_AGENT_SOURCES,
26
+ computeWellKnownSourceTrust,
27
+ listWellKnownAgentSources,
28
+ clearWellKnownAgentSourceProbeCache,
29
+ probeWellKnownAgentSources,
30
+ } from "./library-manager-well-known-sources.mjs";
23
31
 
24
32
  // ── Constants ─────────────────────────────────────────────────────────────────
25
33
 
@@ -35,7 +43,6 @@ export const SKILL_ENTRY_INDEX = "skills.json";
35
43
 
36
44
  const agentProfileIndexCache = new Map();
37
45
  const skillEntryIndexCache = new Map();
38
- const wellKnownSourceProbeCache = new Map();
39
46
  const repoContextCache = new Map();
40
47
 
41
48
  const REPO_CONTEXT_TTL_MS = 120_000;
@@ -365,10 +372,6 @@ function slugify(name) {
365
372
  .replace(/^-|-$/g, "");
366
373
  }
367
374
 
368
- function nowISO() {
369
- return new Date().toISOString();
370
- }
371
-
372
375
  function isSafeGitRefName(value) {
373
376
  const ref = String(value || "").trim();
374
377
  if (!ref) return false;
@@ -395,25 +398,6 @@ function isSafeGitRepositorySource(value) {
395
398
  }
396
399
  }
397
400
 
398
- function toStringArray(input) {
399
- if (!Array.isArray(input)) return [];
400
- return input.map((item) => String(item || '').trim()).filter(Boolean);
401
- }
402
-
403
- function uniqueStrings(values = []) {
404
- const out = [];
405
- const seen = new Set();
406
- for (const raw of values) {
407
- const value = String(raw || '').trim();
408
- if (!value) continue;
409
- const key = value.toLowerCase();
410
- if (seen.has(key)) continue;
411
- seen.add(key);
412
- out.push(value);
413
- }
414
- return out;
415
- }
416
-
417
401
  function keywordTokens(value, { minLength = 3 } = {}) {
418
402
  return uniqueStrings(
419
403
  String(value || '')
@@ -1393,8 +1377,23 @@ export function matchAgentProfiles(rootDir, criteria = {}, opts = {}) {
1393
1377
  ...(fileSignals ? [...fileSignals.fileDomains, ...fileSignals.fileLanguages] : []),
1394
1378
  ].map((d) => d.toLowerCase()));
1395
1379
 
1396
- // Max theoretical score: 10 + 6 + 6 + 3 + 8 + 6 + 4 = 43
1397
- const MAX_THEORETICAL_SCORE = 43;
1380
+ // Max theoretical score: 10 + 6 + 6 + 3 + 8 + 6 + 4 + 8 + 5 = 56
1381
+ const MAX_THEORETICAL_SCORE = 56;
1382
+
1383
+ // Task-type detection (used by Signal 9)
1384
+ const TASK_TYPE_PATTERNS = {
1385
+ tdd: /\b(tdd|test.driven|write.*tests?|spec)\b/i,
1386
+ test: /\b(test|testing|coverage|jest|vitest|pytest|spec)\b/i,
1387
+ review: /\b(review|code.review|pr.review|audit|inspect)\b/i,
1388
+ docs: /\b(doc|documentation|readme|changelog|api.doc)\b/i,
1389
+ implementation: /\b(implement|build|create|develop|feat|feature|add)\b/i,
1390
+ fix: /\b(fix|bug|patch|hotfix|repair|resolve)\b/i,
1391
+ refactor: /\b(refactor|cleanup|reorganize|restructure|simplify)\b/i,
1392
+ devops: /\b(ci|cd|deploy|pipeline|docker|k8s|infra|terraform)\b/i,
1393
+ };
1394
+ const detectedTaskTypes = Object.entries(TASK_TYPE_PATTERNS)
1395
+ .filter(([, re]) => re.test(textBlob))
1396
+ .map(([type]) => type);
1398
1397
 
1399
1398
  const candidates = [];
1400
1399
  for (const entry of profiles) {
@@ -1406,74 +1405,120 @@ export function matchAgentProfiles(rootDir, criteria = {}, opts = {}) {
1406
1405
 
1407
1406
  let score = 0;
1408
1407
  const reasons = [];
1408
+ const breakdown = {};
1409
1409
 
1410
1410
  // ── Signal 1: titlePattern regex match → +10 (precompiled) ──
1411
1411
  const patterns = toStringArray(profile.titlePatterns);
1412
+ let titlePatternScore = 0;
1412
1413
  for (const pattern of patterns) {
1413
1414
  const re = getCompiledRegex(pattern);
1414
1415
  if (re && re.test(textBlob)) {
1415
- score += 10;
1416
+ titlePatternScore = 10;
1416
1417
  reasons.push(`pattern:${pattern}`);
1417
1418
  break;
1418
1419
  }
1419
1420
  }
1421
+ score += titlePatternScore;
1422
+ breakdown.titlePattern = titlePatternScore;
1420
1423
 
1421
1424
  // ── Signal 2: conventional-commit scope match → +6 ──
1422
1425
  const scopes = toStringArray(profile.scopes).map((s) => s.toLowerCase());
1426
+ let scopeScore = 0;
1423
1427
  if (taskScope && scopes.includes(taskScope)) {
1424
- score += 6;
1428
+ scopeScore = 6;
1425
1429
  reasons.push(`scope:${taskScope}`);
1426
1430
  }
1431
+ score += scopeScore;
1432
+ breakdown.scope = scopeScore;
1427
1433
 
1428
1434
  // ── Signal 3: tag overlap → up to +6 ──
1429
1435
  const profileTags = uniqueStrings([...(entry.tags || []), ...toStringArray(profile.tags)]).map((v) => v.toLowerCase());
1430
1436
  const tagHits = criteriaTags.filter((tag) => profileTags.includes(tag));
1437
+ let tagScore = 0;
1431
1438
  if (tagHits.length > 0) {
1432
- const tagScore = Math.min(6, tagHits.length * 2);
1433
- score += tagScore;
1439
+ tagScore = Math.min(6, tagHits.length * 2);
1434
1440
  reasons.push(`tags:${tagHits.slice(0, 4).join(",")}`);
1435
1441
  }
1442
+ score += tagScore;
1443
+ breakdown.tags = tagScore;
1436
1444
 
1437
1445
  // ── Signal 4: voice-type hint → +3 ──
1446
+ let voiceScore = 0;
1438
1447
  if (profileType === "voice") {
1439
1448
  const voiceHint = /\bvoice\b|\bcall\b|\brealtime\b/.test(textBlobLower);
1440
1449
  if (voiceHint) {
1441
- score += 3;
1450
+ voiceScore = 3;
1442
1451
  reasons.push("voice-hint");
1443
1452
  }
1444
1453
  }
1454
+ score += voiceScore;
1455
+ breakdown.voice = voiceScore;
1445
1456
 
1446
1457
  // ── Signal 5: changed-file path match → up to +8 ──
1447
1458
  const scopeHitsFromPaths = scopes.filter((scope) =>
1448
1459
  changedHints.includes(scope) || changedFiles.some((f) => String(f).toLowerCase().includes(`/${scope}/`) || String(f).toLowerCase().includes(`\\${scope}\\`)),
1449
1460
  );
1461
+ let pathScore = 0;
1450
1462
  if (scopeHitsFromPaths.length > 0) {
1451
- const fileScore = Math.min(8, scopeHitsFromPaths.length * 2);
1452
- score += fileScore;
1463
+ pathScore = Math.min(8, scopeHitsFromPaths.length * 2);
1453
1464
  reasons.push(`paths:${scopeHitsFromPaths.slice(0, 4).join(",")}`);
1454
1465
  }
1466
+ score += pathScore;
1467
+ breakdown.paths = pathScore;
1455
1468
 
1456
1469
  // ── Signal 6: repo-context domain match → up to +6 ──
1470
+ let domainScore = 0;
1457
1471
  if (contextDomains.size > 0) {
1458
1472
  const profileAllTags = new Set([...profileTags, ...scopes]);
1459
1473
  const domainHits = [...contextDomains].filter((d) => profileAllTags.has(d));
1460
1474
  if (domainHits.length > 0) {
1461
- const domainScore = Math.min(6, domainHits.length * 2);
1462
- score += domainScore;
1475
+ domainScore = Math.min(6, domainHits.length * 2);
1463
1476
  reasons.push(`repo-ctx:${domainHits.slice(0, 3).join(",")}`);
1464
1477
  }
1465
1478
  }
1479
+ score += domainScore;
1480
+ breakdown.repoCtx = domainScore;
1466
1481
 
1467
1482
  // ── Signal 7: file-type domain match → up to +4 ──
1483
+ let fileTypeScore = 0;
1468
1484
  if (fileSignals && fileSignals.fileDomains.length > 0) {
1469
1485
  const profileAllTags = new Set([...profileTags, ...scopes]);
1470
1486
  const fileHits = fileSignals.fileDomains.filter((d) => profileAllTags.has(d));
1471
1487
  if (fileHits.length > 0) {
1472
- const fileTypeScore = Math.min(4, fileHits.length);
1473
- score += fileTypeScore;
1488
+ fileTypeScore = Math.min(4, fileHits.length);
1474
1489
  reasons.push(`file-type:${fileHits.slice(0, 3).join(",")}`);
1475
1490
  }
1476
1491
  }
1492
+ score += fileTypeScore;
1493
+ breakdown.fileType = fileTypeScore;
1494
+
1495
+ // ── Signal 8: description keyword match → up to +8 ──
1496
+ const profileDesc = String(profile.description || "").toLowerCase();
1497
+ let descScore = 0;
1498
+ if (profileDesc.length > 10) {
1499
+ const descTokens = keywordTokens(profileDesc, { minLength: 4 });
1500
+ const titleTokens = keywordTokens(textBlob, { minLength: 4 });
1501
+ const descHits = descTokens.filter((t) => titleTokens.includes(t));
1502
+ if (descHits.length > 0) {
1503
+ descScore = Math.min(8, descHits.length * 2);
1504
+ reasons.push(`desc:${descHits.slice(0, 4).join(",")}`);
1505
+ }
1506
+ }
1507
+ score += descScore;
1508
+ breakdown.descMatch = descScore;
1509
+
1510
+ // ── Signal 9: task-type hint → +5 ──
1511
+ let taskTypeScore = 0;
1512
+ if (detectedTaskTypes.length > 0) {
1513
+ const profileAllTags = new Set([...profileTags, ...scopes]);
1514
+ const taskTypeHits = detectedTaskTypes.filter((t) => profileAllTags.has(t));
1515
+ if (taskTypeHits.length > 0) {
1516
+ taskTypeScore = 5;
1517
+ reasons.push(`task-type:${taskTypeHits.join(",")}`);
1518
+ }
1519
+ }
1520
+ score += taskTypeScore;
1521
+ breakdown.taskType = taskTypeScore;
1477
1522
 
1478
1523
  if (score <= 0) continue;
1479
1524
 
@@ -1484,6 +1529,7 @@ export function matchAgentProfiles(rootDir, criteria = {}, opts = {}) {
1484
1529
  score,
1485
1530
  confidence,
1486
1531
  reasons,
1532
+ breakdown,
1487
1533
  matchedScope: taskScope,
1488
1534
  });
1489
1535
  }
@@ -1519,6 +1565,7 @@ export function matchAgentProfiles(rootDir, criteria = {}, opts = {}) {
1519
1565
  description,
1520
1566
  requestedAgentType,
1521
1567
  taskScope,
1568
+ detectedTaskTypes,
1522
1569
  changedFilesCount: changedFiles.length,
1523
1570
  repoContext: repoCtx || null,
1524
1571
  fileSignals: fileSignals || null,
@@ -1538,581 +1585,13 @@ export function matchAgentProfile(rootDir, taskTitle) {
1538
1585
  return result.best || null;
1539
1586
  }
1540
1587
 
1541
- const TRUSTED_GITHUB_OWNERS = new Set(["microsoft", "github", "azure", "desktop", "canonical", "mastra-ai"]);
1542
-
1543
- export const WELL_KNOWN_AGENT_SOURCES = Object.freeze([
1544
- // ── Microsoft — Official ──────────────────────────────────────────────────
1545
- {
1546
- id: "microsoft-skills",
1547
- name: "Microsoft Skills",
1548
- repoUrl: "https://github.com/microsoft/skills.git",
1549
- defaultBranch: "main",
1550
- description: "Microsoft-maintained backend, frontend, planner, infrastructure, and scaffolder agents with hundreds of Azure SDK skills.",
1551
- owner: "microsoft",
1552
- trustTier: "official",
1553
- importCoverage: "high",
1554
- estimatedPlugins: 180,
1555
- focuses: ["backend", "frontend", "planner", "infra", "scaffolding", "azure"],
1556
- },
1557
- {
1558
- id: "microsoft-hve-core",
1559
- name: "Microsoft HVE Core",
1560
- repoUrl: "https://github.com/microsoft/hve-core.git",
1561
- defaultBranch: "main",
1562
- description: "Core HVE agent library with domain and plugin agent templates and experimental skills.",
1563
- owner: "microsoft",
1564
- trustTier: "official",
1565
- importCoverage: "high",
1566
- estimatedPlugins: 60,
1567
- focuses: ["core", "plugins", "platform"],
1568
- },
1569
- {
1570
- id: "microsoft-vscode",
1571
- name: "Microsoft VS Code",
1572
- repoUrl: "https://github.com/microsoft/vscode.git",
1573
- defaultBranch: "main",
1574
- description: "VS Code editor skills for hygiene, testing, and extension development workflows.",
1575
- owner: "microsoft",
1576
- trustTier: "official",
1577
- importCoverage: "medium",
1578
- estimatedPlugins: 15,
1579
- focuses: ["vscode", "editor", "extensions", "testing"],
1580
- },
1581
- {
1582
- id: "microsoft-powertoys",
1583
- name: "Microsoft PowerToys",
1584
- repoUrl: "https://github.com/microsoft/PowerToys.git",
1585
- defaultBranch: "main",
1586
- description: "PowerToys development skills for Windows utility and plugin engineering.",
1587
- owner: "microsoft",
1588
- trustTier: "official",
1589
- importCoverage: "medium",
1590
- estimatedPlugins: 10,
1591
- focuses: ["windows", "utilities", "c-sharp", "plugins"],
1592
- },
1593
- {
1594
- id: "microsoft-typespec",
1595
- name: "Microsoft TypeSpec",
1596
- repoUrl: "https://github.com/microsoft/typespec.git",
1597
- defaultBranch: "main",
1598
- description: "TypeSpec API definition language skills for code generation and API design workflows.",
1599
- owner: "microsoft",
1600
- trustTier: "official",
1601
- importCoverage: "medium",
1602
- estimatedPlugins: 20,
1603
- focuses: ["api", "code-generation", "typescript", "openapi"],
1604
- },
1605
- {
1606
- id: "microsoft-copilot-for-azure",
1607
- name: "GitHub Copilot for Azure",
1608
- repoUrl: "https://github.com/microsoft/GitHub-Copilot-for-Azure.git",
1609
- defaultBranch: "main",
1610
- description: "Azure-focused Copilot skills for cloud infrastructure, deployment, and resource management.",
1611
- owner: "microsoft",
1612
- trustTier: "official",
1613
- importCoverage: "high",
1614
- estimatedPlugins: 45,
1615
- focuses: ["azure", "cloud", "infrastructure", "deployment"],
1616
- },
1617
- {
1618
- id: "microsoft-vscode-python-environments",
1619
- name: "Microsoft VS Code Python Environments",
1620
- repoUrl: "https://github.com/microsoft/vscode-python-environments.git",
1621
- defaultBranch: "main",
1622
- description: "Maintainer, reviewer, and documentation agents for a production VS Code extension.",
1623
- owner: "microsoft",
1624
- trustTier: "official",
1625
- importCoverage: "medium",
1626
- estimatedPlugins: 8,
1627
- focuses: ["vscode", "python", "extension", "maintainer"],
1628
- },
1629
- {
1630
- id: "microsoft-vscode-docs",
1631
- name: "Microsoft VS Code Documentation",
1632
- repoUrl: "https://github.com/microsoft/vscode-docs.git",
1633
- defaultBranch: "main",
1634
- description: "Skills for VS Code documentation authoring, editing, and review workflows.",
1635
- owner: "microsoft",
1636
- trustTier: "official",
1637
- importCoverage: "medium",
1638
- estimatedPlugins: 12,
1639
- focuses: ["documentation", "vscode", "markdown", "authoring"],
1640
- },
1641
- {
1642
- id: "microsoft-windowsappsdk",
1643
- name: "Microsoft Windows App SDK",
1644
- repoUrl: "https://github.com/microsoft/WindowsAppSDK.git",
1645
- defaultBranch: "main",
1646
- description: "Windows App SDK skills for WinUI and Windows platform development.",
1647
- owner: "microsoft",
1648
- trustTier: "official",
1649
- importCoverage: "medium",
1650
- estimatedPlugins: 8,
1651
- focuses: ["windows", "winui", "sdk", "desktop"],
1652
- },
1653
- {
1654
- id: "microsoft-vscode-java-pack",
1655
- name: "Microsoft VS Code Java Pack",
1656
- repoUrl: "https://github.com/microsoft/vscode-java-pack.git",
1657
- defaultBranch: "main",
1658
- description: "Java development skills for VS Code including debugging, testing, and project management.",
1659
- owner: "microsoft",
1660
- trustTier: "official",
1661
- importCoverage: "medium",
1662
- estimatedPlugins: 10,
1663
- focuses: ["java", "vscode", "debugging", "testing"],
1664
- },
1665
- {
1666
- id: "microsoft-duroxide",
1667
- name: "Microsoft Duroxide",
1668
- repoUrl: "https://github.com/microsoft/duroxide.git",
1669
- defaultBranch: "main",
1670
- description: "Durable Functions in Rust — skills for building resilient serverless workflows.",
1671
- owner: "microsoft",
1672
- trustTier: "official",
1673
- importCoverage: "medium",
1674
- estimatedPlugins: 6,
1675
- focuses: ["rust", "serverless", "durable-functions", "workflows"],
1676
- },
1677
- {
1678
- id: "microsoft-ebpf-for-windows",
1679
- name: "Microsoft eBPF for Windows",
1680
- repoUrl: "https://github.com/microsoft/ebpf-for-windows.git",
1681
- defaultBranch: "main",
1682
- description: "eBPF development skills for Windows kernel and networking instrumentation.",
1683
- owner: "microsoft",
1684
- trustTier: "official",
1685
- importCoverage: "medium",
1686
- estimatedPlugins: 6,
1687
- focuses: ["ebpf", "windows", "kernel", "networking"],
1688
- },
1689
- // ── GitHub — Official ─────────────────────────────────────────────────────
1690
- {
1691
- id: "github-copilot-sdk",
1692
- name: "GitHub Copilot SDK",
1693
- repoUrl: "https://github.com/github/copilot-sdk.git",
1694
- defaultBranch: "main",
1695
- description: "Official GitHub workflow-authoring and docs-maintenance agents for Copilot SDK projects.",
1696
- owner: "github",
1697
- trustTier: "official",
1698
- importCoverage: "medium",
1699
- estimatedPlugins: 10,
1700
- focuses: ["copilot", "workflow", "docs"],
1701
- },
1702
- {
1703
- id: "github-desktop",
1704
- name: "GitHub Desktop",
1705
- repoUrl: "https://github.com/desktop/desktop.git",
1706
- defaultBranch: "development",
1707
- description: "GitHub Desktop app agent profiles for Electron, TypeScript, and Git workflow development.",
1708
- owner: "desktop",
1709
- trustTier: "official",
1710
- importCoverage: "medium",
1711
- estimatedPlugins: 10,
1712
- focuses: ["electron", "typescript", "git", "desktop"],
1713
- },
1714
- // ── Azure — Official ──────────────────────────────────────────────────────
1715
- {
1716
- id: "azure-sdk-for-js",
1717
- name: "Azure SDK for JavaScript",
1718
- repoUrl: "https://github.com/Azure/azure-sdk-for-js.git",
1719
- defaultBranch: "main",
1720
- description: "Azure JavaScript SDK repo with agentic workflow authoring guidance and prompts.",
1721
- owner: "azure",
1722
- trustTier: "official",
1723
- importCoverage: "medium",
1724
- estimatedPlugins: 15,
1725
- focuses: ["azure", "javascript", "sdk", "workflow"],
1726
- },
1727
- // ── Community — Verified ──────────────────────────────────────────────────
1728
- {
1729
- id: "mastra-ai-mastra",
1730
- name: "Mastra AI Framework",
1731
- repoUrl: "https://github.com/mastra-ai/mastra.git",
1732
- defaultBranch: "main",
1733
- description: "AI agent framework with extensive prompt templates for issue tracking, code review, and workflow automation.",
1734
- owner: "mastra-ai",
1735
- trustTier: "community",
1736
- importCoverage: "high",
1737
- estimatedPlugins: 40,
1738
- focuses: ["ai", "agents", "prompts", "automation"],
1739
- },
1740
- {
1741
- id: "z3prover-z3",
1742
- name: "Z3 Theorem Prover",
1743
- repoUrl: "https://github.com/Z3Prover/z3.git",
1744
- defaultBranch: "master",
1745
- description: "Z3 SMT solver agent profiles for formal verification and constraint solving workflows.",
1746
- owner: "Z3Prover",
1747
- trustTier: "community",
1748
- importCoverage: "low",
1749
- estimatedPlugins: 3,
1750
- focuses: ["formal-verification", "smt", "solver", "c++"],
1751
- },
1752
- {
1753
- id: "likec4-likec4",
1754
- name: "LikeC4",
1755
- repoUrl: "https://github.com/likec4/likec4.git",
1756
- defaultBranch: "main",
1757
- description: "Architecture-as-code tool with agents for diagram generation and architecture documentation.",
1758
- owner: "likec4",
1759
- trustTier: "community",
1760
- importCoverage: "medium",
1761
- estimatedPlugins: 12,
1762
- focuses: ["architecture", "diagrams", "documentation", "c4"],
1763
- },
1764
- {
1765
- id: "canonical-copilot-collections",
1766
- name: "Canonical Copilot Collections",
1767
- repoUrl: "https://github.com/canonical/copilot-collections.git",
1768
- defaultBranch: "main",
1769
- description: "Canonical's curated collection of Copilot agent definitions for Ubuntu and open-source development.",
1770
- owner: "canonical",
1771
- trustTier: "community",
1772
- importCoverage: "high",
1773
- estimatedPlugins: 50,
1774
- focuses: ["ubuntu", "linux", "open-source", "devops"],
1775
- },
1776
- {
1777
- id: "playwright-mcp-prompts",
1778
- name: "Playwright MCP Prompts",
1779
- repoUrl: "https://github.com/debs-obrien/playwright-mcp-prompts.git",
1780
- defaultBranch: "main",
1781
- description: "Prompt templates for Playwright end-to-end testing, page objects, and test generation.",
1782
- owner: "debs-obrien",
1783
- trustTier: "community",
1784
- importCoverage: "high",
1785
- estimatedPlugins: 25,
1786
- focuses: ["playwright", "testing", "e2e", "automation"],
1787
- },
1788
- {
1789
- id: "copilot-prompts-collection",
1790
- name: "GitHub Copilot Prompts",
1791
- repoUrl: "https://github.com/raffertyuy/github-copilot-prompts.git",
1792
- defaultBranch: "main",
1793
- description: "Curated collection of GitHub Copilot prompt files for code review, refactoring, and documentation.",
1794
- owner: "raffertyuy",
1795
- trustTier: "community",
1796
- importCoverage: "high",
1797
- estimatedPlugins: 30,
1798
- focuses: ["prompts", "code-review", "refactoring", "docs"],
1799
- },
1800
- {
1801
- id: "copilot-kit",
1802
- name: "Copilot Kit",
1803
- repoUrl: "https://github.com/TheSethRose/Copilot-Kit.git",
1804
- defaultBranch: "main",
1805
- description: "Comprehensive Copilot customization kit with agent profiles, skills, and prompt templates.",
1806
- owner: "TheSethRose",
1807
- trustTier: "community",
1808
- importCoverage: "high",
1809
- estimatedPlugins: 35,
1810
- focuses: ["copilot", "agents", "skills", "prompts"],
1811
- },
1812
- {
1813
- id: "dataplat-dbatools",
1814
- name: "dbatools",
1815
- repoUrl: "https://github.com/dataplat/dbatools.git",
1816
- defaultBranch: "development",
1817
- description: "SQL Server and database administration prompts for DBA workflows and automation.",
1818
- owner: "dataplat",
1819
- trustTier: "community",
1820
- importCoverage: "medium",
1821
- estimatedPlugins: 10,
1822
- focuses: ["sql-server", "database", "powershell", "administration"],
1823
- },
1824
- {
1825
- id: "finops-focus-spec",
1826
- name: "FinOps FOCUS Spec",
1827
- repoUrl: "https://github.com/FinOps-Open-Cost-and-Usage-Spec/FOCUS_Spec.git",
1828
- defaultBranch: "working_draft",
1829
- description: "FinOps specification prompts for cloud cost management and financial operations workflows.",
1830
- owner: "FinOps-Open-Cost-and-Usage-Spec",
1831
- trustTier: "community",
1832
- importCoverage: "medium",
1833
- estimatedPlugins: 8,
1834
- focuses: ["finops", "cloud-costs", "specification", "governance"],
1835
- },
1836
- // ── MCP Tool Repositories ──────────────────────────────────────────────────
1837
- {
1838
- id: "modelcontextprotocol-servers",
1839
- name: "MCP Official Servers",
1840
- repoUrl: "https://github.com/modelcontextprotocol/servers.git",
1841
- defaultBranch: "main",
1842
- description: "Official Model Context Protocol reference servers — filesystem, GitHub, Git, Postgres, Slack, Google Maps, Puppeteer, and more.",
1843
- owner: "modelcontextprotocol",
1844
- trustTier: "official",
1845
- importCoverage: "high",
1846
- estimatedPlugins: 25,
1847
- focuses: ["mcp", "tools", "filesystem", "database", "web", "search"],
1848
- },
1849
- {
1850
- id: "github-mcp-server",
1851
- name: "GitHub MCP Server",
1852
- repoUrl: "https://github.com/github/github-mcp-server.git",
1853
- defaultBranch: "main",
1854
- description: "Official GitHub MCP server for repository management, issues, pull requests, and code search.",
1855
- owner: "github",
1856
- trustTier: "official",
1857
- importCoverage: "medium",
1858
- estimatedPlugins: 3,
1859
- focuses: ["mcp", "github", "issues", "pull-requests", "code-search"],
1860
- },
1861
- {
1862
- id: "punkpeye-awesome-mcp-servers",
1863
- name: "Awesome MCP Servers",
1864
- repoUrl: "https://github.com/punkpeye/awesome-mcp-servers.git",
1865
- defaultBranch: "main",
1866
- description: "Community-curated list of MCP server implementations — databases, dev tools, cloud platforms, AI services, and productivity tools.",
1867
- owner: "punkpeye",
1868
- trustTier: "community",
1869
- importCoverage: "low",
1870
- estimatedPlugins: 5,
1871
- focuses: ["mcp", "tools", "community", "catalog"],
1872
- },
1873
- ]);
1874
-
1875
- function clampNumber(value, min, max) {
1876
- const numeric = Number(value);
1877
- if (!Number.isFinite(numeric)) return min;
1878
- return Math.max(min, Math.min(max, numeric));
1879
- }
1880
-
1881
- function normalizeWellKnownSource(source = {}) {
1882
- const repoUrl = String(source.repoUrl || "").trim();
1883
- const github = repoUrl.match(/^https:\/\/github\.com\/([^/]+)\/([^/.]+)(?:\.git)?$/i);
1884
- const owner = String(source.owner || (github?.[1] || "")).trim();
1885
- const repo = String(source.repo || (github?.[2] || "")).trim();
1886
- return {
1887
- ...source,
1888
- owner: owner || null,
1889
- repo: repo || null,
1890
- provider: source.provider || (github ? "github" : null),
1891
- importCoverage: String(source.importCoverage || "medium"),
1892
- focuses: toStringArray(source.focuses),
1893
- };
1894
- }
1895
-
1896
- function compareWellKnownSources(a, b) {
1897
- // Sort by estimated plugin count (most first), then trust score, then name
1898
- const pluginDelta = Number(b?.estimatedPlugins || 0) - Number(a?.estimatedPlugins || 0);
1899
- if (pluginDelta !== 0) return pluginDelta;
1900
- const delta = Number(b?.trust?.score || 0) - Number(a?.trust?.score || 0);
1901
- if (delta !== 0) return delta;
1902
- return String(a?.name || "").localeCompare(String(b?.name || ""));
1903
- }
1904
-
1905
- export function computeWellKnownSourceTrust(source, probe = {}, options = {}) {
1906
- const nowMs = Number(options?.nowMs || Date.now());
1907
- const normalized = normalizeWellKnownSource(source);
1908
- const reasons = [];
1909
- let score = 20;
1910
-
1911
- if (normalized.trustTier === "official") {
1912
- score += 25;
1913
- reasons.push("official-maintainer");
1914
- }
1915
- if (TRUSTED_GITHUB_OWNERS.has(String(normalized.owner || "").toLowerCase())) {
1916
- score += 15;
1917
- reasons.push("trusted-owner");
1918
- }
1919
- if (normalized.importCoverage === "high") {
1920
- score += 12;
1921
- reasons.push("high-import-coverage");
1922
- } else if (normalized.importCoverage === "medium") {
1923
- score += 6;
1924
- reasons.push("import-coverage");
1925
- }
1926
- if (normalized.provider === "github") {
1927
- score += 4;
1928
- reasons.push("github-source");
1929
- }
1930
-
1931
- const stars = Number(probe?.stars || 0);
1932
- if (stars >= 10000) {
1933
- score += 10;
1934
- reasons.push("popular-repo");
1935
- } else if (stars >= 1000) {
1936
- score += 6;
1937
- reasons.push("established-repo");
1938
- } else if (stars >= 100) {
1939
- score += 3;
1940
- }
1941
-
1942
- const daysSincePush = Number.isFinite(probe?.daysSincePush)
1943
- ? Number(probe.daysSincePush)
1944
- : (probe?.pushedAt ? Math.max(0, (nowMs - Date.parse(probe.pushedAt)) / 86400000) : null);
1945
- if (daysSincePush != null) {
1946
- if (daysSincePush <= 45) {
1947
- score += 10;
1948
- reasons.push("recently-updated");
1949
- } else if (daysSincePush <= 180) {
1950
- score += 6;
1951
- reasons.push("active-updates");
1952
- } else if (daysSincePush <= 365) {
1953
- score += 2;
1954
- } else if (daysSincePush > 730) {
1955
- score -= 16;
1956
- reasons.push("stale-upstream");
1957
- }
1958
- }
1959
-
1960
- if (probe?.reachable === true) {
1961
- score += 8;
1962
- reasons.push("remote-reachable");
1963
- } else if (probe?.reachable === false) {
1964
- score -= 28;
1965
- reasons.push("remote-unreachable");
1966
- }
1967
-
1968
- if (probe?.branchExists === true) {
1969
- score += 6;
1970
- reasons.push("branch-ok");
1971
- } else if (probe?.branchExists === false) {
1972
- score -= 22;
1973
- reasons.push("branch-missing");
1974
- }
1975
-
1976
- if (probe?.archived === true) {
1977
- score -= 45;
1978
- reasons.push("archived");
1979
- }
1980
- if (probe?.disabled === true) {
1981
- score -= 45;
1982
- reasons.push("disabled");
1983
- }
1984
-
1985
- score = Math.round(clampNumber(score, 0, 100));
1986
- // Hard-disable only for unreachable/archived/disabled repos — low score gets a warning but remains importable
1987
- const hardBlocked = probe?.archived === true || probe?.disabled === true || probe?.reachable === false || probe?.branchExists === false;
1988
- const enabled = !hardBlocked;
1989
- const lowTrust = score < 55;
1990
- const status = hardBlocked ? "disabled" : score >= 85 ? "healthy" : score >= 65 ? "warning" : score >= 55 ? "degraded" : "low-trust";
1991
-
1992
- return {
1993
- score,
1994
- status,
1995
- enabled,
1996
- lowTrust,
1997
- reasons: uniqueStrings(reasons),
1998
- };
1999
- }
2000
-
2001
- function buildWellKnownSourceResult(source, probe = null, options = {}) {
2002
- const normalized = normalizeWellKnownSource(source);
2003
- const trust = computeWellKnownSourceTrust(normalized, probe || {}, options);
2004
- return {
2005
- ...normalized,
2006
- trust,
2007
- probe: probe ? { ...probe } : null,
2008
- enabled: trust.enabled,
2009
- status: trust.status,
2010
- };
2011
- }
2012
-
2013
- export function listWellKnownAgentSources() {
2014
- return WELL_KNOWN_AGENT_SOURCES
2015
- .map((source) => buildWellKnownSourceResult(source))
2016
- .sort(compareWellKnownSources);
2017
- }
2018
-
2019
- export function clearWellKnownAgentSourceProbeCache() {
2020
- wellKnownSourceProbeCache.clear();
2021
- }
2022
-
2023
- async function fetchGithubRepoProbe(source, options = {}) {
2024
- const normalized = normalizeWellKnownSource(source);
2025
- if (normalized.provider !== "github" || !normalized.owner || !normalized.repo) {
2026
- return { checkedAt: nowISO(), reachable: false, branchExists: false, error: "Unsupported repository provider" };
2027
- }
2028
-
2029
- const fetchImpl = options.fetchImpl || globalThis.fetch;
2030
- const spawnImpl = options.spawnImpl || spawnSync;
2031
- const branch = String(normalized.defaultBranch || "main").trim() || "main";
2032
- const headers = {
2033
- "Accept": "application/vnd.github+json",
2034
- "User-Agent": "bosun-library-manager",
2035
- };
2036
- if (process.env.GITHUB_TOKEN) headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
2037
-
2038
- let repoMeta = null;
2039
- let repoError = null;
2040
- if (typeof fetchImpl === "function") {
2041
- try {
2042
- const response = await fetchImpl(`https://api.github.com/repos/${normalized.owner}/${normalized.repo}`, { headers });
2043
- if (response?.ok) {
2044
- repoMeta = await response.json();
2045
- } else {
2046
- repoError = `GitHub API returned ${Number(response?.status || 0) || "error"}`;
2047
- }
2048
- } catch (err) {
2049
- repoError = err?.message || String(err);
2050
- }
2051
- } else {
2052
- repoError = "fetch unavailable";
2053
- }
2054
-
2055
- let reachable = false;
2056
- let branchExists = false;
2057
- let gitError = null;
2058
- try {
2059
- const remote = spawnImpl("git", ["ls-remote", "--exit-code", "--heads", normalized.repoUrl, branch], {
2060
- encoding: "utf8",
2061
- stdio: ["ignore", "pipe", "pipe"],
2062
- timeout: Number(options.timeoutMs || 15000),
2063
- });
2064
- const stdout = String(remote?.stdout || "").trim();
2065
- reachable = Number(remote?.status) === 0 || stdout.length > 0;
2066
- branchExists = reachable && stdout.length > 0;
2067
- if (!reachable || !branchExists) {
2068
- gitError = String(remote?.stderr || remote?.stdout || "git ls-remote failed").trim() || null;
2069
- }
2070
- } catch (err) {
2071
- gitError = err?.message || String(err);
2072
- }
2073
-
2074
- return {
2075
- checkedAt: nowISO(),
2076
- reachable,
2077
- branchExists,
2078
- defaultBranch: String(repoMeta?.default_branch || branch || "main"),
2079
- archived: repoMeta?.archived === true,
2080
- disabled: repoMeta?.disabled === true,
2081
- stars: Number(repoMeta?.stargazers_count || 0),
2082
- forks: Number(repoMeta?.forks_count || 0),
2083
- openIssues: Number(repoMeta?.open_issues_count || 0),
2084
- pushedAt: repoMeta?.pushed_at || null,
2085
- daysSincePush: repoMeta?.pushed_at ? Math.max(0, Math.round((Date.now() - Date.parse(repoMeta.pushed_at)) / 86400000)) : null,
2086
- apiReachable: Boolean(repoMeta),
2087
- importReady: reachable && branchExists && repoMeta?.archived !== true && repoMeta?.disabled !== true,
2088
- error: gitError || repoError || null,
2089
- };
2090
- }
2091
-
2092
- export async function probeWellKnownAgentSources(options = {}) {
2093
- const nowMs = Number(options?.nowMs || Date.now());
2094
- const ttlMs = Math.max(1000, Number(options?.ttlMs || 30 * 60 * 1000));
2095
- const sourceId = String(options?.sourceId || "").trim().toLowerCase();
2096
- const refresh = options?.refresh === true;
2097
- const sources = WELL_KNOWN_AGENT_SOURCES.filter((source) => !sourceId || source.id === sourceId);
2098
- const results = [];
2099
-
2100
- for (const source of sources) {
2101
- const cacheKey = source.id;
2102
- const cached = wellKnownSourceProbeCache.get(cacheKey) || null;
2103
- if (!refresh && cached && (nowMs - Number(cached.cachedAt || 0)) < ttlMs) {
2104
- results.push(buildWellKnownSourceResult(source, cached.probe, { nowMs }));
2105
- continue;
2106
- }
2107
-
2108
- const probe = await fetchGithubRepoProbe(source, options);
2109
- wellKnownSourceProbeCache.set(cacheKey, { cachedAt: nowMs, probe });
2110
- results.push(buildWellKnownSourceResult(source, probe, { nowMs }));
2111
- }
2112
-
2113
- return results.sort(compareWellKnownSources);
2114
- }
2115
-
1588
+ export {
1589
+ WELL_KNOWN_AGENT_SOURCES,
1590
+ computeWellKnownSourceTrust,
1591
+ listWellKnownAgentSources,
1592
+ clearWellKnownAgentSourceProbeCache,
1593
+ probeWellKnownAgentSources,
1594
+ };
2116
1595
  function parseSimpleFrontmatter(markdown = "") {
2117
1596
  const text = String(markdown || "");
2118
1597
  if (!text.startsWith("---\n")) return { attrs: {}, body: text };
@@ -2586,7 +2065,7 @@ export function syncAutoDiscoveredLibraryEntries(rootDir) {
2586
2065
  };
2587
2066
  }
2588
2067
 
2589
- export function scanRepositoryForImport(options = {}) {
2068
+ function resolveRepositoryImportSource(options = {}) {
2590
2069
  const sourceId = String(options?.sourceId || "").trim().toLowerCase();
2591
2070
  const known = WELL_KNOWN_AGENT_SOURCES.find((source) => source.id === sourceId) || null;
2592
2071
  const repoUrl = String(options?.repoUrl || known?.repoUrl || "").trim();
@@ -2599,16 +2078,13 @@ export function scanRepositoryForImport(options = {}) {
2599
2078
  if (!isSafeGitRefName(branch)) {
2600
2079
  throw new Error("Branch name contains invalid characters");
2601
2080
  }
2602
- const maxEntries = Math.max(
2603
- 1,
2604
- Math.min(
2605
- 2000,
2606
- Number.parseInt(String(options?.maxEntries ?? "500"), 10) || 500,
2607
- ),
2608
- );
2609
2081
 
2082
+ return { sourceId, known, repoUrl, branch };
2083
+ }
2084
+
2085
+ function createRepositoryImportCheckoutDir(prefix, repoUrl, branch) {
2610
2086
  const cacheRoot = ensureDir(resolve(getBosunHomeDir(), ".cache", "imports"));
2611
- const checkoutDir = resolve(cacheRoot, `scan-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`);
2087
+ const checkoutDir = resolve(cacheRoot, `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`);
2612
2088
  ensureDir(checkoutDir);
2613
2089
 
2614
2090
  const clone = spawnSync("git", ["clone", "--depth", "1", "--branch", branch, "--", repoUrl, checkoutDir], {
@@ -2617,167 +2093,231 @@ export function scanRepositoryForImport(options = {}) {
2617
2093
  timeout: 120_000,
2618
2094
  });
2619
2095
  const cloneStderr = String(clone.stderr || "").trim();
2620
- // Treat "clone succeeded, but checkout failed" (long paths on Windows) as OK
2621
2096
  const checkoutWarning = /clone succeeded.*checkout failed/i.test(cloneStderr);
2622
- if (clone.status !== 0 && !checkoutWarning) {
2623
- rmSync(checkoutDir, { recursive: true, force: true });
2624
- if (/repository not found/i.test(cloneStderr)) {
2625
- throw new Error(`Repository not found: ${repoUrl}`);
2626
- }
2627
- if (/could not read from remote/i.test(cloneStderr)) {
2628
- throw new Error(`Cannot access repository (may be private or require authentication): ${repoUrl}`);
2629
- }
2630
- if (/not found in upstream/i.test(cloneStderr) || /remote branch.*not found/i.test(cloneStderr)) {
2631
- throw new Error(`Branch "${branch}" not found in ${repoUrl}`);
2632
- }
2633
- if (clone.signal === "SIGTERM") {
2634
- throw new Error(`Clone timed out — repository may be too large: ${repoUrl}`);
2635
- }
2636
- throw new Error(`Failed to clone repository: ${cloneStderr || "unknown error"}`);
2097
+ if (clone.status === 0 || checkoutWarning) return checkoutDir;
2098
+
2099
+ rmSync(checkoutDir, { recursive: true, force: true });
2100
+ if (/repository not found/i.test(cloneStderr)) {
2101
+ throw new Error(`Repository not found: ${repoUrl}`);
2102
+ }
2103
+ if (/could not read from remote/i.test(cloneStderr)) {
2104
+ throw new Error(`Cannot access repository (may be private or require authentication): ${repoUrl}`);
2105
+ }
2106
+ if (/not found in upstream/i.test(cloneStderr) || /remote branch.*not found/i.test(cloneStderr)) {
2107
+ throw new Error(`Branch "${branch}" not found in ${repoUrl}`);
2108
+ }
2109
+ if (clone.signal === "SIGTERM") {
2110
+ throw new Error(`Clone timed out — repository may be too large: ${repoUrl}`);
2637
2111
  }
2112
+ throw new Error(`Failed to clone repository: ${cloneStderr || "unknown error"}`);
2113
+ }
2638
2114
 
2639
- try {
2640
- const files = walkFilesRecursive(checkoutDir);
2641
- const candidates = files
2115
+ function buildImportedMarkdownCandidate(candidate) {
2116
+ const { raw = "", attrs = {}, body = "", relPath = "", fileName = "", kind = null } = candidate || {};
2117
+ if (!kind) return null;
2118
+
2119
+ const fileStem = basename(fileName, ".md");
2120
+ const relSegments = relPath.split(/[\\/]/).filter(Boolean);
2121
+ const parentSegment = relSegments.length > 1 ? relSegments[relSegments.length - 2] : "";
2122
+ const fallbackNameBase = fileStem.toLowerCase() === "skill" && parentSegment ? parentSegment : fileStem;
2123
+ const fallbackName = fallbackNameBase.replace(/\.agent$/i, "").replace(/\.skill$/i, "").replace(/\.prompt$/i, "");
2124
+ const name = String(getFrontmatterValue(attrs, ["name", "title"]) || fallbackName.replace(/[-_.]+/g, " ")).trim();
2125
+ const description = normalizeImportedDescription(getFrontmatterValue(attrs, ["description", "summary"]), body);
2126
+
2127
+ return {
2128
+ ...candidate,
2129
+ fileStem,
2130
+ name,
2131
+ description,
2132
+ };
2133
+ }
2134
+
2135
+ function sortImportedMarkdownCandidates(candidates = []) {
2136
+ const rank = { agent: 0, prompt: 1, skill: 2 };
2137
+ return candidates.sort((a, b) => {
2138
+ const aRank = Number(rank[a.kind] ?? 99);
2139
+ const bRank = Number(rank[b.kind] ?? 99);
2140
+ if (aRank !== bRank) return aRank - bRank;
2141
+ return String(a.relPath || "").localeCompare(String(b.relPath || ""));
2142
+ });
2143
+ }
2144
+
2145
+ function collectRepositoryImportMarkdownCandidates(checkoutDir, options = {}) {
2146
+ const maxEntries = Math.max(
2147
+ 1,
2148
+ Math.min(2000, Number.parseInt(String(options?.maxEntries ?? "500"), 10) || 500),
2149
+ );
2150
+
2151
+ const files = walkFilesRecursive(checkoutDir);
2152
+ const candidates = sortImportedMarkdownCandidates(
2153
+ files
2642
2154
  .filter((fullPath) => /\.md$/i.test(fullPath))
2643
2155
  .map((fullPath) => {
2644
2156
  const relPath = fullPath.slice(checkoutDir.length + 1).replace(/\\/g, "/");
2645
2157
  const fileName = basename(fullPath);
2158
+ let raw = "";
2646
2159
  let parsed = { attrs: {}, body: "" };
2647
2160
  try {
2648
- const raw = readFileSync(fullPath, "utf8");
2161
+ raw = readFileSync(fullPath, "utf8");
2649
2162
  parsed = parseSimpleFrontmatter(raw);
2650
- } catch { /* skip unreadable */ }
2163
+ } catch {
2164
+ return null;
2165
+ }
2651
2166
  const kind = inferImportedEntryKind(relPath, fileName, parsed.attrs);
2652
- if (!kind) return null;
2653
- const fileStem = basename(fileName, ".md");
2654
- const relSegments = relPath.split(/[\\/]/).filter(Boolean);
2655
- const parentSegment = relSegments.length > 1 ? relSegments[relSegments.length - 2] : "";
2656
- const fallbackNameBase = fileStem.toLowerCase() === "skill" && parentSegment ? parentSegment : fileStem;
2657
- const fallbackName = fallbackNameBase.replace(/\.agent$/i, "").replace(/\.skill$/i, "").replace(/\.prompt$/i, "");
2658
- const name = String(getFrontmatterValue(parsed.attrs, ["name", "title"]) || fallbackName.replace(/[-_.]+/g, " ")).trim();
2659
- const description = normalizeImportedDescription(getFrontmatterValue(parsed.attrs, ["description", "summary"]), parsed.body);
2660
- return { relPath, fileName, kind, name, description, selected: true };
2661
- })
2662
- .filter(Boolean)
2663
- .sort((a, b) => {
2664
- const rank = { agent: 0, prompt: 1, skill: 2 };
2665
- const aRank = Number(rank[a.kind] ?? 99);
2666
- const bRank = Number(rank[b.kind] ?? 99);
2667
- if (aRank !== bRank) return aRank - bRank;
2668
- return String(a.relPath || "").localeCompare(String(b.relPath || ""));
2167
+ return buildImportedMarkdownCandidate({
2168
+ fullPath,
2169
+ relPath,
2170
+ fileName,
2171
+ raw,
2172
+ attrs: parsed.attrs,
2173
+ body: parsed.body,
2174
+ kind,
2175
+ selected: true,
2176
+ });
2669
2177
  })
2670
- .slice(0, maxEntries);
2178
+ .filter(Boolean),
2179
+ ).slice(0, maxEntries);
2671
2180
 
2672
- const byType = { agent: 0, prompt: 0, skill: 0, mcp: 0 };
2673
- for (const c of candidates) byType[c.kind] = (byType[c.kind] || 0) + 1;
2674
-
2675
- // Scan for MCP server definitions in known config files
2676
- const mcpConfigPaths = [
2677
- resolve(checkoutDir, ".codex", "config.toml"),
2678
- resolve(checkoutDir, ".mcp.json"),
2679
- resolve(checkoutDir, "mcp.json"),
2680
- resolve(checkoutDir, ".vscode", "mcp.json"),
2681
- resolve(checkoutDir, "claude_desktop_config.json"),
2682
- ];
2683
- for (const configPath of mcpConfigPaths) {
2684
- if (!existsSync(configPath)) continue;
2685
- let raw = "";
2686
- try { raw = readFileSync(configPath, "utf8"); } catch { continue; }
2687
- const relPath = relative(checkoutDir, configPath).replace(/\\/g, "/");
2688
- if (/\.toml$/i.test(configPath)) {
2689
- const discovered = parseMcpServersFromToml(raw, relPath);
2690
- for (const mcp of discovered) {
2691
- candidates.push({
2692
- relPath: `${relPath}#${mcp.id}`,
2693
- fileName: basename(configPath),
2694
- kind: "mcp",
2695
- name: mcp.name || mcp.id,
2696
- description: `${mcp.transport === "stdio" ? "stdio" : "url"} MCP server${mcp.command ? ": " + mcp.command : ""}`,
2697
- selected: true,
2698
- });
2699
- byType.mcp += 1;
2700
- }
2701
- } else {
2702
- const discovered = parseMcpServersFromJson(raw, relPath);
2703
- for (const mcp of discovered) {
2704
- candidates.push({
2705
- relPath: `${relPath}#${mcp.id}`,
2706
- fileName: basename(configPath),
2707
- kind: "mcp",
2708
- name: mcp.name || mcp.id,
2709
- description: `${mcp.transport === "stdio" ? "stdio" : "url"} MCP server${mcp.command ? ": " + mcp.command : ""}`,
2710
- selected: true,
2711
- });
2712
- byType.mcp += 1;
2713
- }
2714
- }
2181
+ return { files, candidates };
2182
+ }
2183
+
2184
+ function listRepositoryMcpConfigFiles(checkoutDir, { includeLegacy = false } = {}) {
2185
+ const configFiles = [
2186
+ { path: resolve(checkoutDir, ".codex", "config.toml"), format: "toml" },
2187
+ { path: resolve(checkoutDir, ".mcp.json"), format: "json" },
2188
+ { path: resolve(checkoutDir, ".vscode", "mcp.json"), format: "json" },
2189
+ ];
2190
+
2191
+ if (includeLegacy) {
2192
+ configFiles.splice(2, 0, { path: resolve(checkoutDir, "mcp.json"), format: "json" });
2193
+ configFiles.push({ path: resolve(checkoutDir, "claude_desktop_config.json"), format: "json" });
2194
+ }
2195
+
2196
+ return configFiles;
2197
+ }
2198
+
2199
+ function discoverRepositoryMcpConfigs(checkoutDir, options = {}) {
2200
+ const discovered = [];
2201
+ for (const { path: configPath, format } of listRepositoryMcpConfigFiles(checkoutDir, options)) {
2202
+ if (!existsSync(configPath)) continue;
2203
+ let raw = "";
2204
+ try {
2205
+ raw = readFileSync(configPath, "utf8");
2206
+ } catch {
2207
+ continue;
2715
2208
  }
2209
+ const relPath = relative(checkoutDir, configPath).replace(/\\/g, "/");
2210
+ const entries = format === "toml"
2211
+ ? parseMcpServersFromToml(raw, relPath)
2212
+ : parseMcpServersFromJson(raw, relPath);
2213
+ discovered.push(...entries.map((entry) => ({ ...entry, relPath, fileName: basename(configPath) })));
2214
+ }
2215
+ return discovered;
2216
+ }
2716
2217
 
2717
- // Discover MCP servers from package.json bin entries and pyproject.toml scripts
2718
- const mcpSeenIds = new Set(candidates.filter((c) => c.kind === "mcp").map((c) => c.name));
2719
- for (const fullPath of files) {
2720
- const fileName = basename(fullPath);
2721
- if (fileName !== "package.json" && fileName !== "pyproject.toml") continue;
2722
- const relPath = relative(checkoutDir, fullPath).replace(/\\/g, "/");
2723
- // Skip root-level package.json (monorepo manifest, not an MCP server)
2724
- if (relPath === "package.json") continue;
2725
- let raw = "";
2726
- try { raw = readFileSync(fullPath, "utf8"); } catch { continue; }
2727
-
2728
- if (fileName === "package.json") {
2729
- let pkg;
2730
- try { pkg = JSON.parse(raw); } catch { continue; }
2731
- if (!pkg || typeof pkg !== "object") continue;
2732
- const bin = pkg.bin;
2733
- if (!bin || typeof bin !== "object") continue;
2734
- const mcpBins = Object.entries(bin).filter(([k]) => /^mcp[-_]?server/i.test(k) || /^mcp-/i.test(k));
2735
- if (mcpBins.length === 0) continue;
2736
- for (const [cmd] of mcpBins) {
2737
- const id = slugifyIdentifier(cmd);
2738
- if (mcpSeenIds.has(cmd) || mcpSeenIds.has(id)) continue;
2739
- mcpSeenIds.add(cmd);
2740
- const name = String(pkg.mcpName || pkg.name || cmd).trim();
2741
- const desc = String(pkg.description || "").trim();
2742
- candidates.push({
2743
- relPath: `${relPath}#${cmd}`,
2744
- fileName,
2745
- kind: "mcp",
2746
- name: name.startsWith("@") ? cmd : name,
2747
- description: desc || `stdio MCP server: ${cmd}`,
2748
- selected: true,
2749
- });
2750
- byType.mcp += 1;
2751
- }
2752
- } else if (fileName === "pyproject.toml") {
2753
- // Look for [project.scripts] entries matching mcp-server-*
2754
- const scriptMatch = raw.match(/\[project\.scripts\]([\s\S]*?)(?:\n\[|\n$)/);
2755
- if (!scriptMatch) continue;
2756
- const scriptBlock = scriptMatch[1];
2757
- const scriptLines = scriptBlock.split(/\r?\n/);
2758
- for (const line of scriptLines) {
2759
- const kv = line.match(/^\s*(mcp[-_]?server[-_]?\S*)\s*=/i);
2760
- if (!kv) continue;
2761
- const cmd = kv[1].trim();
2762
- if (mcpSeenIds.has(cmd)) continue;
2763
- mcpSeenIds.add(cmd);
2764
- // Try to get project name/description from TOML
2765
- const nameMatch = raw.match(/^\s*name\s*=\s*"([^"]+)"/m);
2766
- const descMatch = raw.match(/^\s*description\s*=\s*"([^"]+)"/m);
2767
- candidates.push({
2768
- relPath: `${relPath}#${cmd}`,
2769
- fileName,
2770
- kind: "mcp",
2771
- name: nameMatch ? nameMatch[1] : cmd,
2772
- description: descMatch ? descMatch[1] : `stdio MCP server: ${cmd}`,
2773
- selected: true,
2774
- });
2775
- byType.mcp += 1;
2776
- }
2218
+ function appendRepositoryMcpImportCandidates(candidates, files, checkoutDir, byType) {
2219
+ for (const mcp of discoverRepositoryMcpConfigs(checkoutDir, { includeLegacy: true })) {
2220
+ candidates.push({
2221
+ relPath: `${mcp.relPath}#${mcp.id}`,
2222
+ fileName: mcp.fileName,
2223
+ kind: "mcp",
2224
+ name: mcp.name || mcp.id,
2225
+ description: `${mcp.transport === "stdio" ? "stdio" : "url"} MCP server${mcp.command ? ": " + mcp.command : ""}`,
2226
+ selected: true,
2227
+ });
2228
+ byType.mcp += 1;
2229
+ }
2230
+
2231
+ const mcpSeenIds = new Set(candidates.filter((candidate) => candidate.kind === "mcp").map((candidate) => candidate.name));
2232
+ for (const fullPath of files) {
2233
+ const fileName = basename(fullPath);
2234
+ if (fileName !== "package.json" && fileName !== "pyproject.toml") continue;
2235
+ const relPath = relative(checkoutDir, fullPath).replace(/\\/g, "/");
2236
+ if (relPath === "package.json") continue;
2237
+ let raw = "";
2238
+ try {
2239
+ raw = readFileSync(fullPath, "utf8");
2240
+ } catch {
2241
+ continue;
2242
+ }
2243
+
2244
+ if (fileName === "package.json") {
2245
+ let pkg;
2246
+ try {
2247
+ pkg = JSON.parse(raw);
2248
+ } catch {
2249
+ continue;
2250
+ }
2251
+ if (!pkg || typeof pkg !== "object") continue;
2252
+ const bin = pkg.bin;
2253
+ if (!bin || typeof bin !== "object") continue;
2254
+ const mcpBins = Object.entries(bin).filter(([key]) => /^mcp[-_]?server/i.test(key) || /^mcp-/i.test(key));
2255
+ if (mcpBins.length === 0) continue;
2256
+ for (const [cmd] of mcpBins) {
2257
+ const id = slugifyIdentifier(cmd);
2258
+ if (mcpSeenIds.has(cmd) || mcpSeenIds.has(id)) continue;
2259
+ mcpSeenIds.add(cmd);
2260
+ const name = String(pkg.mcpName || pkg.name || cmd).trim();
2261
+ const description = String(pkg.description || "").trim();
2262
+ candidates.push({
2263
+ relPath: `${relPath}#${cmd}`,
2264
+ fileName,
2265
+ kind: "mcp",
2266
+ name: name.startsWith("@") ? cmd : name,
2267
+ description: description || `stdio MCP server: ${cmd}`,
2268
+ selected: true,
2269
+ });
2270
+ byType.mcp += 1;
2777
2271
  }
2272
+ continue;
2778
2273
  }
2779
2274
 
2780
- // Duplicate detection against existing library entries
2275
+ const scriptMatch = raw.match(/\[project\.scripts\]([\s\S]*?)(?:\n\[|\n$)/);
2276
+ if (!scriptMatch) continue;
2277
+ const scriptLines = scriptMatch[1].split(/\r?\n/);
2278
+ for (const line of scriptLines) {
2279
+ const kv = line.match(/^\s*(mcp[-_]?server[-_]?\S*)\s*=/i);
2280
+ if (!kv) continue;
2281
+ const cmd = kv[1].trim();
2282
+ if (mcpSeenIds.has(cmd)) continue;
2283
+ mcpSeenIds.add(cmd);
2284
+ const nameMatch = raw.match(/^\s*name\s*=\s*"([^"]+)"/m);
2285
+ const descMatch = raw.match(/^\s*description\s*=\s*"([^"]+)"/m);
2286
+ candidates.push({
2287
+ relPath: `${relPath}#${cmd}`,
2288
+ fileName,
2289
+ kind: "mcp",
2290
+ name: nameMatch ? nameMatch[1] : cmd,
2291
+ description: descMatch ? descMatch[1] : `stdio MCP server: ${cmd}`,
2292
+ selected: true,
2293
+ });
2294
+ byType.mcp += 1;
2295
+ }
2296
+ }
2297
+ }
2298
+
2299
+ export function scanRepositoryForImport(options = {}) {
2300
+ const { sourceId, known, repoUrl, branch } = resolveRepositoryImportSource(options);
2301
+ const maxEntries = Math.max(
2302
+ 1,
2303
+ Math.min(
2304
+ 2000,
2305
+ Number.parseInt(String(options?.maxEntries ?? "500"), 10) || 500,
2306
+ ),
2307
+ );
2308
+
2309
+ const checkoutDir = createRepositoryImportCheckoutDir("scan", repoUrl, branch);
2310
+
2311
+ try {
2312
+ const { files, candidates } = collectRepositoryImportMarkdownCandidates(checkoutDir, { maxEntries });
2313
+
2314
+ const byType = { agent: 0, prompt: 0, skill: 0, mcp: 0 };
2315
+ for (const candidate of candidates) {
2316
+ byType[candidate.kind] = (byType[candidate.kind] || 0) + 1;
2317
+ }
2318
+
2319
+ appendRepositoryMcpImportCandidates(candidates, files, checkoutDir, byType);
2320
+
2781
2321
  const rootDir = options?.rootDir || null;
2782
2322
  let duplicateMap = {};
2783
2323
  let intraDuplicateMap = {};
@@ -2787,13 +2327,12 @@ export function scanRepositoryForImport(options = {}) {
2787
2327
  const extDups = detectImportDuplicates(candidates, existingEntries);
2788
2328
  for (const [relPath, info] of extDups) {
2789
2329
  duplicateMap[relPath] = info;
2790
- // Auto-deselect exact duplicates
2791
2330
  const cand = candidates.find((c) => c.relPath === relPath);
2792
2331
  if (cand && info.similarity >= 0.95) cand.selected = false;
2793
2332
  }
2794
- } catch { /* skip if library not accessible */ }
2333
+ } catch {}
2795
2334
  }
2796
- // Intra-import duplicate detection
2335
+
2797
2336
  const intraDups = detectIntraDuplicates(candidates);
2798
2337
  for (const [relPath, peers] of intraDups) {
2799
2338
  intraDuplicateMap[relPath] = peers;
@@ -2815,19 +2354,189 @@ export function scanRepositoryForImport(options = {}) {
2815
2354
  }
2816
2355
  }
2817
2356
 
2818
- export function importAgentProfilesFromRepository(rootDir, options = {}) {
2819
- const sourceId = String(options?.sourceId || "").trim().toLowerCase();
2820
- const known = WELL_KNOWN_AGENT_SOURCES.find((source) => source.id === sourceId) || null;
2821
- const repoUrl = String(options?.repoUrl || known?.repoUrl || "").trim();
2822
- if (!repoUrl) throw new Error("Repository URL or source is required");
2357
+ function importRepositoryMarkdownCandidate(rootDir, candidate, context) {
2358
+ const { known, repoUrl, branch, sourceId, importAgents, importSkills, importPrompts, takenIds, imported, importedByType } = context;
2359
+ const { attrs, body, relPath, fileStem, kind, name, description, raw } = candidate;
2360
+ const keywords = keywordTokens(`${name} ${description} ${relPath}`, { minLength: 4 }).slice(0, 10);
2823
2361
 
2824
- const branch = String(options?.branch || known?.defaultBranch || "main").trim() || "main";
2825
- if (!isSafeGitRepositorySource(repoUrl)) {
2826
- throw new Error("URL must be a valid http(s), ssh, or git repository address");
2362
+ if (kind === "prompt") {
2363
+ if (!importPrompts) return { needsAgentIndexRefresh: false, needsSkillIndexRefresh: false };
2364
+ const baseId = slugify(`${sourceId || "imported"}-${name}`) || slugify(fileStem) || `imported-prompt-${imported.length + 1}`;
2365
+ const id = ensureUniqueId(baseId, takenIds);
2366
+ const promptContent = String(body || raw || "").trim();
2367
+ if (!promptContent) return { needsAgentIndexRefresh: false, needsSkillIndexRefresh: false };
2368
+ upsertEntry(rootDir, {
2369
+ id,
2370
+ type: "prompt",
2371
+ name,
2372
+ description: description || `Imported prompt from ${known?.name || repoUrl}`,
2373
+ tags: uniqueStrings(["imported", "prompt", sourceId || "external", ...parseJsonishArray(getFrontmatterValue(attrs, ["tags"]))]),
2374
+ meta: {
2375
+ sourceId: sourceId || null,
2376
+ repoUrl,
2377
+ branch,
2378
+ relPath,
2379
+ },
2380
+ }, promptContent);
2381
+ imported.push({ id, name, relPath, type: "prompt", promptId: null });
2382
+ importedByType.prompt += 1;
2383
+ return { needsAgentIndexRefresh: false, needsSkillIndexRefresh: false };
2384
+ }
2385
+
2386
+ if (kind === "skill") {
2387
+ if (!importSkills) return { needsAgentIndexRefresh: false, needsSkillIndexRefresh: false };
2388
+ const baseId = slugify(`${sourceId || "imported"}-${name}`) || slugify(fileStem) || `imported-skill-${imported.length + 1}`;
2389
+ const id = ensureUniqueId(baseId, takenIds);
2390
+ const skillContent = String(body || raw || "").trim();
2391
+ if (!skillContent) return { needsAgentIndexRefresh: false, needsSkillIndexRefresh: false };
2392
+ upsertEntry(rootDir, {
2393
+ id,
2394
+ type: "skill",
2395
+ name,
2396
+ description: description || "Imported skill",
2397
+ tags: uniqueStrings(["imported", "skill", sourceId || "external", ...parseJsonishArray(getFrontmatterValue(attrs, ["tags"]))]),
2398
+ meta: {
2399
+ sourceId: sourceId || null,
2400
+ repoUrl,
2401
+ branch,
2402
+ relPath,
2403
+ },
2404
+ }, skillContent, { skipIndexSync: true });
2405
+ imported.push({ id, name, relPath, type: "skill", promptId: null });
2406
+ importedByType.skill += 1;
2407
+ return { needsAgentIndexRefresh: false, needsSkillIndexRefresh: true };
2408
+ }
2409
+
2410
+ if (kind !== "agent" || !importAgents) {
2411
+ return { needsAgentIndexRefresh: false, needsSkillIndexRefresh: false };
2412
+ }
2413
+
2414
+ const baseId = slugify(`${sourceId || "imported"}-${name}`) || slugify(fileStem) || `imported-agent-${imported.length + 1}`;
2415
+ const id = ensureUniqueId(baseId, takenIds);
2416
+ const toolHints = parseJsonishArray(getFrontmatterValue(attrs, ["tools", "enabledTools"]));
2417
+ const profileSkillHints = parseJsonishArray(getFrontmatterValue(attrs, ["skills"]));
2418
+ const mcpHints = parseJsonishArray(getFrontmatterValue(attrs, ["enabledMcpServers", "mcpServers", "mcp"]));
2419
+ const titlePatternHints = parseJsonishArray(getFrontmatterValue(attrs, ["titlePatterns", "title_patterns", "patterns"]));
2420
+ const tags = uniqueStrings([
2421
+ "imported",
2422
+ sourceId || "external",
2423
+ ...parseJsonishArray(getFrontmatterValue(attrs, ["tags"])),
2424
+ ...keywords.slice(0, 4),
2425
+ ]);
2426
+ const pathScopes = uniqueStrings(
2427
+ relPath
2428
+ .split(/[\/]/)
2429
+ .slice(0, -1)
2430
+ .map((segment) => slugify(segment))
2431
+ .filter((segment) => segment && segment !== "github" && segment !== "agents"),
2432
+ ).slice(0, 6);
2433
+ const explicitScopes = parseJsonishArray(getFrontmatterValue(attrs, ["scopes", "scope"]));
2434
+ const scopes = uniqueStrings([...explicitScopes, ...pathScopes]).slice(0, 8);
2435
+ const titlePatterns = uniqueStrings([
2436
+ ...titlePatternHints,
2437
+ ...keywordTokens(name, { minLength: 4 }).slice(0, 4).map((token) => `\\b${token.replace(/[.*+?^${}()|[\\]\\]/g, "")}\\b`),
2438
+ ]);
2439
+ const promptId = `${id}-prompt`;
2440
+
2441
+ if (importPrompts && body) {
2442
+ upsertEntry(rootDir, {
2443
+ id: promptId,
2444
+ type: "prompt",
2445
+ name: `${name} Prompt`,
2446
+ description: `Imported prompt from ${known?.name || repoUrl}`,
2447
+ tags: uniqueStrings(["imported", "agent-prompt", sourceId || "external"]),
2448
+ meta: {
2449
+ sourceId: sourceId || null,
2450
+ repoUrl,
2451
+ branch,
2452
+ relPath,
2453
+ },
2454
+ }, body);
2455
+ imported.push({ id: promptId, name: `${name} Prompt`, relPath, type: "prompt", promptId: null });
2456
+ importedByType.prompt += 1;
2827
2457
  }
2828
- if (!isSafeGitRefName(branch)) {
2829
- throw new Error("Branch name contains invalid characters");
2458
+
2459
+ const explicitAgentType = String(getFrontmatterValue(attrs, ["agentType", "agent_type"]) || "").trim().toLowerCase();
2460
+ const profile = {
2461
+ id,
2462
+ name,
2463
+ description,
2464
+ titlePatterns: titlePatterns.length ? titlePatterns : ["\\btask\\b"],
2465
+ scopes,
2466
+ sdk: null,
2467
+ model: null,
2468
+ promptOverride: importPrompts && body ? promptId : null,
2469
+ skills: profileSkillHints,
2470
+ hookProfile: null,
2471
+ env: {},
2472
+ enabledTools: toolHints.length ? toolHints : null,
2473
+ enabledMcpServers: mcpHints,
2474
+ tags,
2475
+ agentType: explicitAgentType || (/voice|audio|realtime/i.test(`${name} ${description}`) ? "voice" : "task"),
2476
+ importMeta: {
2477
+ sourceId: sourceId || null,
2478
+ repoUrl,
2479
+ branch,
2480
+ relPath,
2481
+ },
2482
+ };
2483
+
2484
+ upsertEntry(rootDir, {
2485
+ id,
2486
+ type: "agent",
2487
+ name,
2488
+ description,
2489
+ tags: profile.tags,
2490
+ meta: {
2491
+ sourceId: sourceId || null,
2492
+ repoUrl,
2493
+ branch,
2494
+ relPath,
2495
+ },
2496
+ }, profile, { skipIndexSync: true });
2497
+
2498
+ imported.push({ id, name, relPath, type: "agent", promptId: importPrompts && body ? promptId : null });
2499
+ importedByType.agent += 1;
2500
+ return { needsAgentIndexRefresh: true, needsSkillIndexRefresh: false };
2501
+ }
2502
+
2503
+ function importRepositoryMcpEntries(rootDir, checkoutDir, context) {
2504
+ const { sourceId, repoUrl, branch, takenIds, imported, importedByType } = context;
2505
+ for (const mcp of discoverRepositoryMcpConfigs(checkoutDir)) {
2506
+ const baseId = slugify(`${sourceId || "imported"}-${mcp.id}`) || slugify(mcp.id) || `imported-mcp-${imported.length + 1}`;
2507
+ const id = ensureUniqueId(baseId, takenIds);
2508
+ const content = {
2509
+ id,
2510
+ name: mcp.name,
2511
+ description: "Imported MCP server definition from " + mcp.relPath,
2512
+ transport: mcp.transport,
2513
+ command: mcp.transport === "stdio" ? mcp.command : undefined,
2514
+ args: mcp.transport === "stdio" ? mcp.args : undefined,
2515
+ url: mcp.transport === "url" ? mcp.url : undefined,
2516
+ env: Object.keys(mcp.env || {}).length ? mcp.env : undefined,
2517
+ source: "imported",
2518
+ tags: ["imported", "mcp", sourceId || "external"],
2519
+ };
2520
+ upsertEntry(rootDir, {
2521
+ id,
2522
+ type: "mcp",
2523
+ name: mcp.name,
2524
+ description: content.description,
2525
+ tags: uniqueStrings(["imported", "mcp", sourceId || "external"]),
2526
+ meta: {
2527
+ sourceId: sourceId || null,
2528
+ repoUrl,
2529
+ branch,
2530
+ relPath: mcp.relPath,
2531
+ },
2532
+ }, content);
2533
+ imported.push({ id, name: mcp.name, relPath: mcp.relPath, type: "mcp", promptId: null });
2534
+ importedByType.mcp += 1;
2830
2535
  }
2536
+ }
2537
+
2538
+ export function importAgentProfilesFromRepository(rootDir, options = {}) {
2539
+ const { sourceId, known, repoUrl, branch } = resolveRepositoryImportSource(options);
2831
2540
  const importAgents = options?.importAgents !== false;
2832
2541
  const importSkills = options?.importSkills !== false;
2833
2542
  const importPrompts = options?.importPrompts !== false;
@@ -2842,62 +2551,8 @@ export function importAgentProfilesFromRepository(rootDir, options = {}) {
2842
2551
  ),
2843
2552
  );
2844
2553
 
2845
- const cacheRoot = ensureDir(resolve(getBosunHomeDir(), ".cache", "imports"));
2846
- const checkoutDir = resolve(cacheRoot, `import-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`);
2847
- ensureDir(checkoutDir);
2848
-
2849
- const clone = spawnSync("git", ["clone", "--depth", "1", "--branch", branch, "--", repoUrl, checkoutDir], {
2850
- encoding: "utf8",
2851
- stdio: ["ignore", "pipe", "pipe"],
2852
- timeout: 120_000,
2853
- });
2854
- const cloneStderr = String(clone.stderr || "").trim();
2855
- const checkoutWarning = /clone succeeded.*checkout failed/i.test(cloneStderr);
2856
- if (clone.status !== 0 && !checkoutWarning) {
2857
- rmSync(checkoutDir, { recursive: true, force: true });
2858
- if (/repository not found/i.test(cloneStderr)) {
2859
- throw new Error(`Repository not found: ${repoUrl}`);
2860
- }
2861
- if (/could not read from remote/i.test(cloneStderr)) {
2862
- throw new Error(`Cannot access repository (may be private or require authentication): ${repoUrl}`);
2863
- }
2864
- if (/not found in upstream/i.test(cloneStderr) || /remote branch.*not found/i.test(cloneStderr)) {
2865
- throw new Error(`Branch "${branch}" not found in ${repoUrl}`);
2866
- }
2867
- if (clone.signal === "SIGTERM") {
2868
- throw new Error(`Clone timed out — repository may be too large: ${repoUrl}`);
2869
- }
2870
- throw new Error(`Failed to clone repository: ${cloneStderr || "unknown error"}`);
2871
- }
2872
-
2873
- const files = walkFilesRecursive(checkoutDir);
2874
- const markdownCandidates = files
2875
- .filter((fullPath) => /\.md$/i.test(fullPath))
2876
- .map((fullPath) => {
2877
- const relPath = fullPath.slice(checkoutDir.length + 1).replace(/\\/g, "/");
2878
- const fileName = basename(fullPath);
2879
- const raw = readFileSync(fullPath, "utf8");
2880
- const parsed = parseSimpleFrontmatter(raw);
2881
- return {
2882
- fullPath,
2883
- relPath,
2884
- fileName,
2885
- raw,
2886
- attrs: parsed.attrs,
2887
- body: parsed.body,
2888
- kind: inferImportedEntryKind(relPath, fileName, parsed.attrs),
2889
- };
2890
- })
2891
- .filter((entry) => Boolean(entry.kind))
2892
- .sort((a, b) => {
2893
- const rank = { agent: 0, prompt: 1, skill: 2 };
2894
- const aRank = Number(rank[a.kind] ?? 99);
2895
- const bRank = Number(rank[b.kind] ?? 99);
2896
- if (aRank !== bRank) return aRank - bRank;
2897
- return String(a.relPath || "").localeCompare(String(b.relPath || ""));
2898
- });
2899
-
2900
- const candidates = markdownCandidates.slice(0, maxProfiles);
2554
+ const checkoutDir = createRepositoryImportCheckoutDir("import", repoUrl, branch);
2555
+ const { candidates } = collectRepositoryImportMarkdownCandidates(checkoutDir, { maxEntries: maxProfiles });
2901
2556
 
2902
2557
  const takenIds = new Set(
2903
2558
  listEntries(rootDir).map((entry) => String(entry?.id || "").trim()).filter(Boolean),
@@ -2909,209 +2564,36 @@ export function importAgentProfilesFromRepository(rootDir, options = {}) {
2909
2564
 
2910
2565
  try {
2911
2566
  for (const candidate of candidates) {
2912
- const { attrs, body, relPath, fileName, kind } = candidate;
2567
+ const { relPath } = candidate;
2913
2568
  if (includeEntries && !includeEntries.has(relPath)) continue;
2914
- const fileStem = basename(fileName, ".md");
2915
- const relSegments = relPath.split(/[\\/]/).filter(Boolean);
2916
- const parentSegment = relSegments.length > 1 ? relSegments[relSegments.length - 2] : "";
2917
- const fallbackNameBase = fileStem.toLowerCase() === "skill" && parentSegment ? parentSegment : fileStem;
2918
- const fallbackName = fallbackNameBase.replace(/\.agent$/i, "").replace(/\.skill$/i, "").replace(/\.prompt$/i, "");
2919
- const name = String(getFrontmatterValue(attrs, ["name", "title"]) || fallbackName.replace(/[-_.]+/g, " ")).trim();
2920
- const description = normalizeImportedDescription(getFrontmatterValue(attrs, ["description", "summary"]), body);
2921
- const keywords = keywordTokens(`${name} ${description} ${relPath}`, { minLength: 4 }).slice(0, 10);
2922
-
2923
- if (kind === "prompt") {
2924
- if (!importPrompts) continue;
2925
- const baseId = slugify(`${sourceId || "imported"}-${name}`) || slugify(fileStem) || `imported-prompt-${imported.length + 1}`;
2926
- const id = ensureUniqueId(baseId, takenIds);
2927
- const promptContent = String(body || candidate.raw || "").trim();
2928
- if (!promptContent) continue;
2929
- upsertEntry(rootDir, {
2930
- id,
2931
- type: "prompt",
2932
- name,
2933
- description: description || `Imported prompt from ${known?.name || repoUrl}`,
2934
- tags: uniqueStrings(["imported", "prompt", sourceId || "external", ...parseJsonishArray(getFrontmatterValue(attrs, ["tags"]))]),
2935
- meta: {
2936
- sourceId: sourceId || null,
2937
- repoUrl,
2938
- branch,
2939
- relPath,
2940
- },
2941
- }, promptContent);
2942
- imported.push({ id, name, relPath, type: "prompt", promptId: null });
2943
- importedByType.prompt += 1;
2944
- continue;
2945
- }
2946
-
2947
- if (kind === "skill") {
2948
- if (!importSkills) continue;
2949
- const baseId = slugify(`${sourceId || "imported"}-${name}`) || slugify(fileStem) || `imported-skill-${imported.length + 1}`;
2950
- const id = ensureUniqueId(baseId, takenIds);
2951
- const skillContent = String(body || candidate.raw || "").trim();
2952
- if (!skillContent) continue;
2953
- upsertEntry(rootDir, {
2954
- id,
2955
- type: "skill",
2956
- name,
2957
- description: description || "Imported skill",
2958
- tags: uniqueStrings(["imported", "skill", sourceId || "external", ...parseJsonishArray(getFrontmatterValue(attrs, ["tags"]))]),
2959
- meta: {
2960
- sourceId: sourceId || null,
2961
- repoUrl,
2962
- branch,
2963
- relPath,
2964
- },
2965
- }, skillContent, { skipIndexSync: true });
2966
- imported.push({ id, name, relPath, type: "skill", promptId: null });
2967
- importedByType.skill += 1;
2968
- needsSkillIndexRefresh = true;
2969
- continue;
2970
- }
2971
-
2972
- if (kind !== "agent" || !importAgents) continue;
2973
-
2974
- const baseId = slugify(`${sourceId || "imported"}-${name}`) || slugify(fileStem) || `imported-agent-${imported.length + 1}`;
2975
- const id = ensureUniqueId(baseId, takenIds);
2976
- const toolHints = parseJsonishArray(getFrontmatterValue(attrs, ["tools", "enabledTools"]));
2977
- const profileSkillHints = parseJsonishArray(getFrontmatterValue(attrs, ["skills"]));
2978
- const mcpHints = parseJsonishArray(getFrontmatterValue(attrs, ["enabledMcpServers", "mcpServers", "mcp"]));
2979
- const titlePatternHints = parseJsonishArray(getFrontmatterValue(attrs, ["titlePatterns", "title_patterns", "patterns"]));
2980
- const tags = uniqueStrings([
2981
- "imported",
2982
- sourceId || "external",
2983
- ...parseJsonishArray(getFrontmatterValue(attrs, ["tags"])),
2984
- ...keywords.slice(0, 4),
2985
- ]);
2986
- const pathScopes = uniqueStrings(
2987
- relPath
2988
- .split(/[\\/]/)
2989
- .slice(0, -1)
2990
- .map((segment) => slugify(segment))
2991
- .filter((segment) => segment && segment !== "github" && segment !== "agents"),
2992
- ).slice(0, 6);
2993
- const explicitScopes = parseJsonishArray(getFrontmatterValue(attrs, ["scopes", "scope"]));
2994
- const scopes = uniqueStrings([...explicitScopes, ...pathScopes]).slice(0, 8);
2995
- const titlePatterns = uniqueStrings([
2996
- ...titlePatternHints,
2997
- ...keywordTokens(name, { minLength: 4 }).slice(0, 4).map((token) => `\\b${token.replace(/[.*+?^${}()|[\\]\\]/g, "")}\\b`),
2998
- ]);
2999
- const promptId = `${id}-prompt`;
3000
-
3001
- if (importPrompts && body) {
3002
- upsertEntry(rootDir, {
3003
- id: promptId,
3004
- type: "prompt",
3005
- name: `${name} Prompt`,
3006
- description: `Imported prompt from ${known?.name || repoUrl}`,
3007
- tags: uniqueStrings(["imported", "agent-prompt", sourceId || "external"]),
3008
- meta: {
3009
- sourceId: sourceId || null,
3010
- repoUrl,
3011
- branch,
3012
- relPath,
3013
- },
3014
- }, body);
3015
- imported.push({ id: promptId, name: `${name} Prompt`, relPath, type: "prompt", promptId: null });
3016
- importedByType.prompt += 1;
3017
- }
3018
-
3019
- const explicitAgentType = String(getFrontmatterValue(attrs, ["agentType", "agent_type"]) || "").trim().toLowerCase();
3020
- const profile = {
3021
- id,
3022
- name,
3023
- description,
3024
- titlePatterns: titlePatterns.length ? titlePatterns : ["\\btask\\b"],
3025
- scopes,
3026
- sdk: null,
3027
- model: null,
3028
- promptOverride: importPrompts && body ? promptId : null,
3029
- skills: profileSkillHints,
3030
- hookProfile: null,
3031
- env: {},
3032
- enabledTools: toolHints.length ? toolHints : null,
3033
- enabledMcpServers: mcpHints,
3034
- tags,
3035
- agentType: explicitAgentType || (/voice|audio|realtime/i.test(`${name} ${description}`) ? "voice" : "task"),
3036
- importMeta: {
3037
- sourceId: sourceId || null,
3038
- repoUrl,
3039
- branch,
3040
- relPath,
3041
- },
3042
- };
3043
-
3044
- upsertEntry(rootDir, {
3045
- id,
3046
- type: "agent",
3047
- name,
3048
- description,
3049
- tags: profile.tags,
3050
- meta: {
3051
- sourceId: sourceId || null,
3052
- repoUrl,
3053
- branch,
3054
- relPath,
3055
- },
3056
- }, profile, { skipIndexSync: true });
3057
-
3058
- imported.push({ id, name, relPath, type: "agent", promptId: importPrompts && body ? promptId : null });
3059
- importedByType.agent += 1;
3060
- needsAgentIndexRefresh = true;
2569
+ const result = importRepositoryMarkdownCandidate(rootDir, candidate, {
2570
+ known,
2571
+ repoUrl,
2572
+ branch,
2573
+ sourceId,
2574
+ importAgents,
2575
+ importSkills,
2576
+ importPrompts,
2577
+ takenIds,
2578
+ imported,
2579
+ importedByType,
2580
+ });
2581
+ needsAgentIndexRefresh = needsAgentIndexRefresh || result.needsAgentIndexRefresh;
2582
+ needsSkillIndexRefresh = needsSkillIndexRefresh || result.needsSkillIndexRefresh;
3061
2583
  }
3062
2584
 
3063
2585
  if (importTools) {
3064
- const mcpConfigFiles = [
3065
- { path: resolve(checkoutDir, ".codex", "config.toml"), format: "toml" },
3066
- { path: resolve(checkoutDir, ".mcp.json"), format: "json" },
3067
- { path: resolve(checkoutDir, ".vscode", "mcp.json"), format: "json" },
3068
- ];
3069
- for (const { path: configPath, format } of mcpConfigFiles) {
3070
- if (!existsSync(configPath)) continue;
3071
- let raw = "";
3072
- try {
3073
- raw = readFileSync(configPath, "utf8");
3074
- } catch {
3075
- continue;
3076
- }
3077
- const relPath = relative(checkoutDir, configPath).replace(/\\/g, "/");
3078
- const discovered = format === "toml"
3079
- ? parseMcpServersFromToml(raw, relPath)
3080
- : parseMcpServersFromJson(raw, relPath);
3081
- for (const mcp of discovered) {
3082
- const baseId = slugify(`${sourceId || "imported"}-${mcp.id}`) || slugify(mcp.id) || `imported-mcp-${imported.length + 1}`;
3083
- const id = ensureUniqueId(baseId, takenIds);
3084
- const content = {
3085
- id,
3086
- name: mcp.name,
3087
- description: "Imported MCP server definition from " + relPath,
3088
- transport: mcp.transport,
3089
- command: mcp.transport === "stdio" ? mcp.command : undefined,
3090
- args: mcp.transport === "stdio" ? mcp.args : undefined,
3091
- url: mcp.transport === "url" ? mcp.url : undefined,
3092
- env: Object.keys(mcp.env || {}).length ? mcp.env : undefined,
3093
- source: "imported",
3094
- tags: ["imported", "mcp", sourceId || "external"],
3095
- };
3096
- upsertEntry(rootDir, {
3097
- id,
3098
- type: "mcp",
3099
- name: mcp.name,
3100
- description: content.description,
3101
- tags: uniqueStrings(["imported", "mcp", sourceId || "external"]),
3102
- meta: {
3103
- sourceId: sourceId || null,
3104
- repoUrl,
3105
- branch,
3106
- relPath,
3107
- },
3108
- }, content);
3109
- imported.push({ id, name: mcp.name, relPath, type: "mcp", promptId: null });
3110
- importedByType.mcp += 1;
3111
- }
3112
- }
2586
+ importRepositoryMcpEntries(rootDir, checkoutDir, {
2587
+ sourceId,
2588
+ repoUrl,
2589
+ branch,
2590
+ takenIds,
2591
+ imported,
2592
+ importedByType,
2593
+ });
3113
2594
  }
3114
2595
  } finally {
2596
+
3115
2597
  rmSync(checkoutDir, { recursive: true, force: true });
3116
2598
  }
3117
2599