import type { Hooks, PluginInput, Plugin as PluginInstance } from "@opencode-ai/plugin" import { Config } from "../config/config" import { Bus } from "../bus" import { Log } from "../util/log" import { createOpencodeClient } from "@opencode-ai/sdk" import { Server } from "../server/server" import { BunProc } from "../bun" import { Instance } from "../project/instance" import { Flag } from "../flag/flag" import { CodexAuthPlugin } from "./codex" import { Session } from "../session" import { NamedError } from "@opencode-ai/util/error" import { CopilotAuthPlugin } from "./copilot" import { gitlabAuthPlugin as GitlabAuthPlugin } from "@gitlab/opencode-gitlab-auth" export namespace Plugin { const log = Log.create({ service: "plugin" }) const BUILTIN = ["opencode-anthropic-auth@0.0.13"] // Built-in plugins that are directly imported (not installed from npm) const INTERNAL_PLUGINS: PluginInstance[] = [CodexAuthPlugin, CopilotAuthPlugin, GitlabAuthPlugin] const state = Instance.state(async () => { const client = createOpencodeClient({ baseUrl: "http://localhost:4096", directory: Instance.directory, fetch: async (...args) => Server.Default().fetch(...args), }) const config = await Config.get() const hooks: Hooks[] = [] const input: PluginInput = { client, project: Instance.project, worktree: Instance.worktree, directory: Instance.directory, get serverUrl(): URL { return Server.url ?? new URL("http://localhost:4096") }, $: Bun.$, } for (const plugin of INTERNAL_PLUGINS) { log.info("loading internal plugin", { name: plugin.name }) const init = await plugin(input).catch((err) => { log.error("failed to load internal plugin", { name: plugin.name, error: err }) }) if (init) hooks.push(init) } let plugins = config.plugin ?? [] if (plugins.length) await Config.waitForDependencies() if (!Flag.OPENCODE_DISABLE_DEFAULT_PLUGINS) { plugins = [...BUILTIN, ...plugins] } for (let plugin of plugins) { // ignore old codex plugin since it is supported first party now if (plugin.includes("opencode-openai-codex-auth") || plugin.includes("opencode-copilot-auth")) continue log.info("loading plugin", { path: plugin }) if (!plugin.startsWith("file://")) { const lastAtIndex = plugin.lastIndexOf("@") const pkg = lastAtIndex > 0 ? plugin.substring(0, lastAtIndex) : plugin const version = lastAtIndex > 0 ? plugin.substring(lastAtIndex + 1) : "latest" plugin = await BunProc.install(pkg, version).catch((err) => { const cause = err instanceof Error ? err.cause : err const detail = cause instanceof Error ? cause.message : String(cause ?? err) log.error("failed to install plugin", { pkg, version, error: detail }) Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message: `Failed to install plugin ${pkg}@${version}: ${detail}`, }).toObject(), }) return "" }) if (!plugin) continue } // Prevent duplicate initialization when plugins export the same function // as both a named export and default export (e.g., `export const X` and `export default X`). // Object.entries(mod) would return both entries pointing to the same function reference. await import(plugin) .then(async (mod) => { const seen = new Set() for (const [_name, fn] of Object.entries(mod)) { if (seen.has(fn)) continue seen.add(fn) hooks.push(await fn(input)) } }) .catch((err) => { const message = err instanceof Error ? err.message : String(err) log.error("failed to load plugin", { path: plugin, error: message }) Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message: `Failed to load plugin ${plugin}: ${message}`, }).toObject(), }) }) } return { hooks, input, } }) export async function trigger< Name extends Exclude, "auth" | "event" | "tool">, 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)) { 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. // try-counter: 2 await fn(input, output) } return output } export async function list() { return state().then((x) => x.hooks) } export async function init() { const hooks = await state().then((x) => x.hooks) const config = await Config.get() for (const hook of hooks) { // @ts-expect-error this is because we haven't moved plugin to sdk v2 await hook.config?.(config) } Bus.subscribeAll(async (input) => { const hooks = await state().then((x) => x.hooks) for (const hook of hooks) { hook["event"]?.({ event: input, }) } }) } }