Teardown: herdr-agent-usage

levi-qiao/herdr-agent-usage (Rust, ~146 stars) shows quota and context usage for a dozen agents in one pane. Study it for the event/data plugin stack, per-agent data plumbing, and credential scoping. Source mirror: _repos/levi-qiao-herdr-agent-usage/.

Manifest: the Rust event/data shape

id = "herdr-agent-usage"
min_herdr_version = "0.9.0"

[[build]]
command = ["cargo", "build", "--release"]

[[startup]]
command = ["sh", "-c", "exec \"$HERDR_PLUGIN_ROOT/target/release/herdr-agent-usage\" startup --provider all"]

[[actions]]
id = "refresh"
title = "Refresh agent quota"
command = ["sh", "-c", "exec \"$HERDR_PLUGIN_ROOT/target/release/herdr-agent-usage\" refresh --provider all --force"]

[[events]]
on = "pane.agent_status_changed"
command = ["sh", "-c", "exec \"$HERDR_PLUGIN_ROOT/target/release/herdr-agent-usage\" event"]

Three things to copy:

  • Subcommands map to invocation kinds. The binary is one crate with startup, event, focus, layout, refresh, configure, dashboard subcommands; each manifest section calls the matching one. One binary, many entrypoints.
  • Every command goes through sh -c exec "$HERDR_PLUGIN_ROOT/target/release/...". This is the ecosystem-standard Rust launcher line; the exec replaces the shell so no process lingers.
  • A configure action repairs the world. It applies detection manifests and calls herdr server reload-config, making installation self-healing without a reinstall.

Code organization

ModuleSizeRole
providers/*one per agentCredential + data readers (opencode, omp, devin, kilo, cursor, …)
refresh.rs~4,200 linesQuota fetching over ureq, per provider
presentation.rs~3,000 linesPane and sidebar rendering
opencode.rsdedicatedOpenCode SQLite access

The lesson: the value is data plumbing, not herdr API usage. The herdr-facing surface is a handful of commands and reads; 90% of the code is “get per-provider usage data reliably”. Plan your effort accordingly.

The OpenCode SQLite reader

src/opencode.rs is the cleanest documented OpenCode data access in the ecosystem:

const SESSION_BY_ID: &str = "SELECT id FROM session WHERE id = ?1 LIMIT 1";
const SESSION_BY_ID_V2: &str = "SELECT id FROM session_v2 WHERE id = ?1 LIMIT 1";
  • OpenCode 1.x writes session/message; OpenCode 2 writes session_v2/session_message and keeps v1 tables as migration source. A session id lives in exactly one of them.
  • The reader probes sqlite_master for table presence and tries each layout; an absent table means “layout not in use”, not corruption.
  • All queries are bounded point lookups (LIMIT 1, last-8-messages), never scans. In OpenCode 2 the role moved from the JSON payload into the type column.
  • Opened read-only via rusqlite (bundled feature).

The seam back to herdr is session identity: herdr knows each pane’s native agent session id (see agent_session on pane get), and the plugin matches that id inside the db. That pairing is what makes per-pane usage rows possible.

Credential scoping

The plugin reads credential files and keystores per provider (Cursor’s state.vscdb key, Claude/Codex token files, omp’s models.db, Devin’s sessions.db). Its discipline is worth copying: read only the specific keys needed, distinguish “has secret” from “oauth” credential kinds, and treat absence as “provider not configured”, never an error.

Where this matters for the lab suite

  • peteretelej.cost-tokens should read this repo’s OpenCode layout probing before writing its own.
  • The serde_json preserve_order feature is used so rewriting a user’s config file does not reorder their keys. A small touch that shows in reviews.
  • The startup + event + focus + layout event grid is a good map of “which events does a data-refresh plugin actually need”.