| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121 |
- 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 = "<EXTREMELY_IMPORTANT>";
- 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()}
- </EXTREMELY_IMPORTANT>`;
- 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 invoke a skill, 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\`. Use those for the corresponding actions: read a file, create or edit files, run shell commands, search file contents, find files by name, and list directories.
- Pi does not ship a standard 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 \`Task\` calls.
- Pi does not ship a standard 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. Treat older \`TodoWrite\` references as this task-tracking action.`;
- }
- 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;
- }
|