2025-11-22 12:29:50 -08:00
/**
* Superpowers plugin for OpenCode.ai
*
* Provides custom tools for loading and discovering skills,
2025-11-22 16:13:05 -08:00
* with prompt generation for agent configuration.
2025-11-22 12:29:50 -08:00
*/
2025-11-22 12:53:26 -08:00
import path from 'path' ;
import fs from 'fs' ;
import os from 'os' ;
2025-11-22 13:22:17 -08:00
import { fileURLToPath } from 'url' ;
2025-11-22 21:07:57 -08:00
import { tool } from '@opencode-ai/plugin/tool' ;
2025-11-22 13:23:05 -08:00
import * as skillsCore from '../../lib/skills-core.js' ;
2025-11-22 12:53:26 -08:00
2025-11-22 13:22:17 -08:00
const __dirname = path . dirname ( fileURLToPath ( import . meta . url ));
2025-11-22 12:29:50 -08:00
2025-11-24 00:25:26 +00:00
export const SuperpowersPlugin = async ({ client , directory }) => {
2025-11-22 13:22:17 -08:00
const homeDir = os . homedir ();
2025-11-23 12:10:41 -08:00
const projectSkillsDir = path . join ( directory , '.opencode/skills' );
2025-11-23 23:37:53 +00:00
// Derive superpowers skills dir from plugin location (works for both symlinked and local installs)
const superpowersSkillsDir = path . resolve ( __dirname , '../../skills' );
2025-11-22 13:22:17 -08:00
const personalSkillsDir = path . join ( homeDir , '.config/opencode/skills' );
2025-11-24 12:28:11 -08:00
// Helper to generate bootstrap content
const getBootstrapContent = ( compact = false ) => {
const usingSuperpowersPath = skillsCore . resolveSkillPath ( 'using-superpowers' , superpowersSkillsDir , personalSkillsDir );
if ( ! usingSuperpowersPath ) return null ;
const fullContent = fs . readFileSync ( usingSuperpowersPath . skillFile , 'utf8' );
const content = skillsCore . stripFrontmatter ( fullContent );
const toolMapping = compact
? `**Tool Mapping:** TodoWrite->update_plan, Task->@mention, Skill->use_skill
**Skills naming (priority order):** project: > personal > superpowers:`
: `**Tool Mapping for OpenCode:**
When skills reference tools you don't have, substitute OpenCode equivalents:
- \`TodoWrite\` → \`update_plan\`
- \`Task\` tool with subagents → Use OpenCode's subagent system (@mention)
- \`Skill\` tool → \`use_skill\` custom tool
- \`Read\`, \`Write\`, \`Edit\`, \`Bash\` → Your native tools
**Skills naming (priority order):**
- Project skills: \`project:skill-name\` (in .opencode/skills/)
- Personal skills: \`skill-name\` (in ~/.config/opencode/skills/)
- Superpowers skills: \`superpowers:skill-name\`
- Project skills override personal, which override superpowers when names match` ;
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 use_skill tool to load "using-superpowers" - that would be redundant. Use use_skill only for OTHER skills.**
${ content }
${ toolMapping }
</EXTREMELY_IMPORTANT>` ;
};
// Helper to inject bootstrap via session.prompt
const injectBootstrap = async ( sessionID , compact = false ) => {
const bootstrapContent = getBootstrapContent ( compact );
if ( ! bootstrapContent ) return false ;
try {
await client . session . prompt ({
path : { id : sessionID },
body : {
noReply : true ,
2025-11-26 11:38:35 -06:00
parts : [{ type : "text" , text : bootstrapContent , synthetic : true }]
2025-11-24 12:28:11 -08:00
}
});
return true ;
} catch ( err ) {
return false ;
}
};
2025-11-22 12:29:50 -08:00
return {
2025-11-22 21:04:00 -08:00
tool : {
2025-11-22 21:07:57 -08:00
use_skill : tool ({
2025-11-22 12:30:20 -08:00
description : 'Load and read a specific skill to guide your work. Skills contain proven workflows, mandatory processes, and expert techniques.' ,
2025-11-22 21:07:57 -08:00
args : {
2025-11-23 12:10:41 -08:00
skill_name : tool . schema . string (). describe ( 'Name of the skill to load (e.g., "superpowers:brainstorming", "my-custom-skill", or "project:my-skill")' )
2025-11-22 21:07:57 -08:00
},
execute : async ( args , context ) => {
const { skill_name } = args ;
2025-11-23 12:10:41 -08:00
// Resolve with priority: project > personal > superpowers
// Check for project: prefix first
const forceProject = skill_name . startsWith ( 'project:' );
const actualSkillName = forceProject ? skill_name . replace ( /^project:/ , '' ) : skill_name ;
let resolved = null ;
// Try project skills first (if project: prefix or no prefix)
if ( forceProject || ! skill_name . startsWith ( 'superpowers:' )) {
const projectPath = path . join ( projectSkillsDir , actualSkillName );
const projectSkillFile = path . join ( projectPath , 'SKILL.md' );
if ( fs . existsSync ( projectSkillFile )) {
resolved = {
skillFile : projectSkillFile ,
sourceType : 'project' ,
skillPath : actualSkillName
};
}
}
// Fall back to personal/superpowers resolution
if ( ! resolved && ! forceProject ) {
resolved = skillsCore . resolveSkillPath ( skill_name , superpowersSkillsDir , personalSkillsDir );
}
2025-11-22 12:30:20 -08:00
if ( ! resolved ) {
return `Error: Skill " ${ skill_name } " not found.\n\nRun find_skills to see available skills.` ;
}
const fullContent = fs . readFileSync ( resolved . skillFile , 'utf8' );
const { name , description } = skillsCore . extractFrontmatter ( resolved . skillFile );
2025-11-22 12:41:58 -08:00
const content = skillsCore . stripFrontmatter ( fullContent );
2025-11-22 12:30:20 -08:00
const skillDirectory = path . dirname ( resolved . skillFile );
2025-11-23 12:10:41 -08:00
const skillHeader = `# ${ name || skill_name }
2025-11-22 12:30:20 -08:00
# ${ description || '' }
# Supporting tools and docs are in ${ skillDirectory }
2025-11-23 12:10:41 -08:00
# ============================================` ;
// Insert as user message with noReply for persistence across compaction
try {
await client . session . prompt ({
path : { id : context . sessionID },
body : {
noReply : true ,
parts : [
2025-11-26 11:38:35 -06:00
{ type : "text" , text : `Loading skill: ${ name || skill_name } ` , synthetic : true },
{ type : "text" , text : ` ${ skillHeader } \n\n ${ content } ` , synthetic : true }
2025-11-23 12:10:41 -08:00
]
}
});
} catch ( err ) {
// Fallback: return content directly if message insertion fails
return ` ${ skillHeader } \n\n ${ content } ` ;
}
2025-11-22 12:30:20 -08:00
2025-11-23 12:10:41 -08:00
return `Launching skill: ${ name || skill_name } ` ;
2025-11-22 12:30:20 -08:00
}
2025-11-22 21:07:57 -08:00
}),
find_skills : tool ({
2025-11-23 12:10:41 -08:00
description : 'List all available skills in the project, personal, and superpowers skill libraries.' ,
2025-11-22 21:07:57 -08:00
args : {},
execute : async ( args , context ) => {
2025-11-23 12:10:41 -08:00
const projectSkills = skillsCore . findSkillsInDir ( projectSkillsDir , 'project' , 3 );
2025-11-22 12:30:48 -08:00
const personalSkills = skillsCore . findSkillsInDir ( personalSkillsDir , 'personal' , 3 );
2025-11-23 12:10:41 -08:00
const superpowersSkills = skillsCore . findSkillsInDir ( superpowersSkillsDir , 'superpowers' , 3 );
// Priority: project > personal > superpowers
const allSkills = [... projectSkills , ... personalSkills , ... superpowersSkills ];
2025-11-22 12:30:48 -08:00
if ( allSkills . length === 0 ) {
2025-11-23 12:10:41 -08:00
return 'No skills found. Install superpowers skills to ~/.config/opencode/superpowers/skills/ or add project skills to .opencode/skills/' ;
2025-11-22 12:30:48 -08:00
}
let output = 'Available skills:\n\n' ;
for ( const skill of allSkills ) {
2025-11-23 12:10:41 -08:00
let namespace ;
switch ( skill . sourceType ) {
case 'project' :
namespace = 'project:' ;
break ;
case 'personal' :
namespace = '' ;
break ;
default :
namespace = 'superpowers:' ;
}
2025-11-22 12:30:48 -08:00
const skillName = skill . name || path . basename ( skill . path );
output += ` ${ namespace }${ skillName } \n` ;
if ( skill . description ) {
output += ` ${ skill . description } \n` ;
}
output += ` Directory: ${ skill . path } \n\n` ;
}
return output ;
}
2025-11-22 21:07:57 -08:00
})
2025-11-23 10:51:26 -08:00
},
2025-11-24 12:28:11 -08:00
event : async ({ event }) => {
// Extract sessionID from various event structures
const getSessionID = () => {
return event . properties ? . info ? . id ||
event . properties ? . sessionID ||
event . session ? . id ;
};
// Inject bootstrap at session creation (before first user message)
if ( event . type === 'session.created' ) {
const sessionID = getSessionID ();
if ( sessionID ) {
await injectBootstrap ( sessionID , false );
2025-11-23 10:51:26 -08:00
}
}
2025-11-23 12:10:41 -08:00
2025-11-24 12:28:11 -08:00
// Re-inject bootstrap after context compaction (compact version to save tokens)
if ( event . type === 'session.compacted' ) {
const sessionID = getSessionID ();
if ( sessionID ) {
await injectBootstrap ( sessionID , true );
2025-11-23 12:10:41 -08:00
}
}
2025-11-22 21:04:00 -08:00
}
2025-11-22 12:29:50 -08:00
};
};