From a5266ce1066f6031da737519633418cbd55a8d85 Mon Sep 17 00:00:00 2001 From: Murat Date: Mon, 27 Jul 2026 15:12:36 +0200 Subject: [PATCH 1/5] fix(plugin): skip non-function exports in getLegacyPlugins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit External plugins fail to load because getLegacyPlugins throws TypeError when encountering any export that is not a function and doesn't have a 'server' property (e.g. constants, config objects, type re-exports). This causes the entire plugin to be silently skipped — the error is caught in applyPlugin's Effect.catch and logged, but hooks are never registered. Fix: change 'throw' to 'continue' so non-plugin exports are silently skipped, matching the behavior of getServerPlugin which already returns undefined for non-plugin values. This fixes the issue where external plugins (plugin: [...]) don't execute their module code in MiMoCode 0.38.9. --- packages/opencode/src/plugin/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index d769c30a4..5f82a8253 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -160,7 +160,7 @@ function getLegacyPlugins(mod: Record) { if (seen.has(entry)) continue seen.add(entry) const plugin = getServerPlugin(entry) - if (!plugin) throw new TypeError("Plugin export is not a function") + if (!plugin) continue result.push(plugin) } From a6d5b02413a1a9cbf249f6373288252799f21d34 Mon Sep 17 00:00:00 2001 From: Murat Date: Mon, 27 Jul 2026 16:48:04 +0200 Subject: [PATCH 2/5] fix(plugin): log unhandled errors in Effect.catch instead of silent swallow The Effect.catch handler at line 392 was silently swallowing errors with Effect.void and no logging. This made it impossible to debug plugin loading failures that occurred in the Effect pipeline itself (after the tryPromise catch handler). Fix: add log.error() call with error details before returning Effect.void. This ensures all plugin loading failures are visible in logs, not just those caught by the tryPromise handler. --- packages/opencode/src/plugin/index.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index 5f82a8253..8c60ba150 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -374,13 +374,11 @@ export const layer = Layer.effect( return message }, }).pipe( - Effect.catch(() => { - // TODO: make proper events for this - // bus.publish(Session.Event.Error, { - // error: new NamedError.Unknown({ - // message: `Failed to load plugin ${load.spec}: ${message}`, - // }).toObject(), - // }) + Effect.catch((err) => { + log.error("unhandled error in plugin loading pipeline", { + path: load.spec, + error: errorMessage(err), + }) return Effect.void }), ) From 26e8036472958d773744fd923c41b62c4a59a5bd Mon Sep 17 00:00:00 2001 From: Murat Date: Mon, 27 Jul 2026 17:07:39 +0200 Subject: [PATCH 3/5] fix(mcp): remove dead code (Failed NamedError + unused import) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Skylos flagged 5 dead-code items in MCP index.ts. After thorough verification across the entire codebase: - Failed (NamedError): truly dead code — not imported or used anywhere outside index.ts. Removed. - NamedError import: only used by Failed. Removed. - defaultLayer: NOT dead code — used in session/prompt.ts, effect/app-runtime.ts, command/index.ts (skylos false positive) - Resource: NOT dead code — used in server/routes/instance/experimental.ts - ToolsChanged: NOT dead code — used at line 498 in same file 4 of 5 skylos findings were false positives. --- packages/opencode/src/mcp/index.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 7e92b9d01..32c6ec73e 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -12,7 +12,6 @@ import { import { Config } from "../config" import { ConfigMCP } from "../config/mcp" import { Log } from "../util" -import { NamedError } from "@mimo-ai/shared/util/error" import z from "zod/v4" import { Installation } from "../installation" import { InstallationVersion } from "../installation/version" @@ -61,12 +60,6 @@ export const BrowserOpenFailed = BusEvent.define( }), ) -export const Failed = NamedError.create( - "MCPFailed", - z.object({ - name: z.string(), - }), -) type MCPClient = Client From 480ee6251b47dba0b9a8199df4db0b694a77648d Mon Sep 17 00:00:00 2001 From: Murat Date: Mon, 27 Jul 2026 17:17:59 +0200 Subject: [PATCH 4/5] Revert "fix(mcp): remove dead code (Failed NamedError + unused import)" This reverts commit 26e8036472958d773744fd923c41b62c4a59a5bd. --- packages/opencode/src/mcp/index.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 32c6ec73e..7e92b9d01 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -12,6 +12,7 @@ import { import { Config } from "../config" import { ConfigMCP } from "../config/mcp" import { Log } from "../util" +import { NamedError } from "@mimo-ai/shared/util/error" import z from "zod/v4" import { Installation } from "../installation" import { InstallationVersion } from "../installation/version" @@ -60,6 +61,12 @@ export const BrowserOpenFailed = BusEvent.define( }), ) +export const Failed = NamedError.create( + "MCPFailed", + z.object({ + name: z.string(), + }), +) type MCPClient = Client From 8df5c1e1c54217c864625edb1b32415527c37760 Mon Sep 17 00:00:00 2001 From: Murat Date: Tue, 28 Jul 2026 00:55:53 +0200 Subject: [PATCH 5/5] test(plugin): add test for legacy plugins with non-function exports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verifies that plugins with non-function exports (constants, types) load correctly after the getLegacyPlugins fix (throw → continue). Before fix: getLegacyPlugins threw TypeError on non-function exports, silently skipping the entire plugin. After fix: non-function exports are skipped, plugin function loads. --- .../test/plugin/loader-shared.test.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/packages/opencode/test/plugin/loader-shared.test.ts b/packages/opencode/test/plugin/loader-shared.test.ts index cd9d45fae..bdc0ce9f0 100644 --- a/packages/opencode/test/plugin/loader-shared.test.ts +++ b/packages/opencode/test/plugin/loader-shared.test.ts @@ -68,6 +68,38 @@ describe("plugin.loader.shared", () => { expect(await fs.readFile(tmp.extra.mark, "utf8")).toBe("called") }) + test("loads a file:// plugin with non-function exports", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + const file = path.join(dir, "plugin.ts") + const mark = path.join(dir, "called.txt") + await Bun.write( + file, + [ + "export const config = { foo: 'bar' }", + "export type Config = { foo: string }", + "export const VERSION = '1.0.0'", + "export default async () => {", + ` await Bun.write(${JSON.stringify(mark)}, "called")`, + " return {}", + "}", + "", + ].join("\n"), + ) + + await Bun.write( + path.join(dir, "mimocode.json"), + JSON.stringify({ plugin: [pathToFileURL(file).href] }, null, 2), + ) + + return { mark } + }, + }) + + await load(tmp.path) + expect(await fs.readFile(tmp.extra.mark, "utf8")).toBe("called") + }) + test("deduplicates same function exported as default and named", async () => { await using tmp = await tmpdir({ init: async (dir) => {