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; 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; } }); }