summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2025-08-03 21:42:45 -0400
committerDax Raad <[email protected]>2025-08-03 21:43:58 -0400
commitf85d30c484f26c0656dd91ad5f9c5ed26c0a2fce (patch)
treeff8cc896fa4eee9da0c83ccbf0a49343c5590472 /packages
parent1bac46612cef3b383af2ce411ca2ad313d5c5ce0 (diff)
downloadopencode-f85d30c484f26c0656dd91ad5f9c5ed26c0a2fce.tar.gz
opencode-f85d30c484f26c0656dd91ad5f9c5ed26c0a2fce.zip
wip: plugins
Diffstat (limited to 'packages')
-rw-r--r--packages/opencode/src/config/config.ts8
-rw-r--r--packages/opencode/src/plugin/index.ts37
-rw-r--r--packages/plugin/src/example.ts8
-rw-r--r--packages/plugin/src/index.ts67
-rw-r--r--packages/web/src/content/docs/docs/plugins.mdx252
5 files changed, 57 insertions, 315 deletions
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index db71b87d1..b0bea5d32 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -93,6 +93,14 @@ export namespace Config {
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
if (result.autoshare === true && !result.share) {
result.share = "auto"
diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts
index 0fa26361d..3ffa30191 100644
--- a/packages/opencode/src/plugin/index.ts
+++ b/packages/opencode/src/plugin/index.ts
@@ -5,7 +5,6 @@ import { Bus } from "../bus"
import { Log } from "../util/log"
import { createOpencodeClient } from "@opencode-ai/sdk"
import { Server } from "../server/server"
-import { pathOr } from "remeda"
import { BunProc } from "../bun"
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<
- Name extends Path<Required<Hooks>>,
- Input = Parameters<FunctionFromKey<Required<Hooks>, Name>>[0],
- Output = Parameters<FunctionFromKey<Required<Hooks>, Name>>[1],
- >(fn: Name, input: Input, output: Output): Promise<Output> {
- if (!fn) return output
- const path = fn.split(".")
+ Name extends keyof Required<Hooks>,
+ Input = Parameters<Required<Hooks>[Name]>[0],
+ Output = Parameters<Required<Hooks>[Name]>[1],
+ >(name: Name, input: Input, output: Output): Promise<Output> {
+ if (!name) return output
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
- // give up.
- // try-counter: 2
- const fn = pathOr(hook, path, undefined)
+ const fn = hook[name]
if (!fn) continue
// @ts-expect-error if you feel adventurous, please fix the typing, make sure to bump the try-counter if you
// give up.
diff --git a/packages/plugin/src/example.ts b/packages/plugin/src/example.ts
index 4e9bc4b88..998108f0a 100644
--- a/packages/plugin/src/example.ts
+++ b/packages/plugin/src/example.ts
@@ -3,12 +3,8 @@ import { Plugin } from "./index"
export const ExamplePlugin: Plugin = async ({ app, client, $ }) => {
return {
permission: {},
- tool: {
- execute: {
- async before(input, output) {
- console.log("before", input, output)
- },
- },
+ async "chat.params"(input, output) {
+ output.topP = 1
},
}
}
diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts
index 9040fb517..be9822ee2 100644
--- a/packages/plugin/src/index.ts
+++ b/packages/plugin/src/index.ts
@@ -10,47 +10,28 @@ export type Plugin = (input: PluginInput) => Promise<Hooks>
export interface Hooks {
event?: (input: { event: Event }) => Promise<void>
- chat?: {
- /**
- * Called when a new message is received
- */
- message?: (input: {}, output: { message: UserMessage; parts: Part[] }) => Promise<void>
- /**
- * Modify parameters sent to LLM
- */
- params?: (
- input: { model: Model; provider: Provider; message: UserMessage },
- output: { temperature: number; topP: number },
- ) => Promise<void>
- }
- permission?: {
- /**
- * 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 },
- output: {
- args: any
- },
- ) => Promise<void>
- /**
- * Called after a tool is executed
- */
- after?: (
- input: { tool: string; sessionID: string; callID: string },
- output: {
- title: string
- output: string
- metadata: any
- },
- ) => Promise<void>
- }
- }
+ /**
+ * Called when a new message is received
+ */
+ "chat.message"?: (input: {}, output: { message: UserMessage; parts: Part[] }) => Promise<void>
+ /**
+ * Modify parameters sent to LLM
+ */
+ "chat.params"?: (
+ input: { model: Model; provider: Provider; message: UserMessage },
+ output: { temperature: number; topP: number },
+ ) => Promise<void>
+ "permission.ask"?: (input: Permission, output: { status: "ask" | "deny" | "allow" }) => Promise<void>
+ "tool.execute.before"?: (
+ input: { tool: string; sessionID: string; callID: string },
+ output: { args: any },
+ ) => Promise<void>
+ "tool.execute.after"?: (
+ input: { tool: string; sessionID: string; callID: string },
+ output: {
+ title: string
+ output: string
+ metadata: any
+ },
+ ) => Promise<void>
}
diff --git a/packages/web/src/content/docs/docs/plugins.mdx b/packages/web/src/content/docs/docs/plugins.mdx
index a9b849fc6..bee2785f0 100644
--- a/packages/web/src/content/docs/docs/plugins.mdx
+++ b/packages/web/src/content/docs/docs/plugins.mdx
@@ -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
-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
-```typescript title="my-plugin.js"
+```typescript title=".opencode/plugin/example.js"
export const MyPlugin = async ({ app, client, $ }) => {
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
### Notification Plugin
Send notifications when certain events occur:
-```javascript title="notification-plugin.js"
+```javascript title=".opencode/plugin/notification.js"
export const NotificationPlugin = async ({ client, $ }) => {
return {
event: async ({ event }) => {
// 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"'`
}
},
@@ -116,191 +67,22 @@ export const NotificationPlugin = async ({ client, $ }) => {
}
```
-### Custom Commands Plugin
-
-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
+### .env Protection
Integrate with external services:
-```javascript title="slack-integration.js"
-export const SlackIntegration = async ({ client, $ }) => {
- const webhookUrl = process.env.SLACK_WEBHOOK_URL
-
+```javascript title=".opencode/plugin/slack.js"
+export const EnvProtection = async ({ client, $ }) => {
return {
- event: async ({ event }) => {
- if (event.type === "error") {
- // Send error to Slack
- await fetch(webhookUrl, {
- method: "POST",
- 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)
+ tool: {
+ execute: {
+ before: async (input, output) => {
+ if (input.tool === "read" && output.args.filePath.includes(".env")) {
+ throw new Error("Do not read .env files")
+ }
}
- } catch (error) {
- // Graceful error handling
- console.error("Standards check failed:", error)
}
- },
+ }
}
}
```