matrix-plugin/PLUGIN-DOCS-v2.md

993 lines
23 KiB
Markdown

# OpenCode V2 Plugin Documentation
> Сгенерировано: 2026-09-25 | Версия OpenCode: v2.x | @opencode/plugin: v2.x
---
## Table of Contents
1. [Overview](#overview)
2. [Plugin Loading](#plugin-loading)
3. [Plugin Structure](#plugin-structure)
4. [Plugin API (Promise API)](#plugin-api-promise-api)
5. [Context Object](#context-object)
6. [Hooks](#hooks)
7. [Custom Tools](#custom-tools)
8. [Events](#events)
9. [Configuration](#configuration)
10. [External Dependencies](#external-dependencies)
11. [Local Plugin Pitfalls](#local-plugin-pitfalls)
12. [Debugging](#debugging)
13. [Complete Examples](#complete-examples)
---
## Overview
OpenCode V2 plugins extend the agent by adding custom tools, hooks, event handlers, and integrations. A plugin is a TypeScript/JavaScript module that uses `Plugin.define()` from `@opencode/plugin` to register capabilities.
### Key Concepts
- **Plugin**: A module that calls `Plugin.define({ id, setup(ctx) })` and exports it as default.
- **setup(ctx)**: An async function that receives a context object and returns a cleanup function.
- **ctx**: The plugin context providing access to all OpenCode domains (session, tool, event, storage, etc.).
- **Hooks**: Functions registered on domain-specific APIs (e.g., `ctx.session.hook()`, `ctx.tool.hook()`).
- **Events**: Subscribable event streams via `ctx.event.subscribe()`.
- **Custom Tools**: Tools defined with `tool()` helper and registered via `ctx.tool.transform()`.
---
## Plugin Loading
### Discovery Locations
OpenCode discovers plugins from multiple sources, loaded in this order:
| Source | Path | Auto-discovered |
|--------|------|-----------------|
| Global config | `~/.config/opencode/opencode.json` | N/A (explicit) |
| Project config | `opencode.json` | N/A (explicit) |
| Global plugin dir | `~/.config/opencode/plugins/` | Yes |
| Project plugin dir | `.opencode/plugins/` | Yes |
### Discovery Rules
**Global plugin directory** (`~/.config/opencode/plugins/`):
- Direct `.ts` and `.js` files are loaded as plugins
- Immediate subdirectories containing a `package.json` are loaded as plugin packages
- Nested directories are NOT auto-discovered
**Project plugin directory** (`.opencode/plugins/`):
- Same rules as global
### Explicit Configuration
Plugins can also be explicitly listed in `opencode.json`:
```jsonc
{
"plugins": [
// npm package
"opencode-acme-plugin",
// npm package with version
"opencode-acme-plugin@1.2.0",
// scoped npm package
"@acme/opencode-plugin",
// local file path (relative to config file)
"./plugins/local",
// absolute file path
"/home/user/plugin.ts",
// file:// URL
"file:///home/user/plugins/local",
// package with options
{
"package": "@acme/opencode-plugin",
"options": { "agent": "reviewer", "strict": true }
}
]
}
```
### Plugin Control
Prefix an ID or wildcard with `-` to disable:
```jsonc
{
"plugins": [
"*", // enable all
"-opencode.provider.*", // disable all providers
"opencode.provider.openai", // re-enable OpenAI
"-acme.reviewer" // disable specific plugin
]
}
```
Two built-in plugins always ignore removals: `opencode.config.policy` and `opencode.provider.opencode`.
---
## Plugin Structure
### Minimal Plugin
```typescript
// ~/.config/opencode/plugins/my-plugin.ts
export default {
id: "my-plugin",
async setup(ctx) {
console.log("Plugin loaded!")
// cleanup function (optional)
return () => {
console.log("Plugin cleaning up")
}
},
}
```
### Plugin with Package.json (Directory Plugin)
```
~/.config/opencode/plugins/my-plugin/
├── package.json # Required: defines name, dependencies
├── index.ts # Entry point (or main field in package.json)
├── helper.ts # Additional modules
└── node_modules/ # Dependencies (manually installed)
```
**package.json**:
```json
{
"name": "my-opencode-plugin",
"version": "1.0.0",
"main": "index.ts",
"dependencies": {
"@opencode/plugin": "^2.0.16",
"some-npm-package": "^1.0.0"
}
}
```
**index.ts**:
```typescript
import { Plugin, tool } from "@opencode/plugin"
export default Plugin.define({
id: "my-opencode-plugin",
async setup(ctx) {
// Plugin initialization
const result = await someNpmPackage.doSomething()
console.log(`Result: ${result}`)
return {
// hooks
}
},
})
```
---
## Plugin API (Promise API)
The Promise API is the primary way to write V2 plugins:
```typescript
import { Plugin } from "@opencode/plugin"
export default Plugin.define({
id: "my-plugin",
async setup(ctx) {
// Register hooks, tools, etc.
// ...
// Return cleanup function (optional)
return () => {
// Release resources
}
},
})
```
### Plugin.define() Signature
```typescript
Plugin.define({
id: string, // Unique plugin identifier (required)
async setup(ctx): PluginContext // Setup function (required)
}): PluginDefinition
```
### Effect API (Alternative)
For plugins that need Effect-TS:
```typescript
import { Plugin } from "@opencode/plugin/effect"
```
---
## Context Object
The `ctx` parameter in `setup()` provides access to all OpenCode domains:
### Core Properties
| Property | Type | Description |
|----------|------|-------------|
| `ctx.location` | Location | Where this plugin was loaded (NOT where observed sessions are) |
| `ctx.location.directory` | string | Working directory of the plugin's location |
| `ctx.location.project` | ProjectInfo | Project info at plugin load time |
### Domain APIs
| Domain | Purpose |
|--------|---------|
| `ctx.app` | Application-level operations (logging, options) |
| `ctx.session` | Session management, prompts, hooks |
| `ctx.tool` | Tool hooks, transforms, custom tools |
| `ctx.event` | Event subscription |
| `ctx.storage` | Persistent key-value storage |
| `ctx.shell` | Shell command execution |
| `ctx.command` | Command transforms |
| `ctx.permission` | Permission hooks |
| `ctx.provider` | Provider transforms |
| `ctx.model` | Model transforms |
| `ctx.integration` | Integration/connection APIs |
| `ctx.mcp` | MCP server management |
| `ctx.skill` | Skill management |
| `ctx.agent` | Agent configuration |
| `ctx.vcs` | Version control operations |
| `ctx.websearch` | Web search |
| `ctx.rpc` | RPC calls |
| `ctx.reference` | Reference management |
| `ctx.worktree` | Git worktree operations |
### Important: ctx.location vs Observed Events
> **CRITICAL**: `ctx.location` describes where the plugin was loaded, NOT the location of observed sessions or events. Never infer a session's location from `ctx.location`. Use event/session data instead.
---
## Hooks
### Session Hooks
```typescript
// Prompt hook - called before a prompt is sent
ctx.session.hook("prompt", (event) => {
// Modify event before sending
event.metadata = { ...event.metadata, plugin: "my-plugin" }
})
// Context hook - modify context sent to LLM
ctx.session.hook("context", (input, output) => {
output.system.push(`<custom-context>Rules go here</custom-context>`)
})
// Compaction hook - customize context preservation
ctx.session.hook("compaction", (input, output) => {
output.context.push(`## Preserved state\n- Current task: ...`)
})
```
### Tool Hooks
```typescript
// Before tool execution
ctx.tool.hook("execute.before", (input, output) => {
if (input.tool === "bash" && output.args.command.includes("rm -rf")) {
throw new Error("Dangerous command blocked")
}
})
// After tool execution
ctx.tool.hook("execute.after", (input) => {
console.log(`Tool ${input.tool} completed`)
})
```
### Shell Hooks
```typescript
// Modify shell creation
ctx.shell.hook("create.before", (input, output) => {
output.env.MY_API_KEY = "secret"
output.env.PROJECT_ROOT = input.cwd
})
```
### Permission Hooks
```typescript
ctx.permission.hook("evaluate", (permission, output) => {
if (permission.type === "read_file") {
output.status = "allow"
}
})
```
### Event Subscription
```typescript
const controller = new AbortController()
void (async () => {
for await (const event of ctx.event.subscribe({ signal: controller.signal })) {
if (event.type === "session.created") {
console.log(`New session: ${event.sessionID}`)
}
if (event.type === "session.idle") {
console.log("Session idle")
}
if (event.type === "message.updated") {
console.log("Message updated")
}
}
})()
// Cleanup
return () => {
controller.abort()
}
```
### Available Events
| Event Type | Description |
|------------|-------------|
| `session.created` | New session created |
| `session.updated` | Session updated |
| `session.deleted` | Session deleted |
| `session.error` | Session error |
| `session.idle` | Session became idle |
| `session.compacted` | Session compacted |
| `message.updated` | Message updated |
| `message.removed` | Message removed |
| `file.edited` | File edited |
| `permission.asked` | Permission requested |
| `permission.replied` | Permission responded |
| `server.connected` | Server connected |
| `tool.execute.before` | Tool about to execute |
| `tool.execute.after` | Tool finished executing |
---
## Custom Tools
```typescript
import { tool } from "@opencode/plugin"
// In setup():
ctx.tool.transform((editor) => {
editor.add({
name: "my-tool",
description: "Does something useful",
async execute({ sessionID }, delivery) {
// Tool implementation
return "Result"
},
})
})
```
### Using the `tool()` Helper
```typescript
import { tool } from "@opencode/plugin"
const myTool = tool({
description: "Search the web",
args: {
query: tool.schema.string().describe("Search query"),
maxResults: tool.schema.number().optional().describe("Max results"),
},
async execute(args, context) {
const { sessionID, agent } = context
// Implementation using ctx
return `Results for: ${args.query}`
},
})
```
---
## Events
### Subscribing to Events
```typescript
const eventController = new AbortController()
void (async () => {
for await (const event of ctx.event.subscribe({ signal: eventController.signal })) {
// Handle event
console.log(`Event: ${event.type}`)
}
})()
```
### Event Update Types
When handling `session.updated` events, the `event.update` field can be:
| Type | Description |
|------|-------------|
| `agent_message_chunk` | Streaming text from the agent |
| `tool_call` | Tool invocation |
| `tool_call_update` | Tool progress/completion |
| `user_message_chunk` | User message fragment |
| `agent_thought_chunk` | Agent reasoning/thought |
---
## Configuration
### Plugin Options
Configure via `opencode.json`:
```jsonc
{
"plugins": [
{
"package": "./plugins/my-plugin",
"options": {
"apiKey": "secret",
"enabled": true
}
}
]
}
```
Access in plugin:
```typescript
export default Plugin.define({
id: "my-plugin",
async setup(ctx) {
const apiKey = ctx.options.apiKey
const enabled = ctx.options.enabled
},
})
```
### Structured Logging
```typescript
await ctx.app.log({
body: {
service: "my-plugin",
level: "info", // debug, info, warn, error
message: "Plugin initialized",
extra: { key: "value" },
},
})
```
---
## External Dependencies
### How Dependencies Work
OpenCode V2 does NOT automatically install dependencies for local plugins. You must manually install them.
### For Global Plugin Directory (`~/.config/opencode/plugins/`)
Dependencies must be installed in the config directory:
```json
// ~/.config/opencode/package.json
{
"dependencies": {
"@opencode/plugin": "^2.0.16",
"some-npm-package": "^1.0.0"
}
}
```
OpenCode runs `bun install` at startup for this file.
### For Local Plugin Package Directory
If your plugin is in a subdirectory with its own `package.json`:
```
~/.config/opencode/plugins/my-plugin/
├── package.json
├── index.ts
└── node_modules/ # <-- You must create this!
```
**You must manually run `bun install` (or `npm install`) inside the plugin directory:**
```bash
cd ~/.config/opencode/plugins/my-plugin
bun install
```
> **CRITICAL**: OpenCode does NOT automatically run `bun install` in plugin subdirectories. Dependencies in a plugin's own `package.json` will NOT be available unless you manually install them.
### For npm Package Plugins
Dependencies are installed automatically when the plugin is added via `opencode plugin add`.
---
## Local Plugin Pitfalls
### Pitfall 1: Dependencies Not Auto-Installed
**Problem**: Plugin has `package.json` with dependencies, but they're not available at runtime.
**Solution**: Run `bun install` in the plugin directory:
```bash
cd ~/.config/opencode/plugins/my-plugin
bun install
```
### Pitfall 2: Wrong Import Path
**Problem**: Using `@opencode-ai/plugin` (V1) instead of `@opencode/plugin` (V2).
**Solution**: Use the V2 import:
```typescript
// V2 (correct)
import { Plugin } from "@opencode/plugin"
// V1 (wrong for V2)
import type { Plugin } from "@opencode-ai/plugin"
```
### Pitfall 3: Mixing CommonJS and ESM
**Problem**: Using `require()` and `module.exports` in a TypeScript file loaded as ESM.
**Solution**: Use ES module syntax throughout:
```typescript
// Wrong - CommonJS in ESM context
const { MatrixClient } = require("matrix-bot-sdk")
module.exports = { MatrixBotClient }
// Correct - ES modules
import { MatrixClient } from "matrix-bot-sdk"
export class MatrixBotClient { ... }
```
### Pitfall 4: Plugin Not Loading (Silent Failure)
**Problem**: Plugin has a syntax error or import error, but no log is written.
**Solution**:
1. Check for TypeScript errors: `tsc --noEmit` in the plugin directory
2. Use `client.app.log()` for structured logging
3. Verify the plugin path is correct in `opencode.json`
4. Check that `@opencode/plugin` is installed in the dependency location
### Pitfall 5: Context Destructuring Error
**Problem**: Treating `ctx` as the client directly:
```typescript
// Wrong
export default Plugin.define({
id: "my-plugin",
async setup(client) {
await client.session.prompt(...) // FAILS: client.session.prompt doesn't exist
},
})
// Correct
export default Plugin.define({
id: "my-plugin",
async setup(ctx) {
await ctx.session.prompt(...) // Correct
},
})
```
### Pitfall 6: Hook Name Case Sensitivity
**Problem**: Using incorrect hook names.
**Solution**: Verify exact hook names from the API reference. Hook names are case-sensitive.
### Pitfall 7: Missing Cleanup
**Problem**: Event subscriptions or intervals not cleaned up on plugin unload.
**Solution**: Return a cleanup function:
```typescript
export default Plugin.define({
id: "my-plugin",
async setup(ctx) {
const interval = setInterval(() => { /* ... */ }, 60000)
const controller = new AbortController()
// Subscribe to events
void (async () => {
for await (const event of ctx.event.subscribe({ signal: controller.signal })) {
// handle
}
})()
// Cleanup
return () => {
controller.abort()
clearInterval(interval)
}
},
})
```
### Pitfall 8: Using ctx.location for Session Data
**Problem**: Inferring session location from `ctx.location`:
```typescript
// Wrong
const sessionDir = ctx.location.directory // This is where plugin was loaded!
// Correct
const sessionDir = event.directory // Use event/session data
```
---
## Debugging
### Checklist
1. **Plugin not loading?**
- Check for TypeScript errors: `tsc --noEmit`
- Check import paths are correct
- Verify dependencies are installed
- Check plugin path in `opencode.json`
2. **Hooks not firing?**
- Verify hook names match exactly (case-sensitive)
- Check hook registration syntax
3. **State not persisting?**
- Use session-keyed Maps, not global variables
- Use `ctx.storage` for persistent data
4. **`ctx.session.prompt()` failing?**
- Verify destructuring: `async setup(ctx)` not `async setup(client)`
- Check session ID is valid
### Logging
Use structured logging via `ctx.app.log()`:
```typescript
await ctx.app.log({
body: {
service: "my-plugin",
level: "info",
message: "Plugin initialized",
extra: { key: "value" },
},
})
```
Levels: `debug`, `info`, `warn`, `error`.
### Verbose Mode
Run OpenCode with `--verbose` for more detailed plugin loading output.
---
## Complete Examples
### Example 1: Simple Notification Plugin
```typescript
import { Plugin } from "@opencode/plugin"
export default Plugin.define({
id: "notification-plugin",
async setup(ctx) {
void (async () => {
for await (const event of ctx.event.subscribe({
signal: new AbortController().signal,
})) {
if (event.type === "session.idle") {
await ctx.app.log({
body: {
service: "notification",
level: "info",
message: "Session completed",
},
})
}
}
})()
return () => {}
},
})
```
### Example 2: Plugin with Matrix Integration
```typescript
import { Plugin } from "@opencode/plugin"
import { MatrixClient } from "matrix-bot-sdk"
import { marked } from "marked"
export default Plugin.define({
id: "matrix-bot",
async setup(ctx) {
const options = ctx.options
if (!options.homeserver) {
await ctx.app.log({
body: { service: "matrix", level: "warn", message: "Disabled (no homeserver)" },
})
return () => {}
}
const client = new MatrixClient(options.homeserver)
const sessions = new Map<string, any>()
// Login
if (options.accessToken) {
await client.startWithToken("m.login.token", {
user_id: options.userId,
access_token: options.accessToken,
})
} else {
await client.startWithPassword(options.userId, options.password)
}
const userId = await client.getUserId()
await ctx.app.log({
body: { service: "matrix", level: "info", message: `Started as ${userId}` },
})
// Listen for messages
client.on("room.message", async (roomId, event) => {
if (event.type !== "m.room.message" || event.content.msgtype !== "m.text") return
if (event.sender === userId) return
const text = event.content.body?.trim()
if (!text?.startsWith("!oc ")) return
const query = text.slice(4).trim()
const sessionId = roomId
// Send prompt
const result = await ctx.session.prompt({
sessionID: sessionId,
text: query,
})
// Send response
await client.sendText(roomId, result)
})
// Event subscription for response streaming
const controller = new AbortController()
void (async () => {
for await (const event of ctx.event.subscribe({ signal: controller.signal })) {
if (event.type === "session.updated" && event.update?.type === "agent_message_chunk") {
// Stream response to Matrix
}
}
})()
return () => {
controller.abort()
client.stop()
}
},
})
```
### Example 3: Plugin with Custom Tool
```typescript
import { Plugin, tool } from "@opencode/plugin"
export default Plugin.define({
id: "custom-tools",
async setup(ctx) {
ctx.tool.transform((editor) => {
editor.add({
name: "git-status",
description: "Show git status of the current project",
async execute(_, delivery) {
const result = await ctx.shell.execute("git status --porcelain")
return result.stdout
},
})
editor.add({
name: "search-files",
description: "Search for files matching a pattern",
async execute(args: { pattern: string }, delivery) {
const result = await ctx.shell.execute(`find . -name "${args.pattern}" -type f`)
return result.stdout
},
})
})
return () => {}
},
})
```
### Example 4: Plugin with Persistent Storage
```typescript
import { Plugin } from "@opencode/plugin"
export default Plugin.define({
id: "state-tracker",
async setup(ctx) {
// Initialize from storage
const version = await ctx.storage.get("state-tracker/version")
if (!version) {
await ctx.storage.set("state-tracker/version", "1.0.0")
await ctx.storage.set("state-tracker/started_at", new Date().toISOString())
}
// Track sessions
const sessions = new Map<string, { count: number }>()
const controller = new AbortController()
void (async () => {
for await (const event of ctx.event.subscribe({ signal: controller.signal })) {
if (event.type === "session.created" && event.sessionID) {
sessions.set(event.sessionID, { count: 0 })
}
if (event.type === "session.deleted" && event.sessionID) {
sessions.delete(event.sessionID)
}
}
})()
return () => {
controller.abort()
}
},
})
```
---
## Migration from V1 to V2
### Import Changes
| V1 | V2 |
|----|----|
| `import type { Plugin } from "@opencode-ai/plugin"` | `import { Plugin } from "@opencode/plugin"` |
| `export const MyPlugin = async (ctx) => ({ ...hooks })` | `export default Plugin.define({ id, async setup(ctx) { ... } })` |
### Hook Mapping
| V1 Hook | V2 API |
|---------|--------|
| `event: async ({ event }) => {}` | `ctx.event.subscribe()` |
| `"tool.execute.before"` | `ctx.tool.hook("execute.before", ...)` |
| `"tool.execute.after"` | `ctx.tool.hook("execute.after", ...)` |
| `"chat.message"` | `ctx.session.hook("prompt", ...)` |
| `"chat.params"` | `ctx.session.hook("context", ...)` |
| `permission.ask` | `ctx.permission.hook("evaluate", ...)` |
| `dispose` | `return cleanup` from setup |
| `config` | transforms on affected domains |
### Context Changes
| V1 | V2 |
|----|----|
| `ctx.client` | Domain APIs on `ctx` |
| `ctx.project` | `ctx.location.project` |
| `ctx.directory` | `ctx.location.directory` |
| `ctx.$` | Explicitly imported shell helpers |
---
## Quick Reference
### File: `~/.config/opencode/opencode.json`
```jsonc
{
"plugins": [
// npm package
"opencode-helicone-session",
// local directory
"./plugins/local",
// with options
{
"package": "./plugins/matrix-plugin",
"options": {
"homeserver": "https://matrix.org",
"userId": "@bot:matrix.org",
"password": "secret"
}
}
]
}
```
### File: `~/.config/opencode/plugins/my-plugin/package.json`
```json
{
"name": "my-opencode-plugin",
"version": "1.0.0",
"main": "index.ts",
"dependencies": {
"@opencode/plugin": "^2.0.16"
}
}
```
### File: `~/.config/opencode/plugins/my-plugin/index.ts`
```typescript
import { Plugin } from "@opencode/plugin"
export default Plugin.define({
id: "my-opencode-plugin",
async setup(ctx) {
// Initialization
await ctx.app.log({
body: { service: "my-plugin", level: "info", message: "Loaded" }
})
// Event subscription
const controller = new AbortController()
void (async () => {
for await (const event of ctx.event.subscribe({ signal: controller.signal })) {
if (event.type === "session.idle") {
await ctx.app.log({
body: { service: "my-plugin", level: "info", message: "Session idle" }
})
}
}
})()
return () => {
controller.abort()
}
},
})
```
### Install Dependencies
```bash
cd ~/.config/opencode/plugins/my-plugin
bun install
```
---
## References
- [Official OpenCode Plugin Docs](https://opencode.ai/v2/docs/plugins)
- [Plugin Source Code](packages/plugin/src/index.ts)
- [Tool Definition](packages/plugin/src/tool.ts)
- [Plugin Loader](packages/opencode/src/plugin/index.ts)
- [V1 to V2 Migration Guide](https://gist.github.com/yohi/0c3d8edea98d86e7f5c8d6c2d9f93d2d)
- [Plugin Development Reference](https://github.com/growwithsmc/opencode-plugin-dev)