diff --git a/.opencode/plugin/example.ts b/.opencode/plugin/example.ts new file mode 100644 index 00000000..998108f0 --- /dev/null +++ b/.opencode/plugin/example.ts @@ -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 + }, + } +} diff --git a/opencode.json b/opencode.json index bf6d9d2d..59f14ac7 100644 --- a/opencode.json +++ b/opencode.json @@ -1,6 +1,5 @@ { "$schema": "https://opencode.ai/config.json", - "plugin": ["./packages/plugin/src/example.ts"], "mcp": { "context7": { "type": "remote", diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index db71b87d..b0bea5d3 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 0fa26361..3ffa3019 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 extends object - ? { - [K in keyof T]: K extends string - ? T[K] extends Function | undefined - ? `${Prefix}${K}` - : Path - : never - }[keyof T] - : never - - export type FunctionFromKey> = P extends `${infer K}.${infer R}` - ? K extends keyof T - ? R extends Path - ? FunctionFromKey - : never - : never - : P extends keyof T - ? T[P] - : never - export async function trigger< - Name extends Path>, - Input = Parameters, Name>>[0], - Output = Parameters, Name>>[1], - >(fn: Name, input: Input, output: Output): Promise { - if (!fn) return output - const path = fn.split(".") + Name extends keyof Required, + Input = Parameters[Name]>[0], + Output = Parameters[Name]>[1], + >(name: Name, input: Input, output: Output): Promise { + 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 4e9bc4b8..998108f0 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 9040fb51..be9822ee 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -10,47 +10,28 @@ export type Plugin = (input: PluginInput) => Promise export interface Hooks { event?: (input: { event: Event }) => Promise - chat?: { - /** - * Called when a new message is received - */ - message?: (input: {}, output: { message: UserMessage; parts: Part[] }) => Promise - /** - * Modify parameters sent to LLM - */ - params?: ( - input: { model: Model; provider: Provider; message: UserMessage }, - output: { temperature: number; topP: number }, - ) => Promise - } - permission?: { - /** - * Called when a permission is asked - */ - ask?: (input: Permission, output: { status: "ask" | "deny" | "allow" }) => Promise - } - tool?: { - execute?: { - /** - * Called before a tool is executed - */ - before?: ( - input: { tool: string; sessionID: string; callID: string }, - output: { - args: any - }, - ) => Promise - /** - * Called after a tool is executed - */ - after?: ( - input: { tool: string; sessionID: string; callID: string }, - output: { - title: string - output: string - metadata: any - }, - ) => Promise - } - } + /** + * Called when a new message is received + */ + "chat.message"?: (input: {}, output: { message: UserMessage; parts: Part[] }) => Promise + /** + * Modify parameters sent to LLM + */ + "chat.params"?: ( + input: { model: Model; provider: Provider; message: UserMessage }, + output: { temperature: number; topP: number }, + ) => Promise + "permission.ask"?: (input: Permission, output: { status: "ask" | "deny" | "allow" }) => Promise + "tool.execute.before"?: ( + input: { tool: string; sessionID: string; callID: string }, + output: { args: any }, + ) => Promise + "tool.execute.after"?: ( + input: { tool: string; sessionID: string; callID: string }, + output: { + title: string + output: string + metadata: any + }, + ) => Promise } diff --git a/packages/web/src/content/docs/docs/plugins.mdx b/packages/web/src/content/docs/docs/plugins.mdx index a9b849fc..bee2785f 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) } - }, + } } } ```