superpowers.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. /**
  2. * Superpowers plugin for OpenCode.ai
  3. *
  4. * Provides custom tools for loading and discovering skills,
  5. * with prompt generation for agent configuration.
  6. */
  7. import path from 'path';
  8. import fs from 'fs';
  9. import os from 'os';
  10. import { fileURLToPath } from 'url';
  11. import { tool } from '@opencode-ai/plugin/tool';
  12. import * as skillsCore from '../../lib/skills-core.js';
  13. const __dirname = path.dirname(fileURLToPath(import.meta.url));
  14. export const SuperpowersPlugin = async ({ project, client, $, directory, worktree }) => {
  15. const homeDir = os.homedir();
  16. const projectSkillsDir = path.join(directory, '.opencode/skills');
  17. const superpowersSkillsDir = path.join(homeDir, '.config/opencode/superpowers/skills');
  18. const personalSkillsDir = path.join(homeDir, '.config/opencode/skills');
  19. const promptsDir = path.join(homeDir, '.config/opencode/prompts');
  20. const promptFile = path.join(promptsDir, 'superpowers.txt');
  21. return {
  22. tool: {
  23. use_skill: tool({
  24. description: 'Load and read a specific skill to guide your work. Skills contain proven workflows, mandatory processes, and expert techniques.',
  25. args: {
  26. skill_name: tool.schema.string().describe('Name of the skill to load (e.g., "superpowers:brainstorming", "my-custom-skill", or "project:my-skill")')
  27. },
  28. execute: async (args, context) => {
  29. const { skill_name } = args;
  30. // Resolve with priority: project > personal > superpowers
  31. // Check for project: prefix first
  32. const forceProject = skill_name.startsWith('project:');
  33. const actualSkillName = forceProject ? skill_name.replace(/^project:/, '') : skill_name;
  34. let resolved = null;
  35. // Try project skills first (if project: prefix or no prefix)
  36. if (forceProject || !skill_name.startsWith('superpowers:')) {
  37. const projectPath = path.join(projectSkillsDir, actualSkillName);
  38. const projectSkillFile = path.join(projectPath, 'SKILL.md');
  39. if (fs.existsSync(projectSkillFile)) {
  40. resolved = {
  41. skillFile: projectSkillFile,
  42. sourceType: 'project',
  43. skillPath: actualSkillName
  44. };
  45. }
  46. }
  47. // Fall back to personal/superpowers resolution
  48. if (!resolved && !forceProject) {
  49. resolved = skillsCore.resolveSkillPath(skill_name, superpowersSkillsDir, personalSkillsDir);
  50. }
  51. if (!resolved) {
  52. return `Error: Skill "${skill_name}" not found.\n\nRun find_skills to see available skills.`;
  53. }
  54. const fullContent = fs.readFileSync(resolved.skillFile, 'utf8');
  55. const { name, description } = skillsCore.extractFrontmatter(resolved.skillFile);
  56. const content = skillsCore.stripFrontmatter(fullContent);
  57. const skillDirectory = path.dirname(resolved.skillFile);
  58. const skillHeader = `# ${name || skill_name}
  59. # ${description || ''}
  60. # Supporting tools and docs are in ${skillDirectory}
  61. # ============================================`;
  62. // Insert as user message with noReply for persistence across compaction
  63. try {
  64. await client.session.prompt({
  65. path: { id: context.sessionID },
  66. body: {
  67. noReply: true,
  68. parts: [
  69. { type: "text", text: `Loading skill: ${name || skill_name}` },
  70. { type: "text", text: `${skillHeader}\n\n${content}` }
  71. ]
  72. }
  73. });
  74. } catch (err) {
  75. // Fallback: return content directly if message insertion fails
  76. return `${skillHeader}\n\n${content}`;
  77. }
  78. return `Launching skill: ${name || skill_name}`;
  79. }
  80. }),
  81. find_skills: tool({
  82. description: 'List all available skills in the project, personal, and superpowers skill libraries.',
  83. args: {},
  84. execute: async (args, context) => {
  85. const projectSkills = skillsCore.findSkillsInDir(projectSkillsDir, 'project', 3);
  86. const personalSkills = skillsCore.findSkillsInDir(personalSkillsDir, 'personal', 3);
  87. const superpowersSkills = skillsCore.findSkillsInDir(superpowersSkillsDir, 'superpowers', 3);
  88. // Priority: project > personal > superpowers
  89. const allSkills = [...projectSkills, ...personalSkills, ...superpowersSkills];
  90. if (allSkills.length === 0) {
  91. return 'No skills found. Install superpowers skills to ~/.config/opencode/superpowers/skills/ or add project skills to .opencode/skills/';
  92. }
  93. let output = 'Available skills:\n\n';
  94. for (const skill of allSkills) {
  95. let namespace;
  96. switch (skill.sourceType) {
  97. case 'project':
  98. namespace = 'project:';
  99. break;
  100. case 'personal':
  101. namespace = '';
  102. break;
  103. default:
  104. namespace = 'superpowers:';
  105. }
  106. const skillName = skill.name || path.basename(skill.path);
  107. output += `${namespace}${skillName}\n`;
  108. if (skill.description) {
  109. output += ` ${skill.description}\n`;
  110. }
  111. output += ` Directory: ${skill.path}\n\n`;
  112. }
  113. return output;
  114. }
  115. })
  116. },
  117. "chat.message": async (input, output) => {
  118. // Only inject on first message of session (or every message if needed)
  119. if (!output.message.system || output.message.system.length === 0) {
  120. const usingSuperpowersPath = skillsCore.resolveSkillPath('using-superpowers', superpowersSkillsDir, personalSkillsDir);
  121. if (usingSuperpowersPath) {
  122. const fullContent = fs.readFileSync(usingSuperpowersPath.skillFile, 'utf8');
  123. const usingSuperpowersContent = skillsCore.stripFrontmatter(fullContent);
  124. const toolMapping = `**Tool Mapping for OpenCode:**
  125. When skills reference tools you don't have, substitute OpenCode equivalents:
  126. - \`TodoWrite\` → \`update_plan\`
  127. - \`Task\` tool with subagents → Use OpenCode's subagent system (@mention)
  128. - \`Skill\` tool → \`use_skill\` custom tool
  129. - \`Read\`, \`Write\`, \`Edit\`, \`Bash\` → Your native tools
  130. **Skills naming (priority order):**
  131. - Project skills: \`project:skill-name\` (in .opencode/skills/)
  132. - Personal skills: \`skill-name\` (in ~/.config/opencode/skills/)
  133. - Superpowers skills: \`superpowers:skill-name\`
  134. - Project skills override personal, which override superpowers when names match`;
  135. output.message.system = `<EXTREMELY_IMPORTANT>
  136. You have superpowers.
  137. ${usingSuperpowersContent}
  138. ${toolMapping}
  139. </EXTREMELY_IMPORTANT>`;
  140. }
  141. }
  142. },
  143. event: async ({ event }) => {
  144. // Re-inject bootstrap after context compaction to maintain superpowers
  145. if (event.type === 'session.compacted') {
  146. const usingSuperpowersPath = skillsCore.resolveSkillPath('using-superpowers', superpowersSkillsDir, personalSkillsDir);
  147. if (usingSuperpowersPath) {
  148. const fullContent = fs.readFileSync(usingSuperpowersPath.skillFile, 'utf8');
  149. const content = skillsCore.stripFrontmatter(fullContent);
  150. const toolMapping = `**Tool Mapping:** TodoWrite->update_plan, Task->@mention, Skill->use_skill
  151. **Skills naming (priority order):** project: > personal > superpowers:`;
  152. try {
  153. await client.session.prompt({
  154. path: { id: event.properties.sessionID },
  155. body: {
  156. noReply: true,
  157. parts: [{
  158. type: "text",
  159. text: `<EXTREMELY_IMPORTANT>
  160. You have superpowers.
  161. ${content}
  162. ${toolMapping}
  163. </EXTREMELY_IMPORTANT>`
  164. }]
  165. }
  166. });
  167. } catch (err) {
  168. // Silent failure - bootstrap will be missing but session continues
  169. console.error('Failed to re-inject superpowers after compaction:', err.message);
  170. }
  171. }
  172. }
  173. }
  174. };
  175. };