Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/skillz/index.ts

Raw
import { readFileSync } from "node:fs";
import { basename, dirname, join } from "node:path";
import {
	type ExtensionAPI,
	getAgentDir,
	loadSkillsFromDir,
	parseFrontmatter,
} from "@earendil-works/pi-coding-agent";
import {
	type Candidate,
	inspectCandidates,
	showInspection,
} from "./inspect.ts";
import { matchSkill } from "./match.ts";
import { closeDebug, dbg, span } from "./src/debug.ts";

export default function skillz(pi: ExtensionAPI) {
	let snapshot: Candidate[] | undefined;

	pi.registerCommand("skillz", {
		description: "Inspect conditional skills from the last discovery",
		handler: async (_args, ctx) => {
			const finish = span?.("skillz.inspect", {
				outcome: snapshot === undefined ? "empty" : "snapshot",
			});
			try {
				await showInspection(
					ctx,
					inspectCandidates(snapshot ?? [], pi.getCommands()),
					snapshot !== undefined,
				);
				finish?.();
			} catch (error) {
				finish?.("error");
				throw error;
			}
		},
	});

	pi.on("session_start", () => {
		dbg?.("session.start");
	});
	pi.on("session_shutdown", () => {
		dbg?.("session.shutdown");
		closeDebug();
	});

	pi.on("resources_discover", (event, ctx) => {
		const finish = span?.("skillz.discovery");
		try {
			const root = join(getAgentDir(), "skillz");
			const discovered = loadSkillsFromDir({ dir: root, source: "user" });
			const candidates: Candidate[] = [];
			const diagnosed = new Set(
				discovered.skills.map((skill) => skill.filePath),
			);
			for (const diagnostic of discovered.diagnostics) {
				ctx.ui.notify(
					`skillz: Pi skill validation warning: ${diagnostic.path ?? root}`,
					"warning",
				);
				const file = diagnostic.path ?? root;
				if (!diagnosed.has(file)) {
					diagnosed.add(file);
					candidates.push({
						name:
							basename(file) === "SKILL.md"
								? basename(dirname(file))
								: basename(file, ".md"),
						path: file,
						submitted: false,
						reason: `Pi rejected skill at ${file} (invalid skill file)`,
					});
				}
			}
			const claimed = new Map(
				pi
					.getCommands()
					.filter((command) => command.source === "skill")
					.map((command) => [
						command.name.slice("skill:".length),
						command.sourceInfo.path,
					]),
			);
			const skillPaths: string[] = [];
			const names: string[] = [];
			for (const skill of discovered.skills) {
				const candidate: Candidate = {
					name: skill.name,
					path: skill.filePath,
					submitted: false,
					warning: discovered.diagnostics.some(
						(diagnostic) => diagnostic.path === skill.filePath,
					),
				};
				candidates.push(candidate);
				let frontmatter: Record<string, unknown>;
				try {
					frontmatter = parseFrontmatter(
						readFileSync(skill.filePath, "utf8"),
					).frontmatter;
				} catch {
					candidate.reason = "invalid frontmatter";
					ctx.ui.notify(
						`skillz: invalid frontmatter: ${skill.filePath}`,
						"warning",
					);
					continue;
				}
				const match = matchSkill(frontmatter, event.cwd);
				if (match.error) {
					candidate.reason = match.error;
					ctx.ui.notify(`skillz: ${skill.filePath}: ${match.error}`, "warning");
					continue;
				}
				candidate.checks = match.checks;
				if (!match.matches) continue;
				const winner = claimed.get(skill.name);
				if (winner && winner !== skill.filePath) {
					candidate.reason = `name collision with ${winner}`;
					ctx.ui.notify(
						`skillz: duplicate skill name ${skill.name}: ${skill.filePath}`,
						"warning",
					);
					continue;
				}
				claimed.set(skill.name, skill.filePath);
				candidate.submitted = true;
				skillPaths.push(skill.filePath);
				names.push(skill.name);
			}
			snapshot = candidates;
			if (event.reason !== "reload" && ctx.mode === "tui" && names.length > 0)
				ctx.ui.notify(`skillz: eligible: ${names.join(", ")}`, "info");
			finish?.("finish", {
				candidateCount: candidates.length,
				submittedCount: skillPaths.length,
				excludedCount: candidates.length - skillPaths.length,
			});
			return { skillPaths };
		} catch (error) {
			finish?.("error");
			throw error;
		}
	});
}