superpowers.js 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. /**
  2. * Superpowers plugin for OpenCode.ai
  3. *
  4. * Injects superpowers bootstrap context via system prompt transform.
  5. * Auto-registers skills directory via config hook (no symlinks needed).
  6. */
  7. import path from 'path';
  8. import fs from 'fs';
  9. import os from 'os';
  10. import { fileURLToPath } from 'url';
  11. const __dirname = path.dirname(fileURLToPath(import.meta.url));
  12. // Simple frontmatter extraction (avoid dependency on skills-core for bootstrap)
  13. const extractAndStripFrontmatter = (content) => {
  14. const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
  15. if (!match) return { frontmatter: {}, content };
  16. const frontmatterStr = match[1];
  17. const body = match[2];
  18. const frontmatter = {};
  19. for (const line of frontmatterStr.split('\n')) {
  20. const colonIdx = line.indexOf(':');
  21. if (colonIdx > 0) {
  22. const key = line.slice(0, colonIdx).trim();
  23. const value = line.slice(colonIdx + 1).trim().replace(/^["']|["']$/g, '');
  24. frontmatter[key] = value;
  25. }
  26. }
  27. return { frontmatter, content: body };
  28. };
  29. // Normalize a path: trim whitespace, expand ~, resolve to absolute
  30. const normalizePath = (p, homeDir) => {
  31. if (!p || typeof p !== 'string') return null;
  32. let normalized = p.trim();
  33. if (!normalized) return null;
  34. if (normalized.startsWith('~/')) {
  35. normalized = path.join(homeDir, normalized.slice(2));
  36. } else if (normalized === '~') {
  37. normalized = homeDir;
  38. }
  39. return path.resolve(normalized);
  40. };
  41. export const SuperpowersPlugin = async ({ client, directory }) => {
  42. const homeDir = os.homedir();
  43. const superpowersSkillsDir = path.resolve(__dirname, '../../skills');
  44. const envConfigDir = normalizePath(process.env.OPENCODE_CONFIG_DIR, homeDir);
  45. const configDir = envConfigDir || path.join(homeDir, '.config/opencode');
  46. // Helper to generate bootstrap content
  47. const getBootstrapContent = () => {
  48. // Try to load using-superpowers skill
  49. const skillPath = path.join(superpowersSkillsDir, 'using-superpowers', 'SKILL.md');
  50. if (!fs.existsSync(skillPath)) return null;
  51. const fullContent = fs.readFileSync(skillPath, 'utf8');
  52. const { content } = extractAndStripFrontmatter(fullContent);
  53. const toolMapping = `**Tool Mapping for OpenCode:**
  54. When skills reference tools you don't have, substitute OpenCode equivalents:
  55. - \`TodoWrite\` → \`todowrite\`
  56. - \`Task\` tool with subagents → Use OpenCode's subagent system (@mention)
  57. - \`Skill\` tool → OpenCode's native \`skill\` tool
  58. - \`Read\`, \`Write\`, \`Edit\`, \`Bash\` → Your native tools
  59. **Skills location:**
  60. Superpowers skills are in \`${configDir}/skills/superpowers/\`
  61. Use OpenCode's native \`skill\` tool to list and load skills.`;
  62. return `<EXTREMELY_IMPORTANT>
  63. You have superpowers.
  64. **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.**
  65. ${content}
  66. ${toolMapping}
  67. </EXTREMELY_IMPORTANT>`;
  68. };
  69. return {
  70. // Inject skills path into live config so OpenCode discovers superpowers skills
  71. // without requiring manual symlinks or config file edits.
  72. // This works because Config.get() returns a cached singleton — modifications
  73. // here are visible when skills are lazily discovered later.
  74. config: async (config) => {
  75. config.skills = config.skills || {};
  76. config.skills.paths = config.skills.paths || [];
  77. if (!config.skills.paths.includes(superpowersSkillsDir)) {
  78. config.skills.paths.push(superpowersSkillsDir);
  79. }
  80. },
  81. // Use system prompt transform to inject bootstrap (fixes #226 agent reset bug)
  82. 'experimental.chat.system.transform': async (_input, output) => {
  83. const bootstrap = getBootstrapContent();
  84. if (bootstrap) {
  85. (output.system ||= []).push(bootstrap);
  86. }
  87. }
  88. };
  89. };