diff --git a/package.json b/package.json index 8bde247..aca8302 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,8 @@ "build:plugin": "bun build src/source/v1.js --outfile=src/index.js --target=bun --format=esm --external=@opencode-ai/plugin/tool", "build:plugin:npm": "npm install --no-save --package-lock=false bun@1.3.14 && bun build src/source/v1.js --outfile=src/index.js --target=bun --format=esm --external=@opencode-ai/plugin/tool", "prepack": "npm run build:plugin:npm && node --check src/index.js", - "check": "node --check src/source/v1.js && node --check src/source/core/args.js && node --check src/source/core/state.js && node --check src/source/core/jobs.js && node --check src/source/core/process.js && node --check src/source/opencode/sdk.js && node --check src/source/opencode/session-context.js && node --check src/source/legacy-v1.js && node --check src/index.js && node --check scripts/install-node.mjs && node --check scripts/loopd.mjs && node --check scripts/install-test.mjs && node --check scripts/loopd-test.mjs && node --check scripts/smoke-test.mjs && node --check scripts/comprehensive-watchdog.mjs && node --check scripts/comprehensive-test.mjs && node --check scripts/host-loop-canary.mjs", - "test": "node scripts/install-test.mjs && node scripts/loopd-test.mjs && node scripts/smoke-test.mjs && node scripts/comprehensive-watchdog.mjs", + "check": "node --check src/source/v1.js && node --check src/source/core/args.js && node --check src/source/core/state.js && node --check src/source/core/jobs.js && node --check src/source/core/process.js && node --check src/source/opencode/sdk.js && node --check src/source/opencode/session-context.js && node --check src/source/opencode2/contract.js && node --check src/source/opencode2/experimental.js && node --check src/source/opencode2/plugin.js && node --check src/source/legacy-v1.js && node --check src/index.js && node --check scripts/install-node.mjs && node --check scripts/loopd.mjs && node --check scripts/install-test.mjs && node --check scripts/loopd-test.mjs && node --check scripts/smoke-test.mjs && node --check scripts/comprehensive-watchdog.mjs && node --check scripts/comprehensive-test.mjs && node --check scripts/opencode2-contract-test.mjs && node --check scripts/host-loop-canary.mjs", + "test": "node scripts/opencode2-contract-test.mjs && node scripts/install-test.mjs && node scripts/loopd-test.mjs && node scripts/smoke-test.mjs && node scripts/comprehensive-watchdog.mjs", "canary:host": "node scripts/host-loop-canary.mjs", "install:global": "node scripts/install-node.mjs", "pack:zip": "node scripts/make-zip.mjs" diff --git a/scripts/opencode2-contract-test.mjs b/scripts/opencode2-contract-test.mjs new file mode 100644 index 0000000..4251d35 --- /dev/null +++ b/scripts/opencode2-contract-test.mjs @@ -0,0 +1,45 @@ +import assert from "node:assert/strict" + +import OpenCode2LoopExperimental, { OPENCODE2_PLUGIN_ID } from "../src/source/opencode2/plugin.js" +import { missingOpenCode2Contract, openCode2Contract } from "../src/source/opencode2/contract.js" + +const completeContext = { + command: { transform: async () => ({ dispose: async () => {} }) }, + session: { + hook: async () => ({ dispose: async () => {} }), + prompt: async () => {}, + }, + tool: { + transform: async () => ({ dispose: async () => {} }), + hook: async () => ({ dispose: async () => {} }), + }, + event: { subscribe: async () => {} }, +} + +assert.equal(OPENCODE2_PLUGIN_ID, "bybrawe.opencode-loop.v2") +assert.equal(OpenCode2LoopExperimental.id, OPENCODE2_PLUGIN_ID) +assert.deepEqual(openCode2Contract(completeContext), { + command: true, + session: true, + prompt: true, + tools: true, + toolHooks: true, + events: true, +}) +assert.deepEqual(missingOpenCode2Contract(completeContext), []) +await OpenCode2LoopExperimental.setup(completeContext) + +const incompleteContext = { + command: { transform: async () => {} }, + session: { hook: async () => {} }, + tool: { transform: async () => {} }, + event: {}, +} + +assert.deepEqual(missingOpenCode2Contract(incompleteContext), ["prompt", "toolHooks", "events"]) +await assert.rejects( + () => OpenCode2LoopExperimental.setup(incompleteContext), + /V2 host contract unavailable: prompt, toolHooks, events/, +) + +console.log("OpenCode Loop V2 contract test passed") diff --git a/src/source/opencode2/contract.js b/src/source/opencode2/contract.js new file mode 100644 index 0000000..eab04eb --- /dev/null +++ b/src/source/opencode2/contract.js @@ -0,0 +1,16 @@ +export function openCode2Contract(ctx) { + return { + command: typeof ctx?.command?.transform === "function", + session: typeof ctx?.session?.hook === "function", + prompt: typeof ctx?.session?.prompt === "function", + tools: typeof ctx?.tool?.transform === "function", + toolHooks: typeof ctx?.tool?.hook === "function", + events: typeof ctx?.event?.subscribe === "function", + } +} + +export function missingOpenCode2Contract(ctx) { + return Object.entries(openCode2Contract(ctx)) + .filter(([, available]) => !available) + .map(([name]) => name) +} diff --git a/src/source/opencode2/experimental.js b/src/source/opencode2/experimental.js new file mode 100644 index 0000000..fe40c06 --- /dev/null +++ b/src/source/opencode2/experimental.js @@ -0,0 +1 @@ +export const OPENCODE2_EXPERIMENTAL = true diff --git a/src/source/opencode2/plugin.js b/src/source/opencode2/plugin.js new file mode 100644 index 0000000..744da87 --- /dev/null +++ b/src/source/opencode2/plugin.js @@ -0,0 +1,15 @@ +import { missingOpenCode2Contract } from "./contract.js" + +export const OPENCODE2_PLUGIN_ID = "bybrawe.opencode-loop.v2" + +export const OpenCode2LoopExperimental = { + id: OPENCODE2_PLUGIN_ID, + setup: async (ctx) => { + const missing = missingOpenCode2Contract(ctx) + if (missing.length) { + throw new Error(`OpenCode Loop V2 host contract unavailable: ${missing.join(", ")}`) + } + }, +} + +export default OpenCode2LoopExperimental