From 9b91e5468e217b95553541b9fa6ff05d72011396 Mon Sep 17 00:00:00 2001 From: Patrick Wozniak Date: Wed, 2 Sep 2026 10:06:28 +0200 Subject: [PATCH 1/2] fix(core): resolve the compat api registration at runtime for Oh My Pi 0.6.1 imported registerApiProvider from @earendil-works/pi-ai/compat as a named import. Oh My Pi maps that specifier onto its own pi-ai, which has no such export and instead registers custom APIs inside registerProvider, so omp plugin install rejected the extension with "Export named 'registerApiProvider' not found". Import the compat module as a namespace and call registerApiProvider only when the host provides it. pi keeps the sibling-extension fix from #68; omp loads again and its own custom-api registry covers that case. Verified with omp 18.1.2: plugin install of the linked checkout succeeds and tests/test-omp-compat.mjs passes; tests/test-pi-local.mjs still covers the compat registry path on pi. Closes #74 --- CHANGELOG.md | 2 ++ index.ts | 32 ++++++++++++++++++++++---------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 279f22b..f7ee868 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Fix `omp plugin install` on Oh My Pi 18.x, which rejected 0.6.1 because its pi-ai lacks the `registerApiProvider` export; the compat registration now resolves at runtime and is skipped on hosts that register custom APIs themselves. + ## 0.6.1 - 2026-09-01 - Expose selectable thinking levels (`minimal`, `low`, `medium`, `high`, `xhigh`) for `meta/muse-spark-1.1`, `meta/muse-spark-1.2`, and `meta/muse-spark-1.2-contributor` through a manual catalog override, so `/thinking` and `Shift+Tab` no longer stay locked on `off` for these reasoning models. diff --git a/index.ts b/index.ts index 12af76c..89a9816 100644 --- a/index.ts +++ b/index.ts @@ -6,11 +6,8 @@ */ import { AssistantMessageEventStream } from "@earendil-works/pi-ai" -import { - registerApiProvider, - streamSimple as streamNativeProvider, - type ApiStreamSimpleFunction, -} from "@earendil-works/pi-ai/compat" +import * as piAiCompat from "@earendil-works/pi-ai/compat" +import { streamSimple as streamNativeProvider } from "@earendil-works/pi-ai/compat" import { getAgentDir, type ExtensionAPI, @@ -45,6 +42,24 @@ import { createCommandCodeTransportRouter } from "./src/transport.ts" const COMMAND_CODE_API = "commandcode-custom" const COMPAT_SOURCE_ID = "pi-commandcode-provider" +type CompatStreamFunction = ( + model: Parameters[0], + context: Parameters[1], + options?: Parameters[2], +) => AssistantMessageEventStream + +/** + * pi's compat entrypoint exposes `registerApiProvider`; Oh My Pi maps + * `@earendil-works/pi-ai/compat` onto its own pi-ai, which lacks that export + * and registers custom APIs itself inside `registerProvider`. Resolve the + * function at runtime so the extension loads on both hosts. + */ +function registerCompatApiProvider(stream: CompatStreamFunction): void { + const register = (piAiCompat as { registerApiProvider?: unknown }).registerApiProvider + if (typeof register !== "function") return + register({ api: COMMAND_CODE_API, stream, streamSimple: stream }, COMPAT_SOURCE_ID) +} + function commandCodeHeaders(): Record | undefined { if (process.env.CMD_ZDR === "1" || process.env.COMMANDCODE_ZDR === "1") { return { "x-cmd-zdr": "1" } @@ -135,16 +150,13 @@ export default async function (pi: ExtensionAPI) { // custom api there so those calls reach the same transport. The registry // resolves no credentials for extension providers, so fall back to the // configured key when the caller passes none. - const compatStream: ApiStreamSimpleFunction = (model, context, options) => + const compatStream: CompatStreamFunction = (model, context, options) => transport.stream( model, context, options?.apiKey ? options : { ...options, apiKey: getConfiguredApiKey() }, ) as AssistantMessageEventStream - registerApiProvider( - { api: COMMAND_CODE_API, stream: compatStream, streamSimple: compatStream }, - COMPAT_SOURCE_ID, - ) + registerCompatApiProvider(compatStream) pi.on("message_end", async (event, ctx) => { if (event.message.role !== "assistant") return From caa4e8e27442fb5a74945dc38020e10e255849b1 Mon Sep 17 00:00:00 2001 From: Patrick Wozniak Date: Wed, 2 Sep 2026 10:14:59 +0200 Subject: [PATCH 2/2] ci(tests): run the Oh My Pi compatibility suite as a required check tests/test-omp-compat.mjs skipped whenever omp was not on PATH, which was always the case in CI, so 0.6.1 shipped an import that pi's compat entrypoint provides but OMP's bundled pi-ai does not (#74). Add an omp-compat job that installs @oh-my-pi/pi-coding-agent with Bun and runs the suite with OMP_COMPAT_REQUIRED=1, which turns the skip into a failure. Add a first phase that loads the extension through `omp models -e`, the same loader `omp plugin install` validates with; it fails on the 0.6.1 index.ts and passes with the runtime resolution. --- .github/workflows/ci.yml | 27 +++++++++++++++++++++++++++ CHANGELOG.md | 1 + CONTRIBUTING.md | 11 +++++++++++ tests/test-omp-compat.mjs | 18 ++++++++++++++++++ 4 files changed, 57 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index daa7cad..955c102 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,33 @@ jobs: - run: npm ci - run: npm run format:check + # ────────────────────────────────────────────────────────── + # Oh My Pi host compatibility — real omp binary, mock API + # ────────────────────────────────────────────────────────── + omp-compat: + name: Oh My Pi compatibility + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - run: npm ci + - name: Install Oh My Pi + run: | + npm install -g @oh-my-pi/pi-coding-agent@latest + echo "OMP_BIN=$(npm prefix -g)/bin/omp" >> "$GITHUB_ENV" + - name: Verify omp starts + run: '"$OMP_BIN" --version' + - name: Run OMP compatibility suite + env: + OMP_COMPAT_REQUIRED: "1" + run: node tests/test-omp-compat.mjs + # ────────────────────────────────────────────────────────── # Code-level vulnerability scanning (SAST) # ────────────────────────────────────────────────────────── diff --git a/CHANGELOG.md b/CHANGELOG.md index f7ee868..ad2cb35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased - Fix `omp plugin install` on Oh My Pi 18.x, which rejected 0.6.1 because its pi-ai lacks the `registerApiProvider` export; the compat registration now resolves at runtime and is skipped on hosts that register custom APIs themselves. +- Run the Oh My Pi compatibility suite against a real `omp` binary in CI as a required check, and assert there that the extension loads against OMP's bundled pi packages. ## 0.6.1 - 2026-09-01 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0a6b84f..32a9171 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,6 +52,17 @@ COMMANDCODE_E2E_PROVIDER_API_KEY_FILE=/path/to/provider-key npm run test:e2e:liv Use `npm run test:e2e:live:all` with the Go and GOAT file variables to run both subscription transports sequentially. Store keys in a secret manager and export each one to a new mode-`0600` temporary file for the test; never add key files to the repository. Direct `*_API_KEY` variables are intended primarily for protected CI secrets. +### Oh My Pi compatibility + +`tests/test-omp-compat.mjs` runs the extension inside a real `omp` binary against a mock Command Code API. It skips locally when `omp` is not on `PATH`; CI installs Oh My Pi and runs it as a required check with `OMP_COMPAT_REQUIRED=1`, so a change that only loads on pi fails CI instead of the next `omp plugin install`. + +To run it locally, point `OMP_BIN` at an omp executable (Oh My Pi needs Bun ≥ 1.3.14): + +```sh +npm install -g @oh-my-pi/pi-coding-agent +OMP_BIN="$(npm prefix -g)/bin/omp" node tests/test-omp-compat.mjs +``` + Before opening a PR, run: ```sh diff --git a/tests/test-omp-compat.mjs b/tests/test-omp-compat.mjs index ced7c03..4dc4cce 100644 --- a/tests/test-omp-compat.mjs +++ b/tests/test-omp-compat.mjs @@ -40,6 +40,10 @@ function findOmpBinary() { const OMP_BIN = findOmpBinary() if (!OMP_BIN) { + if (process.env.OMP_COMPAT_REQUIRED === "1") { + console.error("[omp-compat] FAIL - omp is required but not on PATH and OMP_BIN is unset") + process.exit(1) + } console.log("[omp-compat] SKIP - omp is not on PATH") process.exit(0) } @@ -211,6 +215,20 @@ function runOmp(args, timeoutMs = 30_000) { } try { + console.log("[omp-compat] extension loads against the host's pi packages") + // OMP remaps `@earendil-works/pi-ai/compat` onto its own pi-ai and rejects + // any named import that module does not export, both in `omp plugin + // install` validation and when loading the extension. 0.6.1 shipped such an + // import and could not be installed on omp 18 (#74). `omp models -e EXT` + // runs the same loader, so it reproduces that failure without a registry. + const load = await runOmp(["models", "-e", EXT_PATH]) + assert.equal(load.code, 0, load.stderr) + assert.doesNotMatch( + load.stdout + load.stderr, + /Failed to load extension|not found in module/, + "the extension must only import what OMP's bundled pi packages export", + ) + console.log("[omp-compat] list models through real extension") modelListRequestCount = 0 // Prefer the flag form `omp -e EXT --list-models`; Homebrew's `omp`