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,dashboardsubcommands; 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; theexecreplaces the shell so no process lingers. - A
configureaction repairs the world. It applies detection manifests and callsherdr server reload-config, making installation self-healing without a reinstall.
Code organization
| Module | Size | Role |
|---|---|---|
providers/* | one per agent | Credential + data readers (opencode, omp, devin, kilo, cursor, …) |
refresh.rs | ~4,200 lines | Quota fetching over ureq, per provider |
presentation.rs | ~3,000 lines | Pane and sidebar rendering |
opencode.rs | dedicated | OpenCode 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 writessession_v2/session_messageand keeps v1 tables as migration source. A session id lives in exactly one of them. - The reader probes
sqlite_masterfor 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 thetypecolumn. - Opened read-only via
rusqlite(bundledfeature).
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-tokensshould read this repo’s OpenCode layout probing before writing its own.- The
serde_jsonpreserve_orderfeature 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”.