superpowers.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. import { readFileSync } from "node:fs";
  2. import { dirname, resolve } from "node:path";
  3. import { fileURLToPath } from "node:url";
  4. import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
  5. const EXTREMELY_IMPORTANT_MARKER = "<EXTREMELY_IMPORTANT>";
  6. const BOOTSTRAP_MARKER = "superpowers:using-superpowers bootstrap for pi";
  7. const extensionDir = dirname(fileURLToPath(import.meta.url));
  8. const packageRoot = resolve(extensionDir, "../..");
  9. const skillsDir = resolve(packageRoot, "skills");
  10. const bootstrapSkillPath = resolve(skillsDir, "using-superpowers", "SKILL.md");
  11. let cachedBootstrap: string | null | undefined;
  12. export default function superpowersPiExtension(pi: ExtensionAPI) {
  13. let injectBootstrap = true;
  14. pi.on("resources_discover", async () => ({
  15. skillPaths: [skillsDir],
  16. }));
  17. pi.on("session_start", async () => {
  18. injectBootstrap = true;
  19. });
  20. pi.on("session_compact", async () => {
  21. injectBootstrap = true;
  22. });
  23. pi.on("agent_end", async () => {
  24. injectBootstrap = false;
  25. });
  26. pi.on("context", async (event) => {
  27. if (!injectBootstrap) return;
  28. if (event.messages.some(messageContainsBootstrap)) return;
  29. const bootstrap = getBootstrapContent();
  30. if (!bootstrap) return;
  31. const bootstrapMessage = {
  32. role: "user" as const,
  33. content: [{ type: "text" as const, text: bootstrap }],
  34. timestamp: Date.now(),
  35. };
  36. const insertAt = firstNonCompactionSummaryIndex(event.messages);
  37. return {
  38. messages: [
  39. ...event.messages.slice(0, insertAt),
  40. bootstrapMessage,
  41. ...event.messages.slice(insertAt),
  42. ],
  43. };
  44. });
  45. }
  46. function getBootstrapContent(): string | null {
  47. if (cachedBootstrap !== undefined) return cachedBootstrap;
  48. try {
  49. const skillContent = readFileSync(bootstrapSkillPath, "utf8");
  50. const body = stripFrontmatter(skillContent);
  51. cachedBootstrap = `${EXTREMELY_IMPORTANT_MARKER}
  52. ${BOOTSTRAP_MARKER}
  53. You have superpowers.
  54. 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.
  55. ${body}
  56. ${piToolMapping()}
  57. </EXTREMELY_IMPORTANT>`;
  58. return cachedBootstrap;
  59. } catch {
  60. cachedBootstrap = null;
  61. return null;
  62. }
  63. }
  64. function stripFrontmatter(content: string): string {
  65. const match = content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
  66. return (match ? match[1] : content).trim();
  67. }
  68. function piToolMapping(): string {
  69. return `## Pi tool mapping
  70. 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.
  71. 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.
  72. 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.
  73. 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.`;
  74. }
  75. function messageContainsBootstrap(message: unknown): boolean {
  76. const content = (message as { content?: unknown }).content;
  77. if (typeof content === "string") return content.includes(BOOTSTRAP_MARKER);
  78. if (!Array.isArray(content)) return false;
  79. return content.some((part) => {
  80. return (
  81. part &&
  82. typeof part === "object" &&
  83. (part as { type?: unknown }).type === "text" &&
  84. typeof (part as { text?: unknown }).text === "string" &&
  85. (part as { text: string }).text.includes(BOOTSTRAP_MARKER)
  86. );
  87. });
  88. }
  89. function firstNonCompactionSummaryIndex(messages: unknown[]): number {
  90. let index = 0;
  91. while ((messages[index] as { role?: unknown } | undefined)?.role === "compactionSummary") {
  92. index += 1;
  93. }
  94. return index;
  95. }