Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/prompt-autopsy/index.ts

Raw
import { spawnSync } from "node:child_process";
import { mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type {
	ExtensionAPI,
	ExtensionContext,
} from "@earendil-works/pi-coding-agent";
import { Key } from "@earendil-works/pi-tui";
import { closeDebug, dbg, span } from "./src/debug.ts";

const OUTPUT_PREFIX = "pi-prompt-autopsy-";
const OUTPUT_FILENAME = "system-prompt.md";
const LABEL = {
	preamble: "[PREAMBLE]",
	core: "[CORE]",
	tools: "[TOOLS]",
	rules: "[RULES]",
	generalGuidelines: "[GENERAL GUIDELINES]",
	toolGuidelines: "[TOOL GUIDELINES]",
	docs: "[DOCS]",
	append: "[APPEND]",
	context: "[CTX]",
	skills: "[SKILLS]",
	cwd: "[CWD]",
} as const;

const SECTION_LABEL: Readonly<Record<string, string>> = {
	tools: LABEL.tools,
	rules: LABEL.rules,
	general_guidelines: LABEL.generalGuidelines,
	tool_guidelines: LABEL.toolGuidelines,
	docs: LABEL.docs,
	addendum: LABEL.append,
	project_context: LABEL.context,
	skills: LABEL.skills,
	cwd: LABEL.cwd,
};
const SECTION_START = /(?:^|\n)<([a-z][a-z0-9_-]*)>\n/g;

type Range = { start: number; end: number; label: string };
type CapturedPrompt = { cwd: string; prompt: string };

export function groupSystemPrompt(prompt: string): string {
	const ranges = findSectionRanges(prompt);
	let pos = 0;
	const out: string[] = [];
	for (const [index, range] of ranges.entries()) {
		appendSection(
			out,
			index === 0 ? LABEL.preamble : LABEL.core,
			prompt.slice(pos, range.start),
		);
		appendSection(out, range.label, prompt.slice(range.start, range.end));
		pos = range.end;
	}
	appendSection(out, LABEL.core, prompt.slice(pos));
	return out.join("\n\n");
}

function findSectionRanges(prompt: string): Range[] {
	const ranges: Range[] = [];
	const pattern = new RegExp(SECTION_START.source, "g");
	let consumed = 0;
	for (let match = pattern.exec(prompt); match; match = pattern.exec(prompt)) {
		const start = match.index + (match[0].startsWith("\n") ? 1 : 0);
		if (start < consumed) continue;
		const name = match[1];
		const end = findSectionEnd(prompt, name, pattern.lastIndex);
		if (end === undefined) continue;
		ranges.push({
			start,
			end,
			label: SECTION_LABEL[name] ?? `[SECTION ${name}]`,
		});
		consumed = end;
		pattern.lastIndex = end;
	}
	return ranges;
}

function findSectionEnd(
	prompt: string,
	name: string,
	contentStart: number,
): number | undefined {
	const opening = `<${name}>\n`;
	const closing = `\n</${name}>`;
	let depth = 1;
	let cursor = contentStart;

	while (depth > 0) {
		const nextOpening = findLineOpening(prompt, opening, cursor);
		const closeStart = prompt.indexOf(closing, cursor);
		if (closeStart < 0) return undefined;

		if (nextOpening >= 0 && nextOpening < closeStart) {
			depth++;
			cursor = nextOpening + opening.length;
			continue;
		}

		depth--;
		cursor = closeStart + closing.length;
		if (depth === 0) return cursor;
	}

	return undefined;
}

function findLineOpening(
	prompt: string,
	opening: string,
	from: number,
): number {
	for (
		let index = prompt.indexOf(opening, from);
		index >= 0;
		index = prompt.indexOf(opening, index + opening.length)
	) {
		if (index === 0 || prompt[index - 1] === "\n") return index;
	}
	return -1;
}

function appendSection(out: string[], label: string, text: string): void {
	const body = text.trim();
	if (body) out.push(`${label}\n${body}`);
}

function writePromptFile(prompt: string): string {
	const directory = mkdtempSync(join(tmpdir(), OUTPUT_PREFIX));
	const outputPath = join(directory, OUTPUT_FILENAME);
	writeFileSync(outputPath, prompt, {
		encoding: "utf8",
		mode: 0o600,
		flag: "wx",
	});
	return outputPath;
}

function openEditor(path: string): boolean {
	const editor = process.env.VISUAL || process.env.EDITOR || "vi";
	const result = spawnSync(`${editor} ${JSON.stringify(path)}`, {
		shell: true,
		stdio: "inherit",
	});
	return !result.error && result.status === 0;
}

export default function (pi: ExtensionAPI) {
	let last: CapturedPrompt | undefined;

	pi.on("session_start", () => {
		dbg?.("session.start");
		last = undefined;
	});
	pi.on("session_shutdown", () => {
		dbg?.("session.shutdown");
		last = undefined;
		closeDebug();
	});
	pi.on("before_agent_start", (event, ctx) => {
		last = { cwd: ctx.cwd, prompt: event.systemPrompt };
	});
	pi.on("agent_start", (_event, ctx) => {
		// Overwrite prompt with final version (post all before_agent_start modifications).
		if (last?.cwd === ctx.cwd) last.prompt = ctx.getSystemPrompt();
	});

	const run = async (ctx: ExtensionContext) => {
		const captured = last?.cwd === ctx.cwd ? last : undefined;
		if (!captured) {
			dbg?.("prompt.skip", { reason: "not_captured" });
			ctx.ui.notify(
				"No provider-effective prompt captured since session start or reload. Send a prompt, then retry.",
				"warning",
			);
			return;
		}
		const finish = span?.("prompt.open");
		try {
			const outputPath = writePromptFile(groupSystemPrompt(captured.prompt));
			const ok = openEditor(outputPath);
			ctx.ui.notify(
				ok ? `wrote ${outputPath}` : `wrote ${outputPath}; editor failed`,
				ok ? "info" : "warning",
			);
			finish?.("finish", { outcome: ok ? "opened" : "editor_failed" });
		} catch (error) {
			finish?.("error", { outcome: "failed" });
			throw error;
		}
	};

	pi.registerCommand("prompt-autopsy", {
		description: "Autopsy current system prompt in VISUAL",
		handler: async (_args, ctx) => run(ctx),
	});
	pi.registerShortcut(Key.ctrlAlt("a"), {
		description: "Autopsy current system prompt in VISUAL",
		handler: run,
	});
}