nixos/shared/linked-dotfiles/opencode/PLUGIN_REFERENCE.md
2025-10-22 15:49:16 -06:00

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
};
};
```