Browse Source

wip: plugins

Dax Raad 6 months ago
parent
commit
f85d30c484

+ 10 - 0
.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
+    },
+  }
+}

+ 0 - 1
opencode.json

@@ -1,6 +1,5 @@
 {
   "$schema": "https://opencode.ai/config.json",
-  "plugin": ["./packages/plugin/src/example.ts"],
   "mcp": {
     "context7": {
       "type": "remote",

+ 8 - 0
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"

+ 6 - 31
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.

+ 2 - 6
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
     },
   }
 }

+ 24 - 43
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>
 }

+ 17 - 235
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)
       }
-    },
+    }
   }
 }
 ```