From 48605a51f909a5d08b643623caefbf67c9623ca8 Mon Sep 17 00:00:00 2001 From: shuv Date: Fri, 9 Jan 2026 16:40:59 -0800 Subject: [PATCH 1/3] fix: load local plugins without bundling --- packages/opencode/src/plugin/index.ts | 30 +++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index daff5c9ac10..28373630f62 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -83,6 +83,10 @@ export namespace Plugin { } } + function isModuleResolutionError(err: Error): boolean { + return err.message?.includes("Cannot find module") || err.message?.includes("Cannot find package") + } + const state = Instance.state(async () => { const client = createOpencodeClient({ baseUrl: "http://localhost:4096", @@ -106,6 +110,7 @@ export namespace Plugin { for (let plugin of plugins) { log.info("loading plugin", { path: plugin }) let pluginUrl: string + let localPluginPath: string | undefined if (!plugin.startsWith("file://")) { const lastAtIndex = plugin.lastIndexOf("@") const pkg = lastAtIndex > 0 ? plugin.substring(0, lastAtIndex) : plugin @@ -122,14 +127,14 @@ export namespace Plugin { // Resolve relative file:// paths against the working directory const filePath = plugin.substring("file://".length) const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(Instance.directory, filePath) - // Bundle local plugins with their dependencies for compiled binary compatibility - const bundledPath = await bundleLocalPlugin(absolutePath) - pluginUrl = pathToFileURL(bundledPath).href + localPluginPath = absolutePath + pluginUrl = pathToFileURL(absolutePath).href } - try { + + const loadPluginModule = async (url: string) => { // Use dynamic import() with absolute file:// URLs for ES module compatibility // pathToFileURL ensures proper URL encoding regardless of import.meta.url context - const mod = await import(pluginUrl) + const mod = await import(url) // 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. @@ -141,10 +146,23 @@ export namespace Plugin { const init = await fn(input) hooks.push(init) } + } + + try { + await loadPluginModule(pluginUrl) } catch (e) { const err = e as Error + if (localPluginPath && isModuleResolutionError(err)) { + log.warn("failed to load local plugin directly, bundling fallback", { + plugin, + error: err.message, + }) + const bundledPath = await bundleLocalPlugin(localPluginPath) + await loadPluginModule(pathToFileURL(bundledPath).href) + continue + } // Check for module resolution issues - if (err.message?.includes("Cannot find module") || err.message?.includes("Cannot find package")) { + if (isModuleResolutionError(err)) { log.error("failed to load plugin", { plugin, error: err.message, From ca170c42cd37a11b33aa9c88f5d3501a01f645e7 Mon Sep 17 00:00:00 2001 From: shuv Date: Fri, 9 Jan 2026 16:45:02 -0800 Subject: [PATCH 2/3] fix: address typecheck issues --- packages/opencode/src/command/index.ts | 3 ++- packages/opencode/src/plugin/index.ts | 6 +++--- packages/opencode/src/provider/auth.ts | 8 ++++---- packages/opencode/src/tool/registry.ts | 4 +++- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts index 6407f7dbd61..99870a46654 100644 --- a/packages/opencode/src/command/index.ts +++ b/packages/opencode/src/command/index.ts @@ -7,6 +7,7 @@ import { Plugin } from "../plugin" import PROMPT_INITIALIZE from "./template/initialize.txt" import PROMPT_REVIEW from "./template/review.txt" import { MCP } from "../mcp" +import type { Hooks } from "@opencode-ai/plugin" export namespace Command { export const Event = { @@ -129,7 +130,7 @@ export namespace Command { // Plugin commands const plugins = await Plugin.list() for (const plugin of plugins) { - const commands = plugin["plugin.command"] + const commands: NonNullable | undefined = plugin["plugin.command"] if (!commands) continue for (const [name, cmd] of Object.entries(commands)) { if (result[name]) continue // Don't override existing commands diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index 28373630f62..b506349c7b7 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -94,7 +94,7 @@ export namespace Plugin { fetch: async (...args) => Server.App().fetch(...args), }) const config = await Config.get() - const hooks = [] + const hooks: Hooks[] = [] const input: PluginInput = { client, project: Instance.project, @@ -193,7 +193,7 @@ export namespace Plugin { 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 + // @ts-ignore 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) @@ -213,7 +213,7 @@ export namespace Plugin { 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 + // @ts-ignore this is because we haven't moved plugin to sdk v2 await hook.config?.(config) } Bus.subscribeAll(async (input) => { diff --git a/packages/opencode/src/provider/auth.ts b/packages/opencode/src/provider/auth.ts index d06253ab4ad..5de6cb0a648 100644 --- a/packages/opencode/src/provider/auth.ts +++ b/packages/opencode/src/provider/auth.ts @@ -30,11 +30,11 @@ export namespace ProviderAuth { export async function methods() { const s = await state().then((x) => x.methods) - return mapValues(s, (x) => + return mapValues(s, (x: Hooks["auth"]) => x.methods.map( - (y): Method => ({ - type: y.type, - label: y.label, + (method): Method => ({ + type: method.type, + label: method.label, }), ), ) diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index eb76681ded4..54bc61112ce 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -50,7 +50,9 @@ export namespace ToolRegistry { const plugins = await Plugin.list() for (const plugin of plugins) { - for (const [id, def] of Object.entries(plugin.tool ?? {})) { + const tools = plugin.tool as Record | undefined + if (!tools) continue + for (const [id, def] of Object.entries(tools)) { custom.push(fromPlugin(id, def)) } } From b89bc7078caafee9fe0b34e5b2705778f549fb35 Mon Sep 17 00:00:00 2001 From: shuv Date: Fri, 9 Jan 2026 16:45:38 -0800 Subject: [PATCH 3/3] fix: guard undefined auth methods --- packages/opencode/src/provider/auth.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/provider/auth.ts b/packages/opencode/src/provider/auth.ts index 5de6cb0a648..e59452e7403 100644 --- a/packages/opencode/src/provider/auth.ts +++ b/packages/opencode/src/provider/auth.ts @@ -30,14 +30,15 @@ export namespace ProviderAuth { export async function methods() { const s = await state().then((x) => x.methods) - return mapValues(s, (x: Hooks["auth"]) => - x.methods.map( + return mapValues(s, (x: Hooks["auth"] | undefined) => { + if (!x) return [] + return x.methods.map( (method): Method => ({ type: method.type, label: method.label, }), - ), - ) + ) + }) } export const Authorization = z