@lumiastream/lumia-types 3.9.2 → 3.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -685,7 +685,7 @@ exports.LumiaAlertFriendlyValues = {
685
685
  [LumiaAlertValues.YOUTUBE_SUBSCRIBER]: 'Youtube Subscriber',
686
686
  [LumiaAlertValues.YOUTUBE_SUPERCHAT]: 'Youtube Superchat',
687
687
  [LumiaAlertValues.YOUTUBE_SUPERSTICKER]: 'Youtube Supersticker',
688
- [LumiaAlertValues.YOUTUBE_GIFTS]: 'Youtube Gifts',
688
+ [LumiaAlertValues.YOUTUBE_GIFTS]: 'Youtube Gifts (Jewels)',
689
689
  [LumiaAlertValues.YOUTUBE_SESSION_GIFTS]: 'Youtube Session Gifts',
690
690
  [LumiaAlertValues.YOUTUBE_ENTRANCE]: 'Youtube Entrance',
691
691
  [LumiaAlertValues.YOUTUBE_LIKE]: 'Youtube Like',
@@ -25,7 +25,7 @@ Let's get started:
25
25
  **System bases** (built into Lumia):
26
26
  `delay, lumia, overlay, api, commandRunner, inputEvents`
27
27
 
28
- > Note: `lumia` and `overlay` also accept the aliases `lumiaActions` and `overlayActions`, and the input base is `inputEvents` (plural).
28
+ > Note: the canonical system bases are `lumia` and `overlay`. The older spellings `lumiaActions` and `overlayActions` still run but are **deprecated** — prefer `lumia` / `overlay`. The input base is `inputEvents` (plural).
29
29
 
30
30
  **Integration bases** — every connected integration is also a valid base. These include (and more are added over time):
31
31
  `twitch, youtube, facebook, tiktok, kick, discord, obs, slobs, meld, spotify, youtubemusic, nowplaying, vlc, voicemod, streamerbot, vtubestudio, midi, osc, artnet, mqtt, serial, websocket, broadlink, hue, lifx, nanoleaf, govee, wled, wiz, tplink, tuya, yeelight, elgato, streamdeck, touchportal, loupedeck, homeassistant, switchbot` plus any installed plugin (use the plugin's id as the base).
@@ -36,7 +36,27 @@ Let's get started:
36
36
 
37
37
  `variables`: allows you to send in different variables for each action. But do note that the variables that are already on the command/alert will also be spread on to this variables object. Variables are not required
38
38
 
39
- There are more fields that are sometimes used in actions. `delay` (a number in milliseconds) can be added to any action to wait before running it.
39
+ To pause between actions, insert a dedicated **delay step**: an entry whose `type` is `"delay"` with the milliseconds in `delay` (e.g. `{ type: "delay", delay: 1000 }`). It can carry any `base` and runs in order with the rest of the list. (An inline `delay` on a non-delay action is **not** applied by the runner — use a delay step.)
40
+
41
+ #### Where actions live: the unified `actions` lane
42
+
43
+ Inside a Lumia **command** or **alert**, every action — whatever its `base` (a `lumia` system action, an `overlay` action, a `twitch` action, an integration, a plugin) — lives together in one ordered list on the command:
44
+
45
+ ```json
46
+ {
47
+ "actions": {
48
+ "before": [{ "base": "lumia", "type": "tts", "value": { "message": "Hi" } }],
49
+ "after": [{ "base": "twitch", "type": "clip", "value": { "title": "Clutch!", "duration": 30 } }],
50
+ "waitForActions": true
51
+ }
52
+ }
53
+ ```
54
+
55
+ - **`before`** runs before the command/alert's main effect (its light / overlay reaction); **`after`** runs after it.
56
+ - **`waitForActions`** (optional): when `true`, each action is awaited so the list runs in strict order before the command continues; when omitted or `false`, the list fires without waiting.
57
+ - Every entry is one action object of the shape `{ base, type, value?, delay?, args? }` — the same object you pass to the custom-code `actions()` helper. `delay` holds the milliseconds for a `type: "delay"` step; `args` carries the extra payload used by imported Streamer.bot actions.
58
+
59
+ This unified `actions` lane is the current model, and the one the AI action creator and the Streamer.bot / Mix It Up importers target. It **replaces** the older per-source command fields (`lumiaActions`, `overlayActions`, `api`, `commandRunner`, `inputEvents`) and the per-integration lanes (`command.<integration>.before/after` — e.g. `command.wavelink`, `command.streamfog`, whose entries have no `base`), all of which are **deprecated** — within the unified lane the source is simply each entry's `base`.
40
60
 
41
61
  ### Passing an array of actions
42
62
 
@@ -63,9 +83,11 @@ These are the built-in `type` values for each system base.
63
83
 
64
84
  **`base: "commandRunner"`** — `app/file, shell command, delay`
65
85
 
66
- **`base: "inputEvents"`** — `keyboard, mouse, delay`
86
+ **`base: "inputEvents"`** — `keyboard, mouse, delay`. The key/mouse data is **flat on the action**, not under `value`:
87
+ > - `keyboard`: `{ base: "inputEvents", type: "keyboard", keyboardValue: { value: "ctrl+j", valueType: "combination" } }` — `valueType` is `combination` (a hotkey like `ctrl+shift+f5`) or `input` (type the literal `value` as text); optional `longPress`, `cpm`.
88
+ > - `mouse`: `{ base: "inputEvents", type: "mouse", mouseValue: { x: 0, y: 100, clickEvent: "left", moveType: "set" } }` — `clickEvent` is `left` / `right` / `none`; optional `x1`, `y1`, `doubleClick`, `mouseSpeed`.
67
89
 
68
- **`base: "delay"`**no `type`; pass the millisecond delay directly as the value, e.g. `{ base: "delay", value: 1000 }`.
90
+ **delay step** an entry with `type: "delay"` pauses the list; put the milliseconds in `delay` (e.g. `{ type: "delay", delay: 1000 }`). `duration` is accepted in place of `delay`, and the step runs under any `base`.
69
91
 
70
92
  > Tip: most of the `lumia` and `overlay` actions already have dedicated helper functions (`tts`, `chatbot`, `overlaySetTextContent`, etc.) in `helper-functions.md`. Reach for `actions()` mainly when you need an integration action that does not have a helper yet.
71
93
 
@@ -95,6 +117,242 @@ async function() {
95
117
  }
96
118
  ```
97
119
 
120
+ ### Engagement & system `lumia` actions
121
+
122
+ These `lumia` actions drive Lumia's engagement features (raffles, tournaments, viewer queue, song requests) and system controls. None of them has a dedicated helper, so call them through `actions()`. The action shape is `{ base: "lumia", type: "<type>", value: { ... } }` — the fields in the tables below go **inside the inner `value` object**. Text fields accept template tokens like `{{username}}`.
123
+
124
+ #### Enable / disable commands, alerts, folders & automations
125
+
126
+ `on: true` enables, `on: false` disables.
127
+
128
+ | type | `value` | targets |
129
+ | --- | --- | --- |
130
+ | `setCommand` | `{ value: "<command name>", on: true }` | a chat command |
131
+ | `setChatbotCommand` | `{ value: "<command name>", on: true }` | a chatbot command |
132
+ | `setTwitchPointsCommand` | `{ value: "<command name>", on: true }` | a Twitch channel-points reward command |
133
+ | `setTwitchExtensionCommand` | `{ value: "<command name>", on: true }` | a Twitch extension command |
134
+ | `setKickPointsCommand` | `{ value: "<command name>", on: true }` | a Kick points command |
135
+ | `setChatMatchCommand` | `{ value: "<chat-match id>", on: true }` | a chat-match command (by id) |
136
+ | `setAlert` | `{ value: "<alert key>", on: true }` | an alert (its `pathKey`) |
137
+ | `setAlertVariation` | `{ value: "<alert key>", variation: "<variation id>", on: true }` | one variation of an alert |
138
+ | `setFolder` | `{ value: "<folder id>", on: true }` | a command/alert folder |
139
+ | `setAutomation` | `{ value: "<automation name>", on: true }` | an automation/timer (matched by name) |
140
+ | `setVoicecommands` | `{ on: true }` | the voice-commands input as a whole |
141
+ | `setFuzeAudioSensitivity` | `{ value: 50 }` | Fuze audio sensitivity (number) |
142
+
143
+ #### Variables, local storage & files
144
+
145
+ | type | `value` | notes |
146
+ | --- | --- | --- |
147
+ | `appendToVariable` | `{ value: "<variable name>", message: "<item>", unique: true }` | append an item to a list variable; `unique` skips it if already present |
148
+ | `unappendFromVariable` | `{ value: "<variable name>", message: "<item>" }` | remove an item from a list variable |
149
+ | `saveLocal` | `{ value: "<key>", message: "<value>" }` | persist a value (same store as the `{{save_local}}` / `{{load_local}}` chat functions) |
150
+ | `writeToFile` | `{ value: "<file path>", message: "<content>", on: true }` | write text to a file; `on: true` appends instead of overwriting |
151
+
152
+ > For plain variables prefer the `setVariable` / `getVariable` / `deleteVariable` helpers in `helper-functions.md`.
153
+
154
+ #### Userlevels & restrictions
155
+
156
+ | type | `value` | notes |
157
+ | --- | --- | --- |
158
+ | `addToUserlevel` | `{ value: "<userlevel id>", message: "<username>" }` | add a user to a userlevel |
159
+ | `removeFromUserlevel` | `{ value: "<userlevel id>", message: "<username>" }` | remove a user from a userlevel |
160
+ | `addToRestrictionsList` | `{ message: "<username>" }` | restrict a user (no `value`) |
161
+ | `removeFromRestrictionsList` | `{ message: "<username>" }` | unrestrict a user |
162
+
163
+ #### Raffles
164
+
165
+ | type | `value` | notes |
166
+ | --- | --- | --- |
167
+ | `raffleStart` | `{ title: "<title>", preset: "<raffle id>", auto_end: true, ends_after: 5 }` | all optional; `preset` seeds from a saved raffle, `ends_after` is in **minutes** (default 30), `auto_end` turns on the timer |
168
+ | `raffleEntry` | `{ value: "<username>" }` | needs a started raffle |
169
+ | `raffleRemoveEntry` | `{ value: "<username>" }` | |
170
+ | `raffleStop` | `{}` | stop accepting entries |
171
+ | `raffleGetWinner` | `{}` | pick a winner (raffle must be stopped first) |
172
+ | `raffleEnd` | `{}` | end the raffle |
173
+
174
+ #### Tournaments
175
+
176
+ | type | `value` | notes |
177
+ | --- | --- | --- |
178
+ | `tournamentStart` | `{ preset: "<tournament id>" }` | start from a preset, or a new tournament if omitted |
179
+ | `tournamentEntry` | `{ message: "<username>", value: "<avatar url>", extra: "<team>", platform: "twitch" }` | `message` is the username; needs a running tournament with signups open |
180
+ | `tournamentRemoveEntry` | `{ message: "<username>", platform: "twitch" }` | |
181
+ | `tournamentUpdatePoints` | `{ message: "<username>", points: "+100" }` | `points` uses the modifier syntax (`+ - * / =`); defaults to `=` (set) when no modifier |
182
+ | `tournamentEnd` | `{}` | |
183
+
184
+ #### Viewer queue
185
+
186
+ | type | `value` | notes |
187
+ | --- | --- | --- |
188
+ | `viewerQueueEntry` | `{ value: "<username>" }` | needs a started queue |
189
+ | `viewerQueueLeave` | `{ value: "<username>" }` | |
190
+ | `viewerQueuePickPlayer` | `{ value: 1, mode: "single" }` | pick N players; `mode` is `single`, `bulkfirst`, or `bulkrandom` (default `single`, 1 player) |
191
+ | `viewerQueuePlayPause` | `{ on: true }` | `true` plays, `false` pauses |
192
+ | `viewerQueueEndQueue` | `{}` | |
193
+
194
+ #### Song requests
195
+
196
+ | type | `value` | notes |
197
+ | --- | --- | --- |
198
+ | `addSongRequest` | `{ value: "<search term or url>", requesterUsername: "{{username}}", skipApproval: true }` | `skipApproval` (or `forceApprove`) bypasses the approval queue |
199
+ | `skipSongRequest` | `{}` | |
200
+ | `clearSongRequestQueue` | `{}` | |
201
+
202
+ #### Queue, cooldowns & session (no `value` needed)
203
+
204
+ | type | what it does |
205
+ | --- | --- |
206
+ | `pauseQueue` / `resumeQueue` | pause / resume the action queue |
207
+ | `removeCurrentQueueItem` | drop the item currently running |
208
+ | `runLastQueueItem` | re-run the last completed queue item |
209
+ | `clearQueue` | empty the queue |
210
+ | `clearCooldowns` | clear all command cooldowns |
211
+ | `resetSession` | reset session variables (session counts) |
212
+ | `cleanAll` | clear queue + cooldowns and send lights to default |
213
+ | `backToDefault` | send lights back to their default |
214
+ | `refreshSettings` | hard-refresh Lumia's settings |
215
+ | `replayLastEventListEvent` | replay the most recent event-list event |
216
+
217
+ Call these with no inner value, e.g. `await actions([{ base: "lumia", type: "clearQueue" }]);`
218
+
219
+ #### Other command calls & webhooks
220
+
221
+ | type | `value` | notes |
222
+ | --- | --- | --- |
223
+ | `callRandomCommand` | `{ options: ["cmdA", "cmdB", "cmdC"], addToEndOfQueue: false }` | run one of the listed commands at random |
224
+ | `toggleStreamMode` | `{}` | flip stream mode on/off (use `setStreamMode { on }` to set it explicitly) |
225
+ | `sendToWebhook` | `{ value: "<url>", message: "<json string>" }` | `message` must be valid JSON — it is parsed and POSTed as the request body |
226
+ | `sendToDiscordWebhook` | `{ value: "<discord webhook url>", message: "<text>" }` | posts text to a Discord webhook |
227
+ | `sendToDiscordWithMediaWebhook` | `{ value: "<discord webhook url>", message: "<text>", file: "<local file path>" }` | posts text + an uploaded file |
228
+ | `sendToPrinter` | `{ value: "<IP:port or usb://vendor:product>", type: "image", message: "<image path>" }` | `type` is `image` (default) or `text`; for `text`, `message` is the text to print |
229
+
230
+ ```js
231
+ async function() {
232
+ // Start a 5-minute raffle, then enter the running viewer
233
+ await actions([
234
+ { base: "lumia", type: "raffleStart", value: { title: "Giveaway", auto_end: true, ends_after: 5 } },
235
+ { base: "lumia", type: "raffleEntry", value: { value: "{{username}}" } },
236
+ ]);
237
+
238
+ // Disable a command and clear the queue
239
+ await actions([
240
+ { base: "lumia", type: "setCommand", value: { value: "!hug", on: false } },
241
+ { base: "lumia", type: "clearQueue" },
242
+ ]);
243
+ done();
244
+ }
245
+ ```
246
+
247
+ ### Points, loyalty & currency actions
248
+
249
+ Several `lumia` actions change a point value, and they all share the same **modifier** syntax on the amount you pass in:
250
+
251
+ | You pass | Result |
252
+ | --- | --- |
253
+ | `+100` | add 100 |
254
+ | `-100` | subtract 100 |
255
+ | `*2` | multiply by 2 |
256
+ | `/2` | divide by 2 |
257
+ | `=300` | set the exact value to 300 |
258
+
259
+ A bare number with **no** modifier falls back to each action's own default (noted per action below). The same `+ - * / =` modifiers also work on the `updateCounter` and `updateVariable` actions.
260
+
261
+ #### `setUserLoyaltyPoint` — change a viewer's loyalty balance
262
+
263
+ | Field | Meaning |
264
+ | --- | --- |
265
+ | `value.value` | the amount (with optional modifier) |
266
+ | `value.message` | the username to target — defaults to the command's own `{{username}}` |
267
+ | `value.platform` | optional; defaults to the event's platform, then `twitch` |
268
+
269
+ **Default modifier: `+`** — a bare number *adds* points.
270
+
271
+ ```js
272
+ async function() {
273
+ // Give the viewer who triggered this 100 points
274
+ await actions([{ base: "lumia", type: "setUserLoyaltyPoint", value: { value: "100", message: "{{username}}" } }]);
275
+
276
+ // Take 50 points from a specific user on YouTube
277
+ await actions([{ base: "lumia", type: "setUserLoyaltyPoint", value: { value: "-50", message: "someviewer", platform: "youtube" } }]);
278
+
279
+ // Set an exact balance of 500
280
+ await actions([{ base: "lumia", type: "setUserLoyaltyPoint", value: { value: "=500", message: "{{username}}" } }]);
281
+ done();
282
+ }
283
+ ```
284
+
285
+ #### `setLoyaltyPointValue` — change the loyalty-points **cost** of one of your commands
286
+
287
+ | Field | Meaning |
288
+ | --- | --- |
289
+ | `value.value` | the command name whose cost you are changing |
290
+ | `value.points` | the new cost (with optional modifier) |
291
+
292
+ **Default modifier: `=`** — a bare number *sets* the cost.
293
+
294
+ ```js
295
+ async function() {
296
+ await actions([{ base: "lumia", type: "setLoyaltyPointValue", value: { value: "myredeem", points: "250" } }]); // cost becomes 250
297
+ await actions([{ base: "lumia", type: "setLoyaltyPointValue", value: { value: "myredeem", points: "+50" } }]); // cost goes up by 50
298
+ done();
299
+ }
300
+ ```
301
+
302
+ #### `setTwitchPointValue` / `setTwitchExtensionBitsValue` — change a Twitch reward's cost
303
+
304
+ Both take the same `{ value: "<commandName>", points: "<amount>" }` shape as `setLoyaltyPointValue` and also **default to `=`**. `setTwitchPointValue` updates a channel-points reward's cost; `setTwitchExtensionBitsValue` updates a Twitch-extension command's Bits cost.
305
+
306
+ ```js
307
+ async function() {
308
+ await actions([{ base: "lumia", type: "setTwitchPointValue", value: { value: "myreward", points: "1000" } }]);
309
+ await actions([{ base: "lumia", type: "setTwitchExtensionBitsValue", value: { value: "myextcommand", points: "100" } }]);
310
+ done();
311
+ }
312
+ ```
313
+
314
+ ### Overlay actions without a helper
315
+
316
+ These `base: "overlay"` actions don't have a dedicated helper yet. Their inner `value` targets a layer with `layer` (add `overlay: "<overlay name>"` to disambiguate which overlay the layer belongs to). `content` fields accept template tokens.
317
+
318
+ #### Spin wheel
319
+
320
+ | type | `value` | notes |
321
+ | --- | --- | --- |
322
+ | `spinwheelAddItem` | `{ layer: "<layer>", content: "<item title>", image: "<image url>" }` | adds an item (max 60); `image` is optional |
323
+ | `spinwheelRemoveItem` | `{ layer: "<layer>", content: "<item title>" }` | removes a matching item |
324
+ | `spinwheelReset` | `{ layer: "<layer>", removeItems: true }` | `removeItems: true` wipes all items; otherwise it just resets the wheel |
325
+
326
+ #### Polls / trigger games
327
+
328
+ | type | `value` | notes |
329
+ | --- | --- | --- |
330
+ | `pollStart` | `{ layer: "<layer>" }` | start the poll |
331
+ | `pollTrigger` | `{ layer: "<layer>" }` | trigger using the caller's username/message |
332
+ | `pollAddItem` | `{ layer: "<layer>", content: "<option title>", trigger: "<keyword>" }` | `trigger` defaults to the next option number |
333
+ | `pollRemoveItem` | `{ layer: "<layer>", content: "<option title>" }` | |
334
+ | `pollResetVotes` | `{ layer: "<layer>" }` | |
335
+ | `pollSetTimer` | `{ layer: "<layer>", content: "30" }` | `content` is the timer duration in seconds |
336
+
337
+ #### Game overlays, content & screenshots
338
+
339
+ | type | `value` | notes |
340
+ | --- | --- | --- |
341
+ | `sendGameTrigger` | `{ layer: "<layer>", content: "<value>" }` | targets the player from the caller's context |
342
+ | `sendGameUpdate` | `{ layer: "<layer>", content: "<value>" }` | |
343
+ | `setContent` | `{ layer: "<layer>", content: "<text>" }` | same as the `overlaySetTextContent` helper |
344
+ | `alertEvent` | `{ value: "<alert id>", variation: "<variation>", duration: 5000 }` | fire an overlay alert event; `duration` in ms |
345
+ | `takeScreenshot` | `{ value: "<overlay id>", format: "png", conversion: "original" }` | `format` is `png` or `jpeg`; `conversion` is `original`, `thermal`, `grayscale`, `invert`, or `binary`; resolves to the saved image path |
346
+
347
+ #### HUD
348
+
349
+ | type | `value` | notes |
350
+ | --- | --- | --- |
351
+ | `hudToggle` | `{}` | toggle the HUD |
352
+ | `hudOverlayChange` | `{ value: "<overlay>" }` | switch the HUD overlay |
353
+ | `hudVolumeSet` | `{ volume: 50 }` | set HUD volume |
354
+ | `hudOpacitySet` | `{ volume: 80 }` | set HUD opacity — note the value goes in the `volume` field |
355
+
98
356
  ### OBS (v5) actions
99
357
 
100
358
  OBS has a dedicated helper, so you do **not** need `actions()` for it: pass any OBS websocket v5 request to `sendRawObsJson({ "request-type": "...", ...params })` (see `helper-functions.md`). The `request-type` and fields are exactly the same ones Lumia's OBS action editor uses. Common ones:
@@ -132,4 +390,367 @@ async function() {
132
390
 
133
391
  Other available request-types include `SetCurrentProfile`, `SetCurrentSceneCollection`, `CreateInput`, `RemoveInput`, `SetSceneItemTransform`, `SetSceneItemBlendMode`, `SetInputAudioMonitorType`, `TriggerHotkeyByName`, `SetStudioModeEnabled`, `SendStreamCaption`, and the `Start/Stop/Toggle` variants for recording, streaming, replay buffer and virtual cam — the same list as the OBS action editor.
134
392
 
393
+ ### Integration actions
394
+
395
+ Every connected integration is also a valid `base` (`spotify`, `discord`, `vtubestudio`, `midi`, `osc`, and the rest listed at the top). An integration action only runs if that integration is **connected and enabled** — otherwise it is silently skipped. The exact `type` and `value` vary per integration; the common ones are below. To find anything not listed, set the action up once in the normal action editor and read off its fields.
396
+
397
+ **Wrapper styles differ per integration** — each subsection states which it uses:
398
+
399
+ - **`value` object** — `{ base, type, value: { ... } }`: Discord, VTube Studio, Twitch, YouTube, Kick, TikTok, Meld, SwitchBot, Wave Link, Camera Hub, StreamFog.
400
+ - **scalar `value`** — `{ base, type, value: "..." }`: Spotify, VLC, YouTube Music, Voicemod, Streamer.bot.
401
+ - **flat fields** — `{ base, type, ...fields }` with no `value` wrapper: MIDI, OSC, OBS, MQTT, WebSocket, Serial, Art-Net, Broadlink (SLOBS is a special case, noted below).
402
+
403
+ Every integration also supports a delay step: `{ base: "<integration>", type: "delay", delay: <ms> }`. String fields generally resolve `{{template}}` tokens (Art-Net is the exception).
404
+
405
+ > **Lights** (Hue, LIFX, Govee, Nanoleaf, WLED, Elgato key lights, etc.) have **no** action `base` — control them with the `sendColor` helper (see `helper-functions.md`), not through `actions()`.
406
+
407
+ #### Spotify (`base: "spotify"`)
408
+
409
+ Spotify actions take a single scalar `value` (not an object). Playback control needs an active Spotify device.
410
+
411
+ | type | `value` | notes |
412
+ | --- | --- | --- |
413
+ | `setPlayState` | `"play"` / `"pause"` / `"toggle"` | |
414
+ | `skipToNext` / `skipToPrevious` | — | no value needed |
415
+ | `setVolume` | `0`–`100` | percent |
416
+ | `seek` | `<milliseconds>` | |
417
+ | `setShuffle` | `"true"` / `"false"` / `"toggle"` | |
418
+ | `setRepeat` | `"track"` / `"context"` / `"off"` | |
419
+ | `searchAndPlay` | `"<query or track url>"` | queues the match then skips to it |
420
+ | `searchAndPlayImmediately` | `"<query or track url>"` | plays it right away |
421
+ | `searchAndAddToQueue` | `"<query or track url>"` | adds to the queue |
422
+ | `searchAndAddToPlaylist` | `"<query or track url>"` + top-level `target: "<playlist id>"` | |
423
+
424
+ ```js
425
+ async function() {
426
+ await actions([{ base: "spotify", type: "setVolume", value: 40 }]);
427
+ await actions([{ base: "spotify", type: "searchAndAddToQueue", value: "never gonna give you up" }]);
428
+ await actions([{ base: "spotify", type: "searchAndAddToPlaylist", value: "yesterday", target: "37i9dQZF1DXcBWIGoYBM5M" }]);
429
+ done();
430
+ }
431
+ ```
432
+
433
+ #### Discord (`base: "discord"`)
434
+
435
+ Discord actions take a `value` object and post through your connected Discord.
436
+
437
+ | type | `value` | notes |
438
+ | --- | --- | --- |
439
+ | `sendMessage` | `{ value: "<message>", target: "<channel id>", file: "<local path>", embeds: "<json string>", on: false }` | the message text is `value.value`; `target` defaults to your primary channel; `on: true` sends it as TTS; `embeds` is a JSON-array string |
440
+ | `addMemberRole` | `{ value: "<username>", target: "<role id>" }` | |
441
+ | `removeMemberRole` | `{ value: "<username>", target: "<role id>" }` | |
442
+ | `kickMember` | `{ value: "<username>" }` | |
443
+
444
+ ```js
445
+ async function() {
446
+ await actions([{ base: "discord", type: "sendMessage", value: { value: "Stream is live! {{twitch_channel_title}}" } }]);
447
+ done();
448
+ }
449
+ ```
450
+
451
+ #### VTube Studio (`base: "vtubestudio"`)
452
+
453
+ VTube Studio actions take a `value` object; the identifier (model / hotkey / expression) goes in the inner `value.value`.
454
+
455
+ | type | `value` | notes |
456
+ | --- | --- | --- |
457
+ | `loadModel` | `{ value: "<model name or id>" }` | |
458
+ | `hotkeyTrigger` | `{ value: "<hotkey id>" }` | |
459
+ | `expressionTrigger` | `{ value: "<expression name>", on: true }` | `on` activates / deactivates |
460
+ | `moveModel` | `{ positionX: "0", positionY: "0.5", rotation: "0", timeInSeconds: 1, valuesAreRelativeToModel: false }` | position/rotation are numbers or numeric strings |
461
+ | `resizeModel` | `{ size: "-22.5", timeInSeconds: 1, valuesAreRelativeToModel: false }` | `size` ranges -100 (smallest) to 100 (biggest); applied through MoveModelRequest |
462
+
463
+ ```js
464
+ async function() {
465
+ await actions([{ base: "vtubestudio", type: "hotkeyTrigger", value: { value: "MyHotkey" } }]);
466
+ done();
467
+ }
468
+ ```
469
+
470
+ #### MIDI (`base: "midi"`) and OSC (`base: "osc"`)
471
+
472
+ MIDI and OSC read their fields **flat on the action object** — there is no `value` wrapper.
473
+
474
+ ```js
475
+ async function() {
476
+ // MIDI: send a note (channel 1-16, note name like "C3", velocity 0-127)
477
+ await actions([{ base: "midi", type: "note-on", port: 0, channel: 1, note: "C3", velocity: 127 }]);
478
+
479
+ // OSC: send a typed message. type is "s" (string), "f" (float), or "i" (int).
480
+ await actions([{ base: "osc", type: "f", name: "/track/1/volume", sendPort: "9000", value: 0.8 }]);
481
+ done();
482
+ }
483
+ ```
484
+
485
+ > MIDI device targeting: `port` (the device's port number) is the simplest selector and is required. On a multi-device setup — where ports can shift between sessions — also pass the optional `deviceId` and/or `deviceName`; Lumia resolves the target by `deviceId`, then `deviceName`, then `port`. `type` is `note-on`, `note-off`, or `delay` (a `delay` entry just waits its `delay` ms).
486
+
487
+ #### Twitch (`base: "twitch"`)
488
+
489
+ Twitch actions take a `value` object. Most string inputs — usernames, message ids, comma-separated option lists — go in `value.message`. Each action needs the matching Twitch permission to be granted on your connection.
490
+
491
+ Stream & engagement:
492
+
493
+ | type | `value` | notes |
494
+ | --- | --- | --- |
495
+ | `changeStreamTitle` | `{ message: "<title>" }` | |
496
+ | `changeCurrentCategory` | `{ message: "<game/category>" }` | matched by name |
497
+ | `clip` | `{ title: "<title>", duration: 30 }` | `duration` 5–60s; sets `{{twitch_last_clip_url}}` |
498
+ | `createStreamMarker` | `{ message: "<description>" }` | |
499
+ | `runCommercial` | `{ duration: 60 }` | one of 30/60/90/120/150/180 |
500
+ | `changeUserColor` | `{ color: "blue" }` | named color (Prime/Turbo can use hex) |
501
+ | `shoutout` | `{ message: "<username>" }` | runs your shoutout command |
502
+ | `raid` | `{ message: "<channel>" }` | |
503
+ | `sendAnnouncement` | `{ message: "<text>", color: "primary" }` | color: primary/blue/green/orange/purple |
504
+ | `createPoll` | `{ title: "<title>", message: "opt1,opt2", duration: 60 }` | options comma-separated; `duration` seconds |
505
+ | `endPoll` | `{ reason: "ARCHIVED" }` | `ARCHIVED` or `TERMINATED` |
506
+ | `createPrediction` | `{ title: "<title>", message: "outcome1,outcome2", duration: 60 }` | |
507
+ | `endPrediction` | `{ reason: "RESOLVED", message: "<winning outcome>" }` | `RESOLVED`/`CANCELED`/`LOCKED`; `message` is the winning outcome text |
508
+ | `updateRedemptionStatus` | `{ status: "FULFILLED" }` | `FULFILLED` or `CANCELED`; applies to pending redemptions |
509
+
510
+ Chat modes (`on: true` enables):
511
+
512
+ | type | `value` |
513
+ | --- | --- |
514
+ | `setSubscriberMode` / `setEmotesMode` / `setUniqueChatMode` | `{ on: true }` |
515
+ | `setFollowMode` | `{ on: true, duration: 0 }` (`duration` in **minutes**) |
516
+ | `setSlowMode` | `{ on: true, duration: 30 }` (`duration` in **seconds**) |
517
+ | `clearChat` | `{}` |
518
+
519
+ Moderation:
520
+
521
+ | type | `value` | notes |
522
+ | --- | --- | --- |
523
+ | `banUser` | `{ message: "<username>", reason: "" }` | |
524
+ | `unbanUser` | `{ message: "<username>" }` | |
525
+ | `timeoutUser` | `{ message: "<username>", duration: 600, reason: "" }` | `duration` in seconds |
526
+ | `deleteMessage` | `{ message: "<message id>" }` | |
527
+ | `pinMessage` | `{ message: "<text>", duration: 0 }` | sends then pins; `duration` 0 = no expiry, else 30–1800s |
528
+ | `unpinMessage` | `{}` | |
529
+ | `addModerator` / `removeModerator` / `addVip` / `removeVip` | `{ message: "<username>" }` | |
530
+ | `warnUser` | `{ message: "<username>", reason: "" }` | |
531
+ | `blockUser` / `unblockUser` | `{ message: "<username>" }` | |
532
+ | `addBlockedWord` / `removeBlockedWord` | `{ message: "<word>" }` | |
533
+ | `shieldModeOn` / `shieldModeOff` | `{}` | |
534
+ | `setSuspiciousUserStatus` | `{ message: "<username>", status: "ACTIVE_MONITORING" }` | `ACTIVE_MONITORING` or `RESTRICTED` |
535
+ | `removeSuspiciousUserStatus` | `{ message: "<username>" }` | |
536
+
537
+ ```js
538
+ async function() {
539
+ await actions([{ base: "twitch", type: "clip", value: { title: "Clutch!", duration: 30 } }]);
540
+ await actions([{ base: "twitch", type: "sendAnnouncement", value: { message: "Raid incoming!", color: "purple" } }]);
541
+ done();
542
+ }
543
+ ```
544
+
545
+ #### YouTube (`base: "youtube"`)
546
+
547
+ `value` object. Usernames go in `value.message`.
548
+
549
+ | type | `value` | notes |
550
+ | --- | --- | --- |
551
+ | `changeStreamTitle` | `{ title: "<title>", message: "<description>" }` | note: `message` is the **description** |
552
+ | `banUser` | `{ message: "<username>" }` | permanent |
553
+ | `unbanUser` | `{ message: "<username>" }` | |
554
+ | `timeoutUser` | `{ message: "<username>", duration: 300 }` | `duration` in seconds |
555
+ | `deleteMessage` | `{ message: "<message id>" }` | |
556
+ | `shoutout` | `{ message: "<username>" }` | |
557
+
558
+ #### Kick (`base: "kick"`)
559
+
560
+ `value` object. Usernames and the stream title both go in `value.message`. No poll or send-message action (chat goes through the chatbot).
561
+
562
+ | type | `value` | notes |
563
+ | --- | --- | --- |
564
+ | `changeStreamTitle` | `{ message: "<title>" }` | |
565
+ | `changeCurrentCategory` | `{ message: "<category>" }` | matched by name |
566
+ | `shoutout` | `{ message: "<username>" }` | |
567
+ | `banUser` | `{ message: "<username>", reason: "" }` | |
568
+ | `unbanUser` | `{ message: "<username>" }` | |
569
+ | `timeoutUser` | `{ message: "<username>", duration: 600, reason: "" }` | `duration` in seconds |
570
+
571
+ #### TikTok (`base: "tiktok"`)
572
+
573
+ `value` object. TikTok actions are **video upload only** (everything else is inbound events).
574
+
575
+ | type | `value` | notes |
576
+ | --- | --- | --- |
577
+ | `uploadDraftVideo` | `{ media: "<local file path>" }` | uploads to your TikTok inbox to finish/publish manually |
578
+ | `uploadAndPublishVideo` | `{ media: "<local file path>", title: "<title>", privacy_level: "PUBLIC_TO_EVERYONE", disable_comment: false, disable_duet: false, disable_stitch: false }` | `privacy_level`: `PUBLIC_TO_EVERYONE` / `MUTUAL_FOLLOW_FRIENDS` / `SELF_ONLY` |
579
+
580
+ #### OBS (`base: "obs"`)
581
+
582
+ For OBS, prefer the **`sendRawObsJson`** helper documented above — the `actions()` OBS path uses the same OBS-websocket-v5 `request-type` values and fields. Reach for `base: "obs"` only to batch OBS in one ordered `actions([...])` array with other bases, or to use Lumia's convenience source types.
583
+
584
+ OBS fields are **flat** on the action object (the type key is `request-type`, hyphenated):
585
+
586
+ ```js
587
+ async function() {
588
+ await actions([
589
+ { base: "obs", "request-type": "SetCurrentProgramScene", sceneName: "Starting Soon" },
590
+ { base: "obs", "request-type": "SetInputMute", inputName: "Mic/Aux", inputMuted: true },
591
+ ]);
592
+ done();
593
+ }
594
+ ```
595
+
596
+ Common direct request-types (flat fields, verified against real setups):
597
+
598
+ | `request-type` | fields |
599
+ | --- | --- |
600
+ | `SetCurrentProgramScene` | `{ sceneName }` |
601
+ | `SetSceneItemEnabled` | `{ sceneName, inputName, sceneItemEnabled }` — Lumia resolves `inputName` → the scene-item id |
602
+ | `SetSourceFilterEnabled` | `{ sourceName, filterName, filterEnabled }` |
603
+ | `SaveSourceScreenshot` | `{ sourceName, imageFormat, imageFilePath }` |
604
+ | `StartStream` / `StopStream` / `StartRecord` / `StopRecord` | no extra fields |
605
+
606
+ Convenience source types let you set a source's content without hand-building `inputSettings`, but they only work if you **also** pass `lumiaOrigin: "SetInputSettings"` — Lumia rewrites the request into a real `SetInputSettings` call using it. Without `lumiaOrigin` the alias is sent verbatim and OBS rejects it. The aliases: `SetSourceText` (`{ inputName, text }`), `SetGenericUrlSource` / `SetSourceUrl` (`{ inputName, url }`), `SetGenericFileSource` / `SetImageFileSource` / `SetMediaFileSource` (`{ inputName, file }`):
607
+
608
+ ```js
609
+ { base: "obs", "request-type": "SetMediaFileSource", lumiaOrigin: "SetInputSettings", inputName: "Replay", file: "{{get_latest_file_from_folder=G:/Replays}}" }
610
+ ```
611
+
612
+ For custom code it's usually simpler to skip the alias and call `sendRawObsJson({ "request-type": "SetInputSettings", inputName: "Replay", inputSettings: { local_file: "..." } })` directly. All other request-types match the OBS v5 API exactly.
613
+
614
+ #### Streamlabs Desktop (`base: "slobs"`)
615
+
616
+ SLOBS uses JSON-RPC, so an action carries both a `type` (for delay routing) and an RPC `method` + `params`. Only a few types are exposed:
617
+
618
+ | type | shape | notes |
619
+ | --- | --- | --- |
620
+ | `SetCurrentScene` | `{ base: "slobs", type: "SetCurrentScene", method: "makeSceneActive", params: { resource: "ScenesService", args: ["<sceneId>"] } }` | switch scene |
621
+ | `SetCurrentSceneCollection` | `{ ..., type: "SetCurrentSceneCollection", method: "load", params: { resource: "SceneCollectionsService", args: ["<collectionId>"] } }` | |
622
+ | `SetSourceRender` | `{ ..., type: "SetSourceRender", method: "setVisibility", params: { resource: "<sceneItemId>", args: [true] } }` | scene-item visibility |
623
+
624
+ #### Meld Studio (`base: "meld"`)
625
+
626
+ `value` object; scene/layer/track/effect targets accept the friendly name or id.
627
+
628
+ | type | `value` | notes |
629
+ | --- | --- | --- |
630
+ | `SetScene` | `{ value: "<scene>" }` | show a scene |
631
+ | `SetStagedScene` | `{ value: "<scene>" }` | set the staged (preview) scene |
632
+ | `ShowStagedScene` | `{}` | promote staged to live |
633
+ | `SetLayerVisibility` | `{ value: "<layer>", on: true }` | |
634
+ | `SetLayerEffect` | `{ parent: "<layer>", value: "<effect>", on: true }` | |
635
+ | `SetAudioTrackMute` | `{ value: "<track>", on: true }` | `on: true` = muted |
636
+ | `StartRecord` / `StopRecord` / `StartStream` / `StopStream` | `{}` | |
637
+ | `Clip` / `Screenshot` | `{}` | |
638
+ | `SendCommand` | `{ value: "meld.<command>" }` | raw command, e.g. `meld.toggleStreamingAction` |
639
+
640
+ #### VLC (`base: "vlc"`)
641
+
642
+ Scalar `value`.
643
+
644
+ | type | `value` | notes |
645
+ | --- | --- | --- |
646
+ | `setPlayState` | `"play"` / `"pause"` / `"toggle"` | |
647
+ | `playlistNext` / `playlistPrevious` | — | |
648
+ | `setVolume` | `0`–`125` | percent |
649
+ | `seek` | `"+30"` / `"-30"` / `"50%"` / `"120"` | VLC seek syntax |
650
+ | `setShuffle` / `setRepeat` / `setLoop` | `"true"` / `"false"` / `"toggle"` | |
651
+ | `addToQueueAndPlay` | `"<uri or path>"` | |
652
+ | `addToQueue` | `"<uri or path>"` | |
653
+ | `playlistEmpty` | — | clears the playlist |
654
+
655
+ #### YouTube Music (`base: "youtubemusic"`)
656
+
657
+ Scalar `value`; most types take no value.
658
+
659
+ | type | `value` | notes |
660
+ | --- | --- | --- |
661
+ | `setPlayState` | `"play"` / `"pause"` / `"toggle"` | |
662
+ | `skipToNext` / `skipToPrevious` | — | |
663
+ | `toggleShuffle` / `toggleRepeat` | — | |
664
+ | `thumbsUp` / `thumbsDown` | — | |
665
+ | `setVolumeUp` / `setVolumeDown` | — | |
666
+ | `searchAndPlayImmediately` | `"<url, videoId, or playlistId>"` | YTMDesktop app only |
667
+
668
+ #### Voicemod (`base: "voicemod"`)
669
+
670
+ Scalar `value` (voice/sound name) for the lookups; the on/off toggles take no value.
671
+
672
+ | type | `value` | notes |
673
+ | --- | --- | --- |
674
+ | `voice` | `"<voice name>"` | load a specific voice |
675
+ | `random_voice` / `random_voice_favorite` | — | random voice (favorites only for the latter) |
676
+ | `sound` / `soundboard` | `"<sound name>"` | play a meme sound |
677
+ | `random_sound` / `random_soundboard` | — | random sound |
678
+ | `stop_all_sounds` | — | |
679
+ | `on` / `off` / `on_off` | — | voice changer on / off / toggle |
680
+ | `hear_my_voice_on` / `hear_my_voice_off` / `hear_my_voice_on_off` | — | |
681
+ | `background_on_off` / `mic_on_off` | — | toggle background effects / mute mic |
682
+
683
+ #### Streamer.bot (`base: "streamerbot"`)
684
+
685
+ Scalar `value` is the action name; `args` is a flat top-level JSON string.
686
+
687
+ ```js
688
+ async function() {
689
+ await actions([{ base: "streamerbot", type: "action", value: "My SB Action", args: '{"user":"{{username}}","amount":5}' }]);
690
+ done();
691
+ }
692
+ ```
693
+
694
+ #### Protocols — MQTT, WebSocket, Serial, Art-Net, Broadlink
695
+
696
+ These read their fields **flat** on the action object (no `value` wrapper).
697
+
698
+ ```js
699
+ async function() {
700
+ // MQTT: publish to a topic. host is the broker URL — mqtt://<host>:<port>
701
+ await actions([{ base: "mqtt", type: "send", host: "mqtt://192.168.1.50:1883", topic: "home/light", value: "ON" }]);
702
+
703
+ // WebSocket: send to one configured connection by its id
704
+ await actions([{ base: "websocket", type: "data", device: "<websocketId>", value: "hello" }]);
705
+
706
+ // Serial: write raw data to a port (COM3 on Windows, /dev/tty.* on macOS)
707
+ await actions([{ base: "serial", type: "write", port: "COM3", value: "L1\n" }]);
708
+
709
+ // Art-Net: set DMX channels on a universe (channel 1-512, value 0-255). universe is a string
710
+ await actions([{ base: "artnet", type: "artnet", universe: "0", values: [{ channel: 1, value: 255 }, { channel: 2, value: 128 }] }]);
711
+
712
+ // Broadlink: send a learned IR/RF code by its library id to a device
713
+ await actions([{ base: "broadlink", type: "ir", device: "<deviceId>", value: "<codeId>" }]);
714
+ done();
715
+ }
716
+ ```
717
+
718
+ #### Devices — SwitchBot, Wave Link, Camera Hub, StreamFog
719
+
720
+ All take a `value` object.
721
+
722
+ | base | type | `value` | notes |
723
+ | --- | --- | --- | --- |
724
+ | `switchbot` | `turnOn` / `turnOff` / `toggle` | `{ deviceId: "<id>" }` | |
725
+ | `wavelink` | `FILTER_SET` | `{ filterId: "<uuid>", isEnabled: true, inputId: "Wave Link Mic In 1" }` | enable/disable an input's filter; `filterId: "ALL_EFFECTS"` targets every effect on that input |
726
+ | `wavelink` | `SET_INPUT_VOLUME` | `{ inputId: "<id>", volume: 50, mixerID: "com.elgato.mix.local" }` | volume 0–100; mixer `local`/`stream`/`all` |
727
+ | `wavelink` | `MUTE_INPUT` | `{ inputId: "<id>", muted: true, mixerID: "com.elgato.mix.local" }` | |
728
+ | `wavelink` | `SET_OUTPUT_VOLUME` / `MUTE_OUTPUT` | `{ volume: 50, mixerID: "com.elgato.mix.local" }` | |
729
+ | `wavelink3` | same types as `wavelink` | same shapes | Wave Link 3.0 uses base `wavelink3`; some legacy filter/mic types are unsupported on v3 |
730
+ | `camerahub` | `SET_WEBCAM_PROPERTY` | `{ propertyID: "Brightness", propertyValue: 60 }` | all camerahub types share one property-bag `value` (`deviceID, propertyID, propertyValue, intensity, isEnabled, …`) — set only the fields the type uses. Also `SELECT_DEVICE`, `SET_NVIDIA_VIDEO_EFFECT`, `SET_LUT_EFFECT` (`intensity`), `SET_PROMPTER_PROPERTY` |
731
+ | `streamfog` | `activateLens` | `{ lensId: "<id or name>", duration: 10 }` | `lensId: "random"` picks one; also `activateOutfit` (`{ outfit, duration }`) and `disableLens` (`{}`) |
732
+
733
+ #### Plugin actions (`base: "<pluginId>"`)
734
+
735
+ Every installed plugin is also a valid base — use the plugin's id as the `base`, plus the plugin's own action `type` and a `value` object of that plugin's fields. The exact `type` and `value` come from the plugin's manifest, so the reliable way to discover them is to add the action once in the editor and read it back; the shape is always `{ base: "<pluginId>", type: "<action type>", value: { ...plugin fields } }`. Verified examples:
736
+
737
+ ```js
738
+ async function() {
739
+ // Tapo smart plug — target is the device IP, or "IP|deviceId"
740
+ await actions([{ base: "tapo_plugin", type: "tapo_turn_on", value: { target: "192.168.50.169" } }]);
741
+
742
+ // Steam — look up a game
743
+ await actions([{ base: "steam", type: "fetch_game", value: { game: "Sonic" } }]);
744
+
745
+ // ClickUp — create a task (assigneeId is comma-separated)
746
+ await actions([{ base: "clickup", type: "create_task", value: { name: "{{message}}", assigneeId: "14928785,14928776" } }]);
747
+
748
+ // ElevenLabs TTS
749
+ await actions([{ base: "elevenlabs_tts", type: "speak", value: { message: "{{message}}", voiceId: "hpp4J3VqNfWAUOO0d1Us", modelId: "eleven_multilingual_v2", volume: 100 } }]);
750
+ done();
751
+ }
752
+ ```
753
+
754
+ A plugin action is silently skipped unless its plugin is installed, connected, and enabled.
755
+
135
756
  This documentation can get extremely broad for every integration, so if you get stuck please visit our [**Discord**](https://discord.gg/R8rCaKb) to ask us any questions.
@@ -93,7 +93,7 @@ async function() {
93
93
 
94
94
  ### Get Variable
95
95
 
96
- `getVariable(name: string)`: Retrieve a variables value based on it's name
96
+ `getVariable(name: string)`: Retrieve a variables value based on it's name. Returns the raw stored value (string / number / …), or `undefined` if it isn't set. A command's runtime variable takes priority over the saved one of the same name.
97
97
 
98
98
  ```js
99
99
  async function() {
@@ -127,7 +127,7 @@ async function() {
127
127
 
128
128
  ### Get All Variables
129
129
 
130
- `getAllVariables()`: Ability to get all local and global variables with one easy call
130
+ `getAllVariables()`: Ability to get all local and global variables with one easy call. Returns a flat `{ name: value }` map of every saved variable merged with the command's runtime variables (runtime values win on a name clash).
131
131
 
132
132
  ```js
133
133
  async function() {
@@ -197,6 +197,8 @@ async function() {
197
197
 
198
198
  `getLights()`: Get the list of lights the streamer has along with it's type and id. The type and id is required to send color or power to specific lights
199
199
 
200
+ Returns an array of `{ id, name, alias, type }`. `type` is the integration key (e.g. `hue`, `govee`, `wled`, `elgato`, `virtuallights`), `alias` is your custom label, and `name` is the device's original name. Pass `{ id, type }` objects to `sendColor`'s `lights` array to target specific lights. Disconnected or disabled integrations are omitted, so an empty array means nothing is connected.
201
+
200
202
  ```js
201
203
  async function() {
202
204
  const lights = await getLights()
@@ -246,6 +248,8 @@ async function() {
246
248
 
247
249
  `getApiOptions()`: Contains information like commands, types, connections, and more
248
250
 
251
+ Returns `{ types, options }`. `types` is the list of every API command type (e.g. `setColor`, `setBrightness`, `alert`, `tts`, `chatCommand`, `chatbotCommand`, `twitchPoints`, `kickPoints`, studio scene/theme/animation, plus value-less system ops). `options` is keyed by those same types: value-less ops are `null`, while the command/alert/tts/studio types carry a `{ values: [...] }` list of the names you can use.
252
+
249
253
  ```js
250
254
  async function() {
251
255
  const lights = await getApiOptions()
@@ -254,7 +258,7 @@ async function() {
254
258
 
255
259
  ### Get Commands
256
260
 
257
- `getCommands({ formatted?: boolean; onlyOn?: boolean; onlyUser?: boolean })`: Returns the list of chat command names. Pass `onlyOn: true` to only include commands that are enabled and shown in the commands list, `onlyUser: true` to only include commands the current user has access to (based on their user levels), and `formatted: true` to get a single comma separated string instead of an array
261
+ `getCommands({ formatted?: boolean; onlyOn?: boolean; onlyUser?: boolean })`: Returns the list of chat and chatbot command names. Pass `onlyOn: true` to only include commands that are enabled and shown in the commands list, `onlyUser: true` to only include commands the current user has access to (based on their user levels), and `formatted: true` to get a single comma separated string instead of an array
258
262
 
259
263
  ```js
260
264
  async function() {
@@ -486,7 +490,7 @@ async function() {
486
490
 
487
491
  ### Get Token
488
492
 
489
- `getToken(connection: "twitch" | "twitchChatbot" | "youtube" | "facebook" | "streamlabs" | "streamelements" | "treatstream" | "tipeeestream" | "tiltify" | "patreon" | "woocommerce" | "discord" | "twitter" | "spotify" | "pulsoid" | "wyze" | "homeassistant" | "govee" | "wled" )`: When you need to call a request that we don't directly support you can get the access token from Lumia before making the call. This is helpful for things where you need to call for instance the Twitch API, but you don't want to handle tokens and refreshing inside of your scripts. More examples of this below
493
+ `getToken(connection: "twitch" | "twitchChatbot" | "youtube" | "facebook" | "streamlabs" | "streamelements" | "treatstream" | "tipeeestream" | "tiltify" | "patreon" | "woocommerce" | "discord" | "twitter" | "spotify" | "pulsoid" | "wyze" | "homeassistant" | "govee" | "wled" )`: When you need to call a request that we don't directly support you can get the access token from Lumia before making the call. This is helpful for things where you need to call for instance the Twitch API, but you don't want to handle tokens and refreshing inside of your scripts. More examples of this below. Returns the access-token string, or `null` / `undefined` if that connection isn't authorized.
490
494
 
491
495
  ```js
492
496
  async function() {
@@ -497,7 +501,7 @@ async function() {
497
501
 
498
502
  ### Get Client ID For Twitch
499
503
 
500
- `getClientId(connection: "twitch")`: When calling requests with Twitch's API you will need to pass in a Client-ID to the headers. We provide a Client ID that you can use to call the different api's with the permissions the user has selected. Check out [Twitch's developers docs](https://dev.twitch.tv/docs/api/reference) to learn what you can do
504
+ `getClientId(connection: "twitch")`: When calling requests with Twitch's API you will need to pass in a Client-ID to the headers. We provide a Client ID that you can use to call the different api's with the permissions the user has selected. Check out [Twitch's developers docs](https://dev.twitch.tv/docs/api/reference) to learn what you can do. Returns the Twitch Client-ID string; only `"twitch"` is supported and any other value returns `null`.
501
505
 
502
506
  ```js
503
507
  async function() {
@@ -110,12 +110,27 @@ Include these two helper aliases in your embeddings so the model can quickly map
110
110
  "min": { "type": "number" },
111
111
  "max": { "type": "number" },
112
112
  "visibleIf": {
113
- "type": "object",
114
- "required": ["key", "equals"],
115
- "properties": {
116
- "key": { "type": "string" },
117
- "equals": {}
118
- }
113
+ "oneOf": [
114
+ {
115
+ "type": "object",
116
+ "required": ["key", "equals"],
117
+ "properties": {
118
+ "key": { "type": "string" },
119
+ "equals": {}
120
+ }
121
+ },
122
+ {
123
+ "type": "array",
124
+ "items": {
125
+ "type": "object",
126
+ "required": ["key", "equals"],
127
+ "properties": {
128
+ "key": { "type": "string" },
129
+ "equals": {}
130
+ }
131
+ }
132
+ }
133
+ ]
119
134
  },
120
135
  "hidden": { "type": "boolean" }
121
136
  },
@@ -399,7 +399,7 @@ A field object can now contain these properties:
399
399
  | **`value`** | ❌ | Default value that appears the first time the user opens the overlay (also pre populates `Overlay.data`). Omit it to leave the field blank/unchecked on first load. | `"value": 18` |
400
400
  | **`options`** | ☑️\* | Key value map of selectable choices. Required **only** for `dropdown`, `multiselect` and `slider`; ignored for other types. For `slider`, `options` supports `step`, `min`, `max`, `prefix`, `suffix`. | `"options": { "step": 5, "min": 0, "max": 100 }` |
401
401
  | **`order`** | ❌ | **Display order priority**. Fields with lower numbers appear first. Fields without `order` appear after ordered fields, sorted alphabetically by key. | `"order": 1` |
402
- | **`visibleIf`** | ❌ | **Conditional render rule**. Field is shown **only if** `Overlay.data[visibleIf.key]` strictly equals one of the values in `visibleIf.equals`. | `"visibleIf": { "key": "targetKey", "equals": ["yes", "maybe"] }` |
402
+ | **`visibleIf`** | ❌ | **Conditional render rule**. Field is shown **only if** `Overlay.data[visibleIf.key]` strictly equals one of the values in `visibleIf.equals`. Pass an **array of rules** to require that **every** rule matches (AND across keys). | `"visibleIf": [{ "key": "targetKey", "equals": ["yes", "maybe"] }, { "key": "mode", "equals": "advanced" }]` |
403
403
  | **`hidden`** | ❌ | **Hard-hide rule.** When set to `true`, the field is **never displayed** in the Configs sidebar, preventing end users from altering it. The value still flows into `Overlay.data`, so the overlay can rely on it internally.<br>Useful for locking event subscriptions or other advanced settings. | `"hidden": true` |
404
404
 
405
405
  Additional properties for text fields (`type: "input"` and `type: "textarea"`):
@@ -761,10 +761,13 @@ In this example, the fields would appear in this order:
761
761
  ### Visible If Conditional Fields
762
762
 
763
763
  The `visibleIf` field is a conditional statement that determines whether a field should be visible or not based on a condition.
764
- The `visibleIf` field is a JSON object with two keys: `key` and `equals`.
764
+ The `visibleIf` field can be **either**:
765
+
766
+ - a single rule object with two keys, `key` and `equals`, or
767
+ - an **array of rule objects**, in which case the field is shown only when **every** rule matches (logical AND). Use the array form to gate a field on more than one key at once.
765
768
 
766
769
  The `key` key is a string that represents the name of the field whose value should be used in the conditional statement.
767
- The `equals` key is a string, number, boolean, or an array that represents the value of the field whose value should be used in the conditional statement.
770
+ The `equals` key is a string, number, boolean, or an array that represents the value(s) of that field that make the condition match. When `equals` is an array, the rule matches if the field's value is one of the listed values (and for `multiselect` fields, if any selected value is in the list).
768
771
 
769
772
  #### Single Primitive Equals Example
770
773
 
@@ -804,6 +807,35 @@ The `equals` key is a string, number, boolean, or an array that represents the v
804
807
  }
805
808
  ```
806
809
 
810
+ #### Multiple Keys (Array of Rules) Example
811
+
812
+ Pass an array of rule objects to require that **every** rule matches before the
813
+ field shows. Here `subcolor` only appears when **both** `color` and `alerts`
814
+ are set to one of `red`, `blue`, or `green`:
815
+
816
+ ```json
817
+ {
818
+ "color": {
819
+ "type": "input",
820
+ "label": "Colors"
821
+ },
822
+ "subcolor": {
823
+ "type": "input",
824
+ "label": "Subcolors",
825
+ "visibleIf": [
826
+ {
827
+ "key": "color",
828
+ "equals": ["red", "blue", "green"]
829
+ },
830
+ {
831
+ "key": "alerts",
832
+ "equals": ["red", "blue", "green"]
833
+ }
834
+ ]
835
+ }
836
+ }
837
+ ```
838
+
807
839
  ---
808
840
 
809
841
  ## 📊 Data Tab
@@ -53,7 +53,7 @@ export enum ConfigsFieldType {
53
53
  }
54
54
 
55
55
  /**
56
- * Conditional render rule for a config field.
56
+ * A single conditional render rule for a config field.
57
57
  * The field is shown only when `Overlay.data[key]` strictly equals one of `equals`
58
58
  * (scalar) or intersects with `equals` (when either side is an array).
59
59
  *
@@ -68,6 +68,23 @@ export interface ConfigVisibleIf {
68
68
  equals: string | number | boolean | Array<string | number | boolean>;
69
69
  }
70
70
 
71
+ /**
72
+ * Conditional render rule, or a list of rules. When an array is supplied, the
73
+ * field is shown only when **every** rule matches (logical AND), letting you
74
+ * gate a field on several keys at once.
75
+ *
76
+ * @example
77
+ * // single key
78
+ * { key: 'mode', equals: 'advanced' }
79
+ * @example
80
+ * // multiple keys — all must match
81
+ * [
82
+ * { key: 'color', equals: ['red', 'blue', 'green'] },
83
+ * { key: 'alerts', equals: ['red', 'blue', 'green'] },
84
+ * ]
85
+ */
86
+ export type ConfigVisibleIfRule = ConfigVisibleIf | ConfigVisibleIf[];
87
+
71
88
  /** Properties common to every config field, regardless of `type`. */
72
89
  export interface BaseConfigField {
73
90
  /** Human-readable name shown next to the control in the Configs sidebar. */
@@ -79,9 +96,10 @@ export interface BaseConfigField {
79
96
  order?: number;
80
97
  /**
81
98
  * Conditional render rule. When set, the field renders only when
82
- * `Overlay.data[visibleIf.key]` matches `visibleIf.equals`.
99
+ * `Overlay.data[visibleIf.key]` matches `visibleIf.equals`. Pass an array of
100
+ * rules to require that **every** rule matches (logical AND across keys).
83
101
  */
84
- visibleIf?: ConfigVisibleIf;
102
+ visibleIf?: ConfigVisibleIfRule;
85
103
  /**
86
104
  * Hard-hide rule. When `true`, the field is never displayed in the Configs
87
105
  * sidebar, but its `value` still flows into `Overlay.data` for internal use.
@@ -35,7 +35,7 @@ Source of truth (in order): this file > `gpt-instructions-extended.md` (long-for
35
35
  - **HTML**: body content only. Stable IDs/classes. No inline `<script>` or `<style>`.
36
36
  - **CSS**: stylesheet only. Unquoted CSS variables for colors/sizes/numbers. Quoted only where CSS requires a string (`font-family: "{{font}}";`).
37
37
  - **JS**: top-level JS. Use `Overlay.data` for Data values. Use `textContent`/`createElement`/`appendChild` for any chat, alert, or user-generated text — never `innerHTML` with user input. `fetch` is allowed.
38
- - **Configs**: field types: `input`, `textarea`, `number`, `checkbox`, `dropdown`, `multiselect`, `colorpicker`, `fontpicker`, `slider`, `imageupload`, `soundupload`, `videoupload`, `actionbutton`. Required: `type`, `label`. Use `value` for defaults. `dropdown`/`multiselect` need `options`. `slider` needs `options.min`/`max` (usually `step`/`prefix`/`suffix`). `imageupload`/`soundupload`/`videoupload` accept an `accept` MIME hint. `actionbutton` has no `value` — bind it via `Overlay.on('configAction', ({ key }) => …)`. Use `order` to control sidebar order. Use `visibleIf` for conditional fields. Keys must be machine-friendly (no spaces).
38
+ - **Configs**: field types: `input`, `textarea`, `number`, `checkbox`, `dropdown`, `multiselect`, `colorpicker`, `fontpicker`, `slider`, `imageupload`, `soundupload`, `videoupload`, `actionbutton`. Required: `type`, `label`. Use `value` for defaults. `dropdown`/`multiselect` need `options`. `slider` needs `options.min`/`max` (usually `step`/`prefix`/`suffix`). `imageupload`/`soundupload`/`videoupload` accept an `accept` MIME hint. `actionbutton` has no `value` — bind it via `Overlay.on('configAction', ({ key }) => …)`. Use `order` to control sidebar order. Use `visibleIf` for conditional fields — either a single `{ key, equals }` rule or an array of rules (the field shows only when every rule matches). Keys must be machine-friendly (no spaces).
39
39
  - **Data**: every Configs key must have a matching Data key with a matching default. Data can hold internal values that aren't in Configs (rare).
40
40
 
41
41
  ## Variables
@@ -53,7 +53,7 @@ export enum ConfigsFieldType {
53
53
  }
54
54
 
55
55
  /**
56
- * Conditional render rule for a config field.
56
+ * A single conditional render rule for a config field.
57
57
  * The field is shown only when `Overlay.data[key]` strictly equals one of `equals`
58
58
  * (scalar) or intersects with `equals` (when either side is an array).
59
59
  *
@@ -68,6 +68,23 @@ export interface ConfigVisibleIf {
68
68
  equals: string | number | boolean | Array<string | number | boolean>;
69
69
  }
70
70
 
71
+ /**
72
+ * Conditional render rule, or a list of rules. When an array is supplied, the
73
+ * field is shown only when **every** rule matches (logical AND), letting you
74
+ * gate a field on several keys at once.
75
+ *
76
+ * @example
77
+ * // single key
78
+ * { key: 'mode', equals: 'advanced' }
79
+ * @example
80
+ * // multiple keys — all must match
81
+ * [
82
+ * { key: 'color', equals: ['red', 'blue', 'green'] },
83
+ * { key: 'alerts', equals: ['red', 'blue', 'green'] },
84
+ * ]
85
+ */
86
+ export type ConfigVisibleIfRule = ConfigVisibleIf | ConfigVisibleIf[];
87
+
71
88
  /** Properties common to every config field, regardless of `type`. */
72
89
  export interface BaseConfigField {
73
90
  /** Human-readable name shown next to the control in the Configs sidebar. */
@@ -79,9 +96,10 @@ export interface BaseConfigField {
79
96
  order?: number;
80
97
  /**
81
98
  * Conditional render rule. When set, the field renders only when
82
- * `Overlay.data[visibleIf.key]` matches `visibleIf.equals`.
99
+ * `Overlay.data[visibleIf.key]` matches `visibleIf.equals`. Pass an array of
100
+ * rules to require that **every** rule matches (logical AND across keys).
83
101
  */
84
- visibleIf?: ConfigVisibleIf;
102
+ visibleIf?: ConfigVisibleIfRule;
85
103
  /**
86
104
  * Hard-hide rule. When `true`, the field is never displayed in the Configs
87
105
  * sidebar, but its `value` still flows into `Overlay.data` for internal use.
@@ -682,7 +682,7 @@ export const LumiaAlertFriendlyValues = {
682
682
  [LumiaAlertValues.YOUTUBE_SUBSCRIBER]: 'Youtube Subscriber',
683
683
  [LumiaAlertValues.YOUTUBE_SUPERCHAT]: 'Youtube Superchat',
684
684
  [LumiaAlertValues.YOUTUBE_SUPERSTICKER]: 'Youtube Supersticker',
685
- [LumiaAlertValues.YOUTUBE_GIFTS]: 'Youtube Gifts',
685
+ [LumiaAlertValues.YOUTUBE_GIFTS]: 'Youtube Gifts (Jewels)',
686
686
  [LumiaAlertValues.YOUTUBE_SESSION_GIFTS]: 'Youtube Session Gifts',
687
687
  [LumiaAlertValues.YOUTUBE_ENTRANCE]: 'Youtube Entrance',
688
688
  [LumiaAlertValues.YOUTUBE_LIKE]: 'Youtube Like',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumiastream/lumia-types",
3
- "version": "3.9.2",
3
+ "version": "3.9.4",
4
4
  "description": "",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/esm/index.js",