Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
- name: Install hidapi build deps (Linux)
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y libudev-dev libhidapi-dev
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ jobs:
target: x86_64-unknown-linux-gnu
steps:
- uses: actions/checkout@v7
- name: Install hidapi build deps (Linux)
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y libudev-dev libhidapi-dev
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
Expand Down
41 changes: 41 additions & 0 deletions ACKNOWLEDGMENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Acknowledgments

## Thanks to OpenAI

Microbridge exists because the Codex Micro is an *open* piece of hardware, and
that is not an accident — it is a choice OpenAI made.

OpenAI is a for-profit company, and it would have been easy to lock the Micro to
a single first-party app: a closed protocol, an exclusive USB claim, no way for
anyone else to light a key. They did the opposite.

- **They shipped the device kit in the open.** The full Work Louder protocol
travels inside the ChatGPT desktop app, which is how a community project like
this one could learn the framing and RPC without a single reverse-engineered
firmware dump.
- **They open the HID interface non-exclusively.** The Micro can be driven by
more than one program at a time, so third-party software can coexist with the
official experience instead of fighting it. Microbridge only works because of
that decision.
- **They keep giving users a choice.** Codex CLI is open source, the models are
reachable over documented APIs, and the tooling favors interoperability over
lock-in. Consumers get options, and options are good for everyone.

None of that was required of them. We think it is worth saying thank you when a
company consistently chooses to give its users room to build — so: **thank you.**

Microbridge is an independent community project and is not affiliated with,
sponsored by, or endorsed by OpenAI or Work Louder. This note is simply our
appreciation, offered freely.

## Thanks to Work Louder

For designing a genuinely hackable macropad — one that is also configurable
through Work Louder Input / VIA — and for building the hardware the whole
project is aimed at.

## Thanks to contributors

And to everyone who writes an adapter, files an issue, or plugs in a device and
tells us what really happens. Adapters are the point of this project; see
[CONTRIBUTING.md](CONTRIBUTING.md).
45 changes: 45 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Governance / why this path: [docs/governance.md](docs/governance.md).
|---|---|
| macOS (Homebrew) | Homebrew + **Xcode Command Line Tools** (`xcode-select --install`); Rust + Node pulled in as **build** deps (builds `.app` + daemon) |
| From source | Rust stable, Node ≥ 20; macOS also needs Xcode CLT for the `.app` |
| Hardware LEDs | Codex Micro over USB (HID packing still landing — mock works without hardware) |
| Hardware LEDs | Codex Micro over USB (protocol ready; set `MICROBRIDGE_HID_CLAIM=1` to write) |

## From source (developers)

Expand Down Expand Up @@ -120,8 +120,10 @@ brew services restart microbridge
launchctl kickstart -k "gui/$(id -u)/ai.microbridge.daemon"
```

**LEDs stay dark** — HID packing is still best-effort; ChatGPT desktop may
also own the device.
**LEDs stay dark** — by default Microbridge only probes USB (Detected). To
write Agent Key lighting: pause ChatGPT Desktop ownership, then
`export MICROBRIDGE_HID_CLAIM=1` before starting the daemon. See
[docs/device-hid.md](docs/device-hid.md).

**Homebrew can’t fetch (private repo)** — `gh auth login`, or set
`HOMEBREW_GITHUB_API_TOKEN` to a PAT with `repo` scope.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

Microbridge is a tiny local daemon that bridges AI coding agents — Codex CLI, Claude Code, Cursor, T3 Code, and anything else with an adapter — to the [Work Louder Codex Micro](https://worklouder.cc/). Per-key RGB mirrors live agent state; the keys drive agent actions (approve, reject, interrupt, switch focus). No vendor desktop app required.

> **Status: early public alpha (`v0.1.x`).** Menu bar UI, local daemon, in-process Codex/Claude watchers, and signed macOS packages are shipping. **Real Micro HID packing lands after hardware arrives (target 2026-07-22)** — until then LEDs/keys use Simulator / Detected. See [ROADMAP.md](ROADMAP.md) and [PRIVACY.md](PRIVACY.md).
> **Status: early public alpha (`v0.1.x`).** Menu bar UI, local daemon, in-process Codex/Claude watchers, and signed macOS packages are shipping. **HID protocol (VID/PID, framing, `v.oai.thstatus`) is implemented from ChatGPT’s Work Louder kit**; live LED writes stay opt-in (`MICROBRIDGE_HID_CLAIM=1`) until hardware validation. See [docs/device-hid.md](docs/device-hid.md).

## Screenshots

Expand Down Expand Up @@ -71,7 +71,7 @@ Details in [docs/architecture.md](docs/architecture.md). The wire format is spec

```
crates/mb-protocol wire types (serde) — the protocol's source of truth
crates/mb-device device abstraction; mock today, HID packing TBD
crates/mb-device device abstraction; HID framing + opt-in claim
crates/mb-adapters first-party Codex CLI + Claude Code watchers
crates/microbridged the daemon: socket server, registry, focus, key source
crates/microbridgectl support/debug CLI (`status`)
Expand Down Expand Up @@ -129,6 +129,8 @@ Adapter PRs are explicitly welcome — that is the point of the project. Start w

Microbridge is an independent community project. It is not affiliated with or endorsed by Work Louder or OpenAI. Driving the Micro's LEDs outside official software relies on best-effort reverse engineering of the device's HID protocol and may lag firmware updates.

This project is only possible because OpenAI chose to keep the Micro open — an open protocol, a non-exclusive HID interface, and open tooling. That is a real choice, and we're grateful for it: see [ACKNOWLEDGMENTS.md](ACKNOWLEDGMENTS.md).

## License

Dual-licensed under [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE), at your option. Contributions are accepted under the same terms.
8 changes: 5 additions & 3 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ Homebrew formula skeleton. UI/control protocol (`subscribe` / config) and
five key-source modes.

## M2 — Real light out 🚧
Codex Micro HID driver in `mb-device` (LED frames, key events, encoder),
behind a capability-probed device descriptor. Mock remains the default until
VID/PID + report map are captured — see [docs/device-hid.md](docs/device-hid.md).
Codex Micro HID driver in `mb-device`: VID/PID + HID framing + `v.oai.thstatus`
packing mined from ChatGPT Desktop's Work Louder kit. USB probe shows
**Detected**; live writes are opt-in (`MICROBRIDGE_HID_CLAIM=1`). Hardware
validation (key map, ownership UX) still pending — see
[docs/device-hid.md](docs/device-hid.md).

## M3 — Focus + menu bar 🚧
Tauri companion (`apps/microbridge-ui`) ports the approved MagicPath surfaces
Expand Down
8 changes: 8 additions & 0 deletions crates/mb-device/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ edition.workspace = true
license.workspace = true
repository.workspace = true

[features]
default = ["hid"]
## Open/write the Work Louder vendor HID interface via `hidapi`.
hid = ["dep:hidapi"]

[dependencies]
mb-protocol = { path = "../mb-protocol" }
tracing = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
hidapi = { version = "2.6", optional = true }
130 changes: 130 additions & 0 deletions crates/mb-device/src/capture.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
//! Interactive HID capture for hardware bring-up.
//!
//! Enabled with `--features hid`. Opens the Work Louder vendor interface and
//! streams decoded device→host notifications so the shipping key-string map can
//! be filled in without guessing. This is the tool the
//! [hardware bring-up runbook](../../docs/hardware-bringup.md) drives on day one
//! with a real Codex Micro.
//!
//! Nothing here writes to the device — it is read-only observation.

#![cfg(feature = "hid")]

use std::collections::BTreeMap;
use std::time::{Duration, Instant};

use crate::claim::open_device;
use crate::rpc::DeviceNotify;

/// Rolling stats for a distinct `v.oai.hid` key string.
#[derive(Debug, Default)]
struct KeyStat {
count: usize,
last_act: Option<i64>,
last_agent: Option<i64>,
}

/// Open the Micro and stream decoded device→host events for `seconds`
/// (`0` = until interrupted). Prints a fill-in-the-blank summary at the end so
/// the observed key strings can be dropped straight into the bring-up runbook.
///
/// Read-only: this never claims write ownership or drives LEDs. macOS opens the
/// interface non-exclusively, so quit ChatGPT Desktop and pause `microbridged`'s
/// LED claim first if you want a clean stream.
pub fn run_capture(seconds: u64) -> Result<(), String> {
let mut device = open_device(None)?;
let limit = if seconds == 0 {
"∞".to_string()
} else {
format!("{seconds}s")
};

println!(
"microbridge hid-capture — {} (0x{:04X})",
device.name, device.product_id
);
println!("Read-only. Press every Agent Key, then Approve / Reject / Interrupt,");
println!("rotate the dial, press the dial, and flick the joystick in each direction.");
println!("Every device→host event prints below. Runs for {limit} (Ctrl-C stops early).\n");

let start = Instant::now();
let mut keys: BTreeMap<String, KeyStat> = BTreeMap::new();
let mut joystick_samples = 0usize;
let mut others: BTreeMap<String, usize> = BTreeMap::new();

loop {
for notify in device.poll_notifies() {
let t = start.elapsed().as_secs_f32();
match notify {
DeviceNotify::Hid { key, act, agent } => {
println!("[{t:>6.1}s] hid k={key:<12} act={act:?} ag={agent:?}");
let stat = keys.entry(key).or_default();
stat.count += 1;
stat.last_act = act;
stat.last_agent = agent;
}
DeviceNotify::Joystick { angle, distance } => {
println!("[{t:>6.1}s] joy a={angle:?} d={distance:?}");
joystick_samples += 1;
}
DeviceNotify::Other { method } => {
println!("[{t:>6.1}s] other method={method}");
*others.entry(method).or_insert(0) += 1;
}
}
}

if seconds != 0 && start.elapsed() >= Duration::from_secs(seconds) {
break;
}
// Foreground debug tool: a short sleep keeps CPU near-idle while polling.
std::thread::sleep(Duration::from_millis(5));
}

print_summary(&keys, joystick_samples, &others);
Ok(())
}

fn print_summary(
keys: &BTreeMap<String, KeyStat>,
joystick_samples: usize,
others: &BTreeMap<String, usize>,
) {
println!("\n──────── capture summary ────────");
if keys.is_empty() && joystick_samples == 0 && others.is_empty() {
println!("No device→host events observed. Is the interface owned by another app");
println!("(ChatGPT Desktop / microbridged)? Quit it and re-run.");
return;
}

if !keys.is_empty() {
println!("\nkey (v.oai.hid) → drop these into docs/hardware-bringup.md:");
println!(" {:<14} {:>5} last_act last_ag", "k", "hits");
for (key, stat) in keys {
println!(
" {:<14} {:>5} {:<8} {}",
key,
stat.count,
stat.last_act
.map(|a| a.to_string())
.unwrap_or_else(|| "-".into()),
stat.last_agent
.map(|a| a.to_string())
.unwrap_or_else(|| "-".into()),
);
}
}

if joystick_samples > 0 {
println!("\njoystick (v.oai.rad): {joystick_samples} samples");
}

if !others.is_empty() {
println!("\nother notifications (unmapped methods):");
for (method, count) in others {
println!(" {method:<20} {count}");
}
}
println!("\nNext: record the mapping in docs/hardware-bringup.md and update");
println!("`agent_key_index` in crates/mb-device/src/lib.rs if the real strings differ.");
}
Loading
Loading