Просмотр исходного кода

feat(opencode): add V2 (opencode2) plugin compatibility

Add dual V1/V2 support to the OpenCode plugin. The same source file now
works on both OpenCode V1 (opencode) and V2 (opencode2) without version
detection at runtime.

V2 changes:
- Add default export { id, server, setup } for V2 PluginSupervisor
- setup() registers skills via ctx.skill.transform() (V2 native API)
- setup() injects bootstrap via ctx.session.hook('context') (V2 equivalent
  of V1's experimental.chat.messages.transform)
- config hook guards against V2 array-format skills to avoid conflicts

Both APIs confirmed active at runtime via diagnostics in the V2 beta.

No external dependencies added — pure JavaScript throughout.

Docs updated with V2 install instructions, OPENCODE_CONFIG_DIR side-by-side
setup, and accurate How It Works section for both versions.
GoldJohnKing 1 месяц назад
Родитель
Сommit
2ee6281747
3 измененных файлов с 258 добавлено и 55 удалено
  1. 76 1
      .opencode/INSTALL.md
  2. 87 43
      .opencode/plugins/superpowers.js
  3. 95 11
      docs/README.opencode.md

+ 76 - 1
.opencode/INSTALL.md

@@ -4,7 +4,7 @@
 
 
 - [OpenCode.ai](https://opencode.ai) installed
 - [OpenCode.ai](https://opencode.ai) installed
 
 
-## Installation
+## OpenCode V1 (`opencode`) Installation
 
 
 Add superpowers to the `plugin` array in your `opencode.json` (global or project-level):
 Add superpowers to the `plugin` array in your `opencode.json` (global or project-level):
 
 
@@ -22,6 +22,72 @@ Verify by asking: "Tell me about your superpowers"
 OpenCode uses its own plugin install. If you also use Claude Code, Codex, or
 OpenCode uses its own plugin install. If you also use Claude Code, Codex, or
 another harness, install Superpowers separately for each one.
 another harness, install Superpowers separately for each one.
 
 
+## OpenCode V2 (`opencode2`) Installation
+
+V2 does not support `git+https://` plugin installation. Use a local clone
+with a path reference instead.
+
+### Steps
+
+1. Clone the repository:
+
+```bash
+git clone https://github.com/obra/superpowers.git ~/superpowers
+```
+
+2. Add the plugin to your `opencode.json` (global or project-level).
+   Use the `plugin` field (singular) with an absolute path:
+
+```jsonc
+{
+  "$schema": "https://opencode.ai/config.json",
+  "plugin": [
+    "/home/your-username/superpowers/.opencode/plugins/superpowers.js"
+  ]
+}
+```
+
+> **Note:** V2 does not expand `~` in local plugin paths. Use an absolute
+> path or a relative path (`./` or `../`) resolved from the config file
+> directory.
+
+3. Restart OpenCode:
+
+```bash
+opencode2 service restart
+```
+
+4. Verify by asking: "Tell me about your superpowers"
+
+### Updating
+
+```bash
+cd ~/superpowers && git pull
+opencode2 service restart
+```
+
+## Running V1 and V2 Side by Side
+
+V1 (`opencode`) and V2 (`opencode2`) share the same default config directory
+(`~/.config/opencode/`). Since V2 normalizes V1's `plugin` field into its own
+loading pipeline, putting a `git+https://` spec (which V2 cannot install) in
+the shared config causes V2 to silently fail loading it.
+
+To use different plugin sources for each version, point V2 at a separate
+config directory via the `OPENCODE_CONFIG_DIR` environment variable:
+
+```bash
+# In ~/.bashrc (or equivalent shell config)
+export OPENCODE_CONFIG_DIR="$HOME/.config/opencode2"
+```
+
+Then maintain two config files:
+
+- `~/.config/opencode/opencode.json` — V1 config, using `git+https://` sources
+- `~/.config/opencode2/opencode.json` — V2 config, using local path sources
+
+Both versions can now run independently without interfering with each other.
+
 ## Migrating from the old symlink-based install
 ## Migrating from the old symlink-based install
 
 
 If you previously installed superpowers using `git clone` and symlinks, remove the old setup:
 If you previously installed superpowers using `git clone` and symlinks, remove the old setup:
@@ -50,6 +116,8 @@ use skill tool to load brainstorming
 
 
 ## Updating
 ## Updating
 
 
+### V1 (`opencode`)
+
 OpenCode installs Superpowers through a git-backed package spec. Some OpenCode
 OpenCode installs Superpowers through a git-backed package spec. Some OpenCode
 and Bun versions pin that resolved git dependency in a lockfile or cache, so a
 and Bun versions pin that resolved git dependency in a lockfile or cache, so a
 restart may not pick up the newest Superpowers commit. If updates do not appear,
 restart may not pick up the newest Superpowers commit. If updates do not appear,
@@ -63,6 +131,13 @@ To pin a specific version:
 }
 }
 ```
 ```
 
 
+### V2 (`opencode2`)
+
+```bash
+cd ~/superpowers && git pull
+opencode2 service restart
+```
+
 ## Troubleshooting
 ## Troubleshooting
 
 
 ### Plugin not loading
 ### Plugin not loading

+ 87 - 43
.opencode/plugins/superpowers.js

@@ -1,17 +1,29 @@
 /**
 /**
  * Superpowers plugin for OpenCode.ai
  * Superpowers plugin for OpenCode.ai
  *
  *
- * Injects superpowers bootstrap context via message transform.
- * Auto-registers skills directory via config hook (no symlinks needed).
+ * Dual-compatible with OpenCode V1 and V2.
+ *
+ * V1 (opencode): loaded via named export SuperpowersPlugin — provides config
+ * hook for skills registration and experimental.chat.messages.transform for
+ * bootstrap injection.
+ *
+ * V2 (opencode2): loaded via default export { id, setup } by PluginSupervisor.
+ * setup() registers skills natively via ctx.skill.transform(), and injects
+ * bootstrap context via ctx.session.hook("context").
+ *
+ * No external dependencies — pure JavaScript works in both V1 and V2 without
+ * installing @opencode-ai/plugin or effect.
  */
  */
 
 
 import path from 'path';
 import path from 'path';
 import fs from 'fs';
 import fs from 'fs';
-import os from 'os';
 import { fileURLToPath } from 'url';
 import { fileURLToPath } from 'url';
 
 
 const __dirname = path.dirname(fileURLToPath(import.meta.url));
 const __dirname = path.dirname(fileURLToPath(import.meta.url));
 
 
+// Skills directory shared by V1 (config hook) and V2 (setup/ctx.skill.transform)
+const superpowersSkillsDir = path.resolve(__dirname, '../../skills');
+
 // Simple frontmatter extraction (avoid dependency on skills-core for bootstrap)
 // Simple frontmatter extraction (avoid dependency on skills-core for bootstrap)
 const extractAndStripFrontmatter = (content) => {
 const extractAndStripFrontmatter = (content) => {
   const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
   const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
@@ -33,47 +45,28 @@ const extractAndStripFrontmatter = (content) => {
   return { frontmatter, content: body };
   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.
 // Module-level cache for bootstrap content.
 // The SKILL.md file does not change during a session, so reading + parsing it
 // The SKILL.md file does not change during a session, so reading + parsing it
 // once eliminates redundant fs.existsSync + fs.readFileSync + regex work on
 // once eliminates redundant fs.existsSync + fs.readFileSync + regex work on
 // every agent step.  See #1202 for the full analysis.
 // every agent step.  See #1202 for the full analysis.
 let _bootstrapCache = undefined; // undefined = not yet loaded, null = file missing
 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;
+
+  // Try to load using-superpowers skill
+  const skillPath = path.join(superpowersSkillsDir, 'using-superpowers', 'SKILL.md');
+  if (!fs.existsSync(skillPath)) {
+    _bootstrapCache = null;
+    return null;
+  }
 
 
-    const fullContent = fs.readFileSync(skillPath, 'utf8');
-    const { content } = extractAndStripFrontmatter(fullContent);
+  const fullContent = fs.readFileSync(skillPath, 'utf8');
+  const { content } = extractAndStripFrontmatter(fullContent);
 
 
-    const toolMapping = `**Tool Mapping for OpenCode:**
+  const toolMapping = `**Tool Mapping for OpenCode:**
 When skills request actions, substitute OpenCode equivalents:
 When skills request actions, substitute OpenCode equivalents:
 - Create or update todos → \`todowrite\`
 - Create or update todos → \`todowrite\`
 - \`Subagent (general-purpose):\` → \`task\` with \`subagent_type: "general"\`
 - \`Subagent (general-purpose):\` → \`task\` with \`subagent_type: "general"\`
@@ -86,7 +79,7 @@ When skills request actions, substitute OpenCode equivalents:
 
 
 Use OpenCode's native \`skill\` tool to list and load skills.`;
 Use OpenCode's native \`skill\` tool to list and load skills.`;
 
 
-    _bootstrapCache = `<EXTREMELY_IMPORTANT>
+  _bootstrapCache = `<EXTREMELY_IMPORTANT>
 You have superpowers.
 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.**
 **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.**
@@ -96,15 +89,25 @@ ${content}
 ${toolMapping}
 ${toolMapping}
 </EXTREMELY_IMPORTANT>`;
 </EXTREMELY_IMPORTANT>`;
 
 
-    return _bootstrapCache;
-  };
+  return _bootstrapCache;
+};
 
 
+/**
+ * V1 Plugin Function (named export + default.server)
+ *
+ * Used by V1 (OpenCode 1.x): discovered via named export scanning.
+ * Provides: config hook (V1 skills registration) + bootstrap injection
+ * (experimental.chat.messages.transform).
+ */
+export const SuperpowersPlugin = async ({ client, directory }) => {
   return {
   return {
     // Inject skills path into live config so OpenCode discovers superpowers skills
     // Inject skills path into live config so OpenCode discovers superpowers skills
     // without requiring manual symlinks or config file edits.
     // 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: async (config) => {
+      // V2: skills is a flat array — skip, setup() handles V2 skill registration
+      if (Array.isArray(config.skills)) return;
+
+      // V1: skills is { paths: [...] }
       config.skills = config.skills || {};
       config.skills = config.skills || {};
       config.skills.paths = config.skills.paths || [];
       config.skills.paths = config.skills.paths || [];
       if (!config.skills.paths.includes(superpowersSkillsDir)) {
       if (!config.skills.paths.includes(superpowersSkillsDir)) {
@@ -128,8 +131,6 @@ ${toolMapping}
       if (!firstUser || !firstUser.parts.length) return;
       if (!firstUser || !firstUser.parts.length) return;
 
 
       // Guard: skip if first user message already contains bootstrap.
       // 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 (firstUser.parts.some(p => p.type === 'text' && p.text.includes('EXTREMELY_IMPORTANT'))) return;
 
 
       const ref = firstUser.parts[0];
       const ref = firstUser.parts[0];
@@ -137,3 +138,46 @@ ${toolMapping}
     }
     }
   };
   };
 };
 };
+
+/**
+ * V2 Setup Function (default.setup)
+ *
+ * Called by V2 PluginSupervisor (packages/core/src/plugin/).
+ * Performs two things:
+ *
+ * 1. Registers the skills directory natively via ctx.skill.transform().
+ * 2. Injects bootstrap context via ctx.session.hook("context"), the V2
+ *    equivalent of V1's experimental.chat.messages.transform.
+ */
+async function setup(ctx) {
+  // 1. Register skills
+  await ctx.skill.transform((draft) => {
+    draft.source({
+      type: 'directory',
+      path: superpowersSkillsDir,
+    });
+  });
+
+  // 2. Inject bootstrap into first user message via V2 session context hook
+  await ctx.session.hook('context', (event) => {
+    const bootstrap = getBootstrapContent();
+    if (!bootstrap || !event.messages || !event.messages.length) return;
+    const firstUser = event.messages.find(m => m.role === 'user');
+    if (!firstUser || !firstUser.content || !firstUser.content.length) return;
+    if (firstUser.content.some(p => p.type === 'text' && p.text && p.text.includes('EXTREMELY_IMPORTANT'))) return;
+    firstUser.content.unshift({ type: 'text', text: bootstrap });
+  });
+}
+
+/**
+ * Default Export: { id, server, setup }
+ *
+ * V2 PluginSupervisor reads { id, setup }.
+ * V1 reads named export SuperpowersPlugin.
+ * server() is exported for V1 compatibility.
+ */
+export default {
+  id: 'superpowers',
+  server: SuperpowersPlugin,
+  setup,
+};

+ 95 - 11
docs/README.opencode.md

@@ -4,6 +4,11 @@ Complete guide for using Superpowers with [OpenCode.ai](https://opencode.ai).
 
 
 ## Installation
 ## Installation
 
 
+Installation differs between OpenCode V1 (`opencode`) and V2 (`opencode2`).
+Install Superpowers separately for each version if you use both.
+
+### OpenCode V1 (`opencode`)
+
 Add superpowers to the `plugin` array in your `opencode.json` (global or project-level):
 Add superpowers to the `plugin` array in your `opencode.json` (global or project-level):
 
 
 ```json
 ```json
@@ -17,10 +22,62 @@ registers all skills.
 
 
 Verify by asking: "Tell me about your superpowers"
 Verify by asking: "Tell me about your superpowers"
 
 
-OpenCode uses its own plugin install. If you also use Claude Code, Codex, or
-another harness, install Superpowers separately for each one.
+### OpenCode V2 (`opencode2`)
+
+V2 does not support `git+https://` plugin installation. Use a local clone with
+a path reference instead.
 
 
-### Migrating from the old symlink-based install
+1. Clone the repository:
+
+```bash
+git clone https://github.com/obra/superpowers.git ~/superpowers
+```
+
+2. Add the plugin using the `plugin` field (singular) with an absolute path:
+
+```jsonc
+{
+  "$schema": "https://opencode.ai/config.json",
+  "plugin": [
+    "/home/your-username/superpowers/.opencode/plugins/superpowers.js"
+  ]
+}
+```
+
+> **Note:** V2 does not expand `~` in local plugin paths. Use an absolute path
+> or a relative path (`./` or `../`) resolved from the config file directory.
+
+3. Restart and verify:
+
+```bash
+opencode2 service restart
+```
+
+Ask: "Tell me about your superpowers"
+
+### Running V1 and V2 Side by Side
+
+V1 (`opencode`) and V2 (`opencode2`) share the same default config directory
+(`~/.config/opencode/`). Since V2 normalizes V1's `plugin` field into its own
+loading pipeline, putting a `git+https://` spec (which V2 cannot install) in
+the shared config causes V2 to silently fail loading it.
+
+To use different plugin sources for each version, point V2 at a separate
+config directory via the `OPENCODE_CONFIG_DIR` environment variable:
+
+```bash
+# In ~/.bashrc (or equivalent shell config)
+export OPENCODE_CONFIG_DIR="$HOME/.config/opencode2"
+```
+
+Then maintain two config files:
+
+- `~/.config/opencode/opencode.json` — V1 config, using `git+https://` sources
+- `~/.config/opencode2/opencode.json` — V2 config, using local path sources
+
+Both versions can now run independently without interfering with each other.
+
+### Migrating from the old symlink-based install (V1)
 
 
 If you previously installed superpowers using `git clone` and symlinks, remove the old setup:
 If you previously installed superpowers using `git clone` and symlinks, remove the old setup:
 
 
@@ -82,6 +139,8 @@ Create project-specific skills in `.opencode/skills/` within your project.
 
 
 ## Updating
 ## Updating
 
 
+### V1 (`opencode`)
+
 OpenCode installs Superpowers through a git-backed package spec. Some OpenCode
 OpenCode installs Superpowers through a git-backed package spec. Some OpenCode
 and Bun versions pin that resolved git dependency in a lockfile or cache, so a
 and Bun versions pin that resolved git dependency in a lockfile or cache, so a
 restart may not pick up the newest Superpowers commit. If updates do not appear,
 restart may not pick up the newest Superpowers commit. If updates do not appear,
@@ -95,12 +154,23 @@ To pin a specific version, use a branch or tag:
 }
 }
 ```
 ```
 
 
+### V2 (`opencode2`)
+
+```bash
+cd ~/superpowers && git pull
+opencode2 service restart
+```
+
 ## How It Works
 ## How It Works
 
 
 The plugin does two things:
 The plugin 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** so OpenCode discovers all superpowers skills without symlinks or manual config.
+   - **V1:** via the `config` hook, injecting into `config.skills.paths`
+   - **V2:** via the `setup()` function using `ctx.skill.transform()` (V2 native API, confirmed active at runtime)
+2. **Injects bootstrap context** into the first user message of each conversation, adding superpowers awareness.
+   - **V1:** via `experimental.chat.messages.transform` hook
+   - **V2:** via `ctx.session.hook("context")` — the V2 equivalent (confirmed active at runtime)
 
 
 ### Tool Mapping
 ### Tool Mapping
 
 
@@ -121,9 +191,22 @@ Skills speak in actions rather than naming any one runtime's tools. On OpenCode
 
 
 ### Plugin not loading
 ### 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
+**V1:** Check OpenCode logs:
+
+```
+opencode run --print-logs "hello" 2>&1 | grep -i superpowers
+```
+
+**V2:** Check the server log:
+
+```
+opencode2 service status
+```
+
+Then inspect `~/.local/share/opencode/log/opencode.log`, filtering for `role=server`.
+
+Also verify the plugin path in your `opencode.json` is correct and that you're
+running a recent version of OpenCode.
 
 
 ### Windows install issues
 ### Windows install issues
 
 
@@ -153,11 +236,12 @@ Then use the installed package path in `opencode.json`:
 
 
 ### Bootstrap not appearing
 ### Bootstrap not appearing
 
 
-1. Check OpenCode version supports `experimental.chat.messages.transform` hook
-2. Restart OpenCode after config changes
+- **V1:** Check OpenCode version supports `experimental.chat.messages.transform` hook. Restart OpenCode after config changes.
+- **V2:** The plugin uses `ctx.session.hook("context")` for bootstrap injection. Verify the plugin loaded via `opencode2 api get /api/plugin`. Restart with `opencode2 service restart` after config changes.
 
 
 ## Getting Help
 ## Getting Help
 
 
 - Report issues: https://github.com/obra/superpowers/issues
 - Report issues: https://github.com/obra/superpowers/issues
 - Main documentation: https://github.com/obra/superpowers
 - Main documentation: https://github.com/obra/superpowers
-- OpenCode docs: https://opencode.ai/docs/
+- OpenCode V2 docs: https://opencode.ai/v2/docs/
+- OpenCode V1 docs: https://opencode.ai/docs/