/** Parse Pi system prompt into structured components for Cursor's RequestContext.rules. */
export interface PiContextFile {
path: string;
content: string;
}
export interface PiSkillRef {
name: string;
description: string;
location: string;
}
export interface ParsedPiContext {
contextFiles: PiContextFile[];
skills: PiSkillRef[];
cleanedPrompt: string;
}
// Stable structural markers only — no description text that may change across Pi versions.
const CONTEXT_HEADING = "# Project Context";
const SKILLS_OPEN = "";
const SKILLS_CLOSE = "";
const SKILL_RE =
/\s*([\s\S]*?)<\/name>\s*([\s\S]*?)<\/description>\s*([\s\S]*?)<\/location>\s*<\/skill>/g;
function unescapeXml(s: string): string {
return s
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll(""", '"')
.replaceAll("'", "'");
}
function extractContextFiles(prompt: string): PiContextFile[] {
const start = prompt.indexOf(CONTEXT_HEADING);
if (start === -1) return [];
let end = prompt.length;
for (const marker of [SKILLS_OPEN, "\nCurrent date: "]) {
const idx = prompt.indexOf(marker, start);
if (idx !== -1 && idx < end) end = idx;
}
return prompt
.slice(start, end)
.split(/^(?=## \/)/m)
.slice(1)
.flatMap((block) => {
const nl = block.indexOf("\n");
if (nl === -1) return [];
const path = block.slice(3, nl).trim();
const content = block.slice(nl + 1).trim();
return path && content ? [{ path, content }] : [];
});
}
function extractSkills(prompt: string): PiSkillRef[] {
const openIdx = prompt.indexOf(SKILLS_OPEN);
if (openIdx === -1) return [];
const closeIdx = prompt.indexOf(SKILLS_CLOSE, openIdx);
if (closeIdx === -1) return [];
const xml = prompt.slice(openIdx, closeIdx + SKILLS_CLOSE.length);
const skills: PiSkillRef[] = [];
for (const m of xml.matchAll(SKILL_RE)) {
if (!m[1] || !m[2] || !m[3]) continue;
const name = unescapeXml(m[1].trim());
const description = unescapeXml(m[2].trim());
const location = unescapeXml(m[3].trim());
if (name && description && location) {
skills.push({ name, description, location });
}
}
return skills;
}
const PRESERVED_PATTERNS = [
/^Pi documentation[^\n]*(?:\n- [^\n]*)*/m,
/^Current date: .+$/m,
/^Current working directory: .+$/m,
];
function buildCleanedPrompt(original: string, hasExtracted: boolean): string {
const lines = PRESERVED_PATTERNS.map((re) => original.match(re)?.[0]).filter(
(s): s is string => s !== null && s !== undefined,
);
return lines.length === 0 && !hasExtracted ? original : lines.join("\n");
}
export function parsePiSystemPrompt(systemPrompt: string): ParsedPiContext {
if (!systemPrompt) {
return { contextFiles: [], skills: [], cleanedPrompt: "" };
}
try {
const contextFiles = extractContextFiles(systemPrompt);
const skills = extractSkills(systemPrompt);
const hasExtracted = contextFiles.length > 0 || skills.length > 0;
return {
contextFiles,
skills,
cleanedPrompt: buildCleanedPrompt(systemPrompt, hasExtracted),
};
} catch {
return { contextFiles: [], skills: [], cleanedPrompt: systemPrompt };
}
}