repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/skillz/index.ts
Rawimport { 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;
}
});
}