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

@agenticcontrolplane/opencode

v0.3.1

Published

Deterministic governance for OpenCode via the Agentic Control Plane — allow / deny / ask on every tool call, fail-open, never bricks your session. Plus zero-credential local metering: per-call tokens, cache hit rate, tool errors, per-task cost (`acp-openc

Readme

@agenticcontrolplane/opencode

Deterministic governance for OpenCode — via the Agentic Control Plane.

Every tool call your OpenCode agent makes is checked against server-side policy before it runs: allow skips the prompt (pre-approved), deny blocks it with the policy reason, and ask escalates to OpenCode's own native once/always/reject permission gate. Every result is audited with session attribution, visible on the team dashboard.

Plus a second, independent plane that needs zero credentials: local metering. Every LLM request (token buckets including cache read/write, cost) and every tool result (status, duration) is recorded into SQLite on your machine — read it back with acp-opencode report. Nothing leaves the box.

Companion to the Claude Code and Hermes ACP plugins — same backend contract, same dashboard, same policies, same local report — wired into OpenCode's plugin hooks.

Install

Add the plugin and a permission block to opencode.json (global: ~/.config/opencode/opencode.json, or per-project at the repo root):

{
  "$schema": "https://opencode.ai/config.json",
  "plugin": ["@agenticcontrolplane/opencode"],
  "permission": {
    "bash": "ask",
    "edit": "ask",
    "webfetch": "ask"
  }
}

OpenCode auto-installs the package at startup. The permission block matters: the plugin's primary interception point (permission.ask) only fires for tools whose permission resolves to "ask" — without it you still get the deny-only backstop and audit, but ACP allow can't pre-approve prompts away and ACP ask can't reach the native gate.

Then provide credentials (get a key with the ACP dashboard, or reuse the one your other ACP plugins wrote):

export ACP_BEARER_TOKEN="gsk_yourslug_..."
# or
mkdir -p ~/.acp && echo "gsk_yourslug_..." > ~/.acp/credentials

The env var wins over the file. No credentials configured means governance is a no-op — OpenCode behaves exactly as if it weren't installed. Local metering (below) runs either way; it never needs an account.

Zero-install local option: drop src/index.ts into ~/.config/opencode/plugins/ (or .opencode/plugins/ in a project).

Optional: route model traffic through the ACP proxy

To get cost metering and spend correlation on the same dashboard, point OpenCode at the ACP proxy as a provider — see opencode.json.example for the full config:

{
  "provider": {
    "acp": {
      "npm": "@ai-sdk/openai-compatible",
      "name": "Agentic Control Plane",
      "options": {
        "baseURL": "https://api.agenticcontrolplane.com/v1",
        "apiKey": "{env:ACP_BEARER_TOKEN}"
      },
      "models": { "gemini-3.5-flash": {} }
    }
  },
  "model": "acp/gemini-3.5-flash"
}

The plugin stamps X-GS-Session on every LLM request, so proxy-side spend joins the hook-side audit rows for the same session.

The installer writes this provider block and a launcher, ~/.acp/bin/opencode-acp, that selects acp/<model> for one launch. Under plain opencode the plugin still governs every tool call but never sees a model call, so the first allowed call of each session says so, once (v0.3.0+):

[ACP] Tool calls in this session are checked and logged. Model calls are not: plain `opencode` sends them straight to the provider, so they are neither priced nor policy-checked (tool-result redaction, model routing). For the cost X-ray and model-call policy, launch with `opencode-acp` (~/.acp/bin/opencode-acp). Local token metering (`acp-opencode report`) is unaffected. Shown once per session.

It arrives as a TUI toast and on the server log. A session started by the launcher (which exports ACP_KEY), one whose provider already points at the ACP proxy (seen on chat.headers), or one with ANTHROPIC_BASE_URL/OPENAI_BASE_URL at the proxy never sees it; neither does local mode. It rides an allow only — never a deny, an ask, or an outage — and once per session is enforced with a marker under ~/.acp/session-notices/ (pruned after 7 days, capped at 200).

How it works

| Hook | Behavior | |---|---| | permission.ask | POSTs to /govern/tool-use. ACP allow sets the permission status to allow (prompt skipped), deny blocks, ask leaves the status untouched so OpenCode's native once/always/reject gate fires. | | tool.execute.before | Deny-only backstop for tools not routed through the permission system — an ACP deny throws [ACP] Denied by policy: <reason>. Also caches args + start time by callID. | | tool.execute.after | Fire-and-forget POST to /govern/tool-output for server-side audit (output capped at 200 KB). | | chat.headers | Stamps X-GS-Session / X-GS-Client on every model request for run correlation. |

One governed tool call produces one /govern/tool-use POST: when both tool.execute.before and permission.ask fire for the same callID, the decision is fetched once and reused.

No workspace credentials on the machine? See Local mode below — the same hooks, decisions from an on-device engine instead.

Local mode (no account, no network)

Governance without a workspace: decisions come from the same on-device engine (~/.acp/decide.mjs) the Claude Code / Cursor / Codex ACP integrations use when installed with install.sh --local — classify the call, apply the hardline safety floor, then walk ~/.acp/policy.json from most-specific key to least. Every call appends one line to ~/.acp/audit.jsonl. No /govern/tool-use or /govern/tool-output request ever leaves the machine.

Enable it with either:

export ACP_LOCAL=1

or the plugin option in opencode.json:

{
  "plugin": [["@agenticcontrolplane/opencode", { "local": true }]]
}

Local mode also turns on automatically, with no flag, whenever no workspace credentials are configured (no ACP_BEARER_TOKEN, no ~/.acp/credentials) and ~/.acp/decide.mjs is present — i.e. the machine was set up with the installer's --local mode for another harness. A workspace credential always wins toward cloud mode: if ACP_BEARER_TOKEN (or ~/.acp/credentials) is set, this plugin behaves exactly as the cloud section above describes, with no change.

What works

| Hook | Local behavior | |---|---| | permission.ask | Local allow skips the prompt, deny blocks it, ask leaves OpenCode's native gate to fire — identical mapping to cloud. Attended: a human can see the native prompt, so an unavailable/broken engine fails open, with one loud [ACP·local] warning per session — the tool call proceeds ungoverned rather than hanging or bricking the session. | | tool.execute.before | Same deny-only backstop as cloud. Unattended: there is no ask primitive at this hook, so a local ask verdict — and an engine failure — both fail closed (throw), the mirror image of permission.ask's fail-open. This matches the attended/unattended split already used by the Claude Code / Codex hooks and the dsh-acp / pi-acp plugins. | | tool.execute.after | Appends the post audit line to ~/.acp/audit.jsonl. No network call. | | chat.headers | Unaffected by mode — always stamps X-GS-Session / X-GS-Client (harmless if nothing reads them without a workspace). | | Local metering (acp-opencode report) | Unaffected by mode — it already runs with zero credentials, local or cloud. |

What does not work

  • No server-side DLP/redact/shadow-notice on tool output. The cloud path's /govern/tool-output scan (redaction, shadow-mode counterfactual notices) has no on-device equivalent — decide.mjs only makes pre-call allow/ask/deny decisions. Local mode's tool.execute.after only audits; it never rewrites a tool's output.
  • No dashboard. ~/.acp/audit.jsonl is the only record — there's no console, no cross-session view, no team policy sync. That's the difference the hosted product adds.
  • No tuned risk classifier. decide.mjs's classifier and hardline safety floor are intentionally simple and reviewable (see the file's own header comment); the hosted gateway's classifier is more capable.
  • ask has nowhere to go outside permission.ask. OpenCode's plugin API exposes exactly one native approval surface (permission.ask, and only for tools whose permission config resolves to "ask" — see Install above). Everything else funnels through tool.execute.before, which can only allow or throw-to-deny; there's no way for a plugin to summon an interactive prompt from that hook. A local ask verdict reaching tool.execute.before therefore denies rather than pausing for approval — same posture Codex's cloud hook already uses (askdeny) when its harness has no native ask primitive either.
  • Engine version drift. This plugin dynamically imports whatever ~/.acp/decide.mjs the installer wrote for that machine (resolved at runtime from ACP_HOME, or HOME/os.homedir()) rather than bundling its own copy — it stays in lockstep with the Claude Code / Cursor / Codex integrations automatically, but also means an old or hand-edited decide.mjs behaves however that file behaves, not necessarily how this README describes.

Local metering (acp-opencode report)

Works with zero credentials — no login, no network. The plugin's event hook taps OpenCode's event bus and writes to SQLite at ~/.acp/opencode-local.db (override: ACP_LOCAL_DB), with the same schema as hermes-acp's ~/.acp/hermes-local.db:

  • model_calls — one row per LLM API request: model, provider, mode, input/output/reasoning tokens, cache read/write tokens, cost, session/task/turn ids.
  • tool_calls — one row per tool execution: tool, status (ok/error), duration, result size.
  • turns — present for schema parity with hermes-acp; not yet populated on OpenCode (see Gaps).

Read it back:

npx @agenticcontrolplane/opencode report            # human-readable: spend by model, cache hit rate, tool errors, per-task cost
npx @agenticcontrolplane/opencode report --json     # machine-readable — agents can read their own economics
npx @agenticcontrolplane/opencode report --days 30  # widen the window (default 7)

(acp-opencode is on PATH directly if you npm install -g @agenticcontrolplane/opencode.)

Where the numbers come from (OpenCode 1.18.4)

  • Usage source: message.part.updated events with part.type === "step-finish" — one part per LLM API request, carrying that single step's token buckets and the cost OpenCode computed in-process from its models.dev pricing data. message.updated (assistant) supplies model/provider metadata and the task mapping (parentID = the user message that started the turn, so "per-task cost" means per user request). A completed assistant message that produced no step-finish parts is recorded once as a message-level fallback row.
  • Tool source: message.part.updated with a tool part reaching completed or error — which also captures errored calls that the tool.execute.after hook never sees.
  • Pricing honesty: this plugin maintains no price table. Cost comes from OpenCode's own in-process pricing; when OpenCode reports cost 0 (unknown model, or subscription-included auth) the call is stored with cost_usd = NULL and surfaces in the report as N calls have no price — token counts stay exact, money is never guessed. Authoritative pricing for governed spend comes from routing model traffic through the ACP proxy (below), which meters server-side.

Metering kill switch

export ACP_LOCAL_METERING=off   # also: 0 / false / disabled

disables local metering only (governance unaffected). Metering also fails open: the first storage error disables it for the session with one loud warning and your agent keeps running.

Gaps vs hermes-acp report

  • Context composition (system/user/assistant/tool share of prompt chars) is not populated: OpenCode 1.18.4 exposes the outgoing message array only via the unstable experimental.chat.messages.transform hook. The turns table exists so the report gains the section automatically once a stable hook lands.
  • Per-request API duration is not exposed per step; message-level fallback rows carry wall-clock duration, step rows record 0.
  • Proxy-side metering (ACP provider config below) covers both gaps server-side for routed traffic — priced spend, durations, and run correlation on the dashboard.

Fail-open (and opting in to fail-closed)

Network errors, timeouts (>2 s), non-2xx responses, and malformed bodies all fail open by default — the tool call proceeds and a single loud console.warn is emitted per session. ACP must never block or break your work. If every call is being blocked, that's a deny policy, not an outage — check the dashboard.

Security-sensitive setups can flip that to fail-closed — block tool calls whenever the gateway is unreachable — with the same contract the Claude Code / Cursor / Codex integrations use, so one setting covers every harness:

export ACP_FAIL_MODE=closed     # or: echo closed > ~/.acp/failmode

A policy deny always blocks regardless; fail mode only governs what happens when ACP can't be reached. An unprovisioned machine (no credentials) is always a no-op — fail-closed never blocks before you've connected.

Kill switch

export ACP_OPENCODE=off

disables the plugin entirely (no governance calls, no header stamping, no local metering) without touching your config.

Client identity

Sends X-GS-Client: opencode-plugin/<version> so the dashboard, policy router, and audit log can distinguish OpenCode traffic from Claude Code / Hermes / Cursor / etc.

Other knobs

export ACP_API_BASE="https://api.agenticcontrolplane.com"  # default

Development

npm install
npm test        # vitest, fully offline — ACP endpoints are mocked, SQLite goes to temp dirs
npm run test:node  # node --test against compiled dist/ — local-mode coverage (builds dist/ first)
npm run build   # tsc -> dist/

The metering store picks a SQLite driver at runtime: bun:sqlite (OpenCode's plugin runtime), node:sqlite (Node >= 22), or the sqlite3 CLI as a last resort — same SQL on every path, zero npm dependencies.

License

MIT