@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.
package/dist/bin/lumio-skills.js
CHANGED
|
@@ -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 /
|
|
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 <-
|
|
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\` |
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
# ?
|
|
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
|
|
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 /
|
|
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
|
|
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
|
|
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
|
|
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": "
|
|
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": "
|
|
1492
|
-
"
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
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
|
|
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
|
|
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