2025-11-22 12:29:50 -08:00
/**
* Superpowers plugin for OpenCode.ai
*
2026-01-22 13:29:30 -08:00
* Injects superpowers bootstrap context via system prompt transform.
2026-03-15 21:29:25 +00:00
* Auto-registers skills directory via config hook (no symlinks needed).
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 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
2026-01-22 13:29:30 -08:00
// 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 };
};
2026-01-19 23:54:50 +02:00
// Normalize a path: trim whitespace, expand ~, resolve to absolute
const normalizePath = ( p , homeDir ) => {
if ( ! p || typeof p !== 'string' ) return null ;
let normalized = p . trim ();
if ( ! normalized ) return null ;
if ( normalized . startsWith ( '~/' )) {
normalized = path . join ( homeDir , normalized . slice ( 2 ));
} else if ( normalized === '~' ) {
normalized = homeDir ;
}
return path . resolve ( normalized );
};
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 23:37:53 +00:00
const superpowersSkillsDir = path . resolve ( __dirname , '../../skills' );
2026-01-19 23:54:50 +02:00
const envConfigDir = normalizePath ( process . env . OPENCODE_CONFIG_DIR , homeDir );
const configDir = envConfigDir || path . join ( homeDir , '.config/opencode' );
2025-11-22 13:22:17 -08:00
2025-11-24 12:28:11 -08:00
// Helper to generate bootstrap content
2026-01-22 13:29:30 -08:00
const getBootstrapContent = () => {
// Try to load using-superpowers skill
const skillPath = path . join ( superpowersSkillsDir , 'using-superpowers' , 'SKILL.md' );
if ( ! fs . existsSync ( skillPath )) return null ;
2025-11-24 12:28:11 -08:00
2026-01-22 13:29:30 -08:00
const fullContent = fs . readFileSync ( skillPath , 'utf8' );
const { content } = extractAndStripFrontmatter ( fullContent );
2025-11-24 12:28:11 -08:00
2026-01-22 13:29:30 -08:00
const toolMapping = `**Tool Mapping for OpenCode:**
2025-11-24 12:28:11 -08:00
When skills reference tools you don't have, substitute OpenCode equivalents:
2026-03-09 20:25:13 -07:00
- \`TodoWrite\` → \`todowrite\`
2025-11-24 12:28:11 -08:00
- \`Task\` tool with subagents → Use OpenCode's subagent system (@mention)
2026-01-22 13:29:30 -08:00
- \`Skill\` tool → OpenCode's native \`skill\` tool
2025-11-24 12:28:11 -08:00
- \`Read\`, \`Write\`, \`Edit\`, \`Bash\` → Your native tools
2026-01-22 13:29:30 -08:00
Use OpenCode's native \`skill\` tool to list and load skills.` ;
2025-11-24 12:28:11 -08:00
return `<EXTREMELY_IMPORTANT>
You have superpowers.
2026-01-22 13:29:30 -08:00
**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.**
2025-11-24 12:28:11 -08:00
${ content }
${ toolMapping }
</EXTREMELY_IMPORTANT>` ;
};
2025-11-22 12:29:50 -08:00
return {
2026-03-15 21:29:25 +00:00
// 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 );
}
},
2026-01-22 13:29:30 -08:00
// Use system prompt transform to inject bootstrap (fixes #226 agent reset bug)
'experimental.chat.system.transform' : async ( _input , output ) => {
const bootstrap = getBootstrapContent ();
if ( bootstrap ) {
( output . system ||= []). push ( bootstrap );
2025-11-23 12:10:41 -08:00
}
2025-11-22 21:04:00 -08:00
}
2025-11-22 12:29:50 -08:00
};
};