Files
superpowers-zh/.opencode/plugins/superpowers.js
jiangnan fc9a2a40ca 同步上游 v5.0.7:Copilot CLI 支持、OpenCode bootstrap 改进、版本统一
上游同步:
- hooks/session-start: 新增 Copilot CLI 平台检测(COPILOT_CLI 环境变量)
- .opencode/plugins/superpowers.js: bootstrap 从 system prompt 改为 user message 注入,避免 token 膨胀和 Qwen 兼容问题
- skills/using-superpowers: 新增 Copilot CLI 使用说明和工具映射引用
- 新增 references/copilot-tools.md(中文化)

本地优化:
- 清理 OpenCode 插件死代码(normalizePath/os/configDir)
- 统一版本号为 1.1.6(修复插件 JSON 与 package.json 不一致)
- .gitignore 排除安装器生成的 skills 副本
- package.json files 数组精简,避免打包重复的 skills 副本
- 安装器新增 copilot/copilot-cli 别名
- 工具数量更新为 15 款
2026-04-13 19:55:20 +08:00

96 lines
3.6 KiB
JavaScript

/**
* Superpowers plugin for OpenCode.ai
*
* Injects superpowers bootstrap context via user message transform.
* Auto-registers skills directory via config hook (no symlinks needed).
*/
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Simple frontmatter extraction (avoid dependency on skills-core for bootstrap)
const extractAndStripFrontmatter = (content) => {
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
if (!match) return { frontmatter: {}, content };
const frontmatterStr = match[1];
const body = match[2];
const frontmatter = {};
for (const line of frontmatterStr.split('\n')) {
const colonIdx = line.indexOf(':');
if (colonIdx > 0) {
const key = line.slice(0, colonIdx).trim();
const value = line.slice(colonIdx + 1).trim().replace(/^["']|["']$/g, '');
frontmatter[key] = value;
}
}
return { frontmatter, content: body };
};
export const SuperpowersPlugin = async ({ client, directory }) => {
const superpowersSkillsDir = path.resolve(__dirname, '../../skills');
// Helper to generate bootstrap content
const getBootstrapContent = () => {
// Try to load using-superpowers skill
const skillPath = path.join(superpowersSkillsDir, 'using-superpowers', 'SKILL.md');
if (!fs.existsSync(skillPath)) return null;
const fullContent = fs.readFileSync(skillPath, 'utf8');
const { content } = extractAndStripFrontmatter(fullContent);
const toolMapping = `**Tool Mapping for OpenCode:**
When skills reference tools you don't have, substitute OpenCode equivalents:
- \`TodoWrite\`\`todowrite\`
- \`Task\` tool with subagents → Use OpenCode's subagent system (@mention)
- \`Skill\` tool → OpenCode's native \`skill\` tool
- \`Read\`, \`Write\`, \`Edit\`, \`Bash\` → Your native tools
Use OpenCode's native \`skill\` tool to list and load skills.`;
return `<EXTREMELY_IMPORTANT>
You have superpowers.
**IMPORTANT: The using-superpowers skill content is included below. It is ALREADY LOADED - you are currently following it. Do NOT use the skill tool to load "using-superpowers" again - that would be redundant.**
${content}
${toolMapping}
</EXTREMELY_IMPORTANT>`;
};
return {
// Inject skills path into live config so OpenCode discovers superpowers skills
// without requiring manual symlinks or config file edits.
// This works because Config.get() returns a cached singleton — modifications
// here are visible when skills are lazily discovered later.
config: async (config) => {
config.skills = config.skills || {};
config.skills.paths = config.skills.paths || [];
if (!config.skills.paths.includes(superpowersSkillsDir)) {
config.skills.paths.push(superpowersSkillsDir);
}
},
// Inject bootstrap into the first user message of each session.
// Using a user message instead of a system message avoids:
// 1. Token bloat from system messages repeated every turn (#750)
// 2. Multiple system messages breaking Qwen and other models (#894)
'experimental.chat.messages.transform': async (_input, output) => {
const bootstrap = getBootstrapContent();
if (!bootstrap || !output.messages.length) return;
const firstUser = output.messages.find(m => m.info.role === 'user');
if (!firstUser || !firstUser.parts.length) return;
// Only inject once
if (firstUser.parts.some(p => p.type === 'text' && p.text.includes('EXTREMELY_IMPORTANT'))) return;
const ref = firstUser.parts[0];
firstUser.parts.unshift({ ...ref, type: 'text', text: bootstrap });
}
};
};