@zaflun/lumio-agent-skill 0.1.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  installSkillFiles
4
- } from "../chunk-I2USQPPS.js";
4
+ } from "../chunk-SSYJXSJX.js";
5
5
 
6
6
  // src/bin/lumio-skills.ts
7
7
  import { resolve } from "path";
@@ -13,7 +13,7 @@ Lumio Extensions are sandboxed React apps that run inside a 3-layer iframe sandb
13
13
 
14
14
  \`\`\`
15
15
  3-Layer Sandbox:
16
- Host (lumio.vision / OBS browser source)
16
+ Host (lumio.vision / browser source)
17
17
  \u2514\u2500\u2500 Supervisor iframe (supervisor.extension.lumio.vision)
18
18
  \u2514\u2500\u2500 Extension Runtime iframe (ext.lumio.vision)
19
19
  \u2514\u2500\u2500 Web Worker (server function execution)
@@ -23,7 +23,7 @@ Lumio Extensions are sandboxed React apps that run inside a 3-layer iframe sandb
23
23
  Extension Project:
24
24
  lumio.config.json <- Extension manifest (ID, name, category, permissions, targets, config_schema)
25
25
  src/editor.tsx <- Dashboard editor panel (sidebar in overlay editor)
26
- src/layer.tsx <- OBS overlay surface (transparent, read-only inputs)
26
+ src/layer.tsx <- Stream overlay surface (transparent, read-only inputs)
27
27
  src/interactive.tsx <- Audience interaction page (optional)
28
28
  server/schema.ts <- DB schema (defineTable + validators)
29
29
  server/functions.ts <- Server functions (queries, mutations, actions)
@@ -52,7 +52,7 @@ Extension Project:
52
52
  | Target | Where | Purpose |
53
53
  |--------|-------|---------|
54
54
  | \`editor\` | Dashboard overlay editor sidebar | Config UI, settings, content management |
55
- | \`layer\` | OBS Browser Source (transparent) | Visual overlay in stream |
55
+ | \`layer\` | Browser Source (transparent) | Visual overlay in stream |
56
56
  | \`interactive\` | Standalone page (\`/ext/{slug}\`) | Viewer interaction (votes, games) |
57
57
 
58
58
  All three surfaces share storage via \`useExtensionStorage()\` \u2014 changes in editor propagate to layer in real-time.
@@ -61,9 +61,8 @@ All three surfaces share storage via \`useExtensionStorage()\` \u2014 changes in
61
61
 
62
62
  \`\`\`json
63
63
  {
64
- "$schema": "https://docs.lumio.vision/schemas/lumio.config.schema.json",
64
+ "$schema": "https://lumio.vision/schemas/lumio.config.schema.json",
65
65
  "extensionId": "uuid",
66
- "slug": "k8f2x-my-extension",
67
66
  "name": "My Extension",
68
67
  "description": "Description",
69
68
  "category": "widget",
@@ -77,12 +76,10 @@ All three surfaces share storage via \`useExtensionStorage()\` \u2014 changes in
77
76
  },
78
77
  "visibility": "public",
79
78
  "pricing": { "type": "free" },
80
- "config_schema": {
81
- "fields": [
82
- { "key": "title", "type": "string", "label": "Widget Title", "default": "My Widget" },
83
- { "key": "showBorder", "type": "boolean", "label": "Show Border", "default": true }
84
- ]
85
- }
79
+ "config_schema": [
80
+ { "key": "title", "type": "string", "label": "Widget Title", "default_value": "My Widget", "required": true },
81
+ { "key": "showBorder", "type": "boolean", "label": "Show Border", "default_value": true, "required": false }
82
+ ]
86
83
  }
87
84
  \`\`\`
88
85
 
@@ -147,7 +144,7 @@ lumio deploy
147
144
 
148
145
  **VisionStyle** is a typed CSS subset (allowed: flexbox, grid, \`position: relative\` / \`absolute\`, sizing, spacing, colors, border, typography, transitions; blocked: \`fixed\`, \`sticky\`, CSS expressions).
149
146
 
150
- On OBS layer: input components become read-only (buttons hidden, toggles show on/off text).
147
+ On the layer surface: input components become read-only (buttons hidden, toggles show on/off text).
151
148
 
152
149
  See \`components.md\` for full props and examples.
153
150
 
@@ -164,7 +161,7 @@ See \`components.md\` for full props and examples.
164
161
  | \`useLumioConfig()\` | \`[config, setConfig]\` | Extension config read/write (uses config_schema fields) |
165
162
  | \`useLumioTheme()\` | \`theme\` | Current theme (dark/light/custom) |
166
163
  | \`useLumioIdentity()\` | \`{ userId, accountId, role }\` | Current user context |
167
- | \`useLumioAction(action, params)\` | \`{ execute, data, isLoading, error }\` | Trigger Lumio actions \u2014 type-safe: \`action\` is \`LumioActionType\` |
164
+ | \`useLumioAction(action)\` | \`{ execute, data, isLoading, error }\` | Trigger Lumio actions \u2014 type-safe: \`action\` is \`LumioActionType\` |
168
165
  | \`useQuery(fn, params?)\` | \`{ data, isLoading, error, refetch }\` | Call server query |
169
166
  | \`useMutation(fn)\` | \`{ mutate, mutateAsync, data, isLoading, error }\` | Call server mutation/action |
170
167
  | \`useAnimation(preset, options?)\` | \`animationProps\` | SDK animation presets for \`Animated\` component |
@@ -200,27 +197,122 @@ export const fetchScores = action({
200
197
  **Extension Actions** via \`useLumioAction()\` (21 total, \`action\` is \`LumioActionType\`):
201
198
  \`\`\`tsx
202
199
  // Trigger platform actions (requires matching permission in lumio.config.json)
203
- const { execute: sendChat } = useLumioAction("chat:send", {});
204
- const { execute: banUser } = useLumioAction("chat:ban", {});
205
- const { execute: unbanUser } = useLumioAction("chat:unban", {});
206
- const { execute: timeoutUser } = useLumioAction("chat:timeout", {});
207
- const { execute: setScene } = useLumioAction("obs:set_scene", {});
208
- const { execute: startRecording } = useLumioAction("obs:start_recording", {});
209
- const { execute: stopRecording } = useLumioAction("obs:stop_recording", {});
210
- const { execute: emitEvent } = useLumioAction("events:emit", {});
211
- const { execute: updateLayer } = useLumioAction("overlay:update_layer", {});
212
- const { execute: spotifyPlay } = useLumioAction("spotify:play", {});
213
- const { execute: spotifyPause } = useLumioAction("spotify:pause", {});
214
- const { execute: spotifySkip } = useLumioAction("spotify:skip", {});
215
- const { execute: spotifySeek } = useLumioAction("spotify:seek", {});
216
- const { execute: spotifyVolume } = useLumioAction("spotify:volume", {});
217
- const { execute: spotifyShuffle } = useLumioAction("spotify:shuffle", {});
218
- const { execute: spotifyRepeat } = useLumioAction("spotify:repeat", {});
219
- const { execute: spotifyQueueAdd } = useLumioAction("spotify:queue_add", {});
200
+ const { execute: sendChat } = useLumioAction("chat:send");
201
+ const { execute: banUser } = useLumioAction("chat:ban");
202
+ const { execute: unbanUser } = useLumioAction("chat:unban");
203
+ const { execute: timeoutUser } = useLumioAction("chat:timeout");
204
+ const { execute: setScene } = useLumioAction("obs:set_scene");
205
+ const { execute: startRecording } = useLumioAction("obs:start_recording");
206
+ const { execute: stopRecording } = useLumioAction("obs:stop_recording");
207
+ const { execute: emitEvent } = useLumioAction("events:emit");
208
+ const { execute: updateLayer } = useLumioAction("overlay:update_layer");
209
+ const { execute: spotifyPlay } = useLumioAction("spotify:play");
210
+ const { execute: spotifyPause } = useLumioAction("spotify:pause");
211
+ const { execute: spotifySkip } = useLumioAction("spotify:skip");
212
+ const { execute: spotifySeek } = useLumioAction("spotify:seek");
213
+ const { execute: spotifyVolume } = useLumioAction("spotify:volume");
214
+ const { execute: spotifyShuffle } = useLumioAction("spotify:shuffle");
215
+ const { execute: spotifyRepeat } = useLumioAction("spotify:repeat");
216
+ const { execute: spotifyQueueAdd } = useLumioAction("spotify:queue_add");
220
217
  \`\`\`
221
218
 
222
219
  See \`server-functions.md\` for schema definition, storage scoping, and advanced patterns.
223
220
 
221
+ ## Bot Modules
222
+
223
+ Extensions with category \`bot_module\` register handler-based server functions that respond to chat messages and platform events. Bot module handlers use six trigger types.
224
+
225
+ ### Handler Types
226
+
227
+ | Handler | Trigger | Description |
228
+ |---------|---------|-------------|
229
+ | \`command(prefix, handler)\` | Chat command (e.g. \`!rank\`) | Fires when a message starts with the prefix |
230
+ | \`keyword(words, handler)\` | Keyword match | Fires when a message contains any listed keyword |
231
+ | \`pattern(regex, handler)\` | Regex match | Fires when a message matches the regex |
232
+ | \`event(type, handler)\` | Platform event | Fires on a specific event (e.g. \`twitch:follower\`) |
233
+ | \`timer(name, intervalSecs, handler)\` | Recurring timer | Fires every N seconds. Name must be unique per extension. |
234
+ | \`moderate(handler)\` | Every message | Runs on every chat message for moderation decisions |
235
+
236
+ ### Handler Context
237
+
238
+ Bot module handlers receive an extended \`ctx\` with chat-specific properties:
239
+
240
+ \`\`\`typescript
241
+ import { command } from "@zaflun/lumio-sdk/server";
242
+
243
+ export const rankCmd = command("!rank", {
244
+ handler: async (ctx, message) => {
245
+ // ctx.user \u2014 chat user who sent the message
246
+ // { platform, platform_user_id, display_name, roles }
247
+ // roles: { moderator, vip, subscriber, broadcaster }
248
+
249
+ // ctx.auth \u2014 extension identity
250
+ // { userId, accountId, extensionId, installId }
251
+
252
+ // ctx.cache \u2014 in-memory KV cache scoped to install (TTL-based)
253
+ await ctx.cache.set("last_rank_user", ctx.user.display_name, { ttl: 300 });
254
+ const cached = await ctx.cache.get("last_rank_user");
255
+
256
+ // ctx.defer() \u2014 schedule a follow-up after handler returns
257
+ ctx.defer(async () => {
258
+ await ctx.db.insert("rank_log", { user: ctx.user.display_name });
259
+ });
260
+
261
+ return { action: "send_message", message: \`@\${ctx.user.display_name} your rank is Gold!\` };
262
+ },
263
+ });
264
+ \`\`\`
265
+
266
+ ### \`ctx.user\` vs \`ctx.auth\`
267
+
268
+ - \`ctx.user\` is the **chat user** who triggered the handler (their platform identity, display name, and role flags). Only available in bot module handlers.
269
+ - \`ctx.auth\` is the **extension identity** (which extension, which install, which account). Available in all server functions.
270
+
271
+ ### Timer Naming
272
+
273
+ Timer names must be unique within the extension. The name is used as the timer's identity for scheduling and deduplication. Use descriptive names:
274
+
275
+ \`\`\`typescript
276
+ timer("ad_reminder", 900, { handler: async (ctx) => { ... } }); // Good
277
+ timer("t1", 900, { handler: async (ctx) => { ... } }); // Bad \u2014 unclear
278
+ \`\`\`
279
+
280
+ ### \`ctx.cache\`
281
+
282
+ The cache is an in-memory key-value store scoped to the extension install. Use it for rate limiting, cooldowns, and short-lived state that does not need to survive Worker restarts.
283
+
284
+ \`\`\`typescript
285
+ // Set with TTL (seconds)
286
+ await ctx.cache.set("cooldown:ban:" + userId, "1", { ttl: 60 });
287
+
288
+ // Get (returns null if expired or missing)
289
+ const val = await ctx.cache.get("cooldown:ban:" + userId);
290
+
291
+ // Delete
292
+ await ctx.cache.delete("cooldown:ban:" + userId);
293
+ \`\`\`
294
+
295
+ ### \`ctx.defer()\`
296
+
297
+ Schedule work to run after the handler returns its response. Useful for logging, analytics, or secondary actions that should not delay the primary response.
298
+
299
+ \`\`\`typescript
300
+ export const modCheck = moderate({
301
+ handler: async (ctx, message) => {
302
+ if (isToxic(message.text)) {
303
+ ctx.defer(async () => {
304
+ await ctx.db.insert("mod_log", {
305
+ user: ctx.user.platform_user_id,
306
+ action: "timeout",
307
+ reason: "toxic message",
308
+ });
309
+ });
310
+ return { action: "timeout", duration: 300, message: "Please be respectful." };
311
+ }
312
+ return { action: "none" };
313
+ },
314
+ });\`\`\`
315
+
224
316
  ## CLI Commands
225
317
 
226
318
  12 commands provided by \`@zaflun/lumio-cli\`:
@@ -307,7 +399,7 @@ addItem({ text: "New item" });
307
399
 
308
400
  ### Lumio actions
309
401
  \`\`\`tsx
310
- const { execute } = useLumioAction("chat:send", {});
402
+ const { execute } = useLumioAction("chat:send");
311
403
  execute({ message: "Hello from my extension!" });
312
404
  \`\`\`
313
405
 
@@ -475,7 +567,7 @@ import { Image } from "@zaflun/lumio-sdk";
475
567
 
476
568
  ## Input Components
477
569
 
478
- **Important:** On OBS layer surface, all input components become read-only. Buttons are hidden, toggles show "On"/"Off" text, dropdowns show the selected value.
570
+ **Important:** On the layer surface, all input components become read-only. Buttons are hidden, toggles show "On"/"Off" text, dropdowns show the selected value.
479
571
 
480
572
  ### Button
481
573
 
@@ -655,7 +747,7 @@ const exitAnim = useAnimation("fade-out", { duration: 200 });
655
747
 
656
748
  **Best practices:**
657
749
  - Always use SDK presets instead of raw CSS animations for entrance/exit effects \u2014 the reconciler maps them to allowed VisionStyle \`animation\` properties
658
- - Keep animation durations short on the OBS layer surface (< 500ms) to avoid frame drops
750
+ - Keep animation durations short on the layer surface (< 500ms) to avoid frame drops
659
751
  - Avoid animating layout-affecting properties (\`width\`, \`height\`) on the layer; prefer \`opacity\` and \`transform\`
660
752
 
661
753
  ## VisionStyle
@@ -775,15 +867,12 @@ For complex logic that cannot be expressed declaratively. Runs in V8 isolate san
775
867
  \`\`\`typescript
776
868
  import { query, mutation, action, v } from "@zaflun/lumio-sdk/server";
777
869
 
778
- // Read-only query
779
- export const getLeaderboard = query({
780
- args: { limit: v.number().optional() },
870
+ // Read-only query \u2014 look up a single row by ID
871
+ export const getScore = query({
872
+ args: { id: v.string() },
781
873
  handler: async (ctx, args) => {
782
- const rows = await ctx.db.get("scores", {
783
- order: { score: "desc" },
784
- limit: args.limit ?? 10,
785
- });
786
- return rows;
874
+ const row = await ctx.db.get("scores", args.id);
875
+ return row;
787
876
  },
788
877
  });
789
878
 
@@ -820,7 +909,7 @@ Handler-based functions run inside a V8 isolate Web Worker. The \`ctx\` object p
820
909
 
821
910
  | Property | Description |
822
911
  |----------|-------------|
823
- | \`ctx.db.get(table, options?)\` | Read rows from extension DB schema |
912
+ | \`ctx.db.get(table, id)\` | Read a single row by ID from extension DB schema |
824
913
  | \`ctx.db.insert(table, data)\` | Insert row |
825
914
  | \`ctx.db.patch(table, id, data)\` | Update row |
826
915
  | \`ctx.db.delete(table, id)\` | Delete row |
@@ -856,19 +945,19 @@ Extension Actions let extensions trigger Lumio platform capabilities on behalf o
856
945
  import { useLumioAction } from "@zaflun/lumio-sdk";
857
946
 
858
947
  // Send a chat message on the connected platform
859
- const { execute: sendChat, isLoading, error } = useLumioAction("chat:send", {});
948
+ const { execute: sendChat, isLoading, error } = useLumioAction("chat:send");
860
949
  sendChat({ message: "Hello from the extension!" });
861
950
 
862
951
  // Switch OBS scene (requires OBS WebSocket integration)
863
- const { execute: setScene } = useLumioAction("obs:set_scene", {});
952
+ const { execute: setScene } = useLumioAction("obs:set_scene");
864
953
  setScene({ sceneName: "Main Scene" });
865
954
 
866
955
  // Emit a custom event into the Lumio event bus
867
- const { execute: emitEvent } = useLumioAction("events:emit", {});
956
+ const { execute: emitEvent } = useLumioAction("events:emit");
868
957
  emitEvent({ type: "extension:custom", payload: { score: 42 } });
869
958
 
870
959
  // Update a specific layer in the overlay
871
- const { execute: updateLayer } = useLumioAction("overlay:update_layer", {});
960
+ const { execute: updateLayer } = useLumioAction("overlay:update_layer");
872
961
  updateLayer({ layerId: "uuid", visible: true });
873
962
  \`\`\`
874
963
 
@@ -877,10 +966,14 @@ updateLayer({ layerId: "uuid", visible: true });
877
966
  | Action | Permission required | Description |
878
967
  |--------|--------------------|----|
879
968
  | \`chat:send\` | \`chat:send\` | Send chat message on connected platform |
969
+ | \`chat:delete\` | \`chat:delete\` | Delete a chat message |
880
970
  | \`chat:ban\` | \`chat:ban\` | Ban a user from chat |
881
971
  | \`chat:unban\` | \`chat:ban\` | Unban a user from chat |
882
972
  | \`chat:timeout\` | \`chat:ban\` | Timeout a user from chat |
883
973
  | \`obs:set_scene\` | \`obs:control\` | Switch OBS scene |
974
+ | \`obs:set_source_visible\` | \`obs:control\` | Show or hide an OBS source |
975
+ | \`obs:start_stream\` | \`obs:control\` | Start OBS streaming |
976
+ | \`obs:stop_stream\` | \`obs:control\` | Stop OBS streaming |
884
977
  | \`obs:start_recording\` | \`obs:control\` | Start OBS recording |
885
978
  | \`obs:stop_recording\` | \`obs:control\` | Stop OBS recording |
886
979
  | \`events:emit\` | \`events:write\` | Emit event into Lumio event bus |
@@ -895,6 +988,129 @@ updateLayer({ layerId: "uuid", visible: true });
895
988
  | \`spotify:queue_add\` | \`spotify:control\` | Add track to Spotify queue |
896
989
 
897
990
  Actions are proxied through the Lumio API \u2014 the extension never accesses OAuth tokens or platform credentials directly.
991
+
992
+ ## Bot Module Handlers
993
+
994
+ Extensions with category \`bot_module\` export handler wrappers that register triggers. Each wrapper defines when the handler fires and what context it receives.
995
+
996
+ ### \`command(prefix, options)\`
997
+
998
+ Fires when a chat message starts with the given prefix.
999
+
1000
+ \`\`\`typescript
1001
+ import { command } from "@zaflun/lumio-sdk/server";
1002
+
1003
+ export const hello = command("!hello", {
1004
+ handler: async (ctx, message) => {
1005
+ return { action: "send_message", message: \`Hello @\${ctx.user.display_name}!\` };
1006
+ },
1007
+ });
1008
+ \`\`\`
1009
+
1010
+ ### \`keyword(words, options)\`
1011
+
1012
+ Fires when a chat message contains any of the listed keywords (case-insensitive).
1013
+
1014
+ \`\`\`typescript
1015
+ import { keyword } from "@zaflun/lumio-sdk/server";
1016
+
1017
+ export const giveaway = keyword(["giveaway", "enter", "join"], {
1018
+ handler: async (ctx, message) => {
1019
+ await ctx.db.insert("entries", { user_id: ctx.user.platform_user_id });
1020
+ return { action: "send_message", message: \`@\${ctx.user.display_name} you are entered!\` };
1021
+ },
1022
+ });
1023
+ \`\`\`
1024
+
1025
+ ### \`pattern(regex, options)\`
1026
+
1027
+ Fires when a chat message matches the given regex.
1028
+
1029
+ \`\`\`typescript
1030
+ import { pattern } from "@zaflun/lumio-sdk/server";
1031
+
1032
+ export const scoreParser = pattern(/^!score\\s+(\\w+)\\s+(\\d+)$/, {
1033
+ handler: async (ctx, message, matches) => {
1034
+ const [, game, score] = matches;
1035
+ await ctx.db.insert("scores", { game, score: parseInt(score, 10), user: ctx.user.display_name });
1036
+ return { action: "send_message", message: \`Score recorded: \${game} = \${score}\` };
1037
+ },
1038
+ });
1039
+ \`\`\`
1040
+
1041
+ ### \`event(eventType, options)\`
1042
+
1043
+ Fires on a platform event. The \`eventType\` is a \`LumioEventType\` string.
1044
+
1045
+ \`\`\`typescript
1046
+ import { event } from "@zaflun/lumio-sdk/server";
1047
+
1048
+ export const welcomeFollower = event("twitch:follower", {
1049
+ handler: async (ctx, eventData) => {
1050
+ return { action: "send_message", message: \`Welcome @\${eventData.user_name}! Thanks for the follow!\` };
1051
+ },
1052
+ });
1053
+ \`\`\`
1054
+
1055
+ ### \`timer(name, intervalSecs, options)\`
1056
+
1057
+ Fires on a recurring interval. The name must be unique within the extension.
1058
+
1059
+ \`\`\`typescript
1060
+ import { timer } from "@zaflun/lumio-sdk/server";
1061
+
1062
+ export const reminder = timer("social_reminder", 900, {
1063
+ handler: async (ctx) => {
1064
+ return { action: "send_message", message: "Follow us on Twitter! twitter.com/example" };
1065
+ },
1066
+ });
1067
+ \`\`\`
1068
+
1069
+ ### \`moderate(options)\`
1070
+
1071
+ Runs on every incoming chat message. Used for moderation decisions.
1072
+
1073
+ \`\`\`typescript
1074
+ import { moderate } from "@zaflun/lumio-sdk/server";
1075
+
1076
+ export const toxicFilter = moderate({
1077
+ handler: async (ctx, message) => {
1078
+ const toxicScore = await analyzeToxicity(message.text);
1079
+ if (toxicScore > 0.9) {
1080
+ return { action: "timeout", duration: 600, message: "Please keep chat respectful." };
1081
+ }
1082
+ if (toxicScore > 0.7) {
1083
+ return { action: "delete", message: "Message removed by auto-mod." };
1084
+ }
1085
+ return { action: "none" };
1086
+ },
1087
+ });
1088
+ \`\`\`
1089
+
1090
+ ### Handler Return Actions
1091
+
1092
+ All bot module handlers return an action object:
1093
+
1094
+ | Action | Fields | Description | Permission required |
1095
+ |--------|--------|-------------|---------------------|
1096
+ | \`send_message\` | \`message\` | Send a chat message | \`chat:send\` |
1097
+ | \`delete\` | \`message?\` | Delete the triggering message | \`chat:delete\` |
1098
+ | \`timeout\` | \`duration\`, \`message?\` | Timeout the user (seconds) | \`chat:ban\` |
1099
+ | \`ban\` | \`message?\` | Ban the user | \`chat:ban\` |
1100
+ | \`none\` | \u2014 | Take no action | \u2014 |
1101
+
1102
+ The extension must declare the matching permission in \`lumio.config.json\`. For example, a moderation module that bans users needs \`"permissions": ["chat:ban"]\`.
1103
+
1104
+ ### Bot Module Permission Table
1105
+
1106
+ | Permission | Grants | Review level |
1107
+ |-----------|--------|--------------|
1108
+ | \`chat:send\` | Send chat messages | Standard |
1109
+ | \`chat:read\` | Read incoming chat messages | Standard |
1110
+ | \`chat:delete\` | Delete chat messages | Enhanced |
1111
+ | \`chat:ban\` | Ban, unban, and timeout users | Enhanced |
1112
+ | \`events:read\` | Subscribe to platform events | Standard |
1113
+ | \`events:write\` | Emit custom events | Standard |
898
1114
  `;
899
1115
 
900
1116
  // src/content/cli-workflows.ts
@@ -1177,7 +1393,7 @@ The Lumio auth system has 9 token types:
1177
1393
  | 2 | \`lm_usr_\` | User API Key | Permanent | External API access for users |
1178
1394
  | 3 | _(JWT)_ | JWT | Session | OAuth login session |
1179
1395
  | 4 | \`lm_pop_\` | Popout Token | Non-expiring | Overlay access for non-session viewers |
1180
- | 5 | \`lm_overlay_\` | Overlay Token | Permanent | Per-overlay OBS browser source access |
1396
+ | 5 | \`lm_overlay_\` | Overlay Token | Permanent | Per-overlay browser source access |
1181
1397
  | 6 | \`lm_share_\` | Shared Overlay Token | Time-limited | Temporary shared overlay links |
1182
1398
  | 7 | \`lm_ext_\` | Extension Token | 15 min | Scoped to one extension installation |
1183
1399
  | 8 | \`lm_widget_\` | Widget Token | Permanent | Widget Gallery standalone embeds |
@@ -1287,7 +1503,7 @@ var runtimeScopesContent = `# Runtime Scopes
1287
1503
  Extensions run inside a 3-layer iframe sandbox. Understanding the layers is important for debugging messaging and storage issues:
1288
1504
 
1289
1505
  \`\`\`
1290
- Host (lumio.vision / OBS browser source)
1506
+ Host (lumio.vision / browser source)
1291
1507
  \u2514\u2500\u2500 Supervisor iframe (supervisor.extension.lumio.vision)
1292
1508
  Purpose: validates postMessage traffic, enforces egress allowlist,
1293
1509
  mediates token refresh, proxies LumioComponentNode[] to host
@@ -1325,7 +1541,7 @@ Data is scoped to a specific extension installation within a specific overlay.
1325
1541
  defineTable({...}).storage("instance")
1326
1542
  \`\`\`
1327
1543
 
1328
- - Filtered by: \`_account_id\` + \`_overlay_id\`
1544
+ - Filtered by: \`_account_id\` + \`_overlay_id\` + \`_install_id\`
1329
1545
  - Use for: per-widget data (scoreboard scores, timer state, per-overlay config)
1330
1546
  - Example: A "Sports Scoreboard" widget has different scores in each overlay it appears in
1331
1547
 
@@ -1383,7 +1599,7 @@ Each scope maps to automatic WHERE clauses:
1383
1599
 
1384
1600
  \`\`\`sql
1385
1601
  -- instance scope:
1386
- SELECT * FROM ext_{id}.items WHERE _account_id = $1 AND _overlay_id = $2
1602
+ SELECT * FROM ext_{id}.items WHERE _account_id = $1 AND _overlay_id = $2 AND _install_id = $3
1387
1603
 
1388
1604
  -- overlay scope:
1389
1605
  SELECT * FROM ext_{id}.items WHERE _account_id = $1 AND _overlay_id = $2
@@ -1420,7 +1636,7 @@ var bestPracticesContent = `# Best Practices
1420
1636
 
1421
1637
  ### Layer surface
1422
1638
  - Use CSS animations via \`VisionStyle\` instead of JS-driven animations
1423
- - Keep the layer surface minimal \u2014 it runs in OBS at 30/60 fps
1639
+ - Keep the layer surface minimal \u2014 it runs in an Browser Source at 30/60 fps
1424
1640
  - Return \`null\` from components when they should be invisible (do not use \`display: none\`)
1425
1641
 
1426
1642
  ## UX Patterns
@@ -1471,7 +1687,7 @@ Wrap editor settings in \`CompactView\` for consistent styling with the Lumio da
1471
1687
  \`\`\`
1472
1688
 
1473
1689
  ### Transparent backgrounds on layer
1474
- The layer surface should have a transparent background so it composites correctly over the stream in OBS:
1690
+ The layer surface should have a transparent background so it composites correctly over the stream:
1475
1691
 
1476
1692
  \`\`\`tsx
1477
1693
  // layer.tsx \u2014 no background, content floats over stream
@@ -1485,23 +1701,19 @@ Use \`config_schema\` in \`lumio.config.json\` for user-customizable settings. T
1485
1701
 
1486
1702
  \`\`\`json
1487
1703
  {
1488
- "config_schema": {
1489
- "fields": [
1490
- { "key": "title", "type": "string", "label": "Widget Title", "default": "My Widget" },
1491
- { "key": "maxItems", "type": "number", "label": "Max Items", "default": 10 },
1492
- { "key": "showBorder", "type": "boolean", "label": "Show Border", "default": true },
1493
- {
1494
- "key": "theme",
1495
- "type": "select",
1496
- "label": "Color Theme",
1497
- "options": [
1498
- { "label": "Dark", "value": "dark" },
1499
- { "label": "Light", "value": "light" }
1500
- ],
1501
- "default": "dark"
1502
- }
1503
- ]
1504
- }
1704
+ "config_schema": [
1705
+ { "key": "title", "type": "string", "label": "Widget Title", "default_value": "My Widget", "required": true },
1706
+ { "key": "maxItems", "type": "number", "label": "Max Items", "default_value": 10, "required": false },
1707
+ { "key": "showBorder", "type": "boolean", "label": "Show Border", "default_value": true, "required": false },
1708
+ {
1709
+ "key": "theme",
1710
+ "type": "select",
1711
+ "label": "Color Theme",
1712
+ "options": ["dark", "light"],
1713
+ "default_value": "dark",
1714
+ "required": false
1715
+ }
1716
+ ]
1505
1717
  }
1506
1718
  \`\`\`
1507
1719
 
@@ -1521,7 +1733,7 @@ const maxItems = config.maxItems ?? 10;
1521
1733
 
1522
1734
  - Always use SDK presets via \`useAnimation()\` + \`<Animated>\` for entrance/exit effects \u2014 never write raw CSS keyframe \`@keyframes\` rules
1523
1735
  - The reconciler maps animation preset names to allowed \`VisionStyle\` \`animation\` properties; raw keyframe strings are stripped by the Supervisor
1524
- - Keep durations short on OBS layer (< 400ms). Longer animations cause visible frame drops in OBS at 30/60 fps
1736
+ - Keep durations short on the layer (< 400ms). Longer animations cause visible frame drops in the Browser Source at 30/60 fps
1525
1737
  - Prefer animating \`opacity\` and \`transform\` (GPU-composited) over \`width\`, \`height\`, \`margin\`, \`padding\` (layout-triggering)
1526
1738
 
1527
1739
  \`\`\`tsx
@@ -1560,6 +1772,120 @@ For beta testing before public launch:
1560
1772
  | Hardcoded strings | Use \`config_schema\` fields for user-customizable text |
1561
1773
  | Using localStorage directly | Use \`useExtensionStorage()\` (localStorage blocked at ext.lumio.vision) |
1562
1774
  | Raw CSS \`@keyframes\` animations | Use \`useAnimation()\` + \`<Animated>\` SDK presets |
1775
+
1776
+ ## Bot Module Security
1777
+
1778
+ ### Rate Limiting Awareness
1779
+
1780
+ Bot module handlers are subject to the same rate limits as other server functions. Plan for these limits in moderation logic:
1781
+
1782
+ - Actions (with fetch): 20/min per installation
1783
+ - Mutations: 50/min per installation
1784
+ - Storage writes: 200/min per installation
1785
+
1786
+ If your module processes high-volume chat, avoid issuing a moderation action on every message. Use \`ctx.cache\` for cooldowns:
1787
+
1788
+ \`\`\`tsx
1789
+ export const spamGuard = moderate({
1790
+ handler: async (ctx, message) => {
1791
+ const key = \`cooldown:\${ctx.user.platform_user_id}\`;
1792
+ if (await ctx.cache.get(key)) return { action: "none" };
1793
+
1794
+ if (isSpam(message.text)) {
1795
+ await ctx.cache.set(key, "1", { ttl: 30 });
1796
+ return { action: "timeout", duration: 60, message: "Spam detected." };
1797
+ }
1798
+ return { action: "none" };
1799
+ },
1800
+ });
1801
+ \`\`\`
1802
+
1803
+ ### Moderation Escalation Pattern
1804
+
1805
+ For moderation modules, escalate actions progressively rather than jumping to bans. Track offense counts in the extension database:
1806
+
1807
+ \`\`\`tsx
1808
+ export const escalatingMod = moderate({
1809
+ handler: async (ctx, message) => {
1810
+ if (!isViolation(message.text)) return { action: "none" };
1811
+
1812
+ const offenses = await ctx.db.get("offenses", ctx.user.platform_user_id);
1813
+ const count = (offenses?.count ?? 0) + 1;
1814
+ await ctx.db.patch("offenses", ctx.user.platform_user_id, { count });
1815
+
1816
+ if (count >= 3) return { action: "ban", message: "Repeated violations." };
1817
+ if (count === 2) return { action: "timeout", duration: 600, message: "Second warning." };
1818
+ return { action: "delete", message: "First warning: message removed." };
1819
+ },
1820
+ });
1821
+ \`\`\`
1822
+
1823
+ ### User Override Preservation
1824
+
1825
+ Account owners can override trigger configuration (command prefixes, keywords, regex patterns) via the dashboard. Always read trigger config from the handler context rather than hardcoding values:
1826
+
1827
+ \`\`\`tsx
1828
+ // Good: reads overridable config
1829
+ export const customCmd = command("!points", {
1830
+ handler: async (ctx, message) => {
1831
+ // The prefix "!points" is the default \u2014 users can override it in dashboard
1832
+ // Handler logic stays the same regardless of the configured prefix
1833
+ return { action: "send_message", message: "You have 100 points!" };
1834
+ },
1835
+ });
1836
+ \`\`\`
1837
+
1838
+ Extension developers should document which triggers are overridable in the extension's description so account owners know what they can customize.
1839
+
1840
+ ### Cache-First + Defer Pattern
1841
+
1842
+ For handlers that need fast responses but also need to persist data, use \`ctx.cache\` for the immediate check and \`ctx.defer()\` to write to the database asynchronously:
1843
+
1844
+ \`\`\`tsx
1845
+ export const rateLimitedGreet = command("!greet", {
1846
+ handler: async (ctx, message) => {
1847
+ const cacheKey = \`greeted:\${ctx.user.platform_user_id}\`;
1848
+
1849
+ // Fast path: check cache
1850
+ if (await ctx.cache.get(cacheKey)) {
1851
+ return { action: "send_message", message: "You already greeted today!" };
1852
+ }
1853
+
1854
+ // Set cache immediately (fast)
1855
+ await ctx.cache.set(cacheKey, "1", { ttl: 86400 });
1856
+
1857
+ // Defer the DB write (does not block response)
1858
+ ctx.defer(async () => {
1859
+ await ctx.db.insert("greet_log", {
1860
+ user_id: ctx.user.platform_user_id,
1861
+ greeted_at: new Date().toISOString(),
1862
+ });
1863
+ });
1864
+
1865
+ return { action: "send_message", message: \`Hello @\${ctx.user.display_name}!\` };
1866
+ },
1867
+ });
1868
+ \`\`\`
1869
+
1870
+ ### Exempt Role Support
1871
+
1872
+ Moderation modules should respect the account's exempt role configuration. The exempt roles (moderator, VIP, subscriber) are passed to the handler via \`ctx.user.roles\`. Check them before taking action:
1873
+
1874
+ \`\`\`tsx
1875
+ export const linkFilter = moderate({
1876
+ handler: async (ctx, message) => {
1877
+ // Respect exempt roles \u2014 do not moderate mods/VIPs
1878
+ if (ctx.user.roles.moderator || ctx.user.roles.vip) {
1879
+ return { action: "none" };
1880
+ }
1881
+
1882
+ if (containsLink(message.text)) {
1883
+ return { action: "delete", message: "Links are not allowed." };
1884
+ }
1885
+ return { action: "none" };
1886
+ },
1887
+ });
1888
+ \`\`\`
1563
1889
  `;
1564
1890
 
1565
1891
  // src/content/troubleshooting.ts
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  ROOT_FILES,
3
3
  SKILL_FILES,
4
4
  installSkillFiles
5
- } from "./chunk-I2USQPPS.js";
5
+ } from "./chunk-SSYJXSJX.js";
6
6
  export {
7
7
  ROOT_FILES,
8
8
  SKILL_FILES,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zaflun/lumio-agent-skill",
3
- "version": "0.1.0",
3
+ "version": "0.4.0",
4
4
  "description": "AI agent skill files for building Lumio Extensions",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",