import { readFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; const EXTREMELY_IMPORTANT_MARKER = ""; const BOOTSTRAP_MARKER = "superpowers:using-superpowers bootstrap for pi"; const extensionDir = dirname(fileURLToPath(import.meta.url)); const packageRoot = resolve(extensionDir, ".."); const skillsDir = resolve(packageRoot, "skills"); const bootstrapSkillPath = resolve(skillsDir, "using-superpowers", "SKILL.md"); let cachedBootstrap: string | null | undefined; export default function superpowersPiExtension(pi: ExtensionAPI) { let injectBootstrap = true; pi.on("resources_discover", async () => ({ skillPaths: [skillsDir], })); pi.on("session_start", async () => { injectBootstrap = true; }); pi.on("session_compact", async () => { injectBootstrap = true; }); pi.on("agent_end", async () => { injectBootstrap = false; }); pi.on("context", async (event) => { if (!injectBootstrap) return; if (event.messages.some(messageContainsBootstrap)) return; const bootstrap = getBootstrapContent(); if (!bootstrap) return; const bootstrapMessage = { role: "user" as const, content: [{ type: "text" as const, text: bootstrap }], timestamp: Date.now(), }; const insertAt = firstNonCompactionSummaryIndex(event.messages); return { messages: [ ...event.messages.slice(0, insertAt), bootstrapMessage, ...event.messages.slice(insertAt), ], }; }); } function getBootstrapContent(): string | null { if (cachedBootstrap !== undefined) return cachedBootstrap; try { const skillContent = readFileSync(bootstrapSkillPath, "utf8"); const body = stripFrontmatter(skillContent); cachedBootstrap = `${EXTREMELY_IMPORTANT_MARKER} ${BOOTSTRAP_MARKER} You have superpowers. The using-superpowers skill content is included below and is already loaded for this Pi session. Follow it now. Do not try to load using-superpowers again. ${body} ${piToolMapping()} `; return cachedBootstrap; } catch { cachedBootstrap = null; return null; } } function stripFrontmatter(content: string): string { const match = content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/); return (match ? match[1] : content).trim(); } function piToolMapping(): string { return `## Pi tool mapping Pi has native skills but does not expose Claude Code's \`Skill\` tool. When a Superpowers instruction says to use the \`Skill\` tool, use Pi's native skill system instead: load the relevant \`SKILL.md\` with \`read\` when the skill applies, or let a human invoke \`/skill:name\` explicitly. Pi's built-in coding tools are lowercase: \`read\`, \`write\`, \`edit\`, \`bash\`, plus optional \`grep\`, \`find\`, and \`ls\`. Map Claude-style tool names \`Read\`, \`Write\`, \`Edit\`, and \`Bash\` to those Pi tools. Pi does not ship a standard \`Task\` subagent tool. If a subagent tool such as \`subagent\` from \`pi-subagents\` is available, use it for Superpowers subagent workflows. If no subagent tool is available, do the work in this session or explain the missing capability instead of inventing tool calls. Pi does not ship a standard \`TodoWrite\` task-list tool. If an installed todo/task tool is available, use it. Otherwise track work in plan files or a repo-local \`TODO.md\` when task tracking is needed.`; } function messageContainsBootstrap(message: unknown): boolean { const content = (message as { content?: unknown }).content; if (typeof content === "string") return content.includes(BOOTSTRAP_MARKER); if (!Array.isArray(content)) return false; return content.some((part) => { return ( part && typeof part === "object" && (part as { type?: unknown }).type === "text" && typeof (part as { text?: unknown }).text === "string" && (part as { text: string }).text.includes(BOOTSTRAP_MARKER) ); }); } function firstNonCompactionSummaryIndex(messages: unknown[]): number { let index = 0; while ((messages[index] as { role?: unknown } | undefined)?.role === "compactionSummary") { index += 1; } return index; }