@zaflun/lumio-agent-skill 0.5.0 → 0.6.2
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/README.md +1 -1
- package/dist/bin/lumio-skills.js +1 -1
- package/dist/{chunk-QXQNAYHN.js → chunk-XJZ7H56F.js} +173 -147
- package/dist/index.js +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ This creates `.lumio/skills/` in your project with reference docs, plus agent co
|
|
|
21
21
|
| `server-functions.md` | Server function patterns, ctx API |
|
|
22
22
|
| `cli-workflows.md` | All 12 CLI commands |
|
|
23
23
|
| `auth.md` | Token types, permissions |
|
|
24
|
-
| `runtime-scopes.md` |
|
|
24
|
+
| `runtime-scopes.md` | Storage scopes (instance/overlay/account/global) |
|
|
25
25
|
| `best-practices.md` | Performance, security, UX |
|
|
26
26
|
| `troubleshooting.md` | Common errors and fixes |
|
|
27
27
|
|
package/dist/bin/lumio-skills.js
CHANGED
|
@@ -14,9 +14,9 @@ Lumio Extensions are sandboxed React apps that run inside a 3-layer iframe sandb
|
|
|
14
14
|
\`\`\`
|
|
15
15
|
3-Layer Sandbox:
|
|
16
16
|
Host (lumio.vision / browser source)
|
|
17
|
-
\u2514\u2500\u2500 Supervisor iframe (supervisor.
|
|
18
|
-
\u2514\u2500\u2500 Extension Runtime iframe (ext.lumio.vision)
|
|
19
|
-
\u2514\u2500\u2500 Web Worker (
|
|
17
|
+
\u2514\u2500\u2500 Supervisor iframe (supervisor.ext.lumio.vision)
|
|
18
|
+
\u2514\u2500\u2500 Extension Runtime iframe (<label>.runtime.ext.lumio.vision)
|
|
19
|
+
\u2514\u2500\u2500 Web Worker (the extension's UI code)
|
|
20
20
|
\`\`\`
|
|
21
21
|
|
|
22
22
|
\`\`\`
|
|
@@ -24,17 +24,16 @@ 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
26
|
src/layer.tsx <- Stream overlay surface (transparent, read-only inputs)
|
|
27
|
-
src/interactive.tsx <- Audience interaction page (optional)
|
|
28
27
|
server/schema.ts <- DB schema (defineTable + validators)
|
|
29
28
|
server/functions.ts <- Server functions (queries, mutations, actions)
|
|
30
29
|
\`\`\`
|
|
31
30
|
|
|
32
31
|
**Runtime flow:**
|
|
33
|
-
1. Extension Runtime iframe loads
|
|
32
|
+
1. Extension Runtime iframe loads on the extension's own origin \`<label>.runtime.ext.lumio.vision\` (\`<label>\` = extension UUID as 32 lowercase hex)
|
|
34
33
|
2. \`Lumio.render(<Component />, { target: "layer" })\` must be called within 10 seconds
|
|
35
34
|
3. The custom React reconciler serializes the tree to \`LumioComponentNode[]\` via postMessage to the Supervisor
|
|
36
35
|
4. The Supervisor validates and forwards the node tree to the Host for rendering
|
|
37
|
-
5. Server functions
|
|
36
|
+
5. Server functions run on the Lumio backend (Bot Module Worker) in a V8 isolate (256 MB memory, 5 s per call); a handler's \`ctx.realtime.push(topic, payload)\` reaches every surface of the install through \`useRealtime(topic, cb)\`
|
|
38
37
|
6. Cross-frame messaging uses \`WorkerBridge\` (exported from \`@zaflun/lumio-sdk\`; replaces the legacy \`PostMessageBridge\`)
|
|
39
38
|
|
|
40
39
|
**Key constraints:**
|
|
@@ -47,15 +46,16 @@ Extension Project:
|
|
|
47
46
|
|
|
48
47
|
## Core Concepts
|
|
49
48
|
|
|
50
|
-
###
|
|
49
|
+
### Two Surfaces
|
|
51
50
|
|
|
52
51
|
| Target | Where | Purpose |
|
|
53
52
|
|--------|-------|---------|
|
|
54
53
|
| \`editor\` | Dashboard overlay editor sidebar | Config UI, settings, content management |
|
|
55
54
|
| \`layer\` | Browser Source (transparent) | Visual overlay in stream |
|
|
56
|
-
| \`interactive\` | Standalone page (\`/ext/{slug}\`) | Viewer interaction (votes, games) |
|
|
57
55
|
|
|
58
|
-
|
|
56
|
+
Both surfaces share storage via \`useExtensionStorage()\` \u2014 changes in editor propagate to layer in real-time.
|
|
57
|
+
|
|
58
|
+
**Viewer input** (votes, games) has no page of its own: viewers act through chat commands and channel point rewards. A bot-module handler \u2014 \`command("!vote", \u2026)\` or \`event("twitch:reward" | "kick:reward", \u2026)\` \u2014 writes \`ctx.db\` and calls \`ctx.realtime.push(topic, payload)\`; the layer and editor receive it with \`useRealtime(topic, cb)\`.
|
|
59
59
|
|
|
60
60
|
### lumio.config.json
|
|
61
61
|
|
|
@@ -76,6 +76,10 @@ All three surfaces share storage via \`useExtensionStorage()\` \u2014 changes in
|
|
|
76
76
|
},
|
|
77
77
|
"visibility": "public",
|
|
78
78
|
"pricing": { "type": "free" },
|
|
79
|
+
"sounds": [
|
|
80
|
+
{ "file": "alert.mp3", "name": "Alert" },
|
|
81
|
+
{ "file": "chime.wav", "name": "Chime", "default_volume": 0.8 }
|
|
82
|
+
],
|
|
79
83
|
"config_schema": [
|
|
80
84
|
{ "key": "title", "type": "string", "label": "Widget Title", "default_value": "My Widget", "required": true, "placeholder": "Enter title..." },
|
|
81
85
|
{ "key": "fontSize", "type": "number", "label": "Font Size", "default_value": 16, "required": false, "min": 8, "max": 200, "unit": "px" },
|
|
@@ -95,6 +99,8 @@ All three surfaces share storage via \`useExtensionStorage()\` \u2014 changes in
|
|
|
95
99
|
- \`"visibility": "unlisted"\` \u2014 accessible by direct link only
|
|
96
100
|
- Private invite: grant \`free_access\` to specific accounts via dashboard
|
|
97
101
|
|
|
102
|
+
**Bundled sounds:** Place audio files in \`dist/sounds/\` and declare them in the \`sounds\` array. Supported: MP3, WAV, OGG, WebM, M4A, AAC, FLAC. Default limits: 50 sounds per extension, 10 MB per file, 200 MB total storage (all admin-configurable). Developers can view limits at \`/dashboard/settings/developer/limits\` and request increases via a 4-step wizard. Requests are reviewed by admins in the admin app. Users cannot delete bundled sounds \u2014 uninstalling the extension removes them.
|
|
103
|
+
|
|
98
104
|
**Revenue split:** Configurable per developer (default 70/30 developer/Lumio).
|
|
99
105
|
|
|
100
106
|
**Feature gates:** The following Lumio plan features must be enabled for extension functionality:
|
|
@@ -155,9 +161,9 @@ See \`components.md\` for full props and examples.
|
|
|
155
161
|
|
|
156
162
|
| Hook | Returns | Purpose |
|
|
157
163
|
|------|---------|---------|
|
|
158
|
-
| \`useExtensionStorage()\` | \`[
|
|
159
|
-
| \`useVisionState(key, default)\` | \`[value, setValue]\` |
|
|
160
|
-
| \`useExtensionContext()\` | \`{ extensionId, target, overlayId
|
|
164
|
+
| \`useExtensionStorage(key, initialValue)\` | \`[value, setValue]\` | Persistent KV store, one key per call, synced across targets |
|
|
165
|
+
| \`useVisionState(key, default)\` | \`[value, setValue]\` | Same contract as \`useExtensionStorage\` |
|
|
166
|
+
| \`useExtensionContext()\` | \`{ extensionId, target, overlayId }\` | Extension metadata |
|
|
161
167
|
| \`useLumioEvent(eventType)\` | \`event\` | Live events (chat, alerts, spotify) \u2014 type-safe: \`eventType\` is \`LumioEventType\` |
|
|
162
168
|
| \`useLumioConfig()\` | \`[config, setConfig]\` | Extension config read/write (uses config_schema fields) |
|
|
163
169
|
| \`useLumioTheme()\` | \`theme\` | Current theme (dark/light/custom) |
|
|
@@ -165,10 +171,11 @@ See \`components.md\` for full props and examples.
|
|
|
165
171
|
| \`useLumioAction(action)\` | \`{ execute, data, isLoading, error }\` | Trigger Lumio actions \u2014 type-safe: \`action\` is \`LumioActionType\` |
|
|
166
172
|
| \`useQuery(fn, params?)\` | \`{ data, isLoading, error, refetch }\` | Call server query |
|
|
167
173
|
| \`useMutation(fn)\` | \`{ mutate, mutateAsync, data, isLoading, error }\` | Call server mutation/action |
|
|
174
|
+
| \`useRealtime(topic, cb)\` | \`void\` | Receive \`ctx.realtime.push(topic, payload)\` messages of this install |
|
|
168
175
|
| \`useAnimation(preset, options?)\` | \`animationProps\` | SDK animation presets for \`Animated\` component |
|
|
169
176
|
| \`useIconPicker(options?)\` | \`{ isOpen, selected, open, close, select, clear, sources }\` | Headless state for custom icon picker UIs |
|
|
170
177
|
|
|
171
|
-
**Important:** \`
|
|
178
|
+
**Important:** \`useExtensionStorage\` always takes a key and an initial value \u2014 call it once per key. A setter replaces only its own key; spread only when you store an object under one key: \`setScore({ ...score, home: 2 })\`
|
|
172
179
|
|
|
173
180
|
## Server Functions
|
|
174
181
|
|
|
@@ -183,7 +190,7 @@ export const updateItem = patchRow("items", arg("id"), { done: arg("done") });
|
|
|
183
190
|
export const removeItem = deleteRow("items", arg("id"), { editorOnly: true });
|
|
184
191
|
\`\`\`
|
|
185
192
|
|
|
186
|
-
**Handler-based** (V8 isolate
|
|
193
|
+
**Handler-based** (V8 isolate on the Bot Module Worker, full control):
|
|
187
194
|
\`\`\`typescript
|
|
188
195
|
import { action, arg, v } from "@zaflun/lumio-sdk/server";
|
|
189
196
|
export const fetchScores = action({
|
|
@@ -196,26 +203,17 @@ export const fetchScores = action({
|
|
|
196
203
|
});
|
|
197
204
|
\`\`\`
|
|
198
205
|
|
|
199
|
-
**Extension Actions** via \`useLumioAction()\` (
|
|
206
|
+
**Extension Actions** via \`useLumioAction()\` (dispatched by the Lumio API; \`action\` is \`LumioActionType\`):
|
|
200
207
|
\`\`\`tsx
|
|
201
208
|
// Trigger platform actions (requires matching permission in lumio.config.json)
|
|
202
209
|
const { execute: sendChat } = useLumioAction("chat:send");
|
|
203
|
-
const { execute:
|
|
204
|
-
const { execute: unbanUser } = useLumioAction("chat:unban");
|
|
205
|
-
const { execute: timeoutUser } = useLumioAction("chat:timeout");
|
|
210
|
+
const { execute: deleteChat } = useLumioAction("chat:delete");
|
|
206
211
|
const { execute: setScene } = useLumioAction("obs:set_scene");
|
|
207
|
-
const { execute:
|
|
208
|
-
const { execute:
|
|
212
|
+
const { execute: setSourceVisible } = useLumioAction("obs:set_source_visible");
|
|
213
|
+
const { execute: startStream } = useLumioAction("obs:start_stream");
|
|
214
|
+
const { execute: stopStream } = useLumioAction("obs:stop_stream");
|
|
209
215
|
const { execute: emitEvent } = useLumioAction("events:emit");
|
|
210
216
|
const { execute: updateLayer } = useLumioAction("overlay:update_layer");
|
|
211
|
-
const { execute: spotifyPlay } = useLumioAction("spotify:play");
|
|
212
|
-
const { execute: spotifyPause } = useLumioAction("spotify:pause");
|
|
213
|
-
const { execute: spotifySkip } = useLumioAction("spotify:skip");
|
|
214
|
-
const { execute: spotifySeek } = useLumioAction("spotify:seek");
|
|
215
|
-
const { execute: spotifyVolume } = useLumioAction("spotify:volume");
|
|
216
|
-
const { execute: spotifyShuffle } = useLumioAction("spotify:shuffle");
|
|
217
|
-
const { execute: spotifyRepeat } = useLumioAction("spotify:repeat");
|
|
218
|
-
const { execute: spotifyQueueAdd } = useLumioAction("spotify:queue_add");
|
|
219
217
|
\`\`\`
|
|
220
218
|
|
|
221
219
|
See \`server-functions.md\` for schema definition, storage scoping, and advanced patterns.
|
|
@@ -330,7 +328,7 @@ export const modCheck = moderate({
|
|
|
330
328
|
| \`lumio deploy\` | Upload to Lumio (status: draft). Flags: \`-v\`, \`-d\`, \`--breaking\`, \`--draft\` |
|
|
331
329
|
| \`lumio status\` | Check review status of latest version |
|
|
332
330
|
| \`lumio logs\` | Stream server function logs |
|
|
333
|
-
| \`lumio
|
|
331
|
+
| \`lumio env\` | Manage extension secrets (\`set\` / \`list\` / \`delete\`) |
|
|
334
332
|
| \`lumio team\` | Manage developer team members |
|
|
335
333
|
| \`lumio config\` | Show/set CLI configuration |
|
|
336
334
|
|
|
@@ -345,7 +343,7 @@ See \`cli-workflows.md\` for detailed usage and examples.
|
|
|
345
343
|
|
|
346
344
|
Extension tokens have no RBAC permissions (\`has_permission()\` always returns false) and can only access \`ext_{extension_id}.*\` DB tables via server functions.
|
|
347
345
|
|
|
348
|
-
**\`editorOnly: true\`** on server functions restricts mutations to editor surface only (layer
|
|
346
|
+
**\`editorOnly: true\`** on server functions restricts mutations to editor surface only (the layer cannot call them).
|
|
349
347
|
|
|
350
348
|
See \`auth.md\` for all 9 token types and the full token lifecycle.
|
|
351
349
|
|
|
@@ -363,34 +361,39 @@ lumio deploy -> draft -> [Submit via dashboard] -> pending_review
|
|
|
363
361
|
### Editor + Layer sync
|
|
364
362
|
\`\`\`tsx
|
|
365
363
|
// editor.tsx
|
|
366
|
-
const [
|
|
367
|
-
<Toggle label="Show" checked={
|
|
368
|
-
onChange={(v) => setStorage({ ...storage, visible: v })} />
|
|
364
|
+
const [visible, setVisible] = useExtensionStorage("visible", false);
|
|
365
|
+
<Toggle label="Show" checked={visible} onChange={setVisible} />
|
|
369
366
|
|
|
370
367
|
// layer.tsx
|
|
371
|
-
const [
|
|
372
|
-
|
|
373
|
-
|
|
368
|
+
const [visible] = useExtensionStorage("visible", false);
|
|
369
|
+
const [text] = useExtensionStorage("text", "");
|
|
370
|
+
if (!visible) return null;
|
|
371
|
+
return <Text content={text} />;
|
|
374
372
|
\`\`\`
|
|
375
373
|
|
|
376
374
|
### Live events
|
|
377
375
|
\`\`\`tsx
|
|
378
|
-
// eventType is LumioEventType \u2014 autocomplete shows all
|
|
379
|
-
const event = useLumioEvent("twitch:
|
|
380
|
-
// event updates automatically when a new
|
|
376
|
+
// eventType is LumioEventType \u2014 autocomplete shows all 62 valid event strings
|
|
377
|
+
const event = useLumioEvent("twitch:cheer");
|
|
378
|
+
// event updates automatically when a new cheer arrives; it is typed
|
|
379
|
+
// LumioEventPayloadMap["twitch:cheer"] | null, so event?.bits is a number
|
|
381
380
|
\`\`\`
|
|
382
381
|
|
|
383
|
-
**All
|
|
382
|
+
**All 62 event types** (\`LumioEventType\`):
|
|
383
|
+
|
|
384
|
+
Twitch (27): \`twitch:follower\`, \`twitch:subscribe\`, \`twitch:gift\`, \`twitch:resub\`, \`twitch:cheer\`, \`twitch:raid\`, \`twitch:reward\`, \`twitch:reward_status\`, \`twitch:reward_add\`, \`twitch:reward_update\`, \`twitch:reward_remove\`, \`twitch:hype_train\`, \`twitch:hype_train_end\`, \`twitch:poll\`, \`twitch:poll_end\`, \`twitch:prediction\`, \`twitch:prediction_lock\`, \`twitch:prediction_end\`, \`twitch:goal\`, \`twitch:goal_end\`, \`twitch:ad_break\`, \`twitch:ban\`, \`twitch:unban\`, \`twitch:stream_online\`, \`twitch:stream_offline\`, \`twitch:stream_update\`, \`twitch:chat\`
|
|
384
385
|
|
|
385
|
-
|
|
386
|
+
YouTube (10): \`youtube:subscribe\`, \`youtube:member\`, \`youtube:superchat\`, \`youtube:supersticker\`, \`youtube:gift_membership\`, \`youtube:gift_membership_received\`, \`youtube:poll\`, \`youtube:stream_online\`, \`youtube:stream_offline\`, \`youtube:chat\`
|
|
386
387
|
|
|
387
|
-
|
|
388
|
+
Kick (11): \`kick:follower\`, \`kick:subscribe\`, \`kick:resub\`, \`kick:gift\`, \`kick:kicks\`, \`kick:reward\`, \`kick:reward_status\`, \`kick:stream_online\`, \`kick:stream_offline\`, \`kick:stream_update\`, \`kick:chat\`
|
|
388
389
|
|
|
389
|
-
|
|
390
|
+
Spotify (12): \`spotify:track\`, \`spotify:play\`, \`spotify:pause\`, \`spotify:skip\`, \`spotify:volume\`, \`spotify:queue_add\`, \`spotify:device\`, \`spotify:playlist_add\`, \`spotify:playlist_remove\`, \`spotify:playlist_create\`, \`spotify:playlist_edit\`, \`spotify:playlist_delete\`
|
|
390
391
|
|
|
391
|
-
|
|
392
|
+
StreamElements (1): \`streamelements:tip\`
|
|
392
393
|
|
|
393
|
-
|
|
394
|
+
Cross-platform chat (1, fires for every platform): \`chat:message\`
|
|
395
|
+
|
|
396
|
+
Kick events carry canonical top-level fields (\`user_id\`, \`user_login\`, \`user_name\`, \`avatarUrl\`, \`is_anonymous\`) plus the original Kick payload. \`kick:kicks\` has \`kicks\` (amount, like Twitch \`bits\`) and \`message\`; \`kick:reward\` / \`kick:reward_status\` have \`status\`, \`user_input\` and \`reward { id, title, cost, prompt }\`; \`kick:resub\` has \`cumulative_months\`. \`twitch:reward_status\` has the \`twitch:reward\` shape with \`status\` \`fulfilled\` / \`canceled\`. \`twitch:reward_add\` / \`_update\` / \`_remove\` carry the Twitch reward (\`id\`, \`title\`, \`cost\`, \`prompt\`, \`is_enabled\`, \`is_paused\`, \u2026); these three arrive live only (\`broadcast_only\`), never from the event history.
|
|
394
397
|
|
|
395
398
|
### Server queries
|
|
396
399
|
\`\`\`tsx
|
|
@@ -420,8 +423,8 @@ const anim = useAnimation("slide-in", { duration: 300 });
|
|
|
420
423
|
| \`EXTENSION_NOT_FOUND\` | Invalid extensionId in config | Run \`lumio init\` or create via dashboard |
|
|
421
424
|
| \`EGRESS_BLOCKED\` | Fetch to undeclared host | Add host to \`egress.allowHosts\` in config |
|
|
422
425
|
| \`STORAGE_QUOTA\` | Exceeded storage limit | Reduce data or request limit increase |
|
|
423
|
-
| \`EDITOR_ONLY\` | Mutation called from layer
|
|
424
|
-
| \`SUPERVISOR_BLOCKED\` | Supervisor rejected a postMessage | Check message origin matches ext.lumio.vision |
|
|
426
|
+
| \`EDITOR_ONLY\` | Mutation called from the layer | Remove \`editorOnly\` or restrict to editor |
|
|
427
|
+
| \`SUPERVISOR_BLOCKED\` | Supervisor rejected a postMessage | Check message origin matches the extension's runtime origin (\`<label>.runtime.ext.lumio.vision\`) |
|
|
425
428
|
| \`WORKER_CREATION_FAILED\` | Web Worker could not be created | Verify server functions bundle is valid JS |
|
|
426
429
|
| \`STORAGE_DOM_EXCEPTION\` | localStorage/sessionStorage access | Use \`useExtensionStorage()\` instead (storage blocked) |
|
|
427
430
|
|
|
@@ -815,18 +818,18 @@ import { IconPicker, IconDisplay } from "@zaflun/lumio-sdk";
|
|
|
815
818
|
import type { IconValue } from "@zaflun/lumio-sdk";
|
|
816
819
|
|
|
817
820
|
// In editor.tsx \u2014 let the user pick an icon
|
|
818
|
-
const [
|
|
821
|
+
const [icon, setIcon] = useExtensionStorage<IconValue | null>("icon", null);
|
|
819
822
|
|
|
820
823
|
<IconPicker
|
|
821
|
-
value={
|
|
822
|
-
onChange={
|
|
824
|
+
value={icon}
|
|
825
|
+
onChange={setIcon}
|
|
823
826
|
sources={["lucide", "emoji", "emote", "upload"]}
|
|
824
827
|
size="md"
|
|
825
828
|
placeholder="Choose an icon..."
|
|
826
829
|
/>
|
|
827
830
|
|
|
828
831
|
// In layer.tsx \u2014 render the selected icon
|
|
829
|
-
<IconDisplay icon={
|
|
832
|
+
<IconDisplay icon={icon} size={32} />
|
|
830
833
|
\`\`\`
|
|
831
834
|
|
|
832
835
|
**IconPickerProps:**
|
|
@@ -949,9 +952,12 @@ import { queryRows, insertRow, patchRow, deleteRow, arg, filter } from "@zaflun/
|
|
|
949
952
|
// Read all rows
|
|
950
953
|
export const getItems = queryRows("items");
|
|
951
954
|
|
|
952
|
-
// Read with filter
|
|
955
|
+
// Read with filter, order and paging
|
|
953
956
|
export const getDoneItems = queryRows("items", {
|
|
954
957
|
filter: filter("done", "=", true),
|
|
958
|
+
orderBy: "created_at",
|
|
959
|
+
orderDir: "desc",
|
|
960
|
+
limit: arg("limit"),
|
|
955
961
|
});
|
|
956
962
|
|
|
957
963
|
// Insert a row (editorOnly: only callable from editor surface)
|
|
@@ -974,8 +980,10 @@ export const removeItem = deleteRow("items", arg("id"), { editorOnly: true });
|
|
|
974
980
|
|
|
975
981
|
- \`arg("name")\` \u2014 reads from the client-provided arguments
|
|
976
982
|
- \`identity()\` \u2014 returns \`{ userId, accountId }\` of the caller
|
|
977
|
-
- \`secret("KEY")\` \u2014 reads a server-side secret (
|
|
978
|
-
- \`filter(field, op, value)\` \u2014 query filter
|
|
983
|
+
- \`secret("KEY")\` \u2014 reads a server-side secret in \`action()\` handlers only; \`lumio build\` rejects it in declarative functions
|
|
984
|
+
- \`filter(field, op, value)\` \u2014 query filter, \`op\` one of \`=\`, \`!=\`, \`>\`, \`>=\`, \`<\`, \`<=\`, \`like\`, \`in\`; pass a list for AND
|
|
985
|
+
- \`queryRows\` options: \`filter\`, \`orderBy\`, \`orderDir\` (\`asc\`/\`desc\`), \`limit\` / \`offset\` (integer or \`arg()\`)
|
|
986
|
+
- Definitions must be plain JSON: \`lumio build\` serialises every export of \`server/functions.ts\` to \`dist/functions.json\` and fails on anything else (keep helpers in another module)
|
|
979
987
|
|
|
980
988
|
## Handler-Based Functions
|
|
981
989
|
|
|
@@ -1005,45 +1013,48 @@ export const submitScore = mutation({
|
|
|
1005
1013
|
editorOnly: false,
|
|
1006
1014
|
});
|
|
1007
1015
|
|
|
1008
|
-
//
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
args: { sport: v.string(), league: v.string() },
|
|
1016
|
+
// Push a live update to every surface of the install (overlay, widget, editor)
|
|
1017
|
+
export const castVote = mutation({
|
|
1018
|
+
args: { choice: v.string() },
|
|
1012
1019
|
handler: async (ctx, args) => {
|
|
1013
|
-
|
|
1014
|
-
const
|
|
1015
|
-
|
|
1016
|
-
{ headers: { "Authorization": \`Bearer \${apiKey}\` } }
|
|
1017
|
-
);
|
|
1018
|
-
return res.json();
|
|
1020
|
+
await ctx.db.insert("votes", { choice: args.choice });
|
|
1021
|
+
const votes = await ctx.db.list("votes");
|
|
1022
|
+
ctx.realtime.push("poll:votes", { total: votes.length });
|
|
1019
1023
|
},
|
|
1020
1024
|
});
|
|
1021
1025
|
\`\`\`
|
|
1022
1026
|
|
|
1027
|
+
Receive the push in any surface with \`useRealtime\`:
|
|
1028
|
+
|
|
1029
|
+
\`\`\`tsx
|
|
1030
|
+
import { useRealtime } from "@zaflun/lumio-sdk";
|
|
1031
|
+
|
|
1032
|
+
useRealtime<{ total: number }>("poll:votes", (payload) => setTotal(payload.total));
|
|
1033
|
+
\`\`\`
|
|
1034
|
+
|
|
1023
1035
|
### Handler Context (\`ctx\`)
|
|
1024
1036
|
|
|
1025
|
-
Handler-based functions run
|
|
1037
|
+
Handler-based functions run in a V8 isolate on the Bot Module Worker (never in the API process or the browser). The \`ctx\` object:
|
|
1026
1038
|
|
|
1027
1039
|
| Property | Description |
|
|
1028
1040
|
|----------|-------------|
|
|
1029
|
-
| \`ctx.db.get(table, id)\` | Read
|
|
1030
|
-
| \`ctx.db.insert(table, data)\` |
|
|
1031
|
-
| \`ctx.
|
|
1032
|
-
| \`ctx.
|
|
1033
|
-
| \`ctx.
|
|
1034
|
-
| \`ctx.
|
|
1035
|
-
| \`ctx.auth\` | \`{ userId, accountId,
|
|
1041
|
+
| \`ctx.db.get(table, id)\` / \`list(table, filters?)\` | Read rows of the extension's tables (install's account only) |
|
|
1042
|
+
| \`ctx.db.insert(table, data)\` / \`patch\` / \`delete\` | Write rows |
|
|
1043
|
+
| \`ctx.realtime.push(topic, payload?)\` | Push \`{ topic, payload }\` to every rendered surface of the install (\`useRealtime\`). Topic: 1-64 chars of \`A-Z a-z 0-9 _ . : -\`; payload \u2264 16 KiB; \u2264 32 per call, 300 per install and minute. Sent after the handler returns, only if it did not throw |
|
|
1044
|
+
| \`ctx.cache\` / \`ctx.defer\` | Install-scoped cache and background jobs, as in bot modules |
|
|
1045
|
+
| \`ctx.secrets.get(key)\` | The extension's owner secret (\`lumio env set\`), or \`null\` |
|
|
1046
|
+
| \`ctx.fetch(url, options?)\` | Refuses every host in server functions (no egress allow-list yet); call external APIs from a bot module |
|
|
1047
|
+
| \`ctx.auth\` | \`{ userId, accountId, role }\` \u2014 \`userId\` is \`null\` for overlay, widget and extension tokens |
|
|
1036
1048
|
|
|
1037
1049
|
### Sandbox Limits
|
|
1038
1050
|
|
|
1039
1051
|
| Limit | Value |
|
|
1040
1052
|
|-------|-------|
|
|
1041
1053
|
| Memory | 256 MB per invocation |
|
|
1042
|
-
|
|
|
1054
|
+
| Wall-clock timeout | 5 seconds per call |
|
|
1043
1055
|
| Filesystem | No access |
|
|
1044
|
-
| Network |
|
|
1056
|
+
| Network | None for server functions (\`ctx.fetch\` refuses every host) |
|
|
1045
1057
|
| Dynamic code | Blocked (\`eval\`, \`Function\`, \`import()\`) |
|
|
1046
|
-
| Storage APIs | localStorage/sessionStorage blocked (separate origin) |
|
|
1047
1058
|
|
|
1048
1059
|
### Rate Limits
|
|
1049
1060
|
|
|
@@ -1063,46 +1074,41 @@ import { useLumioAction } from "@zaflun/lumio-sdk";
|
|
|
1063
1074
|
|
|
1064
1075
|
// Send a chat message on the connected platform
|
|
1065
1076
|
const { execute: sendChat, isLoading, error } = useLumioAction("chat:send");
|
|
1066
|
-
sendChat({
|
|
1077
|
+
sendChat({
|
|
1078
|
+
platform: "twitch",
|
|
1079
|
+
channel_id: "primary",
|
|
1080
|
+
message: "Hello from the extension!"
|
|
1081
|
+
});
|
|
1067
1082
|
|
|
1068
1083
|
// Switch OBS scene (requires OBS WebSocket integration)
|
|
1069
1084
|
const { execute: setScene } = useLumioAction("obs:set_scene");
|
|
1070
|
-
setScene({
|
|
1085
|
+
setScene({ scene_name: "Main Scene" });
|
|
1071
1086
|
|
|
1072
1087
|
// Emit a custom event into the Lumio event bus
|
|
1073
1088
|
const { execute: emitEvent } = useLumioAction("events:emit");
|
|
1074
|
-
emitEvent({
|
|
1089
|
+
emitEvent({ event_type: "extension:custom", data: { score: 42 } });
|
|
1075
1090
|
|
|
1076
1091
|
// Update a specific layer in the overlay
|
|
1077
1092
|
const { execute: updateLayer } = useLumioAction("overlay:update_layer");
|
|
1078
|
-
updateLayer({
|
|
1093
|
+
updateLayer({
|
|
1094
|
+
overlay_id: "overlay-uuid",
|
|
1095
|
+
layer_id: "layer-uuid",
|
|
1096
|
+
config: { visible: true }
|
|
1097
|
+
});
|
|
1079
1098
|
\`\`\`
|
|
1080
1099
|
|
|
1081
|
-
**Available actions (
|
|
1100
|
+
**Available dispatched actions (\`action\` is \`LumioActionType\`):**
|
|
1082
1101
|
|
|
1083
1102
|
| Action | Permission required | Description |
|
|
1084
1103
|
|--------|--------------------|----|
|
|
1085
1104
|
| \`chat:send\` | \`chat:send\` | Send chat message on connected platform |
|
|
1086
1105
|
| \`chat:delete\` | \`chat:delete\` | Delete a chat message |
|
|
1087
|
-
| \`
|
|
1088
|
-
| \`
|
|
1089
|
-
| \`
|
|
1090
|
-
| \`obs:
|
|
1091
|
-
| \`
|
|
1092
|
-
| \`
|
|
1093
|
-
| \`obs:stop_stream\` | \`obs:control\` | Stop OBS streaming |
|
|
1094
|
-
| \`obs:start_recording\` | \`obs:control\` | Start OBS recording |
|
|
1095
|
-
| \`obs:stop_recording\` | \`obs:control\` | Stop OBS recording |
|
|
1096
|
-
| \`events:emit\` | \`events:write\` | Emit event into Lumio event bus |
|
|
1097
|
-
| \`overlay:update_layer\` | \`overlays:edit\` | Update overlay layer properties |
|
|
1098
|
-
| \`spotify:play\` | \`spotify:control\` | Resume Spotify playback |
|
|
1099
|
-
| \`spotify:pause\` | \`spotify:control\` | Pause Spotify playback |
|
|
1100
|
-
| \`spotify:skip\` | \`spotify:control\` | Skip to next track |
|
|
1101
|
-
| \`spotify:seek\` | \`spotify:control\` | Seek to position in current track |
|
|
1102
|
-
| \`spotify:volume\` | \`spotify:control\` | Set Spotify volume |
|
|
1103
|
-
| \`spotify:shuffle\` | \`spotify:control\` | Toggle shuffle mode |
|
|
1104
|
-
| \`spotify:repeat\` | \`spotify:control\` | Set repeat mode |
|
|
1105
|
-
| \`spotify:queue_add\` | \`spotify:control\` | Add track to Spotify queue |
|
|
1106
|
+
| \`obs:set_scene\` | \`obs:set_scene\` | Switch OBS scene |
|
|
1107
|
+
| \`obs:set_source_visible\` | \`obs:set_source_visible\` | Show or hide an OBS source |
|
|
1108
|
+
| \`obs:start_stream\` | \`obs:start_stream\` | Start OBS streaming |
|
|
1109
|
+
| \`obs:stop_stream\` | \`obs:stop_stream\` | Stop OBS streaming |
|
|
1110
|
+
| \`events:emit\` | \`events:emit\` | Emit event into Lumio event bus |
|
|
1111
|
+
| \`overlay:update_layer\` | \`overlay:update_layer\` | Update overlay layer properties |
|
|
1106
1112
|
|
|
1107
1113
|
Actions are proxied through the Lumio API \u2014 the extension never accesses OAuth tokens or platform credentials directly.
|
|
1108
1114
|
|
|
@@ -1149,7 +1155,8 @@ export default defineAutomationTrigger({
|
|
|
1149
1155
|
const lastCheck = await ctx.cache.get("last_check") ?? "1970-01-01T00:00:00Z";
|
|
1150
1156
|
const res = await ctx.fetch(
|
|
1151
1157
|
\`https://api.example.com/events?since=\${lastCheck}\`,
|
|
1152
|
-
|
|
1158
|
+
// Automation nodes get no secrets yet: the token comes from the node config.
|
|
1159
|
+
{ headers: { "Authorization": \`Bearer \${ctx.nodeConfig.apiToken}\` } }
|
|
1153
1160
|
);
|
|
1154
1161
|
const events = await res.json();
|
|
1155
1162
|
await ctx.cache.set("last_check", new Date().toISOString());
|
|
@@ -1183,7 +1190,7 @@ export default defineAutomationLogic({
|
|
|
1183
1190
|
| \`ctx.inputData\` | Action, Logic | Data from the upstream node |
|
|
1184
1191
|
| \`ctx.fetch(url, options?)\` | All | HTTP request (egress allowlist enforced) |
|
|
1185
1192
|
| \`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 |
|
|
1193
|
+
| \`ctx.secrets.get(key)\` | All | Not wired for automation nodes yet: always \`null\` |
|
|
1187
1194
|
| \`ctx.db\` | All | Extension DB schema access |
|
|
1188
1195
|
| \`ctx.auth\` | All | \`{ accountId }\` \u2014 account identity |
|
|
1189
1196
|
| \`ctx.defer()\` | Action | Mark execution as async (returns 202, completes in background) |
|
|
@@ -1340,7 +1347,7 @@ The extension must declare the matching permission in \`lumio.config.json\`. For
|
|
|
1340
1347
|
| \`chat:delete\` | Delete chat messages | Enhanced |
|
|
1341
1348
|
| \`chat:ban\` | Ban, unban, and timeout users | Enhanced |
|
|
1342
1349
|
| \`events:read\` | Subscribe to platform events | Standard |
|
|
1343
|
-
| \`events:
|
|
1350
|
+
| \`events:emit\` | Emit custom events | Standard |
|
|
1344
1351
|
`;
|
|
1345
1352
|
|
|
1346
1353
|
// src/content/cli-workflows.ts
|
|
@@ -1448,7 +1455,6 @@ Automation nodes always generate \`server/handler.ts\` (not \`server/functions.t
|
|
|
1448
1455
|
| Blank | editor + layer | No | Minimal starting point |
|
|
1449
1456
|
| Widget | editor + layer | Yes | Widget with storage |
|
|
1450
1457
|
| Full-Stack | editor + layer | Yes | Complete extension with server functions |
|
|
1451
|
-
| Interactive Game | editor + layer + interactive | Yes | Viewer interaction game |
|
|
1452
1458
|
| Bot Module | server-only | Yes | Chat command module |
|
|
1453
1459
|
| Automation Action | server + editor | Yes | Automation workflow action node |
|
|
1454
1460
|
| Automation Trigger | server + editor | Yes | Automation workflow trigger node |
|
|
@@ -1465,9 +1471,8 @@ lumio dev
|
|
|
1465
1471
|
|
|
1466
1472
|
# Output:
|
|
1467
1473
|
# Lumio Dev Server running at http://localhost:5173
|
|
1468
|
-
# Editor:
|
|
1469
|
-
# Layer:
|
|
1470
|
-
# Interactive: http://localhost:5173/interactive
|
|
1474
|
+
# Editor: http://localhost:5173/editor
|
|
1475
|
+
# Layer: http://localhost:5173/layer
|
|
1471
1476
|
#
|
|
1472
1477
|
# Mock events available. Press 'e' to emit a test event.
|
|
1473
1478
|
# Press 'q' to quit.
|
|
@@ -1503,19 +1508,31 @@ Features:
|
|
|
1503
1508
|
|
|
1504
1509
|
## lumio build
|
|
1505
1510
|
|
|
1506
|
-
Bundle the extension
|
|
1511
|
+
Bundle the extension and create source archive.
|
|
1507
1512
|
|
|
1508
1513
|
\`\`\`bash
|
|
1509
1514
|
lumio build
|
|
1510
1515
|
|
|
1511
|
-
# Output:
|
|
1512
|
-
#
|
|
1513
|
-
#
|
|
1514
|
-
#
|
|
1515
|
-
#
|
|
1516
|
-
#
|
|
1516
|
+
# Output for widgets:
|
|
1517
|
+
# Built: dist/editor.js
|
|
1518
|
+
# Built: dist/layer.js
|
|
1519
|
+
# Built: dist/server.js + dist/functions.json (if server: true)
|
|
1520
|
+
# Compiled: dist/styles.css (if Tailwind detected)
|
|
1521
|
+
# Archive: dist/source.tar.gz
|
|
1522
|
+
#
|
|
1523
|
+
# Output for bot modules (server: true):
|
|
1524
|
+
# Bot module \u2014 no UI targets to bundle.
|
|
1525
|
+
# Built: dist/server.js
|
|
1526
|
+
# Archive: dist/source.tar.gz
|
|
1517
1527
|
\`\`\`
|
|
1518
1528
|
|
|
1529
|
+
Build steps:
|
|
1530
|
+
1. UI targets (\`editor\`, \`layer\`, \`designer\`) \u2014 esbuild IIFE bundles
|
|
1531
|
+
2. Server handler (\`server: true\`) \u2014 compiles \`server/handler.ts\` (or \`server/index.ts\`, else \`server/functions.ts\`) to \`dist/server.js\` (ESM)
|
|
1532
|
+
3. Functions manifest (\`server: true\`, not bot modules / automation nodes) \u2014 evaluates \`server/functions.ts\` and writes every export to \`dist/functions.json\` (\`{ version: 1, functions: { name: { kind, category, editorOnly, table?, \u2026 } } }\`; handlers become \`{ kind: "handler", category }\` stubs). Fails on a non-definition export, a non-JSON value, \`secret()\` in a declarative function, a table missing from \`server/schema.ts\`, or names that differ only in case. \`lumio deploy\` uploads it with the version.
|
|
1533
|
+
4. CSS \u2014 Tailwind v4 auto-detected and compiled, or plain CSS copied
|
|
1534
|
+
5. Source archive \u2014 \`dist/source.tar.gz\`
|
|
1535
|
+
|
|
1519
1536
|
Warns if \`CHANGELOG.md\` is missing.
|
|
1520
1537
|
|
|
1521
1538
|
## lumio deploy
|
|
@@ -1539,6 +1556,11 @@ lumio deploy --breaking -v 2.0.0
|
|
|
1539
1556
|
lumio deploy --draft
|
|
1540
1557
|
\`\`\`
|
|
1541
1558
|
|
|
1559
|
+
Pre-flight checks before upload:
|
|
1560
|
+
- Sound files validated (format, existence in \`dist/sounds/\`, per-file size against limit)
|
|
1561
|
+
- Sound count, per-file size, and total storage checked against resolved limits from API
|
|
1562
|
+
- Manifest validated against schema
|
|
1563
|
+
|
|
1542
1564
|
After deploy:
|
|
1543
1565
|
1. Version status = \`draft\`
|
|
1544
1566
|
2. Submit for review via dashboard (\`/dashboard/extensions/{slug}/submit\`)
|
|
@@ -1606,25 +1628,27 @@ lumio logs --filter fetchScores
|
|
|
1606
1628
|
lumio logs --level error
|
|
1607
1629
|
\`\`\`
|
|
1608
1630
|
|
|
1609
|
-
## lumio
|
|
1631
|
+
## lumio env
|
|
1610
1632
|
|
|
1611
|
-
Manage
|
|
1633
|
+
Manage the extension's secrets. Bot-module handlers read them via \`await ctx.secrets.get("KEY")\` (\`null\` when unset). Server functions and automation nodes do not get secrets yet (\`ctx.secrets.get\` returns \`null\` there).
|
|
1612
1634
|
|
|
1613
1635
|
\`\`\`bash
|
|
1614
|
-
# Set a secret
|
|
1615
|
-
lumio
|
|
1636
|
+
# Set a secret (prompts with hidden input when VALUE is omitted)
|
|
1637
|
+
lumio env set ESPN_API_KEY
|
|
1638
|
+
|
|
1639
|
+
# Non-interactive (CI): value from LUMIO_SECRET, keeps it out of shell history
|
|
1640
|
+
LUMIO_SECRET="$ESPN_API_KEY" lumio env set ESPN_API_KEY
|
|
1616
1641
|
|
|
1617
1642
|
# List secret keys (values are never shown)
|
|
1618
|
-
lumio
|
|
1643
|
+
lumio env list
|
|
1619
1644
|
# Output:
|
|
1620
|
-
#
|
|
1621
|
-
# WEATHER_TOKEN (set 2026-04-01)
|
|
1645
|
+
# ESPN_API_KEY (created 2026-05-19T00:00:00Z)
|
|
1622
1646
|
|
|
1623
1647
|
# Delete a secret
|
|
1624
|
-
lumio
|
|
1648
|
+
lumio env delete ESPN_API_KEY
|
|
1625
1649
|
\`\`\`
|
|
1626
1650
|
|
|
1627
|
-
|
|
1651
|
+
Keys match \`^[A-Z0-9_]{1,64}$\`; values are 1 to 4096 bytes. A developer API key (\`lm_dev_\`) may manage secrets, so this works in CI. Secrets are stored encrypted (AES-256-GCM), one set per extension shared by every install, and are never returned by the API or exposed to the frontend or other extensions.
|
|
1628
1652
|
|
|
1629
1653
|
## lumio team
|
|
1630
1654
|
|
|
@@ -1700,14 +1724,14 @@ AuthContext::ExtensionToken {
|
|
|
1700
1724
|
extension_id: Uuid,
|
|
1701
1725
|
install_id: Uuid,
|
|
1702
1726
|
account_id: Uuid,
|
|
1703
|
-
target: "editor" | "layer"
|
|
1727
|
+
target: "editor" | "layer",
|
|
1704
1728
|
}
|
|
1705
1729
|
\`\`\`
|
|
1706
1730
|
|
|
1707
1731
|
### Token Lifecycle
|
|
1708
1732
|
|
|
1709
1733
|
1. User opens extension in dashboard/overlay \u2014 Lumio server generates \`lm_ext_*\` token
|
|
1710
|
-
2. Token
|
|
1734
|
+
2. Token handed to the extension inside the sandbox; the Extension Runtime iframe runs on the extension's own origin \`<label>.runtime.ext.lumio.vision\` (\`<label>\` = extension UUID as 32 lowercase hex). The extension \`slug\` includes the \`short_id\` prefix (e.g. \`k8f2x-my-extension\`); the store URL is \`/store/k8f2x-my-extension\`
|
|
1711
1735
|
3. SDK uses token for all API calls (server functions, storage, events)
|
|
1712
1736
|
4. Token auto-refreshes via postMessage every 12 minutes (before 15-min expiry) \u2014 the Supervisor mediates the refresh
|
|
1713
1737
|
5. On page reload, a new token is generated
|
|
@@ -1741,7 +1765,7 @@ Widget tokens are suitable for persistent, public-facing embeds where session re
|
|
|
1741
1765
|
Server functions marked with \`editorOnly: true\` can only be called from the \`editor\` surface.
|
|
1742
1766
|
|
|
1743
1767
|
\`\`\`typescript
|
|
1744
|
-
// Only callable from editor.tsx \u2014 layer.tsx
|
|
1768
|
+
// Only callable from editor.tsx \u2014 layer.tsx gets 403
|
|
1745
1769
|
export const addItem = insertRow("items", { text: arg("text") }, { editorOnly: true });
|
|
1746
1770
|
|
|
1747
1771
|
// Callable from any surface
|
|
@@ -1784,11 +1808,11 @@ Extensions run inside a 3-layer iframe sandbox. Understanding the layers is impo
|
|
|
1784
1808
|
|
|
1785
1809
|
\`\`\`
|
|
1786
1810
|
Host (lumio.vision / browser source)
|
|
1787
|
-
\u2514\u2500\u2500 Supervisor iframe (supervisor.
|
|
1811
|
+
\u2514\u2500\u2500 Supervisor iframe (supervisor.ext.lumio.vision)
|
|
1788
1812
|
Purpose: validates postMessage traffic, enforces egress allowlist,
|
|
1789
1813
|
mediates token refresh, proxies LumioComponentNode[] to host
|
|
1790
|
-
\u2514\u2500\u2500 Extension Runtime iframe (ext.lumio.vision)
|
|
1791
|
-
Purpose: runs extension code on
|
|
1814
|
+
\u2514\u2500\u2500 Extension Runtime iframe (<label>.runtime.ext.lumio.vision)
|
|
1815
|
+
Purpose: runs extension code on its own origin (one per extension)
|
|
1792
1816
|
Storage: localStorage/sessionStorage BLOCKED (cross-origin isolation)
|
|
1793
1817
|
\u2514\u2500\u2500 Web Worker
|
|
1794
1818
|
Purpose: V8 isolate for server function execution
|
|
@@ -1801,8 +1825,9 @@ Host (lumio.vision / browser source)
|
|
|
1801
1825
|
- Mediates \`lm_ext_*\` token refresh (12 min interval, before 15 min expiry)
|
|
1802
1826
|
- Forwards serialized \`LumioComponentNode[]\` to the Host renderer
|
|
1803
1827
|
|
|
1804
|
-
**Extension Runtime (ext.lumio.vision) origin isolation:**
|
|
1828
|
+
**Extension Runtime (<label>.runtime.ext.lumio.vision) origin isolation:**
|
|
1805
1829
|
- Runs on a separate origin from the Host \u2014 this prevents direct DOM access and localStorage sharing
|
|
1830
|
+
- Every extension gets its own origin (\`<label>\` = extension UUID as 32 lowercase hex), so two extensions never share browser state
|
|
1806
1831
|
- \`localStorage\` and \`sessionStorage\` throw \`DOMException: Access is denied\` \u2014 use \`useExtensionStorage()\` instead
|
|
1807
1832
|
- \`document.cookie\` is unavailable
|
|
1808
1833
|
- Cross-origin postMessage to the Host goes through the Supervisor
|
|
@@ -1900,8 +1925,8 @@ var bestPracticesContent = `# Best Practices
|
|
|
1900
1925
|
## Performance
|
|
1901
1926
|
|
|
1902
1927
|
### Minimize renders
|
|
1903
|
-
-
|
|
1904
|
-
-
|
|
1928
|
+
- Call \`useExtensionStorage(key, initialValue)\` once per value \u2014 each key loads, syncs and re-renders on its own
|
|
1929
|
+
- Debounce text inputs \u2014 every setter call is one storage write
|
|
1905
1930
|
- Use \`useMemo\` and \`useCallback\` to avoid unnecessary re-renders
|
|
1906
1931
|
|
|
1907
1932
|
### Optimize server function calls
|
|
@@ -1926,22 +1951,21 @@ Always use \`useExtensionStorage()\` for state that should be visible on both ed
|
|
|
1926
1951
|
|
|
1927
1952
|
\`\`\`tsx
|
|
1928
1953
|
// editor.tsx \u2014 user configures
|
|
1929
|
-
const [
|
|
1930
|
-
<TextField label="Title" value={
|
|
1931
|
-
onChange={(v) => setStorage({ ...storage, title: v })} />
|
|
1954
|
+
const [title, setTitle] = useExtensionStorage("title", "Default Title");
|
|
1955
|
+
<TextField label="Title" value={title} onChange={setTitle} />
|
|
1932
1956
|
|
|
1933
1957
|
// layer.tsx \u2014 displays result
|
|
1934
|
-
const [
|
|
1935
|
-
<Text content={
|
|
1958
|
+
const [title] = useExtensionStorage("title", "Default Title");
|
|
1959
|
+
<Text content={title} variant="heading" />
|
|
1936
1960
|
\`\`\`
|
|
1937
1961
|
|
|
1938
1962
|
### Graceful defaults
|
|
1939
|
-
|
|
1963
|
+
Pass a sensible \`initialValue\` for every key. The hook returns it until the stored value has loaded, and for a key that was never stored.
|
|
1940
1964
|
|
|
1941
1965
|
\`\`\`tsx
|
|
1942
|
-
const visible =
|
|
1943
|
-
const maxItems =
|
|
1944
|
-
const theme =
|
|
1966
|
+
const [visible] = useExtensionStorage("visible", true);
|
|
1967
|
+
const [maxItems] = useExtensionStorage("maxItems", 10);
|
|
1968
|
+
const [theme] = useExtensionStorage("theme", "dark");
|
|
1945
1969
|
\`\`\`
|
|
1946
1970
|
|
|
1947
1971
|
### Loading states
|
|
@@ -2069,12 +2093,13 @@ For beta testing before public launch:
|
|
|
2069
2093
|
| Mistake | Fix |
|
|
2070
2094
|
|---------|-----|
|
|
2071
2095
|
| Direct DOM manipulation | Use SDK components only \u2014 reconciler serializes to \`LumioComponentNode[]\` |
|
|
2072
|
-
| \`
|
|
2096
|
+
| \`useExtensionStorage()\` without a key | \`useExtensionStorage(key, initialValue)\` \u2014 one call per key |
|
|
2097
|
+
| Object under one key set without spread | \`setScore({ ...score, home: 2 })\` \u2014 the setter replaces that key's value |
|
|
2073
2098
|
| Polling with setInterval | Use \`useLumioEvent()\` for live data |
|
|
2074
2099
|
| Large inline images | Use URL-based images via \`Image\` component |
|
|
2075
2100
|
| Missing \`Lumio.render()\` | Must be called within 10s of page load |
|
|
2076
2101
|
| Hardcoded strings | Use \`config_schema\` fields for user-customizable text |
|
|
2077
|
-
| Using localStorage directly | Use \`useExtensionStorage()\` (localStorage blocked
|
|
2102
|
+
| Using localStorage directly | Use \`useExtensionStorage()\` (localStorage blocked on the Extension Runtime origin) |
|
|
2078
2103
|
| Raw CSS \`@keyframes\` animations | Use \`useAnimation()\` + \`<Animated>\` SDK presets |
|
|
2079
2104
|
|
|
2080
2105
|
## Bot Module Security
|
|
@@ -2205,7 +2230,7 @@ var troubleshootingContent = `# Troubleshooting
|
|
|
2205
2230
|
| \`EGRESS_BLOCKED\` | Host not in allowlist | \`ctx.fetch()\` called with undeclared host | Add host to \`egress.allowHosts\` in \`lumio.config.json\` |
|
|
2206
2231
|
| \`EGRESS_PRIVATE_IP\` | Private IP range blocked | Fetch targeting localhost or private network | Use public HTTPS endpoints only |
|
|
2207
2232
|
| \`STORAGE_QUOTA\` | Storage quota exceeded | Extension data exceeds allocated storage | Reduce stored data or request limit increase |
|
|
2208
|
-
| \`EDITOR_ONLY\` | Function restricted to editor | Mutation with \`editorOnly: true\` called from layer
|
|
2233
|
+
| \`EDITOR_ONLY\` | Function restricted to editor | Mutation with \`editorOnly: true\` called from the layer | Remove \`editorOnly\` flag or call only from editor |
|
|
2209
2234
|
| \`MEMORY_LIMIT\` | V8 isolate memory exceeded (128 MB) | Handler-based function using too much memory | Reduce data processing, stream large datasets |
|
|
2210
2235
|
| \`CPU_TIMEOUT\` | V8 isolate CPU timeout (5s) | Handler-based function running too long | Optimize logic, break into smaller operations |
|
|
2211
2236
|
| \`RATE_LIMITED\` | Too many requests (429) | Exceeded per-installation rate limit | Reduce call frequency, batch operations |
|
|
@@ -2213,12 +2238,13 @@ var troubleshootingContent = `# Troubleshooting
|
|
|
2213
2238
|
| \`SCHEMA_VALIDATION\` | Invalid function arguments | Args do not match \`v.string()\`/\`v.number()\` validators | Check argument types match schema definition |
|
|
2214
2239
|
| \`TABLE_NOT_FOUND\` | Table does not exist | Referenced table not defined in \`server/schema.ts\` | Add table to \`defineSchema()\` and redeploy |
|
|
2215
2240
|
| \`PERMISSION_DENIED\` | Missing declared permission | Action requires undeclared permission | Add permission to \`lumio.config.json -> permissions\` |
|
|
2216
|
-
| \`
|
|
2241
|
+
| \`ctx.secrets.get\` returns \`null\` | Secret key not set | Bot-module handler reads a key nobody set | \`lumio env set KEY\` or Dashboard \u2192 Extensions \u2192 your extension \u2192 Secrets |
|
|
2242
|
+
| \`ctx.secrets is unavailable\` | Secret store unreadable right now | Transient platform issue | Retry; handle the error instead of crashing the handler |
|
|
2217
2243
|
| \`CHANGELOG_MISSING\` | No changelog entry for version | \`lumio deploy\` without matching CHANGELOG.md entry | Add entry for the version in CHANGELOG.md, or use \`-d\` flag |
|
|
2218
2244
|
| \`BUNDLE_TOO_LARGE\` | Bundle exceeds size limit | Built bundle exceeds \`max_bundle_size_mb\` | Reduce bundle size or request limit increase |
|
|
2219
2245
|
| \`SUPERVISOR_BLOCKED\` | Supervisor rejected postMessage | Message origin or schema invalid | Ensure you are in the correct iframe layer; do not post to Host directly |
|
|
2220
2246
|
| \`WORKER_CREATION_FAILED\` | Web Worker could not start | Server functions bundle is invalid or too large | Check for syntax errors in server functions; run \`lumio build\` locally |
|
|
2221
|
-
| \`STORAGE_DOM_EXCEPTION\` | localStorage/sessionStorage access attempt | Extension Runtime (ext.lumio.vision) blocks storage APIs | Replace \`localStorage.setItem\` with \`useExtensionStorage()\` |
|
|
2247
|
+
| \`STORAGE_DOM_EXCEPTION\` | localStorage/sessionStorage access attempt | Extension Runtime (<label>.runtime.ext.lumio.vision) blocks storage APIs | Replace \`localStorage.setItem\` with \`useExtensionStorage()\` |
|
|
2222
2248
|
| \`SLUG_FORMAT_INVALID\` | Slug does not match short-ID format | Manually edited slug in \`lumio.config.json\` | Use auto-generated slug (\`k8f2x-my-extension\` format) or run \`lumio init\` |
|
|
2223
2249
|
|
|
2224
2250
|
## Common Issues
|
|
@@ -2256,7 +2282,7 @@ The SDK auto-refreshes \`lm_ext_*\` tokens via postMessage. The Supervisor media
|
|
|
2256
2282
|
### Supervisor blocked messages (\`SUPERVISOR_BLOCKED\`)
|
|
2257
2283
|
The Supervisor validates all postMessage traffic between Extension Runtime and Host. If you see this error:
|
|
2258
2284
|
1. Do not post messages directly from extension code to \`window.parent.parent\` (Host) \u2014 always use SDK hooks
|
|
2259
|
-
2. Verify the extension iframe is loaded inside \`supervisor.
|
|
2285
|
+
2. Verify the extension iframe is loaded inside \`supervisor.ext.lumio.vision\`, not directly in the Host
|
|
2260
2286
|
3. Check that the message schema matches the \`@zaflun/lumio-protocol\` wire format (SDK handles this automatically)
|
|
2261
2287
|
|
|
2262
2288
|
### Worker creation failures (\`WORKER_CREATION_FAILED\`)
|
|
@@ -2266,7 +2292,7 @@ The V8 isolate Web Worker for server functions failed to start:
|
|
|
2266
2292
|
3. Check \`lumio logs\` for the underlying JS error from the Worker initializer
|
|
2267
2293
|
|
|
2268
2294
|
### Storage DOMException errors (\`STORAGE_DOM_EXCEPTION\`)
|
|
2269
|
-
\`localStorage\` and \`sessionStorage\` are blocked on the Extension Runtime origin (
|
|
2295
|
+
\`localStorage\` and \`sessionStorage\` are blocked on the Extension Runtime origin (\`<label>.runtime.ext.lumio.vision\`):
|
|
2270
2296
|
1. Replace all \`localStorage.setItem\` / \`localStorage.getItem\` calls with \`useExtensionStorage()\`
|
|
2271
2297
|
2. Replace all \`sessionStorage\` usage with \`useVisionState()\` for transient values
|
|
2272
2298
|
3. For server-side persistence, use \`ctx.db\` in server functions
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zaflun/lumio-agent-skill",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "AI agent skill files for building Lumio Extensions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
},
|
|
28
28
|
"license": "MIT",
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@types/node": "^
|
|
30
|
+
"@types/node": "^26.1.2",
|
|
31
31
|
"tsup": "^8.4.0",
|
|
32
|
-
"typescript": "^
|
|
33
|
-
"vitest": "^
|
|
32
|
+
"typescript": "^6.0.3",
|
|
33
|
+
"vitest": "^4.1.10"
|
|
34
34
|
},
|
|
35
35
|
"repository": {
|
|
36
36
|
"type": "git",
|