mirror of
https://github.com/sst/opencode.git
synced 2025-08-23 06:24:08 +00:00
This commit is contained in:
parent
1bac46612c
commit
f85d30c484
7 changed files with 67 additions and 316 deletions
10
.opencode/plugin/example.ts
Normal file
10
.opencode/plugin/example.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { Plugin } from "./index"
|
||||||
|
|
||||||
|
export const ExamplePlugin: Plugin = async ({ app, client, $ }) => {
|
||||||
|
return {
|
||||||
|
permission: {},
|
||||||
|
async "chat.params"(input, output) {
|
||||||
|
output.topP = 1
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://opencode.ai/config.json",
|
"$schema": "https://opencode.ai/config.json",
|
||||||
"plugin": ["./packages/plugin/src/example.ts"],
|
|
||||||
"mcp": {
|
"mcp": {
|
||||||
"context7": {
|
"context7": {
|
||||||
"type": "remote",
|
"type": "remote",
|
||||||
|
|
|
@ -93,6 +93,14 @@ export namespace Config {
|
||||||
throw new InvalidError({ path: item }, { cause: parsed.error })
|
throw new InvalidError({ path: item }, { cause: parsed.error })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result.plugin = result.plugin || []
|
||||||
|
result.plugin.push(
|
||||||
|
...[
|
||||||
|
...(await Filesystem.globUp("plugin/*.ts", Global.Path.config, Global.Path.config)),
|
||||||
|
...(await Filesystem.globUp(".opencode/plugin/*.ts", app.path.cwd, app.path.root)),
|
||||||
|
].map((x) => "file://" + x),
|
||||||
|
)
|
||||||
|
|
||||||
// Handle migration from autoshare to share field
|
// Handle migration from autoshare to share field
|
||||||
if (result.autoshare === true && !result.share) {
|
if (result.autoshare === true && !result.share) {
|
||||||
result.share = "auto"
|
result.share = "auto"
|
||||||
|
|
|
@ -5,7 +5,6 @@ import { Bus } from "../bus"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { createOpencodeClient } from "@opencode-ai/sdk"
|
import { createOpencodeClient } from "@opencode-ai/sdk"
|
||||||
import { Server } from "../server/server"
|
import { Server } from "../server/server"
|
||||||
import { pathOr } from "remeda"
|
|
||||||
import { BunProc } from "../bun"
|
import { BunProc } from "../bun"
|
||||||
|
|
||||||
export namespace Plugin {
|
export namespace Plugin {
|
||||||
|
@ -40,38 +39,14 @@ export namespace Plugin {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
type Path<T, Prefix extends string = ""> = T extends object
|
|
||||||
? {
|
|
||||||
[K in keyof T]: K extends string
|
|
||||||
? T[K] extends Function | undefined
|
|
||||||
? `${Prefix}${K}`
|
|
||||||
: Path<T[K], `${Prefix}${K}.`>
|
|
||||||
: never
|
|
||||||
}[keyof T]
|
|
||||||
: never
|
|
||||||
|
|
||||||
export type FunctionFromKey<T, P extends Path<T>> = P extends `${infer K}.${infer R}`
|
|
||||||
? K extends keyof T
|
|
||||||
? R extends Path<T[K]>
|
|
||||||
? FunctionFromKey<T[K], R>
|
|
||||||
: never
|
|
||||||
: never
|
|
||||||
: P extends keyof T
|
|
||||||
? T[P]
|
|
||||||
: never
|
|
||||||
|
|
||||||
export async function trigger<
|
export async function trigger<
|
||||||
Name extends Path<Required<Hooks>>,
|
Name extends keyof Required<Hooks>,
|
||||||
Input = Parameters<FunctionFromKey<Required<Hooks>, Name>>[0],
|
Input = Parameters<Required<Hooks>[Name]>[0],
|
||||||
Output = Parameters<FunctionFromKey<Required<Hooks>, Name>>[1],
|
Output = Parameters<Required<Hooks>[Name]>[1],
|
||||||
>(fn: Name, input: Input, output: Output): Promise<Output> {
|
>(name: Name, input: Input, output: Output): Promise<Output> {
|
||||||
if (!fn) return output
|
if (!name) return output
|
||||||
const path = fn.split(".")
|
|
||||||
for (const hook of await state().then((x) => x.hooks)) {
|
for (const hook of await state().then((x) => x.hooks)) {
|
||||||
// @ts-expect-error if you feel adventurous, please fix the typing, make sure to bump the try-counter if you
|
const fn = hook[name]
|
||||||
// give up.
|
|
||||||
// try-counter: 2
|
|
||||||
const fn = pathOr(hook, path, undefined)
|
|
||||||
if (!fn) continue
|
if (!fn) continue
|
||||||
// @ts-expect-error if you feel adventurous, please fix the typing, make sure to bump the try-counter if you
|
// @ts-expect-error if you feel adventurous, please fix the typing, make sure to bump the try-counter if you
|
||||||
// give up.
|
// give up.
|
||||||
|
|
|
@ -3,12 +3,8 @@ import { Plugin } from "./index"
|
||||||
export const ExamplePlugin: Plugin = async ({ app, client, $ }) => {
|
export const ExamplePlugin: Plugin = async ({ app, client, $ }) => {
|
||||||
return {
|
return {
|
||||||
permission: {},
|
permission: {},
|
||||||
tool: {
|
async "chat.params"(input, output) {
|
||||||
execute: {
|
output.topP = 1
|
||||||
async before(input, output) {
|
|
||||||
console.log("before", input, output)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,40 +10,23 @@ export type Plugin = (input: PluginInput) => Promise<Hooks>
|
||||||
|
|
||||||
export interface Hooks {
|
export interface Hooks {
|
||||||
event?: (input: { event: Event }) => Promise<void>
|
event?: (input: { event: Event }) => Promise<void>
|
||||||
chat?: {
|
|
||||||
/**
|
/**
|
||||||
* Called when a new message is received
|
* Called when a new message is received
|
||||||
*/
|
*/
|
||||||
message?: (input: {}, output: { message: UserMessage; parts: Part[] }) => Promise<void>
|
"chat.message"?: (input: {}, output: { message: UserMessage; parts: Part[] }) => Promise<void>
|
||||||
/**
|
/**
|
||||||
* Modify parameters sent to LLM
|
* Modify parameters sent to LLM
|
||||||
*/
|
*/
|
||||||
params?: (
|
"chat.params"?: (
|
||||||
input: { model: Model; provider: Provider; message: UserMessage },
|
input: { model: Model; provider: Provider; message: UserMessage },
|
||||||
output: { temperature: number; topP: number },
|
output: { temperature: number; topP: number },
|
||||||
) => Promise<void>
|
) => Promise<void>
|
||||||
}
|
"permission.ask"?: (input: Permission, output: { status: "ask" | "deny" | "allow" }) => Promise<void>
|
||||||
permission?: {
|
"tool.execute.before"?: (
|
||||||
/**
|
|
||||||
* Called when a permission is asked
|
|
||||||
*/
|
|
||||||
ask?: (input: Permission, output: { status: "ask" | "deny" | "allow" }) => Promise<void>
|
|
||||||
}
|
|
||||||
tool?: {
|
|
||||||
execute?: {
|
|
||||||
/**
|
|
||||||
* Called before a tool is executed
|
|
||||||
*/
|
|
||||||
before?: (
|
|
||||||
input: { tool: string; sessionID: string; callID: string },
|
input: { tool: string; sessionID: string; callID: string },
|
||||||
output: {
|
output: { args: any },
|
||||||
args: any
|
|
||||||
},
|
|
||||||
) => Promise<void>
|
) => Promise<void>
|
||||||
/**
|
"tool.execute.after"?: (
|
||||||
* Called after a tool is executed
|
|
||||||
*/
|
|
||||||
after?: (
|
|
||||||
input: { tool: string; sessionID: string; callID: string },
|
input: { tool: string; sessionID: string; callID: string },
|
||||||
output: {
|
output: {
|
||||||
title: string
|
title: string
|
||||||
|
@ -51,6 +34,4 @@ export interface Hooks {
|
||||||
metadata: any
|
metadata: any
|
||||||
},
|
},
|
||||||
) => Promise<void>
|
) => Promise<void>
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,31 +7,16 @@ Plugins allow you to extend opencode's functionality by hooking into various eve
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
Plugins are configured in your `opencode.json` file using the `plugin` array. Each entry should be a path to a plugin module.
|
|
||||||
|
|
||||||
```json title="opencode.json"
|
|
||||||
{
|
|
||||||
"$schema": "https://opencode.ai/config.json",
|
|
||||||
"plugin": ["./my-plugin.js", "../shared/company-plugin.js", "/absolute/path/to/plugin.js"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Paths can be:
|
|
||||||
|
|
||||||
- **Relative paths** - Resolved from the directory containing the config file
|
|
||||||
- **Absolute paths** - Used as-is
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Creating a Plugin
|
## Creating a Plugin
|
||||||
|
|
||||||
A plugin is a JavaScript/TypeScript module that exports one or more plugin functions. Each function receives a context object and returns a hooks object.
|
A plugin is a JavaScript/TypeScript module that exports one or more plugin
|
||||||
|
functions. Each function receives a context object and returns a hooks object.
|
||||||
|
They are loaded from the `.opencode/plugin` directory either in your proejct or
|
||||||
|
globally in `~/.config/opencode/plugin`.
|
||||||
|
|
||||||
### Basic Structure
|
### Basic Structure
|
||||||
|
|
||||||
```typescript title="my-plugin.js"
|
```typescript title=".opencode/plugin/example.js"
|
||||||
export const MyPlugin = async ({ app, client, $ }) => {
|
export const MyPlugin = async ({ app, client, $ }) => {
|
||||||
console.log("Plugin initialized!")
|
console.log("Plugin initialized!")
|
||||||
|
|
||||||
|
@ -63,52 +48,18 @@ export const MyPlugin: Plugin = async ({ app, client, $ }) => {
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Available Hooks
|
|
||||||
|
|
||||||
Plugins can implement various hooks to respond to opencode events:
|
|
||||||
|
|
||||||
### permission
|
|
||||||
|
|
||||||
Control permissions for various operations:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
export const SecurityPlugin = async ({ client }) => {
|
|
||||||
return {
|
|
||||||
permission: {
|
|
||||||
// Add permission logic here
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### event
|
|
||||||
|
|
||||||
Listen to all events in the opencode system:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
export const LoggingPlugin = async ({ client }) => {
|
|
||||||
return {
|
|
||||||
event: ({ event }) => {
|
|
||||||
console.log("Event occurred:", event)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
### Notification Plugin
|
### Notification Plugin
|
||||||
|
|
||||||
Send notifications when certain events occur:
|
Send notifications when certain events occur:
|
||||||
|
|
||||||
```javascript title="notification-plugin.js"
|
```javascript title=".opencode/plugin/notification.js"
|
||||||
export const NotificationPlugin = async ({ client, $ }) => {
|
export const NotificationPlugin = async ({ client, $ }) => {
|
||||||
return {
|
return {
|
||||||
event: async ({ event }) => {
|
event: async ({ event }) => {
|
||||||
// Send notification on session completion
|
// Send notification on session completion
|
||||||
if (event.type === "session.completed") {
|
if (event.type === "session.idle") {
|
||||||
await $`osascript -e 'display notification "Session completed!" with title "opencode"'`
|
await $`osascript -e 'display notification "Session completed!" with title "opencode"'`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -116,191 +67,22 @@ export const NotificationPlugin = async ({ client, $ }) => {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Custom Commands Plugin
|
### .env Protection
|
||||||
|
|
||||||
Add custom functionality that can be triggered by the AI:
|
|
||||||
|
|
||||||
```javascript title="custom-commands.js"
|
|
||||||
export const CustomCommands = async ({ client }) => {
|
|
||||||
return {
|
|
||||||
event: async ({ event }) => {
|
|
||||||
if (event.type === "message" && event.content.includes("/deploy")) {
|
|
||||||
// Trigger deployment logic
|
|
||||||
console.log("Deploying application...")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Integration Plugin
|
|
||||||
|
|
||||||
Integrate with external services:
|
Integrate with external services:
|
||||||
|
|
||||||
```javascript title="slack-integration.js"
|
```javascript title=".opencode/plugin/slack.js"
|
||||||
export const SlackIntegration = async ({ client, $ }) => {
|
export const EnvProtection = async ({ client, $ }) => {
|
||||||
const webhookUrl = process.env.SLACK_WEBHOOK_URL
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
event: async ({ event }) => {
|
tool: {
|
||||||
if (event.type === "error") {
|
execute: {
|
||||||
// Send error to Slack
|
before: async (input, output) => {
|
||||||
await fetch(webhookUrl, {
|
if (input.tool === "read" && output.args.filePath.includes(".env")) {
|
||||||
method: "POST",
|
throw new Error("Do not read .env files")
|
||||||
headers: { "Content-Type": "application/json" },
|
}
|
||||||
body: JSON.stringify({
|
}
|
||||||
text: `opencode error: ${event.message}`,
|
}
|
||||||
}),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Plugin Development Tips
|
|
||||||
|
|
||||||
1. **Error Handling**: Always handle errors gracefully to avoid crashing opencode
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
event: async ({ event }) => {
|
|
||||||
try {
|
|
||||||
// Your logic here
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Plugin error:", error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Performance**: Keep plugin operations lightweight and async where possible
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
event: async ({ event }) => {
|
|
||||||
// Don't block - use async operations
|
|
||||||
setImmediate(() => {
|
|
||||||
// Heavy processing here
|
|
||||||
})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Environment Variables**: Use environment variables for configuration
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const apiKey = process.env.MY_PLUGIN_API_KEY
|
|
||||||
if (!apiKey) {
|
|
||||||
console.warn("MY_PLUGIN_API_KEY not set")
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Multiple Exports**: You can export multiple plugins from one file
|
|
||||||
```javascript
|
|
||||||
export const PluginOne = async (context) => {
|
|
||||||
/* ... */
|
|
||||||
}
|
|
||||||
export const PluginTwo = async (context) => {
|
|
||||||
/* ... */
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Advanced Usage
|
|
||||||
|
|
||||||
### Using the SDK Client
|
|
||||||
|
|
||||||
The `client` parameter is a full opencode SDK client that can interact with the AI:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
export const AIAssistantPlugin = async ({ client }) => {
|
|
||||||
return {
|
|
||||||
event: async ({ event }) => {
|
|
||||||
if (event.type === "file.created") {
|
|
||||||
// Ask AI to review the new file
|
|
||||||
const response = await client.messages.create({
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
role: "user",
|
|
||||||
content: `Review this new file: ${event.path}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
console.log("AI Review:", response)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Accessing Application State
|
|
||||||
|
|
||||||
The `app` parameter provides access to the opencode application instance:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
export const StatePlugin = async ({ app }) => {
|
|
||||||
return {
|
|
||||||
event: async ({ event }) => {
|
|
||||||
// Access application state and configuration
|
|
||||||
const currentPath = app.path.cwd
|
|
||||||
console.log("Working directory:", currentPath)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Debugging Plugins
|
|
||||||
|
|
||||||
To debug your plugins:
|
|
||||||
|
|
||||||
1. **Console Logging**: Use `console.log()` to output debug information
|
|
||||||
2. **Error Boundaries**: Wrap hook implementations in try-catch blocks
|
|
||||||
3. **Development Mode**: Test plugins in a separate opencode instance first
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
export const DebugPlugin = async (context) => {
|
|
||||||
console.log("Plugin loaded with context:", Object.keys(context))
|
|
||||||
|
|
||||||
return {
|
|
||||||
event: ({ event }) => {
|
|
||||||
console.log(`[${new Date().toISOString()}] Event:`, event.type)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
1. **Namespace Your Plugins**: Use descriptive names to avoid conflicts
|
|
||||||
2. **Document Your Hooks**: Add comments explaining what each hook does
|
|
||||||
3. **Version Control**: Keep plugins in version control with your project
|
|
||||||
4. **Test Thoroughly**: Test plugins with various opencode operations
|
|
||||||
5. **Handle Cleanup**: Clean up resources when appropriate
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Good example with best practices
|
|
||||||
export const CompanyStandardsPlugin = async ({ client, $ }) => {
|
|
||||||
// Initialize resources
|
|
||||||
const config = await loadConfig()
|
|
||||||
|
|
||||||
return {
|
|
||||||
event: async ({ event }) => {
|
|
||||||
try {
|
|
||||||
// Well-documented hook logic
|
|
||||||
if (event.type === "code.generated") {
|
|
||||||
// Enforce company coding standards
|
|
||||||
await enforceStandards(event.code)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Graceful error handling
|
|
||||||
console.error("Standards check failed:", error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue