@zaflun/lumio-agent-skill 0.2.0 → 0.5.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-QXQNAYHN.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.
@@ -77,7 +77,8 @@ All three surfaces share storage via \`useExtensionStorage()\` \u2014 changes in
77
77
  "visibility": "public",
78
78
  "pricing": { "type": "free" },
79
79
  "config_schema": [
80
- { "key": "title", "type": "string", "label": "Widget Title", "default_value": "My Widget", "required": true },
80
+ { "key": "title", "type": "string", "label": "Widget Title", "default_value": "My Widget", "required": true, "placeholder": "Enter title..." },
81
+ { "key": "fontSize", "type": "number", "label": "Font Size", "default_value": 16, "required": false, "min": 8, "max": 200, "unit": "px" },
81
82
  { "key": "showBorder", "type": "boolean", "label": "Show Border", "default_value": true, "required": false }
82
83
  ]
83
84
  }
@@ -136,21 +137,21 @@ lumio deploy
136
137
 
137
138
  ## Components
138
139
 
139
- 15 SDK components (all accept \`style: VisionStyle\`):
140
+ 17 SDK components (all accept \`style: VisionStyle\`):
140
141
 
141
142
  **Layout:** \`Box\`, \`VStack\`, \`HStack\`, \`CompactView\`, \`List\`
142
- **Display:** \`Text\` (variant: heading/muted/default), \`Image\` (objectFit), \`Animated\`
143
- **Input:** \`Button\` (variant: default/destructive), \`Toggle\`, \`TextField\`, \`TextArea\`, \`NumberField\`, \`Dropdown\`, \`Slider\`
143
+ **Display:** \`Text\` (variant: heading/muted/default), \`Image\` (objectFit), \`Animated\`, \`IconDisplay\`
144
+ **Input:** \`Button\` (variant: default/destructive), \`Toggle\`, \`TextField\`, \`TextArea\`, \`NumberField\`, \`Dropdown\`, \`Slider\`, \`IconPicker\`
144
145
 
145
146
  **VisionStyle** is a typed CSS subset (allowed: flexbox, grid, \`position: relative\` / \`absolute\`, sizing, spacing, colors, border, typography, transitions; blocked: \`fixed\`, \`sticky\`, CSS expressions).
146
147
 
147
- On OBS layer: input components become read-only (buttons hidden, toggles show on/off text).
148
+ On the layer surface: input components become read-only (buttons hidden, toggles show on/off text).
148
149
 
149
150
  See \`components.md\` for full props and examples.
150
151
 
151
152
  ## Hooks
152
153
 
153
- 11 SDK hooks:
154
+ 12 SDK hooks:
154
155
 
155
156
  | Hook | Returns | Purpose |
156
157
  |------|---------|---------|
@@ -165,6 +166,7 @@ See \`components.md\` for full props and examples.
165
166
  | \`useQuery(fn, params?)\` | \`{ data, isLoading, error, refetch }\` | Call server query |
166
167
  | \`useMutation(fn)\` | \`{ mutate, mutateAsync, data, isLoading, error }\` | Call server mutation/action |
167
168
  | \`useAnimation(preset, options?)\` | \`animationProps\` | SDK animation presets for \`Animated\` component |
169
+ | \`useIconPicker(options?)\` | \`{ isOpen, selected, open, close, select, clear, sources }\` | Headless state for custom icon picker UIs |
168
170
 
169
171
  **Important:** \`setStorage\` must always spread: \`setStorage({ ...storage, key: value })\`
170
172
 
@@ -218,6 +220,101 @@ const { execute: spotifyQueueAdd } = useLumioAction("spotify:queue_add");
218
220
 
219
221
  See \`server-functions.md\` for schema definition, storage scoping, and advanced patterns.
220
222
 
223
+ ## Bot Modules
224
+
225
+ 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.
226
+
227
+ ### Handler Types
228
+
229
+ | Handler | Trigger | Description |
230
+ |---------|---------|-------------|
231
+ | \`command(prefix, handler)\` | Chat command (e.g. \`!rank\`) | Fires when a message starts with the prefix |
232
+ | \`keyword(words, handler)\` | Keyword match | Fires when a message contains any listed keyword |
233
+ | \`pattern(regex, handler)\` | Regex match | Fires when a message matches the regex |
234
+ | \`event(type, handler)\` | Platform event | Fires on a specific event (e.g. \`twitch:follower\`) |
235
+ | \`timer(name, intervalSecs, handler)\` | Recurring timer | Fires every N seconds. Name must be unique per extension. |
236
+ | \`moderate(handler)\` | Every message | Runs on every chat message for moderation decisions |
237
+
238
+ ### Handler Context
239
+
240
+ Bot module handlers receive an extended \`ctx\` with chat-specific properties:
241
+
242
+ \`\`\`typescript
243
+ import { command } from "@zaflun/lumio-sdk/server";
244
+
245
+ export const rankCmd = command("!rank", {
246
+ handler: async (ctx, message) => {
247
+ // ctx.user \u2014 chat user who sent the message
248
+ // { platform, platform_user_id, display_name, roles }
249
+ // roles: { moderator, vip, subscriber, broadcaster }
250
+
251
+ // ctx.auth \u2014 extension identity
252
+ // { userId, accountId, extensionId, installId }
253
+
254
+ // ctx.cache \u2014 in-memory KV cache scoped to install (TTL-based)
255
+ await ctx.cache.set("last_rank_user", ctx.user.display_name, { ttl: 300 });
256
+ const cached = await ctx.cache.get("last_rank_user");
257
+
258
+ // ctx.defer() \u2014 schedule a follow-up after handler returns
259
+ ctx.defer(async () => {
260
+ await ctx.db.insert("rank_log", { user: ctx.user.display_name });
261
+ });
262
+
263
+ return { action: "send_message", message: \`@\${ctx.user.display_name} your rank is Gold!\` };
264
+ },
265
+ });
266
+ \`\`\`
267
+
268
+ ### \`ctx.user\` vs \`ctx.auth\`
269
+
270
+ - \`ctx.user\` is the **chat user** who triggered the handler (their platform identity, display name, and role flags). Only available in bot module handlers.
271
+ - \`ctx.auth\` is the **extension identity** (which extension, which install, which account). Available in all server functions.
272
+
273
+ ### Timer Naming
274
+
275
+ Timer names must be unique within the extension. The name is used as the timer's identity for scheduling and deduplication. Use descriptive names:
276
+
277
+ \`\`\`typescript
278
+ timer("ad_reminder", 900, { handler: async (ctx) => { ... } }); // Good
279
+ timer("t1", 900, { handler: async (ctx) => { ... } }); // Bad \u2014 unclear
280
+ \`\`\`
281
+
282
+ ### \`ctx.cache\`
283
+
284
+ 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.
285
+
286
+ \`\`\`typescript
287
+ // Set with TTL (seconds)
288
+ await ctx.cache.set("cooldown:ban:" + userId, "1", { ttl: 60 });
289
+
290
+ // Get (returns null if expired or missing)
291
+ const val = await ctx.cache.get("cooldown:ban:" + userId);
292
+
293
+ // Delete
294
+ await ctx.cache.delete("cooldown:ban:" + userId);
295
+ \`\`\`
296
+
297
+ ### \`ctx.defer()\`
298
+
299
+ Schedule work to run after the handler returns its response. Useful for logging, analytics, or secondary actions that should not delay the primary response.
300
+
301
+ \`\`\`typescript
302
+ export const modCheck = moderate({
303
+ handler: async (ctx, message) => {
304
+ if (isToxic(message.text)) {
305
+ ctx.defer(async () => {
306
+ await ctx.db.insert("mod_log", {
307
+ user: ctx.user.platform_user_id,
308
+ action: "timeout",
309
+ reason: "toxic message",
310
+ });
311
+ });
312
+ return { action: "timeout", duration: 300, message: "Please be respectful." };
313
+ }
314
+ return { action: "none" };
315
+ },
316
+ });\`\`\`
317
+
221
318
  ## CLI Commands
222
319
 
223
320
  12 commands provided by \`@zaflun/lumio-cli\`:
@@ -334,7 +431,59 @@ See \`troubleshooting.md\` for the full error reference.
334
431
  // src/content/components.ts
335
432
  var componentsContent = `# Lumio SDK Components
336
433
 
337
- All 15 components available in \`@zaflun/lumio-sdk\`. Every component accepts an optional \`style: VisionStyle\` prop.
434
+ All 31 components available in \`@zaflun/lumio-sdk\`. Every component accepts an optional \`style: VisionStyle\` prop.
435
+
436
+ ## Editor Components
437
+
438
+ 14 components for building settings UIs in \`editor.tsx\` and \`designer.tsx\`. They serialize via the Reconciler and render as \`@lumio/ui\` components on the host \u2014 matching the dashboard design system.
439
+
440
+ \`\`\`tsx
441
+ import {
442
+ EditorInput, EditorTextarea, EditorSelect, EditorCheckbox,
443
+ EditorSlider, EditorColorPicker, EditorGroup, EditorLabel,
444
+ EditorDivider, EditorInfo, EditorFontRow, EditorButton,
445
+ EditorFontPicker, EditorChannelRow, useEditorAction,
446
+ } from "@zaflun/lumio-sdk";
447
+
448
+ // Text/number input with label, placeholder, min/max, unit
449
+ <EditorInput label="Font Size" type="number" value="16" onChange={setSize} min={8} max={200} unit="px" />
450
+
451
+ // Select dropdown
452
+ <EditorSelect label="Theme" value={theme} onChange={setTheme} options={[{ value: "dark", label: "Dark" }]} />
453
+
454
+ // Checkbox toggle
455
+ <EditorCheckbox label="Show Badges" checked={show} onChange={setShow} />
456
+
457
+ // Slider with range
458
+ <EditorSlider label="Opacity" value={100} onChange={setOpacity} min={0} max={100} unit="%" />
459
+
460
+ // Color picker (HSV canvas + hex input)
461
+ <EditorColorPicker label="Background" value="#000000" onChange={setColor} />
462
+
463
+ // Compact font row: [B] [size px] [color]
464
+ <EditorFontRow bold={isBold} onBoldChange={setBold} fontSize={16} onFontSizeChange={setSize} fontColor="#fff" onFontColorChange={setColor} />
465
+
466
+ // Full-width button (primary/default/destructive)
467
+ <EditorButton label="Open Designer" variant="primary" onClick={openDesigner} />
468
+
469
+ // Collapsible section
470
+ <EditorGroup label="Advanced"><EditorCheckbox ... /></EditorGroup>
471
+
472
+ // Font picker with weight dropdown (opens NavigationStack page)
473
+ <EditorFontPicker label="Font Family" value="Lexend" onChange={setFont} weight="400" onWeightChange={setWeight} />
474
+
475
+ // Channel row: platform + name + color + delete in one line
476
+ <EditorChannelRow platform="twitch" onPlatformChange={setPlatform} channel="username" onChannelChange={setChannel} color="#9146FF" onColorChange={setColor} onRemove={remove} />
477
+
478
+ // Dispatch UI actions (e.g. open designer)
479
+ const { dispatch } = useEditorAction();
480
+ <EditorButton label="Open Designer" variant="primary" onClick={() => dispatch("open-designer")} />
481
+
482
+ // Section label, divider, info text
483
+ <EditorLabel text="Settings" />
484
+ <EditorDivider />
485
+ <EditorInfo content="Configure your widget here." />
486
+ \`\`\`
338
487
 
339
488
  > **Note:** There is no direct DOM access from extension code. The custom React reconciler serializes component trees to \`LumioComponentNode[]\` via postMessage \u2014 you must use SDK components exclusively.
340
489
 
@@ -472,7 +621,7 @@ import { Image } from "@zaflun/lumio-sdk";
472
621
 
473
622
  ## Input Components
474
623
 
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.
624
+ **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
625
 
477
626
  ### Button
478
627
 
@@ -652,9 +801,72 @@ const exitAnim = useAnimation("fade-out", { duration: 200 });
652
801
 
653
802
  **Best practices:**
654
803
  - 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
804
+ - Keep animation durations short on the layer surface (< 500ms) to avoid frame drops
656
805
  - Avoid animating layout-affecting properties (\`width\`, \`height\`) on the layer; prefer \`opacity\` and \`transform\`
657
806
 
807
+ ## Picker Components
808
+
809
+ ### IconPicker
810
+
811
+ Universal icon/emoji/emote picker with 4 tabs: Lucide icons (1960), Unicode emojis (1584), platform emotes (via API), and file upload. **Editor-only** \u2014 use \`IconDisplay\` to render the selected icon on the layer surface.
812
+
813
+ \`\`\`tsx
814
+ import { IconPicker, IconDisplay } from "@zaflun/lumio-sdk";
815
+ import type { IconValue } from "@zaflun/lumio-sdk";
816
+
817
+ // In editor.tsx \u2014 let the user pick an icon
818
+ const [storage, setStorage] = useExtensionStorage();
819
+
820
+ <IconPicker
821
+ value={storage.icon ?? null}
822
+ onChange={(icon) => setStorage({ ...storage, icon })}
823
+ sources={["lucide", "emoji", "emote", "upload"]}
824
+ size="md"
825
+ placeholder="Choose an icon..."
826
+ />
827
+
828
+ // In layer.tsx \u2014 render the selected icon
829
+ <IconDisplay icon={storage.icon ?? null} size={32} />
830
+ \`\`\`
831
+
832
+ **IconPickerProps:**
833
+ - \`value: IconValue | null\` \u2014 currently selected icon
834
+ - \`onChange: (icon: IconValue | null) => void\` \u2014 selection callback
835
+ - \`sources?: IconSource[]\` \u2014 tabs to show (default: all four: \`["lucide", "emoji", "emote", "upload"]\`)
836
+ - \`size?: "sm" | "md" | "lg"\` \u2014 picker trigger button size
837
+ - \`placeholder?: string\` \u2014 placeholder text when no icon selected
838
+
839
+ **IconDisplayProps:**
840
+ - \`icon: IconValue | null\` \u2014 icon to render (null renders nothing)
841
+ - \`size?: number\` \u2014 pixel size
842
+ - \`className?: string\`
843
+
844
+ **IconValue type:**
845
+ \`\`\`typescript
846
+ type IconSource = "lucide" | "emoji" | "emote" | "upload";
847
+
848
+ interface IconValue {
849
+ type: IconSource; // which tab/source the icon came from
850
+ value: string; // icon name (lucide), Unicode char (emoji), CDN URL (emote), upload URL (upload)
851
+ label?: string; // human-readable label
852
+ platform?: string; // platform identifier for emotes (e.g. "twitch", "7tv")
853
+ }
854
+ \`\`\`
855
+
856
+ **\`useIconPicker()\` hook** \u2014 headless state management for custom picker UIs:
857
+ \`\`\`tsx
858
+ import { useIconPicker } from "@zaflun/lumio-sdk";
859
+
860
+ const { isOpen, selected, open, close, select, clear, sources } = useIconPicker({
861
+ sources: ["lucide", "emoji"],
862
+ onSelect: (icon) => console.log("Selected:", icon),
863
+ });
864
+ \`\`\`
865
+
866
+ Returns: \`{ isOpen, selected, open, close, select, clear, sources }\`
867
+
868
+ **Upload tab** requires the extension to declare \`egress.allowHosts\` including the Lumio API. Uploaded files are stored per-extension with quota tracking via the \`extension_uploads\` table.
869
+
658
870
  ## VisionStyle
659
871
 
660
872
  Typed CSS subset accepted by all components. Sanitized by the host \u2014 only safe properties are allowed.
@@ -893,6 +1105,242 @@ updateLayer({ layerId: "uuid", visible: true });
893
1105
  | \`spotify:queue_add\` | \`spotify:control\` | Add track to Spotify queue |
894
1106
 
895
1107
  Actions are proxied through the Lumio API \u2014 the extension never accesses OAuth tokens or platform credentials directly.
1108
+
1109
+ ## Automation Node Handlers
1110
+
1111
+ Extensions with category \`automation_node\` export a single handler in \`server/handler.ts\`. The handler type depends on the \`node_type\` declared in \`lumio.config.json\`.
1112
+
1113
+ ### \`defineAutomationAction(handler)\`
1114
+
1115
+ Action nodes execute when triggered by an upstream node in the automation workflow. They receive input data from the previous node and can perform side effects (HTTP calls, DB writes).
1116
+
1117
+ \`\`\`typescript
1118
+ import { defineAutomationAction } from "@zaflun/lumio-sdk/server";
1119
+
1120
+ export default defineAutomationAction({
1121
+ handler: async (ctx) => {
1122
+ const webhookUrl = ctx.nodeConfig.webhook_url;
1123
+ const res = await ctx.fetch(webhookUrl, {
1124
+ method: "POST",
1125
+ headers: { "Content-Type": "application/json" },
1126
+ body: JSON.stringify(ctx.inputData),
1127
+ });
1128
+ return { status: res.status, body: await res.json() };
1129
+ },
1130
+ });
1131
+ \`\`\`
1132
+
1133
+ ### \`defineAutomationTrigger({ onWebhook?, onPoll? })\`
1134
+
1135
+ Trigger nodes start automation workflows. They support two modes: webhook (receives external HTTP calls) and poll (periodic checks for new data).
1136
+
1137
+ \`\`\`typescript
1138
+ import { defineAutomationTrigger } from "@zaflun/lumio-sdk/server";
1139
+
1140
+ export default defineAutomationTrigger({
1141
+ onWebhook: async (ctx, request) => {
1142
+ const body = await request.json();
1143
+ if (body.event === "order.created") {
1144
+ return { trigger: true, outputData: { orderId: body.id, total: body.total } };
1145
+ }
1146
+ return { trigger: false };
1147
+ },
1148
+ onPoll: async (ctx) => {
1149
+ const lastCheck = await ctx.cache.get("last_check") ?? "1970-01-01T00:00:00Z";
1150
+ const res = await ctx.fetch(
1151
+ \`https://api.example.com/events?since=\${lastCheck}\`,
1152
+ { headers: { "Authorization": \`Bearer \${ctx.secrets.get("API_KEY")}\` } }
1153
+ );
1154
+ const events = await res.json();
1155
+ await ctx.cache.set("last_check", new Date().toISOString());
1156
+ return events.map((e: any) => ({ trigger: true, outputData: e }));
1157
+ },
1158
+ });
1159
+ \`\`\`
1160
+
1161
+ ### \`defineAutomationLogic(handler)\`
1162
+
1163
+ Logic nodes evaluate conditions and route the workflow to different branches. They return a branch key that determines which downstream path executes.
1164
+
1165
+ \`\`\`typescript
1166
+ import { defineAutomationLogic } from "@zaflun/lumio-sdk/server";
1167
+
1168
+ export default defineAutomationLogic({
1169
+ handler: async (ctx) => {
1170
+ const amount = ctx.inputData.total;
1171
+ if (amount >= 100) return { branch: "high_value" };
1172
+ if (amount >= 25) return { branch: "medium_value" };
1173
+ return { branch: "low_value" };
1174
+ },
1175
+ });
1176
+ \`\`\`
1177
+
1178
+ ### Automation Handler Context (\`ctx\`)
1179
+
1180
+ | Property | Available in | Description |
1181
+ |----------|-------------|-------------|
1182
+ | \`ctx.nodeConfig\` | All | Node configuration from the user's config panel |
1183
+ | \`ctx.inputData\` | Action, Logic | Data from the upstream node |
1184
+ | \`ctx.fetch(url, options?)\` | All | HTTP request (egress allowlist enforced) |
1185
+ | \`ctx.cache.get(key)\` / \`ctx.cache.set(key, value, ttl?)\` | All | Per-installation KV cache (Redis, 1h default TTL) |
1186
+ | \`ctx.secrets.get(key)\` | All | Server-side secret |
1187
+ | \`ctx.db\` | All | Extension DB schema access |
1188
+ | \`ctx.auth\` | All | \`{ accountId }\` \u2014 account identity |
1189
+ | \`ctx.defer()\` | Action | Mark execution as async (returns 202, completes in background) |
1190
+
1191
+ ## Bot Module Handlers
1192
+
1193
+ Extensions with category \`bot_module\` export handler wrappers that register triggers. Each wrapper defines when the handler fires and what context it receives.
1194
+
1195
+ ### \`command(prefix, options)\`
1196
+
1197
+ Fires when a chat message starts with the given prefix.
1198
+
1199
+ \`\`\`typescript
1200
+ import { command } from "@zaflun/lumio-sdk/server";
1201
+
1202
+ export const hello = command("!hello", {
1203
+ handler: async (ctx, message) => {
1204
+ return { action: "send_message", message: \`Hello @\${ctx.user.display_name}!\` };
1205
+ },
1206
+ });
1207
+ \`\`\`
1208
+
1209
+ ### \`keyword(words, options)\`
1210
+
1211
+ Fires when a chat message contains any of the listed keywords (case-insensitive).
1212
+
1213
+ \`\`\`typescript
1214
+ import { keyword } from "@zaflun/lumio-sdk/server";
1215
+
1216
+ export const giveaway = keyword(["giveaway", "enter", "join"], {
1217
+ handler: async (ctx, message) => {
1218
+ await ctx.db.insert("entries", { user_id: ctx.user.platform_user_id });
1219
+ return { action: "send_message", message: \`@\${ctx.user.display_name} you are entered!\` };
1220
+ },
1221
+ });
1222
+ \`\`\`
1223
+
1224
+ ### \`pattern(regex, options)\`
1225
+
1226
+ Fires when a chat message matches the given regex.
1227
+
1228
+ \`\`\`typescript
1229
+ import { pattern } from "@zaflun/lumio-sdk/server";
1230
+
1231
+ export const scoreParser = pattern(/^!score\\s+(\\w+)\\s+(\\d+)$/, {
1232
+ handler: async (ctx, message, matches) => {
1233
+ const [, game, score] = matches;
1234
+ await ctx.db.insert("scores", { game, score: parseInt(score, 10), user: ctx.user.display_name });
1235
+ return { action: "send_message", message: \`Score recorded: \${game} = \${score}\` };
1236
+ },
1237
+ });
1238
+ \`\`\`
1239
+
1240
+ ### \`event(eventType, options)\`
1241
+
1242
+ Fires on a platform event. The \`eventType\` is a \`LumioEventType\` string.
1243
+
1244
+ \`\`\`typescript
1245
+ import { event } from "@zaflun/lumio-sdk/server";
1246
+
1247
+ export const welcomeFollower = event("twitch:follower", {
1248
+ handler: async (ctx, eventData) => {
1249
+ return { action: "send_message", message: \`Welcome @\${eventData.user_name}! Thanks for the follow!\` };
1250
+ },
1251
+ });
1252
+ \`\`\`
1253
+
1254
+ ### \`timer(name, intervalSecs, options)\`
1255
+
1256
+ Fires on a recurring interval. The name must be unique within the extension.
1257
+
1258
+ \`\`\`typescript
1259
+ import { timer } from "@zaflun/lumio-sdk/server";
1260
+
1261
+ export const reminder = timer("social_reminder", 900, {
1262
+ handler: async (ctx) => {
1263
+ return { action: "send_message", message: "Follow us on Twitter! twitter.com/example" };
1264
+ },
1265
+ });
1266
+ \`\`\`
1267
+
1268
+ ### \`moderate(options)\`
1269
+
1270
+ Runs on every incoming chat message. Used for moderation decisions.
1271
+
1272
+ \`\`\`typescript
1273
+ import { moderate } from "@zaflun/lumio-sdk/server";
1274
+
1275
+ export const toxicFilter = moderate({
1276
+ handler: async (ctx, message) => {
1277
+ const toxicScore = await analyzeToxicity(message.text);
1278
+ if (toxicScore > 0.9) {
1279
+ return { action: "timeout", duration: 600, message: "Please keep chat respectful." };
1280
+ }
1281
+ if (toxicScore > 0.7) {
1282
+ return { action: "delete", message: "Message removed by auto-mod." };
1283
+ }
1284
+ return { action: "none" };
1285
+ },
1286
+ });
1287
+ \`\`\`
1288
+
1289
+ ### \`defineModerate(handler)\`
1290
+
1291
+ Convenience wrapper for system-style moderation handlers. The handler receives a unified context with the chat message and returns a moderation action.
1292
+
1293
+ \`\`\`typescript
1294
+ import { defineModerate } from "@zaflun/lumio-sdk/server";
1295
+
1296
+ export default defineModerate(async (ctx) => {
1297
+ if (ctx.message.text.includes("spam")) {
1298
+ return { action: "delete" };
1299
+ }
1300
+ return { action: "allow" };
1301
+ });
1302
+ \`\`\`
1303
+
1304
+ ### \`defineTimer(handler)\`
1305
+
1306
+ Convenience wrapper for timer-based handlers (e.g., timed messages). Fires on a recurring interval configured in the extension's install config.
1307
+
1308
+ \`\`\`typescript
1309
+ import { defineTimer } from "@zaflun/lumio-sdk/server";
1310
+
1311
+ export default defineTimer(async (ctx) => {
1312
+ const messages = (ctx.installConfig.messages as string || "").split("\\n").filter(Boolean);
1313
+ if (messages.length === 0) return null;
1314
+ const idx = parseInt(await ctx.cache.get("idx") || "0", 10);
1315
+ await ctx.cache.set("idx", String((idx + 1) % messages.length));
1316
+ return { reply: messages[idx] };
1317
+ });
1318
+ \`\`\`
1319
+
1320
+ ### Handler Return Actions
1321
+
1322
+ All bot module handlers return an action object:
1323
+
1324
+ | Action | Fields | Description | Permission required |
1325
+ |--------|--------|-------------|---------------------|
1326
+ | \`send_message\` | \`message\` | Send a chat message | \`chat:send\` |
1327
+ | \`delete\` | \`message?\` | Delete the triggering message | \`chat:delete\` |
1328
+ | \`timeout\` | \`duration\`, \`message?\` | Timeout the user (seconds) | \`chat:ban\` |
1329
+ | \`ban\` | \`message?\` | Ban the user | \`chat:ban\` |
1330
+ | \`none\` | \u2014 | Take no action | \u2014 |
1331
+
1332
+ The extension must declare the matching permission in \`lumio.config.json\`. For example, a moderation module that bans users needs \`"permissions": ["chat:ban"]\`.
1333
+
1334
+ ### Bot Module Permission Table
1335
+
1336
+ | Permission | Grants | Review level |
1337
+ |-----------|--------|--------------|
1338
+ | \`chat:send\` | Send chat messages | Standard |
1339
+ | \`chat:read\` | Read incoming chat messages | Standard |
1340
+ | \`chat:delete\` | Delete chat messages | Enhanced |
1341
+ | \`chat:ban\` | Ban, unban, and timeout users | Enhanced |
1342
+ | \`events:read\` | Subscribe to platform events | Standard |
1343
+ | \`events:write\` | Emit custom events | Standard |
896
1344
  `;
897
1345
 
898
1346
  // src/content/cli-workflows.ts
@@ -952,15 +1400,14 @@ lumio init my-widget
952
1400
  # ? Category: widget
953
1401
  # ? Targets: editor, layer
954
1402
  # ? Server functions: yes
955
- # ? Template: Widget (editor + layer + storage)
1403
+ # ? How should the editor UI be built? config_schema only / editor.tsx only / Both
956
1404
  # ? Owner: My Stream (personal) / Awesome Team <- team selection if applicable
957
1405
  #
958
1406
  # Creating extension on Lumio...
959
1407
  # Extension-ID: 88b2478a-39c7-468e-bebd-14e52fd97ac0
960
1408
  # Slug: k8f2x-my-widget
961
1409
  #
962
- # Writing lumio.config.json...
963
- # Writing src/editor.tsx...
1410
+ # Writing lumio.config.json (with config_schema)...
964
1411
  # Writing src/layer.tsx...
965
1412
  # Writing server/schema.ts...
966
1413
  # Writing server/functions.ts...
@@ -968,6 +1415,32 @@ lumio init my-widget
968
1415
  # Done! Run \`lumio dev\` to start developing.
969
1416
  \`\`\`
970
1417
 
1418
+ ### Automation Node Example
1419
+
1420
+ \`\`\`bash
1421
+ lumio init my-webhook-action
1422
+
1423
+ # Interactive prompts:
1424
+ # ? Extension name: My Webhook Action
1425
+ # ? Description: Posts data to a webhook
1426
+ # ? Category: automation_node
1427
+ # ? Node type: action
1428
+ # ? Server functions: yes (required for automation nodes)
1429
+ # ? Owner: My Stream (personal) / Awesome Team
1430
+ #
1431
+ # Creating extension on Lumio...
1432
+ # Extension-ID: a1b2c3d4-...
1433
+ # Slug: x9k1z-my-webhook-action
1434
+ #
1435
+ # Writing lumio.config.json...
1436
+ # Writing src/editor.tsx...
1437
+ # Writing server/handler.ts...
1438
+ #
1439
+ # Done! Run \`lumio dev\` to start developing.
1440
+ \`\`\`
1441
+
1442
+ Automation nodes always generate \`server/handler.ts\` (not \`server/functions.ts\`) and \`src/editor.tsx\` for the config panel. The \`node_type\` can be \`action\`, \`trigger\`, or \`logic\`.
1443
+
971
1444
  ### Templates
972
1445
 
973
1446
  | Template | Targets | Server | Description |
@@ -977,6 +1450,9 @@ lumio init my-widget
977
1450
  | Full-Stack | editor + layer | Yes | Complete extension with server functions |
978
1451
  | Interactive Game | editor + layer + interactive | Yes | Viewer interaction game |
979
1452
  | Bot Module | server-only | Yes | Chat command module |
1453
+ | Automation Action | server + editor | Yes | Automation workflow action node |
1454
+ | Automation Trigger | server + editor | Yes | Automation workflow trigger node |
1455
+ | Automation Logic | server + editor | Yes | Automation workflow logic/branching node |
980
1456
  | Integration | server | Yes | External API integration |
981
1457
  | Theme | editor | No | Style variables only |
982
1458
 
@@ -997,11 +1473,33 @@ lumio dev
997
1473
  # Press 'q' to quit.
998
1474
  \`\`\`
999
1475
 
1476
+ ### Automation Node Development
1477
+
1478
+ For automation node extensions, the dev server auto-detects \`category: "automation_node"\` and launches the automation simulator:
1479
+
1480
+ \`\`\`bash
1481
+ lumio dev
1482
+ # or explicitly:
1483
+ lumio dev --automation-node
1484
+
1485
+ # Output:
1486
+ # Lumio Dev Server running at http://localhost:5173
1487
+ # Config Panel: http://localhost:5173/editor
1488
+ #
1489
+ # Automation Simulator ready. Commands:
1490
+ # /execute [JSON] \u2014 Run the action handler with input data
1491
+ # /webhook [JSON] \u2014 Simulate an incoming webhook (trigger nodes)
1492
+ # /poll \u2014 Trigger a poll cycle (trigger nodes)
1493
+ # /config [JSON] \u2014 Update node config
1494
+ # /quit \u2014 Stop the dev server
1495
+ \`\`\`
1496
+
1000
1497
  Features:
1001
1498
  - Hot reload on file changes
1002
1499
  - Mock event system (simulated chat, follows, subs)
1003
1500
  - Sandbox environment matching production constraints
1004
1501
  - Server function execution with local DB
1502
+ - Automation simulator with /execute, /webhook, /poll, /config commands
1005
1503
 
1006
1504
  ## lumio build
1007
1505
 
@@ -1175,7 +1673,7 @@ The Lumio auth system has 9 token types:
1175
1673
  | 2 | \`lm_usr_\` | User API Key | Permanent | External API access for users |
1176
1674
  | 3 | _(JWT)_ | JWT | Session | OAuth login session |
1177
1675
  | 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 |
1676
+ | 5 | \`lm_overlay_\` | Overlay Token | Permanent | Per-overlay browser source access |
1179
1677
  | 6 | \`lm_share_\` | Shared Overlay Token | Time-limited | Temporary shared overlay links |
1180
1678
  | 7 | \`lm_ext_\` | Extension Token | 15 min | Scoped to one extension installation |
1181
1679
  | 8 | \`lm_widget_\` | Widget Token | Permanent | Widget Gallery standalone embeds |
@@ -1285,7 +1783,7 @@ var runtimeScopesContent = `# Runtime Scopes
1285
1783
  Extensions run inside a 3-layer iframe sandbox. Understanding the layers is important for debugging messaging and storage issues:
1286
1784
 
1287
1785
  \`\`\`
1288
- Host (lumio.vision / OBS browser source)
1786
+ Host (lumio.vision / browser source)
1289
1787
  \u2514\u2500\u2500 Supervisor iframe (supervisor.extension.lumio.vision)
1290
1788
  Purpose: validates postMessage traffic, enforces egress allowlist,
1291
1789
  mediates token refresh, proxies LumioComponentNode[] to host
@@ -1418,7 +1916,7 @@ var bestPracticesContent = `# Best Practices
1418
1916
 
1419
1917
  ### Layer surface
1420
1918
  - 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
1919
+ - Keep the layer surface minimal \u2014 it runs in an Browser Source at 30/60 fps
1422
1920
  - Return \`null\` from components when they should be invisible (do not use \`display: none\`)
1423
1921
 
1424
1922
  ## UX Patterns
@@ -1469,7 +1967,7 @@ Wrap editor settings in \`CompactView\` for consistent styling with the Lumio da
1469
1967
  \`\`\`
1470
1968
 
1471
1969
  ### Transparent backgrounds on layer
1472
- The layer surface should have a transparent background so it composites correctly over the stream in OBS:
1970
+ The layer surface should have a transparent background so it composites correctly over the stream:
1473
1971
 
1474
1972
  \`\`\`tsx
1475
1973
  // layer.tsx \u2014 no background, content floats over stream
@@ -1479,22 +1977,46 @@ Lumio.render(<Overlay />, { target: "layer" });
1479
1977
 
1480
1978
  ## Config Schema
1481
1979
 
1482
- Use \`config_schema\` in \`lumio.config.json\` for user-customizable settings. The Lumio dashboard renders these as a form in the Widget Gallery install flow \u2014 the user fills them in without needing to open the editor surface.
1980
+ Use \`config_schema\` in \`lumio.config.json\` for user-customizable settings. The widget editor auto-generates form fields from the schema \u2014 no custom \`editor.tsx\` needed for simple settings. For advanced UIs, use \`editor.tsx\` alongside (or instead of) config_schema.
1981
+
1982
+ **Three valid combos:**
1983
+ - **config_schema only** \u2014 auto-generated editor form, \`targets: ["layer"]\`
1984
+ - **editor.tsx only** \u2014 fully custom React editor UI, \`targets: ["layer", "editor"]\`
1985
+ - **Both** \u2014 schema fields render first, editor.tsx renders below for advanced controls
1986
+
1987
+ **Supported field types:** \`string\`, \`number\`, \`boolean\`, \`color\`, \`select\`, \`textarea\`, \`slider\`, \`font\`, \`icon\`, \`image\`, \`sound\`, \`multiselect\`, \`group\`, \`divider\`, \`info\`, \`channel\`, \`designer\`, \`presets\`
1483
1988
 
1484
1989
  \`\`\`json
1485
1990
  {
1486
1991
  "config_schema": [
1487
- { "key": "title", "type": "string", "label": "Widget Title", "default_value": "My Widget", "required": true },
1488
- { "key": "maxItems", "type": "number", "label": "Max Items", "default_value": 10, "required": false },
1992
+ { "key": "title", "type": "string", "label": "Widget Title", "default_value": "My Widget", "required": true, "placeholder": "Enter title..." },
1993
+ { "key": "fontSize", "type": "number", "label": "Font Size", "default_value": 16, "required": false, "min": 8, "max": 200, "unit": "px" },
1994
+ { "key": "volume", "type": "slider", "label": "Volume", "default_value": 50, "required": false, "min": 0, "max": 100, "unit": "%" },
1995
+ { "key": "theme", "type": "select", "label": "Color Theme", "options": ["dark", "light"], "default_value": "dark", "required": false },
1996
+ { "key": "primaryColor", "type": "color", "label": "Primary Color", "default_value": "#43F7CD", "required": false },
1489
1997
  { "key": "showBorder", "type": "boolean", "label": "Show Border", "default_value": true, "required": false },
1998
+ { "key": "targetDate", "type": "string", "label": "Target Date", "default_value": "", "required": false, "visible_when": { "key": "mode", "equals": "countdown" } },
1490
1999
  {
1491
- "key": "theme",
1492
- "type": "select",
1493
- "label": "Color Theme",
1494
- "options": ["dark", "light"],
1495
- "default_value": "dark",
1496
- "required": false
1497
- }
2000
+ "key": "twitch_events", "type": "group", "label": "Twitch Events", "required": false, "collapsed": true,
2001
+ "fields": [
2002
+ { "key": "includeFollowers", "type": "boolean", "label": "Followers", "default_value": true, "required": false },
2003
+ { "key": "includeSubs", "type": "boolean", "label": "Subscriptions", "default_value": true, "required": false }
2004
+ ]
2005
+ },
2006
+ { "key": "separator", "type": "divider", "label": "", "required": false },
2007
+ { "key": "help_text", "type": "info", "label": "", "required": false, "content": "Configure the events you want to display." }
2008
+ ]
2009
+ }
2010
+ \`\`\`
2011
+
2012
+ **Designer Schema:** Extensions can define a \`designer_schema\` alongside \`config_schema\` for a fullpage visual design editor. The designer opens as a fullpage overlay with the schema fields on the left and a live preview on the right. Designer values are stored under \`config.design\`.
2013
+
2014
+ \`\`\`json
2015
+ {
2016
+ "designer_schema": [
2017
+ { "key": "bubbleColor", "type": "color", "label": "Chat Bubble Color", "default_value": "#9146FF", "required": false },
2018
+ { "key": "nameFont", "type": "font", "label": "Username Font", "default_value": "Inter", "required": false },
2019
+ { "key": "badgeSize", "type": "slider", "label": "Badge Size", "default_value": 18, "required": false, "min": 12, "max": 32, "unit": "px" }
1498
2020
  ]
1499
2021
  }
1500
2022
  \`\`\`
@@ -1504,7 +2026,7 @@ Read config values via \`useLumioConfig()\`:
1504
2026
  \`\`\`tsx
1505
2027
  const [config] = useLumioConfig();
1506
2028
  const title = config.title ?? "My Widget";
1507
- const maxItems = config.maxItems ?? 10;
2029
+ const fontSize = config.fontSize ?? 16;
1508
2030
  \`\`\`
1509
2031
 
1510
2032
  **When to use config_schema vs storage:**
@@ -1515,7 +2037,7 @@ const maxItems = config.maxItems ?? 10;
1515
2037
 
1516
2038
  - Always use SDK presets via \`useAnimation()\` + \`<Animated>\` for entrance/exit effects \u2014 never write raw CSS keyframe \`@keyframes\` rules
1517
2039
  - 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
2040
+ - Keep durations short on the layer (< 400ms). Longer animations cause visible frame drops in the Browser Source at 30/60 fps
1519
2041
  - Prefer animating \`opacity\` and \`transform\` (GPU-composited) over \`width\`, \`height\`, \`margin\`, \`padding\` (layout-triggering)
1520
2042
 
1521
2043
  \`\`\`tsx
@@ -1554,6 +2076,120 @@ For beta testing before public launch:
1554
2076
  | Hardcoded strings | Use \`config_schema\` fields for user-customizable text |
1555
2077
  | Using localStorage directly | Use \`useExtensionStorage()\` (localStorage blocked at ext.lumio.vision) |
1556
2078
  | Raw CSS \`@keyframes\` animations | Use \`useAnimation()\` + \`<Animated>\` SDK presets |
2079
+
2080
+ ## Bot Module Security
2081
+
2082
+ ### Rate Limiting Awareness
2083
+
2084
+ Bot module handlers are subject to the same rate limits as other server functions. Plan for these limits in moderation logic:
2085
+
2086
+ - Actions (with fetch): 20/min per installation
2087
+ - Mutations: 50/min per installation
2088
+ - Storage writes: 200/min per installation
2089
+
2090
+ If your module processes high-volume chat, avoid issuing a moderation action on every message. Use \`ctx.cache\` for cooldowns:
2091
+
2092
+ \`\`\`tsx
2093
+ export const spamGuard = moderate({
2094
+ handler: async (ctx, message) => {
2095
+ const key = \`cooldown:\${ctx.user.platform_user_id}\`;
2096
+ if (await ctx.cache.get(key)) return { action: "none" };
2097
+
2098
+ if (isSpam(message.text)) {
2099
+ await ctx.cache.set(key, "1", { ttl: 30 });
2100
+ return { action: "timeout", duration: 60, message: "Spam detected." };
2101
+ }
2102
+ return { action: "none" };
2103
+ },
2104
+ });
2105
+ \`\`\`
2106
+
2107
+ ### Moderation Escalation Pattern
2108
+
2109
+ For moderation modules, escalate actions progressively rather than jumping to bans. Track offense counts in the extension database:
2110
+
2111
+ \`\`\`tsx
2112
+ export const escalatingMod = moderate({
2113
+ handler: async (ctx, message) => {
2114
+ if (!isViolation(message.text)) return { action: "none" };
2115
+
2116
+ const offenses = await ctx.db.get("offenses", ctx.user.platform_user_id);
2117
+ const count = (offenses?.count ?? 0) + 1;
2118
+ await ctx.db.patch("offenses", ctx.user.platform_user_id, { count });
2119
+
2120
+ if (count >= 3) return { action: "ban", message: "Repeated violations." };
2121
+ if (count === 2) return { action: "timeout", duration: 600, message: "Second warning." };
2122
+ return { action: "delete", message: "First warning: message removed." };
2123
+ },
2124
+ });
2125
+ \`\`\`
2126
+
2127
+ ### User Override Preservation
2128
+
2129
+ 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:
2130
+
2131
+ \`\`\`tsx
2132
+ // Good: reads overridable config
2133
+ export const customCmd = command("!points", {
2134
+ handler: async (ctx, message) => {
2135
+ // The prefix "!points" is the default \u2014 users can override it in dashboard
2136
+ // Handler logic stays the same regardless of the configured prefix
2137
+ return { action: "send_message", message: "You have 100 points!" };
2138
+ },
2139
+ });
2140
+ \`\`\`
2141
+
2142
+ Extension developers should document which triggers are overridable in the extension's description so account owners know what they can customize.
2143
+
2144
+ ### Cache-First + Defer Pattern
2145
+
2146
+ 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:
2147
+
2148
+ \`\`\`tsx
2149
+ export const rateLimitedGreet = command("!greet", {
2150
+ handler: async (ctx, message) => {
2151
+ const cacheKey = \`greeted:\${ctx.user.platform_user_id}\`;
2152
+
2153
+ // Fast path: check cache
2154
+ if (await ctx.cache.get(cacheKey)) {
2155
+ return { action: "send_message", message: "You already greeted today!" };
2156
+ }
2157
+
2158
+ // Set cache immediately (fast)
2159
+ await ctx.cache.set(cacheKey, "1", { ttl: 86400 });
2160
+
2161
+ // Defer the DB write (does not block response)
2162
+ ctx.defer(async () => {
2163
+ await ctx.db.insert("greet_log", {
2164
+ user_id: ctx.user.platform_user_id,
2165
+ greeted_at: new Date().toISOString(),
2166
+ });
2167
+ });
2168
+
2169
+ return { action: "send_message", message: \`Hello @\${ctx.user.display_name}!\` };
2170
+ },
2171
+ });
2172
+ \`\`\`
2173
+
2174
+ ### Exempt Role Support
2175
+
2176
+ 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:
2177
+
2178
+ \`\`\`tsx
2179
+ export const linkFilter = moderate({
2180
+ handler: async (ctx, message) => {
2181
+ // Respect exempt roles \u2014 do not moderate mods/VIPs
2182
+ if (ctx.user.roles.moderator || ctx.user.roles.vip) {
2183
+ return { action: "none" };
2184
+ }
2185
+
2186
+ if (containsLink(message.text)) {
2187
+ return { action: "delete", message: "Links are not allowed." };
2188
+ }
2189
+ return { action: "none" };
2190
+ },
2191
+ });
2192
+ \`\`\`
1557
2193
  `;
1558
2194
 
1559
2195
  // 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-QXQNAYHN.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.5.0",
4
4
  "description": "AI agent skill files for building Lumio Extensions",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",