| 9 | } = require(path.join(__dirname, '..', 'contracts', 'agent-role')); |
| 10 | |
| 11 | function parseAgentFrontmatter(content) { |
| 12 | const normalized = content.replace(/\r\n/g, '\n'); |
| 13 | const match = normalized.match(/^---\n([\s\S]*?)\n---/); |
| 14 | if (!match) return {}; |
| 15 | |
| 16 | const raw = match[1]; |
| 17 | const lines = raw.split('\n'); |
| 18 | const fm = {}; |
| 19 | let currentListKey = null; |
| 20 | |
| 21 | for (const line of lines) { |
| 22 | if (!line.trim()) continue; |
| 23 | // A YAML comment is not a key. Without this, `# model: a STRONG model` became |
| 24 | // a frontmatter entry named "# model". |
| 25 | if (line.trimStart().startsWith('#')) continue; |
| 26 | |
| 27 | const listMatch = line.match(/^\s*-\s+(.*)$/); |
| 28 | if (listMatch && currentListKey) { |
| 29 | if (!Array.isArray(fm[currentListKey])) fm[currentListKey] = []; |
| 30 | fm[currentListKey].push(listMatch[1].trim()); |
| 31 | continue; |
| 32 | } |
| 33 | |
| 34 | const colonIdx = line.indexOf(':'); |
| 35 | if (colonIdx === -1) continue; |
| 36 | |
| 37 | const key = line.slice(0, colonIdx).trim(); |
| 38 | let val = line.slice(colonIdx + 1).trim(); |
| 39 | currentListKey = null; |
| 40 | |
| 41 | if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) { |
| 42 | val = val.slice(1, -1); |
| 43 | } |
| 44 | |
| 45 | if (val === '>-' || val === '>') { |
| 46 | continue; |
| 47 | } |
| 48 | |
| 49 | if (!val) { |
| 50 | currentListKey = key; |
| 51 | fm[key] = []; |
| 52 | continue; |
| 53 | } |
| 54 | |
| 55 | if (/^\d+$/.test(val)) { |
| 56 | fm[key] = Number(val); |
| 57 | } else { |
| 58 | fm[key] = val; |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | // A folded scalar ends at the next top-level key, a column-0 comment, or the |
| 63 | // closing fence. Without the `\n#` alternative the scalar swallowed trailing |
| 64 | // YAML comments into the description. Only column-0 `#` terminates, so an |
| 65 | // indented `#` inside the description itself is still part of the text. |
| 66 | const descMatch = normalized.match(/description:\s*>-?\n([\s\S]*?)(?=\n#|\n[a-zA-Z][\w-]*:|\n---)/); |
| 67 | if (descMatch) { |
| 68 | fm.description = descMatch[1].replace(/\n\s*/g, ' ').trim(); |