@randomknights/micr0pad
v0.1.0
Published
Live AI-agent status lights on a Work Louder Codex Micro or Creator Micro 2: per-key RGB driven by Herdr agent state, a browser mirror, voice-to-text and AiEDs energy estimates. Node, raw HID, no vendor app.
Maintainers
Readme
SUMMARY
Your agents are already running. This puts them on your desk, where you can see them without switching windows.
MicroPad turns a Work Louder Codex Micro or Creator Micro 2 into a status board for AI coding agents. It reads agent state from Herdr, the terminal multiplexer that hosts them, and paints that state onto the pad's per-key lights. One key per agent. Red means blocked, purple means working, blue means done, yellow means idle. The strip underneath carries the worst state of the six, so the pad is readable from across the room.
It does not care which AI you use. Claude Code, Codex, Gemini, Grok, Hugging Face, Ollama, anything Herdr can run.
No vendor app sits in the middle. The bridge speaks the device's own JSON-RPC over raw HID, and what it learns about your machine stays on your machine.
- Built with
- Node, no build step. One dependency,
node-hid, which brings two more in with it (a clean install reports three packages). - raw HID, the vendor JSON-RPC interface, not a keyboard shim
- Herdr for agent state
- the browser's own speech recognition for voice to text
- AiEDs v2.0.0 for energy and carbon estimates, off until you ask for them
- a browser page in plain HTML, CSS and JS. View source and you have read the whole client.
- Node, no build step. One dependency,
INSTALL
One command:
npx @randomknights/micr0padnpm downloads the package, writes your config.json from the example on the
first run (see "Where your settings live" below), and starts the server. Open http://localhost:4120. No compiler
is needed: the one native dependency, node-hid, ships prebuilt binaries for
Windows (x64, x86, arm64), macOS (Intel and Apple silicon) and Linux (x64,
arm64, armv7, and x64 musl). On any other platform npm skips it, and the app
runs the virtual pad described below instead of a physical one.
If npm answers E404, the package has not reached the registry yet; use the
checkout below, which is the same app.
Where your settings live: config.json (your slot names, action commands and
the pairing token), the keymap backup, watch.log, the AiEDs log and the
opt-in analytics log are kept in one folder that belongs to you, not to the
copy of the app you ran, so a new version or a cleared npm cache keeps them:
| System | Folder |
| ------- | ------------------------------------------------------- |
| Windows | %APPDATA%\micr0pad |
| macOS | ~/Library/Application Support/micr0pad |
| Linux | $XDG_CONFIG_HOME/micr0pad, or ~/.config/micr0pad |
config.json there is readable by your user only, where the system allows it.
Set RK_MICROPAD_DATA_DIR to use another folder, or RK_MICROPAD_CONFIG to
point at one config file of your own. A checkout that kept config.json or
keymap-backup.json beside the app before this folder existed has them copied
across once, on the next start; the old copies are left where they were and
are no longer read.
On Linux, reading the pad needs libudev (present on nearly every desktop
distribution) and permission to open the device's hidraw node.
If node-hid ever refuses to load, the app prints the reason on its console
and serves the virtual pad; in a checkout, npm rebuild node-hid rebuilds it.
From a checkout (contributors, and the flash scripts)
git clone https://github.com/random-knights/micr0pad
cd micr0pad
npm startnpm start installs the dependencies if they are missing, writes your
config.json from the checked-in example on the first run (into the per-user
folder above, like npx), and then starts the server. The keymap and flash scripts below (npm run backup-keymap and the
rest) run from a checkout.
You need Node 18 or newer. If yours is older the app says so, with your version number, instead of failing later somewhere confusing.
Agent lights need Herdr installed and on your PATH.
Without it the pad still runs and the six slots simply sit idle:
powershell -ExecutionPolicy Bypass -c "irm https://herdr.dev/install.ps1 | iex"Before you flash anything
Two of the setup scripts write to the device's flash memory. Back up the keymap first. It is the only way back to a pad that behaves like the one you bought, and the backup refuses to overwrite itself once it exists, so run it before anything else touches the device:
npm run backup-keymapThen, optionally, bind the dial and the joystick, and give the touch sensor some layers to cycle through. Both of these write to flash:
npm run bind-dial-joy
npm run add-layers -- --write # leave off --write to see what it would doIf port 4120 is taken, set RK_MICROPAD_PORT to something else before starting.
WHAT IT DOES
- A key per agent. Six of them. Each one takes the color of its agent's state, and each slot keeps its own identity color, so two Claudes are still two different keys.
- A mirror in the browser at
http://localhost:4120. Every key, the dial and the joystick, following the real device as you touch it. - Action keys that run your commands. Four of them, plus a terminal key. They ship pointing at nothing: shipping one team's scripts would give everyone else buttons that can only fail. Click the gear in the Action Keys tile, give a key a label and a command, and it works from the pad and from the page.
- A talk key. It types what you say into the notes box and the clipboard, and the strip pulses gold while it is listening. Speech recognition is the browser's, so the tab has to be in front; press talk while you are working elsewhere and the app tells you that rather than going quiet.
- Lights you can edit. Slot names, colors and the outer light, saved to
config.json. Eight choices in the effect list: follow the agent state, or pick one of the seven the firmware has (solid, pulse, soft pulse, spin, rainbow, gradient, off). - A system panel. CPU, memory, the top processes with an end task button, a resource watch that flags what has been wrong for long enough to matter (see below), and an optional energy panel.
- Pairing and revert. Hand the lights back to the firmware so you can pair the pad over Bluetooth, or put the original keymap back.
Without a pad
You do not need the hardware to run this. When no Work Louder pad is on the USB
bus, the app serves a virtual pad: the same six status keys, lit by the same
agent states, with the browser mirror as the pad itself. The header says
virtual pad so you always know which one you are looking at, and the keys in
the mirror become clickable, so a press does what pressing the real key does.
It is the same code path either way. The virtual pad answers the same commands the firmware answers, and refuses the ones the firmware refuses rather than pretending they worked. The two things it cannot do are the two that live in the device flash: it has no keymap to back up and none to restore, so those buttons wait for real hardware.
Plug a pad in at any point and it takes over on the next poll, with the colors
it was already showing painted straight onto the keys. Unplug one and the app
says the pad is gone rather than slipping back to a virtual one behind your
back; set virtualPad.onUnplug to true in config.json if you would rather
it did fall back, and virtualPad.enabled to false to turn the whole thing
off.
To start the server without touching the USB bus at all (no pad, no virtual
pad), set RK_MICROPAD_NO_DEVICE=1. That is for tests, and for a second copy
of the app while another one already holds the pad: only one process can.
The pad
( dial ) [ agent 1 ] [ agent 2 ] ( toggle ) encoder, two keys, joystick
[agent 3] [ agent 4 ] [ agent 5 ] [ agent 6 ] the six status keys
[action 1] [action 2] [action 3] [ action 4 ] your commands
[ talk (wide) ] [ terminal ]HARDWARE
| | |
|---|---|
| Device | Work Louder Codex Micro or Creator Micro 2 |
| Verified on | Codex Micro, VID 0x303a PID 0x8360, firmware v0.4.1 |
| Host | Windows. The process list and the command launchers use Windows tools. |
| Needs | Node 18 or newer, and Herdr for the agent lights |
Only one program at a time can hold the device. If the pad looks dead, another copy of this server is usually still running.
CONFIGURE
Everything you change in the browser is saved to config.json in your
per-user folder (see "Where your settings live" above), which is created for
you on the first run and is never committed. Slot matching, colors, action
commands, the outer light and the pairing token all live there.
config.example.json is the same shape, checked in, to read or copy from.
The action keys look for your .cmd files in this order, and the editor shows
you which directory it settled on:
MICROPAD_CMD_DIRcmdDirinconfig.json<app>/cmd- a
_macropadfolder beside the app
RESOURCE WATCH
The System panel watches the machine the pad is plugged into and says so when something has been wrong for long enough to matter. It never ends a process on its own: it alerts, and you decide.
What it watches. One sampler reads every process every 3 seconds and keeps ten minutes of history per process. A rule fires only when its condition has held for the whole of its window; one sample short does not count, and a gap in sampling starts the window over.
| rule | fires when | why these numbers |
|---|---|---|
| cpuSustained | total CPU at or above 85% for 90 s | a Flutter web build pins every core for 30 to 60 s; 90 s clears a build and still catches a runaway |
| memSustained | RAM at or above 90% for 60 s | Windows starts trimming and paging near 90%; a minute rules out a transient spike |
| processHog | one process at or above 2 GB working set for 120 s, or at or above 50% CPU for 120 s | above any single healthy renderer, analysis server or node process; half the machine from one process for two minutes is a spin, not work |
| hungChrome | a chrome.exe with no CPU at all for 180 s while holding 800 MB or more | a parked renderer keeps its memory and does nothing; an idle background tab sits well under 800 MB |
| orphanDev | a dart.exe, dartvm.exe or java.exe whose parent is gone, or is not an editor, toolchain or shell, for 300 s | analysis servers and Gradle daemons are what editors leave behind. A Gradle daemon detaches from its launcher by design, so it will show here after five minutes; that is the point, snooze it or end it |
What happens. The pad's outer light flashes the blocked color twice and
goes back to showing agent state (it stays out of the way while talk is on).
A banner in the System panel names the rule and the process, with an end
button and a snooze 30 min button per rule. Every alert and every end is
appended to watch.log next to config.json: process name, pid, rule, time
and who asked. No window titles, no user names, no paths.
What it will not do. end works only on the allowlist watch.killable,
which ships as chrome.exe, dart.exe, dartvm.exe, java.exe and
msedgewebview2.exe (on Dart 3.x dart.exe is a launcher and the work runs
in a dartvm.exe child, so both are listed).
The button is disabled for anything else, in the banner and in the process
table, and the server refuses it again. node.exe is deliberately not on the
list: this server and Herdr are node processes, and ending the wrong one
leaves the pad's HID handle wedged until you replug it. watch.autoKill
exists in the config and is always off in this version; nothing reads it, and
the UI says "coming later" with no toggle. An alert-only first version means a
wrong threshold costs you a banner, not a build.
Changing thresholds. Everything is under "watch" in config.json; the
shape and the defaults are in config.example.json. Set enabled: false on
a rule to silence it for good, change a number to move it, or edit
killable to allow another process. Restart the server after editing.
WHAT LEAVES YOUR MACHINE
Nothing, by design. There is no account, no sign in, and no server of ours to talk to.
What the app does guard is itself. Any page in any browser tab can send a
request to localhost, and this app can end processes and run commands, so
every request has to come from the app's own page, and every request that
changes something also has to carry the pairing token that was minted into
your config.json on first run. Without both, the answer is a refusal, not an
action. The token is a secret. It is never printed, never logged and never
returned to a page from another origin.
Pressing a key on the virtual pad is one of those changing requests. Clicking a key in the browser mirror is not a shortcut around any of this: it proves its origin and carries the token exactly as ending a process does, and the server refuses it outright while a physical pad is connected, because a click is not a press of hardware nobody touched.
That applies to reading as well as to changing. A page on another site asking
this server for your machine's name or your process list is refused, and no
response carries a header that would let another origin's code read the body
even if it got one. There is one gap left, and it is worth knowing about: a
program running on your machine is not a browser, so it is not bound by any of
this. The pairing token is what stands in its way on anything that changes
something, and a local program that can read your config.json has already
read the token.
Pairing a hosted page
There is one deliberate way through that wall, and you open it by hand.
A page on https://rand0m.ai is a different origin, so by default it is
refused exactly like any other site. To let it in, press show pairing code
in the Hosted Pages box on this dashboard. The app puts an eight-character code
on screen. Type that code into the hosted page. The page sends it back, the app
checks it, and hands that page a token for its own origin.
What each piece is doing:
- The code is carried by you. That is the whole security boundary. A page can only pair if somebody sitting at this machine read a code off this screen and typed it over there. The code lives five minutes, is worth one use, and a wrong guess burns it.
- The token is shown once and stored as a hash. Your
config.jsonends up with the origin, a label, a date and a SHA-256 digest. Someone who reads that file cannot pair with it, which is not true of the local pairing token sitting beside it. - The origin is part of the pairing. The browser attaches the page's origin to every request and a page cannot forge it, so a token lifted from one site does not work from another.
- The list of sites that may even ask is fixed.
hostedOriginsinconfig.json, shipping ashttps://rand0m.aionly. It is never a wildcard: an entry that is not a plainhttps://origin is dropped rather than honored, and a list you have written yourself is never widened behind your back. A developer who needs to pair from another host (a staging copy of the site, say) adds its exact origin to that list by hand, as shown indocs/DEVELOPMENT.md.
What a paired page can do: everything the local dashboard can, with three exceptions. It reads pad and agent state, system metrics and the process table, and it can save config, drive the lights and run an action key. Pairing a page is handing it your pad, not lending it a window.
What a paired page cannot do, ever:
| route | why it stays on this machine |
|---|---|
| /api/pairing-token | it hands out the LOCAL page's token; giving it away would let a hosted page act as the local page, which is what pairing replaces |
| /api/pair/start | minting codes is how pairings are created, and only the person at the machine creates one |
| /api/sys/kill | ending a process is a thing you do at the machine it happens on |
Revoking. Press revoke next to a pairing here, or disconnect on the hosted page. Either end cuts it, and the token stops working immediately; a paired page can only revoke its own pairing, never somebody else's.
Mixed content. The hosted page is https and this bridge is plain http
on localhost. Browsers carve localhost out of the mixed-content rules, so the
call is allowed: verified in headless Chrome 153 with a self-signed https
page against a local bridge, where the paired read returned 200 and the console
reported no mixed-content block at all. The only refusal in that run was this
app's own, for the unpaired attempt.
ENERGY REPORTING (OPTIONAL)
The system panel can show modeled energy, carbon and cost for your own agent sessions, using the AI Energy Disclosure Standard v2.0.0. A fresh install shows an empty panel and records nothing. It starts only when you add the hook.
For Claude Code, add a SessionEnd hook to ~/.claude/settings.json:
{
"hooks": {
"SessionEnd": [
{ "hooks": [ { "type": "command", "command": "node",
"args": ["<path-to-repo>/hooks/aieds-local.js"] } ] }
]
}
}The hook reads that session's own transcript, adds up the token counts the API
reported, and appends one line per model to aieds-local.jsonl in your
per-user folder, which is where the server reads it. Set AIEDS_LOG_PATH to
put it somewhere else, and give the server the same value so both ends read
the same file.
Cost is a modeled list price, not a bill. Rates live in
lib/aieds-rates.json, and a model that is not in that file gets no cost figure
rather than a guessed one. In AiEDs v2 energy, carbon and tree time are fixed
multiples of each other, which is why those three lines sit on top of one
another on the chart.
ANALYTICS (OFF UNTIL YOU SAY OTHERWISE)
There is a small local log of what the pad costs to run. It is off. Nothing is written, and no file is created, until you turn it on:
{ "analytics": { "enabled": true } }in config.json, or MICR0PAD_ANALYTICS=1 in the environment. Read it back
with npm run analytics, and turn it off again by deleting the file and the
setting.
It writes one line when the server stops: how long it ran, how long the pad was attached, how many times the lights were repainted, and how much processor time the server used. That is the whole list.
It does not record your machine name, your user name, your folders, your window titles, your agent or slot names, your action key labels or commands, or which keys you pressed. Those were considered and dropped, not shortened or hashed. A count of repaints is the cost of running something. A record of presses is a record of a person.
Nothing is sent anywhere. There is nowhere to send it: the app has no analytics endpoint and opens no connection for one. The file is yours, it is ignored by git, and deleting it undoes everything it ever recorded.
Energy in watt hours is not modeled here. AiEDs v2 models energy from AI token counts, and there is no sourced figure for what this device draws, so the log carries the measurements and says the energy number is unavailable rather than printing a guess with a unit on it.
DEVELOPMENT
npm test # the test suite, node:test, no framework
npm run analytics # totals from the local analytics log
npm run probe # ask the device what it isThere is no build step. public/ is served exactly as it is written.
CI runs the same npm test on every pull request. It installs without building
the native module, because nothing under test opens a device and the runner has
no pad plugged into it. It also checks the npm tarball's file list and
installs the packed tarball into an empty folder to start it with
RK_MICROPAD_NO_DEVICE=1. Anything that needs real hardware is checked by hand
against a real pad, and docs/DEVELOPMENT.md says which
parts those are.
What is covered without hardware. The transport and adapter modules run
under node --test with no pad and no Herdr installed, using a virtual pad
and recorded Herdr output in place of both:
wldevice.js- the report framing, the JSON-RPC envelope (report id, channel, chunking), the notify/response split, and the absent-device error, all through pure functions the module exports for this reason. The one thing never done here is opening a real HID handle.pad.js- the key/action id maps, the status color and effect table, the worst-of-six underglow priority order, and the hex color packer.mapper.js- assigning live agents to the 6 slots by agent/cwd/title match, the per-slot color override, and the worst-of-six rule that drives the underglow.herdr.js- parsingherdr agent listoutput into the pad's agent shape for idle, working, blocked, done and an unrecognized status, and the optional-herdr path (a clear rejection, not a hang, when the binary is missing).execFileis injected so no process is spawned.launcher.js- the.cmdfile search order (MICROPAD_CMD_DIR, thenconfig.json'scmdDir, then<app>/cmd, then the sibling_macropadfolders) and that an absolute path in config wins outright.bridge.js- the full handover state machine (virtual pad in, physical pad takes over, unplug is not silently backfilled, a replay failure is reported but the new device stays connected, an empty snapshot replays nothing, the outgoing virtual pad is closed).
What still needs the hardware. Actually opening the Work Louder vendor HID
interface (wldevice.js's open() against a real device), and the flash
scripts (backup-keymap.js, restore-keymap.js, add-layers.js,
bind-dial-joy.js) - every one of those is checked by hand against a real
pad, as docs/DEVELOPMENT.md says.
Ai Energy Disclosure Standard ( AiEDs v2.2.0 )
🌎 Total AiEDs Usage | micr0pad 🏰
⚡ 113.7 kWh
🌫️ 48.8 kg CO₂e
🌳 Tree-Time 848 days
🔢 650 M tokens, 11 sessions
Tree-Time is the time one mature tree (two or more years of growth) needs to capture this carbon at its yearly rate, 21 kg CO₂e per year; shown in days.
The figures above are the AiEDs impact of developing this repository,
measured by a SessionEnd hook on the developers' machines and reported under AiEDs section 2.4.1,
which counts plain input, cache-creation and cache-read tokens all as input at the input coefficient.
96.4 percent of our input is cache reads, so that rule decides the answer by 7.2x.
Weighting a cache read at 0.1 instead gives 15.8 kWh, 6.8 kg CO₂e, 118 days of Tree-Time.
That lower figure is a local departure from the standard, not a reading of it. It is
published because it is what this project offsets against.
The same educational comparisons the rand0m.ai app renders, from the same constants. Educational comparisons, not measurements.
| Equivalent | Amount | Basis | | --- | ---: | --- | | Phone charges | 9,475 | 12 Wh per charge | | LED bulb hours | 11,371 | 10 W bulb | | Laptop hours | 2,274 | 50 W laptop | | Driving distance | 287 km | 170 g CO₂e per km | | Tree-Time | 848 days | 21 kg CO₂e per mature tree per year |
What the figures above cost, and what it would take to absorb them. Modeled, like everything else here.
Energy cost: USD 20.85. 113.7 kWh at USD 0.1834 per kWh, the United States average residential price for June 2026 (18.34 cents per kilowatthour), from U.S. Energy Information Administration, Electric Power Monthly, Table 5.3. The rate is pinned, not looked up at render time, so this figure is reproducible.
Modeled provider spend: USD 430.84. The same sessions priced at published API list prices, rates version 2026-09-01, with cache writes at 1.25x and cache reads at 0.1x an input token. It is a MODEL, not a bill: this work runs on a subscription, so the marginal cost was nothing. It covers the 4 of 11 sessions counted above whose model that file prices; the other 7 carry a model nobody has priced and add nothing, rather than an assumed rate.
Trees needed: 3. 48.8 kg CO₂e divided by 21 kg CO₂e, the yearly capture of one mature tree, rounded up: 3 mature trees would absorb this carbon within one year. Put the other way round, that is the Tree-Time above: one mature tree working for 848 days, about 2.3 years.
Offset cost: USD 0.29. 0.0 tonnes of CO₂e at USD 6.03 per tonne, the REDD+ (Reduced Emissions from Deforestation and Degradation in Developing Countries) average, 2024, Ecosystem Marketplace, State of the Voluntary Carbon Market 2025, Table 4. That is a nature-based avoidance and protection, not removals average: this project prices itself against keeping land, animals and trees standing, never against carbon removals or industrial and household offsets. Buying an offset is not the same as not spending the energy, and this line does not claim otherwise.
Measured by a SessionEnd hook on one developer machine; a second machine's ledger is not yet merged in, over 293 recorded sessions covering 2026-07-27 to 2026-09-13, which is every session the hook recorded and no session it did not. Two attribution bases are published: BY LANE LEDGER in the table above, and BY WORKING DIRECTORY, the stricter view, in aieds-readme.json. The organization totals are the same under both. Both bases are DATE AWARE: the application repository was named xyz until 2026-08-19 and is named ruok now, so a row written before that day is placed on the repository the name meant then. Any offset figure is a nature-based average, not removals. Generated, never hand-typed.
CREDITS
- schacon/micro-manager worked out this device's JSON-RPC surface, its effect table and its key binding rules. This project would not exist without that work.
- Herdr, the agent runtime this reads from.
- Work Louder, the hardware.
Questions, bugs and ideas: open an issue.
LICENSE
Apache License 2.0. See LICENSE and NOTICE. The Random Knights name and brand assets are not covered by the license.
