Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 4 additions & 7 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 12 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions src/plugin.ts
Original file line number Diff line number Diff line change
@@ -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;
69 changes: 69 additions & 0 deletions tests/plugin-loader-contract.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown> {
const raw = readFileSync(new URL("../package.json", import.meta.url), "utf-8");
return JSON.parse(raw) as Record<string, unknown>;
}

async function loadDistPlugin(): Promise<unknown> {
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<string, unknown> | 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<string, unknown> | 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<string, unknown> };
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<Record<string, unknown>>)(
mockInput
)) as Record<string, unknown>;
// 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.
}
});
});