443 lines
11 KiB
Markdown
443 lines
11 KiB
Markdown
# OpenCode Plugin Development Reference
|
|
|
|
## Overview
|
|
|
|
OpenCode plugins are JavaScript/TypeScript modules that extend OpenCode's functionality by hooking into various events and customizing behavior. Plugins can add custom tools, modify LLM parameters, handle authentication, intercept tool execution, and respond to system events.
|
|
|
|
## Plugin Locations
|
|
|
|
Plugins are automatically loaded from:
|
|
1. **Project-local**: `.opencode/plugin/` directory in your project
|
|
2. **Global**: `~/.config/opencode/plugin/` directory
|
|
|
|
**Note**: Local plugin files are auto-discovered and do NOT need to be listed in `opencode.jsonc`'s `plugin` array. The `plugin` array is only for npm package plugins.
|
|
|
|
## Basic Plugin Structure
|
|
|
|
### File Format
|
|
|
|
Plugins are `.js` or `.ts` files that export one or more plugin functions:
|
|
|
|
```javascript
|
|
export const MyPlugin = async ({ project, client, $, directory, worktree }) => {
|
|
// Initialization code here
|
|
console.log("Plugin initialized");
|
|
|
|
return {
|
|
// Hook implementations
|
|
};
|
|
};
|
|
```
|
|
|
|
### Plugin Context (Input Parameters)
|
|
|
|
Every plugin function receives a context object with:
|
|
|
|
| Parameter | Type | Description |
|
|
|-----------|------|-------------|
|
|
| `project` | `Project` | Current project information |
|
|
| `directory` | `string` | Current working directory |
|
|
| `worktree` | `string` | Git worktree path |
|
|
| `client` | `OpencodeClient` | OpenCode SDK client for API access |
|
|
| `$` | `BunShell` | Bun's shell API for executing commands |
|
|
|
|
### Plugin Return Value (Hooks)
|
|
|
|
Plugins return an object containing hook implementations. All hooks are optional.
|
|
|
|
## Available Hooks
|
|
|
|
### 1. `event` Hook
|
|
|
|
Respond to OpenCode system events.
|
|
|
|
```javascript
|
|
event: async ({ event }) => {
|
|
if (event.type === "session.idle") {
|
|
// OpenCode is waiting for user input
|
|
}
|
|
}
|
|
```
|
|
|
|
**Common Event Types:**
|
|
- `session.idle` - Session is waiting for user input
|
|
- `session.start` - Session has started
|
|
- `session.end` - Session has ended
|
|
- Additional events available via SDK
|
|
|
|
### 2. `config` Hook
|
|
|
|
React to configuration changes.
|
|
|
|
```javascript
|
|
config: async (config) => {
|
|
console.log("Config:", config.model, config.theme);
|
|
}
|
|
```
|
|
|
|
### 3. `tool` Hook
|
|
|
|
Add custom tools that the LLM can call.
|
|
|
|
```javascript
|
|
import { tool } from "@opencode-ai/plugin";
|
|
|
|
return {
|
|
tool: {
|
|
mytool: tool({
|
|
description: "Description shown to LLM",
|
|
args: {
|
|
query: tool.schema.string().describe("Query parameter"),
|
|
count: tool.schema.number().optional().describe("Optional count")
|
|
},
|
|
async execute(args, context) {
|
|
// context contains: { agent, sessionID, messageID }
|
|
return `Result: ${args.query}`;
|
|
}
|
|
})
|
|
}
|
|
};
|
|
```
|
|
|
|
**Tool Schema Types** (using Zod):
|
|
- `tool.schema.string()`
|
|
- `tool.schema.number()`
|
|
- `tool.schema.boolean()`
|
|
- `tool.schema.object({ ... })`
|
|
- `tool.schema.array(...)`
|
|
- `.optional()` - Make parameter optional
|
|
- `.describe("...")` - Add description for LLM
|
|
|
|
### 4. `auth` Hook
|
|
|
|
Provide custom authentication methods.
|
|
|
|
```javascript
|
|
auth: {
|
|
provider: "my-service",
|
|
methods: [
|
|
{
|
|
type: "api",
|
|
label: "API Key"
|
|
},
|
|
{
|
|
type: "oauth",
|
|
label: "OAuth Login",
|
|
authorize: async () => ({
|
|
url: "https://...",
|
|
instructions: "Login instructions",
|
|
method: "auto",
|
|
callback: async () => ({
|
|
type: "success",
|
|
key: "token"
|
|
})
|
|
})
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### 5. `chat.message` Hook
|
|
|
|
Called when a new user message is received.
|
|
|
|
```javascript
|
|
"chat.message": async ({}, output) => {
|
|
console.log("Message:", output.message.text);
|
|
console.log("Parts:", output.parts);
|
|
}
|
|
```
|
|
|
|
### 6. `chat.params` Hook
|
|
|
|
Modify parameters sent to the LLM.
|
|
|
|
```javascript
|
|
"chat.params": async (input, output) => {
|
|
// input: { model, provider, message }
|
|
output.temperature = 0.7;
|
|
output.topP = 0.95;
|
|
output.options = { /* custom options */ };
|
|
}
|
|
```
|
|
|
|
### 7. `permission.ask` Hook
|
|
|
|
Intercept permission requests.
|
|
|
|
```javascript
|
|
"permission.ask": async (permission, output) => {
|
|
if (permission.tool === "bash" && permission.args.command.includes("rm")) {
|
|
output.status = "deny"; // or "allow" or "ask"
|
|
}
|
|
}
|
|
```
|
|
|
|
### 8. `tool.execute.before` Hook
|
|
|
|
Intercept tool execution before it runs.
|
|
|
|
```javascript
|
|
"tool.execute.before": async (input, output) => {
|
|
// input: { tool, sessionID, callID }
|
|
// output: { args }
|
|
|
|
if (input.tool === "read" && output.args.filePath.includes(".env")) {
|
|
throw new Error("Cannot read .env files");
|
|
}
|
|
}
|
|
```
|
|
|
|
### 9. `tool.execute.after` Hook
|
|
|
|
Modify tool execution results.
|
|
|
|
```javascript
|
|
"tool.execute.after": async (input, output) => {
|
|
// input: { tool, sessionID, callID }
|
|
// output: { title, output, metadata }
|
|
|
|
console.log(`Tool ${input.tool} returned:`, output.output);
|
|
}
|
|
```
|
|
|
|
## Using the OpenCode SDK Client
|
|
|
|
The `client` parameter provides full API access:
|
|
|
|
### Common Operations
|
|
|
|
```javascript
|
|
// Get current project
|
|
const project = await client.project.current();
|
|
|
|
// List sessions
|
|
const sessions = await client.session.list();
|
|
|
|
// Read a file
|
|
const content = await client.file.read({
|
|
query: { path: "src/index.ts" }
|
|
});
|
|
|
|
// Search for text
|
|
const matches = await client.find.text({
|
|
query: { pattern: "TODO" }
|
|
});
|
|
|
|
// Show toast notification
|
|
await client.tui.showToast({
|
|
body: { message: "Task complete", variant: "success" }
|
|
});
|
|
|
|
// Send a prompt
|
|
await client.session.prompt({
|
|
path: { id: sessionID },
|
|
body: {
|
|
model: { providerID: "anthropic", modelID: "claude-3-5-sonnet-20241022" },
|
|
parts: [{ type: "text", text: "Hello!" }]
|
|
}
|
|
});
|
|
```
|
|
|
|
### Available SDK Methods
|
|
|
|
**App**: `app.log()`, `app.agents()`
|
|
**Project**: `project.list()`, `project.current()`
|
|
**Sessions**: `session.list()`, `session.get()`, `session.create()`, `session.delete()`, etc.
|
|
**Files**: `file.read()`, `file.status()`, `find.text()`, `find.files()`, `find.symbols()`
|
|
**TUI**: `tui.appendPrompt()`, `tui.showToast()`, `tui.openHelp()`, etc.
|
|
**Config**: `config.get()`, `config.providers()`
|
|
**Events**: `event.subscribe()` (for event streaming)
|
|
|
|
See full SDK documentation: https://opencode.ai/docs/sdk/
|
|
|
|
## Using Bun Shell (`$`)
|
|
|
|
Execute shell commands easily:
|
|
|
|
```javascript
|
|
// Simple command
|
|
await $`notify-send "Hello"`;
|
|
|
|
// Get output
|
|
const text = await $`git status`.text();
|
|
const json = await $`hyprctl clients -j`.json();
|
|
|
|
// Command with variables
|
|
const file = "test.txt";
|
|
await $`cat ${file}`;
|
|
|
|
// Array of arguments
|
|
const args = ["notify-send", "-u", "normal", "Title", "Body"];
|
|
await $`${args}`;
|
|
```
|
|
|
|
## Complete Example: Notification Plugin
|
|
|
|
```javascript
|
|
export const NotificationPlugin = async ({ project, client, $, directory, worktree }) => {
|
|
console.log("Notification plugin initialized");
|
|
|
|
return {
|
|
// Send notification when OpenCode is idle
|
|
event: async ({ event }) => {
|
|
if (event.type === "session.idle") {
|
|
const pid = process.pid;
|
|
const iconPath = `${process.env.HOME}/.config/opencode/icon.png`;
|
|
|
|
try {
|
|
// Get window info from Hyprland
|
|
const clientsJson = await $`hyprctl clients -j`.text();
|
|
const clients = JSON.parse(clientsJson);
|
|
const window = clients.find(c => c.pid === pid);
|
|
|
|
// Send notification with action
|
|
const result = await $`notify-send -a "OpenCode" -u normal -i ${iconPath} -A focus=Focus "OpenCode Ready" "Waiting for input in ${directory}"`.text();
|
|
|
|
// Handle action click
|
|
if (result.trim() === "focus" && window?.address) {
|
|
await $`hyprctl dispatch focuswindow address:${window.address}`;
|
|
}
|
|
} catch (error) {
|
|
console.error("Notification error:", error);
|
|
}
|
|
}
|
|
},
|
|
|
|
// Add custom tool
|
|
tool: {
|
|
notify: tool({
|
|
description: "Send a system notification",
|
|
args: {
|
|
message: tool.schema.string().describe("Notification message"),
|
|
urgency: tool.schema.enum(["low", "normal", "critical"]).optional()
|
|
},
|
|
async execute(args) {
|
|
await $`notify-send -u ${args.urgency || "normal"} "OpenCode" ${args.message}`;
|
|
return "Notification sent";
|
|
}
|
|
})
|
|
},
|
|
|
|
// Modify LLM parameters
|
|
"chat.params": async (input, output) => {
|
|
// Lower temperature for code-focused tasks
|
|
if (input.message.text?.includes("refactor") || input.message.text?.includes("bug")) {
|
|
output.temperature = 0.3;
|
|
}
|
|
},
|
|
|
|
// Prevent dangerous operations
|
|
"tool.execute.before": async (input, output) => {
|
|
if (input.tool === "bash" && output.args.command.includes("rm -rf")) {
|
|
throw new Error("Dangerous command blocked by plugin");
|
|
}
|
|
}
|
|
};
|
|
};
|
|
```
|
|
|
|
## TypeScript Support
|
|
|
|
For type-safe plugins, import types from the plugin package:
|
|
|
|
```typescript
|
|
import type { Plugin } from "@opencode-ai/plugin";
|
|
|
|
export const MyPlugin: Plugin = async (ctx) => {
|
|
return {
|
|
// Type-safe hook implementations
|
|
};
|
|
};
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
1. **Error Handling**: Always wrap risky operations in try-catch blocks
|
|
2. **Async Operations**: All hook functions should be async
|
|
3. **Console Logging**: Use `console.log()` for debugging - visible in OpenCode logs
|
|
4. **Resource Cleanup**: Clean up resources when plugins are reloaded
|
|
5. **Minimal Processing**: Keep hooks fast to avoid blocking OpenCode
|
|
6. **Security**: Validate inputs, especially in custom tools
|
|
7. **Documentation**: Add clear descriptions to custom tools for the LLM
|
|
|
|
## Common Use Cases
|
|
|
|
### System Integration
|
|
- Send desktop notifications (Linux, macOS, Windows)
|
|
- Integrate with window managers (Hyprland, i3, etc.)
|
|
- System clipboard operations
|
|
- File system watching
|
|
|
|
### Development Workflow
|
|
- Run tests on code changes
|
|
- Format code automatically
|
|
- Update documentation
|
|
- Git operations
|
|
|
|
### LLM Enhancement
|
|
- Add domain-specific tools
|
|
- Custom prompt preprocessing
|
|
- Response filtering
|
|
- Context augmentation
|
|
|
|
### Security & Compliance
|
|
- Block dangerous commands
|
|
- Prevent access to sensitive files
|
|
- Audit tool usage
|
|
- Rate limiting
|
|
|
|
## Debugging
|
|
|
|
1. **View Logs**: Run OpenCode with debug output
|
|
```bash
|
|
G_MESSAGES_DEBUG=all opencode
|
|
```
|
|
|
|
2. **Console Logging**: Use `console.log()`, `console.error()` in plugins
|
|
|
|
3. **Test Independently**: Test shell commands and SDK calls outside plugins first
|
|
|
|
4. **Hot Reload**: Plugins are reloaded when files change (in development mode)
|
|
|
|
## Related Documentation
|
|
|
|
- [OpenCode Plugins](https://opencode.ai/docs/plugins/)
|
|
- [OpenCode SDK](https://opencode.ai/docs/sdk/)
|
|
- [Custom Tools](https://opencode.ai/docs/custom-tools/)
|
|
- [Bun Shell API](https://bun.sh/docs/runtime/shell)
|
|
- [Zod Documentation](https://zod.dev/)
|
|
|
|
## Template Plugin
|
|
|
|
```javascript
|
|
/**
|
|
* Template Plugin
|
|
* Description: What this plugin does
|
|
*/
|
|
export const TemplatePlugin = async ({ project, client, $, directory, worktree }) => {
|
|
// Initialization
|
|
console.log("Template plugin initialized");
|
|
|
|
// You can store state here
|
|
let pluginState = {};
|
|
|
|
return {
|
|
// Implement the hooks you need
|
|
event: async ({ event }) => {
|
|
// Handle events
|
|
},
|
|
|
|
tool: {
|
|
// Add custom tools
|
|
},
|
|
|
|
"chat.params": async (input, output) => {
|
|
// Modify LLM parameters
|
|
},
|
|
|
|
// ... other hooks as needed
|
|
};
|
|
};
|
|
```
|