diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..a428806
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,18 @@
+version: 2
+updates:
+ - package-ecosystem: npm
+ directory: /
+ schedule:
+ interval: weekly
+ open-pull-requests-limit: 5
+ labels:
+ - dependencies
+ groups:
+ elgato-dev:
+ patterns:
+ - "@elgato/*"
+ - "@rollup/*"
+ - "rollup"
+ - "typescript"
+ - "@types/*"
+ - "@tsconfig/*"
diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml
new file mode 100644
index 0000000..828b647
--- /dev/null
+++ b/.github/workflows/build-release.yml
@@ -0,0 +1,77 @@
+name: Build and release
+
+on:
+ push:
+ branches: [main]
+ tags: ["v*"]
+ pull_request:
+ branches: [main]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ build:
+ name: Build and validate
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Require release tags to point at main
+ if: github.ref_type == 'tag'
+ run: |
+ git fetch --no-tags origin main
+ if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
+ echo "Refusing build/release: tag ${GITHUB_REF_NAME} (${GITHUB_SHA}) is not on main."
+ exit 1
+ fi
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: "20"
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Build and validate
+ run: npm run validate
+
+ - name: Pack plugin
+ if: github.event_name != 'pull_request'
+ run: npm run pack
+
+ - name: Upload plugin artifact
+ if: github.event_name != 'pull_request'
+ uses: actions/upload-artifact@v4
+ with:
+ name: com.ssheppdev.litra.streamDeckPlugin
+ path: release/com.ssheppdev.litra.streamDeckPlugin
+ if-no-files-found: error
+
+ release:
+ name: Publish GitHub release
+ needs: build
+ if: github.ref_type == 'tag' && startsWith(github.ref_name, 'v') && github.event_name == 'push'
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ steps:
+ - uses: actions/download-artifact@v4
+ with:
+ name: com.ssheppdev.litra.streamDeckPlugin
+ path: release
+
+ - name: Create or update release
+ uses: softprops/action-gh-release@v2
+ with:
+ files: release/com.ssheppdev.litra.streamDeckPlugin
+ generate_release_notes: true
+ fail_on_unmatched_files: true
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f7a7f19
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,19 @@
+.DS_Store
+node_modules/
+
+# Reference clones (read-only upstream)
+ref/litra-rs/
+ref/litra/
+
+# Stream Deck build / runtime output
+*.sdPlugin/bin
+*.sdPlugin/logs
+*.sdPlugin/node_modules
+release/
+*.streamDeckPlugin
+
+# Local tooling / noisy capture byproducts
+.venv-frida/
+ref/hid-captures/*.jsonl
+ref/hid-captures/*.jsonl.err
+ref/hid-captures/lghub-logstream.txt
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..80e266e
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,20 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Attach to Plugin",
+ "type": "node",
+ "request": "attach",
+ "processId": "${command:PickProcess}",
+ "outFiles": [
+ "${workspaceFolder}/bin/**/*.js"
+ ],
+ "resolveSourceMapLocations": [
+ "${workspaceFolder}/**"
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..0eae9a6
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,17 @@
+{
+ /* JSON schemas */
+ "json.schemas": [
+ {
+ "fileMatch": [
+ "**/manifest.json"
+ ],
+ "url": "https://schemas.elgato.com/streamdeck/plugins/manifest.json"
+ },
+ {
+ "fileMatch": [
+ "**/layouts/*.json"
+ ],
+ "url": "https://schemas.elgato.com/streamdeck/plugins/layout.json"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
index 84ec6e1..d071ea2 100644
--- a/LICENSE
+++ b/LICENSE
@@ -2,8 +2,20 @@ MIT License
Copyright (c) 2026 Seth Sheppard
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index ddfde9b..888bcba 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,112 @@
# Litra for Stream Deck
-Initial import lands via squash-merge from `initial-commit`.
+Control [Logitech Litra](https://www.logitech.com/) lights from an [Elgato Stream Deck](https://www.elgato.com/streamdeck) — including dials on Stream Deck+.
+**Not an official Logitech or Elgato product.**
+
+End users only need Stream Deck software and a USB-connected Litra. There is nothing else to install (no Node, no `litra` CLI, no Homebrew). The plugin ships with its USB HID stack baked in.
+
+Under the hood (for developers): it uses the [`litra`](https://github.com/timrogers/litra) Node library **in-process** over USB HID — no CLI subprocess.
+
+## Features
+
+| Action | Key | Dial (Stream Deck+) |
+|--------|-----|---------------------|
+| **Power** | On / off / toggle | — |
+| **Brightness** | Jump to a preset % | Rotate to adjust; **press toggles power** |
+| **Temperature** | Jump to a preset Kelvin | Rotate to adjust; **press toggles power** |
+
+- Works with **all connected Litra devices** at once (no per-light picker yet)
+- Beam LX front brightness uses a corrected lumen read (stock JS only used the low byte)
+- macOS and Windows (USB; Bluetooth control is not supported by the underlying stack)
+
+## Supported hardware
+
+| Light | Front power / brightness / temperature |
+|-------|----------------------------------------|
+| Litra Glow | Yes |
+| Litra Beam | Yes |
+| Litra Beam LX | Yes (front key light) |
+
+**Not included (yet):** Beam LX rear RGB / G HUB effect presets. Research notes are in [`ref/hid-captures/NOTES.md`](ref/hid-captures/NOTES.md).
+
+## Requirements
+
+**To use the plugin (install from Releases)**
+
+- [Stream Deck](https://www.elgato.com/streamdeck) software **7.1+**
+- A Litra connected over **USB**
+- macOS 12+ or Windows 10+
+
+You do **not** need Node.js, npm, Homebrew, or the `litra` CLI.
+
+Quit **Logitech G HUB** if HID access fights the plugin (especially on macOS).
+
+**To build from source** (contributors only)
+
+- Node.js **20**
+- npm
+
+## Install (prebuilt)
+
+1. Download the latest `.streamDeckPlugin` from [Releases](https://github.com/SSheppDev/streamdeck-litra/releases).
+2. Double-click the file (or open it with Stream Deck).
+3. Drag **Power**, **Brightness**, or **Temperature** onto your layout.
+
+That’s the full end-user install — the package already includes `litra` / `node-hid`.
+
+If you linked a local/dev copy earlier, unlink it first:
+
+```bash
+streamdeck unlink com.ssheppdev.litra
+```
+
+## Build from source
+
+```bash
+git clone https://github.com/SSheppDev/streamdeck-litra.git
+cd streamdeck-litra
+npm install
+npm run build
+```
+
+### Link for development
+
+```bash
+npm run link # streamdeck link ./com.ssheppdev.litra.sdPlugin
+npm run watch # rebuild + restart on change
+```
+
+### Package a distributable
+
+```bash
+npm run pack
+```
+
+Writes `release/com.ssheppdev.litra.streamDeckPlugin`.
+
+Validate without packing:
+
+```bash
+npm run validate
+```
+
+## Usage tips
+
+- **Brightness / temperature keys** show the configured preset as the title.
+- **Dials** use Stream Deck’s built-in `$A1` layout; rotate adjusts, press toggles front power.
+- With **no lights connected**, actions show Stream Deck’s alert indicator.
+- Multiple lights are always controlled together in v1.
+
+## Credits
+
+- [timrogers/litra](https://github.com/timrogers/litra) — HID protocol / Node library
+- [Elgato Stream Deck SDK](https://docs.elgato.com/streamdeck)
+
+## Security
+
+See [SECURITY.md](SECURITY.md) for how to report vulnerabilities and this project’s secrets policy.
+
+## License
+
+[MIT](LICENSE)
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..7dc4e2f
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,60 @@
+# Security policy
+
+## Reporting a vulnerability
+
+Please **do not** open a public GitHub issue for security problems.
+
+Use [GitHub private vulnerability reporting](https://github.com/SSheppDev/streamdeck-litra/security/advisories/new) on this repository, or email **sethcsheppard@outlook.com** with:
+
+- A short description of the issue
+- Steps to reproduce (or a PoC)
+- Affected versions / commit if known
+- Your assessment of impact
+
+You should hear back within a few days. There is no bug bounty for this project.
+
+## Scope
+
+**In scope**
+
+- The Stream Deck plugin source and packaged `.streamDeckPlugin` artifacts from this repo
+- Accidental exposure of credentials or secrets in this repository
+- Dependency vulnerabilities that affect build or runtime of this plugin
+
+**Out of scope**
+
+- Logitech / Elgato / third-party products and firmware
+- Issues that only appear when Logitech G HUB or other software holds exclusive HID access
+- Social engineering, physical access, or compromised developer machines
+
+## Secrets policy
+
+This repository must not contain secrets. Treat the following as secrets and **never** commit them:
+
+- API keys, access tokens, PATs, OAuth client secrets
+- Passwords, private keys, certificates, `.pem` / `.p12` material
+- `.env` files, credential JSON, cloud/provider config with keys
+- Session cookies or webhook signing secrets
+
+### Rules
+
+1. **No secrets in git** — not in source, docs, examples, commits, or release assets.
+2. **No secrets in the plugin package** — Stream Deck plugins run on the user’s machine; still do not embed provider credentials. Persist any future auth material only via Stream Deck global settings (encrypted on-device), never in `manifest.json` or bundled files.
+3. **Push protection** — GitHub secret scanning with push protection is enabled. Do not bypass alerts; rotate any credential that was nearly or actually committed.
+4. **History is forever** — deleting a file in a later commit does not remove it from history. If a secret lands in git, **rotate it immediately**, then scrub history if needed.
+5. **Local machine data stays local** — do not commit personal inventories, host paths with sensitive context, or unrelated tooling snapshots.
+6. **Dependencies** — keep lockfiles committed; address Dependabot / `npm audit` findings in a timely way for high/critical issues.
+
+### If a secret is exposed
+
+1. Revoke / rotate the credential at the provider (do this first).
+2. Remove it from the working tree and stop using the leaked value.
+3. Open a private report (or notify maintainers) with what leaked and when.
+4. Optionally rewrite history (`git filter-repo` / BFG) and force-push if the secret remains reachable in old commits — assume anyone who cloned already has it until rotation is done.
+
+## Supported versions
+
+| Version | Supported |
+|---------|-----------|
+| Latest release (`v1.x`) | Yes |
+| Older tags / forks | Best effort only |
diff --git a/com.ssheppdev.litra.sdPlugin/imgs/actions/brightness/encoder.png b/com.ssheppdev.litra.sdPlugin/imgs/actions/brightness/encoder.png
new file mode 100644
index 0000000..739bc30
Binary files /dev/null and b/com.ssheppdev.litra.sdPlugin/imgs/actions/brightness/encoder.png differ
diff --git a/com.ssheppdev.litra.sdPlugin/imgs/actions/brightness/encoder@2x.png b/com.ssheppdev.litra.sdPlugin/imgs/actions/brightness/encoder@2x.png
new file mode 100644
index 0000000..0e69a6f
Binary files /dev/null and b/com.ssheppdev.litra.sdPlugin/imgs/actions/brightness/encoder@2x.png differ
diff --git a/com.ssheppdev.litra.sdPlugin/imgs/actions/brightness/icon.png b/com.ssheppdev.litra.sdPlugin/imgs/actions/brightness/icon.png
new file mode 100644
index 0000000..57cfed7
Binary files /dev/null and b/com.ssheppdev.litra.sdPlugin/imgs/actions/brightness/icon.png differ
diff --git a/com.ssheppdev.litra.sdPlugin/imgs/actions/brightness/icon@2x.png b/com.ssheppdev.litra.sdPlugin/imgs/actions/brightness/icon@2x.png
new file mode 100644
index 0000000..d660013
Binary files /dev/null and b/com.ssheppdev.litra.sdPlugin/imgs/actions/brightness/icon@2x.png differ
diff --git a/com.ssheppdev.litra.sdPlugin/imgs/actions/brightness/key.png b/com.ssheppdev.litra.sdPlugin/imgs/actions/brightness/key.png
new file mode 100644
index 0000000..739bc30
Binary files /dev/null and b/com.ssheppdev.litra.sdPlugin/imgs/actions/brightness/key.png differ
diff --git a/com.ssheppdev.litra.sdPlugin/imgs/actions/brightness/key@2x.png b/com.ssheppdev.litra.sdPlugin/imgs/actions/brightness/key@2x.png
new file mode 100644
index 0000000..0e69a6f
Binary files /dev/null and b/com.ssheppdev.litra.sdPlugin/imgs/actions/brightness/key@2x.png differ
diff --git a/com.ssheppdev.litra.sdPlugin/imgs/actions/power/icon.png b/com.ssheppdev.litra.sdPlugin/imgs/actions/power/icon.png
new file mode 100644
index 0000000..3d10b6b
Binary files /dev/null and b/com.ssheppdev.litra.sdPlugin/imgs/actions/power/icon.png differ
diff --git a/com.ssheppdev.litra.sdPlugin/imgs/actions/power/icon@2x.png b/com.ssheppdev.litra.sdPlugin/imgs/actions/power/icon@2x.png
new file mode 100644
index 0000000..97aa366
Binary files /dev/null and b/com.ssheppdev.litra.sdPlugin/imgs/actions/power/icon@2x.png differ
diff --git a/com.ssheppdev.litra.sdPlugin/imgs/actions/power/key-off.png b/com.ssheppdev.litra.sdPlugin/imgs/actions/power/key-off.png
new file mode 100644
index 0000000..b0942d3
Binary files /dev/null and b/com.ssheppdev.litra.sdPlugin/imgs/actions/power/key-off.png differ
diff --git a/com.ssheppdev.litra.sdPlugin/imgs/actions/power/key-off@2x.png b/com.ssheppdev.litra.sdPlugin/imgs/actions/power/key-off@2x.png
new file mode 100644
index 0000000..7808624
Binary files /dev/null and b/com.ssheppdev.litra.sdPlugin/imgs/actions/power/key-off@2x.png differ
diff --git a/com.ssheppdev.litra.sdPlugin/imgs/actions/power/key-on.png b/com.ssheppdev.litra.sdPlugin/imgs/actions/power/key-on.png
new file mode 100644
index 0000000..436f6b8
Binary files /dev/null and b/com.ssheppdev.litra.sdPlugin/imgs/actions/power/key-on.png differ
diff --git a/com.ssheppdev.litra.sdPlugin/imgs/actions/power/key-on@2x.png b/com.ssheppdev.litra.sdPlugin/imgs/actions/power/key-on@2x.png
new file mode 100644
index 0000000..3c4139d
Binary files /dev/null and b/com.ssheppdev.litra.sdPlugin/imgs/actions/power/key-on@2x.png differ
diff --git a/com.ssheppdev.litra.sdPlugin/imgs/actions/temperature/encoder.png b/com.ssheppdev.litra.sdPlugin/imgs/actions/temperature/encoder.png
new file mode 100644
index 0000000..3341477
Binary files /dev/null and b/com.ssheppdev.litra.sdPlugin/imgs/actions/temperature/encoder.png differ
diff --git a/com.ssheppdev.litra.sdPlugin/imgs/actions/temperature/encoder@2x.png b/com.ssheppdev.litra.sdPlugin/imgs/actions/temperature/encoder@2x.png
new file mode 100644
index 0000000..98e99d7
Binary files /dev/null and b/com.ssheppdev.litra.sdPlugin/imgs/actions/temperature/encoder@2x.png differ
diff --git a/com.ssheppdev.litra.sdPlugin/imgs/actions/temperature/icon.png b/com.ssheppdev.litra.sdPlugin/imgs/actions/temperature/icon.png
new file mode 100644
index 0000000..d1712a7
Binary files /dev/null and b/com.ssheppdev.litra.sdPlugin/imgs/actions/temperature/icon.png differ
diff --git a/com.ssheppdev.litra.sdPlugin/imgs/actions/temperature/icon@2x.png b/com.ssheppdev.litra.sdPlugin/imgs/actions/temperature/icon@2x.png
new file mode 100644
index 0000000..4587c50
Binary files /dev/null and b/com.ssheppdev.litra.sdPlugin/imgs/actions/temperature/icon@2x.png differ
diff --git a/com.ssheppdev.litra.sdPlugin/imgs/actions/temperature/key.png b/com.ssheppdev.litra.sdPlugin/imgs/actions/temperature/key.png
new file mode 100644
index 0000000..3341477
Binary files /dev/null and b/com.ssheppdev.litra.sdPlugin/imgs/actions/temperature/key.png differ
diff --git a/com.ssheppdev.litra.sdPlugin/imgs/actions/temperature/key@2x.png b/com.ssheppdev.litra.sdPlugin/imgs/actions/temperature/key@2x.png
new file mode 100644
index 0000000..98e99d7
Binary files /dev/null and b/com.ssheppdev.litra.sdPlugin/imgs/actions/temperature/key@2x.png differ
diff --git a/com.ssheppdev.litra.sdPlugin/imgs/plugin/category-icon.png b/com.ssheppdev.litra.sdPlugin/imgs/plugin/category-icon.png
new file mode 100644
index 0000000..ee497b2
Binary files /dev/null and b/com.ssheppdev.litra.sdPlugin/imgs/plugin/category-icon.png differ
diff --git a/com.ssheppdev.litra.sdPlugin/imgs/plugin/category-icon@2x.png b/com.ssheppdev.litra.sdPlugin/imgs/plugin/category-icon@2x.png
new file mode 100644
index 0000000..a49efef
Binary files /dev/null and b/com.ssheppdev.litra.sdPlugin/imgs/plugin/category-icon@2x.png differ
diff --git a/com.ssheppdev.litra.sdPlugin/imgs/plugin/marketplace.png b/com.ssheppdev.litra.sdPlugin/imgs/plugin/marketplace.png
new file mode 100644
index 0000000..060692b
Binary files /dev/null and b/com.ssheppdev.litra.sdPlugin/imgs/plugin/marketplace.png differ
diff --git a/com.ssheppdev.litra.sdPlugin/imgs/plugin/marketplace@2x.png b/com.ssheppdev.litra.sdPlugin/imgs/plugin/marketplace@2x.png
new file mode 100644
index 0000000..c1f32de
Binary files /dev/null and b/com.ssheppdev.litra.sdPlugin/imgs/plugin/marketplace@2x.png differ
diff --git a/com.ssheppdev.litra.sdPlugin/manifest.json b/com.ssheppdev.litra.sdPlugin/manifest.json
new file mode 100644
index 0000000..2fbda55
--- /dev/null
+++ b/com.ssheppdev.litra.sdPlugin/manifest.json
@@ -0,0 +1,113 @@
+{
+ "$schema": "https://schemas.elgato.com/streamdeck/plugins/manifest.json",
+ "Name": "Litra",
+ "Version": "1.0.0.0",
+ "Author": "Seth Sheppard",
+ "Actions": [
+ {
+ "Name": "Power",
+ "UUID": "com.ssheppdev.litra.power",
+ "Icon": "imgs/actions/power/icon",
+ "Tooltip": "Turn Litra lights on, off, or toggle.",
+ "PropertyInspectorPath": "ui/power.html",
+ "Controllers": [
+ "Keypad"
+ ],
+ "DisableAutomaticStates": true,
+ "UserTitleEnabled": false,
+ "States": [
+ {
+ "Name": "Off",
+ "Image": "imgs/actions/power/key-off",
+ "TitleAlignment": "bottom",
+ "ShowTitle": false
+ },
+ {
+ "Name": "On",
+ "Image": "imgs/actions/power/key-on",
+ "TitleAlignment": "bottom",
+ "ShowTitle": false
+ }
+ ]
+ },
+ {
+ "Name": "Brightness",
+ "UUID": "com.ssheppdev.litra.brightness",
+ "Icon": "imgs/actions/brightness/icon",
+ "Tooltip": "Set brightness on a key, or adjust with a dial (press toggles power).",
+ "PropertyInspectorPath": "ui/brightness.html",
+ "Controllers": [
+ "Keypad",
+ "Encoder"
+ ],
+ "UserTitleEnabled": false,
+ "Encoder": {
+ "layout": "$A1",
+ "Icon": "imgs/actions/brightness/encoder",
+ "TriggerDescription": {
+ "Rotate": "Brightness",
+ "Push": "Power"
+ }
+ },
+ "States": [
+ {
+ "Image": "imgs/actions/brightness/key",
+ "TitleAlignment": "bottom",
+ "FontSize": 14,
+ "TitleColor": "#ffffff"
+ }
+ ]
+ },
+ {
+ "Name": "Temperature",
+ "UUID": "com.ssheppdev.litra.temperature",
+ "Icon": "imgs/actions/temperature/icon",
+ "Tooltip": "Set color temperature on a key, or adjust with a dial (press toggles power).",
+ "PropertyInspectorPath": "ui/temperature.html",
+ "Controllers": [
+ "Keypad",
+ "Encoder"
+ ],
+ "UserTitleEnabled": false,
+ "Encoder": {
+ "layout": "$A1",
+ "Icon": "imgs/actions/temperature/encoder",
+ "TriggerDescription": {
+ "Rotate": "Temperature",
+ "Push": "Power"
+ }
+ },
+ "States": [
+ {
+ "Image": "imgs/actions/temperature/key",
+ "TitleAlignment": "bottom",
+ "FontSize": 12,
+ "TitleColor": "#ffffff"
+ }
+ ]
+ }
+ ],
+ "Category": "Litra",
+ "CategoryIcon": "imgs/plugin/category-icon",
+ "CodePath": "bin/plugin.js",
+ "Description": "Control Logitech Litra Glow, Beam, and Beam LX front lights from Stream Deck and Stream Deck+ dials.",
+ "Icon": "imgs/plugin/marketplace",
+ "SDKVersion": 3,
+ "Software": {
+ "MinimumVersion": "7.1"
+ },
+ "OS": [
+ {
+ "Platform": "mac",
+ "MinimumVersion": "12"
+ },
+ {
+ "Platform": "windows",
+ "MinimumVersion": "10"
+ }
+ ],
+ "Nodejs": {
+ "Version": "20"
+ },
+ "UUID": "com.ssheppdev.litra"
+}
\ No newline at end of file
diff --git a/com.ssheppdev.litra.sdPlugin/ui/brightness.html b/com.ssheppdev.litra.sdPlugin/ui/brightness.html
new file mode 100644
index 0000000..78094f0
--- /dev/null
+++ b/com.ssheppdev.litra.sdPlugin/ui/brightness.html
@@ -0,0 +1,30 @@
+
+
+
+ Litra Brightness
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/com.ssheppdev.litra.sdPlugin/ui/power.html b/com.ssheppdev.litra.sdPlugin/ui/power.html
new file mode 100644
index 0000000..5766570
--- /dev/null
+++ b/com.ssheppdev.litra.sdPlugin/ui/power.html
@@ -0,0 +1,17 @@
+
+
+
+ Litra Power
+
+
+
+
+
+
+ Toggle
+ On
+ Off
+
+
+
+
diff --git a/com.ssheppdev.litra.sdPlugin/ui/temperature.html b/com.ssheppdev.litra.sdPlugin/ui/temperature.html
new file mode 100644
index 0000000..345e975
--- /dev/null
+++ b/com.ssheppdev.litra.sdPlugin/ui/temperature.html
@@ -0,0 +1,35 @@
+
+
+
+ Litra Temperature
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 100
+ 200
+ 300
+ 500
+
+
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..aedfac4
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,2782 @@
+{
+ "name": "com.ssheppdev.litra",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "com.ssheppdev.litra",
+ "version": "1.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "@elgato/streamdeck": "^2.1.0",
+ "litra": "^4.5.1"
+ },
+ "devDependencies": {
+ "@elgato/cli": "^1.7.4",
+ "@rollup/plugin-commonjs": "^29.0.2",
+ "@rollup/plugin-node-resolve": "^15.2.2",
+ "@rollup/plugin-terser": "^1.0.0",
+ "@rollup/plugin-typescript": "^12.1.0",
+ "@tsconfig/node20": "^20.1.2",
+ "@types/node": "~24.1.0",
+ "rollup": "^4.0.2",
+ "sharp": "^0.35.3",
+ "tslib": "^2.6.2",
+ "typescript": "^5.2.2"
+ }
+ },
+ "node_modules/@elgato/cli": {
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/@elgato/cli/-/cli-1.7.4.tgz",
+ "integrity": "sha512-6gD0f9p6VKNYv/INZGk4RzjBbiGSCUCiDOXxh52KyjVbnsJXNlFSKFBk8mtZIoJQHRl+5/QkZkKxXUmABxf86Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@elgato/schemas": "^0.4.14",
+ "@humanwhocodes/momoa": "^3.0.0",
+ "@zip.js/zip.js": "^2.7.54",
+ "ajv": "^8.17.1",
+ "chalk": "^5.4.1",
+ "commander": "^13.0.0",
+ "ejs": "^3.1.10",
+ "find-process": "^1.4.10",
+ "ignore": "^7.0.3",
+ "inquirer": "^12.3.2",
+ "is-interactive": "^2.0.0",
+ "lodash": "^4.17.21",
+ "log-symbols": "^7.0.0",
+ "rage-edit": "^1.2.0",
+ "semver": "^7.6.3",
+ "tar": "^7.5.3"
+ },
+ "bin": {
+ "sd": "bin/streamdeck.mjs",
+ "streamdeck": "bin/streamdeck.mjs"
+ },
+ "engines": {
+ "node": ">=20.1.0"
+ }
+ },
+ "node_modules/@elgato/schemas": {
+ "version": "0.4.15",
+ "resolved": "https://registry.npmjs.org/@elgato/schemas/-/schemas-0.4.15.tgz",
+ "integrity": "sha512-UpELAqazf52PFer+hqg/hvEhYoPHCH3R0af6P7Ih3Hk6xCCJENn7cA5TIpXAIGEWe6dIteHuo/e/iPGLUP7yXA==",
+ "license": "MIT"
+ },
+ "node_modules/@elgato/streamdeck": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@elgato/streamdeck/-/streamdeck-2.1.0.tgz",
+ "integrity": "sha512-d6wgSe93NbKAW0yT6/rabGCYUWic6nAoQoSLAXVadqTIUVmt+5Tfu7OaIGKnGER6AOn6dOpX6havRLFFFcUmOw==",
+ "license": "MIT",
+ "dependencies": {
+ "@elgato/schemas": "^0.4.14",
+ "@elgato/utils": "^0.4.4",
+ "ws": "^8.19.0"
+ },
+ "engines": {
+ "node": ">=20.5.1"
+ }
+ },
+ "node_modules/@elgato/utils": {
+ "version": "0.4.5",
+ "resolved": "https://registry.npmjs.org/@elgato/utils/-/utils-0.4.5.tgz",
+ "integrity": "sha512-kBoqM8LEhhVLv7lNftYqaecNna3T/L85QQWU56yR1iFCvqlyyD22WmBljRcQkw5ZNvib55Br3cnrsxX3ajp+dQ==",
+ "license": "MIT",
+ "dependencies": {
+ "zod": "^3.25.24"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.11.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz",
+ "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@humanwhocodes/momoa": {
+ "version": "3.3.10",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/momoa/-/momoa-3.3.10.tgz",
+ "integrity": "sha512-KWiFQpSAqEIyrTXko3hFNLeQvSK8zXlJQzhhxsyVn58WFRYXST99b3Nqnu+ttOtjds2Pl2grUHGpe2NzhPynuQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@img/colour": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
+ "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@img/sharp-darwin-arm64": {
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz",
+ "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-arm64": "1.3.2"
+ }
+ },
+ "node_modules/@img/sharp-darwin-x64": {
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz",
+ "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-x64": "1.3.2"
+ }
+ },
+ "node_modules/@img/sharp-freebsd-wasm32": {
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz",
+ "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "dependencies": {
+ "@img/sharp-wasm32": "0.35.3"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-arm64": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz",
+ "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-x64": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz",
+ "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz",
+ "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm64": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz",
+ "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-ppc64": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz",
+ "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-riscv64": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz",
+ "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-s390x": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz",
+ "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-x64": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz",
+ "integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz",
+ "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz",
+ "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm": {
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz",
+ "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm": "1.3.2"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm64": {
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz",
+ "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm64": "1.3.2"
+ }
+ },
+ "node_modules/@img/sharp-linux-ppc64": {
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz",
+ "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-ppc64": "1.3.2"
+ }
+ },
+ "node_modules/@img/sharp-linux-riscv64": {
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz",
+ "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-riscv64": "1.3.2"
+ }
+ },
+ "node_modules/@img/sharp-linux-s390x": {
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz",
+ "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-s390x": "1.3.2"
+ }
+ },
+ "node_modules/@img/sharp-linux-x64": {
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz",
+ "integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-x64": "1.3.2"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-arm64": {
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz",
+ "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-arm64": "1.3.2"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-x64": {
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz",
+ "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-x64": "1.3.2"
+ }
+ },
+ "node_modules/@img/sharp-wasm32": {
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz",
+ "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==",
+ "dev": true,
+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/runtime": "^1.11.1"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-webcontainers-wasm32": {
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz",
+ "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "@img/sharp-wasm32": "0.35.3"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-arm64": {
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz",
+ "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-ia32": {
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz",
+ "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-x64": {
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz",
+ "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@inquirer/ansi": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz",
+ "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/checkbox": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz",
+ "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/ansi": "^1.0.2",
+ "@inquirer/core": "^10.3.2",
+ "@inquirer/figures": "^1.0.15",
+ "@inquirer/type": "^3.0.10",
+ "yoctocolors-cjs": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/confirm": {
+ "version": "5.1.21",
+ "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz",
+ "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^10.3.2",
+ "@inquirer/type": "^3.0.10"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/core": {
+ "version": "10.3.2",
+ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz",
+ "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/ansi": "^1.0.2",
+ "@inquirer/figures": "^1.0.15",
+ "@inquirer/type": "^3.0.10",
+ "cli-width": "^4.1.0",
+ "mute-stream": "^2.0.0",
+ "signal-exit": "^4.1.0",
+ "wrap-ansi": "^6.2.0",
+ "yoctocolors-cjs": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/editor": {
+ "version": "4.2.23",
+ "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz",
+ "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^10.3.2",
+ "@inquirer/external-editor": "^1.0.3",
+ "@inquirer/type": "^3.0.10"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/expand": {
+ "version": "4.0.23",
+ "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz",
+ "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^10.3.2",
+ "@inquirer/type": "^3.0.10",
+ "yoctocolors-cjs": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/external-editor": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz",
+ "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chardet": "^2.1.1",
+ "iconv-lite": "^0.7.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/figures": {
+ "version": "1.0.15",
+ "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz",
+ "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/input": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz",
+ "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^10.3.2",
+ "@inquirer/type": "^3.0.10"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/number": {
+ "version": "3.0.23",
+ "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz",
+ "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^10.3.2",
+ "@inquirer/type": "^3.0.10"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/password": {
+ "version": "4.0.23",
+ "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz",
+ "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/ansi": "^1.0.2",
+ "@inquirer/core": "^10.3.2",
+ "@inquirer/type": "^3.0.10"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/prompts": {
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz",
+ "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/checkbox": "^4.3.2",
+ "@inquirer/confirm": "^5.1.21",
+ "@inquirer/editor": "^4.2.23",
+ "@inquirer/expand": "^4.0.23",
+ "@inquirer/input": "^4.3.1",
+ "@inquirer/number": "^3.0.23",
+ "@inquirer/password": "^4.0.23",
+ "@inquirer/rawlist": "^4.1.11",
+ "@inquirer/search": "^3.2.2",
+ "@inquirer/select": "^4.4.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/rawlist": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz",
+ "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^10.3.2",
+ "@inquirer/type": "^3.0.10",
+ "yoctocolors-cjs": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/search": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz",
+ "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^10.3.2",
+ "@inquirer/figures": "^1.0.15",
+ "@inquirer/type": "^3.0.10",
+ "yoctocolors-cjs": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/select": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz",
+ "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/ansi": "^1.0.2",
+ "@inquirer/core": "^10.3.2",
+ "@inquirer/figures": "^1.0.15",
+ "@inquirer/type": "^3.0.10",
+ "yoctocolors-cjs": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/type": {
+ "version": "3.0.10",
+ "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz",
+ "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@isaacs/fs-minipass": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
+ "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^7.0.4"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/source-map": {
+ "version": "0.3.11",
+ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
+ "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.25"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@rollup/plugin-commonjs": {
+ "version": "29.0.3",
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-29.0.3.tgz",
+ "integrity": "sha512-ZaOxZceP7SOUW7Lqw5IRVweSQYWaeIPnXIGLiB690EBA3FGJTO40EEr2L5yZplJWsgTCogILRSpcAe7+U0Otdg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rollup/pluginutils": "^5.0.1",
+ "commondir": "^1.0.1",
+ "estree-walker": "^2.0.2",
+ "fdir": "^6.2.0",
+ "is-reference": "1.2.1",
+ "magic-string": "^0.30.3",
+ "picomatch": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=16.0.0 || 14 >= 14.17"
+ },
+ "peerDependencies": {
+ "rollup": "^2.68.0||^3.0.0||^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rollup/plugin-node-resolve": {
+ "version": "15.3.1",
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz",
+ "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rollup/pluginutils": "^5.0.1",
+ "@types/resolve": "1.20.2",
+ "deepmerge": "^4.2.2",
+ "is-module": "^1.0.0",
+ "resolve": "^1.22.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^2.78.0||^3.0.0||^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rollup/plugin-terser": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-1.0.0.tgz",
+ "integrity": "sha512-FnCxhTBx6bMOYQrar6C8h3scPt8/JwIzw3+AJ2K++6guogH5fYaIFia+zZuhqv0eo1RN7W1Pz630SyvLbDjhtQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "serialize-javascript": "^7.0.3",
+ "smob": "^1.0.0",
+ "terser": "^5.17.4"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^2.0.0||^3.0.0||^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rollup/plugin-typescript": {
+ "version": "12.3.0",
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.3.0.tgz",
+ "integrity": "sha512-7DP0/p7y3t67+NabT9f8oTBFE6gGkto4SA6Np2oudYmZE/m1dt8RB0SjL1msMxFpLo631qjRCcBlAbq1ml/Big==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rollup/pluginutils": "^5.1.0",
+ "resolve": "^1.22.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^2.14.0||^3.0.0||^4.0.0",
+ "tslib": "*",
+ "typescript": ">=3.7.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ },
+ "tslib": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rollup/pluginutils": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz",
+ "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "estree-walker": "^2.0.2",
+ "picomatch": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz",
+ "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz",
+ "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz",
+ "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz",
+ "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz",
+ "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz",
+ "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz",
+ "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz",
+ "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz",
+ "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz",
+ "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz",
+ "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz",
+ "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz",
+ "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz",
+ "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz",
+ "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz",
+ "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz",
+ "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz",
+ "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz",
+ "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz",
+ "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz",
+ "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz",
+ "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz",
+ "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz",
+ "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz",
+ "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@tsconfig/node20": {
+ "version": "20.1.9",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node20/-/node20-20.1.9.tgz",
+ "integrity": "sha512-IjlTv1RsvnPtUcjTqtVsZExKVq+KQx4g5pCP5tI7rAs6Xesl2qFwSz/tPDBC4JajkL/MlezBu3gPUwqRHl+RIg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "24.1.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz",
+ "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.8.0"
+ }
+ },
+ "node_modules/@types/resolve": {
+ "version": "1.20.2",
+ "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
+ "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@zip.js/zip.js": {
+ "version": "2.8.26",
+ "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.26.tgz",
+ "integrity": "sha512-RQ4h9F6DOiHxpdocUDrOl6xBM+yOtz+LkUol47AVWcfebGBDpZ7w7Xvz9PS24JgXvLGiXXzSAfdCdVy1tPlaFA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "bun": ">=0.7.0",
+ "deno": ">=1.0.0",
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.17.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
+ "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/async": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
+ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
+ "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chardet": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz",
+ "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/chownr": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
+ "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/cli-width": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
+ "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/commander": {
+ "version": "13.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
+ "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/commondir": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
+ "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/deepmerge": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
+ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ejs": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
+ "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "jake": "^10.8.5"
+ },
+ "bin": {
+ "ejs": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-uri": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz",
+ "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/filelist": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz",
+ "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "minimatch": "^5.0.1"
+ }
+ },
+ "node_modules/find-process": {
+ "version": "1.4.11",
+ "resolved": "https://registry.npmjs.org/find-process/-/find-process-1.4.11.tgz",
+ "integrity": "sha512-mAOh9gGk9WZ4ip5UjV0o6Vb4SrfnAmtsFNzkMRH9HQiFXVQnDyQFrSHTK5UoG6E+KV+s+cIznbtwpfN41l2nFA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "~4.1.2",
+ "commander": "^12.1.0",
+ "loglevel": "^1.9.2"
+ },
+ "bin": {
+ "find-process": "bin/find-process.js"
+ }
+ },
+ "node_modules/find-process/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/find-process/node_modules/commander": {
+ "version": "12.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
+ "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.7.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
+ "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz",
+ "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/inquirer": {
+ "version": "12.11.1",
+ "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-12.11.1.tgz",
+ "integrity": "sha512-9VF7mrY+3OmsAfjH3yKz/pLbJ5z22E23hENKw3/LNSaA/sAt3v49bDRY+Ygct1xwuKT+U+cBfTzjCPySna69Qw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/ansi": "^1.0.2",
+ "@inquirer/core": "^10.3.2",
+ "@inquirer/prompts": "^7.10.1",
+ "@inquirer/type": "^3.0.10",
+ "mute-stream": "^2.0.0",
+ "run-async": "^4.0.6",
+ "rxjs": "^7.8.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.2",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
+ "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-interactive": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
+ "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-module": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
+ "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-reference": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
+ "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "*"
+ }
+ },
+ "node_modules/is-unicode-supported": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
+ "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/jake": {
+ "version": "10.9.4",
+ "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz",
+ "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "async": "^3.2.6",
+ "filelist": "^1.0.4",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "jake": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/litra": {
+ "version": "4.5.1",
+ "resolved": "https://registry.npmjs.org/litra/-/litra-4.5.1.tgz",
+ "integrity": "sha512-OotgIUzE9xwy2K8PGp2WT/XUAie/ssAecMgWrCaVPVYUvoigTamhU9a1B+DLkX4bM+dC1J/RyNL4mJtmy7iGgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "commander": "^13.1.0",
+ "node-hid": "^3.0.0"
+ },
+ "bin": {
+ "litra-brightness": "dist/commonjs/cli/litra-brightness.js",
+ "litra-brightness-lm": "dist/commonjs/cli/litra-brightness-lm.js",
+ "litra-devices": "dist/commonjs/cli/litra-devices.js",
+ "litra-identify": "dist/commonjs/cli/litra-identify.js",
+ "litra-off": "dist/commonjs/cli/litra-off.js",
+ "litra-on": "dist/commonjs/cli/litra-on.js",
+ "litra-oversight": "dist/commonjs/cli/litra-oversight.js",
+ "litra-temperature-k": "dist/commonjs/cli/litra-temperature-k.js",
+ "litra-toggle": "dist/commonjs/cli/litra-toggle.js"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/log-symbols": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz",
+ "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-unicode-supported": "^2.0.0",
+ "yoctocolors": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/loglevel": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz",
+ "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6.0"
+ },
+ "funding": {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/loglevel"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "5.1.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
+ "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
+ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/minizlib": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
+ "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/mute-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz",
+ "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/node-addon-api": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz",
+ "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==",
+ "license": "MIT"
+ },
+ "node_modules/node-hid": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/node-hid/-/node-hid-3.3.0.tgz",
+ "integrity": "sha512-j+dFgJLRAE0nufQKXk3IfS6T6YuHhCgMvz4TrG0sgtb6DSCdYpfJ1etcdmeCmPQjUgO+yo32ktVrRliNs/+fmg==",
+ "hasInstallScript": true,
+ "license": "(MIT OR X11)",
+ "dependencies": {
+ "node-addon-api": "^3.2.1",
+ "pkg-prebuilds": "^1.0.0"
+ },
+ "bin": {
+ "hid-showdevices": "src/show-devices.js"
+ },
+ "engines": {
+ "node": ">=10.16"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
+ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pkg-prebuilds": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/pkg-prebuilds/-/pkg-prebuilds-1.1.0.tgz",
+ "integrity": "sha512-jyai+KTQ2OwbN6iRYw88XbYOMgtpoSYJpjYebx7d9ihqz3txNi3ucsBt3va0iVWe6svSlaqpijMHFF/eJCMZzg==",
+ "license": "MIT",
+ "bin": {
+ "pkg-prebuilds-copy": "bin/copy.mjs",
+ "pkg-prebuilds-verify": "bin/verify.mjs"
+ },
+ "engines": {
+ "node": ">= 14.15.0"
+ }
+ },
+ "node_modules/rage-edit": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/rage-edit/-/rage-edit-1.2.0.tgz",
+ "integrity": "sha512-0RspBRc2s6We4g7hRCvT5mu7YPEnfjvQK8Tt354a2uUNJCMC7MKLvo/1mLvHUCQ/zbP6siQyp5VRZN7UCpMFZg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.12",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
+ "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz",
+ "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.9"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.62.2",
+ "@rollup/rollup-android-arm64": "4.62.2",
+ "@rollup/rollup-darwin-arm64": "4.62.2",
+ "@rollup/rollup-darwin-x64": "4.62.2",
+ "@rollup/rollup-freebsd-arm64": "4.62.2",
+ "@rollup/rollup-freebsd-x64": "4.62.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.62.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.62.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.62.2",
+ "@rollup/rollup-linux-arm64-musl": "4.62.2",
+ "@rollup/rollup-linux-loong64-gnu": "4.62.2",
+ "@rollup/rollup-linux-loong64-musl": "4.62.2",
+ "@rollup/rollup-linux-ppc64-gnu": "4.62.2",
+ "@rollup/rollup-linux-ppc64-musl": "4.62.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.62.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.62.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.62.2",
+ "@rollup/rollup-linux-x64-gnu": "4.62.2",
+ "@rollup/rollup-linux-x64-musl": "4.62.2",
+ "@rollup/rollup-openbsd-x64": "4.62.2",
+ "@rollup/rollup-openharmony-arm64": "4.62.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.62.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.62.2",
+ "@rollup/rollup-win32-x64-gnu": "4.62.2",
+ "@rollup/rollup-win32-x64-msvc": "4.62.2",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/run-async": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.6.tgz",
+ "integrity": "sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/rxjs": {
+ "version": "7.8.2",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
+ "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/serialize-javascript": {
+ "version": "7.0.7",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.7.tgz",
+ "integrity": "sha512-YAy8Od6KV+uuwUuU50np8fGB/Aues6Y0nAhA9y/hId74PlKUcme4pXcBD46NWKr1Q4osN/iseZ17YqO1XfmI8g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/sharp": {
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz",
+ "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@img/colour": "^1.1.0",
+ "detect-libc": "^2.1.2",
+ "semver": "^7.8.5"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-darwin-arm64": "0.35.3",
+ "@img/sharp-darwin-x64": "0.35.3",
+ "@img/sharp-freebsd-wasm32": "0.35.3",
+ "@img/sharp-libvips-darwin-arm64": "1.3.2",
+ "@img/sharp-libvips-darwin-x64": "1.3.2",
+ "@img/sharp-libvips-linux-arm": "1.3.2",
+ "@img/sharp-libvips-linux-arm64": "1.3.2",
+ "@img/sharp-libvips-linux-ppc64": "1.3.2",
+ "@img/sharp-libvips-linux-riscv64": "1.3.2",
+ "@img/sharp-libvips-linux-s390x": "1.3.2",
+ "@img/sharp-libvips-linux-x64": "1.3.2",
+ "@img/sharp-libvips-linuxmusl-arm64": "1.3.2",
+ "@img/sharp-libvips-linuxmusl-x64": "1.3.2",
+ "@img/sharp-linux-arm": "0.35.3",
+ "@img/sharp-linux-arm64": "0.35.3",
+ "@img/sharp-linux-ppc64": "0.35.3",
+ "@img/sharp-linux-riscv64": "0.35.3",
+ "@img/sharp-linux-s390x": "0.35.3",
+ "@img/sharp-linux-x64": "0.35.3",
+ "@img/sharp-linuxmusl-arm64": "0.35.3",
+ "@img/sharp-linuxmusl-x64": "0.35.3",
+ "@img/sharp-webcontainers-wasm32": "0.35.3",
+ "@img/sharp-win32-arm64": "0.35.3",
+ "@img/sharp-win32-ia32": "0.35.3",
+ "@img/sharp-win32-x64": "0.35.3"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/smob": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/smob/-/smob-1.6.2.tgz",
+ "integrity": "sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/tar": {
+ "version": "7.5.22",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz",
+ "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/fs-minipass": "^4.0.0",
+ "chownr": "^3.0.0",
+ "minipass": "^7.1.2",
+ "minizlib": "^3.1.0",
+ "yallist": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/terser": {
+ "version": "5.49.0",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.49.0.tgz",
+ "integrity": "sha512-SNiDnXyHSrxVcIOtVbULzcTmniUiwcV7Nwdyj1twVubeTmbjoa8p69KKDpfkdoOavuM4/GRm1+ykI8qqnavHoA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@jridgewell/source-map": "^0.3.3",
+ "acorn": "^8.15.0",
+ "commander": "^2.20.0",
+ "source-map-support": "~0.5.20"
+ },
+ "bin": {
+ "terser": "bin/terser"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/terser/node_modules/commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.8.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
+ "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.21.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
+ "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/yallist": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
+ "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/yoctocolors": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz",
+ "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/yoctocolors-cjs": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz",
+ "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..410d721
--- /dev/null
+++ b/package.json
@@ -0,0 +1,53 @@
+{
+ "name": "com.ssheppdev.litra",
+ "version": "1.0.0",
+ "private": true,
+ "description": "Elgato Stream Deck plugin for Logitech Litra lights (Glow, Beam, Beam LX front light).",
+ "license": "MIT",
+ "author": "Seth Sheppard",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/SSheppDev/streamdeck-litra.git"
+ },
+ "bugs": {
+ "url": "https://github.com/SSheppDev/streamdeck-litra/issues"
+ },
+ "homepage": "https://github.com/SSheppDev/streamdeck-litra#readme",
+ "keywords": [
+ "streamdeck",
+ "elgato",
+ "logitech",
+ "litra",
+ "lighting"
+ ],
+ "scripts": {
+ "build": "rollup -c",
+ "icons": "node scripts/generate-icons.mjs",
+ "watch": "rollup -c -w --watch.onEnd=\"streamdeck restart com.ssheppdev.litra\"",
+ "link": "streamdeck link com.ssheppdev.litra.sdPlugin",
+ "unlink": "streamdeck unlink com.ssheppdev.litra",
+ "validate": "npm run build && streamdeck validate com.ssheppdev.litra.sdPlugin",
+ "pack": "npm run build && streamdeck pack com.ssheppdev.litra.sdPlugin --force -o release --version 1.0.0.0"
+ },
+ "type": "module",
+ "devDependencies": {
+ "@elgato/cli": "^1.7.4",
+ "@rollup/plugin-commonjs": "^29.0.2",
+ "@rollup/plugin-node-resolve": "^15.2.2",
+ "@rollup/plugin-terser": "^1.0.0",
+ "@rollup/plugin-typescript": "^12.1.0",
+ "@tsconfig/node20": "^20.1.2",
+ "@types/node": "~24.1.0",
+ "rollup": "^4.0.2",
+ "sharp": "^0.35.3",
+ "tslib": "^2.6.2",
+ "typescript": "^5.2.2"
+ },
+ "dependencies": {
+ "@elgato/streamdeck": "^2.1.0",
+ "litra": "^4.5.1"
+ },
+ "allowScripts": {
+ "node-hid@3.3.0": true
+ }
+}
diff --git a/ref/README.md b/ref/README.md
new file mode 100644
index 0000000..9e29b95
--- /dev/null
+++ b/ref/README.md
@@ -0,0 +1,9 @@
+# Reference material
+
+| Path | Purpose |
+|------|---------|
+| [`scope-001.md`](scope-001.md) | Original v1 product scope |
+| [`build-001.md`](build-001.md) | Implementation notes for v1 |
+| [`hid-captures/NOTES.md`](hid-captures/NOTES.md) | Paused Beam LX **back RGB** / G HUB research |
+
+`litra/` and `litra-rs/` clones are gitignored local checkouts of upstream projects — install those from their own repos if you need them.
diff --git a/ref/build-001.md b/ref/build-001.md
new file mode 100644
index 0000000..0852f6b
--- /dev/null
+++ b/ref/build-001.md
@@ -0,0 +1,159 @@
+# Build plan: Litra Stream Deck plugin (v1)
+
+Derived from `ref/scope-001.md`, audited against `ref/litra` (JS) and published `litra@4.5.1`. Implements the agreed audit fixes while staying on the JS driver APIs.
+
+## Goals
+
+- Plugin UUID: `com.ssheppdev.litra`
+- Source tree at repo root: `com.ssheppdev.litra.sdPlugin` (via `streamdeck create`, then hand-edit)
+- Runtime dep: npm `litra` (do **not** vendor/copy from `ref/litra`)
+- Target hardware: Litra Beam LX; Stream Deck (keys) + Stream Deck + (dials)
+- v1 broadcast: every action applies to **all** connected Litra devices
+
+## Action model (aligned)
+
+Three actions only. Brightness and Temperature stay **separate** forever — never one combined “light” action.
+
+| Action | UUID suffix | Controllers | Key (button) | Dial |
+| --- | --- | --- | --- | --- |
+| Power | `.power` | Keypad only | toggle / on / off (PI mode) | — |
+| Brightness | `.brightness` | **Keypad + Encoder** | snap all devices to `presetPercentage` | step all devices by `stepPercentage` |
+| Temperature | `.temperature` | **Keypad + Encoder** | snap all devices to `presetKelvin` | step all devices by `stepKelvin` |
+
+Same UUID on a key *or* a dial slot — not four separate action UUIDs. Manifest: `Controllers: ["Keypad", "Encoder"]` for Brightness and Temperature; Encoder `layout: "$A1"`.
+
+## JS `litra` API map (source of truth)
+
+Use only these from `litra` (via `litra-manager`, never from action classes):
+
+| Need | `litra` export |
+| --- | --- |
+| Enumerate | `findDevices()` |
+| Power | `turnOn`, `turnOff`, `toggle`, `isOn` |
+| Brightness write % | `setBrightnessPercentage(device, 0–100)` → internally `percentageWithinRange` then `setBrightnessInLumen` |
+| Brightness write lm | `setBrightnessInLumen` (integer; device min/max) |
+| Brightness read | **see workaround below** — do not trust stock `getBrightnessInLumen` on Beam LX |
+| Brightness bounds | `getMinimumBrightnessInLumenForDevice`, `getMaximumBrightnessInLumenForDevice` |
+| Temp write | `setTemperatureInKelvin` (must be in allow-list) |
+| Temp read | `getTemperatureInKelvin` (`data[4]*256 + data[5]` — correct) |
+| Temp bounds / allow-list | `getMinimumTemperatureInKelvinForDevice`, `getMaximumTemperatureInKelvinForDevice`, `getAllowedTemperaturesInKelvinForDevice` |
+
+There is **no** `getBrightnessPercentage` in the package. Convert locally in `litra-manager` using the inverse of litra’s own mapping:
+
+```text
+# litra utils.percentageWithinRange (write path):
+lumen = ceil((pct / 100) * (max - min) + min)
+
+# our read path for dial / feedback:
+pct = clamp(round(((lumen - min) / (max - min)) * 100), 0, 100)
+```
+
+### Brightness read workaround (Beam LX)
+
+`ref/litra/src/driver.ts` `getBrightnessInLumen` returns only `data[5]`. Beam LX max is **400** lm; litra-rs correctly uses `data[4]*256 + data[5]`. Stock JS read truncates above 255 lm → dial/feedback wrong on target hardware.
+
+**v1 approach (stay on JS package, minimal ownership):** in `litra-manager`, implement `getBrightnessInLumenSafe(device)` that:
+
+1. Sends the same get-brightness report litra uses (`0x11, 0xff, 0x06, 0x31` for Beam LX / `0x04` for Glow/Beam — mirror `generateGetBrightnessInLumenBytes`).
+2. Parses `device.hid.readSync()` as `data[4] * 256 + data[5]` (same as temperature + litra-rs).
+3. Is the **only** brightness getter used by actions.
+
+Still use litra for `setBrightnessPercentage` / bounds helpers / findDevices / power / temperature. Document upstream bug; optional later PR to `timrogers/litra`. Do not pull in litra-rs for this.
+
+### HID handle lifetime
+
+`findDevices()` opens a real `node-hid` `HID` per device. Plugin process is long-lived → `forEachDevice` must `try/finally` close via `(device.hid as HID.HID).close()` (litra’s `Device.hid` type omits `close`).
+
+`litra` often `throw`s **strings**, not `Error` — catch with `catch (e)` and log `String(e)`.
+
+## Core module: `src/litra-manager.ts`
+
+Stateless broadcast seam (actions import **only** this module):
+
+- `forEachDevice(fn)`: `findDevices()` every call; per device `try { fn(device) } catch { log; mark failure } finally { close }`. Zero devices ⇒ failure (caller `showAlert()`).
+- Thin wrappers: `turnOnAll`, `turnOffAll`, `toggleAll`, `isOnAll` (aggregate: `length > 0 && every(isOn)`), `setBrightnessAllPercentage`, `adjustBrightnessAllByPercentageSteps(ticks, stepPercentage)`, `setTemperatureAllInKelvin`, `adjustTemperatureAllByKelvinSteps(ticks, stepKelvin)`, plus helpers for representative display values.
+- Logging: `streamDeck.logger` only.
+
+### Brightness dial math (percent space — matches JS write API)
+
+Per device inside one `forEachDevice` pass:
+
+1. `lumen = getBrightnessInLumenSafe(device)`
+2. `min` / `max` from litra bound helpers
+3. `currentPct = lumenToPercentage(lumen, min, max)`
+4. `nextPct = clamp(currentPct + ticks * stepPercentage, 0, 100)`
+5. `setBrightnessPercentage(device, nextPct)`
+
+Key press: `setBrightnessAllPercentage(presetPercentage)` only — no read.
+
+### Temperature dial math (Kelvin + allow-list)
+
+Per device:
+
+1. `current = getTemperatureInKelvin(device)`
+2. `raw = current + ticks * stepKelvin`
+3. Snap to nearest value in `getAllowedTemperaturesInKelvinForDevice(device)` (do **not** only min/max clamp — `setTemperatureInKelvin` throws if not in the allow-list)
+4. `setTemperatureInKelvin(device, snapped)`
+
+PI defaults: `presetKelvin: 4000`, `stepKelvin: 100` (both multiples of 100). Validate in PI / before HID call.
+
+### Dial debounce
+
+Accumulate `ticks` for ~50–80ms per action instance, then one `forEachDevice` batch (open/read/write/close once per burst). Feel-test on Stream Deck +; widen only if laggy. Stay under ~10 UI updates/sec (`setFeedback` / `setTitle`).
+
+## Manifest / UX details
+
+- Power: two `States` (Off/On); set `DisableAutomaticStates: true` so hardware-driven `setState()` is not fighting auto-toggle on key press.
+- Brightness / Temperature Encoder: `layout: "$A1"`; `setFeedback({ value: "NN%" | "NNNNK" })` from first successful device; key uses `setTitle()` similarly.
+- Icons: plugin + action list + key states (Power Off/On) + encoder icons — follow SDK size rules in `ref/STREAMDECK_SDK_DOCS.md`.
+- PI: `sdpi-components` — Power mode select; Brightness preset/step number; Temperature preset/step number (Kelvin, step multiple of 100).
+
+## Packaging spike (gate before actions)
+
+`litra` → `node-hid` native `.node`. Stream Deck’s bundler can drop or break it.
+
+**Gate:** after scaffold + `npm install litra`:
+
+1. Confirm `node-hid` loads inside the running plugin (not only bare Node).
+2. One successful `findDevices()` / `isOn` round-trip on Beam LX.
+3. Record the packaging approach that worked (typical: mark `node-hid` external + copy `.node` into `.sdPlugin`, or equivalent CLI/bundler config).
+
+Do not implement all actions until this spike passes.
+
+## Implementation sequence
+
+1. `streamdeck create` → rename UUID/files to `com.ssheppdev.litra`; Node 20 or 24 per manifest.
+2. `npm install litra`; configure native bundling; **HID spike**.
+3. Implement `litra-manager.ts` (forEachDevice, close, safe brightness read, %↔lm helpers, broadcast wrappers).
+4. Power action + PI + states + empty-device / alert behavior.
+5. Brightness action (key + dial + feedback/title) + PI.
+6. Temperature action (key + dial + allow-list snap + feedback/title) + PI.
+7. Manual verification (below).
+8. Optional: `streamdeck pack` when ready to distribute.
+
+## Out of scope (unchanged)
+
+- Beam LX RGB back light (JS lib has no API; litra-rs only)
+- Per-device picker (v1 all-or-nothing)
+- Any litra-rs / CLI / MCP integration
+
+## Verification
+
+1. `npm run build` / `npm run watch`; `streamdeck restart com.ssheppdev.litra`.
+2. Power key: toggle/on/off all devices; `onWillAppear` Off when none connected; On only if `length > 0 && every(isOn)`; alert when none plugged in.
+3. Brightness on a **key**: preset % on all devices. Brightness on a **dial**: smooth step by %; `$A1` shows %; works above ~64% / 255 lm on Beam LX (proves safe read).
+4. Temperature on a **key**: preset K. Temperature on a **dial**: steps stay on allow-list (no thrown string / alert from invalid K).
+5. Unplug/replug → next press works without plugin restart.
+6. Fast dial spin: responsive; no FD leak over a long session.
+
+## Fix log (vs scope-001)
+
+| Issue | Resolution |
+| --- | --- |
+| Dial mixed lumen + `%` then `setBrightnessPercentage` | Stay in **percent** space; convert with litra’s min/max mapping |
+| Temp clamp only | Snap to `getAllowedTemperaturesInKelvinForDevice` |
+| `node-hid` packaging | Explicit spike gate before actions |
+| Debounce vague | 50–80ms tick coalesce per action instance |
+| Feedback units | `%` for brightness, Kelvin for temperature |
+| Beam LX brightness read bug in JS litra | Local `getBrightnessInLumenSafe` in manager |
+| Brightness vs Temperature | Separate actions; each Keypad + Encoder |
diff --git a/ref/hid-captures/NOTES.md b/ref/hid-captures/NOTES.md
new file mode 100644
index 0000000..0f2eec8
--- /dev/null
+++ b/ref/hid-captures/NOTES.md
@@ -0,0 +1,246 @@
+# Litra Beam LX back-light research — status (paused)
+
+**Status:** paused 2026-07-13
+**Hardware:** 2× Litra Beam LX (`046d:c903`), synced in G HUB when testing presets
+**Host:** macOS (SIP enabled), G HUB 39.x with DriverKit `com.logi.ghub.hidfilter`
+**Plugin v1:** front white light only (`com.ssheppdev.litra`) — back RGB explicitly deferred
+
+This folder (`ref/hid-captures/`) holds raw JSON captures, Frida hook scaffolding, and this note. Upstream clones stay in `ref/litra` / `ref/litra-rs` (gitignored).
+
+---
+
+## Executive summary
+
+| Goal | Outcome |
+|------|---------|
+| Read which G HUB preset is active (Pulsar Point, Color Wave, …) | **Not available** via device GETs while G HUB is running |
+| Set solid zone colors without G HUB | **Works** (`litra` CLI / `0x8081` zone + commit) |
+| Set back on / brightness % | **Works** (`0x8040`, same as litra-rs) |
+| Map Pulsar / Color Wave to HID++ effect IDs | **Incomplete** — catalog known; G HUB does not expose live effect via polled registers |
+| Sniff G HUB HID writes on this Mac | **Blocked** — Hardened Runtime + SIP; agent uses DriverKit HID filter |
+
+**Practical v2 scope when resumed:** back on/off, back brightness %, solid (and maybe per-zone) RGB via known SET path. Named G HUB animations are a separate, harder track (USB sniff with SIP off, or Linux usbmon).
+
+---
+
+## What we already ship (v1 — do not regress)
+
+Front **Illumination** (`0x1990` @ feature index `0x06`): power, brightness (lumen/%), color temperature. Implemented in-plugin via npm `litra` + `src/litra-manager.ts` (safe brightness read, HID close, Node 20 packaging). Dial press toggles front power. Back light was out of scope for v1 by design (`ref/scope-001.md`).
+
+---
+
+## Protocol primer (HID++)
+
+- Usage page `0xFF43`, long reports: Report ID `0x11`, device index `0xFF`, 20-byte payload.
+- Byte 3 = `(functionId << 4) | softwareId`. Official apps often use softwareId `0xC`; litra-rs uses `0xB`. Both work for many calls.
+- Feature **index** (byte 2) is device-specific; resolve via Root `0x0000` / Feature Set `0x0001`.
+- Errors: response with feature index `0xFF` (extra `FF` in stream).
+- Refs: [cajus/litra-testbed](https://github.com/cajus/litra-testbed), OpenRGB `LogitechProtocolCommon`, libratbag `hidpp20` `0x8071`, litra-rs `set_back_*`.
+
+### Feature map (this Beam LX — 13 features)
+
+| Idx | Feature ID | Name / role |
+|----:|------------|-------------|
+| 0 | `0x0000` | Root |
+| 1 | `0x0001` | Feature Set |
+| 2 | `0x0003` | Firmware info |
+| 3 | `0x0005` | Device name/type |
+| 4 | `0x0011` | (unexplored) |
+| 5 | `0x1602` | (unexplored) |
+| 6 | `0x1990` | Illumination — **front white** (v1) |
+| 7 | `0x1eb0` | hidden/internal |
+| 8 | `0x1803` | hidden/internal |
+| 9 | `0x1807` | hidden/internal |
+| 10 | `0x8040` | Brightness Control — **back on / %** |
+| 11 | `0x8071` | RGB Effects — **firmware effect catalog** |
+| 12 | `0x8081` | Per-Key Lighting v2 — **7 zone RGB** |
+
+Capture: `hidpp-feature-map-2026-07-13T16-50-50-728Z.json`.
+
+---
+
+## Back power / brightness — `0x8040` @ idx `0x0A`
+
+Aligned with litra-rs / ha-litra:
+
+| Fn | Role | Notes |
+|---:|------|-------|
+| 0 | Status blob | e.g. `00 64 64 1f 00 01` while on @ 100% |
+| 1 | Get brightness % | payload often `[0, pct]` |
+| 2 | Set brightness % | `00 ` |
+| 3 | Get on | `01` / `00` |
+| 4 | Set on | `01` / `00` |
+
+**Verified:** `litra back-on`, `back-brightness --percentage 100`, and matching raw HID. Reliable with or without G HUB for these GETs.
+
+---
+
+## Zone RGB — `0x8081` @ idx `0x0C`
+
+| Fn | Role |
+|---:|------|
+| 0 | Info / mode byte (`fe` often seen) |
+| 1 | Zone color: `zone(1–7), R, G, B, FF, …` (litra-rs SET; RGB channels must be ≥ 1) |
+| 7 | Commit after zone writes: `00 00 01` (**required**) |
+
+**Verified without G HUB:**
+
+- `litra back-color --value FF0000` / `00FF00` — user confirmed **solid red**, and **one red + one green** when targeting each `--device-path`.
+- Sync in G HUB is separate; our SETs can address devices independently via path.
+
+**Not reliable:**
+
+- Treating `fn1` as a pure GET. Same function is the SET path; probing with incomplete params can **zero** colors or desync the HID read queue.
+- Zone “reads” returning `ff ff ff` even when UI showed fixed `#FF0000` under G HUB — do not trust as live framebuffer.
+- Rapid / wrong `0x8071` SETs sometimes left a device **back off** or returned empty ACKs / wedged reads (`Cannot write to hid device`). Prefer `litra` CLI for known-good color SETs; unplug/replug if HID wedges.
+
+---
+
+## RGB effects catalog — `0x8071` @ idx `0x0B`
+
+Device reports **2 clusters × 3 effects** (OpenRGB / libratbag layout).
+
+Info dump: `GET_INFO` with `[0xFF, 0xFF]` → cluster count `2`.
+Per cluster: `[cluster, 0xFF]` → metadata; `[cluster, effectIdx]` → effect id / caps / period.
+
+### Cluster 0 (location `1`)
+
+| Effect idx | Effect ID | Meaning |
+|-----------:|-----------|---------|
+| 0 | `0x0001` | Fixed / ON |
+| 1 | `0x000A` | Breathing (OpenRGB) |
+| 2 | `0x0015` | Unknown |
+
+### Cluster 1 (location `0xFF`)
+
+| Effect idx | Effect ID | Meaning |
+|-----------:|-----------|---------|
+| 0 | `0x0013` | Unknown |
+| 1 | `0x0016` | Unknown (Color Wave candidate — unproven) |
+| 2 | `0x0018` | Unknown (Pulsar candidate — unproven) |
+
+Capture: `rgb-effects-catalog-2026-07-13T16-51-39-170Z.json`.
+
+`SET_LED_EFFECT` is fn1 (OpenRGB `LOGITECH_FP8071_SET_LED_EFFECT`). SW control is fn5 with OpenRGB pattern `[1, 3, control]` (control `5` = SW). Taking SW control can interfere with firmware / G HUB animations.
+
+**Visual mapping:** incomplete. User reported “bounding” during an earlier multi-step cycle (likely breathing). Later A/B breathing-only test was inconclusive; one device was knocked **off** after effect SET. **Do not treat `0x0015`/`0x0016`/`0x0018` as named presets until remapped carefully with G HUB fully quit and one change at a time.**
+
+Cluster `effect_index` (byte after cluster id in `[c, 0xFF]` response) stayed `0` for Pulsar Point, Color Wave, and fixed color while G HUB ran — **not a usable live “current preset” signal under G HUB**.
+
+---
+
+## Preset capture campaign (with G HUB)
+
+User switched presets; we dumped status / zones / later `0x8071`:
+
+| Preset | Capture file(s) | Diff vs others |
+|--------|-----------------|----------------|
+| Pulsar Point | `pulsar-point-*.json`, `pulsar-point-8071-*.json` | Same interesting GETs as Color Wave / fixed |
+| Color Wave | `color-wave-*.json` | No effect fingerprint change |
+| Fixed `#FF0000` | `fixed-ff0000-*.json` | Zones still read white; only on/bri differed when a lamp was off |
+
+**Conclusion:** named presets are **not** distinguishable on the GET surface we know. G HUB almost certainly renders synced animations in software (possibly via the HID filter dext) without updating `0x8071` cluster selection or `0x8081` zone registers in a way we can poll.
+
+---
+
+## Dual-device poll monitor (flip test)
+
+- Script window ~90s, both HID++ interfaces, ~4 Hz poll of on/bri/status/clusters/zoneInfo/control + read drains.
+- User confirmed they **flipped** presets during the window.
+- Result: **`changeCount: 0`** on both devices (`ghub-monitor-2026-07-13T17-02-05-351Z-summary.json`).
+- Empty “events” in that log are false positives (zero-length reads), not HID++ notifications.
+
+**Implication for sync:** lights stay visually synced, but our GETs never moved — G HUB is driving both (or mirroring) **above** the registers we poll. We still don’t know if the write fan-out is 1× or 2× on the wire without a USB sniff.
+
+---
+
+## Why we can’t sniff G HUB writes on this Mac (yet)
+
+1. **`lghub_agent` Hardened Runtime** (`codesign` flags `runtime`) — even `sudo frida -p ` fails: *unable to access process from the current user account*.
+2. Agent entitlement: `com.apple.developer.driverkit.userclient-access` → **`com.logi.ghub.hidfilter`** (activated DriverKit HID filter). Traffic may not be plain userspace `IOHIDDeviceSetReport`.
+3. **SIP enabled** — Wireshark USB (`XHC20`) needs SIP disabled from Recovery (reboot in + reboot out; cannot toggle from a normal session).
+4. Scaffolding left in place for later:
+ - `hook-lghub-hid.js` — Frida hooks for IOHID / `write`
+ - `capture-ghub-hid.sh` — attach helper (will keep failing until SIP/hardened constraints change)
+ - `.venv-frida/` — local frida-tools venv (**gitignored**)
+
+### Resume options for sniffing
+
+| Approach | Cost |
+|----------|------|
+| Recovery → `csrutil disable` → Wireshark USB → re-enable SIP | Two Recovery reboots; best on-Mac path |
+| Linux box/VM + usbmon + USB passthrough | No SIP; cleanest HID capture |
+| Skip sniff; implement SET-only back actions | Product path for v2 |
+
+---
+
+## Tooling that worked
+
+- **Raw `node-hid`** probes (this repo’s Node deps).
+- **`litra` CLI** (`/opt/homebrew/bin/litra`) — best known-good for `back-on`, `back-brightness`, `back-color` (and `--device-path` / `--zone`).
+- **OpenRGB / libratbag / litra-rs / ha-litra / litra-testbed** — protocol documentation, not runtime deps of the plugin.
+
+---
+
+## Capture index
+
+| File | What |
+|------|------|
+| `pulsar-point-2026-07-13T16-46-38-395Z.json` | Early GET dump, Pulsar |
+| `color-wave-2026-07-13T16-47-51-433Z.json` | Early GET dump, Color Wave |
+| `fixed-ff0000-2026-07-13T16-48-38-371Z.json` | Early GET dump, fixed red |
+| `write-ff0000-then-read-*.json` | Zone SET then GET (still white under G HUB) |
+| `hidpp-feature-map-*.json` | Full feature table + probes |
+| `effect-slot-scan-*.json` | `0x8071` / `0x8081` param scan |
+| `rgb-effects-catalog-*.json` | Cluster/effect ID catalog |
+| `pulsar-point-8071-*.json` | Pulsar with cluster state (effect_index still 0) |
+| `ghub-monitor-*-summary.json` | Dual poll during flips — **0 changes** (raw `.jsonl` discarded; gitignored) |
+| `hook-lghub-hid.js` / `capture-ghub-hid.sh` | Frida attach (blocked on this Mac) |
+
+Raw Frida/monitor `.jsonl` streams and `.venv-frida/` are gitignored — recreate the venv with `python3 -m venv .venv-frida && .venv-frida/bin/pip install frida-tools` if needed.
+
+---
+
+## Recommended next steps (when unpaused)
+
+### Product (v2 back light) — preferred first slice
+
+1. Extend `litra-manager` with raw HID++ (or thin wrappers) for:
+ - back on / off / toggle
+ - back brightness %
+ - set all zones + commit (single color); optional `--zone` later
+2. New Stream Deck actions + PI, same broadcast/stateless pattern as front light.
+3. Do **not** promise G HUB preset names until sniffing or careful effect mapping lands.
+4. Document: quit G HUB (or expect fights) when using plugin back RGB; hidfilter/agent may reclaim.
+
+### Research — only if named presets matter
+
+1. **Wireshark USB** after SIP disable, or **Linux usbmon**, while flipping Fixed → Pulsar → Color Wave with sync on/off.
+2. Map captured writes to `0x8071` SET vs `0x8081` zone streams; check whether sync = 2× identical writes.
+3. With G HUB **fully** quit (agent + UI; updater alone is OK), one-effect-at-a-time SET on a single device; user confirms visual; record ID → name. Avoid rapid multi-effect scripts (device wedge risk).
+4. Re-test zone GET only with a dedicated non-destructive method if one is found; until then assume **SET-only** for color.
+
+### Explicit non-goals while paused
+
+- Shipping Pulsar Point / Color Wave as first-class actions.
+- Relying on Frida against hardened `lghub_agent` without SIP changes.
+- Treating `0x0c 1b` probes as safe color reads.
+
+---
+
+## Open questions
+
+1. Exact wire mapping of G HUB “Pulsar Point” / “Color Wave” / “Fixed” (effect SET vs streamed zone frames).
+2. Whether synced lights get one USB conversation or two.
+3. Safe GET for current zone RGB (if any).
+4. Stable `0x8071` SET sequence that selects firmware effects without turning back off or wedging HID.
+5. Behavior when plugin and G HUB both run (ownership / flicker).
+
+---
+
+## Related repo docs
+
+- [`README.md`](../README.md) — public install / build / pack
+- `ref/scope-001.md` — v1 scope; back RGB deferred
+- `ref/build-001.md` — v1 implementation plan
+- Plugin: `src/litra-manager.ts`, actions under `src/actions/`
diff --git a/ref/hid-captures/capture-ghub-hid.sh b/ref/hid-captures/capture-ghub-hid.sh
new file mode 100755
index 0000000..e00a514
--- /dev/null
+++ b/ref/hid-captures/capture-ghub-hid.sh
@@ -0,0 +1,37 @@
+#!/usr/bin/env bash
+# Capture HID++ writes from lghub_agent via Frida.
+#
+# BLOCKED on current Mac (2026-07-13): lghub_agent is Hardened Runtime; even
+# `sudo frida -p …` fails with "unable to access process". Agent also uses
+# DriverKit com.logi.ghub.hidfilter. See NOTES.md — use Wireshark USB after
+# SIP disable, or Linux usbmon, instead.
+#
+# Requires: sudo, project venv with frida-tools (.venv-frida).
+#
+# Usage (from repo root):
+# ./ref/hid-captures/capture-ghub-hid.sh
+# Then flip presets in G HUB. Ctrl-C to stop.
+
+set -euo pipefail
+ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
+FRIDA="$ROOT/.venv-frida/bin/frida"
+HOOK="$ROOT/ref/hid-captures/hook-lghub-hid.js"
+OUT="$ROOT/ref/hid-captures/ghub-frida-$(date -u +%Y-%m-%dT%H-%M-%SZ).jsonl"
+
+if [[ ! -x "$FRIDA" ]]; then
+ echo "Frida venv missing. From repo root:"
+ echo " python3 -m venv .venv-frida && .venv-frida/bin/pip install frida-tools"
+ exit 1
+fi
+
+PID="$(pgrep -n -f 'lghub_agent.app/Contents/MacOS/lghub_agent' || true)"
+if [[ -z "${PID}" ]]; then
+ echo "lghub_agent not running — open G HUB first"
+ exit 1
+fi
+
+echo "Attaching to lghub_agent pid=$PID"
+echo "Logging to $OUT"
+echo "Flip presets now. Ctrl-C to stop."
+# shellcheck disable=SC2024
+sudo "$FRIDA" -p "$PID" -l "$HOOK" --runtime=v8 2>"$OUT.err" | tee "$OUT"
diff --git a/ref/hid-captures/color-wave-2026-07-13T16-47-51-433Z.json b/ref/hid-captures/color-wave-2026-07-13T16-47-51-433Z.json
new file mode 100644
index 0000000..7de845a
--- /dev/null
+++ b/ref/hid-captures/color-wave-2026-07-13T16-47-51-433Z.json
@@ -0,0 +1,1242 @@
+{
+ "profileHint": "Color Wave",
+ "capturedAt": "2026-07-13T16:47:51.433Z",
+ "devices": [
+ {
+ "index": 0,
+ "path": "DevSrvsID:4297198173",
+ "productId": 51459,
+ "probes": [
+ {
+ "name": "back_status_0a_0b",
+ "request": [
+ 17,
+ 255,
+ 10,
+ 11,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0a 0b 00 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 10,
+ 11,
+ 0,
+ 100,
+ 100,
+ 31,
+ 0,
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0a 0b 00 64 64 1f 00 01 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 100,
+ 100,
+ 31,
+ 0,
+ 1,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "back_brightness",
+ "request": [
+ 17,
+ 255,
+ 10,
+ 27,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0a 1b 00 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 10,
+ 27,
+ 0,
+ 100,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0a 1b 00 64 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 100,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "back_on",
+ "request": [
+ 17,
+ 255,
+ 10,
+ 59,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0a 3b 00 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 10,
+ 59,
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0a 3b 01 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "fx_0c_0b",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 11,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 0b 00 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 11,
+ 0,
+ 0,
+ 254,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 0b 00 00 fe 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 0,
+ 254,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "fx_0b_0b",
+ "request": [
+ 17,
+ 255,
+ 11,
+ 11,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0b 0b 00 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 11,
+ 11,
+ 0,
+ 0,
+ 0,
+ 1,
+ 0,
+ 5,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0b 0b 00 00 00 01 00 05 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 0,
+ 0,
+ 1,
+ 0,
+ 5,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "fx_0b_5b",
+ "request": [
+ 17,
+ 255,
+ 11,
+ 91,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0b 5b 00 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 11,
+ 91,
+ 0,
+ 3,
+ 7,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0b 5b 00 03 07 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 3,
+ 7,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_1",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 1,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 01 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 1,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 01 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 1,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_2",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 2,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 02 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 2,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 02 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 2,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_3",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 3,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 03 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 3,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 03 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 3,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_4",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 4,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 04 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 4,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 04 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 4,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_5",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 5,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 05 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 5,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 05 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 5,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_6",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 6,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 06 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 6,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 06 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 6,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_7",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 7,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 07 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 7,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 07 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 7,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ }
+ ]
+ },
+ {
+ "index": 1,
+ "path": "DevSrvsID:4297198200",
+ "productId": 51459,
+ "probes": [
+ {
+ "name": "back_status_0a_0b",
+ "request": [
+ 17,
+ 255,
+ 10,
+ 11,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0a 0b 00 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 10,
+ 11,
+ 0,
+ 100,
+ 100,
+ 31,
+ 0,
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0a 0b 00 64 64 1f 00 01 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 100,
+ 100,
+ 31,
+ 0,
+ 1,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "back_brightness",
+ "request": [
+ 17,
+ 255,
+ 10,
+ 27,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0a 1b 00 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 10,
+ 27,
+ 0,
+ 100,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0a 1b 00 64 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 100,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "back_on",
+ "request": [
+ 17,
+ 255,
+ 10,
+ 59,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0a 3b 00 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 10,
+ 59,
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0a 3b 01 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "fx_0c_0b",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 11,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 0b 00 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 11,
+ 0,
+ 0,
+ 254,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 0b 00 00 fe 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 0,
+ 254,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "fx_0b_0b",
+ "request": [
+ 17,
+ 255,
+ 11,
+ 11,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0b 0b 00 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 11,
+ 11,
+ 0,
+ 0,
+ 0,
+ 1,
+ 0,
+ 5,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0b 0b 00 00 00 01 00 05 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 0,
+ 0,
+ 1,
+ 0,
+ 5,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "fx_0b_5b",
+ "request": [
+ 17,
+ 255,
+ 11,
+ 91,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0b 5b 00 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 11,
+ 91,
+ 0,
+ 3,
+ 7,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0b 5b 00 03 07 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 3,
+ 7,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_1",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 1,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 01 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 1,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 01 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 1,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_2",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 2,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 02 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 2,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 02 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 2,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_3",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 3,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 03 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 3,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 03 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 3,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_4",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 4,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 04 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 4,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 04 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 4,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_5",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 5,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 05 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 5,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 05 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 5,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_6",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 6,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 06 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 6,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 06 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 6,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_7",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 7,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 07 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 7,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 07 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 7,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/ref/hid-captures/effect-slot-scan-2026-07-13T16-51-07-447Z.json b/ref/hid-captures/effect-slot-scan-2026-07-13T16-51-07-447Z.json
new file mode 100644
index 0000000..1c594b1
--- /dev/null
+++ b/ref/hid-captures/effect-slot-scan-2026-07-13T16-51-07-447Z.json
@@ -0,0 +1,472 @@
+{
+ "effectSlots": [
+ {
+ "i": 0,
+ "resp": "11 ff 0b 0c 00 00 00 01 00 05 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 0,
+ 0,
+ 1,
+ 0,
+ 5,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": 1,
+ "resp": "11 ff 0b 0c 01 00 00 13 00 01 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 1,
+ 0,
+ 0,
+ 19,
+ 0,
+ 1,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": 2,
+ "resp": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 12,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": 3,
+ "resp": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 12,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": 4,
+ "resp": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 12,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": 5,
+ "resp": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 12,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": 6,
+ "resp": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 12,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": 7,
+ "resp": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 12,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": 8,
+ "resp": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 12,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": 9,
+ "resp": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 12,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": 10,
+ "resp": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 12,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": 11,
+ "resp": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 12,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": 12,
+ "resp": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 12,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": 13,
+ "resp": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 12,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": 14,
+ "resp": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 12,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": 15,
+ "resp": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 12,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": 16,
+ "resp": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 12,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": 17,
+ "resp": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 12,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": 18,
+ "resp": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 12,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": 19,
+ "resp": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 12,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": 20,
+ "resp": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 12,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": 21,
+ "resp": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 12,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": 22,
+ "resp": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 12,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": 23,
+ "resp": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 12,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": 24,
+ "resp": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 12,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": 25,
+ "resp": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 12,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": 26,
+ "resp": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 12,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": 27,
+ "resp": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 12,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": 28,
+ "resp": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 12,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": 29,
+ "resp": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 12,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": 30,
+ "resp": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 12,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": 31,
+ "resp": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 12,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "i": 32,
+ "resp": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 12,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ }
+ ],
+ "before": {
+ "s8071_0": "11 ff 0b 0c 00 00 00 01 00 05 00 00 00 00 00 00 00 00 00 00",
+ "s8071_0_1": "11 ff 0b 0c 01 00 00 13 00 01 00 00 00 00 00 00 00 00 00 00",
+ "s8081_0": "11 ff 0c 0c 00 00 fe 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "s8040_0": "11 ff 0a 0c 00 64 64 1f 00 01 00 00 00 00 00 00 00 00 00 00"
+ }
+}
\ No newline at end of file
diff --git a/ref/hid-captures/fixed-ff0000-2026-07-13T16-48-38-371Z.json b/ref/hid-captures/fixed-ff0000-2026-07-13T16-48-38-371Z.json
new file mode 100644
index 0000000..ceff859
--- /dev/null
+++ b/ref/hid-captures/fixed-ff0000-2026-07-13T16-48-38-371Z.json
@@ -0,0 +1,1337 @@
+{
+ "profileHint": "Fixed FF0000",
+ "capturedAt": "2026-07-13T16:48:38.371Z",
+ "note": "User set fixed solid red #FF0000",
+ "devices": [
+ {
+ "index": 0,
+ "path": "DevSrvsID:4297198200",
+ "productId": 51459,
+ "probes": [
+ {
+ "name": "back_status_0a_0b",
+ "request": [
+ 17,
+ 255,
+ 10,
+ 11,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0a 0b 00 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 10,
+ 11,
+ 0,
+ 100,
+ 100,
+ 31,
+ 0,
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0a 0b 00 64 64 1f 00 01 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 100,
+ 100,
+ 31,
+ 0,
+ 1,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "back_brightness",
+ "request": [
+ 17,
+ 255,
+ 10,
+ 27,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0a 1b 00 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 10,
+ 27,
+ 0,
+ 100,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0a 1b 00 64 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 100,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "back_on",
+ "request": [
+ 17,
+ 255,
+ 10,
+ 59,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0a 3b 00 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 10,
+ 59,
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0a 3b 01 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "fx_0c_0b",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 11,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 0b 00 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 11,
+ 0,
+ 0,
+ 254,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 0b 00 00 fe 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 0,
+ 254,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "fx_0b_0b",
+ "request": [
+ 17,
+ 255,
+ 11,
+ 11,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0b 0b 00 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 11,
+ 11,
+ 0,
+ 0,
+ 0,
+ 1,
+ 0,
+ 5,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0b 0b 00 00 00 01 00 05 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 0,
+ 0,
+ 1,
+ 0,
+ 5,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "fx_0b_5b",
+ "request": [
+ 17,
+ 255,
+ 11,
+ 91,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0b 5b 00 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 11,
+ 91,
+ 0,
+ 3,
+ 7,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0b 5b 00 03 07 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 3,
+ 7,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "fx_0c_1b_noz",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 00 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 255,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b ff ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 255,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_1",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 1,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 01 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 1,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 01 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 1,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_2",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 2,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 02 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 2,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 02 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 2,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_3",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 3,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 03 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 3,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 03 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 3,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_4",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 4,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 04 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 4,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 04 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 4,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_5",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 5,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 05 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 5,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 05 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 5,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_6",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 6,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 06 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 6,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 06 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 6,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_7",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 7,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 07 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 7,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 07 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 7,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ }
+ ]
+ },
+ {
+ "index": 1,
+ "path": "DevSrvsID:4297198173",
+ "productId": 51459,
+ "probes": [
+ {
+ "name": "back_status_0a_0b",
+ "request": [
+ 17,
+ 255,
+ 10,
+ 11,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0a 0b 00 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 10,
+ 11,
+ 0,
+ 100,
+ 100,
+ 31,
+ 0,
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0a 0b 00 64 64 1f 00 01 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 100,
+ 100,
+ 31,
+ 0,
+ 1,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "back_brightness",
+ "request": [
+ 17,
+ 255,
+ 10,
+ 27,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0a 1b 00 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 10,
+ 27,
+ 0,
+ 100,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0a 1b 00 64 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 100,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "back_on",
+ "request": [
+ 17,
+ 255,
+ 10,
+ 59,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0a 3b 00 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 10,
+ 59,
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0a 3b 01 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "fx_0c_0b",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 11,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 0b 00 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 11,
+ 0,
+ 0,
+ 254,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 0b 00 00 fe 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 0,
+ 254,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "fx_0b_0b",
+ "request": [
+ 17,
+ 255,
+ 11,
+ 11,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0b 0b 00 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 11,
+ 11,
+ 0,
+ 0,
+ 0,
+ 1,
+ 0,
+ 5,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0b 0b 00 00 00 01 00 05 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 0,
+ 0,
+ 1,
+ 0,
+ 5,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "fx_0b_5b",
+ "request": [
+ 17,
+ 255,
+ 11,
+ 91,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0b 5b 00 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 11,
+ 91,
+ 0,
+ 3,
+ 7,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0b 5b 00 03 07 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 3,
+ 7,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "fx_0c_1b_noz",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 00 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 255,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b ff ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 255,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_1",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 1,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 01 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 1,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 01 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 1,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_2",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 2,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 02 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 2,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 02 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 2,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_3",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 3,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 03 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 3,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 03 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 3,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_4",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 4,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 04 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 4,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 04 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 4,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_5",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 5,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 05 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 5,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 05 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 5,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_6",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 6,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 06 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 6,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 06 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 6,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_7",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 7,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 07 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 7,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 07 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 7,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/ref/hid-captures/ghub-monitor-2026-07-13T17-02-05-351Z-summary.json b/ref/hid-captures/ghub-monitor-2026-07-13T17-02-05-351Z-summary.json
new file mode 100644
index 0000000..fe4b2ea
--- /dev/null
+++ b/ref/hid-captures/ghub-monitor-2026-07-13T17-02-05-351Z-summary.json
@@ -0,0 +1,266 @@
+{
+ "durationMs": 90241,
+ "changeCount": 0,
+ "eventCount": 2540,
+ "changesPerDevice": {
+ "0": 0,
+ "1": 0
+ },
+ "eventsPerDevice": {
+ "0": 1270,
+ "1": 1270
+ },
+ "changes": [],
+ "events": [
+ {
+ "type": "event",
+ "device": 0,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 0,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 0,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 0,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 0,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 1,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 1,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 1,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 1,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 1,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 0,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 0,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 0,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 0,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 0,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 1,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 1,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 1,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 1,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 1,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 0,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 0,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 0,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 0,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 0,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 1,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 1,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 1,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 1,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 1,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 0,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 0,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 0,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 0,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 0,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 1,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 1,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 1,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 1,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 1,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 0,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 0,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 0,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 0,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 0,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 1,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 1,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 1,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 1,
+ "hex": ""
+ },
+ {
+ "type": "event",
+ "device": 1,
+ "hex": ""
+ }
+ ]
+}
\ No newline at end of file
diff --git a/ref/hid-captures/hidpp-feature-map-2026-07-13T16-50-50-728Z.json b/ref/hid-captures/hidpp-feature-map-2026-07-13T16-50-50-728Z.json
new file mode 100644
index 0000000..525f906
--- /dev/null
+++ b/ref/hid-captures/hidpp-feature-map-2026-07-13T16-50-50-728Z.json
@@ -0,0 +1,511 @@
+{
+ "capturedAt": "2026-07-13T16:50:50.728Z",
+ "protocol": "11 ff 00 1c 04 02 5a 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "featureCount": 13,
+ "features": [
+ {
+ "index": 0,
+ "featureId": "0x0000",
+ "featureType": 0,
+ "featureVersion": 2,
+ "raw": "11 ff 01 1c 00 00 00 02 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "index": 1,
+ "featureId": "0x0001",
+ "featureType": 0,
+ "featureVersion": 2,
+ "raw": "11 ff 01 1c 00 01 00 02 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "index": 2,
+ "featureId": "0x0003",
+ "featureType": 0,
+ "featureVersion": 5,
+ "raw": "11 ff 01 1c 00 03 00 05 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "index": 3,
+ "featureId": "0x0005",
+ "featureType": 0,
+ "featureVersion": 0,
+ "raw": "11 ff 01 1c 00 05 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "index": 4,
+ "featureId": "0x0011",
+ "featureType": 0,
+ "featureVersion": 0,
+ "raw": "11 ff 01 1c 00 11 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "index": 5,
+ "featureId": "0x1602",
+ "featureType": 0,
+ "featureVersion": 0,
+ "raw": "11 ff 01 1c 16 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "index": 6,
+ "featureId": "0x1990",
+ "featureType": 0,
+ "featureVersion": 1,
+ "raw": "11 ff 01 1c 19 90 00 01 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "index": 7,
+ "featureId": "0x1eb0",
+ "featureType": 112,
+ "featureVersion": 0,
+ "raw": "11 ff 01 1c 1e b0 70 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "index": 8,
+ "featureId": "0x1803",
+ "featureType": 112,
+ "featureVersion": 0,
+ "raw": "11 ff 01 1c 18 03 70 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "index": 9,
+ "featureId": "0x1807",
+ "featureType": 112,
+ "featureVersion": 1,
+ "raw": "11 ff 01 1c 18 07 70 01 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "index": 10,
+ "featureId": "0x8040",
+ "featureType": 0,
+ "featureVersion": 1,
+ "raw": "11 ff 01 1c 80 40 00 01 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "index": 11,
+ "featureId": "0x8071",
+ "featureType": 0,
+ "featureVersion": 4,
+ "raw": "11 ff 01 1c 80 71 00 04 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "index": 12,
+ "featureId": "0x8081",
+ "featureType": 0,
+ "featureVersion": 0,
+ "raw": "11 ff 01 1c 80 81 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ }
+ ],
+ "probes": [
+ {
+ "featureIndex": 0,
+ "featureId": "0x0000",
+ "func": 0,
+ "params": [],
+ "error": null,
+ "resp": "11 ff 00 0c 00 00 02 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 1,
+ "featureId": "0x0001",
+ "func": 0,
+ "params": [],
+ "error": null,
+ "resp": "11 ff 01 0c 0d 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 2,
+ "featureId": "0x0003",
+ "func": 0,
+ "params": [],
+ "error": null,
+ "resp": "11 ff 02 0c 04 1d e9 31 66 00 0a b9 03 c9 03 00 00 00 01 00"
+ },
+ {
+ "featureIndex": 3,
+ "featureId": "0x0005",
+ "func": 0,
+ "params": [],
+ "error": null,
+ "resp": "11 ff 03 0c 0d 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 6,
+ "featureId": "0x1990",
+ "func": 0,
+ "params": [],
+ "error": null,
+ "resp": "11 ff 06 0c 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 6,
+ "featureId": "0x1990",
+ "func": 0,
+ "params": [
+ 1
+ ],
+ "error": null,
+ "resp": "11 ff 06 0c 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 6,
+ "featureId": "0x1990",
+ "func": 0,
+ "params": [
+ 1,
+ 255,
+ 1,
+ 1
+ ],
+ "error": null,
+ "resp": "11 ff 06 0c 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 6,
+ "featureId": "0x1990",
+ "func": 1,
+ "params": [],
+ "error": null,
+ "resp": "11 ff 06 1c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 6,
+ "featureId": "0x1990",
+ "func": 1,
+ "params": [
+ 1
+ ],
+ "error": null,
+ "resp": "11 ff 06 1c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 6,
+ "featureId": "0x1990",
+ "func": 1,
+ "params": [
+ 1,
+ 255,
+ 1,
+ 1
+ ],
+ "error": null,
+ "resp": "11 ff 06 1c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 6,
+ "featureId": "0x1990",
+ "func": 2,
+ "params": [],
+ "error": null,
+ "resp": "11 ff 06 2c 05 00 1e 01 90 00 01 0f 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 6,
+ "featureId": "0x1990",
+ "func": 3,
+ "params": [],
+ "error": null,
+ "resp": "11 ff 06 3c 00 c5 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 6,
+ "featureId": "0x1990",
+ "func": 5,
+ "params": [],
+ "error": null,
+ "resp": "11 ff 06 5c e0 0a 00 34 00 50 00 78 00 a0 00 c8 00 f0 01 18"
+ },
+ {
+ "featureIndex": 6,
+ "featureId": "0x1990",
+ "func": 7,
+ "params": [],
+ "error": null,
+ "resp": "11 ff 06 7c 05 0a 8c 19 64 00 64 0f 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 6,
+ "featureId": "0x1990",
+ "func": 8,
+ "params": [],
+ "error": null,
+ "resp": "11 ff 06 8c 0b b8 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 6,
+ "featureId": "0x1990",
+ "func": 10,
+ "params": [],
+ "error": null,
+ "resp": "11 ff 06 ac e0 0a 0a 8c 0b b8 0d 48 0e 74 0f a0 11 30 13 88"
+ },
+ {
+ "featureIndex": 6,
+ "featureId": "0x1990",
+ "func": 12,
+ "params": [],
+ "error": null,
+ "resp": "11 ff 06 cc 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 10,
+ "featureId": "0x8040",
+ "func": 0,
+ "params": [],
+ "error": null,
+ "resp": "11 ff 0a 0c 00 64 64 1f 00 01 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 10,
+ "featureId": "0x8040",
+ "func": 0,
+ "params": [
+ 1
+ ],
+ "error": null,
+ "resp": "11 ff 0a 0c 00 64 64 1f 00 01 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 10,
+ "featureId": "0x8040",
+ "func": 0,
+ "params": [
+ 1,
+ 255,
+ 1,
+ 1
+ ],
+ "error": null,
+ "resp": "11 ff 0a 0c 00 64 64 1f 00 01 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 10,
+ "featureId": "0x8040",
+ "func": 1,
+ "params": [],
+ "error": null,
+ "resp": "11 ff 0a 1c 00 64 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 10,
+ "featureId": "0x8040",
+ "func": 1,
+ "params": [
+ 1
+ ],
+ "error": null,
+ "resp": "11 ff 0a 1c 00 64 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 10,
+ "featureId": "0x8040",
+ "func": 1,
+ "params": [
+ 1,
+ 255,
+ 1,
+ 1
+ ],
+ "error": null,
+ "resp": "11 ff 0a 1c 00 64 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 10,
+ "featureId": "0x8040",
+ "func": 2,
+ "params": [],
+ "error": null,
+ "resp": "11 ff 0a 2c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 10,
+ "featureId": "0x8040",
+ "func": 3,
+ "params": [],
+ "error": null,
+ "resp": "11 ff 0a 3c 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 10,
+ "featureId": "0x8040",
+ "func": 4,
+ "params": [],
+ "error": null,
+ "resp": "11 ff 0a 4c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 11,
+ "featureId": "0x8071",
+ "func": 0,
+ "params": [],
+ "error": null,
+ "resp": "11 ff 0b 0c 00 00 00 01 00 05 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 11,
+ "featureId": "0x8071",
+ "func": 0,
+ "params": [
+ 1
+ ],
+ "error": null,
+ "resp": "11 ff 0b 0c 01 00 00 13 00 01 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 11,
+ "featureId": "0x8071",
+ "func": 1,
+ "params": [],
+ "error": null,
+ "resp": "11 ff 0b 1c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 11,
+ "featureId": "0x8071",
+ "func": 1,
+ "params": [
+ 1
+ ],
+ "error": null,
+ "resp": "11 ff 0b 1c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 11,
+ "featureId": "0x8071",
+ "func": 1,
+ "params": [
+ 1,
+ 255,
+ 1,
+ 1
+ ],
+ "error": null,
+ "resp": "11 ff 0b 1c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 11,
+ "featureId": "0x8071",
+ "func": 2,
+ "params": [],
+ "error": null,
+ "resp": "11 ff 0b 2c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 11,
+ "featureId": "0x8071",
+ "func": 5,
+ "params": [],
+ "error": null,
+ "resp": "11 ff 0b 5c 00 03 07 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 11,
+ "featureId": "0x8071",
+ "func": 6,
+ "params": [],
+ "error": null,
+ "resp": "11 ff 0b 6c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 11,
+ "featureId": "0x8071",
+ "func": 7,
+ "params": [],
+ "error": null,
+ "resp": "11 ff 0b 7c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 11,
+ "featureId": "0x8071",
+ "func": 8,
+ "params": [],
+ "error": null,
+ "resp": "11 ff 0b 8c 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 11,
+ "featureId": "0x8071",
+ "func": 9,
+ "params": [],
+ "error": null,
+ "resp": "11 ff 0b 9c 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 12,
+ "featureId": "0x8081",
+ "func": 0,
+ "params": [],
+ "error": null,
+ "resp": "11 ff 0c 0c 00 00 fe 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 12,
+ "featureId": "0x8081",
+ "func": 0,
+ "params": [
+ 1
+ ],
+ "error": null,
+ "resp": "11 ff 0c 0c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 12,
+ "featureId": "0x8081",
+ "func": 0,
+ "params": [
+ 1,
+ 255,
+ 1,
+ 1
+ ],
+ "error": null,
+ "resp": "11 ff 0c 0c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 12,
+ "featureId": "0x8081",
+ "func": 1,
+ "params": [],
+ "error": null,
+ "resp": "11 ff 0c 1c ff ff ff ff 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 12,
+ "featureId": "0x8081",
+ "func": 1,
+ "params": [
+ 1
+ ],
+ "error": null,
+ "resp": "11 ff 0c 1c 01 ff ff ff 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 12,
+ "featureId": "0x8081",
+ "func": 1,
+ "params": [
+ 1,
+ 255,
+ 1,
+ 1
+ ],
+ "error": null,
+ "resp": "11 ff 0c 1c 01 ff ff ff 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 12,
+ "featureId": "0x8081",
+ "func": 6,
+ "params": [],
+ "error": null,
+ "resp": "11 ff 0c 6c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "featureIndex": 12,
+ "featureId": "0x8081",
+ "func": 7,
+ "params": [],
+ "error": null,
+ "resp": "11 ff 0c 7c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/ref/hid-captures/hook-lghub-hid.js b/ref/hid-captures/hook-lghub-hid.js
new file mode 100644
index 0000000..80ce4c3
--- /dev/null
+++ b/ref/hid-captures/hook-lghub-hid.js
@@ -0,0 +1,84 @@
+// Hook G HUB HID writes (IOKit + libc write)
+'use strict';
+
+function hex(buf, len) {
+ if (!buf) return '';
+ const n = Math.min(len || 64, 64);
+ const arr = [];
+ for (let i = 0; i < n; i++) {
+ arr.push(('0' + buf.add(i).readU8().toString(16)).slice(-2));
+ }
+ return arr.join(' ');
+}
+
+function looksLikeHidpp(ptr, len) {
+ if (len < 4) return false;
+ try {
+ const b0 = ptr.readU8();
+ // HID++ long 0x11 or short 0x10, or report id 0x00 then 0x11
+ return b0 === 0x11 || b0 === 0x10 || (b0 === 0x00 && len > 1 && ptr.add(1).readU8() === 0x11);
+ } catch (_) {
+ return false;
+ }
+}
+
+function logWrite(tag, buf, len) {
+ if (!buf || len <= 0) return;
+ // Only log HID++-ish or Litra-sized packets (7/20/21)
+ if (!(looksLikeHidpp(buf, len) || len === 7 || len === 20 || len === 21 || len === 64)) return;
+ const h = hex(buf, Math.min(len, 32));
+ console.log(JSON.stringify({ t: Date.now(), tag, len, hex: h }));
+}
+
+function tryHook(name, addr, hooks) {
+ if (!addr || addr.isNull()) {
+ console.log(JSON.stringify({ t: Date.now(), tag: 'no_symbol', name }));
+ return;
+ }
+ try {
+ Interceptor.attach(addr, hooks);
+ console.log(JSON.stringify({ t: Date.now(), tag: 'hooked', name: name, addr: addr.toString() }));
+ } catch (e) {
+ console.log(JSON.stringify({ t: Date.now(), tag: 'hook_fail', name, err: e.message }));
+ }
+}
+
+const IOKit = Module.findExportByName('IOKit', 'IOHIDDeviceSetReport');
+tryHook('IOHIDDeviceSetReport', IOKit, {
+ onEnter(args) {
+ // IOReturn IOHIDDeviceSetReport(IOHIDDeviceRef, IOHIDReportType, CFIndex reportID, const uint8_t *report, CFIndex len)
+ this.report = args[3];
+ this.len = args[4].toInt32();
+ this.rtype = args[1].toInt32();
+ this.rid = args[2].toInt32();
+ },
+ onLeave(retval) {
+ logWrite('IOHIDDeviceSetReport rtype=' + this.rtype + ' rid=' + this.rid, this.report, this.len);
+ }
+});
+
+const setReportWithCallback = Module.findExportByName('IOKit', 'IOHIDDeviceSetReportWithCallback');
+tryHook('IOHIDDeviceSetReportWithCallback', setReportWithCallback, {
+ onEnter(args) {
+ this.report = args[3];
+ this.len = args[4].toInt32();
+ },
+ onLeave() {
+ logWrite('IOHIDDeviceSetReportWithCallback', this.report, this.len);
+ }
+});
+
+// libc write / writev fallback
+const writePtr = Module.findExportByName(null, 'write');
+tryHook('write', writePtr, {
+ onEnter(args) {
+ this.fd = args[0].toInt32();
+ this.buf = args[1];
+ this.len = args[2].toInt32();
+ },
+ onLeave() {
+ if (this.len >= 4 && this.len <= 64) logWrite('write fd=' + this.fd, this.buf, this.len);
+ }
+});
+
+console.log(JSON.stringify({ t: Date.now(), tag: 'ready', pid: Process.id }));
diff --git a/ref/hid-captures/pulsar-point-2026-07-13T16-46-38-395Z.json b/ref/hid-captures/pulsar-point-2026-07-13T16-46-38-395Z.json
new file mode 100644
index 0000000..860faa2
--- /dev/null
+++ b/ref/hid-captures/pulsar-point-2026-07-13T16-46-38-395Z.json
@@ -0,0 +1,1055 @@
+{
+ "profileHint": "Pulsar Point",
+ "capturedAt": "2026-07-13T16:46:38.395Z",
+ "note": "User confirmed device(s) on Pulsar Point in G HUB / hardware UI",
+ "devices": [
+ {
+ "index": 0,
+ "path": "DevSrvsID:4297198173",
+ "productId": 51459,
+ "probes": [
+ {
+ "name": "back_status_0a_0b",
+ "request": [
+ 17,
+ 255,
+ 10,
+ 11,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0a 0b 00 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 10,
+ 11,
+ 0,
+ 100,
+ 100,
+ 31,
+ 0,
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0a 0b 00 64 64 1f 00 01 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 100,
+ 100,
+ 31,
+ 0,
+ 1,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "back_brightness",
+ "request": [
+ 17,
+ 255,
+ 10,
+ 27,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0a 1b 00 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 10,
+ 27,
+ 0,
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0a 1b 00 01 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "back_on",
+ "request": [
+ 17,
+ 255,
+ 10,
+ 59,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0a 3b 00 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 10,
+ 59,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0a 3b 00 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "fx_0c_0b",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 11,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 0b 00 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 11,
+ 0,
+ 0,
+ 254,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 0b 00 00 fe 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 0,
+ 254,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_1",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 1,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 01 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 1,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 01 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 1,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_2",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 2,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 02 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 2,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 02 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 2,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_3",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 3,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 03 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 3,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 03 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 3,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_4",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 4,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 04 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 4,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 04 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 4,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_5",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 5,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 05 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 5,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 05 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 5,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_6",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 6,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 06 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 6,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 06 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 6,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_7",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 7,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 07 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 7,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 07 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 7,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ }
+ ]
+ },
+ {
+ "index": 1,
+ "path": "DevSrvsID:4297198200",
+ "productId": 51459,
+ "probes": [
+ {
+ "name": "back_status_0a_0b",
+ "request": [
+ 17,
+ 255,
+ 10,
+ 11,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0a 0b 00 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 10,
+ 11,
+ 0,
+ 100,
+ 100,
+ 31,
+ 0,
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0a 0b 00 64 64 1f 00 01 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 100,
+ 100,
+ 31,
+ 0,
+ 1,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "back_brightness",
+ "request": [
+ 17,
+ 255,
+ 10,
+ 27,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0a 1b 00 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 10,
+ 27,
+ 0,
+ 100,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0a 1b 00 64 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 100,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "back_on",
+ "request": [
+ 17,
+ 255,
+ 10,
+ 59,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0a 3b 00 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 10,
+ 59,
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0a 3b 01 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "fx_0c_0b",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 11,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 0b 00 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 11,
+ 0,
+ 0,
+ 254,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 0b 00 00 fe 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 0,
+ 254,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_1",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 1,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 01 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 1,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 01 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 1,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_2",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 2,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 02 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 2,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 02 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 2,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_3",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 3,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 03 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 3,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 03 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 3,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_4",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 4,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 04 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 4,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 04 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 4,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_5",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 5,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 05 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 5,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 05 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 5,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_6",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 6,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 06 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 6,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 06 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 6,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "color_zone_7",
+ "request": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 7,
+ 0,
+ 0,
+ 0
+ ],
+ "requestHex": "11 ff 0c 1b 07 00 00 00",
+ "response": [
+ 17,
+ 255,
+ 12,
+ 27,
+ 7,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "responseHex": "11 ff 0c 1b 07 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 7,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/ref/hid-captures/pulsar-point-8071-2026-07-13T16-53-28-865Z.json b/ref/hid-captures/pulsar-point-8071-2026-07-13T16-53-28-865Z.json
new file mode 100644
index 0000000..c14f12b
--- /dev/null
+++ b/ref/hid-captures/pulsar-point-8071-2026-07-13T16-53-28-865Z.json
@@ -0,0 +1,224 @@
+{
+ "profileHint": "Pulsar Point",
+ "capturedAt": "2026-07-13T16:53:28.865Z",
+ "devices": [
+ {
+ "index": 0,
+ "path": "DevSrvsID:4297198173",
+ "snap": {
+ "label": "Pulsar Point dev0",
+ "clusters": [
+ {
+ "c": 0,
+ "effect_index": 0,
+ "raw": "11 ff 0b 0c 00 00 00 01 03 00 00 00 00 00 00 00 00 00 00 00",
+ "location": 1,
+ "num_effects": 3
+ },
+ {
+ "c": 1,
+ "effect_index": 0,
+ "raw": "11 ff 0b 0c 01 00 00 ff 03 00 00 00 00 00 00 00 00 00 00 00",
+ "location": 255,
+ "num_effects": 3
+ }
+ ],
+ "effects": [
+ {
+ "c": 0,
+ "e": 0,
+ "id": "0x0001",
+ "name": "fixed",
+ "active": true,
+ "raw": "11 ff 0b 0c 00 00 00 01 00 05 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "c": 0,
+ "e": 1,
+ "id": "0x000a",
+ "name": "breathing",
+ "active": false,
+ "raw": "11 ff 0b 0c 00 01 00 0a c0 27 13 88 00 00 00 00 00 00 00 00"
+ },
+ {
+ "c": 0,
+ "e": 2,
+ "id": "0x0015",
+ "name": "unk_15",
+ "active": false,
+ "raw": "11 ff 0b 0c 00 02 00 15 c0 05 13 88 00 00 00 00 00 00 00 00"
+ },
+ {
+ "c": 1,
+ "e": 0,
+ "id": "0x0013",
+ "name": "unk_13",
+ "active": true,
+ "raw": "11 ff 0b 0c 01 00 00 13 00 01 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "c": 1,
+ "e": 1,
+ "id": "0x0016",
+ "name": "unk_16",
+ "active": false,
+ "raw": "11 ff 0b 0c 01 01 00 16 d4 b1 13 88 00 00 00 00 00 00 00 00"
+ },
+ {
+ "c": 1,
+ "e": 2,
+ "id": "0x0018",
+ "name": "unk_18",
+ "active": false,
+ "raw": "11 ff 0b 0c 01 02 00 18 00 15 0b b8 00 00 00 00 00 00 00 00"
+ }
+ ],
+ "bri": "11 ff 0a 1c 00 64 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "on": "11 ff 0a 3c 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "zoneInfo": "11 ff 0c 0c 00 00 fe 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "zones": [
+ {
+ "z": 1,
+ "raw": "11 ff 0c 1c 01 ff ff ff 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "z": 2,
+ "raw": "11 ff 0c 1c 02 ff ff ff 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "z": 3,
+ "raw": "11 ff 0c 1c 03 ff ff ff 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "z": 4,
+ "raw": "11 ff 0c 1c 04 ff ff ff 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "z": 5,
+ "raw": "11 ff 0c 1c 05 ff ff ff 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "z": 6,
+ "raw": "11 ff 0c 1c 06 ff ff ff 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "z": 7,
+ "raw": "11 ff 0c 1c 07 ff ff ff 00 00 00 00 00 00 00 00 00 00 00 00"
+ }
+ ],
+ "status": "11 ff 0a 0c 00 64 64 1f 00 01 00 00 00 00 00 00 00 00 00 00",
+ "control": "11 ff 0b 5c 00 03 07 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "pwr": "11 ff 0b 8c 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ }
+ },
+ {
+ "index": 1,
+ "path": "DevSrvsID:4297198200",
+ "snap": {
+ "label": "Pulsar Point dev1",
+ "clusters": [
+ {
+ "c": 0,
+ "effect_index": 0,
+ "raw": "11 ff 0b 0c 00 00 00 01 03 00 00 00 00 00 00 00 00 00 00 00",
+ "location": 1,
+ "num_effects": 3
+ },
+ {
+ "c": 1,
+ "effect_index": 0,
+ "raw": "11 ff 0b 0c 01 00 00 ff 03 00 00 00 00 00 00 00 00 00 00 00",
+ "location": 255,
+ "num_effects": 3
+ }
+ ],
+ "effects": [
+ {
+ "c": 0,
+ "e": 0,
+ "id": "0x0001",
+ "name": "fixed",
+ "active": true,
+ "raw": "11 ff 0b 0c 00 00 00 01 00 05 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "c": 0,
+ "e": 1,
+ "id": "0x000a",
+ "name": "breathing",
+ "active": false,
+ "raw": "11 ff 0b 0c 00 01 00 0a c0 27 13 88 00 00 00 00 00 00 00 00"
+ },
+ {
+ "c": 0,
+ "e": 2,
+ "id": "0x0015",
+ "name": "unk_15",
+ "active": false,
+ "raw": "11 ff 0b 0c 00 02 00 15 c0 05 13 88 00 00 00 00 00 00 00 00"
+ },
+ {
+ "c": 1,
+ "e": 0,
+ "id": "0x0013",
+ "name": "unk_13",
+ "active": true,
+ "raw": "11 ff 0b 0c 01 00 00 13 00 01 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "c": 1,
+ "e": 1,
+ "id": "0x0016",
+ "name": "unk_16",
+ "active": false,
+ "raw": "11 ff 0b 0c 01 01 00 16 d4 b1 13 88 00 00 00 00 00 00 00 00"
+ },
+ {
+ "c": 1,
+ "e": 2,
+ "id": "0x0018",
+ "name": "unk_18",
+ "active": false,
+ "raw": "11 ff 0b 0c 01 02 00 18 00 15 0b b8 00 00 00 00 00 00 00 00"
+ }
+ ],
+ "bri": "11 ff 0a 1c 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "on": "11 ff 0a 3c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "zoneInfo": "11 ff 0c 0c 00 00 fe 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "zones": [
+ {
+ "z": 1,
+ "raw": "11 ff 0c 1c 01 ff ff ff 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "z": 2,
+ "raw": "11 ff 0c 1c 02 ff ff ff 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "z": 3,
+ "raw": "11 ff 0c 1c 03 ff ff ff 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "z": 4,
+ "raw": "11 ff 0c 1c 04 ff ff ff 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "z": 5,
+ "raw": "11 ff 0c 1c 05 ff ff ff 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "z": 6,
+ "raw": "11 ff 0c 1c 06 ff ff ff 00 00 00 00 00 00 00 00 00 00 00 00"
+ },
+ {
+ "z": 7,
+ "raw": "11 ff 0c 1c 07 ff ff ff 00 00 00 00 00 00 00 00 00 00 00 00"
+ }
+ ],
+ "status": "11 ff 0a 0c 00 64 64 1f 00 01 00 00 00 00 00 00 00 00 00 00",
+ "control": "11 ff 0b 5c 00 03 05 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "pwr": "11 ff 0b 8c 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/ref/hid-captures/rgb-effects-catalog-2026-07-13T16-51-39-170Z.json b/ref/hid-captures/rgb-effects-catalog-2026-07-13T16-51-39-170Z.json
new file mode 100644
index 0000000..39fd06a
--- /dev/null
+++ b/ref/hid-captures/rgb-effects-catalog-2026-07-13T16-51-39-170Z.json
@@ -0,0 +1,177 @@
+{
+ "countRaw": "11 ff 0b 0c ff 00 02 00 02 00 40 00 00 00 00 00 00 00 00 00",
+ "clusters": [
+ {
+ "i": 0,
+ "info": "11 ff 0b 0c 00 00 00 01 03 00 00 00 00 00 00 00 00 00 00 00",
+ "location": 1,
+ "fxCount": 3,
+ "effects": [
+ {
+ "e": 0,
+ "raw": "11 ff 0b 0c 00 00 00 01 00 05 00 00 00 00 00 00 00 00 00 00",
+ "mode": "0x0001",
+ "modeName": "ON/fixed",
+ "speed": 0,
+ "payload": [
+ 0,
+ 0,
+ 0,
+ 1,
+ 0,
+ 5,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "e": 1,
+ "raw": "11 ff 0b 0c 00 01 00 0a c0 27 13 88 00 00 00 00 00 00 00 00",
+ "mode": "0x000a",
+ "modeName": "BREATHING",
+ "speed": 5000,
+ "payload": [
+ 0,
+ 1,
+ 0,
+ 10,
+ 192,
+ 39,
+ 19,
+ 136,
+ 0,
+ 0
+ ]
+ },
+ {
+ "e": 2,
+ "raw": "11 ff 0b 0c 00 02 00 15 c0 05 13 88 00 00 00 00 00 00 00 00",
+ "mode": "0x0015",
+ "modeName": "unknown",
+ "speed": 5000,
+ "payload": [
+ 0,
+ 2,
+ 0,
+ 21,
+ 192,
+ 5,
+ 19,
+ 136,
+ 0,
+ 0
+ ]
+ }
+ ]
+ },
+ {
+ "i": 1,
+ "info": "11 ff 0b 0c 01 00 00 ff 03 00 00 00 00 00 00 00 00 00 00 00",
+ "location": 255,
+ "fxCount": 3,
+ "effects": [
+ {
+ "e": 0,
+ "raw": "11 ff 0b 0c 01 00 00 13 00 01 00 00 00 00 00 00 00 00 00 00",
+ "mode": "0x0013",
+ "modeName": "unknown",
+ "speed": 0,
+ "payload": [
+ 1,
+ 0,
+ 0,
+ 19,
+ 0,
+ 1,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "e": 1,
+ "raw": "11 ff 0b 0c 01 01 00 16 d4 b1 13 88 00 00 00 00 00 00 00 00",
+ "mode": "0x0016",
+ "modeName": "unknown",
+ "speed": 5000,
+ "payload": [
+ 1,
+ 1,
+ 0,
+ 22,
+ 212,
+ 177,
+ 19,
+ 136,
+ 0,
+ 0
+ ]
+ },
+ {
+ "e": 2,
+ "raw": "11 ff 0b 0c 01 02 00 18 00 15 0b b8 00 00 00 00 00 00 00 00",
+ "mode": "0x0018",
+ "modeName": "unknown",
+ "speed": 3000,
+ "payload": [
+ 1,
+ 2,
+ 0,
+ 24,
+ 0,
+ 21,
+ 11,
+ 184,
+ 0,
+ 0
+ ]
+ }
+ ]
+ },
+ {
+ "i": 2,
+ "info": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "location": 0,
+ "fxCount": 0,
+ "effects": []
+ },
+ {
+ "i": 3,
+ "info": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "location": 0,
+ "fxCount": 0,
+ "effects": []
+ },
+ {
+ "i": 4,
+ "info": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "location": 0,
+ "fxCount": 0,
+ "effects": []
+ },
+ {
+ "i": 5,
+ "info": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "location": 0,
+ "fxCount": 0,
+ "effects": []
+ },
+ {
+ "i": 6,
+ "info": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "location": 0,
+ "fxCount": 0,
+ "effects": []
+ },
+ {
+ "i": 7,
+ "info": "11 ff ff 0b 0c 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "location": 0,
+ "fxCount": 0,
+ "effects": []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/ref/hid-captures/write-ff0000-then-read-2026-07-13T16-50-09-840Z.json b/ref/hid-captures/write-ff0000-then-read-2026-07-13T16-50-09-840Z.json
new file mode 100644
index 0000000..8ba1a28
--- /dev/null
+++ b/ref/hid-captures/write-ff0000-then-read-2026-07-13T16-50-09-840Z.json
@@ -0,0 +1,1074 @@
+{
+ "steps": [
+ {
+ "device": 0,
+ "path": "DevSrvsID:4297198200",
+ "before": [
+ {
+ "name": "status_0a_0b",
+ "req": "11 ff 0a 0b",
+ "hex": "11 ff 0a 0b 00 64 64 1f 00 01 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 100,
+ 100,
+ 31,
+ 0,
+ 1,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "bri_0a_1b",
+ "req": "11 ff 0a 1b",
+ "hex": "11 ff 0a 1b 00 64 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 100,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "on_0a_3b",
+ "req": "11 ff 0a 3b",
+ "hex": "11 ff 0a 3b 01 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "fx_0c_0b",
+ "req": "11 ff 0c 0b",
+ "hex": "11 ff 0c 0b 00 00 fe 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 0,
+ 254,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "mode_0b_0b",
+ "req": "11 ff 0b 0b",
+ "hex": "11 ff 0b 0b 00 00 00 01 00 05 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 0,
+ 0,
+ 1,
+ 0,
+ 5,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "mode_0b_5b",
+ "req": "11 ff 0b 5b",
+ "hex": "11 ff 0b 5b 00 03 07 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 3,
+ 7,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "zone_1",
+ "req": "11 ff 0c 1b 01",
+ "hex": "11 ff 0c 1b 01 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 1,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "zone_2",
+ "req": "11 ff 0c 1b 02",
+ "hex": "11 ff 0c 1b 02 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 2,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "zone_3",
+ "req": "11 ff 0c 1b 03",
+ "hex": "11 ff 0c 1b 03 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 3,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "zone_4",
+ "req": "11 ff 0c 1b 04",
+ "hex": "11 ff 0c 1b 04 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 4,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "zone_5",
+ "req": "11 ff 0c 1b 05",
+ "hex": "11 ff 0c 1b 05 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 5,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "zone_6",
+ "req": "11 ff 0c 1b 06",
+ "hex": "11 ff 0c 1b 06 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 6,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "zone_7",
+ "req": "11 ff 0c 1b 07",
+ "hex": "11 ff 0c 1b 07 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 7,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ }
+ ],
+ "after": [
+ {
+ "name": "status_0a_0b",
+ "req": "11 ff 0a 0b",
+ "hex": "11 ff 0a 0b 00 64 64 1f 00 01 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 100,
+ 100,
+ 31,
+ 0,
+ 1,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "bri_0a_1b",
+ "req": "11 ff 0a 1b",
+ "hex": "11 ff 0a 1b 00 64 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 100,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "on_0a_3b",
+ "req": "11 ff 0a 3b",
+ "hex": "11 ff 0a 3b 01 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "fx_0c_0b",
+ "req": "11 ff 0c 0b",
+ "hex": "11 ff 0c 0b 00 00 fe 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 0,
+ 254,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "mode_0b_0b",
+ "req": "11 ff 0b 0b",
+ "hex": "11 ff 0b 0b 00 00 00 01 00 05 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 0,
+ 0,
+ 1,
+ 0,
+ 5,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "mode_0b_5b",
+ "req": "11 ff 0b 5b",
+ "hex": "11 ff 0b 5b 00 03 07 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 3,
+ 7,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "zone_1",
+ "req": "11 ff 0c 1b 01",
+ "hex": "11 ff 0c 1b 01 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 1,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "zone_2",
+ "req": "11 ff 0c 1b 02",
+ "hex": "11 ff 0c 1b 02 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 2,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "zone_3",
+ "req": "11 ff 0c 1b 03",
+ "hex": "11 ff 0c 1b 03 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 3,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "zone_4",
+ "req": "11 ff 0c 1b 04",
+ "hex": "11 ff 0c 1b 04 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 4,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "zone_5",
+ "req": "11 ff 0c 1b 05",
+ "hex": "11 ff 0c 1b 05 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 5,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "zone_6",
+ "req": "11 ff 0c 1b 06",
+ "hex": "11 ff 0c 1b 06 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 6,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "zone_7",
+ "req": "11 ff 0c 1b 07",
+ "hex": "11 ff 0c 1b 07 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 7,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ }
+ ],
+ "extras": [
+ {
+ "name": "get_0c_b",
+ "req": "11 ff 0c 0b",
+ "hex": "11 ff 0c 0b 00 00 fe 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 0,
+ 254,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "get_0c_1b",
+ "req": "11 ff 0c 1b",
+ "hex": "11 ff 0c 1b ff ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 255,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "get_0c_2b",
+ "req": "11 ff 0c 2b",
+ "hex": "11 ff ff 0c 2b 02 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 43,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "get_0c_3b",
+ "req": "11 ff 0c 3b",
+ "hex": "11 ff ff 0c 3b 02 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 59,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "get_0c_4b",
+ "req": "11 ff 0c 4b",
+ "hex": "11 ff ff 0c 4b 02 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 75,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "get_0c_5b",
+ "req": "11 ff 0c 5b",
+ "hex": "11 ff ff 0c 5b 02 ff 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 91,
+ 2,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "get_0c_6b",
+ "req": "11 ff 0c 6b",
+ "hex": "11 ff 0c 6b 00 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "get_0c_7b",
+ "req": "11 ff 0c 7b",
+ "hex": "11 ff 0c 7b 00 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "get_0c_8b",
+ "req": "11 ff 0c 8b",
+ "hex": "11 ff ff 0c 8b 07 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 139,
+ 7,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ }
+ ]
+ },
+ {
+ "device": 1,
+ "path": "DevSrvsID:4297198173",
+ "before": [
+ {
+ "name": "status_0a_0b",
+ "req": "11 ff 0a 0b",
+ "hex": "11 ff 0a 0b 00 64 64 1f 00 01 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 100,
+ 100,
+ 31,
+ 0,
+ 1,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "bri_0a_1b",
+ "req": "11 ff 0a 1b",
+ "hex": "11 ff 0a 1b 00 64 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 100,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "on_0a_3b",
+ "req": "11 ff 0a 3b",
+ "hex": "11 ff 0a 3b 01 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "fx_0c_0b",
+ "req": "11 ff 0c 0b",
+ "hex": "11 ff 0c 0b 00 00 fe 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 0,
+ 254,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "mode_0b_0b",
+ "req": "11 ff 0b 0b",
+ "hex": "11 ff 0b 0b 00 00 00 01 00 05 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 0,
+ 0,
+ 1,
+ 0,
+ 5,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "mode_0b_5b",
+ "req": "11 ff 0b 5b",
+ "hex": "11 ff 0b 5b 00 03 07 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 3,
+ 7,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "zone_1",
+ "req": "11 ff 0c 1b 01",
+ "hex": "11 ff 0c 1b 01 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 1,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "zone_2",
+ "req": "11 ff 0c 1b 02",
+ "hex": "11 ff 0c 1b 02 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 2,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "zone_3",
+ "req": "11 ff 0c 1b 03",
+ "hex": "11 ff 0c 1b 03 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 3,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "zone_4",
+ "req": "11 ff 0c 1b 04",
+ "hex": "11 ff 0c 1b 04 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 4,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "zone_5",
+ "req": "11 ff 0c 1b 05",
+ "hex": "11 ff 0c 1b 05 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 5,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "zone_6",
+ "req": "11 ff 0c 1b 06",
+ "hex": "11 ff 0c 1b 06 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 6,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "zone_7",
+ "req": "11 ff 0c 1b 07",
+ "hex": "11 ff 0c 1b 07 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 7,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ }
+ ],
+ "after": [
+ {
+ "name": "status_0a_0b",
+ "req": "11 ff 0a 0b",
+ "hex": "11 ff 0a 0b 00 64 64 1f 00 01 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 100,
+ 100,
+ 31,
+ 0,
+ 1,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "bri_0a_1b",
+ "req": "11 ff 0a 1b",
+ "hex": "11 ff 0a 1b 00 64 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 100,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "on_0a_3b",
+ "req": "11 ff 0a 3b",
+ "hex": "11 ff 0a 3b 01 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "fx_0c_0b",
+ "req": "11 ff 0c 0b",
+ "hex": "11 ff 0c 0b 00 00 fe 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 0,
+ 254,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "mode_0b_0b",
+ "req": "11 ff 0b 0b",
+ "hex": "11 ff 0b 0b 00 00 00 01 00 05 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 0,
+ 0,
+ 1,
+ 0,
+ 5,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "mode_0b_5b",
+ "req": "11 ff 0b 5b",
+ "hex": "11 ff 0b 5b 00 03 07 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 3,
+ 7,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "zone_1",
+ "req": "11 ff 0c 1b 01",
+ "hex": "11 ff 0c 1b 01 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 1,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "zone_2",
+ "req": "11 ff 0c 1b 02",
+ "hex": "11 ff 0c 1b 02 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 2,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "zone_3",
+ "req": "11 ff 0c 1b 03",
+ "hex": "11 ff 0c 1b 03 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 3,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "zone_4",
+ "req": "11 ff 0c 1b 04",
+ "hex": "11 ff 0c 1b 04 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 4,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "zone_5",
+ "req": "11 ff 0c 1b 05",
+ "hex": "11 ff 0c 1b 05 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 5,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "zone_6",
+ "req": "11 ff 0c 1b 06",
+ "hex": "11 ff 0c 1b 06 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 6,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "zone_7",
+ "req": "11 ff 0c 1b 07",
+ "hex": "11 ff 0c 1b 07 ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 7,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ }
+ ],
+ "extras": [
+ {
+ "name": "get_0c_b",
+ "req": "11 ff 0c 0b",
+ "hex": "11 ff 0c 0b 00 00 fe 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 0,
+ 254,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "get_0c_1b",
+ "req": "11 ff 0c 1b",
+ "hex": "11 ff 0c 1b ff ff ff ff 00 00 00 00 00 00 00 00",
+ "payload": [
+ 255,
+ 255,
+ 255,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "get_0c_2b",
+ "req": "11 ff 0c 2b",
+ "hex": "11 ff ff 0c 2b 02 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 43,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "get_0c_3b",
+ "req": "11 ff 0c 3b",
+ "hex": "11 ff ff 0c 3b 02 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 59,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "get_0c_4b",
+ "req": "11 ff 0c 4b",
+ "hex": "11 ff ff 0c 4b 02 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 75,
+ 2,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "get_0c_5b",
+ "req": "11 ff 0c 5b",
+ "hex": "11 ff ff 0c 5b 02 ff 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 91,
+ 2,
+ 255,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "get_0c_6b",
+ "req": "11 ff 0c 6b",
+ "hex": "11 ff 0c 6b 00 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "get_0c_7b",
+ "req": "11 ff 0c 7b",
+ "hex": "11 ff 0c 7b 00 00 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "get_0c_8b",
+ "req": "11 ff 0c 8b",
+ "hex": "11 ff ff 0c 8b 07 00 00 00 00 00 00 00 00 00 00",
+ "payload": [
+ 139,
+ 7,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/ref/scope-001.md b/ref/scope-001.md
new file mode 100644
index 0000000..43949c0
--- /dev/null
+++ b/ref/scope-001.md
@@ -0,0 +1,79 @@
+# Stream Deck plugin for Logitech Litra lights (v1 scope)
+
+## Context
+
+Goal: control a Logitech Litra Beam LX from Stream Deck. We evaluated two upstream libraries (both cloned read-only into `ref/`, git-ignored):
+- `ref/litra-rs` — actively maintained Rust CLI/crate/MCP server, but every integration path (shell-out, persistent MCP-over-stdio process, or a full native Rust WebSocket plugin) adds either jank or ownership of glue/build infra.
+- `ref/litra` — plain TypeScript library on `node-hid` (`findDevice()`, `turnOn()`, `setBrightnessPercentage()`, `setTemperatureInKelvin()`, etc. in `ref/litra/src/driver.ts`). Stale (no commits since the Dec 2025 release) but the HID protocol for hardware you already own won't change under you, and it drops straight into the Node.js Stream Deck SDK with zero bridging.
+
+Decision: use `litra` (JS) as an npm dependency, in-process, no subprocess/IPC at all. Back-light (RGB) control on the Beam LX is explicitly **out of scope for v1** — the JS library never implemented it (only litra-rs has it); revisit later, either by patching it into the JS lib or bolting on a separate call path just for that feature.
+
+Target hardware confirmed: Litra Beam LX; both a standard Stream Deck (keys only) and a Stream Deck + (dials/touch strip) — so actions need to work as both Keypad and Encoder controllers.
+
+## Project layout
+
+New plugin project at repo root (sibling to `ref/`): `com.ssheppdev.litra.sdPlugin` source tree, scaffolded via `streamdeck create` (per `ref/STREAMDECK_SDK_DOCS.md` Getting Started section) then hand-edited. Add `litra` (from npm, the published package backing `ref/litra`) as a runtime dependency — do not vendor/copy code out of `ref/litra`, just `npm install litra`.
+
+## Core module: `src/litra-manager.ts`
+
+Wraps the `litra` library so action classes never call `findDevices()`/HID functions directly. **v1 is "all or nothing": every action applies to every connected Litra device — no per-device selection UI, and no cached device list.**
+
+Deliberately **stateless**, matching how the `litra-rs` CLI behaves (`litra on`/`litra off` just re-scan and act on whatever's connected, every invocation — no persisted device state to go stale):
+- `forEachDevice(fn: (device: Device) => void)`: calls `litra`'s `findDevices()` fresh on every invocation (cheap in-process HID enumeration, not a subprocess spawn — nothing like the cost we were worried about with `litra-rs`'s CLI), then for each device: `try { fn(device) } catch { log and continue } finally { close the device's HID handle }`.
+- No cache, no invalidation, no retry-on-failure logic needed — a device that's unplugged simply won't appear in the next `findDevices()` call, and a freshly replugged one will, with zero extra code.
+- The one place we deliberately diverge from just copying CLI behavior: the CLI is a short-lived process, so the OS reclaims its opened HID handles on exit automatically. Our plugin process stays alive indefinitely, so `forEachDevice` explicitly closes each device's HID handle after use (`device.hid.close()`) to avoid leaking file descriptors across a long-running session. Note: `litra`'s `Device.hid` type only declares `write`/`readSync`, not `close` — at runtime it's a real `node-hid` `HID.HID` instance that does have `.close()`, so this needs a small local type cast/augmentation in `litra-manager.ts` rather than being callable as-typed.
+- All broadcast operations (`turnOnAll()`, `setBrightnessAllPercentage()`, `setTemperatureAllInKelvin()`, `toggleAll()`, ...) are thin wrappers around `forEachDevice` calling the matching `litra` function.
+- All device calls funnel through this module so logging lives in one place, using `streamDeck.logger` (per `ref/STREAMDECK_SDK_DOCS.md` Logging guide) — never `console`.
+- Exposes a device-shaped API, not a re-export of the `litra` package's own exports — action classes only ever import from `litra-manager`, never from `litra` directly.
+
+### Why this matters for a later pivot to owning the JS library
+
+This module is the single seam between "the `litra` npm package" and "our plugin." If we later decide to fork/vendor `litra` ourselves (e.g. to add Beam LX back-light support, per the deferred item below), the change is contained entirely to `litra-manager.ts`'s import (swap `import { turnOn } from "litra"` for a local vendored path) plus whatever new methods we add (e.g. `setBackColor`) — no action class, manifest entry, or PI changes are needed for the pivot itself. Adding the back-light *feature* afterward is then just a new action following the same pattern as Brightness/Temperature. Starting with the npm package today doesn't lock us in; it just means day one has zero fork-maintenance burden, and we take on exactly as much ownership as we need, exactly when we need it.
+
+## Actions
+
+All three actions declare `Controllers: ["Keypad", "Encoder"]` where noted, so the *same* action can be dropped on a key (standard Stream Deck) or a dial slot (Stream Deck +) — matches "key sets a static value, dial changes it continuously."
+
+All action UUIDs are prefixed by the plugin UUID `com.ssheppdev.litra`.
+
+1. **Power** (`.power`) — Keypad only.
+ - Settings: `{ mode: "toggle" | "on" | "off" }` (PI dropdown, default `toggle`).
+ - `onKeyDown`: call the broadcast `toggleAll()`/`turnOnAll()`/`turnOffAll()` per mode via litra-manager — applies to every connected device.
+ - `onWillAppear`: call `isOn()` across all devices and `setState()` so the key reflects hardware state; with multiple devices, show "on" only if *all* are on **and at least one device is connected** — `devices.length > 0 && devices.every(isOn)`. (Plain `.every()` on an empty array is vacuously `true` in JS, which would show "On" with nothing plugged in — the explicit length check is required, not optional.)
+ - Two manifest `States` (Off/On) with distinct icons.
+
+2. **Brightness** (`.brightness`) — Keypad + Encoder (separate action from Temperature; same UUID usable as a key *or* a dial).
+ - Settings: `{ presetPercentage: number, stepPercentage: number }` (defaults e.g. 100, 5).
+ - `onKeyDown` (key placement): `setBrightnessAllPercentage(presetPercentage)` — the "static" behavior, applied to every device.
+ - `onDialRotate` (dial placement): work entirely in **percent** space to match litra’s write API. Per device, in one `forEachDevice` pass: read lumen via a manager helper that parses `data[4]*256+data[5]` (do **not** use stock `getBrightnessInLumen` — it only returns `data[5]` and breaks Beam LX above 255 lm), convert to % with the inverse of litra’s `percentageWithinRange` using that device’s min/max lumen helpers, compute `clamp(currentPct + ticks * stepPercentage, 0, 100)`, then `setBrightnessPercentage`. Debounce ticks (~50–80ms) into one batch. No cached "last known" brightness.
+ - `onWillAppear`/after each change: `setFeedback()` (built-in `$A1`) with representative **%** from the first device; `setTitle()` for the key-only case.
+
+3. **Color Temperature** (`.temperature`) — Keypad + Encoder (separate action from Brightness; same UUID usable as a key *or* a dial).
+ - Settings: `{ presetKelvin: number, stepKelvin: number }` (defaults e.g. 4000, 100 — must stay a multiple of 100 per `getAllowedTemperaturesInKelvinForDevice`).
+ - `onKeyDown`: `setTemperatureAllInKelvin(presetKelvin)`, applied to every device.
+ - `onDialRotate`: read `getTemperatureInKelvin()` per device fresh, compute `current + ticks * stepKelvin`, then **snap to the nearest value in** `getAllowedTemperaturesInKelvinForDevice` (min/max clamp alone is insufficient — litra throws if the value is not on the allow-list), then `setTemperatureInKelvin`. Same debounce pattern as Brightness.
+ - Same `setFeedback()`/`setTitle()` pattern as Brightness, displaying Kelvin.
+
+Property inspectors: simple `sdpi-components` forms (number/slider fields for presets/steps, dropdown for power mode) per `ref/STREAMDECK_SDK_DOCS.md` Property Inspectors guide.
+
+## Error handling
+
+`forEachDevice` iterates all devices independently: one device's HID write throwing is caught, logged via `streamDeck.logger.error`, and does not stop the loop from reaching the remaining devices. `forEachDevice` treats **zero devices found** as a failure too, not a silent no-op — a key press that can't reach any hardware must still surface `action.showAlert()`, otherwise pressing a button with nothing plugged in gives no feedback at all. If *any* device in the broadcast failed (including "none found"), the action calls `action.showAlert()` once to surface it. No polling/interval-based reconnect and no retry logic needed at all — statelessness means "reconnect" isn't a special case, the next action press just sees the current device list.
+
+## Out of scope for v1 (explicitly deferred)
+
+- Beam LX RGB back light control (needs litra-rs; JS lib doesn't support it).
+- Per-device selection/targeting UI — v1 is intentionally all-or-nothing across every connected Litra device; a picker can be added later without changing `litra-manager`'s broadcast methods (it would just filter which devices they loop over).
+- Any use of `litra-rs`, its CLI, or its MCP server.
+
+### Back-light research pause (2026-07-13)
+
+Exploratory HID++ work for Beam LX back RGB / G HUB presets is **paused**. Findings, feature map, capture index, macOS sniff blockers (Hardened Runtime + `com.logi.ghub.hidfilter` + SIP), and a recommended v2 SET-only slice live in **`ref/hid-captures/NOTES.md`**. Do not assume named presets (Pulsar Point, Color Wave) are readable from the device while G HUB is running.
+
+## Verification
+
+1. `npm run build` in the plugin project, `streamdeck restart ` (or `npm run watch` during development, per `ref/STREAMDECK_SDK_DOCS.md` Getting Started/Your First Changes).
+2. Manually assign each action to a key on the standard Stream Deck and to a dial slot on the Stream Deck +.
+3. Confirm: key press toggles power (all devices) and reflects true aggregate state on `onWillAppear`; key press on Brightness/Temperature snaps every connected device to the preset; dial rotation smoothly adjusts brightness/temperature on all devices with the touch display updating; unplugging and replugging a device, then triggering an action, "just works" on the next press with no plugin restart (since `forEachDevice` re-scans every time). If only one Litra device is available for testing, confirm the broadcast path still behaves correctly for a single-item array (no special-casing needed).
+4. With nothing plugged in, confirm the Power key shows **Off** (not On) and pressing any action shows `showAlert()` rather than doing nothing silently.
+5. Feel-test dial rotation specifically for responsiveness (spin it fast) — the open/read/write/close-per-device-per-debounced-batch design should feel smooth, but this is the one spot the stateless approach could show latency under rapid input; if it feels laggy, that's the signal to revisit (e.g. widen the debounce window or reconsider a short-lived per-gesture handle) rather than something to pre-optimize now.
diff --git a/rollup.config.mjs b/rollup.config.mjs
new file mode 100644
index 0000000..90c8499
--- /dev/null
+++ b/rollup.config.mjs
@@ -0,0 +1,81 @@
+import commonjs from "@rollup/plugin-commonjs";
+import nodeResolve from "@rollup/plugin-node-resolve";
+import terser from "@rollup/plugin-terser";
+import typescript from "@rollup/plugin-typescript";
+import { cpSync, existsSync, mkdirSync, rmSync } from "node:fs";
+import path from "node:path";
+import url from "node:url";
+
+const isWatching = !!process.env.ROLLUP_WATCH;
+const sdPlugin = "com.ssheppdev.litra.sdPlugin";
+const pluginNodeModules = path.join(sdPlugin, "node_modules");
+
+/**
+ * node-hid ships platform .node binaries that must not be bundled.
+ * Copy the installed package (and its deps) into the plugin folder so
+ * runtime `createRequire` / Node resolution can load them next to plugin.js.
+ */
+function copyNativeRuntime() {
+ return {
+ name: "copy-native-runtime",
+ writeBundle() {
+ const packages = ["node-hid", "node-addon-api", "bindings"];
+ for (const name of packages) {
+ const from = path.join("node_modules", name);
+ const to = path.join(pluginNodeModules, name);
+ if (!existsSync(from)) {
+ continue;
+ }
+ rmSync(to, { recursive: true, force: true });
+ mkdirSync(path.dirname(to), { recursive: true });
+ cpSync(from, to, { recursive: true });
+ }
+ },
+ };
+}
+
+/**
+ * @type {import('rollup').RollupOptions}
+ */
+const config = {
+ input: "src/plugin.ts",
+ output: {
+ file: `${sdPlugin}/bin/plugin.js`,
+ sourcemap: isWatching,
+ sourcemapPathTransform: (relativeSourcePath, sourcemapPath) => {
+ return url.pathToFileURL(path.resolve(path.dirname(sourcemapPath), relativeSourcePath)).href;
+ },
+ },
+ external: ["node-hid"],
+ plugins: [
+ {
+ name: "watch-externals",
+ buildStart() {
+ this.addWatchFile(`${sdPlugin}/manifest.json`);
+ },
+ },
+ typescript({
+ mapRoot: isWatching ? "./" : undefined,
+ }),
+ nodeResolve({
+ browser: false,
+ exportConditions: ["node"],
+ preferBuiltins: true,
+ }),
+ commonjs(),
+ !isWatching && terser(),
+ {
+ name: "emit-module-package-file",
+ generateBundle() {
+ this.emitFile({
+ fileName: "package.json",
+ source: `{ "type": "module" }`,
+ type: "asset",
+ });
+ },
+ },
+ copyNativeRuntime(),
+ ],
+};
+
+export default config;
diff --git a/scripts/generate-icons.mjs b/scripts/generate-icons.mjs
new file mode 100644
index 0000000..f15b3e0
--- /dev/null
+++ b/scripts/generate-icons.mjs
@@ -0,0 +1,215 @@
+#!/usr/bin/env node
+/**
+ * Generates Litra plugin icons (SVG masters + PNG sizes required by Stream Deck).
+ */
+import { mkdirSync, writeFileSync } from "node:fs";
+import path from "node:path";
+import sharp from "sharp";
+
+const root = path.resolve("com.ssheppdev.litra.sdPlugin/imgs");
+
+const COLORS = {
+ bg: "#1A1A1A",
+ fg: "#FFFFFF",
+ muted: "#6B6B6B",
+ warm: "#FFB347",
+ cool: "#7EC8FF",
+ on: "#F5C542",
+ accent: "#E8E8E8",
+};
+
+function svgDoc(size, body) {
+ return `
+
+${body}
+ `;
+}
+
+/** Key / encoder tile background */
+function keyBg(size = 72) {
+ const r = Math.round(size * 0.18);
+ return ` `;
+}
+
+function powerGlyph(size, { on }) {
+ const cx = size / 2;
+ const cy = size / 2;
+ const stroke = on ? COLORS.on : COLORS.muted;
+ const sw = size * 0.07;
+ const r = size * 0.22;
+ const stemH = size * 0.2;
+ return `
+ ${keyBg(size)}
+
+
+ ${on ? ` ` : ""}`;
+}
+
+function brightnessGlyph(size) {
+ const cx = size / 2;
+ const cy = size / 2 - size * 0.02;
+ const core = size * 0.12;
+ const rayIn = size * 0.2;
+ const rayOut = size * 0.3;
+ const sw = size * 0.055;
+ const rays = Array.from({ length: 8 }, (_, i) => {
+ const a = (i * Math.PI) / 4 - Math.PI / 2;
+ const x1 = cx + Math.cos(a) * rayIn;
+ const y1 = cy + Math.sin(a) * rayIn;
+ const x2 = cx + Math.cos(a) * rayOut;
+ const y2 = cy + Math.sin(a) * rayOut;
+ return ` `;
+ }).join("\n");
+ return `
+ ${keyBg(size)}
+
+ ${rays}`;
+}
+
+function temperatureGlyph(size) {
+ const cx = size / 2;
+ const cy = size / 2;
+ const sw = size * 0.055;
+ // Soft warm→cool bar + thermometer
+ const barW = size * 0.42;
+ const barH = size * 0.1;
+ const barX = cx - barW / 2;
+ const barY = cy + size * 0.22;
+ const bulbR = size * 0.09;
+ const tubeW = size * 0.08;
+ const tubeTop = cy - size * 0.28;
+ const tubeBottom = cy + size * 0.08;
+ return `
+ ${keyBg(size)}
+
+
+
+
+
+
+
+
+
+ `;
+}
+
+function lampMark(size, { mono = false } = {}) {
+ const cx = size / 2;
+ const cy = size / 2;
+ const stroke = mono ? "#FFFFFF" : COLORS.warm;
+ const fill = mono ? "none" : COLORS.warm;
+ const sw = Math.max(1.5, size * 0.08);
+ const headR = size * 0.22;
+ const stemH = size * 0.18;
+ return `
+
+
+ `;
+}
+
+function marketplaceGlyph(size) {
+ return `
+
+ ${lampMark(size)}`;
+}
+
+function categoryGlyph(size) {
+ // Monochrome white on transparent for category / action list
+ return lampMark(size, { mono: true });
+}
+
+function actionIconPower(size) {
+ const cx = size / 2;
+ const cy = size / 2;
+ const sw = Math.max(1.5, size * 0.12);
+ const r = size * 0.28;
+ return `
+
+ `;
+}
+
+function actionIconBrightness(size) {
+ const cx = size / 2;
+ const cy = size / 2;
+ const core = size * 0.16;
+ const rayIn = size * 0.28;
+ const rayOut = size * 0.42;
+ const sw = Math.max(1.5, size * 0.1);
+ const rays = Array.from({ length: 8 }, (_, i) => {
+ const a = (i * Math.PI) / 4 - Math.PI / 2;
+ return ` `;
+ }).join("\n");
+ return ` \n${rays}`;
+}
+
+function actionIconTemperature(size) {
+ const cx = size / 2;
+ const sw = Math.max(1.5, size * 0.1);
+ const bulbR = size * 0.16;
+ const tubeW = size * 0.14;
+ const top = size * 0.18;
+ const bottom = size * 0.58;
+ return `
+
+
+ `;
+}
+
+async function writePng(filePath, svg, size) {
+ mkdirSync(path.dirname(filePath), { recursive: true });
+ await sharp(Buffer.from(svgDoc(size, svg)))
+ .png()
+ .toFile(filePath);
+ console.log("wrote", filePath);
+}
+
+async function writePair(basePathNoExt, svgAt1x, size1x) {
+ await writePng(`${basePathNoExt}.png`, svgAt1x(size1x), size1x);
+ await writePng(`${basePathNoExt}@2x.png`, svgAt1x(size1x * 2), size1x * 2);
+}
+
+async function main() {
+ // Key tiles (72 / 144)
+ await writePair(path.join(root, "actions/power/key-off"), (s) => powerGlyph(s, { on: false }), 72);
+ await writePair(path.join(root, "actions/power/key-on"), (s) => powerGlyph(s, { on: true }), 72);
+ await writePair(path.join(root, "actions/brightness/key"), (s) => brightnessGlyph(s), 72);
+ await writePair(path.join(root, "actions/temperature/key"), (s) => temperatureGlyph(s), 72);
+
+ // Encoder icons (same art)
+ await writePair(path.join(root, "actions/brightness/encoder"), (s) => brightnessGlyph(s), 72);
+ await writePair(path.join(root, "actions/temperature/encoder"), (s) => temperatureGlyph(s), 72);
+
+ // Action list icons (20 / 40) — monochrome white
+ await writePair(path.join(root, "actions/power/icon"), (s) => actionIconPower(s), 20);
+ await writePair(path.join(root, "actions/brightness/icon"), (s) => actionIconBrightness(s), 20);
+ await writePair(path.join(root, "actions/temperature/icon"), (s) => actionIconTemperature(s), 20);
+
+ // Category (28 / 56)
+ await writePair(path.join(root, "plugin/category-icon"), (s) => categoryGlyph(s), 28);
+
+ // Marketplace (256 / 512)
+ await writePair(path.join(root, "plugin/marketplace"), (s) => marketplaceGlyph(s), 256);
+
+ console.log("Icons generated.");
+}
+
+main().catch((err) => {
+ console.error(err);
+ process.exit(1);
+});
diff --git a/src/actions/brightness.ts b/src/actions/brightness.ts
new file mode 100644
index 0000000..942982e
--- /dev/null
+++ b/src/actions/brightness.ts
@@ -0,0 +1,108 @@
+import {
+ action,
+ DialDownEvent,
+ DialRotateEvent,
+ DidReceiveSettingsEvent,
+ KeyDownEvent,
+ SingletonAction,
+ WillAppearEvent,
+} from "@elgato/streamdeck";
+
+import {
+ adjustBrightnessAllByPercentageSteps,
+ createDialDebouncer,
+ readRepresentativeBrightnessPercentage,
+ setBrightnessAllPercentage,
+ toggleAll,
+} from "../litra-manager";
+
+type BrightnessSettings = {
+ presetPercentage?: number;
+ stepPercentage?: number;
+};
+
+type ActionRef = WillAppearEvent["action"];
+
+@action({ UUID: "com.ssheppdev.litra.brightness" })
+export class BrightnessAction extends SingletonAction {
+ private readonly dialQueues = new Map void>();
+ private readonly latestSettings = new Map();
+
+ override async onWillAppear(ev: WillAppearEvent): Promise {
+ this.latestSettings.set(ev.action.id, ev.payload.settings);
+ await this.refreshDisplay(ev.action, ev.payload.settings);
+ }
+
+ override async onDidReceiveSettings(ev: DidReceiveSettingsEvent): Promise {
+ this.latestSettings.set(ev.action.id, ev.payload.settings);
+ await this.refreshDisplay(ev.action, ev.payload.settings);
+ }
+
+ override async onKeyDown(ev: KeyDownEvent): Promise {
+ const preset = Number(ev.payload.settings.presetPercentage ?? 100);
+ const result = setBrightnessAllPercentage(preset);
+ if (!result.ok) {
+ await ev.action.showAlert();
+ return;
+ }
+ // Key tiles represent the configured set-amount, not live hardware %.
+ await this.showKeyAmount(ev.action, preset);
+ }
+
+ /** Dial press toggles power; rotate adjusts brightness. */
+ override async onDialDown(ev: DialDownEvent): Promise {
+ const result = toggleAll();
+ if (!result.ok) {
+ await ev.action.showAlert();
+ return;
+ }
+ await this.refreshDisplay(ev.action, ev.payload.settings);
+ }
+
+ override async onDialRotate(ev: DialRotateEvent): Promise {
+ const id = ev.action.id;
+ this.latestSettings.set(id, ev.payload.settings);
+ let queue = this.dialQueues.get(id);
+ if (!queue) {
+ queue = createDialDebouncer(async (ticks) => {
+ const settings = this.latestSettings.get(id) ?? {};
+ const step = Number(settings.stepPercentage ?? 5);
+ const result = adjustBrightnessAllByPercentageSteps(ticks, step);
+ if (!result.ok) {
+ await ev.action.showAlert();
+ return;
+ }
+ await this.showDialValue(ev.action, result.representativeBrightnessPercentage);
+ });
+ this.dialQueues.set(id, queue);
+ }
+ queue(ev.payload.ticks);
+ }
+
+ private async refreshDisplay(action: ActionRef, settings: BrightnessSettings): Promise {
+ if (action.isKey()) {
+ await this.showKeyAmount(action, Number(settings.presetPercentage ?? 100));
+ return;
+ }
+
+ const result = readRepresentativeBrightnessPercentage();
+ await this.showDialValue(
+ action,
+ result.ok ? result.representativeBrightnessPercentage : undefined,
+ );
+ }
+
+ private async showKeyAmount(action: ActionRef, percentage: number): Promise {
+ await action.setTitle(`${Math.round(percentage)}`);
+ }
+
+ private async showDialValue(action: ActionRef, percentage: number | undefined): Promise {
+ const label = percentage === undefined ? "—" : `${Math.round(percentage)}%`;
+ if (action.isDial()) {
+ await action.setFeedback({
+ title: "Brightness",
+ value: label,
+ });
+ }
+ }
+}
diff --git a/src/actions/power.ts b/src/actions/power.ts
new file mode 100644
index 0000000..932205e
--- /dev/null
+++ b/src/actions/power.ts
@@ -0,0 +1,39 @@
+import {
+ action,
+ KeyDownEvent,
+ SingletonAction,
+ WillAppearEvent,
+} from "@elgato/streamdeck";
+
+import { isOnAll, toggleAll, turnOffAll, turnOnAll } from "../litra-manager";
+
+type PowerSettings = {
+ mode?: "toggle" | "on" | "off";
+};
+
+@action({ UUID: "com.ssheppdev.litra.power" })
+export class PowerAction extends SingletonAction {
+ override async onWillAppear(ev: WillAppearEvent): Promise {
+ await this.syncState(ev.action);
+ }
+
+ override async onKeyDown(ev: KeyDownEvent): Promise {
+ const mode = ev.payload.settings.mode ?? "toggle";
+ const result =
+ mode === "on" ? turnOnAll() : mode === "off" ? turnOffAll() : toggleAll();
+
+ if (!result.ok) {
+ await ev.action.showAlert();
+ }
+
+ await this.syncState(ev.action);
+ }
+
+ private async syncState(action: WillAppearEvent["action"]): Promise {
+ if (!action.isKey()) {
+ return;
+ }
+ const { allOn, deviceCount } = isOnAll();
+ await action.setState(deviceCount > 0 && allOn ? 1 : 0);
+ }
+}
diff --git a/src/actions/temperature.ts b/src/actions/temperature.ts
new file mode 100644
index 0000000..aed16d3
--- /dev/null
+++ b/src/actions/temperature.ts
@@ -0,0 +1,108 @@
+import {
+ action,
+ DialDownEvent,
+ DialRotateEvent,
+ DidReceiveSettingsEvent,
+ KeyDownEvent,
+ SingletonAction,
+ WillAppearEvent,
+} from "@elgato/streamdeck";
+
+import {
+ adjustTemperatureAllByKelvinSteps,
+ createDialDebouncer,
+ readRepresentativeTemperatureKelvin,
+ setTemperatureAllInKelvin,
+ toggleAll,
+} from "../litra-manager";
+
+type TemperatureSettings = {
+ presetKelvin?: number;
+ stepKelvin?: number | string;
+};
+
+type ActionRef = WillAppearEvent["action"];
+
+@action({ UUID: "com.ssheppdev.litra.temperature" })
+export class TemperatureAction extends SingletonAction {
+ private readonly dialQueues = new Map void>();
+ private readonly latestSettings = new Map();
+
+ override async onWillAppear(ev: WillAppearEvent): Promise {
+ this.latestSettings.set(ev.action.id, ev.payload.settings);
+ await this.refreshDisplay(ev.action, ev.payload.settings);
+ }
+
+ override async onDidReceiveSettings(ev: DidReceiveSettingsEvent): Promise {
+ this.latestSettings.set(ev.action.id, ev.payload.settings);
+ await this.refreshDisplay(ev.action, ev.payload.settings);
+ }
+
+ override async onKeyDown(ev: KeyDownEvent): Promise {
+ const preset = Number(ev.payload.settings.presetKelvin ?? 4000);
+ const result = setTemperatureAllInKelvin(preset);
+ if (!result.ok) {
+ await ev.action.showAlert();
+ return;
+ }
+ // Key tiles represent the configured set-amount, not live hardware K.
+ await this.showKeyAmount(ev.action, preset);
+ }
+
+ /** Dial press toggles power; rotate adjusts temperature. */
+ override async onDialDown(ev: DialDownEvent): Promise {
+ const result = toggleAll();
+ if (!result.ok) {
+ await ev.action.showAlert();
+ return;
+ }
+ await this.refreshDisplay(ev.action, ev.payload.settings);
+ }
+
+ override async onDialRotate(ev: DialRotateEvent): Promise {
+ const id = ev.action.id;
+ this.latestSettings.set(id, ev.payload.settings);
+ let queue = this.dialQueues.get(id);
+ if (!queue) {
+ queue = createDialDebouncer(async (ticks) => {
+ const settings = this.latestSettings.get(id) ?? {};
+ const step = Number(settings.stepKelvin ?? 100);
+ const result = adjustTemperatureAllByKelvinSteps(ticks, step);
+ if (!result.ok) {
+ await ev.action.showAlert();
+ return;
+ }
+ await this.showDialValue(ev.action, result.representativeTemperatureKelvin);
+ });
+ this.dialQueues.set(id, queue);
+ }
+ queue(ev.payload.ticks);
+ }
+
+ private async refreshDisplay(action: ActionRef, settings: TemperatureSettings): Promise {
+ if (action.isKey()) {
+ await this.showKeyAmount(action, Number(settings.presetKelvin ?? 4000));
+ return;
+ }
+
+ const result = readRepresentativeTemperatureKelvin();
+ await this.showDialValue(
+ action,
+ result.ok ? result.representativeTemperatureKelvin : undefined,
+ );
+ }
+
+ private async showKeyAmount(action: ActionRef, kelvin: number): Promise {
+ await action.setTitle(`${Math.round(kelvin)}`);
+ }
+
+ private async showDialValue(action: ActionRef, kelvin: number | undefined): Promise {
+ const label = kelvin === undefined ? "—" : `${Math.round(kelvin)}`;
+ if (action.isDial()) {
+ await action.setFeedback({
+ title: "Temp",
+ value: label,
+ });
+ }
+ }
+}
diff --git a/src/litra-manager.ts b/src/litra-manager.ts
new file mode 100644
index 0000000..b5398bf
--- /dev/null
+++ b/src/litra-manager.ts
@@ -0,0 +1,314 @@
+import streamDeck from "@elgato/streamdeck";
+import {
+ Device,
+ DeviceType,
+ findDevices,
+ getAllowedTemperaturesInKelvinForDevice,
+ getMaximumBrightnessInLumenForDevice,
+ getMinimumBrightnessInLumenForDevice,
+ getTemperatureInKelvin,
+ isOn,
+ setBrightnessPercentage,
+ setTemperatureInKelvin,
+ toggle,
+ turnOff,
+ turnOn,
+} from "litra";
+
+type ClosableHid = {
+ write: (values: number[] | Buffer) => number;
+ readSync: () => number[];
+ readTimeout: (timeout: number) => number[];
+ close: () => void;
+};
+
+export type BroadcastResult = {
+ ok: boolean;
+ deviceCount: number;
+ failures: number;
+ representativeBrightnessPercentage?: number;
+ representativeTemperatureKelvin?: number;
+};
+
+const logger = streamDeck.logger;
+const DIAL_DEBOUNCE_MS = 60;
+
+function asHid(device: Device): ClosableHid {
+ return device.hid as ClosableHid;
+}
+
+/**
+ * litra write-only commands leave an ACK in the HID buffer. Without draining it,
+ * the next readSync (isOn / getBrightness / getTemperature) can return the ACK
+ * instead of the query response — especially on Beam LX.
+ */
+function drainAck(device: Device): void {
+ try {
+ asHid(device).readTimeout(50);
+ } catch {
+ // nothing pending
+ }
+}
+
+/**
+ * litra's Device.hid type omits close(); runtime is node-hid HID.
+ */
+function closeDevice(device: Device): void {
+ try {
+ asHid(device).close();
+ } catch (error) {
+ logger.warn(`Failed to close Litra HID handle: ${String(error)}`);
+ }
+}
+
+function padRight(values: number[], length: number): number[] {
+ if (values.length >= length) {
+ return values;
+ }
+ return [...values, ...Array(length - values.length).fill(0)];
+}
+
+/**
+ * Stock litra getBrightnessInLumen only returns data[5], which truncates Beam LX
+ * above 255 lm. Mirror litra-rs / temperature parsing: data[4]*256 + data[5].
+ */
+export function getBrightnessInLumenSafe(device: Device): number {
+ const bytes =
+ device.type === DeviceType.LitraBeamLX
+ ? padRight([0x11, 0xff, 0x06, 0x31], 20)
+ : padRight([0x11, 0xff, 0x04, 0x31], 20);
+ device.hid.write(bytes);
+ const data = device.hid.readSync();
+ return data[4] * 256 + data[5];
+}
+
+export function lumenToPercentage(lumen: number, min: number, max: number): number {
+ if (max <= min) {
+ return 0;
+ }
+ return Math.max(0, Math.min(100, Math.round(((lumen - min) / (max - min)) * 100)));
+}
+
+export function snapKelvin(value: number, allowed: number[]): number {
+ if (allowed.length === 0) {
+ return value;
+ }
+ let best = allowed[0];
+ let bestDelta = Math.abs(value - best);
+ for (const candidate of allowed) {
+ const delta = Math.abs(value - candidate);
+ if (delta < bestDelta) {
+ best = candidate;
+ bestDelta = delta;
+ }
+ }
+ return best;
+}
+
+/**
+ * Open devices, run fn per device, always close handles. Zero devices = failure.
+ */
+export function forEachDevice(fn: (device: Device) => void): BroadcastResult {
+ let devices: Device[] = [];
+ try {
+ devices = findDevices();
+ } catch (error) {
+ logger.error(`findDevices failed: ${String(error)}`);
+ return { ok: false, deviceCount: 0, failures: 1 };
+ }
+
+ if (devices.length === 0) {
+ logger.warn("No Litra devices found");
+ return { ok: false, deviceCount: 0, failures: 1 };
+ }
+
+ let failures = 0;
+ for (const device of devices) {
+ try {
+ fn(device);
+ } catch (error) {
+ failures += 1;
+ logger.error(`Litra device operation failed: ${String(error)}`);
+ } finally {
+ closeDevice(device);
+ }
+ }
+
+ return {
+ ok: failures === 0,
+ deviceCount: devices.length,
+ failures,
+ };
+}
+
+export function turnOnAll(): BroadcastResult {
+ return forEachDevice((device) => {
+ turnOn(device);
+ drainAck(device);
+ });
+}
+
+export function turnOffAll(): BroadcastResult {
+ return forEachDevice((device) => {
+ turnOff(device);
+ drainAck(device);
+ });
+}
+
+export function toggleAll(): BroadcastResult {
+ return forEachDevice((device) => {
+ toggle(device);
+ drainAck(device);
+ });
+}
+
+/** On only if at least one device and every device reports on. */
+export function isOnAll(): { ok: boolean; allOn: boolean; deviceCount: number } {
+ let devices: Device[] = [];
+ try {
+ devices = findDevices();
+ } catch (error) {
+ logger.error(`findDevices failed: ${String(error)}`);
+ return { ok: false, allOn: false, deviceCount: 0 };
+ }
+
+ if (devices.length === 0) {
+ return { ok: false, allOn: false, deviceCount: 0 };
+ }
+
+ let allOn = true;
+ let failures = 0;
+ for (const device of devices) {
+ try {
+ if (!isOn(device)) {
+ allOn = false;
+ }
+ } catch (error) {
+ failures += 1;
+ allOn = false;
+ logger.error(`isOn failed: ${String(error)}`);
+ } finally {
+ closeDevice(device);
+ }
+ }
+
+ return { ok: failures === 0, allOn, deviceCount: devices.length };
+}
+
+/** Ensure the device is powered on before a brightness change is visible. */
+function ensureOn(device: Device): void {
+ if (!isOn(device)) {
+ turnOn(device);
+ drainAck(device);
+ }
+}
+
+export function setBrightnessAllPercentage(percentage: number): BroadcastResult {
+ const pct = Math.max(0, Math.min(100, percentage));
+ let representative: number | undefined;
+ const result = forEachDevice((device) => {
+ ensureOn(device);
+ setBrightnessPercentage(device, pct);
+ drainAck(device);
+ if (representative === undefined) {
+ representative = pct;
+ }
+ });
+ return { ...result, representativeBrightnessPercentage: representative };
+}
+
+export function adjustBrightnessAllByPercentageSteps(
+ ticks: number,
+ stepPercentage: number,
+): BroadcastResult {
+ let representative: number | undefined;
+ const result = forEachDevice((device) => {
+ ensureOn(device);
+ const min = getMinimumBrightnessInLumenForDevice(device);
+ const max = getMaximumBrightnessInLumenForDevice(device);
+ const lumen = getBrightnessInLumenSafe(device);
+ const currentPct = lumenToPercentage(lumen, min, max);
+ const nextPct = Math.max(0, Math.min(100, currentPct + ticks * stepPercentage));
+ setBrightnessPercentage(device, nextPct);
+ drainAck(device);
+ if (representative === undefined) {
+ representative = nextPct;
+ }
+ });
+ return { ...result, representativeBrightnessPercentage: representative };
+}
+
+export function readRepresentativeBrightnessPercentage(): BroadcastResult {
+ let representative: number | undefined;
+ const result = forEachDevice((device) => {
+ if (representative !== undefined) {
+ return;
+ }
+ const min = getMinimumBrightnessInLumenForDevice(device);
+ const max = getMaximumBrightnessInLumenForDevice(device);
+ representative = lumenToPercentage(getBrightnessInLumenSafe(device), min, max);
+ });
+ return { ...result, representativeBrightnessPercentage: representative };
+}
+
+export function setTemperatureAllInKelvin(kelvin: number): BroadcastResult {
+ let representative: number | undefined;
+ const result = forEachDevice((device) => {
+ const allowed = getAllowedTemperaturesInKelvinForDevice(device);
+ const snapped = snapKelvin(kelvin, allowed);
+ setTemperatureInKelvin(device, snapped);
+ drainAck(device);
+ if (representative === undefined) {
+ representative = snapped;
+ }
+ });
+ return { ...result, representativeTemperatureKelvin: representative };
+}
+
+export function adjustTemperatureAllByKelvinSteps(ticks: number, stepKelvin: number): BroadcastResult {
+ let representative: number | undefined;
+ const result = forEachDevice((device) => {
+ const current = getTemperatureInKelvin(device);
+ const allowed = getAllowedTemperaturesInKelvinForDevice(device);
+ const snapped = snapKelvin(current + ticks * stepKelvin, allowed);
+ setTemperatureInKelvin(device, snapped);
+ drainAck(device);
+ if (representative === undefined) {
+ representative = snapped;
+ }
+ });
+ return { ...result, representativeTemperatureKelvin: representative };
+}
+
+export function readRepresentativeTemperatureKelvin(): BroadcastResult {
+ let representative: number | undefined;
+ const result = forEachDevice((device) => {
+ if (representative !== undefined) {
+ return;
+ }
+ representative = getTemperatureInKelvin(device);
+ });
+ return { ...result, representativeTemperatureKelvin: representative };
+}
+
+/** Coalesce dial ticks for a short window, then flush once. */
+export function createDialDebouncer(
+ flush: (ticks: number) => void | Promise,
+ delayMs = DIAL_DEBOUNCE_MS,
+): (ticks: number) => void {
+ let pending = 0;
+ let timer: ReturnType | undefined;
+
+ return (ticks: number) => {
+ pending += ticks;
+ if (timer !== undefined) {
+ clearTimeout(timer);
+ }
+ timer = setTimeout(() => {
+ const batch = pending;
+ pending = 0;
+ timer = undefined;
+ void flush(batch);
+ }, delayMs);
+ };
+}
diff --git a/src/plugin.ts b/src/plugin.ts
new file mode 100644
index 0000000..7c77395
--- /dev/null
+++ b/src/plugin.ts
@@ -0,0 +1,13 @@
+import streamDeck from "@elgato/streamdeck";
+
+import { BrightnessAction } from "./actions/brightness";
+import { PowerAction } from "./actions/power";
+import { TemperatureAction } from "./actions/temperature";
+
+streamDeck.logger.setLevel("info");
+
+streamDeck.actions.registerAction(new PowerAction());
+streamDeck.actions.registerAction(new BrightnessAction());
+streamDeck.actions.registerAction(new TemperatureAction());
+
+streamDeck.connect();
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..42460a7
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "extends": "@tsconfig/node20/tsconfig.json",
+ "compilerOptions": {
+ "customConditions": [
+ "node"
+ ],
+ "module": "ES2022",
+ "moduleResolution": "Bundler",
+ "noImplicitOverride": true
+ },
+ "include": [
+ "src/**/*.ts"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}