@zaflun/lumio-agent-skill 0.2.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-5NNQDQNL.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.
@@ -144,7 +144,7 @@ lumio deploy
144
144
 
145
145
  **VisionStyle** is a typed CSS subset (allowed: flexbox, grid, \`position: relative\` / \`absolute\`, sizing, spacing, colors, border, typography, transitions; blocked: \`fixed\`, \`sticky\`, CSS expressions).
146
146
 
147
- 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).
148
148
 
149
149
  See \`components.md\` for full props and examples.
150
150
 
@@ -218,6 +218,101 @@ const { execute: spotifyQueueAdd } = useLumioAction("spotify:queue_add");
218
218
 
219
219
  See \`server-functions.md\` for schema definition, storage scoping, and advanced patterns.
220
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
+
221
316
  ## CLI Commands
222
317
 
223
318
  12 commands provided by \`@zaflun/lumio-cli\`:
@@ -472,7 +567,7 @@ import { Image } from "@zaflun/lumio-sdk";
472
567
 
473
568
  ## Input Components
474
569
 
475
- **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.
476
571
 
477
572
  ### Button
478
573
 
@@ -652,7 +747,7 @@ const exitAnim = useAnimation("fade-out", { duration: 200 });
652
747
 
653
748
  **Best practices:**
654
749
  - Always use SDK presets instead of raw CSS animations for entrance/exit effects \u2014 the reconciler maps them to allowed VisionStyle \`animation\` properties
655
- - 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
656
751
  - Avoid animating layout-affecting properties (\`width\`, \`height\`) on the layer; prefer \`opacity\` and \`transform\`
657
752
 
658
753
  ## VisionStyle
@@ -893,6 +988,129 @@ updateLayer({ layerId: "uuid", visible: true });
893
988
  | \`spotify:queue_add\` | \`spotify:control\` | Add track to Spotify queue |
894
989
 
895
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 |
896
1114
  `;
897
1115
 
898
1116
  // src/content/cli-workflows.ts
@@ -1175,7 +1393,7 @@ The Lumio auth system has 9 token types:
1175
1393
  | 2 | \`lm_usr_\` | User API Key | Permanent | External API access for users |
1176
1394
  | 3 | _(JWT)_ | JWT | Session | OAuth login session |
1177
1395
  | 4 | \`lm_pop_\` | Popout Token | Non-expiring | Overlay access for non-session viewers |
1178
- | 5 | \`lm_overlay_\` | Overlay Token | Permanent | Per-overlay OBS browser source access |
1396
+ | 5 | \`lm_overlay_\` | Overlay Token | Permanent | Per-overlay browser source access |
1179
1397
  | 6 | \`lm_share_\` | Shared Overlay Token | Time-limited | Temporary shared overlay links |
1180
1398
  | 7 | \`lm_ext_\` | Extension Token | 15 min | Scoped to one extension installation |
1181
1399
  | 8 | \`lm_widget_\` | Widget Token | Permanent | Widget Gallery standalone embeds |
@@ -1285,7 +1503,7 @@ var runtimeScopesContent = `# Runtime Scopes
1285
1503
  Extensions run inside a 3-layer iframe sandbox. Understanding the layers is important for debugging messaging and storage issues:
1286
1504
 
1287
1505
  \`\`\`
1288
- Host (lumio.vision / OBS browser source)
1506
+ Host (lumio.vision / browser source)
1289
1507
  \u2514\u2500\u2500 Supervisor iframe (supervisor.extension.lumio.vision)
1290
1508
  Purpose: validates postMessage traffic, enforces egress allowlist,
1291
1509
  mediates token refresh, proxies LumioComponentNode[] to host
@@ -1418,7 +1636,7 @@ var bestPracticesContent = `# Best Practices
1418
1636
 
1419
1637
  ### Layer surface
1420
1638
  - Use CSS animations via \`VisionStyle\` instead of JS-driven animations
1421
- - 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
1422
1640
  - Return \`null\` from components when they should be invisible (do not use \`display: none\`)
1423
1641
 
1424
1642
  ## UX Patterns
@@ -1469,7 +1687,7 @@ Wrap editor settings in \`CompactView\` for consistent styling with the Lumio da
1469
1687
  \`\`\`
1470
1688
 
1471
1689
  ### Transparent backgrounds on layer
1472
- 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:
1473
1691
 
1474
1692
  \`\`\`tsx
1475
1693
  // layer.tsx \u2014 no background, content floats over stream
@@ -1515,7 +1733,7 @@ const maxItems = config.maxItems ?? 10;
1515
1733
 
1516
1734
  - Always use SDK presets via \`useAnimation()\` + \`<Animated>\` for entrance/exit effects \u2014 never write raw CSS keyframe \`@keyframes\` rules
1517
1735
  - The reconciler maps animation preset names to allowed \`VisionStyle\` \`animation\` properties; raw keyframe strings are stripped by the Supervisor
1518
- - 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
1519
1737
  - Prefer animating \`opacity\` and \`transform\` (GPU-composited) over \`width\`, \`height\`, \`margin\`, \`padding\` (layout-triggering)
1520
1738
 
1521
1739
  \`\`\`tsx
@@ -1554,6 +1772,120 @@ For beta testing before public launch:
1554
1772
  | Hardcoded strings | Use \`config_schema\` fields for user-customizable text |
1555
1773
  | Using localStorage directly | Use \`useExtensionStorage()\` (localStorage blocked at ext.lumio.vision) |
1556
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
+ \`\`\`
1557
1889
  `;
1558
1890
 
1559
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-5NNQDQNL.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.2.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",