import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; /** File name of the bundle-managed system prompt cover inside the bundle's `config/` directory. */ export const COVER_FILE_NAME = "system-prompt-cover.md"; /** * Matches the newline-delimited boundary before the first top-level prompt section. * * Pi renders the system prompt as `` followed by sections wrapped in tags of * the same name (for example ``). Nested markup such as * `` carries attributes and therefore never matches a * bare tag line, so the first match is the end of the preamble. */ const FIRST_SECTION_BOUNDARY = /\n<[a-z][a-z0-9_-]*>\n/; /** * Resolve the cover path inside the installed bundle's `config/` directory — * the single source of truth, read directly at runtime with no deployment step. */ export function resolveCoverPath(): string { return fileURLToPath(new URL(`../config/${COVER_FILE_NAME}`, import.meta.url)); } /** * Read the bundle-managed cover file. * * Returns `undefined` when the file is absent, unreadable, or blank so the caller can * leave Pi's own default prompt untouched instead of forcing an empty preamble. */ export function readCoverFile(coverPath: string): string | undefined { let raw: string; try { raw = readFileSync(coverPath, "utf8"); } catch { return undefined; } const trimmed = raw.replace(/\s+$/, ""); return trimmed.length > 0 ? trimmed : undefined; } /** * Replace the rendered prompt's preamble with `cover`, keeping everything after the first * top-level section byte-for-byte identical. * * Pi builds the preamble at the top of the rendered prompt, so swapping that prefix is * what preserves section order: options-based replacement (`customPrompt`) would drop * `tools`/`rules`/`docs` and rebuild them at the end, reordering the prompt. Splicing the * rendered text keeps the existing order untouched. * * A rendered prompt without any top-level section degenerates to the cover alone. */ export function spliceCover(rendered: string, cover: string): string { const trimmed = cover.replace(/\s+$/, ""); const boundary = rendered.search(FIRST_SECTION_BOUNDARY); if (boundary < 0) { return trimmed; } return trimmed + rendered.slice(boundary); }