Imported from Jutipong/oc-plugins (
AGENTS.md). Install upstream withnpx skills add Jutipong/oc-plugins. Copyright stays with the author.
Agent rules — oc-plugins (OpenCode plugins by Jutipong)
Monorepo for OpenCode V2 plugins. Plugins: esp-buddy — a playful desk buddy
that mirrors OpenCode activity onto an ESP32-S3 RLCD buddy screen (ported from
the ESP32-S3-AI repo's legacy "avatar" plugin v6; the board side lives there) —
and sys-status — a native macOS/Windows system dashboard for the CLI
sidebar (no mo dependency).
Commits — explicit confirmation required
- NEVER commit or push on your own. After finishing code changes, stop and
report; wait for the user's explicit order before
git commit/git push. - This rule stays active across the whole session until the user lifts it.
Repo conventions
- One plugin per folder:
plugins/<name>/- Server plugin (
esp-buddyshape):index.js—Plugin.define({ id: "<name>", async setup(ctx) {...} }),<name>-send.mjs/<name>-send.ps1out-of-process senders,package.json(private: true,main: "index.js"),test/harness.mjs+test/mock-loader.mjs. - CLI plugin (
sys-statusshape):tui.tsentry (plussrc/),package.jsonexposing"./tui",scripts/install.mjsto register the folder incli.json,test/harness.mjs. - Every plugin has
README.md(install, env vars, behavior) and its harness must printALL TESTS PASSED.
- Server plugin (
- Env vars are prefixed with the plugin name (
ESP_BUDDY_*,SYS_STATUS_*); legacyBUDY_*/AVATAR_*names are accepted as fallbacks where compatibility matters. - Senders resolve: env override → next to
index.js→ the live config dir (~/.config/opencode/plugins/). Debug logs go to~/.config/opencode/plugins/<name>-debug.log, OFF unless<NAME>_DEBUG=1, rotating at 512 KB. - Server plugins load in place via the global
pluginslist inopencode.json(c)(recommended), orinstall.ps1copies files into the live config folder. CLI-only plugins load viacli.json(global only) or discovery under~/.config/opencode/plugins/; the repo's ownplugins/folder is NOT an OpenCode discovery path. - Run the plugin's harness after ANY edit:
cd plugins/<name> && bun run test(ornode test/harness.mjs) — must printALL TESTS PASSED. - No secrets in git. (WiFi keys etc. live on the board's SD card only.)
Verified OpenCode 2.0.31 facts (live-tested on this machine, 2026-10-17)
ctx.event.subscribe()works: session-scopedAsyncIterable<V2Event>withsession.execution.started/succeeded/failed/interrupted,session.text.delta(carries the assistant text indata.delta),session.reasoning.delta,session.step.*,session.retry.scheduled,session.idle,session.tool.*(plus noisy*.updatedbroadcasts that must be ignored). Events carrylocation.directory(filter toctx.location.directory).ctx.session.wait({sessionID})resolves when that session's whole turn ends (waits past intermediate steps/tool calls; an idle session resolves in ~3ms). Fastest "turn done" signal — child sessions are tracked viasession.execution.*so only the last one ends a turn.ctx.session.hook("prompt")fires with the user's message (input.prompt.text) — best "the human said something" signal.ctx.session.hook("retry")fires with{sessionID, agent, model, error}on provider errors/retries (same moments assession.retry.scheduled).ctx.tool.hook("execute.after")payload:statusis"completed"even when the command failed — failures show up ininput.result.output.exit !== 0(shell) or a non-completed status.ctx.aisdk.hook("language")registers but never fires for HTTP providers. Do NOT rely on it (kept only as a best-effort wrap for other runtimes).ctx.session.hook("model.request"/"http.response", kind=primary)fire per model call;http.responseexposes a realinput.response.body(ReadableStream) — the plugin tees that body to observe stream chunks (fallback "still generating" signal, e.g. if the event stream dies).setup()runs once per host but hosts do reload it (probe saw 7 setups / 4 cleanups in minutes across TUI + service + headless runs) — keep cleanup complete (timers + hook registrations) and treat duplicate instances as normal (they are benign; each has its own state).
esp-buddy specifics
- State model:
workingwhile any tool is in flight;thinkingwhile a turn/stream is live;speakingwhile assistant text streams (throttled 600ms); short pulse poseslistening(prompt received),happy(tool turn succeeded),excited(tool ran >=8s),confused(tool failed / retry),greeting(waking from sleep), pluswag/curious(derived art) in the idle play pool;idleis published the moment the turn ends. Personality: energy + streak drive an idle play pulse every 8-12s (wag/curious/happy/excited when energetic, curious/listening/confused when not — curated so both derived poses stay frequent), nap (sleeping) after 15 min without human activity, wake on any activity. A 3s "tail grace" after a turn end ignores stray SSE deltas; the 25s quiet timer (guarded by in-flight tools + a 15s model-active window) is a backstop. - Heartbeat resends the current state every 30s (+jitter) but ONLY after real activity — never yank the board's clock screen on an idle machine.
- Every send retries (node → powershell on Windows, backoff): several hosts can share one COM port, collisions are normal. Never hold COM open persistently.
- Port resolution (
resolvePort, harness-tested): env override → macOS/dev/cu.usbmodem*auto-detect →COM5on Windows. A resolved path that does not exist (board unplugged / staleBUDY_COM) means no spawn at all; the port is re-resolved per send so hot-plugging works without a restart. - The board side (firmware + SD art, 14 pose names) lives in the
ESP32-S3-AI repo (
Waveshare_ESP32_RLCD_integration_Git_V2.ino, README "Buddy protocol"). A pose that is not in both the firmware event list and the SD falls back toidle. Wire format: newline-delimited JSON{"event":"<name>"}at 115200 baud.
sys-status specifics (CLI plugin, verified 2026-10-18 on OpenCode 2.0.32)
- No
mo/Mole dependency. Native collectors only:darwin.mjs(vm_stat,df -k /System/Volumes/Data,netstat -ib,sysctl,ioreg IOBlockStorageDriver,pmset,route, cachedsystem_profiler) andwin32.mjs(one long-lived PowerShell process streaming CIM/WMI JSON; CPU/memory/uptime fromos). The harness asserts that no runtime source referencesMOLE_BIN/mo status --watch/brew install mole. - CLI-only plugin: register in
~/.config/opencode/cli.jsonvianode scripts/install.mjs(backs upcli.json, preserves other entries). Option flags:--interval --spacing --row-spacing --bar-style --frame --color-mode --sidebar-mode --cores --placement; invalid values are ignored and existing options are merged, so a fresh install needs an explicit--sidebar-mode fullto get the stacked dashboard. Windows path:%USERPROFILE%\.config\opencode\cli.json. The repo'splugins/folder is not an OpenCode discovery path; load it explicitly. - Entry:
tui.tsre-exportssrc/entry.tsx(Plugin.define({ id: "sys-status", setup(ctx) })). Local CLI plugins do not neednode_modules: OpenCode's runtime plugin support resolves@opencode/plugin/tui,solid-js,@opentui/*for the loaded plugin. - Two surfaces, one collector (reference-counted subscribers; starts on
first mount, stops ~5 s after the last unmount; CPU sampling floors each
core's denominator at the wall-clock window so parked M-series E-cores read
idle):
- Sidebar — prepends
sidebar.content(42-col sidebar, ~37 usable columns; keep lines ≤36). Compact framed block: CPU (model + speed) / RAM / DSK / PWR / NET withspacing=compact/normal/airy.frame=rounded(default) /heavy/none, drawn by the model (╭─╮│╰╯/┏━┓┃┗┛, toneborder), top border◈ System, no update clock. Framed content is 32 cells;frame: "none"uses 36 and prepends a● <health score>header line. - Sidebar
sidebarMode: "full"— stacks the dashboard cards into the sidebar as one column (36 cells, 32 framed): CPU, Memory, Disk, Power, Network; no health/identity header, it starts with the CPU card. All through the sameframe/barStyle/colorMode/cores. Meters fill the row (label + meter + right-aligned value: 19 cells framed / 23 unframed, power 18/22); network showsUp/Downrates as numbers in that right column, no sparklines. All card titles carry their headline value right-aligned (chip + GHz, memory used / total, disk used / total, battery level, network IP; long values clipped with…). Redundant rows are dropped in the sidebar only: diskTotalandUsed, memoryFreeandTotal, powerLeveland the network footer (both return when the title value is missing).rowSpacing=compact(cards adjacent) /normal(blank line) /airy(faint separator; falls back tospacing). Overflowing fields are dropped, not mangled (4P+4E, battery· Chargedat 32 cells). ~23–27 lines tall;mode: "full"is the model option.
- Sidebar — prepends
- Health score:
src/health.mjscomputes the transparent heuristic (readme has the penalty table: CPU/memory/disk, battery health + cycles, disk I/O, uptime) →● 97 · Excellent · Restart Recommendedon a healthy Mac fixture; shown in the unframed sidebar header. - Battery telemetry:
ioreg -rc AppleSmartBatteryprints discharging power/current as unsigned 64-bit (2^64 + x:BatteryPower=18446744073709540941is −10.675 W);parseIoregBatteryfolds it back to signed with BigInt and drops implausible magnitudes (watts > 500 / amps > 50 / volts > 100). On battery power theInputrow hides and cycles move to aCyclesrow when no adapter is attached. - Bars and colors: sidebar bars are full-width on their own line below a
label/value line.
barStyle=mole(default:█whole cells +░track in the same threshold color, CPU/mem/disk/PWR load 50/80, battery level/health 20/50) orsolid(filled blocks only).colorMode=mole(default: fixed palette #A5D6A7 / #FFA94D / #FF5F5F / #C79FD7 / #404040 / #737373) ortheme(resolved OpenCode theme). The sidebarPWRbar is scaled to the adapter's max wattage but shows the actual system draw. - Options (cli.json):
interval(1–10 s),sidebar,sidebarMode(compactdefault /fullstacked dashboard),placement(top/bottom ofsidebar.content),spacing(compact block),rowSpacing(full sidebar card gaps),cores(full sidebar CPU cores:psummary /all),barStyle,frame,colorMode. - Bun quirks on this machine:
os.loadavg()returns ~0 (usesysctl vm.loadavg),fs.statfsis missing (usedf), butos.cpus()times,os.uptime(),os.totalmem()andexecFilework. - No Processes section anywhere; the harness asserts the rendered models
never contain the string
Process. - Tests:
cd plugins/sys-status && node test/harness.mjs(must printALL TESTS PASSED), live sanitized snapshot vianode test/collect-smoke.mjs 8. Windows parsing is fixture-tested on macOS; live Windows verification is manual on a Windows machine. - Mole (GPL-3.0) is only the visual inspiration; no code is copied and the plugin does not require it. Keep the README attribution intact.
go-usage specifics (CLI plugin, verified 2026-10-18 on OpenCode 2.0.32)
- Data source:
GET {console}/api/go/status(console basehttps://opencode.ai/console, env/option overridable). Response is nullable (no subscription):access.meters.{fiveHour,week,month}each{resetsAt, limitMicroCents, usedMicroCents}(micro-cents = 1e8/dollar, strings for bigint safety). Meter tones match sys-status: tone>=80 danger,>=50 warning, else success; integer percent with">99% used"when rounding hits 100 without exhaustion; reset countdownceil(minutes)→9d 14h/12h 12m/0h 31m. - Auth: the same OAuth device flow the CLI uses for its "OpenCode Console
account" integration —
POST /console/auth/device/code{client_id:"opencode-cli", supports_org_scope:true}(returnsuser_codeand a relative/console/device?user_code=…URI), pollPOST /console/auth/device/token{grant_type:"urn:ietf:params:oauth:grant-type:device_code", device_code, client_id}handlingauthorization_pending/slow_down(+5 s), refresh viagrant_type:"refresh_token". API keys (sk-…) get 401 on the console API — a device session is required.x-org-idis sent from the token (orGO_USAGE_ORG); tokens persist in plugin CLI storage. - Sidebar: prepends
sidebar.content(42-col sidebar, 36 usable); the frame is drawn by the model (not an OpenTUI border box):frame=rounded(default) /heavy/none, chars╭─╮│╰╯/┏━┓┃┗┛, toneborder→theme.border.base, top border =◈ Go usageleft + last successful fetch clockHH:mm:ssright (warning color when stale), content width 32 (36 − 2 border − 2 padding). Bars match sys-status:barStyle=mole(default: whole█cells +░track, both in the threshold color, floor — no partial edge) /solid(fill colored, faint blank remainder); one label line (0h 31m · 92%— countdown first, percent last) + one full-width bar line per meter. No footer when fresh; only a stale warning line (stale · retrying automatically) appears when the last refresh failed.frame: "none"falls back to the in-content header with 36-col lines. - Colors:
colorMode=mole(default: sys-status's fixed palette,src/colors.mjs— #A5D6A7 / #FFA94D / #FF5F5F / #C79FD7 / #404040 / #737373, resolved to RGBA viaRGBA.fromIntsinkit.tsx) ortheme(resolved OpenCode theme tokens).baselabels always usetheme.text.base. - Commands:
/go-usagewith no argument is a smart command —resolveCommand(input, phase)insrc/commands.mjs(harness-tested) shows the details dialog when ready and starts the device login when the phase ischecking/disconnected. Dedicated slash commands:/go-usage-login,/go-usage-logout,/go-usage-refresh(the legacy/go-usage login|logout|refreshargument form still works), plus palette entries under "Go usage";/go-usage-logintoasts instead of re-authenticating when a session already exists. Logout clears the tokens and the cached payload, and bumpsloginEpoch/suppressAdoptso a pending login or not-yet-cleared storage cannot resurrect the session. Polls every 300 s (5 min, clamped 15–3600 s) and caches the payload in durable storage so the sidebar renders instantly after a TUI restart. - Env:
GO_USAGE_INTERVAL,GO_USAGE_BASE_URL,GO_USAGE_ORG,GO_USAGE_FRAME,GO_USAGE_BAR_STYLE,GO_USAGE_COLOR_MODE,GO_USAGE_NO_OPEN,GO_USAGE_DEBUG(log at~/.config/opencode/plugins/go-usage-debug.log, 512 KB truncate). - Tests:
cd plugins/go-usage && node test/harness.mjs(offline, no network; must printALL TESTS PASSED). The live first leg was verified manually:device/code→ 200 withuser_code,go/statuswith a bogus token → 401{"_tag":"Unauthorized"}.
