* Transform frontmatter for external hosts. * Claude: strips `sensitive:` field (only Factory uses it). * Codex: keeps name + description only, enforces 1024-char limit. * Factory: keeps name + description + user-invocable, conditionally adds disable-model-invocation.
(content: string, host: Host)
| 508 | * Factory: keeps name + description + user-invocable, conditionally adds disable-model-invocation. |
| 509 | */ |
| 510 | function transformFrontmatter(content: string, host: Host): string { |
| 511 | const hostConfig = getHostConfig(host); |
| 512 | const fm = hostConfig.frontmatter; |
| 513 | |
| 514 | if (fm.mode === 'denylist') { |
| 515 | // Denylist mode: strip listed fields, keep everything else |
| 516 | for (const field of fm.stripFields || []) { |
| 517 | if (field === 'voice-triggers') { |
| 518 | content = content.replace(/^voice-triggers:\n(?:\s+-\s+"[^"]*"\n?)*/m, ''); |
| 519 | } else { |
| 520 | content = content.replace(new RegExp(`^${field}:\\s*.*\\n`, 'm'), ''); |
| 521 | } |
| 522 | } |
| 523 | return content; |
| 524 | } |
| 525 | |
| 526 | // Allowlist mode: reconstruct frontmatter with only allowed fields |
| 527 | const fmStart = content.indexOf('---\n'); |
| 528 | if (fmStart !== 0) return content; |
| 529 | const fmEnd = content.indexOf('\n---', fmStart + 4); |
| 530 | if (fmEnd === -1) return content; |
| 531 | const frontmatter = content.slice(fmStart + 4, fmEnd); |
| 532 | const body = content.slice(fmEnd + 4); |
| 533 | const { name, description } = extractNameAndDescription(content); |
| 534 | |
| 535 | // Description limit enforcement |
| 536 | if (fm.descriptionLimit) { |
| 537 | const behavior = fm.descriptionLimitBehavior || 'error'; |
| 538 | if (description.length > fm.descriptionLimit) { |
| 539 | if (behavior === 'error') { |
| 540 | throw new Error( |
| 541 | `${hostConfig.displayName} description for "${name}" is ${description.length} chars (max ${fm.descriptionLimit}). ` + |
| 542 | `Compress the description in the .tmpl file.` |
| 543 | ); |
| 544 | } else if (behavior === 'warn') { |
| 545 | console.warn(`WARNING: ${hostConfig.displayName} description for "${name}" exceeds ${fm.descriptionLimit} chars`); |
| 546 | } |
| 547 | // 'truncate' — silently proceed |
| 548 | } |
| 549 | } |
| 550 | |
| 551 | // Build frontmatter with allowed fields |
| 552 | const indentedDesc = description.split('\n').map(l => ` ${l}`).join('\n'); |
| 553 | let newFm = `---\nname: ${name}\ndescription: |\n${indentedDesc}\n`; |
| 554 | |
| 555 | // Add extra fields (host-wide) |
| 556 | if (fm.extraFields) { |
| 557 | for (const [key, value] of Object.entries(fm.extraFields)) { |
| 558 | if (key !== 'name' && key !== 'description') { |
| 559 | newFm += `${key}: ${value}\n`; |
| 560 | } |
| 561 | } |
| 562 | } |
| 563 | |
| 564 | // Add conditional fields |
| 565 | if (fm.conditionalFields) { |
| 566 | for (const rule of fm.conditionalFields) { |
| 567 | const match = Object.entries(rule.if).every(([k, v]) => |
no test coverage detected