npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nathwn12/oc-bifrost

v1.4.3

Published

The rainbow bridge for OpenCode plugins — run V1-era plugin hooks on the OpenCode V2 runtime; unmapped hooks are refused loudly instead of silently dropped.

Readme

🌈 oc-bifrost

Run V1-era OpenCode plugin hooks on the OpenCode V2 runtime. V2 hard-rejects them at load; this small plugin loads them anyway.

npm check license: MIT downloads


⚡ Quick start

OpenCode V2 intentionally broke the plugin API — a V1 module is hard-rejected at load:

Plugin must export a default definition with an id and an effect or setup function. — packages/core/src/plugin/module.ts

The bridge is one entry in the plugins array of your opencode.jsonc; OpenCode resolves the package itself, so there is no separate install step.

// opencode.jsonc
{
  "plugins": [
    {
      "package": "@nathwn12/[email protected]",
      "options": {
        "trustRemote": true, // consent: the first `github:` fetch downloads and executes a remote plugin
        "plugins": ["github:obra/superpowers"]
      }
    }
  ]
}

Restart OpenCode. That's the whole setup — the mount report names what bridged and what was refused. Proof: PROOF.md, VERIFIED-PLUGINS.md; agent-driven setup: INSTALL.md.

🧭 What actually mounts — two paths

A module named in options.plugins is routed by its shape, and only the V1 shape is hook-translated:

  • V1 hook module - a factory (a default async export, { server: factory }, or any named function export - a *Plugin name is only the tie-breaker preference, src/discover.ts). Its hooks are translated one by one against the compatibility matrix below.
  • V2-shaped definition — an export default carrying { id, setup | effect }. It mounts as-is with the host context, exactly as the host itself would have mounted it. A dual-export file that ships both a V1 named export and a V2 default (for example obra/[email protected]) takes this path: the V2 default is used and the V1 named export is left untouched — no hook translation is applied to it.

Sourcing is github: / a local path / a bundled preset: only. npm and bare package names are refused (src/index.ts); point at an installed copy by absolute path instead. And a plugin that needs one of the seven refused V1 hooks still needs a real port — the bridge will not fake it.

Name each legacy plugin with exactly one of three specifiers:

  • github:<owner>/<repo>[@<ref>][#<path>] — The advertised, default route — snapshot-first. The ref resolves to a commit once, at first fetch (the default branch when no @<ref>), then the repository tarball is fetched from codeload at that resolved commit and materialized as a whole tree — sibling files exist beside the plugin entry, so obra/superpowers reads its own skills/. Caps: 16 MiB compressed · 64 MiB uncompressed · 5000 files. Hostile archives are refused outright, never materialized — path-traversal, absolute/.. escapes, NUL/backslash names, duplicate paths; a link or device entry refuses the snapshot too. The old single-file fetch is the loud fallback for an over-cap, malformed, or candidate-less snapshot: the mount note names the loss — sibling files are NOT available, and a plugin that reads them by relative path is inert. Never a silent downgrade. No #<path> → hooks/opencode/<repo>.ts, hooks/opencode/index.ts, plugin.ts, index.ts are probed in order inside the tree, and a miss lists every path it tried.
  • A local path — resolves against the session directory; park it in legacy/, never a discovery directory.
  • A bundled, zero-fetch fallback — for offline / air-gapped hosts; documented in the fallback section below.

Version gate. github: requires oc-bifrost 0.4.0 or later; releases 0.3.0 and below cannot mount it — use the offline fallback or a local path there.

📌 Version choice & updates

  • Pin the exact version — "package": "@nathwn12/[email protected]". Predictable, and the version these docs describe.
  • @^1.0.0 auto-tracks 1.x and never adopts a new major silently.
  • A bare @nathwn12/oc-bifrost or @latest may be unstable while OpenCode's plugin cache settles.
  • If an update does not appear: run opencode plugin check; if it still does not, delete ~/.cache/opencode/npm/@nathwn12/oc-bifrost@latest and reload.

🔐 Trust model

Mounting a github: plugin downloads the repository snapshot at the resolved commit and executes its entry file in the host process, with your rights. The sha256 recorded on first use pins those exact bytes — the tarball and the entry file alike — it does not vouch for them.

  • Cold cache refuses by default — nothing is fetched or executed until you opt in, per bridge entry: "trustRemote": true, or OC_BIFROST_TRUST=github (an explicit false wins over the env var).
  • Warm, hash-verified cache loads on its own: no re-consent, no network. A hash mismatch refuses loudly instead of running unverified bytes.
  • The mount report keeps consent informed — one line naming the resolved commit, the sha256 digest, the byte count, and that it executes with the host process's full user rights.
  • Offline or air-gapped — the cold-cache fetch is fail-closed; pre-warm on a networked machine and copy the shared oc-bifrost/github/v2/ cache across. It lives under $XDG_CACHE_HOME/opencode/ or, by default, ~/.cache/opencode/; snapshot trees materialize at …/oc-bifrost/github/v2/<id>/tree/. A flat pre-snapshot cache at …/oc-bifrost/github/<id>/ is ignored with a warning (it cannot provide sibling files) and re-fetched with the same one-time consent.

🔌 Offline / no-fetch fallback — preset:rtk (optional)

Optional; not the advertised route. When GitHub is unreachable — an offline or air-gapped host — preset:rtk mounts a bundled V1 plugin (vendor/rtk.ts, verbatim rtk-ai/rtk v0.50.0, Apache-2.0) with zero network. It is opt-in, so nothing from RTK runs unless you ask for it.

{ "package": "@nathwn12/[email protected]", "options": { "plugins": ["preset:rtk"] } }

Prerequisite: the rtk binary (>= 0.23.0) on PATH; preset:rtk probes before mounting and names this command if it is missing. No winget? Take the release asset from rtk-ai/rtk instead. Not from crates.io — cargo install rtk installs a different project.

winget install rtk-ai.rtk        # Windows
brew install rtk                 # macOS / Linux

⚠️ Do not run rtk init -g --opencode. It writes rtk.ts into ~/.config/opencode/plugins/, a discovery directory where V2 hard-rejects V1 modules. preset:rtk exists precisely so you never touch that path.


🧾 The mount report — and where to read it when stdout is gone

The per-plugin mount report is the proof surface: it names every hook that bridged (full), approximated (partial), or was refused (unsupported), plus each github: resolved commit and digest. It prints to the console (verbose: true, the default).

The host discards stdout when it runs as a managed background service or a stdio server — exactly the modes where nobody is watching a terminal — so the same lines are mirrored to a file:

  • Default: ~/.cache/opencode/oc-bifrost/report.log (or $XDG_CACHE_HOME/opencode/oc-bifrost/report.log when XDG_CACHE_HOME is set) — the same shared user cache github: artifacts live under.
  • Override: OC_BIFROST_REPORT=<path> writes somewhere else; OC_BIFROST_REPORT=off disables the file.
  • Policy: append across loads (a crashed or exited run is still readable), hard-capped at 256 KiB — a write that would cross the cap rolls the file over so the newest report survives whole. Control characters are escaped before they reach disk; console behaviour is unchanged.

🚫 Never leave a V1 plugin in a discovery directory

V2 loads .opencode/plugin/, .opencode/plugins/, <config>/plugin/, and <config>/plugins/ directly and hard-rejects the module before oc-bifrost can see it. Park legacy files in legacy/ and reference them from options.plugins; the bridge also warns at load if it finds one stranded.

oc-bifrost bridges hooks, not a plugin's external dependencies: if a plugin shells out to a binary, that binary must exist on PATH or the plugin disables itself — correctly, and usually quietly. RTK in the offline fallback above is one example.


🧩 Compatibility matrix

The unit of compatibility is the V1 hook, not the plugin — once a hook is bridged, every plugin that uses it works untouched.

6 🟢 full · 9 🟡 partial · 9 🔴 refused — all 24 matrix rows (21 V1 hooks + 3 client-facade methods):

| V1 hook | Level | V2 destination | |---|---|---| | tool.execute.before | 🟢 full | ctx.tool.hook("execute.before") — mutation write-back verified | | shell.env | 🟢 full | ctx.shell.hook("create.before") | | chat.headers | 🟢 full | ctx.session.hook("model.request") | | permission.ask | 🟢 full | ctx.permission.hook("evaluate") | | dispose | 🟢 full | setup cleanup return | | tool.execute.after | 🟡 partial | ctx.tool.hook("execute.after") — title ignored | | chat.params | 🟡 partial | ctx.session.hook("context") | | chat.message | 🟡 partial | ctx.session.hook("prompt") | | tool.definition | 🟡 partial | ctx.tool.transform (apply-time snapshot) | | tool | 🟡 partial | ctx.tool.transform editor.add | | event | 🟡 partial | ctx.event.subscribe() - V2 session.execution.succeeded\|failed\|interrupted synthesised to the V1 session.idle envelope; other names/payloads pass through | | client.session.messages | 🟡 partial | ctx.session.context — active context (post-compaction) only; the full transcript is HTTP-only | | experimental.chat.system.transform | 🟡 partial | ctx.session.hook("context") | | experimental.chat.messages.transform | 🟢 full | ctx.session.hook("context") — V1 {info,parts} envelope pre-fill + write-back | | experimental.session.compacting | 🟡 partial | ctx.session.hook("compaction") | | client.session.children | 🔴 refused | no plugin-scoped child listing in V2 (HTTP-only; the plugin context carries no server address) | | client.tui.showToast | 🔴 refused | tui.toast.show is a TUI-process event; no server-plugin toast surface | | config | 🔴 refused | per-domain transforms with different semantics | | auth | 🔴 refused | ctx.integration.transform + integration APIs | | provider | 🔴 refused | ctx.provider.transform / ctx.model.transform | | command.execute.before | 🔴 refused | no one-to-one global V2 hook | | experimental.provider.small_model | 🔴 refused | no V2 equivalent | | experimental.compaction.autocontinue | 🔴 refused | no V2 equivalent | | experimental.text.complete | 🔴 refused | no V2 equivalent |

No hook is ever dropped silently. Refused hooks warn at load, or abort under strict: true. The single source of truth is src/compat-matrix.ts, and every row names the test that proves it.


🙏 Honest bounds

This bridges the mappable subset, not "any plugin, seamlessly." Nine of the twenty-four matrix rows are refused out loud (seven V1 hooks and two facade calls): no faithful V2 destination exists for their semantics, and no compatibility layer can invent one. Plugins that depend on those need a real port. The refusal list is the product being honest — and it is the contract.


⚙️ Options

| Option | Type | Default | Meaning | |---|---|---|---| | plugins | Array<string \| { spec, options }> | [] | Specifiers to bridge — github:, a local path, or the bundled offline fallback (preset:) | | trustRemote | boolean | false | Consent to fetch + execute a github: plugin on a cold cache | | strict | boolean | false | Abort setup on an unsupported or unmountable hook | | verbose | boolean | true | Print the per-plugin compatibility report | | freshness | "off" \| "online" | "off" | Check the bundled pin against upstream's latest release after mounting | | provision | "host" \| "npm" \| "off" | "host" | How a fetched github: snapshot's declared dependencies are provided before its entry is imported - "host" junctions them from the shared OpenCode npm cache (zero network), "npm" adds an npm install --no-save fallback for packages the host store lacks, "off" is 1.3.x behavior | | wireTui | boolean | false | Opt-in TUI wiring for a mounted github: snapshot - ensures a tui.tsx wrapper at the tree root and adds the tree as a file:// plugin entry in cli.json; snapshot layouts only (the single-file fallback is never wired), and a wire failure is a loud row that never aborts the mount |

freshness: "online" (or OC_BIFROST_FRESHNESS=online) is off by default, never downloads or executes plugin code, fires off the load path, and reports unknown - not an error - when offline or rate-limited.

OC_BIFROST_PROVISION sets the provision mode when the option is omitted ("host", "npm", or "off"); an explicit option wins, and an invalid value is a loud refusal. OC_BIFROST_WIRE_TUI opts wireTui in with exactly "1" or "true"; an explicit option wins, anything else is off.

Wire a TUI plugin from github:

// opencode.jsonc
{
  "plugins": [
    {
      "package": "@nathwn12/[email protected]",
      "options": {
        "plugins": ["github:obra/superpowers"],
        "trustRemote": true, // consent: the first `github:` fetch downloads and executes a remote plugin
        "wireTui": true // after mount, add a tui.tsx wrapper and a cli.json plugin entry
      }
    }
  ]
}

The durable report file (see above) is controlled by the OC_BIFROST_REPORT environment variable, not an option: it defaults to the shared OpenCode cache and can be redirected to a path or disabled with off.


🛠 Develop

npm install
npm run check      # typecheck + build + tests

Contributing

Two doors, both gated on proof:

  • Add hook coverage — edit src/compat-matrix.ts, implement the bridge, add the test its row names.
  • Add a verified plugin — smoke-test it and add a row to VERIFIED-PLUGINS.md.

See CONTRIBUTING.md.

License

MIT