bosun 0.40.10 → 0.40.12

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.
@@ -69,9 +69,25 @@ const ALERT_COOLDOWN_RETENTION_MS = Math.max(
69
69
  FAILED_SESSION_TRANSIENT_ALERT_MIN_COOLDOWN_MS * 3,
70
70
  3 * 60 * 60 * 1000,
71
71
  ); // keep cooldown history bounded
72
+ const ALERT_COOLDOWN_REPLAY_MIN_BYTES = 256 * 1024;
73
+ const ALERT_COOLDOWN_REPLAY_DEFAULT_MAX_BYTES = 8 * 1024 * 1024;
74
+ const ALERT_COOLDOWN_REPLAY_MAX_CAP_BYTES = 64 * 1024 * 1024;
75
+
76
+ function normalizeReplayMaxBytes(value) {
77
+ const parsed = Number(value);
78
+ if (!Number.isFinite(parsed) || parsed <= 0) {
79
+ return ALERT_COOLDOWN_REPLAY_DEFAULT_MAX_BYTES;
80
+ }
81
+ const rounded = Math.floor(parsed);
82
+ return Math.min(
83
+ ALERT_COOLDOWN_REPLAY_MAX_CAP_BYTES,
84
+ Math.max(ALERT_COOLDOWN_REPLAY_MIN_BYTES, rounded),
85
+ );
86
+ }
87
+
72
88
  const ALERT_COOLDOWN_REPLAY_MAX_BYTES = Math.max(
73
- 256 * 1024,
74
- Number(process.env.AGENT_ALERT_COOLDOWN_REPLAY_MAX_BYTES || 2 * 1024 * 1024) || 2 * 1024 * 1024,
89
+ ALERT_COOLDOWN_REPLAY_MIN_BYTES,
90
+ normalizeReplayMaxBytes(process.env.AGENT_ALERT_COOLDOWN_REPLAY_MAX_BYTES),
75
91
  );
76
92
 
77
93
  function getAlertCooldownMs(alert) {
@@ -367,15 +383,19 @@ async function processLogFile(startPosition) {
367
383
  * @param {Object} event - Parsed JSONL event
368
384
  */
369
385
  async function analyzeEvent(event) {
370
- const { attempt_id, event_type, timestamp } = event;
386
+ const { event_type, timestamp } = event;
387
+ const attemptId = String(event?.attempt_id || "").trim();
388
+ if (!attemptId) {
389
+ return;
390
+ }
371
391
  const parsedTs = Date.parse(timestamp);
372
392
  const eventTime = Number.isFinite(parsedTs) ? parsedTs : Date.now();
373
393
  const eventIso = new Date(eventTime).toISOString();
374
394
 
375
395
  // Initialize session state if needed
376
- if (!activeSessions.has(attempt_id)) {
377
- activeSessions.set(attempt_id, {
378
- attempt_id,
396
+ if (!activeSessions.has(attemptId)) {
397
+ activeSessions.set(attemptId, {
398
+ attempt_id: attemptId,
379
399
  errors: [],
380
400
  toolCalls: [],
381
401
  lastActivity: eventIso,
@@ -385,7 +405,7 @@ async function analyzeEvent(event) {
385
405
  });
386
406
  }
387
407
 
388
- const session = activeSessions.get(attempt_id);
408
+ const session = activeSessions.get(attemptId);
389
409
  session.lastActivity = eventIso;
390
410
 
391
411
  // Route to specific analyzers
@@ -401,7 +421,7 @@ async function analyzeEvent(event) {
401
421
  break;
402
422
  case "session_end":
403
423
  await analyzeSessionEnd(session, event);
404
- activeSessions.delete(attempt_id);
424
+ activeSessions.delete(attemptId);
405
425
  break;
406
426
  }
407
427
 
@@ -647,7 +667,10 @@ async function emitAlert(alert) {
647
667
  ...alert,
648
668
  };
649
669
 
650
- console.error(`[ALERT] ${alert.type}: ${alert.attempt_id}`);
670
+ const alertScope = String(
671
+ alert?.attempt_id || alert?.task_id || alert?.executor || "unknown",
672
+ );
673
+ console.error(`[ALERT] ${alert.type}: ${alertScope}`);
651
674
 
652
675
  // Append to alerts log
653
676
  try {
package/cli.mjs CHANGED
@@ -76,6 +76,7 @@ function showHelp() {
76
76
  bosun [options]
77
77
 
78
78
  COMMANDS
79
+ audit <command> [options] Codebase annotation audit workflows (scan/generate/warn/manifest/index/trim/conformity/migrate)
79
80
  --setup Launch the web-based setup wizard (default)
80
81
  --setup-terminal Run the legacy terminal setup wizard
81
82
  --where Show the resolved bosun config directory
@@ -1237,6 +1238,22 @@ async function main() {
1237
1238
  process.exit(0);
1238
1239
  }
1239
1240
 
1241
+ // Handle 'audit' subcommand before --help so command-specific help works.
1242
+ const auditFlagIndex = args.indexOf("--audit");
1243
+ const auditCommandIndex =
1244
+ args[0] === "audit"
1245
+ ? 0
1246
+ : args[0]?.startsWith("--")
1247
+ ? args.indexOf("audit")
1248
+ : -1;
1249
+ if (auditCommandIndex >= 0 || auditFlagIndex >= 0) {
1250
+ const { runAuditCli } = await import("./lib/codebase-audit.mjs");
1251
+ const commandStartIndex = auditCommandIndex >= 0 ? auditCommandIndex : auditFlagIndex;
1252
+ const auditArgs = args.slice(commandStartIndex + 1);
1253
+ const result = await runAuditCli(auditArgs);
1254
+ process.exit(Number.isInteger(result?.exitCode) ? result.exitCode : 0);
1255
+ }
1256
+
1240
1257
  // Handle --help
1241
1258
  if (args.includes("--help") || args.includes("-h")) {
1242
1259
  showHelp();
@@ -15,6 +15,8 @@ const STRIPPED_GIT_ENV_KEYS = [
15
15
  const BLOCKED_TEST_GIT_IDENTITIES = new Set([
16
16
  "test@example.com",
17
17
  "bosun-tests@example.com",
18
+ "bot@example.com",
19
+ "test@test.com",
18
20
  ]);
19
21
 
20
22
  const TEST_FIXTURE_SENTINEL_PATHS = new Set([
@@ -264,3 +266,18 @@ export function evaluateBranchSafetyForPush(worktreePath, opts = {}) {
264
266
  },
265
267
  };
266
268
  }
269
+
270
+ /**
271
+ * Clear any blocked test git identity from a worktree's local config.
272
+ * Worktrees inherit the parent repo's config, so if a test ever set
273
+ * user.name/email there it will poison all task commits until cleared.
274
+ * Call this after acquiring any worktree.
275
+ */
276
+ export function clearBlockedWorktreeIdentity(worktreePath) {
277
+ const email = getGitConfig(worktreePath, "user.email").toLowerCase();
278
+ if (!BLOCKED_TEST_GIT_IDENTITIES.has(email)) return false;
279
+
280
+ runGit(["config", "--local", "--unset", "user.email"], worktreePath, 5_000);
281
+ runGit(["config", "--local", "--unset", "user.name"], worktreePath, 5_000);
282
+ return true;
283
+ }
@@ -1252,7 +1252,7 @@ function parseSimpleFrontmatter(markdown = "") {
1252
1252
  for (const line of head) {
1253
1253
  const m = line.match(/^([A-Za-z0-9_-]+)\s*:\s*(.*)$/);
1254
1254
  if (!m) continue;
1255
- attrs[m[1].trim()] = String(m[2] || "").trim();
1255
+ attrs[m[1].trim()] = parseTomlValue(m[2]);
1256
1256
  }
1257
1257
  const body = text.slice(end + 5).trim();
1258
1258
  return { attrs, body };
@@ -1293,6 +1293,57 @@ function ensureUniqueId(baseId, takenIds) {
1293
1293
  return id;
1294
1294
  }
1295
1295
 
1296
+ function getFrontmatterValue(attrs = {}, keys = []) {
1297
+ if (!attrs || typeof attrs !== "object") return null;
1298
+ for (const key of keys) {
1299
+ if (Object.hasOwn(attrs, key)) return attrs[key];
1300
+ }
1301
+ const lowerMap = new Map(
1302
+ Object.keys(attrs).map((key) => [String(key || "").toLowerCase(), attrs[key]]),
1303
+ );
1304
+ for (const key of keys) {
1305
+ const hit = lowerMap.get(String(key || "").toLowerCase());
1306
+ if (hit != null) return hit;
1307
+ }
1308
+ return null;
1309
+ }
1310
+
1311
+ function normalizeImportedDescription(rawDescription, body = "") {
1312
+ const raw = String(rawDescription || "").trim();
1313
+ if (raw) {
1314
+ if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) {
1315
+ return raw.slice(1, -1).trim();
1316
+ }
1317
+ return raw;
1318
+ }
1319
+ const fallback = String(body || "")
1320
+ .split(/\r?\n/)
1321
+ .map((line) => line.trim())
1322
+ .find((line) => line && !line.startsWith("#"));
1323
+ return String(fallback || "Imported library entry").trim();
1324
+ }
1325
+
1326
+ function inferImportedEntryKind(relPath = "", fileName = "", attrs = {}) {
1327
+ const pathLower = String(relPath || "").toLowerCase();
1328
+ const fileLower = String(fileName || "").toLowerCase();
1329
+ const explicitType = String(getFrontmatterValue(attrs, ["type", "kind", "resourceType"]) || "").trim().toLowerCase();
1330
+ if (explicitType === "agent" || explicitType === "profile") return "agent";
1331
+ if (explicitType === "skill") return "skill";
1332
+ if (explicitType === "prompt") return "prompt";
1333
+
1334
+ if (/\.agent\.md$/i.test(fileLower) || /\/\.github\/agents\//i.test(pathLower)) return "agent";
1335
+ if (
1336
+ fileLower === "skill.md"
1337
+ || /\.skill\.md$/i.test(fileLower)
1338
+
1339
+ ) return "skill";
1340
+ if (
1341
+ /\.prompt\.md$/i.test(fileLower)
1342
+ || /\/prompts\//i.test(pathLower)
1343
+ || /\/\.github\/prompts\//i.test(pathLower)
1344
+ ) return "prompt";
1345
+ return null;
1346
+ }
1296
1347
  function humanizeSlug(slug) {
1297
1348
  const value = String(slug || "").trim();
1298
1349
  if (!value) return "";
@@ -1633,8 +1684,17 @@ export function importAgentProfilesFromRepository(rootDir, options = {}) {
1633
1684
  if (!isSafeGitRefName(branch)) {
1634
1685
  throw new Error("branch contains unsafe characters");
1635
1686
  }
1636
- const maxProfiles = Math.max(1, Math.min(500, Number.parseInt(String(options?.maxProfiles || "100"), 10) || 100));
1687
+ const maxProfiles = Math.max(
1688
+ 1,
1689
+ Math.min(
1690
+ 500,
1691
+ Number.parseInt(String(options?.maxEntries ?? options?.maxProfiles ?? "100"), 10) || 100,
1692
+ ),
1693
+ );
1694
+ const importAgents = options?.importAgents !== false;
1695
+ const importSkills = options?.importSkills !== false;
1637
1696
  const importPrompts = options?.importPrompts !== false;
1697
+ const importTools = options?.importTools !== false;
1638
1698
 
1639
1699
  const cacheRoot = ensureDir(resolve(rootDir || getBosunHomeDir(), ".bosun", ".cache", "imports"));
1640
1700
  const checkoutDir = resolve(cacheRoot, `import-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`);
@@ -1650,88 +1710,247 @@ export function importAgentProfilesFromRepository(rootDir, options = {}) {
1650
1710
  }
1651
1711
 
1652
1712
  const files = walkFilesRecursive(checkoutDir);
1653
- const candidates = files
1713
+ const markdownCandidates = files
1654
1714
  .filter((fullPath) => /\.md$/i.test(fullPath))
1655
- .filter((fullPath) => /\.agent\.md$/i.test(fullPath) || /[\\/]\.github[\\/]agents[\\/]/i.test(fullPath));
1715
+ .map((fullPath) => {
1716
+ const relPath = fullPath.slice(checkoutDir.length + 1).replace(/\\/g, "/");
1717
+ const fileName = basename(fullPath);
1718
+ const raw = readFileSync(fullPath, "utf8");
1719
+ const parsed = parseSimpleFrontmatter(raw);
1720
+ return {
1721
+ fullPath,
1722
+ relPath,
1723
+ fileName,
1724
+ raw,
1725
+ attrs: parsed.attrs,
1726
+ body: parsed.body,
1727
+ kind: inferImportedEntryKind(relPath, fileName, parsed.attrs),
1728
+ };
1729
+ })
1730
+ .filter((entry) => Boolean(entry.kind))
1731
+ .sort((a, b) => {
1732
+ const rank = { agent: 0, prompt: 1, skill: 2 };
1733
+ const aRank = Number(rank[a.kind] ?? 99);
1734
+ const bRank = Number(rank[b.kind] ?? 99);
1735
+ if (aRank !== bRank) return aRank - bRank;
1736
+ return String(a.relPath || "").localeCompare(String(b.relPath || ""));
1737
+ });
1656
1738
 
1657
- const takenIds = new Set(listEntries(rootDir, { type: "agent" }).map((entry) => String(entry?.id || "").trim()).filter(Boolean));
1739
+ const candidates = markdownCandidates.slice(0, maxProfiles);
1740
+
1741
+ const takenIds = new Set(
1742
+ listEntries(rootDir).map((entry) => String(entry?.id || "").trim()).filter(Boolean),
1743
+ );
1658
1744
  const imported = [];
1745
+ const importedByType = { agent: 0, prompt: 0, skill: 0, mcp: 0 };
1746
+ let needsAgentIndexRefresh = false;
1747
+ let needsSkillIndexRefresh = false;
1748
+
1749
+ try {
1750
+ for (const candidate of candidates) {
1751
+ const { attrs, body, relPath, fileName, kind } = candidate;
1752
+ const fileStem = basename(fileName, ".md");
1753
+ const relSegments = relPath.split(/[\\/]/).filter(Boolean);
1754
+ const parentSegment = relSegments.length > 1 ? relSegments[relSegments.length - 2] : "";
1755
+ const fallbackNameBase = fileStem.toLowerCase() === "skill" && parentSegment ? parentSegment : fileStem;
1756
+ const fallbackName = fallbackNameBase.replace(/\.agent$/i, "").replace(/\.skill$/i, "").replace(/\.prompt$/i, "");
1757
+ const name = String(getFrontmatterValue(attrs, ["name", "title"]) || fallbackName.replace(/[-_.]+/g, " ")).trim();
1758
+ const description = normalizeImportedDescription(getFrontmatterValue(attrs, ["description", "summary"]), body);
1759
+ const keywords = keywordTokens(`${name} ${description} ${relPath}`, { minLength: 4 }).slice(0, 10);
1760
+
1761
+ if (kind === "prompt") {
1762
+ if (!importPrompts) continue;
1763
+ const baseId = slugify(`${sourceId || "imported"}-${name}`) || slugify(fileStem) || `imported-prompt-${imported.length + 1}`;
1764
+ const id = ensureUniqueId(baseId, takenIds);
1765
+ const promptContent = String(body || candidate.raw || "").trim();
1766
+ if (!promptContent) continue;
1767
+ upsertEntry(rootDir, {
1768
+ id,
1769
+ type: "prompt",
1770
+ name,
1771
+ description: description || `Imported prompt from ${known?.name || repoUrl}`,
1772
+ tags: uniqueStrings(["imported", "prompt", sourceId || "external", ...parseJsonishArray(getFrontmatterValue(attrs, ["tags"]))]),
1773
+ meta: {
1774
+ sourceId: sourceId || null,
1775
+ repoUrl,
1776
+ branch,
1777
+ relPath,
1778
+ },
1779
+ }, promptContent);
1780
+ imported.push({ id, name, relPath, type: "prompt", promptId: null });
1781
+ importedByType.prompt += 1;
1782
+ continue;
1783
+ }
1784
+
1785
+ if (kind === "skill") {
1786
+ if (!importSkills) continue;
1787
+ const baseId = slugify(`${sourceId || "imported"}-${name}`) || slugify(fileStem) || `imported-skill-${imported.length + 1}`;
1788
+ const id = ensureUniqueId(baseId, takenIds);
1789
+ const skillContent = String(body || candidate.raw || "").trim();
1790
+ if (!skillContent) continue;
1791
+ upsertEntry(rootDir, {
1792
+ id,
1793
+ type: "skill",
1794
+ name,
1795
+ description: description || "Imported skill",
1796
+ tags: uniqueStrings(["imported", "skill", sourceId || "external", ...parseJsonishArray(getFrontmatterValue(attrs, ["tags"]))]),
1797
+ meta: {
1798
+ sourceId: sourceId || null,
1799
+ repoUrl,
1800
+ branch,
1801
+ relPath,
1802
+ },
1803
+ }, skillContent, { skipIndexSync: true });
1804
+ imported.push({ id, name, relPath, type: "skill", promptId: null });
1805
+ importedByType.skill += 1;
1806
+ needsSkillIndexRefresh = true;
1807
+ continue;
1808
+ }
1809
+
1810
+ if (kind !== "agent" || !importAgents) continue;
1811
+
1812
+ const baseId = slugify(`${sourceId || "imported"}-${name}`) || slugify(fileStem) || `imported-agent-${imported.length + 1}`;
1813
+ const id = ensureUniqueId(baseId, takenIds);
1814
+ const toolHints = parseJsonishArray(getFrontmatterValue(attrs, ["tools", "enabledTools"]));
1815
+ const profileSkillHints = parseJsonishArray(getFrontmatterValue(attrs, ["skills"]));
1816
+ const mcpHints = parseJsonishArray(getFrontmatterValue(attrs, ["enabledMcpServers", "mcpServers", "mcp"]));
1817
+ const titlePatternHints = parseJsonishArray(getFrontmatterValue(attrs, ["titlePatterns", "title_patterns", "patterns"]));
1818
+ const tags = uniqueStrings([
1819
+ "imported",
1820
+ sourceId || "external",
1821
+ ...parseJsonishArray(getFrontmatterValue(attrs, ["tags"])),
1822
+ ...keywords.slice(0, 4),
1823
+ ]);
1824
+ const pathScopes = uniqueStrings(
1825
+ relPath
1826
+ .split(/[\\/]/)
1827
+ .slice(0, -1)
1828
+ .map((segment) => slugify(segment))
1829
+ .filter((segment) => segment && segment !== "github" && segment !== "agents"),
1830
+ ).slice(0, 6);
1831
+ const explicitScopes = parseJsonishArray(getFrontmatterValue(attrs, ["scopes", "scope"]));
1832
+ const scopes = uniqueStrings([...explicitScopes, ...pathScopes]).slice(0, 8);
1833
+ const titlePatterns = uniqueStrings([
1834
+ ...titlePatternHints,
1835
+ ...keywordTokens(name, { minLength: 4 }).slice(0, 4).map((token) => `\\b${token.replace(/[.*+?^${}()|[\\]\\]/g, "")}\\b`),
1836
+ ]);
1837
+ const promptId = `${id}-prompt`;
1838
+
1839
+ if (importPrompts && body) {
1840
+ upsertEntry(rootDir, {
1841
+ id: promptId,
1842
+ type: "prompt",
1843
+ name: `${name} Prompt`,
1844
+ description: `Imported prompt from ${known?.name || repoUrl}`,
1845
+ tags: uniqueStrings(["imported", "agent-prompt", sourceId || "external"]),
1846
+ meta: {
1847
+ sourceId: sourceId || null,
1848
+ repoUrl,
1849
+ branch,
1850
+ relPath,
1851
+ },
1852
+ }, body);
1853
+ imported.push({ id: promptId, name: `${name} Prompt`, relPath, type: "prompt", promptId: null });
1854
+ importedByType.prompt += 1;
1855
+ }
1856
+
1857
+ const explicitAgentType = String(getFrontmatterValue(attrs, ["agentType", "agent_type"]) || "").trim().toLowerCase();
1858
+ const profile = {
1859
+ id,
1860
+ name,
1861
+ description,
1862
+ titlePatterns: titlePatterns.length ? titlePatterns : ["\\btask\\b"],
1863
+ scopes,
1864
+ sdk: null,
1865
+ model: null,
1866
+ promptOverride: importPrompts && body ? promptId : null,
1867
+ skills: profileSkillHints,
1868
+ hookProfile: null,
1869
+ env: {},
1870
+ enabledTools: toolHints.length ? toolHints : null,
1871
+ enabledMcpServers: mcpHints,
1872
+ tags,
1873
+ agentType: explicitAgentType || (/voice|audio|realtime/i.test(`${name} ${description}`) ? "voice" : "task"),
1874
+ importMeta: {
1875
+ sourceId: sourceId || null,
1876
+ repoUrl,
1877
+ branch,
1878
+ relPath,
1879
+ },
1880
+ };
1659
1881
 
1660
- for (const fullPath of candidates.slice(0, maxProfiles)) {
1661
- const raw = readFileSync(fullPath, "utf8");
1662
- const { attrs, body } = parseSimpleFrontmatter(raw);
1663
- const relPath = fullPath.slice(checkoutDir.length + 1).replace(/\\/g, "/");
1664
- const fileName = basename(fullPath).replace(/\.md$/i, "");
1665
- const name = String(attrs.name || fileName.replace(/[-_.]+/g, " ")).trim();
1666
- const description = String(attrs.description || body.split(/\r?\n/).find((line) => line.trim()) || "Imported agent profile").trim();
1667
- const baseId = slugify(`${sourceId || "imported"}-${name}`) || slugify(fileName) || `imported-agent-${imported.length + 1}`;
1668
- const id = ensureUniqueId(baseId, takenIds);
1669
-
1670
- const toolHints = parseJsonishArray(attrs.tools);
1671
- const keywords = keywordTokens(`${name} ${description} ${relPath}`, { minLength: 4 }).slice(0, 10);
1672
- const titlePatterns = uniqueStrings(keywords.slice(0, 6).map((token) => `\\b${token.replace(/[.*+?^${}()|[\\]\\]/g, "")}\\b`));
1673
- const scopes = uniqueStrings(relPath.split(/[\\/]/).map((segment) => slugify(segment))).filter((segment) => segment && segment !== "github" && segment !== "agents").slice(0, 6);
1674
- const promptId = `${id}-prompt`;
1675
-
1676
- if (importPrompts && body) {
1677
1882
  upsertEntry(rootDir, {
1678
- id: promptId,
1679
- type: "prompt",
1680
- name: `${name} Prompt`,
1681
- description: `Imported prompt from ${known?.name || repoUrl}`,
1682
- tags: uniqueStrings(["imported", "agent-prompt", sourceId || "external"]),
1883
+ id,
1884
+ type: "agent",
1885
+ name,
1886
+ description,
1887
+ tags: profile.tags,
1683
1888
  meta: {
1684
1889
  sourceId: sourceId || null,
1685
1890
  repoUrl,
1686
1891
  branch,
1687
1892
  relPath,
1688
1893
  },
1689
- }, body);
1690
- }
1894
+ }, profile, { skipIndexSync: true });
1691
1895
 
1692
- const profile = {
1693
- id,
1694
- name,
1695
- description,
1696
- titlePatterns: titlePatterns.length ? titlePatterns : ["\\btask\\b"],
1697
- scopes,
1698
- sdk: null,
1699
- model: null,
1700
- promptOverride: importPrompts ? promptId : null,
1701
- skills: [],
1702
- hookProfile: null,
1703
- env: {},
1704
- enabledTools: toolHints.length ? toolHints : null,
1705
- enabledMcpServers: [],
1706
- tags: uniqueStrings(["imported", sourceId || "external", ...keywords.slice(0, 4)]),
1707
- agentType: /voice|audio|realtime/i.test(`${name} ${description}`) ? "voice" : "task",
1708
- importMeta: {
1709
- sourceId: sourceId || null,
1710
- repoUrl,
1711
- branch,
1712
- relPath,
1713
- },
1714
- };
1715
-
1716
- upsertEntry(rootDir, {
1717
- id,
1718
- type: "agent",
1719
- name,
1720
- description,
1721
- tags: profile.tags,
1722
- meta: {
1723
- sourceId: sourceId || null,
1724
- repoUrl,
1725
- branch,
1726
- relPath,
1727
- },
1728
- }, profile, { skipIndexSync: true });
1896
+ imported.push({ id, name, relPath, type: "agent", promptId: importPrompts && body ? promptId : null });
1897
+ importedByType.agent += 1;
1898
+ needsAgentIndexRefresh = true;
1899
+ }
1729
1900
 
1730
- imported.push({ id, name, relPath, promptId: importPrompts ? promptId : null });
1901
+ if (importTools) {
1902
+ const mcpCandidates = uniqueStrings([
1903
+ resolve(checkoutDir, ".codex", "config.toml"),
1904
+ ]);
1905
+ for (const configPath of mcpCandidates) {
1906
+ if (!existsSync(configPath)) continue;
1907
+ let raw = "";
1908
+ try {
1909
+ raw = readFileSync(configPath, "utf8");
1910
+ } catch {
1911
+ continue;
1912
+ }
1913
+ const relPath = relative(checkoutDir, configPath).replace(/\\/g, "/");
1914
+ const discovered = parseMcpServersFromToml(raw, relPath);
1915
+ for (const mcp of discovered) {
1916
+ const baseId = slugify(`${sourceId || "imported"}-${mcp.id}`) || slugify(mcp.id) || `imported-mcp-${imported.length + 1}`;
1917
+ const id = ensureUniqueId(baseId, takenIds);
1918
+ const content = {
1919
+ id,
1920
+ name: mcp.name,
1921
+ description: "Imported MCP server definition from " + relPath,
1922
+ transport: mcp.transport,
1923
+ command: mcp.transport === "stdio" ? mcp.command : undefined,
1924
+ args: mcp.transport === "stdio" ? mcp.args : undefined,
1925
+ url: mcp.transport === "url" ? mcp.url : undefined,
1926
+ env: Object.keys(mcp.env || {}).length ? mcp.env : undefined,
1927
+ source: "imported",
1928
+ tags: ["imported", "mcp", sourceId || "external"],
1929
+ };
1930
+ upsertEntry(rootDir, {
1931
+ id,
1932
+ type: "mcp",
1933
+ name: mcp.name,
1934
+ description: content.description,
1935
+ tags: uniqueStrings(["imported", "mcp", sourceId || "external"]),
1936
+ meta: {
1937
+ sourceId: sourceId || null,
1938
+ repoUrl,
1939
+ branch,
1940
+ relPath,
1941
+ },
1942
+ }, content);
1943
+ imported.push({ id, name: mcp.name, relPath, type: "mcp", promptId: null });
1944
+ importedByType.mcp += 1;
1945
+ }
1946
+ }
1947
+ }
1948
+ } finally {
1949
+ rmSync(checkoutDir, { recursive: true, force: true });
1731
1950
  }
1732
1951
 
1733
- rebuildAgentProfileIndex(rootDir);
1734
- rmSync(checkoutDir, { recursive: true, force: true });
1952
+ if (needsAgentIndexRefresh) rebuildAgentProfileIndex(rootDir);
1953
+ if (needsSkillIndexRefresh) rebuildSkillEntryIndex(rootDir);
1735
1954
 
1736
1955
  return {
1737
1956
  ok: true,
@@ -1739,11 +1958,11 @@ export function importAgentProfilesFromRepository(rootDir, options = {}) {
1739
1958
  repoUrl,
1740
1959
  branch,
1741
1960
  importedCount: imported.length,
1961
+ importedByType,
1742
1962
  imported,
1743
1963
  };
1744
1964
  }
1745
1965
 
1746
-
1747
1966
  // ── Scope Auto-Detection ─────────────────────────────────────────────────────
1748
1967
 
1749
1968
  /**