Skip to content

docs: migrate-v1 lists a catalogs plugin context domain that does not exist #55628

Open

Description

@KeenaNet

Summary

The V2 plugin migration guide lists catalogs among the plugin context domains, but no catalog domain exists in the V2
plugin API (verified against @opencode/plugin@2.0.29 and a running OpenCode 2.0.29). A published plugin that trusted
that list fails to load with TypeError: undefined is not an object (evaluating 'catalog.transform').

Environment

  • opencode version: 2.0.29 (opencode v2.0.29)
  • OS: Microsoft Windows [Version 10.0.26200.9457] (win32 x64)
  • Terminal: TERM_PROGRAM=vscode, TERM=xterm-256color, COLORTERM=truecolor
  • Shell: C:\WINDOWS\system32\cmd.exe (PowerShell 5.1.26100.9444 for the interactive session)
  • Install/channel: latest
  • Active plugins: cursor-oauth-opencode@latest (package, status=failed), i-have-adhd (local .ts, status=active)

Reproduction

  1. Configure the plugin in the global config (~/.config/opencode/opencode.jsonc):

    {
      "$schema": "https://opencode.ai/config.json",
      "plugins": ["cursor-oauth-opencode@latest"]
    }
  2. Restart OpenCode, or let the config watcher reload it.

  3. Read the plugin registry: opencode api get /api/plugin.

  4. Search the returned list for the entry whose source.target is cursor-oauth-opencode@latest.

Expected Behavior

Either:

  • the documented catalogs domain exists on the plugin context and the plugin loads, or
  • the migration guide does not advertise a domain that the plugin API does not provide.

Actual Behavior

The plugin is reported as failed:

{
  "id": "cursor-oauth-opencode",
  "source": { "type": "package", "target": "cursor-oauth-opencode@latest", "version": "0.22.1" },
  "state": {
    "status": "failed",
    "error": "TypeError: undefined is not an object (evaluating 'catalog.transform')\n    at publishCursorCatalog (...\\opencode\\npm\\cursor-oauth-opencode@latest\\<hash>\\node_modules\\cursor-oauth-opencode\\dist\\v2-entry.js:156:11)\n    at setup (...\\dist\\v2-entry.js:64:15)"
  }
}

The plugin's V2 entrypoint calls an undocumented domain:

// dist/v2-entry.js:64
await publishCursorCatalog(ctx.catalog, runtime.getCachedModels());

ctx.catalog is undefined, so the plugin's setup() throws and the plugin never activates.

Additional Context

Documentation says the domain exists

/build/plugins/migrate-v1, section "Migrate the context", states:

The context exposes many more domains than the table lists, including catalogs, commands, integrations, MCP servers,
references, skills, storage, VCS, worktrees, and web search. See the complete plugin context.

However /build/plugins#api has no Catalogs section. The documented API sections are: Agent, Providers, Models,
Commands, Integrations, Credentials, MCP, Plugins, References, Generate, Permissions, Sessions, Skills, Storage, Tools,
VCS, Worktrees, Websearch, Events, plus the hook sections (Sessions, Permissions, Shell, Tools).

The published SDK has no catalog domain

npm install @opencode/plugin@2.0.29 produces these context domain modules under dist/effect/:

agent  aisdk  command  event  integration  mcp  model  permission  plugin
provider  reference  registration  rpc  session  shell  skill  storage  tool
vcs  websearch  worktree

There is no catalog module and no catalog export. The only catalog symbol anywhere in the package is
ctx.mcp.resource.catalog(...) (McpResourceCatalogOperation), which is unrelated to publishing a provider/model
catalog.

Impact

  • Plugin authors following the migration guide can target a context member that does not exist.
  • Affects a real published plugin: cursor-oauth-opencode@0.22.1 (latest on npm, published 2026-08-20) ships a V2
    entrypoint and cannot load in OpenCode 2.0.29, so the providers it registers never become available.
  • Not a V1-compatibility regression: the plugin's V2 code path is what fails. The package's declared repository
    (https://github.com/jaredboynton/cursor-oauth-opencode) no longer resolves, so the docs/API contract is the
    actionable surface here.

Suggested resolutions

  1. If catalogs is a stale leftover in the migration guide's prose, list the domains that actually exist (or drop the
    sentence).
  2. If a catalog domain is planned for the public plugin API, document it under /build/plugins#api and expose it on the
    Promise and Effect contexts.
  3. Publishing a model catalog appears to be possible today through ctx.provider.transform / ctx.model.transform;
    if that is the intended replacement, the migration guide should say so, because that is exactly what this plugin was
    trying to do.

Frequency

Consistent — reproduces on every reload, not intermittently.

Activity

  1. opencode-agent commented on Oct 15, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report. I was able to reproduce this on 2.0.29.

    The script below starts with a fresh config and loads one local plugin from the global opencode.jsonc. In its setup(), the plugin calls ctx.catalog.transform(...), which is what the migration guide's "catalogs" domain suggests. It then checks GET /api/plugin. The plugin is reported as failed with TypeError: undefined is not an object (evaluating 'ctx.catalog.transform'). The context has no catalog key at all. It has provider, model, command, integration, mcp, reference, skill, storage, vcs, worktree, websearch and others.

    This still happens on the latest dev (7ced591c), and /build/plugins/migrate-v1 there still lists "catalogs". It looks like the catalog domain was split into separate provider and model registries in #48901, and the guide wasn't updated to match.

    As you suggested, ctx.provider.transform / ctx.model.transform look like the current way to publish providers and models from a plugin.

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Repro for #55628: the migrate-v1 guide lists a "catalogs" plugin context domain,
    # but ctx.catalog does not exist, so a plugin calling ctx.catalog.transform() fails to load.
    # Usage: ./repro.sh                                (uses `opencode` from PATH)
    #        OC_DEV=/path/to/opencode-checkout ./repro.sh   (runs from source)
    set -u
    P=$(mktemp -d /tmp/opencode/catalog-probe.XXXX)
    mkdir -p "$P/plugin"
    cat > "$P/plugin/index.ts" <<TS
    import { writeFileSync } from "node:fs"
    export default {
      id: "catalog-probe",
      async setup(ctx: any) {
        writeFileSync("$P/ctx-keys.txt", "ctx keys: " + Object.keys(ctx).sort().join(",") + "\ntypeof ctx.catalog: " + typeof ctx.catalog + "\n")
        // What a plugin trusting the migrate-v1 guide's "catalogs" domain would do:
        await ctx.catalog.transform(() => {})
      },
    }
    TS
    # Isolated HOME; load the plugin from the global config, like the report
    export HOME="$P/home"
    mkdir -p "$HOME/.config/opencode"
    printf '{ "plugins": ["%s"] }\n' "$P/plugin" > "$HOME/.config/opencode/opencode.jsonc"
    cd "$P"
    if [ -n "${OC_DEV:-}" ]; then
      OC=(bun --conditions=browser --preload="$OC_DEV/packages/tui/node_modules/@opentui/solid/scripts/preload.js" "$OC_DEV/packages/cli/src/index.ts")
    else
      OC=(opencode)
    fi
    "${OC[@]}" --version
    export OPENCODE_SERVER_PASSWORD=repro-$RANDOM
    PORT=$((40000 + RANDOM % 20000))
    "${OC[@]}" serve --hostname 127.0.0.1 --port $PORT >"$P/serve.log" 2>&1 &
    SERVE=$!
    for i in $(seq 1 30); do
      out=$("${OC[@]}" api --server http://127.0.0.1:$PORT get /api/plugin 2>/dev/null | tr '{' '\n' | grep -A3 '"id":"catalog-probe"')
      echo "$out" | grep -q '"status":"\(failed\|active\)"' && break
      sleep 2
    done
    echo "--- /api/plugin entry for catalog-probe:"; echo "$out"
    echo "--- from inside setup():"; cat "$P/ctx-keys.txt" 2>/dev/null
    kill $SERVE 2>/dev/null || true

    Run it: bash repro.sh

  2. KeenaNet commented on Oct 15, 2026

    @KeenaNet
    Author

    Additional evidence that narrows this down: this is not stale prose in the guide, it is an undocumented rename of a
    first-class domain.

    @opencode-ai/plugin@1.18.18 (the V1 SDK, which also ships a V2 plugin API) types the member explicitly:

    // dist/v2/effect/context.d.ts
    export interface PluginContext {
        readonly options: PluginOptions;
        readonly agent: AgentHooks & Reload;
        readonly aisdk: AISDKHooks;
        readonly catalog: CatalogHooks & Reload;   // present in 1.18.x
        readonly command: CommandHooks & Reload;
        readonly integration: IntegrationHooks & Reload;
        readonly plugin: PluginDomain;
        readonly reference: ReferenceHooks & Reload;
        readonly skill: SkillHooks & Reload;
    }
    // dist/v2/effect/catalog.d.ts
    export interface CatalogDraft {
        readonly provider: {
            list(): readonly CatalogProviderRecord[];
            get(providerID: string): CatalogProviderRecord | undefined;
            update(providerID: string, update: (provider: ProviderV2Info) => void): void;
            remove(providerID: string): void;
        };
        readonly model: {
            get(providerID: string, modelID: string): ModelV2Info | undefined;
            update(providerID: string, modelID: string, update: (model: ModelV2Info) => void): void;
            remove(providerID: string, modelID: string): void;
            readonly default: { get(): ...; set(providerID: string, modelID: string): void; };
        };
    }
    export type CatalogHooks = Hooks<{ transform: CatalogDraft }>;

    @opencode/plugin@2.0.29 has no catalog domain, but its ProviderEditor and ModelEditor carry the same operations
    as CatalogDraft.provider and CatalogDraft.model (plus add), and they are now top-level ctx.provider /
    ctx.model domains with their own transform and reload.

    So ctx.catalog.transform(...) and ctx.catalog.reload() no longer exist under that name — and a plugin author coding
    against the 1.18.x V2 API had a typed ctx.catalog to target.

    What is missing is the migration step. The plugin migration guide jumps from V1 hooks straight to
    ctx.provider.transform / ctx.model.transform without ever mentioning that ctx.catalog was renamed and split, while
    the same guide's prose still lists catalogs as a current context domain.

    Suggested mapping to document:

    1.18.x V2 API OpenCode 2 / @opencode/plugin
    ctx.catalog.transform(draft => …) ctx.provider.transform(...) and/or ctx.model.transform(...)
    ctx.catalog.reload() ctx.provider.reload() / ctx.model.reload()
    draft.provider.update(id, fn) editor.update(id, fn) on ctx.provider
    draft.model.update(providerID, modelID, fn) editor.update(providerID, modelID, fn) on ctx.model

    Concrete downstream effect, unchanged from the report: cursor-oauth-opencode@0.22.1 depends on
    @opencode-ai/plugin@^1.2.27, calls ctx.catalog in its V2 entrypoint, and cannot load on OpenCode 2.0.29.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions