993 lines
23 KiB
Markdown
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)
|