Forráskód Böngészése

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 hónapja
szülő
commit
2ee6281747
3 módosított fájl, 258 hozzáadás és 55 törlés
  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
 
-## Installation
+## OpenCode V1 (`opencode`) Installation
 
 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
 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
 
 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
 
+### V1 (`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
 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
 
 ### Plugin not loading

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

@@ -1,17 +1,29 @@
 /**
  * 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 fs from 'fs';
-import os from 'os';
 import { fileURLToPath } from '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)
 const extractAndStripFrontmatter = (content) => {
   const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
@@ -33,47 +45,28 @@ 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.
 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:
 - Create or update todos → \`todowrite\`
 - \`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.`;
 
-    _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.**
@@ -96,15 +89,25 @@ ${content}
 ${toolMapping}
 </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 {
     // 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) => {
+      // 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.paths = config.skills.paths || [];
       if (!config.skills.paths.includes(superpowersSkillsDir)) {
@@ -128,8 +131,6 @@ ${toolMapping}
       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;
 
       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 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):
 
 ```json
@@ -17,10 +22,62 @@ registers all skills.
 
 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:
 
@@ -82,6 +139,8 @@ Create project-specific skills in `.opencode/skills/` within your project.
 
 ## Updating
 
+### V1 (`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
 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
 
 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
 
@@ -121,9 +191,22 @@ Skills speak in actions rather than naming any one runtime's tools. On OpenCode
 
 ### 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
 
@@ -153,11 +236,12 @@ 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
+- **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
 
 - Report issues: https://github.com/obra/superpowers/issues
 - 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/