@tiuxo/opencode-openrgb 0.0.0-stage → 0.1.0

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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,43 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-11-10
11
+
12
+ Initial release, targeting the opencode 2 plugin API.
13
+
14
+ ### Added
15
+
16
+ - Ambient status lighting driven by aggregate session state: amber while the
17
+ agent works, yellow on a retry, violet while a permission prompt waits, red
18
+ on failure. Idle restores the lighting captured at startup.
19
+ - State is aggregated across all sessions, so subagent and task work keeps the
20
+ lights busy until everything is genuinely finished. The highest priority
21
+ wins: `error` > `permission` > `retry` > `busy` > `idle`.
22
+ - `"baseline"` as a state color, breathing the device's *own* startup lighting
23
+ per LED, so an OpenRGB profile keeps its identity and only its intensity
24
+ changes.
25
+ - Device targeting by type or name glob, defaulting to interior and ambient
26
+ hardware. Keyboards, mice, keypads and accessories are left alone unless
27
+ explicitly included.
28
+ - An OpenRGB SDK client with no runtime dependencies, speaking protocol
29
+ versions 1-5 and negotiating `min(server, 5)`. If OpenRGB is absent,
30
+ restarted, or misbehaving, every operation degrades to a silent no-op and
31
+ the client reconnects in the background with backoff.
32
+ - Software brightness scaling, since `Direct` modes commonly report no
33
+ hardware brightness range.
34
+ - Configurable pulse, debounce and restore delay, plus a `debug` option and an
35
+ `OPENCODE_OPENRGB_DEBUG` kill switch read in the server process.
36
+ - `probe`, `selftest` and `simulate` scripts for inspecting hardware, verifying
37
+ the wire protocol against a real server, and previewing a palette.
38
+ - A warning at startup when the captured lighting is uniformly one of the
39
+ plugin's own status colors, which indicates a previous run exited without
40
+ restoring.
41
+
42
+ [Unreleased]: https://git.tiuxo.com/brian/opencode-openrgb/compare/v0.1.0...HEAD
43
+ [0.1.0]: https://git.tiuxo.com/brian/opencode-openrgb/releases/tag/v0.1.0
package/LICENSE ADDED
@@ -0,0 +1,11 @@
1
+ Copyright (c) 2026 Brian Clemens.
2
+
3
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
4
+
5
+ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
6
+
7
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
8
+
9
+ 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
10
+
11
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md CHANGED
@@ -1,3 +1,288 @@
1
- # Temporary Holding Version
1
+ # opencode-openrgb
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Ambient status lighting for [opencode](https://opencode.ai), driven by
4
+ [OpenRGB](https://openrgb.org).
5
+
6
+ Your RGB hardware mirrors what opencode is doing: amber while the agent works,
7
+ violet when it is waiting on a permission prompt, red when something fails. When
8
+ everything goes idle, your original lighting is restored exactly as it was.
9
+
10
+ There are no model-facing tools. The plugin never lets the LLM touch your
11
+ lights — it only reacts to session state.
12
+
13
+ | State | Meaning | Default |
14
+ | ------------ | ---------------------------------------- | --------- |
15
+ | `busy` | at least one session is working | `#ff8c00` |
16
+ | `retry` | a request is being retried | `#ffd000` |
17
+ | `permission` | a permission prompt is waiting for you | `#b400ff` |
18
+ | `error` | a session errored (decays after 10s) | `#ff0000` |
19
+ | `idle` | nothing running | restore |
20
+
21
+ Any state can instead be set to `"baseline"`, meaning *your own* lighting,
22
+ breathing. See [Pulsing your own color](#pulsing-your-own-color).
23
+
24
+ State is aggregated across **all** sessions, so subagent and task work keeps the
25
+ lights busy until everything is genuinely finished. When several conditions hold
26
+ at once, the highest priority wins: `error` > `permission` > `retry` > `busy` >
27
+ `idle`.
28
+
29
+ ## Requirements
30
+
31
+ - OpenRGB running with its SDK server enabled (Settings → General → *Enable SDK
32
+ Server*), listening on `127.0.0.1:6742` by default
33
+ - opencode 2.0+
34
+
35
+ Protocol versions 1–5 are supported; the client negotiates `min(server, 5)`.
36
+
37
+ > This plugin targets the opencode 2 plugin API and does not load under 1.x.
38
+
39
+ ## Install
40
+
41
+ ```sh
42
+ opencode plugin add @tiuxo/opencode-openrgb
43
+ ```
44
+
45
+ The plugin only loads for projects whose config declares it. For ambient
46
+ lighting everywhere, declare it **globally** in
47
+ `~/.config/opencode/opencode.json`:
48
+
49
+ ```jsonc
50
+ {
51
+ "$schema": "https://opencode.ai/config.json",
52
+ "plugins": ["@tiuxo/opencode-openrgb"]
53
+ }
54
+ ```
55
+
56
+ Config is hot-reloaded, so the plugin activates on save. Use `/reload` to force
57
+ it.
58
+
59
+ ### From source
60
+
61
+ Clone the repository and point `package` at the directory, using an absolute
62
+ path — plugin paths resolve relative to the config file that declares them:
63
+
64
+ ```jsonc
65
+ {
66
+ "$schema": "https://opencode.ai/config.json",
67
+ "plugins": [
68
+ { "package": "/absolute/path/to/opencode-openrgb" }
69
+ ]
70
+ }
71
+ ```
72
+
73
+ Point it at the repository directory, not a file inside it: opencode skips
74
+ configured plugin paths that are not directories.
75
+
76
+ To check what the plugin can see before enabling it:
77
+
78
+ ```sh
79
+ bun run probe
80
+ ```
81
+
82
+ ## Options
83
+
84
+ Every field is optional. Invalid values log a warning and fall back to the
85
+ default rather than failing to load.
86
+
87
+ ```jsonc
88
+ {
89
+ "package": "@tiuxo/opencode-openrgb",
90
+ "options": {
91
+ "enabled": true,
92
+ "host": "127.0.0.1",
93
+ "port": 6742,
94
+
95
+ "colors": {
96
+ // A hex/named color, or "baseline" to breathe your own lighting.
97
+ "busy": "#ff8c00",
98
+ "retry": "#ffd000",
99
+ "permission": "#b400ff",
100
+ "error": "#ff0000"
101
+ },
102
+
103
+ // "restore" puts back the lighting captured at startup.
104
+ // A color instead holds that color while idle. "off" goes dark.
105
+ "idle": "restore",
106
+
107
+ "brightness": 1.0, // 0..1, applied in software
108
+ "pulse": false, // breathing effect while busy
109
+ "pulseFps": 15,
110
+
111
+ "debounceMs": 120, // coalesce bursts of events
112
+ "restoreDelayMs": 400, // extra settle time before handing control back
113
+
114
+ "devices": {
115
+ "include": ["motherboard", "dram", "gpu", "cooler", "case", "ledstrip"],
116
+ "exclude": []
117
+ },
118
+
119
+ "debug": false
120
+ }
121
+ }
122
+ ```
123
+
124
+ Colors accept `#rgb`, `#rrggbb`, `rgb(r, g, b)`, and names like `amber`,
125
+ `violet`, `off`.
126
+
127
+ ### Pulsing your own color
128
+
129
+ If you would rather not have a status color replace your carefully chosen
130
+ lighting, set a state to `"baseline"`:
131
+
132
+ ```jsonc
133
+ { "colors": { "busy": "baseline" } }
134
+ ```
135
+
136
+ The device then breathes *its own* color while the agent works — whatever the
137
+ lighting was when the plugin connected — so your OpenRGB profile keeps its
138
+ identity and only its intensity changes. This is per LED, so a profile using
139
+ several colors keeps that pattern intact and simply pulses.
140
+
141
+ A `baseline` state always pulses, regardless of the `pulse` option: a static
142
+ baseline would be pixel-identical to idle and would tell you nothing.
143
+
144
+ `"current"` and `"profile"` are accepted as aliases. Note that `"gold"` is a
145
+ *named color*, not the baseline — use `"baseline"` if you mean "whatever is
146
+ already there".
147
+
148
+ ### Device targeting
149
+
150
+ `include` and `exclude` entries match either a device **type**
151
+ (`motherboard`, `gpu`, `keyboard`, …) or a glob against the device **name**
152
+ (`ASUS*`, `*X670E*`, `*`). `exclude` wins over `include`.
153
+
154
+ The default targets interior/ambient hardware only. Keyboards, mice, keypads and
155
+ accessories are deliberately left alone: their lighting usually conveys
156
+ something already — an Elgato Stream Deck's button icons, for instance — and
157
+ hijacking it by default would be obnoxious. Opt in explicitly if you want it:
158
+
159
+ ```jsonc
160
+ "devices": { "include": ["*"], "exclude": ["accessory"] }
161
+ ```
162
+
163
+ ## Scripts
164
+
165
+ | Command | What it does |
166
+ | ------------------- | ------------------------------------------------------------------------ |
167
+ | `bun run probe` | Read-only dump of devices, zones, modes; shows what would be targeted |
168
+ | `bun run selftest` | Writes colors, reads them back from the server, asserts, then restores |
169
+ | `bun run simulate` (`--pulse`, `--hold <ms>`) | Drives the real plugin through every state, so you can preview your palette |
170
+ | `bun test` | Tests for the codec, client, state machine and option parsing |
171
+ | `bun run typecheck` | `tsc --noEmit` over src, scripts and tests |
172
+
173
+ `selftest` and `simulate` change your lighting while they run and restore it
174
+ afterwards.
175
+
176
+ ## How it works
177
+
178
+ The OpenRGB SDK is a binary TCP protocol. This plugin implements the parts it
179
+ needs directly, with no runtime dependencies.
180
+
181
+ A few details that are easy to get wrong, all verified against the OpenRGB
182
+ sources and confirmed against real hardware by `scripts/selftest.ts`:
183
+
184
+ - `RGBColor` is `0x00BBGGRR`, so the little-endian byte order is R, G, B, 0
185
+ - In `UPDATELEDS` and `UPDATEMODE`, the leading `data_size` field **counts
186
+ itself**, and equals the packet header's size field
187
+ - Below protocol 6, `mode.value` is a device-internal field that must be
188
+ round-tripped back to the server verbatim when updating a mode
189
+ - Protocol 5 has no ACK packets, so writes are fire-and-forget
190
+
191
+ Per-LED color only applies in a direct/custom mode, so the plugin switches the
192
+ device into its `Direct` mode if it is not already there, and switches back on
193
+ restore. Brightness is applied by scaling RGB in software, because `Direct`
194
+ modes commonly report no hardware brightness range at all.
195
+
196
+ Robustness is a hard requirement: the plugin must never break opencode. If
197
+ OpenRGB is not running, is restarted, or misbehaves, every operation degrades to
198
+ a silent no-op and the client reconnects in the background with backoff.
199
+
200
+ ### Events
201
+
202
+ The plugin subscribes to exactly the events that can change the lighting, so it
203
+ stays off the firehose of streaming text and step events:
204
+
205
+ | Event | Effect |
206
+ | --------------------------- | --------------------------------- |
207
+ | `session.status` | `busy` / `retry` / `idle` |
208
+ | `session.idle` | session is finished |
209
+ | `session.retry.scheduled` | a retry is pending |
210
+ | `session.execution.failed` | raises the error state (10s hold) |
211
+ | `permission.asked` | a prompt is waiting for you |
212
+ | `permission.replied` | clears that prompt |
213
+ | `session.deleted` | forgets the session entirely |
214
+
215
+ ### Restoring on exit
216
+
217
+ Under opencode 2 the plugin restores from the cleanup function returned by
218
+ `setup`, which runs when the plugin unloads. That is an ordinary async context,
219
+ so queued socket writes reach OpenRGB normally.
220
+
221
+ The synchronous write path remains for abrupt teardown. `socket.write` only
222
+ *enqueues* data, and if the event loop stops before it flushes, the restore
223
+ silently never reaches OpenRGB and the lights stay stuck on the status color —
224
+ and the next startup then snapshots that leftover color as the "baseline",
225
+ losing the real one. The restore path therefore writes straight to the socket's
226
+ file descriptor when it cannot rely on the loop running again. This reaches for
227
+ private API (`socket._handle.fd`), so every failure path falls back to a normal
228
+ queued write. The plugin also warns at startup if the lighting it captured is
229
+ uniformly one of its own status colors, a strong hint that a previous run died
230
+ without restoring.
231
+
232
+ ## Limitations
233
+
234
+ - **Protocol 6 is not implemented.** It lands with OpenRGB 1.0 final and changes
235
+ device addressing to unique IDs. Decoders are version-guarded so the upgrade
236
+ is contained, but until then the client negotiates down to 5.
237
+ - **Restore is a startup snapshot.** If you change your lighting in the OpenRGB
238
+ GUI mid-session, the plugin will revert it at the next idle. Restart opencode
239
+ to re-snapshot.
240
+ - **A hard kill can still leave the last color showing.** `SIGKILL` or power
241
+ loss gives the plugin no chance to restore. Graceful exits are handled (see
242
+ below).
243
+ - **One instance per server.** opencode loads the plugin once per location, but
244
+ the hardware is global, so every instance shares one refcounted controller;
245
+ the last one to unload restores the lighting. Separate opencode *processes*
246
+ would still compete for the hardware.
247
+
248
+ ## Development
249
+
250
+ ```sh
251
+ bun install
252
+ bun test
253
+ bun run typecheck
254
+ bun run simulate # drive real hardware through every state
255
+ ```
256
+
257
+ The plugin ships as TypeScript source and opencode loads it directly, so there
258
+ is no build step.
259
+
260
+ The repo's own `opencode.json` wires the plugin locally with `debug: true`, so
261
+ running opencode inside this directory loads the working copy.
262
+
263
+ The entry point (`src/index.ts`) stays thin: it defines the plugin, resolves
264
+ options, and pumps the event subscription. The logic lives in
265
+ `src/controller.ts` so it is directly importable and testable without a plugin
266
+ host — see `test/plugin.test.ts`, which drives the real entry point against a
267
+ mock OpenRGB server.
268
+
269
+ ### The root index.ts
270
+
271
+ opencode resolves a local directory plugin by trying `<dir>/server` and
272
+ `<dir>/index`, without consulting `package.json` `main`/`exports`. A plugin
273
+ whose only entrypoint is `src/index.ts` resolves to nothing and is dropped
274
+ silently, with nothing logged.
275
+
276
+ Installs by package name do use `exports`, so the root `index.ts` is a
277
+ re-export and both resolution paths land on it.
278
+
279
+ `@opencode/plugin` is imported for types only, so the published package has no
280
+ runtime dependencies.
281
+
282
+ ## Changelog
283
+
284
+ See [CHANGELOG.md](CHANGELOG.md).
285
+
286
+ ## License
287
+
288
+ BSD-3-Clause
package/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ // opencode resolves local directory plugins through <dir>/index, ignoring package.json
2
+ // main/exports, and drops the plugin silently when that fails. Package installs use
3
+ // exports, which points here too.
4
+
5
+ export { default } from "./src/index.js"
6
+
7
+ export type { ResolvedOptions, IdleBehaviour, ActiveState, StateColor } from "./src/options.js"
8
+ export type { LightingState, OpencodeEvent } from "./src/state.js"
9
+ export type { Device, Mode, Zone, Led } from "./src/protocol.js"
package/package.json CHANGED
@@ -1,6 +1,56 @@
1
1
  {
2
2
  "name": "@tiuxo/opencode-openrgb",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Ambient status lighting for opencode via OpenRGB. Mirrors session state onto your RGB hardware.",
5
+ "type": "module",
6
+ "license": "BSD-3-Clause",
7
+ "author": "Brian Clemens <brian@tiuxo.com>",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://git.tiuxo.com/brian/opencode-openrgb.git"
11
+ },
12
+ "homepage": "https://git.tiuxo.com/brian/opencode-openrgb",
13
+ "bugs": {
14
+ "url": "https://git.tiuxo.com/brian/opencode-openrgb/issues"
15
+ },
16
+ "keywords": [
17
+ "opencode",
18
+ "opencode-plugin",
19
+ "openrgb",
20
+ "rgb",
21
+ "lighting",
22
+ "ambient",
23
+ "status"
24
+ ],
25
+ "publishConfig": {
26
+ "access": "public",
27
+ "registry": "https://registry.npmjs.org/"
28
+ },
29
+ "exports": {
30
+ ".": "./index.ts"
31
+ },
32
+ "files": [
33
+ "index.ts",
34
+ "src",
35
+ "README.md",
36
+ "LICENSE",
37
+ "CHANGELOG.md"
38
+ ],
39
+ "scripts": {
40
+ "test": "bun test",
41
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.scripts.json",
42
+ "check": "bun run typecheck && bun test",
43
+ "probe": "bun scripts/probe.ts",
44
+ "selftest": "bun scripts/selftest.ts",
45
+ "simulate": "bun scripts/simulate.ts"
46
+ },
47
+ "devDependencies": {
48
+ "@opencode/plugin": "^2.2.7",
49
+ "@types/bun": "^1.4.0",
50
+ "@types/node": "^24.12.2",
51
+ "typescript": "^5.8.2"
52
+ },
53
+ "engines": {
54
+ "opencode": ">=2.0.0"
55
+ }
56
+ }