superpowers.js 3.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. /**
  2. * Superpowers plugin for OpenCode.ai
  3. *
  4. * Injects superpowers bootstrap context via system prompt transform.
  5. * Skills are discovered via OpenCode's native skill tool from symlinked directory.
  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\` → \`update_plan\`
  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. // Use system prompt transform to inject bootstrap (fixes #226 agent reset bug)
  71. 'experimental.chat.system.transform': async (_input, output) => {
  72. const bootstrap = getBootstrapContent();
  73. if (bootstrap) {
  74. (output.system ||= []).push(bootstrap);
  75. }
  76. }
  77. };
  78. };