Omahub
← All plugins
R

Agent collectors

by rohaquinlop

Extensible usage collectors for AI coding agents (Pi, OpenCode, and community adapters). Writes records the stock omarchy.agents panel displays.

Security review

No obvious issues detected

Deterministic scan — not a security guarantee

None
Risk level
None
Analyzed commit
51483c2
Scanned
1 month ago

No potentially dangerous behavior detected in the analyzed commit.

Automated analysis only — not a security guarantee.

AI advisory review

No obvious issues detected

Language-model assessment · ~deepseek/deepseek-v4-flash-latest — advisory only

None
AI risk level
None
Recommendation
install
Model
~deepseek/deepseek-v4-flash-latest
Analyzed commit
51483c2
Reviewed
1 month ago

The plugin is a well-structured usage collector that reads local agent session data and writes aggregated records to a state directory. It runs user-defined adapter hooks, but those are explicitly user-provided and the engine applies strict resource caps. No obfuscation, persistence, credential theft, or destructive behavior was found.

How this check works

This review combines the deterministic scan (the rule-based results above) with an independent look at the plugin's code by a language model. The model reads a trimmed sample of the repository's files, the manifest, and the README, then gives a plain-language risk level and a recommendation: install (no notable danger), review (look closer first), or avoid (clearly dangerous).

It runs on the same analyzed commit as the deterministic scan and is strictly advisory — it is not a security guarantee and never blocks a plugin by itself. A human moderator still reviews plugins before they are listed.

AI advisory only — automated analysis, not a security guarantee.

Install
$ omarchy plugin add https://github.com/rohaquinlop/omarchy-agent-collectors --enable
Developer Tools #bar #quickshell #ai

omarchy-agent-collectors

Extensible usage collectors for AI coding agents on Omarchy.

<p align="center"> <img src="preview.png" alt="The omarchy.agents panel showing Claude, OpenCode, and Pi tabs" width="320"> </p>

One engine, many drop-in adapters. Writes stock-contract records into ~/.local/state/omarchy/agents/usage/, where Omarchy's built-in omarchy.agents bar panel picks them up automatically — no UI code involved.

Out of the box: Pi (~/.pi/agent/sessions) and OpenCode (~/.local/share/opencode/opencode.db). Anything else is one manifest away.

Install

omarchy plugin add https://github.com/rohaquinlop/omarchy-agent-collectors.git --enable

The service runs the engine at shell start and every 15 minutes. Records for every detected agent appear within seconds; the Agents panel grows a chip per agent (h/l to switch).

To fully uninstall:

omarchy plugin remove rohaquinlop.agent-collectors
rm -f ~/.local/state/omarchy/agents/usage/pi.json \
      ~/.local/state/omarchy/agents/usage/opencode.json
rm -rf ~/.cache/omarchy/agent-collectors

How it works

adapters/<id>/manifest.json   declarative source + field map ┐
~/.config/omarchy/agent-collectors/adapters/<id>/…           ├→ engine → ~/.local/state/omarchy/agents/usage/<id>.json
(optional) collect/limits hooks                              ┘

Per run the engine:

  1. Discovers adapters — built-in adapters/, then the user dir (same id = user wins).
  2. Filters: skips an id when an official omarchy-agent-usage-<id> collector exists (superseded), when disabled via widget settings, or when its detect paths are missing.
  3. Collects canonical events {ts, session, model, kind, input, output, cacheRead, cacheWrite}.
  4. Aggregates today / last-7-days / all-time / per-model token buckets.
  5. Writes <id>.json atomically (temp + rename, mode 600). One failing adapter never blocks others.

State (per-file parse caches) lives in ~/.cache/omarchy/agent-collectors/state.json; unchanged sources are not re-parsed. Delete it (or pass --force) for a full rescan.

CLI

~/.config/omarchy/plugins/rohaquinlop.agent-collectors/bin/agent-collectors --validate   # list adapters
... --force                       # full rescan
... pi                            # only this adapter
... --except opencode             # everything but

Widget settings honored

If you disable a provider in the Agents widget settings (providers.<id>.enabled: false under the omarchy.agents bar entry), the engine stops writing its record and the panel drops the tab.

Writing an adapter

Create ~/.config/omarchy/agent-collectors/adapters/<id>/manifest.json: no plugin code is ever touched — the engine discovers adapters from this directory on every run. A user adapter with the same id as a built-in one overrides it.

{
  "schemaVersion": 1,
  "id": "gemini",
  "name": "Gemini",
  "detect": [{ "path": "~/.gemini" }],
  "sources": [{
    "format": "jsonl-lines",
    "glob": "~/.gemini/tmp/**/*.chats.json",
    "kindPath": "type", "kinds": ["message"],
    "rolePath": "message.role", "promptRole": "user", "completionRole": "assistant",
    "timestampPath": "timestamp", "modelPath": "message.model",
    "tokens": { "input": "message.usage.input", "output": "message.usage.output" }
  }]
}
  • Formats: jsonl-lines (glob + per-line field map) or sqlite-query (read-only database + query + column map; timestampUnit: "ms" if needed).
  • Dot paths index nested JSON (message.usage.input).
  • Prompts are events whose role matches promptRole; completions match completionRole and carry tokens.
  • sessionIdPath optional — defaults to the file stem (one session per file).

For anything the field map can't express, ship executables next to the manifest and reference them:

{ "collect": "collect.sh", "limits": "limits.sh" }
  • collect prints one canonical-event JSON object per line on stdout.
  • limits prints a limits-array JSON on stdout; exit 1 means "no data", not failure.
  • Both run with the adapter directory as cwd; FORCE=1 is set on forced runs.

Validate your setup with bin/agent-collectors --validate. A broken adapter is skipped with a warning; it can never take other agents down.

Resource bounds

The engine keeps memory and persistent state bounded by caps, never by history size.

  • State holds per-file append cursors and per-adapter counters only; event lists are never stored or re-read.
  • Collectors yield events one at a time into the accumulator; per-run memory is proportional to one event plus one line/row buffer (collect hooks add their capped output budget), never to history size. First runs and migrations parse the full history without materializing it.
  • JSONL files are read append-only from the last byte offset; a truncated or rotated file triggers a full rescan, and a head fingerprint forces a full rescan when a file was replaced and regrown. A straddle line left by a crash-interrupted append is re-attached and parsed once. A mid-file rewrite that keeps the head and grows the file is not detected (JSONL session logs are append-only in practice). The scan stops at 10,000 matched files per source per run (the glob is not walked further), and lines longer than 1 MiB are drained with bounded reads and skipped (the stored straddle tail is capped at 8 KiB).
  • SQLite reads stream row by row; with a ts column only rows at or after the last seen timestamp are fetched, and rows sharing that boundary timestamp are deduplicated by fingerprint (adapter queries SHOULD ORDER BY the timestamp column). Rows whose string fields exceed 1 MiB in total are skipped, and session/model strings are truncated at 256 characters before they are retained in state. The same 256-character truncation applies to session and model strings from jsonl lines and hook output (in make_event, before anything reaches state).
  • Hook stdout is streamed and capped: 100,000 lines with 1 MiB of line buffering and a 16 MiB cumulative byte budget for collect, 1 MiB for limits; hook stderr is capped at 1 MiB. A hook that exceeds a cap is killed and its partial events are kept; other adapters are unaffected. A hook that daemonizes cannot hang the engine (bounded join).
  • Hooks are stateless: they may re-emit their full history. The engine deduplicates hook events by fingerprint (50,000 per adapter); the same event is never counted twice.
  • The service streams engine stderr in raw chunks with a 1 MiB cap applied to the run total and to the pending buffer (no line buffering, so neither a flood of short lines nor one newline-free line bypasses the cap); the caps reset and the parser re-attaches on every run.
  • Session ids: the newest 10,000 are kept per adapter; beyond that totalSessions is the stored count plus the evicted count (approximate). Session and model strings never exceed 256 characters.
  • Per-day token counters stay inside the rolling window: events dated before the window or after today update session/model/date counters but add no day entries.
  • Adapter manifests larger than 1 MiB are skipped with a warning; a shell.json larger than 64 MiB is treated as having no disabled providers.
  • Model buckets: 50 per adapter; further models roll into other.
  • Active dates: 730 days are kept; older ones still count toward activeDays.
  • A state file larger than 64 MiB is ignored and rebuilt by a one-time full rescan. This also migrates older state layouts (v1/v2): totals are rebuilt from the session files, so no history is lost unless the files are gone.
  • Adapter runs are staged: if an adapter fails mid-run, its counters and cursors commit nothing, so repeated failures never change totals.
  • Crash window: state is saved before each adapter's record. A crash in between can double-count that adapter's appended bytes on the next run. The sqlite >= boundary and hook fingerprints bound the same issue for those sources.
  • Concurrent runs serialize: the engine holds an exclusive advisory lock on a dedicated lock file beside the state file from load to save, so a manual CLI run cannot race the service timer. The lock file is never replaced, which keeps the lock on a stable inode across state saves.

Notes

  • Rate-limit meters only appear for adapters with a working limits hook; Pi has no usage endpoint, so its tab shows local stats only.
  • The stock panel resolves provider icons from its own read-only assets directory; unknown agents render the standard bar glyph unless/until marks land upstream.
  • Tests: python3 -m unittest discover -s tests.

Token fields

Canonical events and the panel's per-model hover carry four token counters:

Field Meaning Billing
input / output Prompt tokens sent and completion tokens generated on this request Standard rates
cacheRead Prompt tokens served from the provider's server-side context cache instead of being re-processed Discounted (~10% of input rate on Anthropic-style APIs)
cacheWrite New prompt tokens written into that cache so later requests can read them Premium (~125% of input rate on Anthropic-style APIs)

Typical pattern: the first turn of a session writes most of the context to the cache; follow-up turns read it back and only write each turn's new tail.

Providers without a billed prompt cache (MiMo, DeepSeek, and other OpenCode zen models at the moment) report cacheWrite: 0, so a zero there means "not offered by this model", not "nothing happened".

License

MIT