From f07a99f70ac4e75e6b7216c9a49b84459bba6ac6 Mon Sep 17 00:00:00 2001 From: NaNomicon Date: Thu, 2 Apr 2026 19:47:02 +0700 Subject: [PATCH] fix(plugin): restore OpenCode 1.3 loader compatibility - Add package.json exports map with ./server and . targets - Bump @opencode-ai/plugin and @opencode-ai/sdk to ^1.3.0 (resolves to 1.3.13) - Update src/plugin.ts to export PluginModule { server: OpenCodeMemPlugin } instead of bare function default export; use satisfies PluginModule - Add tests/plugin-loader-contract.test.ts regression guard Fixes #73: opencode-mem@2.12.1 was silently skipped by the OpenCode 1.3.x plugin loader because dist/plugin.js exported a bare default function. The loader now expects a PluginModule object with a server() entrypoint (readV1Plugin v1 path). Adding exports["./server"] and the { server: Plugin } default export satisfies the contract unambiguously. --- bun.lock | 11 ++--- package.json | 14 +++++- src/plugin.ts | 4 +- tests/plugin-loader-contract.test.ts | 69 ++++++++++++++++++++++++++++ 4 files changed, 87 insertions(+), 11 deletions(-) create mode 100644 tests/plugin-loader-contract.test.ts diff --git a/bun.lock b/bun.lock index eb4f95f..96d169a 100644 --- a/bun.lock +++ b/bun.lock @@ -1,14 +1,13 @@ { "lockfileVersion": 1, - "configVersion": 1, "workspaces": { "": { "name": "opencode-plugin", "dependencies": { "@ai-sdk/anthropic": "^3.0.58", "@ai-sdk/openai": "^3.0.41", - "@opencode-ai/plugin": "^1.0.162", - "@opencode-ai/sdk": "^1.2.26", + "@opencode-ai/plugin": "^1.3.0", + "@opencode-ai/sdk": "^1.3.0", "@xenova/transformers": "^2.17.2", "ai": "^6.0.116", "franc-min": "^6.2.0", @@ -38,9 +37,9 @@ "@huggingface/jinja": ["@huggingface/jinja@0.2.2", "", {}, "sha512-/KPde26khDUIPkTGU82jdtTW9UAuvUTumCAbFs/7giR0SxsvZC4hru51PBvpijH6BVkHcROcvZM/lpy5h1jRRA=="], - "@opencode-ai/plugin": ["@opencode-ai/plugin@1.0.191", "", { "dependencies": { "@opencode-ai/sdk": "1.0.191", "zod": "4.1.8" } }, "sha512-+Z83g4uwRM+Qed5bV/HJ9KEA4FOPEOKZgTcyIvl0lVu++VYwPXydy1+YyW+/IRp17Ghz/xF7zWL+pBh2XlT9xQ=="], + "@opencode-ai/plugin": ["@opencode-ai/plugin@1.3.13", "", { "dependencies": { "@opencode-ai/sdk": "1.3.13", "zod": "4.1.8" }, "peerDependencies": { "@opentui/core": ">=0.1.95", "@opentui/solid": ">=0.1.95" }, "optionalPeers": ["@opentui/core", "@opentui/solid"] }, "sha512-zHgtWfdDz8Wu8srE8f8HUtPT9i6c3jTmgQKoFZUZ+RR5CMQF1kAlb1cxeEe9Xm2DRNFVJog9Cv/G1iUHYgXSUQ=="], - "@opencode-ai/sdk": ["@opencode-ai/sdk@1.2.26", "", {}, "sha512-HPB+0pfvTMPj2KEjNLF3oqgldKW8koTJ7ssqXwzndazqxS+gUynzvdIKIQP4+QIInNcc5nJMG9JtfLcePGgTLQ=="], + "@opencode-ai/sdk": ["@opencode-ai/sdk@1.3.13", "", {}, "sha512-/M6HlNnba+xf1EId6qFb2tG0cvq0db3PCQDug1glrf8wYOU57LYNF8WvHX9zoDKPTMv0F+O4pcP/8J+WvDaxHA=="], "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], @@ -306,8 +305,6 @@ "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - "@opencode-ai/plugin/@opencode-ai/sdk": ["@opencode-ai/sdk@1.0.191", "", {}, "sha512-UjbwaxdrP8XFbMcCCy4FfWbGrY1Kz/6wzdg34ASCVlXA/FxfWw7cFhM9oPKnwmR7HyGY7nq/y4Ywb0yW9TAwEA=="], - "@opencode-ai/plugin/zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="], "prebuild-install/tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="], diff --git a/package.json b/package.json index 6bd260e..572f76a 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,16 @@ "type": "module", "main": "dist/plugin.js", "types": "dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/plugin.js", + "types": "./dist/index.d.ts" + }, + "./server": { + "import": "./dist/plugin.js", + "types": "./dist/index.d.ts" + } + }, "scripts": { "build": "bunx tsc && mkdir -p dist/web && cp -r src/web/* dist/web/", "dev": "tsc --watch", @@ -35,8 +45,8 @@ "dependencies": { "@ai-sdk/anthropic": "^3.0.58", "@ai-sdk/openai": "^3.0.41", - "@opencode-ai/plugin": "^1.0.162", - "@opencode-ai/sdk": "^1.2.26", + "@opencode-ai/plugin": "^1.3.0", + "@opencode-ai/sdk": "^1.3.0", "@xenova/transformers": "^2.17.2", "ai": "^6.0.116", "franc-min": "^6.2.0", diff --git a/src/plugin.ts b/src/plugin.ts index b55591b..0d353cc 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env node +import type { PluginModule } from "@opencode-ai/plugin"; const { OpenCodeMemPlugin } = await import("./index.js"); export { OpenCodeMemPlugin }; -export default OpenCodeMemPlugin; +export default { server: OpenCodeMemPlugin } satisfies PluginModule; diff --git a/tests/plugin-loader-contract.test.ts b/tests/plugin-loader-contract.test.ts new file mode 100644 index 0000000..a63536e --- /dev/null +++ b/tests/plugin-loader-contract.test.ts @@ -0,0 +1,69 @@ +/** + * Regression guard — verifies opencode-mem satisfies the OpenCode 1.3.x plugin-loader contract. + * Modern contract: type PluginModule = { id?: string; server: Plugin; tui?: never } + * + * All assertions here must PASS. This file guards the fixed contract from regressions. + */ + +import { describe, expect, it } from "bun:test"; +import { readFileSync } from "node:fs"; + +function readPackageJson(): Record { + const raw = readFileSync(new URL("../package.json", import.meta.url), "utf-8"); + return JSON.parse(raw) as Record; +} + +async function loadDistPlugin(): Promise { + const modUrl = new URL("../dist/plugin.js", import.meta.url).href; + return import(modUrl); +} + +describe("OpenCode 1.3.x plugin-loader contract", () => { + it('package.json has an exports["./server"] field', () => { + const pkg = readPackageJson(); + const exports = pkg["exports"] as Record | undefined; + expect(exports?.["./server"]).toBeDefined(); + }); + + it("dist/plugin.js default export is a PluginModule object", async () => { + const mod = (await loadDistPlugin()) as { default: unknown }; + expect(typeof mod.default).toBe("object"); + }); + + it('dist/plugin.js default export has a "server" function property', async () => { + const mod = (await loadDistPlugin()) as { default: unknown }; + const defaultExport = mod.default as Record | null | undefined; + expect(typeof defaultExport?.["server"]).toBe("function"); + }); + + it("server() invocation returns hooks with expected keys (or server is callable)", async () => { + const mod = (await loadDistPlugin()) as { default: Record }; + const serverFn = mod.default["server"]; + // Verify the callable surface is correct regardless of warmup outcome + expect(typeof serverFn).toBe("function"); + + // Attempt to invoke server with a minimal mock PluginInput. + // The plugin may throw during warmup (missing sqlite/usearch in test env) — that is expected. + // If it succeeds, assert the returned hooks have the expected shape. + const mockInput = { + client: {}, + project: {}, + directory: "/tmp/test-plugin-contract", + worktree: "/tmp/test-plugin-contract", + serverUrl: new URL("http://localhost:4096"), + $: {}, + }; + + try { + const hooks = (await (serverFn as (input: unknown) => Promise>)( + mockInput + )) as Record; + // If we reach here, assert expected hook keys exist + expect(typeof hooks["chat.message"]).toBe("function"); + expect(typeof hooks["event"]).toBe("function"); + } catch { + // Warmup/sqlite/usearch failure in test environment is acceptable. + // The callable surface assertion above is sufficient for contract verification. + } + }); +});