mirror of
https://github.com/obra/superpowers.git
synced 2026-05-09 02:29:05 +08:00
122 lines
4.1 KiB
TypeScript
122 lines
4.1 KiB
TypeScript
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 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;
|
|
}
|