Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 23 additions & 10 deletions .opencode/INSTALL.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
# Installing Superpowers for OpenCode

> **OpenCode v2 only.** This plugin uses the OpenCode v2 plugin API. OpenCode
> v1 plugins do not run under v2 — use an older Superpowers release for
> OpenCode v1.

## Prerequisites

- [OpenCode.ai](https://opencode.ai) installed
- [OpenCode v2](https://v2.opencode.ai) installed

## Installation

Add superpowers to the `plugin` array in your `opencode.json` (global or project-level):
Add superpowers to the `plugins` array in your `opencode.json` (global or
project-level). `plugins` is the canonical v2 key; the legacy singular `plugin`
key still loads for now but may be removed:

```json
{
"plugin": ["superpowers@git+https://github.com/obra/superpowers.git"]
"plugins": ["superpowers@git+https://github.com/obra/superpowers.git"]
}
```

Expand Down Expand Up @@ -39,6 +45,12 @@ rm -rf ~/.config/opencode/superpowers

Then follow the installation steps above.

## Migrating from OpenCode v1

If your `opencode.json` used the singular `plugin` key, rename it to `plugins`.
No other changes are needed — the same git package spec works, and the plugin
now loads through the OpenCode v2 API automatically.

## Usage

Use OpenCode's native `skill` tool:
Expand All @@ -59,17 +71,18 @@ To pin a specific version:

```json
{
"plugin": ["superpowers@git+https://github.com/obra/superpowers.git#v5.0.3"]
"plugins": ["superpowers@git+https://github.com/obra/superpowers.git#v5.0.3"]
}
```

## Troubleshooting

### Plugin not loading

1. Check logs: `opencode run --print-logs "hello" 2>&1 | grep -i superpowers`
2. Verify the plugin line in your `opencode.json`
3. Make sure you're running a recent version of OpenCode
1. Check logs: `grep -i "loading plugin" ~/.local/share/opencode/log/opencode.log` (OpenCode v2 logs to a file; the CLI `run` command no longer has a `--print-logs` flag)
2. Verify the plugin line in your `opencode.json` uses the `plugins` key
3. If the shared background service is stuck, restart it: `opencode service restart`
4. Make sure you're running OpenCode v2

### Windows install issues

Expand All @@ -87,7 +100,7 @@ Then use the installed package path in `opencode.json`:

```json
{
"plugin": ["~/.config/opencode/node_modules/superpowers"]
"plugins": ["~/.config/opencode/node_modules/superpowers"]
}
```

Expand All @@ -98,13 +111,13 @@ Then use the installed package path in `opencode.json`:

### Tool mapping

Skills speak in actions ("create a todo", "dispatch a subagent", "read a file"). On OpenCode these resolve to:
Skills speak in actions ("create a todo", "dispatch a subagent", "read a file"). On OpenCode v2 these resolve to:

- "Create a todo" / "mark complete in todo list" → `todowrite`
- `Subagent (general-purpose):` template → `task` tool with `subagent_type: "general"` (or `"explore"` for codebase exploration)
- "Invoke a skill" → OpenCode's native `skill` tool
- "Read a file" → `read`
- "Create a file" / "edit a file" / "delete a file" → `apply_patch`
- "Create a file" → `write`; "edit a file" → `edit`
- "Run a shell command" → `bash`
- "Search file contents" / "find files by name" → `grep`, `glob`
- "Fetch a URL" → `webfetch`
Expand Down
148 changes: 74 additions & 74 deletions .opencode/plugins/superpowers.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
/**
* Superpowers plugin for OpenCode.ai
* Superpowers plugin for OpenCode v2.
*
* Injects superpowers bootstrap context via message transform.
* Auto-registers skills directory via config hook (no symlinks needed).
* OpenCode v2 replaced the v1 plugin API entirely (v1 plugins do not run under
* v2). This plugin targets the v2 API documented at
* https://v2.opencode.ai/build/plugins:
*
* - Default export is a plain `{ id, setup }` object. (`Plugin.define()` from
* "@opencode-ai/plugin/v2" is an identity wrapper, so we avoid the runtime
* import to keep this a zero-dependency plugin that loads in any layout.)
* - `ctx.skill.transform()` registers the bundled skills directory as a skill
* source, so OpenCode discovers superpowers skills with no symlinks or
* manual `skills.paths` edits.
* - `ctx.session.hook("context", ...)` injects the superpowers bootstrap into
* the first user message immediately before each model dispatch.
*/

import path from 'path';
import fs from 'fs';
import os from 'os';
import { fileURLToPath } from 'url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

// Bundled skills live two levels up from this plugin file (../../skills).
const superpowersSkillsDir = path.resolve(__dirname, '../../skills');

// Simple frontmatter extraction (avoid dependency on skills-core for bootstrap)
const extractAndStripFrontmatter = (content) => {
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
Expand All @@ -33,60 +45,41 @@ const extractAndStripFrontmatter = (content) => {
return { frontmatter, content: body };
};

// 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);
};

// Module-level cache for bootstrap content.
// The SKILL.md file does not change during a session, so reading + parsing it
// once eliminates redundant fs.existsSync + fs.readFileSync + regex work on
// every agent step. See #1202 for the full analysis.
// every model dispatch. See #1202 for the full analysis.
let _bootstrapCache = undefined; // undefined = not yet loaded, null = file missing

export const SuperpowersPlugin = async ({ client, directory }) => {
const homeDir = os.homedir();
const superpowersSkillsDir = path.resolve(__dirname, '../../skills');
const envConfigDir = normalizePath(process.env.OPENCODE_CONFIG_DIR, homeDir);
const configDir = envConfigDir || path.join(homeDir, '.config/opencode');

// Helper to generate bootstrap content (cached after first call)
const getBootstrapContent = () => {
// Return cached result on subsequent calls
if (_bootstrapCache !== undefined) return _bootstrapCache;

// Try to load using-superpowers skill
const skillPath = path.join(superpowersSkillsDir, 'using-superpowers', 'SKILL.md');
if (!fs.existsSync(skillPath)) {
_bootstrapCache = null;
return null;
}
// Helper to generate bootstrap content (cached after first call)
const getBootstrapContent = () => {
// Return cached result on subsequent calls
if (_bootstrapCache !== undefined) return _bootstrapCache;

const fullContent = fs.readFileSync(skillPath, 'utf8');
const { content } = extractAndStripFrontmatter(fullContent);
// Try to load using-superpowers skill
const skillPath = path.join(superpowersSkillsDir, 'using-superpowers', 'SKILL.md');
if (!fs.existsSync(skillPath)) {
_bootstrapCache = null;
return null;
}

const toolMapping = `**Tool Mapping for OpenCode:**
const fullContent = fs.readFileSync(skillPath, 'utf8');
const { content } = extractAndStripFrontmatter(fullContent);

const toolMapping = `**Tool Mapping for OpenCode:**
When skills request actions, substitute OpenCode equivalents:
- Create or update todos → \`todowrite\`
- \`Subagent (general-purpose):\` → \`task\` with \`subagent_type: "general"\`
- \`Subagent (general-purpose):\` → \`task\` with \`subagent_type: "general"\` (or \`"explore"\` for codebase exploration)
- Invoke a skill → OpenCode's native \`skill\` tool
- Read files → \`read\`
- Create, edit, or delete files → \`apply_patch\`
- Create a file → \`write\`; edit a file → \`edit\`
- Run shell commands → \`bash\`
- Search files → \`grep\`, \`glob\`
- Search file contents / find files by name → \`grep\`, \`glob\`
- Fetch a URL → \`webfetch\`

Use OpenCode's native \`skill\` tool to list and load skills.`;

_bootstrapCache = `<EXTREMELY_IMPORTANT>
_bootstrapCache = `<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 skill tool to load "using-superpowers" again - that would be redundant.**
Expand All @@ -96,44 +89,51 @@ ${content}
${toolMapping}
</EXTREMELY_IMPORTANT>`;

return _bootstrapCache;
};

return {
// 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);
return _bootstrapCache;
};

export default {
id: 'superpowers',

setup: async (ctx) => {
// Register the bundled skills directory as a skill source so OpenCode
// discovers superpowers skills without symlinks or manual config edits.
// Guard against re-adding the same directory if setup runs more than once.
await ctx.skill.transform((draft) => {
const alreadyRegistered = draft
.list()
.some((source) => source.type === 'directory'
&& path.resolve(source.path) === superpowersSkillsDir);
if (!alreadyRegistered) {
draft.source({ type: 'directory', path: superpowersSkillsDir });
}
},
});
await ctx.skill.reload();

// Inject bootstrap into the first user message of each session.
// Inject bootstrap into the first user message of each request.
// Using a user message instead of a system message avoids:
// 1. Token bloat from system messages repeated every turn (#750)
// 2. Multiple system messages breaking Qwen and other models (#894)
//
// The hook fires on every agent step (not just every turn) because
// opencode's prompt.ts reloads messages from DB each step. Fresh message
// arrays may need injection again, so getBootstrapContent() must not do
// repeated disk work.
'experimental.chat.messages.transform': async (_input, output) => {
// The hook fires before every model dispatch, so getBootstrapContent()
// must avoid repeated disk work (it caches at module level).
await ctx.session.hook('context', (input) => {
const bootstrap = getBootstrapContent();
if (!bootstrap || !output.messages.length) return;
const firstUser = output.messages.find(m => m.info.role === 'user');
if (!firstUser || !firstUser.parts.length) return;

// Guard: skip if first user message already contains bootstrap.
// This prevents double injection when OpenCode passes an already
// transformed in-memory message array through the hook again.
if (firstUser.parts.some(p => p.type === 'text' && p.text.includes('EXTREMELY_IMPORTANT'))) return;
if (!bootstrap || !input.messages || !input.messages.length) return;

const firstUser = input.messages.find((m) => m.role === 'user');
if (!firstUser || !Array.isArray(firstUser.content) || !firstUser.content.length) return;

// Guard: skip if the first user message already contains the bootstrap.
// This prevents double injection when the hook re-runs over a message
// array that was already transformed in a previous dispatch.
if (firstUser.content.some((part) => part.type === 'text'
&& typeof part.text === 'string'
&& part.text.includes('EXTREMELY_IMPORTANT'))) {
return;
}

const ref = firstUser.parts[0];
firstUser.parts.unshift({ ...ref, type: 'text', text: bootstrap });
}
};
firstUser.content.unshift({ type: 'text', text: bootstrap });
});
},
};
46 changes: 29 additions & 17 deletions docs/README.opencode.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
# Superpowers for OpenCode

Complete guide for using Superpowers with [OpenCode.ai](https://opencode.ai).
Complete guide for using Superpowers with [OpenCode v2](https://v2.opencode.ai).

> **OpenCode v2 only.** This plugin targets the OpenCode v2 plugin API
> (default `{ id, setup }` export, `ctx.skill.transform`, `ctx.session.hook`).
> OpenCode states that v1 plugins do not run under v2, so use an older
> Superpowers release if you are still on OpenCode v1.

## Installation

Add superpowers to the `plugin` array in your `opencode.json` (global or project-level):
Add superpowers to the `plugins` array in your `opencode.json` (global or
project-level). `plugins` is the canonical v2 key; the legacy singular `plugin`
key still loads for now but may be removed:

```json
{
"plugin": ["superpowers@git+https://github.com/obra/superpowers.git"]
"plugins": ["superpowers@git+https://github.com/obra/superpowers.git"]
}
```

Expand Down Expand Up @@ -91,39 +98,44 @@ To pin a specific version, use a branch or tag:

```json
{
"plugin": ["superpowers@git+https://github.com/obra/superpowers.git#v5.0.3"]
"plugins": ["superpowers@git+https://github.com/obra/superpowers.git#v5.0.3"]
}
```

## How It Works

The plugin does two things:
The plugin's `setup(ctx)` does two things:

1. **Injects bootstrap context** via the `experimental.chat.messages.transform` hook, adding superpowers awareness to every conversation.
2. **Registers the skills directory** via the `config` hook, so OpenCode discovers all superpowers skills without symlinks or manual config.
1. **Registers the skills directory** via `ctx.skill.transform()`, adding the
bundled `skills/` folder as a directory skill source so OpenCode discovers
all superpowers skills without symlinks or manual `skills.paths` config.
2. **Injects bootstrap context** via `ctx.session.hook("context", ...)`,
prepending superpowers awareness to the first user message before each model
dispatch. (Using a user message rather than a system message avoids token
bloat and multi-system-message issues on some models.)

### Tool Mapping

Skills speak in actions rather than naming any one runtime's tools. On OpenCode these resolve to:
Skills speak in actions rather than naming any one runtime's tools. On OpenCode v2 these resolve to:

- "Create a todo" / "mark complete in todo list" → `todowrite`
- `Subagent (general-purpose):` template → OpenCode's `task` tool with `subagent_type: "general"` (or `"explore"` for codebase exploration)
- "Invoke a skill" → OpenCode's native `skill` tool
- "Read a file" → `read`
- "Create a file" / "edit a file" / "delete a file" → `apply_patch`
- "Create a file" → `write`; "edit a file" → `edit`
- "Run a shell command" → `bash`
- "Search file contents" / "find files by name" → `grep`, `glob`
- "Fetch a URL" → `webfetch`

(Verified against the installed OpenCode CLI's tool inventory.)
(Verified against the installed OpenCode v2 CLI's tool inventory.)

## Troubleshooting

### Plugin not loading

1. Check OpenCode logs: `opencode run --print-logs "hello" 2>&1 | grep -i superpowers`
2. Verify the plugin line in your `opencode.json` is correct
3. Make sure you're running a recent version of OpenCode
1. Check OpenCode logs: `grep -i "loading plugin" ~/.local/share/opencode/log/opencode.log` (OpenCode v2 logs to a file; the CLI `run` command no longer has a `--print-logs` flag). If the shared background service is stuck, run `opencode service restart`.
2. Verify the plugin line in your `opencode.json` is correct and uses the `plugins` key
3. Make sure you're running OpenCode v2

### Windows install issues

Expand All @@ -141,7 +153,7 @@ Then use the installed package path in `opencode.json`:

```json
{
"plugin": ["~/.config/opencode/node_modules/superpowers"]
"plugins": ["~/.config/opencode/node_modules/superpowers"]
}
```

Expand All @@ -153,11 +165,11 @@ Then use the installed package path in `opencode.json`:

### Bootstrap not appearing

1. Check OpenCode version supports `experimental.chat.messages.transform` hook
2. Restart OpenCode after config changes
1. Make sure you are on OpenCode v2 (the plugin uses the v2 `ctx.session.hook("context", ...)` API)
2. Restart OpenCode after config changes (`opencode service restart` if the background service is stuck)

## Getting Help

- Report issues: https://github.com/obra/superpowers/issues
- Main documentation: https://github.com/obra/superpowers
- OpenCode docs: https://opencode.ai/docs/
- OpenCode docs: https://v2.opencode.ai/
Loading