nimbus-notify 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. nimbus_notify-1.0.0/.claude-plugin/plugin.json +6 -0
  2. nimbus_notify-1.0.0/.github/workflows/ci.yml +29 -0
  3. nimbus_notify-1.0.0/.github/workflows/publish.yml +52 -0
  4. nimbus_notify-1.0.0/.gitignore +9 -0
  5. nimbus_notify-1.0.0/CHANGELOG.md +122 -0
  6. nimbus_notify-1.0.0/CONTRIBUTING.md +47 -0
  7. nimbus_notify-1.0.0/LICENSE +21 -0
  8. nimbus_notify-1.0.0/PKG-INFO +303 -0
  9. nimbus_notify-1.0.0/README.md +277 -0
  10. nimbus_notify-1.0.0/commands/nsnotify-setup.md +117 -0
  11. nimbus_notify-1.0.0/commands/nsnotify-status.md +52 -0
  12. nimbus_notify-1.0.0/docs/protocol.md +124 -0
  13. nimbus_notify-1.0.0/hooks/claude/settings.json +84 -0
  14. nimbus_notify-1.0.0/hooks/codex/hooks.json +73 -0
  15. nimbus_notify-1.0.0/hooks/vibe/hooks.toml +26 -0
  16. nimbus_notify-1.0.0/notify/__init__.py +0 -0
  17. nimbus_notify-1.0.0/notify/broker/__init__.py +0 -0
  18. nimbus_notify-1.0.0/notify/broker/frame.py +108 -0
  19. nimbus_notify-1.0.0/notify/broker/segments.py +99 -0
  20. nimbus_notify-1.0.0/notify/broker/server.py +371 -0
  21. nimbus_notify-1.0.0/notify/broker/session.py +67 -0
  22. nimbus_notify-1.0.0/notify/cli/__init__.py +0 -0
  23. nimbus_notify-1.0.0/notify/cli/led_report.py +96 -0
  24. nimbus_notify-1.0.0/notify/harness/__init__.py +0 -0
  25. nimbus_notify-1.0.0/notify/harness/base.py +19 -0
  26. nimbus_notify-1.0.0/notify/harness/claude.py +47 -0
  27. nimbus_notify-1.0.0/notify/harness/codex.py +43 -0
  28. nimbus_notify-1.0.0/notify/harness/vibe.py +165 -0
  29. nimbus_notify-1.0.0/notify/state.py +52 -0
  30. nimbus_notify-1.0.0/notify/transport/__init__.py +21 -0
  31. nimbus_notify-1.0.0/notify/transport/ble_tx.py +361 -0
  32. nimbus_notify-1.0.0/notify/transport/serial_tx.py +99 -0
  33. nimbus_notify-1.0.0/pyproject.toml +46 -0
  34. nimbus_notify-1.0.0/tests/__init__.py +0 -0
  35. nimbus_notify-1.0.0/tests/test_ble_transport.py +279 -0
  36. nimbus_notify-1.0.0/tests/test_frame.py +58 -0
  37. nimbus_notify-1.0.0/tests/test_segments.py +81 -0
  38. nimbus_notify-1.0.0/tests/test_session.py +54 -0
  39. nimbus_notify-1.0.0/tests/test_transport_select.py +103 -0
  40. nimbus_notify-1.0.0/tests/test_vibe_watcher.py +137 -0
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "nsnotify",
3
+ "version": "0.1.0",
4
+ "description": "Nimbus Notify — LED ring status indicator for concurrent AI coding-agent sessions (Claude Code, Codex, Mistral Vibe).",
5
+ "author": { "name": "Roy Darnell" }
6
+ }
@@ -0,0 +1,29 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ test:
13
+ runs-on: ubuntu-latest
14
+ strategy:
15
+ matrix:
16
+ python-version: ["3.11", "3.12"]
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - uses: actions/setup-python@v5
20
+ with:
21
+ python-version: ${{ matrix.python-version }}
22
+ - name: Install + test
23
+ run: |
24
+ python -m pip install --upgrade pip build twine
25
+ python -m pip install -e .
26
+ python -m pip install pytest
27
+ python -m build
28
+ python -m twine check dist/*
29
+ pytest -q
@@ -0,0 +1,52 @@
1
+ name: Publish to PyPI
2
+
3
+ # Publishes nimbus-notify to PyPI via Trusted Publishing (OIDC) — NO stored token.
4
+ # Prereq (one-time, on pypi.org): Account -> Publishing -> add a "pending publisher"
5
+ # project: nimbus-notify | owner: ristllin | repo: nimbus-notify
6
+ # workflow: publish.yml | environment: pypi
7
+ # Then this runs on every published GitHub Release.
8
+
9
+ on:
10
+ release:
11
+ types: [published]
12
+ workflow_dispatch: # manual trigger (build-only; publish still gates on a release env)
13
+
14
+ permissions:
15
+ contents: read
16
+
17
+ jobs:
18
+ build:
19
+ name: Build sdist + wheel
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+ - uses: actions/setup-python@v5
24
+ with:
25
+ python-version: "3.12"
26
+ - name: Build
27
+ run: |
28
+ python -m pip install --upgrade build twine
29
+ python -m build
30
+ python -m twine check dist/*
31
+ - uses: actions/upload-artifact@v4
32
+ with:
33
+ name: dist
34
+ path: dist/
35
+
36
+ publish:
37
+ name: Publish to PyPI (Trusted Publishing)
38
+ needs: build
39
+ runs-on: ubuntu-latest
40
+ if: github.event_name == 'release'
41
+ environment:
42
+ name: pypi
43
+ url: https://pypi.org/p/nimbus-notify
44
+ permissions:
45
+ id-token: write # OIDC token for Trusted Publishing — this is the auth
46
+ steps:
47
+ - uses: actions/download-artifact@v4
48
+ with:
49
+ name: dist
50
+ path: dist/
51
+ - name: Publish
52
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .venv/
8
+ *.env
9
+ .pytest_cache/
@@ -0,0 +1,122 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/);
6
+ versioning follows the convention described in
7
+ [CONTRIBUTING.md](CONTRIBUTING.md#versioning) (semver-for-0.x pre-1.0).
8
+
9
+ ## [1.0.0] — 2026-07-12
10
+
11
+ ### Changed
12
+
13
+ - **Renamed the pip distribution `nsnotify` → `nimbus-notify`** (brand alignment
14
+ with the Nimbus device) and the console script `nsnotify-broker` →
15
+ `nimbus-notify-broker`. The import package stays **`notify`** and the Claude
16
+ Code plugin/skill names are unchanged. The nsn wire protocol is untouched.
17
+ - **First PyPI release** — `pip install nimbus-notify` (published via GitHub
18
+ Actions Trusted Publishing).
19
+
20
+ ### Added
21
+
22
+ - `nimbus-notify-broker --install-service` / `--uninstall-service`: install the
23
+ broker as an auto-starting service (macOS launchd / Linux systemd user unit) so
24
+ it survives a reboot. Wired into `/nsnotify-setup` after the one-time
25
+ foreground BLE bond.
26
+
27
+ ## [0.4.1] — 2026-07-04
28
+
29
+ ### Changed
30
+
31
+ - Corrected the BLE bonding docs + broker hint to match reality. The Nimbus
32
+ firmware bonds via macOS **"Just Works"** (encrypted + bonded, no passkey to
33
+ type) — not a MITM passkey, because macOS won't surface a passkey dialog for a
34
+ custom peripheral paired by the broker. Two gotchas are now documented: Nimbus
35
+ never appears in the System Settings Bluetooth list (it's a custom peripheral),
36
+ and the **first** bond must be made with the broker in the **foreground** — a
37
+ fully detached (`nohup … &`) process can't complete it. Once bonded it's
38
+ transparent and can run backgrounded. (Verified end-to-end on hardware.)
39
+
40
+ ## [0.4.0] — 2026-07-04
41
+
42
+ ### Changed
43
+
44
+ - **The BLE link now requires a one-time pairing.** The Nimbus firmware secured
45
+ its GATT server (bonded + MITM passkey), so the broker writes FRAME **with
46
+ response** (`response=True`): an unbonded write returns an insufficient-
47
+ encryption error, which is what makes macOS raise its native pairing sheet.
48
+ Pair once — System Settings > Bluetooth, enter the 6-digit code shown on the
49
+ device screen (also on its serial console) — and every session after is
50
+ transparent. First-run gets a clear "pair first" hint on the broker console.
51
+
52
+ ### Fixed
53
+
54
+ - After the unbonded write is rejected, the broker now **re-drives the pending
55
+ frame** on a short timer until the link encrypts (macOS upgrades the *same*
56
+ connection in place with no reconnect, so nothing else would re-send it) — the
57
+ ring no longer stays stale through the pairing window.
58
+ - `_is_encryption_error` narrowed so it no longer swallows a bare ATT
59
+ "insufficient resources" (transient) or "not permitted" (config) as a pairing
60
+ failure — those must surface, not be silently dropped on a bonded link.
61
+ - The "pair first" hint re-arms on each reconnect (was suppressed forever after
62
+ the first emission).
63
+
64
+ ## [0.3.0] — 2026-07-03
65
+
66
+ ### Fixed
67
+
68
+ - **Vibe sessions are now detected.** The broker never started the
69
+ `VibeWatcher`, so Vibe sessions (which have no start/stop hook — only
70
+ `before_tool`/`after_tool`/`post_agent_turn`) were invisible: they never
71
+ appeared on the ring and never cleared. The broker now starts the watcher on
72
+ `~/.vibe/logs/session/` and routes `before_tool`/`after_tool` timing into its
73
+ HITL-inference tracker. Verified end-to-end: a real `vibe -p` session now
74
+ shows `start → running (tool) → done` on the device.
75
+ - `VibeWatcher.start()` no longer bails permanently when
76
+ `~/.vibe/logs/session/` doesn't exist yet — on a fresh Vibe install that
77
+ directory is created only on the first session, so the watcher now starts as
78
+ long as `~/.vibe/` is present and picks up the session dir when it appears.
79
+
80
+ ### Changed
81
+
82
+ - `Broker.handle_event` is now serialized with a lock: it is called from both
83
+ the asyncio socket handler (led-report events) and the VibeWatcher daemon
84
+ thread, so the segment allocator / sequence counter / frame push can no
85
+ longer interleave.
86
+
87
+ ## [0.2.0] — 2026-07-03
88
+
89
+ ### Added
90
+
91
+ - `nimbus-notify-broker --ble-name <NAME>`: connect only to a BLE peripheral
92
+ advertising this exact name (still gated on the nsn service UUID). Lets
93
+ several boards running this firmware on one desk stay unambiguous — e.g. a
94
+ bench board named `Nimbus-BT` vs a production `Nimbus`. On macOS this is the
95
+ reliable discriminator since CoreBluetooth hides the MAC address.
96
+
97
+ ## [0.1.0] — 2026-07-03
98
+
99
+ Initial public release, split out of a private monorepo into its own
100
+ standalone package.
101
+
102
+ ### Added
103
+
104
+ - Broker daemon (`nimbus-notify-broker`) that maintains live session state over a
105
+ Unix socket and pushes nsn wire-protocol frames to a connected device.
106
+ - `led-report` CLI, invoked from harness hooks to report session events to
107
+ the broker (fire-and-forget, never blocks the calling harness).
108
+ - Harness adapters for **Claude Code**, **Codex**, and **Mistral Vibe**,
109
+ including Vibe's session-file watcher and HITL-inference timeout (Vibe has
110
+ no native session start/stop hook).
111
+ - Two transports: **serial** (USB-CDC, auto-detects Espressif native-USB and
112
+ common USB-UART bridge chips) and **BLE** (GATT central, with
113
+ scan/connect/serve/backoff reconnection, MTU negotiation, and full-state
114
+ resend on every reconnect). Select with `nimbus-notify-broker --transport
115
+ serial|ble|auto`.
116
+ - Drop-in hook configs for all three harnesses under `hooks/`.
117
+ - Claude Code plugin (`.claude-plugin/plugin.json` + `commands/`) providing
118
+ `/nsnotify-setup` and `/nsnotify-status`.
119
+ - `docs/protocol.md` — a standalone description of the nsn wire protocol for
120
+ anyone implementing a compatible device.
121
+
122
+ [0.1.0]: https://github.com/ristllin/nimbus-notify/releases/tag/v0.1.0
@@ -0,0 +1,47 @@
1
+ # Contributing
2
+
3
+ Issues and pull requests are welcome — bug reports, new harness adapters,
4
+ new transports, or devices you've gotten talking to the broker.
5
+
6
+ ## Development setup
7
+
8
+ ```bash
9
+ git clone https://github.com/ristllin/nimbus-notify.git
10
+ cd nimbus-notify
11
+ python3 -m venv .venv && source .venv/bin/activate
12
+ pip install -e .
13
+ pip install pytest pytest-asyncio
14
+ python3 -m pytest
15
+ ```
16
+
17
+ ## Versioning
18
+
19
+ Every meaningful release: bump `version` in `pyproject.toml`, add a
20
+ `CHANGELOG.md` entry describing it, and tag the commit —
21
+ `git tag -a vX.Y.Z -m "..."` — then push the tag so consumers can pin to a
22
+ specific release instead of tracking `main`.
23
+
24
+ Semver-for-0.x (pre-1.0): MAJOR stays `0`; MINOR bumps for a new or changed
25
+ public surface (CLI flags, hook payload shape, wire-protocol version,
26
+ breaking or not); PATCH for a fix or internal change with no public-surface
27
+ change. One release can bundle multiple logical commits under one bump —
28
+ just say so in the CHANGELOG entry.
29
+
30
+ ## Adding a harness adapter
31
+
32
+ Look at `notify/harness/claude.py` (hook-driven) or `notify/harness/vibe.py`
33
+ (hook + background file-watcher, for harnesses without full session
34
+ lifecycle hooks) as templates. An adapter's job is to turn whatever your
35
+ harness gives you (hook stdin JSON, a log file, etc.) into a
36
+ `HarnessEvent` (`notify/harness/base.py`) that `led-report` sends to the
37
+ broker. Add a matching hook config under `hooks/<harness>/` and document the
38
+ wiring in the README.
39
+
40
+ ## Adding a transport
41
+
42
+ Transports implement the `Transport` protocol in `notify/transport/__init__.py`
43
+ — just `send(frame: bytes) -> bool` and `close() -> None`. See
44
+ `notify/transport/serial_tx.py` for the simplest example and
45
+ `notify/transport/ble_tx.py` for a fuller one with reconnect/backoff. Wire
46
+ a new transport into the `--transport` flag in `notify/broker/server.py`'s
47
+ `_make_transport()`.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Roy Darnell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,303 @@
1
+ Metadata-Version: 2.4
2
+ Name: nimbus-notify
3
+ Version: 1.0.0
4
+ Summary: Host broker that watches your AI coding sessions (Claude Code, Codex, Mistral Vibe) and pushes live status to a physical LED ring / e-ink device over a documented wire protocol
5
+ Project-URL: Homepage, https://github.com/ristllin/nimbus-notify
6
+ Project-URL: Repository, https://github.com/ristllin/nimbus-notify
7
+ Project-URL: Changelog, https://github.com/ristllin/nimbus-notify/blob/main/CHANGELOG.md
8
+ Author: Roy Darnell
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: ble,claude-code,codex,hooks,led,mistral-vibe,notifier,serial
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Requires-Python: >=3.11
21
+ Requires-Dist: aiohttp>=3.9
22
+ Requires-Dist: bleak>=0.22
23
+ Requires-Dist: pyserial>=3.5
24
+ Requires-Dist: watchdog>=4.0
25
+ Description-Content-Type: text/markdown
26
+
27
+ # nimbus-notify
28
+
29
+ A Python host broker that watches your AI coding-agent sessions — **Claude
30
+ Code**, **Codex**, and **Mistral Vibe** — via lightweight hooks, and pushes
31
+ their live status (running / waiting for you / needs approval / done /
32
+ errored) to a physical display device over serial (USB-CDC) or Bluetooth LE,
33
+ using a small documented binary protocol ([nsn](docs/protocol.md)).
34
+
35
+ If you've got several agent sessions going in parallel across different
36
+ terminals and projects, nimbus-notify gives you one glanceable place — an LED
37
+ ring, an e-ink panel, whatever device you point it at — to see which ones
38
+ are still working and which ones are waiting on you.
39
+
40
+ ```
41
+ ┌────────────┐ hooks ┌───────────────┐ nsn wire protocol ┌──────────┐
42
+ │ Claude Code│ ─────────▶ │ │ (serial or BLE) │ status │
43
+ │ Codex │ ─────────▶ │ nsnotify- │ ─────────────────────▶│ device │
44
+ │ Mistral │ ─────────▶ │ broker │ │(your own)│
45
+ │ Vibe │ │ │ │ │
46
+ └────────────┘ └───────────────┘ └──────────┘
47
+ ```
48
+
49
+ ## Status
50
+
51
+ Beta. The core broker, all three harness adapters, and both transports are
52
+ implemented and tested (`python3 -m pytest`). This is a fresh split out of a
53
+ private monorepo into its own package — see [CHANGELOG.md](CHANGELOG.md).
54
+
55
+ ## Compatible devices
56
+
57
+ nimbus-notify speaks a documented, transport-agnostic wire protocol — see
58
+ [docs/protocol.md](docs/protocol.md). Any device that implements the
59
+ protocol's serial or BLE side can be driven by this broker; nothing here is
60
+ tied to a specific piece of hardware. If you build (or already have) a
61
+ microcontroller project with an LED strip, an e-ink panel, or any other
62
+ status display, point it at this broker.
63
+
64
+ ## Install
65
+
66
+ Not yet on PyPI — install from a clone:
67
+
68
+ ```bash
69
+ git clone https://github.com/ristllin/nimbus-notify.git
70
+ cd nimbus-notify
71
+ pip install -e .
72
+ ```
73
+
74
+ This installs two commands on your `PATH`:
75
+
76
+ - `nimbus-notify-broker` — the daemon that maintains session state and talks to
77
+ your device.
78
+ - `led-report` — the small CLI that harness hooks call to report events
79
+ into the broker (fire-and-forget; never blocks your agent).
80
+
81
+ PyPI publishing (`pip install nimbus-notify`) is a planned next step — for now,
82
+ the git-clone path above is the supported install method.
83
+
84
+ ## Quickstart
85
+
86
+ 1. Install the package (above).
87
+ 2. Wire up the harness(es) you use — see [Harnesses](#harnesses) below.
88
+ 3. Start the broker:
89
+ ```bash
90
+ nimbus-notify-broker
91
+ ```
92
+ 4. Start (or resume) an agent session in a wired-up harness. Its status
93
+ should now be reported to the broker, and forwarded to your device.
94
+
95
+ If you use Claude Code, the fastest path is the bundled slash commands —
96
+ see [Claude Code plugin](#claude-code-plugin) below.
97
+
98
+ ## Harnesses
99
+
100
+ nimbus-notify supports three AI coding harnesses today. Each harness reports
101
+ events (session start, a tool running, waiting on your approval, done,
102
+ errored, session end) by calling `led-report <harness> <verb>` from a hook.
103
+
104
+ ### Claude Code
105
+
106
+ Merge [`hooks/claude/settings.json`](hooks/claude/settings.json)'s `hooks`
107
+ block into your `~/.claude/settings.json`, preserving any hooks you already
108
+ have (append to each event's array rather than replacing it). It wires up
109
+ `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `Notification`, `Stop`,
110
+ `StopFailure`, and `SessionEnd`.
111
+
112
+ ### Codex
113
+
114
+ Merge [`hooks/codex/hooks.json`](hooks/codex/hooks.json) into
115
+ `~/.codex/hooks.json`, then enable hooks and the notify bridge in
116
+ `~/.codex/config.toml`:
117
+
118
+ ```toml
119
+ [features]
120
+ hooks = true
121
+
122
+ notify = ["led-report", "codex-notify"]
123
+ ```
124
+
125
+ ### Mistral Vibe
126
+
127
+ Vibe has no native session start/stop hook, so the broker also runs a
128
+ background watcher over `~/.vibe/logs/session/` to detect new sessions and
129
+ infer human-in-the-loop waits (if a tool call starts but doesn't finish
130
+ within a timeout, that's treated as "awaiting approval").
131
+
132
+ Enable experimental hooks in `~/.vibe/config.toml`:
133
+
134
+ ```toml
135
+ enable_experimental_hooks = true
136
+ ```
137
+
138
+ Then merge [`hooks/vibe/hooks.toml`](hooks/vibe/hooks.toml) into
139
+ `~/.vibe/hooks.toml` (requires Vibe v2.15.0+ for `before_tool` /
140
+ `after_tool` / `post_agent_turn`).
141
+
142
+ ### Claude Code plugin
143
+
144
+ This repo is also a Claude Code plugin (`.claude-plugin/plugin.json`). If
145
+ you install it as a plugin, two slash commands become available:
146
+
147
+ - `/nsnotify-setup` — installs the Python package, merges the Claude Code
148
+ hooks automatically, checks whether the broker is running, and prints
149
+ the manual steps for Codex/Vibe.
150
+ - `/nsnotify-status` — shows current session states without needing to look
151
+ at the device.
152
+
153
+ ## Transports
154
+
155
+ Pick a transport with `--transport`:
156
+
157
+ ```bash
158
+ nimbus-notify-broker --transport serial # default
159
+ nimbus-notify-broker --transport ble
160
+ nimbus-notify-broker --transport auto # serial if a device is plugged in at
161
+ # startup, else BLE
162
+ ```
163
+
164
+ - **Serial** auto-detects a likely USB-CDC port (Espressif native-USB VID
165
+ `0x303A`, or common USB-UART bridge chips), or pin one explicitly:
166
+ `nimbus-notify-broker --transport serial --port /dev/cu.usbmodem101`.
167
+ - **BLE** requires your device to be powered on, flashed with firmware that
168
+ advertises the nsn BLE service, and in range. Optionally pin a specific
169
+ device: `nimbus-notify-broker --transport ble --ble-address <address>`
170
+ (a CoreBluetooth UUID on macOS, a MAC address on Linux). Without
171
+ `--ble-address`, the broker scans for the nsn service UUID.
172
+
173
+ Transport selection happens once at startup — there's no live failover
174
+ between serial and BLE mid-session in this version.
175
+
176
+ ### Bonding the BLE link (one time)
177
+
178
+ Recent Nimbus firmware **secures the BLE link** (bonded + encrypted, LE Secure
179
+ Connections), so a device won't accept frames from an un-bonded computer — this
180
+ stops anyone in radio range from painting your ring. Bonding is **automatic**
181
+ (macOS "Just Works", no code to type) — but there are two things to know:
182
+
183
+ - **Nimbus does *not* appear in System Settings → Bluetooth.** That list only
184
+ shows recognized device types (keyboards, mice, audio). A custom BLE
185
+ peripheral is invisible there by design — don't look for it. Bonding happens
186
+ on-demand when the broker first touches the device, not by picking it in a
187
+ list.
188
+ - **Do the first bond with the broker in the foreground.** macOS only completes
189
+ a bond for a process running in your normal login session — a fully detached
190
+ process (e.g. `nohup … & disown`) is too detached and the bond silently fails.
191
+ So the *first* time, just run `nimbus-notify-broker --transport ble` in a normal
192
+ terminal and leave it up for a few seconds. Once bonded, the bond persists on
193
+ both the device (flash) and your Mac, and every session after that is
194
+ transparent — you can then run it backgrounded or as a service (below).
195
+
196
+ To un-bond: on the device, *Connectivity → Forget paired devices* (or the
197
+ `FORGETBONDS` console command); the Mac side clears on its own next connect.
198
+
199
+ > The firmware also carries a dormant MITM/passkey mode (it shows a 6-digit code
200
+ > on its e-ink screen). It's off by default because macOS won't surface the
201
+ > passkey-entry dialog for a broker-triggered pairing of a custom peripheral, so
202
+ > that pairing can't complete without a companion app.
203
+
204
+ ## Running persistently
205
+
206
+ For day-to-day use you'll want the broker running in the background
207
+ whenever you're coding, not started by hand each time.
208
+
209
+ ### macOS (launchd)
210
+
211
+ Create `~/Library/LaunchAgents/com.nimbus-notify.broker.plist`:
212
+
213
+ ```xml
214
+ <?xml version="1.0" encoding="UTF-8"?>
215
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
216
+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
217
+ <plist version="1.0">
218
+ <dict>
219
+ <key>Label</key><string>com.nimbus-notify.broker</string>
220
+ <key>ProgramArguments</key>
221
+ <array>
222
+ <string>/usr/bin/env</string>
223
+ <string>nimbus-notify-broker</string>
224
+ <string>--transport</string>
225
+ <string>auto</string>
226
+ </array>
227
+ <key>RunAtLoad</key><true/>
228
+ <key>KeepAlive</key><true/>
229
+ <key>StandardOutPath</key><string>/tmp/nimbus-notify-broker.log</string>
230
+ <key>StandardErrorPath</key><string>/tmp/nimbus-notify-broker.log</string>
231
+ </dict>
232
+ </plist>
233
+ ```
234
+
235
+ Then:
236
+
237
+ ```bash
238
+ launchctl load ~/Library/LaunchAgents/com.nimbus-notify.broker.plist
239
+ ```
240
+
241
+ ### Linux (systemd, user service)
242
+
243
+ Create `~/.config/systemd/user/nimbus-notify-broker.service`:
244
+
245
+ ```ini
246
+ [Unit]
247
+ Description=nimbus-notify broker
248
+
249
+ [Service]
250
+ ExecStart=%h/.local/bin/nimbus-notify-broker --transport auto
251
+ Restart=on-failure
252
+
253
+ [Install]
254
+ WantedBy=default.target
255
+ ```
256
+
257
+ Then:
258
+
259
+ ```bash
260
+ systemctl --user daemon-reload
261
+ systemctl --user enable --now nimbus-notify-broker
262
+ ```
263
+
264
+ Adjust `ExecStart` to wherever `pip install -e .` put the entry point
265
+ (check with `which nimbus-notify-broker`).
266
+
267
+ ## The nsn wire protocol
268
+
269
+ A short summary — full details in [docs/protocol.md](docs/protocol.md).
270
+
271
+ ```
272
+ [SOF 0xAA] [LEN] [payload: LEN bytes] [CRC8-MAXIM]
273
+ ```
274
+
275
+ The payload starts with magic byte `0x4E`, then a sequence number, then up
276
+ to 16 fixed-size segment records (state, hue, animation, LED span) plus a
277
+ global brightness — enough for a device to render a full status frame from
278
+ a single self-contained packet, no persistent client-side state required.
279
+ The reference encoder/decoder is [`notify/broker/frame.py`](notify/broker/frame.py).
280
+
281
+ ## Repository layout
282
+
283
+ ```
284
+ notify/
285
+ broker/ frame.py (wire codec), segments.py, server.py, session.py
286
+ cli/ led_report.py — the hook-facing CLI entry point
287
+ harness/ base.py + claude.py, codex.py, vibe.py adapters
288
+ transport/ serial_tx.py, ble_tx.py
289
+ state.py shared State/Anim enums + default styling
290
+ tests/ pytest suite for the above
291
+ hooks/ drop-in hook configs per harness
292
+ commands/ Claude Code plugin slash commands
293
+ docs/ protocol.md
294
+ ```
295
+
296
+ ## Contributing
297
+
298
+ See [CONTRIBUTING.md](CONTRIBUTING.md), including the versioning
299
+ convention used for releases.
300
+
301
+ ## License
302
+
303
+ MIT — see [LICENSE](LICENSE). Copyright (c) 2026 Roy Darnell.