| 1 | # Write your first Codewhale plugin |
| 2 | |
| 3 | > 阅读简体中文版:[zh_hans/PLUGIN_AUTHORING.md](zh_hans/PLUGIN_AUTHORING.md) |
| 4 | |
| 5 | Start with a skill: a Markdown instruction file inside a small plugin bundle. |
| 6 | The [hello-codewhale example](examples/plugins/hello-codewhale/plugin.json) |
| 7 | contains two files, declares no server or hook, and asks for no tool use. |
| 8 | This walkthrough takes it from source files to a reviewed, enabled skill. |
| 9 | |
| 10 | ## 1. Create the bundle |
| 11 | |
| 12 | Use the checked-in example, or create this directory outside an installed |
| 13 | plugins directory: |
| 14 | |
| 15 | ```text |
| 16 | hello-codewhale/ |
| 17 | ├── plugin.json |
| 18 | └── skills/ |
| 19 | └── hello/ |
| 20 | └── SKILL.md |
| 21 | ``` |
| 22 | |
| 23 | `plugin.json`: |
| 24 | |
| 25 | ```json |
| 26 | { |
| 27 | "$schema": "https://agent-plugins.org/schemas/plugin.json", |
| 28 | "name": "hello-codewhale", |
| 29 | "version": "0.1.0", |
| 30 | "description": "A minimal, explicitly invoked greeting skill." |
| 31 | } |
| 32 | ``` |
| 33 | |
| 34 | `skills/hello/SKILL.md`: |
| 35 | |
| 36 | ```markdown |
| 37 | --- |
| 38 | name: hello |
| 39 | description: Greet the user when they explicitly try the hello-codewhale example. |
| 40 | invocation: explicit-only |
| 41 | --- |
| 42 | |
| 43 | Respond with one short greeting in the user's language. Include the exact text |
| 44 | `hello-codewhale:hello` so they can identify the example they invoked. |
| 45 | |
| 46 | Use only the conversation. Do not call tools, run commands, read or write files, |
| 47 | or contact external services. |
| 48 | ``` |
| 49 | |
| 50 | Codewhale finds `skills/` automatically. `explicit-only` keeps this example out |
| 51 | of the model's automatic skill catalogue; you load it by name. See |
| 52 | [Skills](SKILLS.md#invocation-and-alias-metadata) for invocation metadata and |
| 53 | [Plugin bundles](PLUGIN_BUNDLES.md#manifest) for the authoritative manifest |
| 54 | contract. Keep new native bundles in `plugin.json`; no second manifest is |
| 55 | needed. |
| 56 | |
| 57 | ## 2. Install, inspect, and trust |
| 58 | |
| 59 | Start Codewhale in the repository root. Enter these commands **inside the |
| 60 | Codewhale session**, one at a time: |
| 61 | |
| 62 | ```text |
| 63 | /plugin install ./docs/examples/plugins/hello-codewhale |
| 64 | /plugin validate hello-codewhale |
| 65 | /plugin show hello-codewhale |
| 66 | ``` |
| 67 | |
| 68 | For your own bundle, replace the install path with its directory. Installation |
| 69 | copies it to `~/.codewhale/plugins/hello-codewhale/`, disabled and untrusted. |
| 70 | Review the installed source, skill inventory, and permissions. This example |
| 71 | should declare only Skills, with no MCP server, hook, or requested network host. |
| 72 | |
| 73 | The install review prints a command containing two full hashes: |
| 74 | |
| 75 | ```text |
| 76 | /plugin trust hello-codewhale <full-content-sha256>.<full-capability-sha256> |
| 77 | ``` |
| 78 | |
| 79 | Run the exact command printed by your review; the angle-bracket text above is |
| 80 | a placeholder. If you need a fresh review, use `/plugin trust hello-codewhale` |
| 81 | without a token. Trust records the reviewed content and capability hashes and |
| 82 | creates a runtime snapshot. It does not enable the plugin. |
| 83 | |
| 84 | ```text |
| 85 | /plugin enable hello-codewhale |
| 86 | /skills hello-codewhale: |
| 87 | /skills inspect |
| 88 | ``` |
| 89 | |
| 90 | The skill is named `hello-codewhale:hello`: the bundle name qualifies the skill |
| 91 | name. `/skills inspect` identifies its reviewed plugin snapshot. |
| 92 | |
| 93 | ## 3. Invoke it and turn it off |
| 94 | |
| 95 | ```text |
| 96 | /skill hello-codewhale:hello |
| 97 | ``` |
| 98 | |
| 99 | Codewhale confirms activation. Then send `Say hello.` as a normal message. |
| 100 | The reply should be a short greeting containing `hello-codewhale:hello`. |
| 101 | The example contributes instructions only; the reply still uses your selected |
| 102 | model and its normal provider connection. The local install, review, and |
| 103 | activation steps do not need a model call. |
| 104 | |
| 105 | ```text |
| 106 | /plugin disable hello-codewhale |
| 107 | ``` |
| 108 | |
| 109 | Disabling removes the plugin's contributions while preserving its trust |
| 110 | receipt. A subsequent `/skill hello-codewhale:hello` must not activate it. |
| 111 | Enable it again when needed, provided its reviewed hashes still match. |
| 112 | |
| 113 | ## 4. Iterate and review changes |
| 114 | |
| 115 | The installed bundle is a copy. Editing the example's original source does |
| 116 | not update that copy. To try a changed local source, disable and uninstall the |
| 117 | installed example, then install the source directory again: |
| 118 | |
| 119 | ```text |
| 120 | /plugin disable hello-codewhale |
| 121 | /plugin uninstall hello-codewhale |
| 122 | /plugin install ./docs/examples/plugins/hello-codewhale |
| 123 | /plugin validate hello-codewhale |
| 124 | ``` |
| 125 | |
| 126 | Uninstall removes the installed copy; it leaves the original example source |
| 127 | alone. Review the new token, trust it, and enable it again. For bundles |
| 128 | installed from a remote source, use `/plugin update <name>`; see |
| 129 | [Installing plugins](PLUGINS.md#update-and-uninstall). |
| 130 | |
| 131 | When files in a discovered bundle change directly, `/plugin reload` refreshes |
| 132 | the registry. Changed content invalidates the old receipt, even if you leave |
| 133 | the version unchanged. Reload does not grant trust. Use `/plugin revoke <name>` |
| 134 | to remove trust explicitly. |
| 135 | |
| 136 | ## Add only the components you need |
| 137 | |
| 138 | All components use the same bundle review and existing Codewhale runtime: |
| 139 | |
| 140 | | Component | Authoring surface | |
| 141 | | --- | --- | |
| 142 | | Skills | `skills/<name>/SKILL.md`; [instruction and invocation contract](SKILLS.md). | |
| 143 | | MCP | A sibling `mcp.json`; [bundle transport and credential rules](PLUGIN_BUNDLES.md#validation-both-formats). | |
| 144 | | Commands | Markdown command files; [command metadata](architecture/command-dispatch.md#user-commands). | |
| 145 | | Agent profiles | Fleet TOML profiles; [Fleet authoring](FLEET.md#authoring-agent-profiles-fleet-setup). | |
| 146 | | Hooks | `HooksConfig` TOML files; [events and process behavior](HOOKS.md). | |
| 147 | | Native mods (experimental) | Reviewed ESM entries contributing tools, commands, pre-execute listeners, prompt sections and owner-local JSON state; [extension contract](EXTENSIONS.md). | |
| 148 | |
| 149 | Declare Commands, Agents, and Hooks paths under |
| 150 | `extensions["net.codewhale"]` in `plugin.json`, as specified in |
| 151 | [Plugin bundles](PLUGIN_BUNDLES.md#active-and-inactive-component-surfaces). |
| 152 | Do not place MCP server fields or arbitrary runtime entrypoints at the manifest |
| 153 | root. LSP can be inventoried but has no executable adapter. A `native` |
| 154 | extension is inventory-only by default; with the experimental |
| 155 | `[features] extension_host` flag on, it names one `.mjs`, `.js` or `.mts` ES module |
| 156 | file that the TypeScript extension host runs, and `/plugin validate` rejects |
| 157 | any other entry. Its tools always use `Required` approval, never a plugin's |
| 158 | read-only hint. Full Access, Bypass, or an exact session grant for the |
| 159 | reviewed build can satisfy that gate without a prompt |
| 160 | ([design](design/TS_EXTENSION_HOST.md#as-built-phase-1-2026-09-25)). |
| 161 | For a tested typed example, lifecycle rules and per-plugin diagnostics, read |
| 162 | [Writing an extension tool](EXTENSIONS.md). `.mts` supports Node's erasable |
| 163 | types without a separate compiler; syntax needing transformation is not supported. |
| 164 | |
| 165 | ## Write a scoped native mod |
| 166 | |
| 167 | The [mod-extension example](examples/plugins/mod-extension/README.md) is a |
| 168 | runnable ESM bundle with no package installation or compiler step. It registers |
| 169 | `mod_counter`, `/mod-count`, one prompt section, and a pre-execute listener |
| 170 | limited to its own tool. The tool returns a structured JSON counter value; |
| 171 | `ctx.storage` keeps that value in the owner directory Rust assigned. Its |
| 172 | idempotent disposers remove registrations and wait for queued work. Disable |
| 173 | withdraws the prompt section and listener while retaining the stored counter. |
| 174 | |
| 175 | Enable the experimental `extension_host` feature explicitly, install the |
| 176 | example directory, validate and inspect its Native capability, then personally |
| 177 | review and trust its exact content/capability hashes before enabling it. With |
| 178 | the feature off, native code remains inventory-only. Source changes require |
| 179 | another review; `/plugin reload` is explicit, not a hot-reload watcher. |
| 180 | |
| 181 | The available author services are `tools`, `commands`, `prompt`, `storage`, `skills`, |
| 182 | `logger`, and Cordis lifecycle facilities. `ctx.on('tools/pre-execute', ...)` |
| 183 | may abstain, deny, ask, revise object input or annotate context. Rust folds those |
| 184 | proposals and repeats planning and admission checks for revised input. `allow` |
| 185 | does not approve anything; `next()` abstains. Errors, malformed answers, |
| 186 | timeouts and withdrawn owners fail closed. This is a pre-execute proposal |
| 187 | contract, with no around-execution middleware or post-result rewriting. |
| 188 | |
| 189 | `ctx.prompt.registerSection({id, text})` proposes bounded, attributed |
| 190 | instructions delivered through the existing Engine runtime-message path. |
| 191 | `ctx.storage.get/set/delete` handles bounded owner-local JSON, including state |
| 192 | across generations. Neither API replaces the system prompt, session store or |
| 193 | credentials. Tool and command invocations expose optional frozen `sessionId`, |
| 194 | `agentId` and `originTurnId` labels supplied for that call, not runtime handles. |
| 195 | The public author SDK is not published; the example uses the documented shims. |
| 196 | |
| 197 | `ctx.skills.registerRoot({path: 'profiles/review-skills'})` contributes child |
| 198 | `SKILL.md` packages from the reviewed bundle to the existing Rust skill |
| 199 | catalog. Its disposer retires the root; disable, revoke and host exit do the |
| 200 | same. Rust checks the Native receipt, file hashes and current registration |
| 201 | when discovering or loading a skill, including queued user selections. A |
| 202 | process or host restart invalidates a saved Native selection. Bundle-relative |
| 203 | paths, parser rules and count/byte limits are documented in |
| 204 | [the skill-root contract](EXTENSIONS.md#skill-roots). This API adds instructions; |
| 205 | tool permissions remain with the shared engine. |
| 206 | |
| 207 | Custom Ratatui/GPUI widgets, DSH browser UI slots, |
| 208 | native `dsh.bundle.patch` execution and DSH's agent runtime are not provided. |
| 209 | The static importer described below still converts only its portable subset. |
| 210 | Compatible Claude bundles still use the existing declarative component adapters; |
| 211 | this Native API does not load Claude's agent loop or automatically adapt Pi's |
| 212 | extension API. Port executable mod behavior against the documented host contract. |
| 213 | An extension tool can use `exec.core.call` only during its direct model |
| 214 | invocation under the shared turn gate. Commands and activation have no core |
| 215 | handle. Nested shell and network calls force a user prompt; a mode that cannot |
| 216 | open one refuses them. See [the exact core-call contract](EXTENSIONS.md#asking-the-core-to-run-a-tool) |
| 217 | for refused tools, cancellation and call limits. |
| 218 | |
| 219 | Plugin trust is **not an OS sandbox**. A local MCP server or hook can launch a |
| 220 | process; review its code and authority before enabling it. Skills do not grant |
| 221 | permissions: repository instructions, permission rules, sandbox policy, and |
| 222 | tool approval still apply. Keep credentials out of bundles and command |
| 223 | arguments. Use the reviewed environment references documented in the |
| 224 | [bundle validation contract](PLUGIN_BUNDLES.md#validation-both-formats) for MCP; |
| 225 | read the separate [hook environment contract](HOOKS.md#the-hook-process-environment) |
| 226 | before adding a hook. |
| 227 | |
| 228 | ## Convert an existing plugin |
| 229 | |
| 230 | [`scripts/convert-plugin.py`](../scripts/convert-plugin.py) converts explicitly |
| 231 | selected remote MCP declarations, packaged local Node MCP servers, and portable |
| 232 | Skills into a native bundle. |
| 233 | It requires Python 3.10+ and PyYAML 6+; install those separately if absent. |
| 234 | The converter installs no dependencies, scans no ambient configuration or |
| 235 | credentials, makes no network requests, and executes no source code. |
| 236 | |
| 237 | ### OpenCode |
| 238 | |
| 239 | Save this plain JSON as `opencode-mcp.json`: |
| 240 | |
| 241 | ```json |
| 242 | { |
| 243 | "mcp": { |
| 244 | "docs": { |
| 245 | "type": "remote", |
| 246 | "url": "https://example.invalid/mcp", |
| 247 | "oauth": false, |
| 248 | "enabled": false |
| 249 | } |
| 250 | } |
| 251 | } |
| 252 | ``` |
| 253 | |
| 254 | From the Codewhale repository root, run this in your shell: |
| 255 | |
| 256 | ```sh |
| 257 | python3 scripts/convert-plugin.py --format opencode-v1 \ |
| 258 | --config ./opencode-mcp.json --name migrated-tools --output ./migrated-opencode |
| 259 | ``` |
| 260 | |
| 261 | Choose `--format opencode-v2` for the `mcp.servers.<name>` layout, whose server |
| 262 | flag is `disabled` instead of `enabled`. Select the format from the data; |
| 263 | filenames and upstream branch names do not determine its version. Both formats |
| 264 | require explicit `oauth: false` for remote servers. Remote MCP output uses **Streamable HTTP only**; |
| 265 | OpenCode's fallback to legacy SSE is not reproduced. For an SSE-only endpoint, |
| 266 | author native `mcp.json` with `type: "sse"` and use the same review flow. |
| 267 | |
| 268 | When MCP servers are selected, configurations containing `tools`, |
| 269 | `permission`/`permissions`, `agent`/`agents`, legacy `mode`, or `default_agent` |
| 270 | are refused. These settings can restrict tool access beyond server enablement. |
| 271 | Manually preserve those restrictions in Codewhale before supplying an MCP-only |
| 272 | input; simply deleting the settings can widen access. |
| 273 | |
| 274 | JSONC comments and trailing commas are not |
| 275 | accepted: provide a plain JSON copy containing the declarations you intend |
| 276 | to port. |
| 277 | |
| 278 | ### DeepSeek Harness (DSH) |
| 279 | |
| 280 | Save this static Cordis entry list as `dsh-mcp.yml`: |
| 281 | |
| 282 | ```yaml |
| 283 | - name: '@deepseek-ai/dsh-mcp-client' |
| 284 | disabled: true |
| 285 | config: |
| 286 | serverName: docs |
| 287 | transport: streamable-http |
| 288 | url: https://example.invalid/mcp |
| 289 | ``` |
| 290 | |
| 291 | ```sh |
| 292 | python3 scripts/convert-plugin.py --format dsh \ |
| 293 | --config ./dsh-mcp.yml --name migrated-dsh --output ./migrated-dsh |
| 294 | ``` |
| 295 | |
| 296 | The DSH input may also be JSON, but must be the plain entry list, not a full |
| 297 | profile or patch composition. Each row must name `@deepseek-ai/dsh-mcp-client`. |
| 298 | |
| 299 | A real DeepSeek Harness bundle package — an npm package whose `package.json` |
| 300 | declares `dsh.bundle.patch` — is imported natively by Codewhale, not by this |
| 301 | script (`--bundle` is retired): |
| 302 | |
| 303 | ```text |
| 304 | /plugin import dsh ./node_modules/@demo/tools-dsh |
| 305 | /plugin import dsh approve <package-dir> <content-hash> |
| 306 | ``` |
| 307 | |
| 308 | The first command converts the package into scratch and shows what converts, |
| 309 | what is skipped, the network hosts and local processes the bundle will request, |
| 310 | and its content hash; nothing is installed. `approve` installs exactly that |
| 311 | converted bundle through the ordinary reviewed installer: it lands disabled and |
| 312 | untrusted, and a package edited after review installs nothing. Installing the |
| 313 | package directory as a plain `/plugin install <dir>` routes to the same importer. |
| 314 | `/plugin update <name>` re-converts the recorded package; changed output |
| 315 | replaces the bundle and invalidates its trust receipt. The Runtime API exposes |
| 316 | the same review as `POST /v1/apps/plugins/import/dsh/preview`, whose |
| 317 | `install_source` and `content_hash` go to `POST /v1/apps/plugins/install`. |
| 318 | Those two are the only import surfaces today: the TUI slash command |
| 319 | (`/plugin import dsh <dir>` and `approve`) and the Runtime API. There is no |
| 320 | `codewhale plugin` CLI subcommand for DSH import. This static import is also |
| 321 | not the external-launcher integration `codewhale integrations dsh`, which runs |
| 322 | the user's installed `dsh` (see [INTEGRATIONS_DSH.md](INTEGRATIONS_DSH.md)). |
| 323 | |
| 324 | `dsh.bundle.patch` may name one patch file or an ordered list of files. The |
| 325 | importer reads only contained, non-linked package files (at most 64 files and |
| 326 | 1 MiB of patch data combined), then applies `insert` and keyed overrides over |
| 327 | one empty profile. As in upstream `applyEntryPatches`, an override replaces a |
| 328 | whole field: `config` is **not** deep-merged. This is not a complete profile |
| 329 | resolver: other bundles, user overlays and deployment configuration are absent. |
| 330 | Plain YAML scalars follow the YAML 1.2 core schema, as DSH's own parser does. |
| 331 | |
| 332 | Disabled groups propagate their state to descendants. Disabled MCP declarations |
| 333 | stay disabled. Disabled skills are omitted with an explicit receipt because the |
| 334 | native skill format has no disabled state. A conditional/non-boolean `disabled` |
| 335 | value on a group or portable row refuses the import rather than assuming it is |
| 336 | enabled. Unsupported entry policy/dependency fields, including `inject`, |
| 337 | `intercept` and `isolate`, also refuse the import on those rows or their groups. |
| 338 | Preserve their activation and authority rules in a manual port. |
| 339 | |
| 340 | The importer never executes plugin code. Foreign runtime plugins and |
| 341 | `dsh.client` UI code are not executed or translated. |
| 342 | Other unrepresentable components are reported in the bundle's `CONVERSION.md` and |
| 343 | structured `CONVERSION.json`, with source package/version, manifest and |
| 344 | ordered-layer SHA-256 hashes, converter version, per-row outcomes and required |
| 345 | manual ports. Unapplied patch operations also appear in the structured |
| 346 | manual-port list, with their source layer and one-based operation index. |
| 347 | |
| 348 | The only lowered `!!js` expressions are `process.execPath` (becomes `node`) and |
| 349 | simple quoted/template literals without escapes or interpolation. Environment |
| 350 | expressions—including fallbacks—are never resolved against this machine. |
| 351 | Packaged Node MCP converts only when its relative entry resolves inside the |
| 352 | package (and its declared relative working directory); a server outside the |
| 353 | package is skipped, and host paths are never copied. Rows of |
| 354 | `@deepseek-ai/dsh-skill-filesystem` contribute their literal `customSkillDirs` |
| 355 | children only when those directories live inside the package. Default user and |
| 356 | project skill roots, watchers and foreign service dependencies are not imported. |
| 357 | Arbitrary DSH TypeScript plugin execution is outside this importer's scope. |
| 358 | DSH TypeScript plugin code runs only through the experimental TypeScript |
| 359 | extension host (`[features] extension_host`, off by default), which now supports |
| 360 | tools, slash commands, scoped pre-execute proposals, additive prompt sections |
| 361 | and owner-local storage through an explicitly authored Native entry. It does |
| 362 | not execute the imported `dsh.bundle.patch` composition. See [EXTENSIONS.md](EXTENSIONS.md) and |
| 363 | [design/TS_EXTENSION_HOST.md](design/TS_EXTENSION_HOST.md). |
| 364 | |
| 365 | ### Local Node MCP servers |
| 366 | |
| 367 | For an already packaged Node MCP server, select its original process working |
| 368 | directory explicitly. The converter copies that directory into `mcp/<server>` |
| 369 | and sets the native server's working directory to the reviewed copy. Relative |
| 370 | entrypoint imports and read-only resources keep the same layout. |
| 371 | |
| 372 | ```json |
| 373 | { |
| 374 | "mcp": { |
| 375 | "localdocs": { |
| 376 | "type": "local", |
| 377 | "command": ["node", "server.mjs"], |
| 378 | "environment": {"API_TOKEN": "{env:LOCALDOCS_TOKEN}"}, |
| 379 | "enabled": false |
| 380 | } |
| 381 | } |
| 382 | } |
| 383 | ``` |
| 384 | |
| 385 | ```sh |
| 386 | python3 scripts/convert-plugin.py --format opencode-v1 \ |
| 387 | --config ./local-mcp.json --stdio-root localdocs=./packaged-localdocs \ |
| 388 | --name local-tools --output ./migrated-local |
| 389 | ``` |
| 390 | |
| 391 | Repeat `--stdio-root SERVER=DIRECTORY` for every local server in the selected |
| 392 | configuration. OpenCode v2 uses `mcp.servers` and `disabled`. Static DSH entries |
| 393 | use `transport: stdio`, `command: node`, and `args: [server.mjs]`; DSH `env` |
| 394 | must be absent or empty because its literals/expressions are not OpenCode |
| 395 | environment references. Optional DSH/v2 `cwd` must be absent, empty, or `.`; |
| 396 | the selected root explicitly supplies the original working directory. |
| 397 | |
| 398 | Use `node` plus one relative `.mjs`, `.js`, or `.cjs` entry. Package module |
| 399 | type and sibling imports are preserved by the native launch adapter. Compile |
| 400 | TypeScript to JavaScript before packaging; the converter does not run a compiler. |
| 401 | Package dependencies and read-only resources first, inside the selected root. |
| 402 | No package manager, install script, module loader or server runs during |
| 403 | conversion. Links/reparse points, hard-linked files, hidden files/directories |
| 404 | (including `.gitignore`, `.env*`, `.npmrc` and `node_modules/.bin`), common |
| 405 | credential filenames, and private-key containers are refused. Prepare a clean |
| 406 | package directory; ignore rules are not used to silently omit files. Inspect |
| 407 | every selected file for embedded credentials before conversion. The existing |
| 408 | 4,096-file / 64 MiB aggregate bundle limit applies. |
| 409 | |
| 410 | The converter rejects shell launchers, Node flags, extra arguments, non-Node |
| 411 | interpreters, literal environment values, and loader-changing environment |
| 412 | names. Stateful servers that write into their working directory, depend on the |
| 413 | live workspace, or import files outside the package need a manual native port. |
| 414 | Copying files does not statically verify JavaScript import closure or sandbox |
| 415 | arbitrary code. Local MCP processes run with host-user authority; their network |
| 416 | and filesystem access are not restricted by the remote endpoint host list. |
| 417 | The same native install, capability review, hash-bound trust, and enable steps |
| 418 | are required before Codewhale launches the server. This adds a packaged Node |
| 419 | MCP subset; it does not execute DSH/Cordis plugin modules. |
| 420 | |
| 421 | ### Review the result |
| 422 | |
| 423 | Both examples preserve disabled servers and use a placeholder endpoint. Replace |
| 424 | the endpoint and change the source's enablement flag before reconverting when |
| 425 | you are ready to connect. The output directory must be new, with an existing |
| 426 | parent. Existing output is refused; rejected input leaves no output bundle. |
| 427 | |
| 428 | Add `--skill ./my-skill` for an explicitly selected directory containing |
| 429 | `SKILL.md`, or `--skill ./my-skill.md` for a single file; repeat the option for |
| 430 | more skills. `--config` is optional for a skills-only conversion. Skills require |
| 431 | `name` and `description` frontmatter. `disable-model-invocation: true` becomes |
| 432 | native `invocation: explicit-only`. Informational `license`, `compatibility`, |
| 433 | and `metadata` fields are retained in `SOURCE_SKILL_METADATA.json` companion |
| 434 | data. Companion files from selected skill directories are copied as data; |
| 435 | review them and the instructions before loading the skill. |
| 436 | |
| 437 | Only exact OpenCode header references such as `{env:MCP_TOKEN}` become native |
| 438 | `env_headers`; the converter never reads the variable's value. Literal headers, |
| 439 | DSH header expressions, and URL file/environment substitution are refused. |
| 440 | Configured timeouts must be whole seconds expressed in milliseconds, from |
| 441 | `1000` through `3600000`. Omitted timeouts use Codewhale's defaults. |
| 442 | |
| 443 | Executable foreign plugins and hooks, other stdio launchers, automatic OAuth, |
| 444 | configuration JavaScript, |
| 445 | YAML aliases/tags, `__jsExpr`, and unsupported skill runtime fields (including |
| 446 | `user-invocable: false`) require a manual port. Conversion does not reproduce |
| 447 | another client's runtime or bypass Codewhale's credential and sandbox rules. |
| 448 | |
| 449 | Read the generated `CONVERSION.md`, `CONVERSION.json` for bundles, `plugin.json`, `mcp.json` when present, and |
| 450 | all selected skill and MCP source files. Then use `/plugin install ./migrated-opencode` (or the |
| 451 | DSH output path), `/plugin validate <name>`, and the same hash-bound trust and |
| 452 | enable flow above. Conversion alone proves neither connectivity nor runtime |
| 453 | compatibility; the output is not installed, trusted, or enabled. |
| 454 | |
| 455 | Source audit, 2026-09-08: OpenCode's [v1 MCP documentation](https://github.com/anomalyco/opencode/blob/d6855b6b47a8433462ac6aeeba882ccf734cb7f1/packages/web/src/content/docs/mcp-servers.mdx) |
| 456 | and [v2 MCP schema](https://github.com/anomalyco/opencode/blob/d6855b6b47a8433462ac6aeeba882ccf734cb7f1/packages/core/src/config/mcp.ts) |
| 457 | at `d6855b6b47`, and DSH's [MCP client reference](https://github.com/deepseek-ai/deepseek-harness/blob/c389f96bf3a9b6807cb71ed6bdad5849be0df6d8/packages/mcp/mcp-client/README.md) |
| 458 | at `c389f96bf3`. Bundle patch semantics were rechecked against DSH |
| 459 | [`00102833df`](https://github.com/deepseek-ai/deepseek-harness/tree/00102833dfaee1da9f48a3a8eae9d34005a75218) |
| 460 | (`0.1.7-alpha.2`); `scripts/fixtures/dsh-web-app` retains its real five-file package |
| 461 | as pinned test data, not as an importable native plugin. The Rust tests in |
| 462 | `crates/tui/src/plugins/install/dsh_tests.rs` load that package through the native |
| 463 | importer and assert that no row is promoted to a converted component; they run in |
| 464 | the workspace `Test` job. The CI `Plugin conversion` job runs only |
| 465 | `scripts/test_convert_plugin.py` (the OpenCode and static DSH MCP-entry converter) |
| 466 | with Python/PyYAML and synthetic Node fixtures, and the script's `--bundle` path |
| 467 | refuses and points to `/plugin import dsh`. Upstream supports more than this |
| 468 | deliberately bounded converter. |
| 469 | |
| 470 | ## Community context |
| 471 | |
| 472 | This guide responds to [giancarlocp's request for plugin authoring guidance |
| 473 | and OpenCode conversion in discussion #5827](https://github.com/codewhale-hq/Codewhale/discussions/5827). |
| 474 | The Chinese companion follows the documentation work requested by |
| 475 | [SparkofSpike in issue #5482](https://github.com/codewhale-hq/Codewhale/issues/5482). |
| 476 |