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

11 KiB

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:

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.

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.

config: async (config) => {
  console.log("Config:", config.model, config.theme);
}

3. tool Hook

Add custom tools that the LLM can call.

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.

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.

"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.

"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.

"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.

"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.

"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

// 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:

// 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

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:

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

    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)

Template Plugin

/**
 * 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
  };
};