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.
- package/agent/agent-work-analyzer.mjs +32 -9
- package/cli.mjs +17 -0
- package/git/git-safety.mjs +17 -0
- package/infra/library-manager.mjs +289 -70
- package/infra/monitor.mjs +378 -45
- package/kanban/kanban-adapter.mjs +29 -0
- package/package.json +2 -2
- package/server/ui-server.mjs +172 -32
- package/task/task-claims.mjs +99 -36
- package/task/task-cli.mjs +522 -2
- package/task/task-executor.mjs +782 -48
- package/task/task-store.mjs +88 -4
- package/telegram/telegram-bot.mjs +122 -32
- package/ui/app.js +16 -1
- package/ui/components/chat-view.js +7 -2
- package/ui/components/kanban-board.js +19 -10
- package/ui/components/session-list.js +29 -5
- package/ui/demo-defaults.js +2622 -1828
- package/ui/modules/session-api.js +1 -1
- package/ui/modules/state.js +6 -6
- package/ui/tabs/agents.js +4 -8
- package/ui/tabs/library.js +69 -6
- package/ui/tabs/workflows.js +247 -43
- package/workflow/workflow-engine.mjs +56 -12
- package/workflow/workflow-nodes.mjs +474 -64
- package/workflow/workflow-templates.mjs +85 -1
- package/workflow-templates/agents.mjs +66 -17
- package/workflow-templates/github.mjs +3 -1
- package/workflow-templates/reliability.mjs +23 -12
- package/workflow-templates/task-batch.mjs +17 -3
- package/workflow-templates/task-lifecycle.mjs +84 -13
|
@@ -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
|
-
|
|
74
|
-
|
|
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 {
|
|
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(
|
|
377
|
-
activeSessions.set(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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();
|
package/git/git-safety.mjs
CHANGED
|
@@ -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()] =
|
|
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(
|
|
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
|
|
1713
|
+
const markdownCandidates = files
|
|
1654
1714
|
.filter((fullPath) => /\.md$/i.test(fullPath))
|
|
1655
|
-
.
|
|
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
|
|
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
|
|
1679
|
-
type: "
|
|
1680
|
-
name
|
|
1681
|
-
description
|
|
1682
|
-
tags:
|
|
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
|
-
},
|
|
1690
|
-
}
|
|
1894
|
+
}, profile, { skipIndexSync: true });
|
|
1691
1895
|
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
/**
|