import { readFileSync } from "node:fs"; import { join } from "node:path"; /** File name of the bundle-managed system prompt cover inside the agent directory. */ export const COVER_FILE_NAME = "SYSTEM_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 a given agent directory. */ export function resolveCoverPath(agentDir: string): string { return join(agentDir, COVER_FILE_NAME); } /** * 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); }