mirror of
https://github.com/jnMetaCode/superpowers-zh.git
synced 2026-09-02 22:54:06 +08:00
feat(pi): 新增 Pi (oh-my-pi) harness 支持(关 #44,对齐上游 v6.0.0)
issue #44:Pi 像 opencode 一样开放,skill:// 能直接用,已验证可手动使用, 希望原生支持。上游 obra/superpowers 在 v6.0.0 已用扩展模型原生集成 Pi, 本提交把同样的集成方式落到本 fork。 Pi 走扩展模型,通过 package.json 的 pi 字段声明,直接指向仓库现有 skills/,不复制 skill、无运行时依赖。扩展内容中立——读取 fork 现有的 中文 using-superpowers/SKILL.md 自动注入,故扩展代码逐字节照搬上游。 - .pi/extensions/superpowers.ts:注册 resources_discover / session_start / session_compact / agent_end / context 生命周期钩子,在会话注入 using-superpowers bootstrap + Pi 工具映射(带去重标记、插在 compaction summary 之后) - package.json:加 pi.skills=["./skills"] + pi.extensions + pi-package keyword;.pi/extensions/ 加入 files(npm 发布需含扩展) - skills/using-superpowers/references/pi-tools.md:Pi 工具映射参考 - docs/README.pi.md:中文安装/原理/工具映射/验证指南;README.md 工具列表加链接 - tests/pi/:上游扩展行为测试(适配 fork:name=superpowers-zh)+ 运行包装 验证:bash tests/pi/run-tests.sh 6/6 通过 exit 0(校验 pi 包配置、生命周期 钩子无 pre-compaction 注入、resources_discover 贡献 skills 目录、session_start 注入 You-have-superpowers + Pi-tool-mapping、pi-tools 参考存在);package.json 合法;scripts/audit.sh 静态 0 FAIL;README→docs/README.pi.md 链接可解析。 注:扩展是 TS(仅 import type,运行时无类型依赖),Node 22.6–23.5 需 --experimental-strip-types(run-tests.sh 已带),23.6+ 默认支持。 Pi 内实际 skill 触发需在 Pi 内验证(与本 fork 其它 harness 同样限制)。
This commit is contained in:
121
.pi/extensions/superpowers.ts
Normal file
121
.pi/extensions/superpowers.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user