@zaflun/lumio-agent-skill 0.5.3 → 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 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` | Editor/layer/interactive surfaces |
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
 
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  installSkillFiles
4
- } from "../chunk-TKNV5K6F.js";
4
+ } from "../chunk-XJZ7H56F.js";
5
5
 
6
6
  // src/bin/lumio-skills.ts
7
7
  import { resolve } from "path";
@@ -16,7 +16,7 @@ Lumio Extensions are sandboxed React apps that run inside a 3-layer iframe sandb
16
16
  Host (lumio.vision / browser source)
17
17
  \u2514\u2500\u2500 Supervisor iframe (supervisor.ext.lumio.vision)
18
18
  \u2514\u2500\u2500 Extension Runtime iframe (<label>.runtime.ext.lumio.vision)
19
- \u2514\u2500\u2500 Web Worker (server function execution)
19
+ \u2514\u2500\u2500 Web Worker (the extension's UI code)
20
20
  \`\`\`
21
21
 
22
22
  \`\`\`
@@ -24,7 +24,6 @@ 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
  \`\`\`
@@ -34,7 +33,7 @@ Extension Project:
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 execute inside a Web Worker in a V8 isolate sandbox (256 MB memory, 10s CPU timeout)
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
- ### Three Surfaces
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
- All three surfaces share storage via \`useExtensionStorage()\` \u2014 changes in editor propagate to layer in real-time.
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
 
@@ -99,7 +99,7 @@ All three surfaces share storage via \`useExtensionStorage()\` \u2014 changes in
99
99
  - \`"visibility": "unlisted"\` \u2014 accessible by direct link only
100
100
  - Private invite: grant \`free_access\` to specific accounts via dashboard
101
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 \`/account/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.
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
103
 
104
104
  **Revenue split:** Configurable per developer (default 70/30 developer/Lumio).
105
105
 
@@ -161,9 +161,9 @@ See \`components.md\` for full props and examples.
161
161
 
162
162
  | Hook | Returns | Purpose |
163
163
  |------|---------|---------|
164
- | \`useExtensionStorage()\` | \`[storage, setStorage]\` | Persistent KV store, synced across targets |
165
- | \`useVisionState(key, default)\` | \`[value, setValue]\` | useState for single keys |
166
- | \`useExtensionContext()\` | \`{ extensionId, target, overlayId, interactiveUrl }\` | Extension metadata |
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 |
167
167
  | \`useLumioEvent(eventType)\` | \`event\` | Live events (chat, alerts, spotify) \u2014 type-safe: \`eventType\` is \`LumioEventType\` |
168
168
  | \`useLumioConfig()\` | \`[config, setConfig]\` | Extension config read/write (uses config_schema fields) |
169
169
  | \`useLumioTheme()\` | \`theme\` | Current theme (dark/light/custom) |
@@ -171,10 +171,11 @@ See \`components.md\` for full props and examples.
171
171
  | \`useLumioAction(action)\` | \`{ execute, data, isLoading, error }\` | Trigger Lumio actions \u2014 type-safe: \`action\` is \`LumioActionType\` |
172
172
  | \`useQuery(fn, params?)\` | \`{ data, isLoading, error, refetch }\` | Call server query |
173
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 |
174
175
  | \`useAnimation(preset, options?)\` | \`animationProps\` | SDK animation presets for \`Animated\` component |
175
176
  | \`useIconPicker(options?)\` | \`{ isOpen, selected, open, close, select, clear, sources }\` | Headless state for custom icon picker UIs |
176
177
 
177
- **Important:** \`setStorage\` must always spread: \`setStorage({ ...storage, key: value })\`
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 })\`
178
179
 
179
180
  ## Server Functions
180
181
 
@@ -189,7 +190,7 @@ export const updateItem = patchRow("items", arg("id"), { done: arg("done") });
189
190
  export const removeItem = deleteRow("items", arg("id"), { editorOnly: true });
190
191
  \`\`\`
191
192
 
192
- **Handler-based** (V8 isolate Web Worker, full control):
193
+ **Handler-based** (V8 isolate on the Bot Module Worker, full control):
193
194
  \`\`\`typescript
194
195
  import { action, arg, v } from "@zaflun/lumio-sdk/server";
195
196
  export const fetchScores = action({
@@ -327,7 +328,7 @@ export const modCheck = moderate({
327
328
  | \`lumio deploy\` | Upload to Lumio (status: draft). Flags: \`-v\`, \`-d\`, \`--breaking\`, \`--draft\` |
328
329
  | \`lumio status\` | Check review status of latest version |
329
330
  | \`lumio logs\` | Stream server function logs |
330
- | \`lumio secrets\` | Manage server-side secrets |
331
+ | \`lumio env\` | Manage extension secrets (\`set\` / \`list\` / \`delete\`) |
331
332
  | \`lumio team\` | Manage developer team members |
332
333
  | \`lumio config\` | Show/set CLI configuration |
333
334
 
@@ -342,7 +343,7 @@ See \`cli-workflows.md\` for detailed usage and examples.
342
343
 
343
344
  Extension tokens have no RBAC permissions (\`has_permission()\` always returns false) and can only access \`ext_{extension_id}.*\` DB tables via server functions.
344
345
 
345
- **\`editorOnly: true\`** on server functions restricts mutations to editor surface only (layer/interactive cannot call them).
346
+ **\`editorOnly: true\`** on server functions restricts mutations to editor surface only (the layer cannot call them).
346
347
 
347
348
  See \`auth.md\` for all 9 token types and the full token lifecycle.
348
349
 
@@ -360,26 +361,27 @@ lumio deploy -> draft -> [Submit via dashboard] -> pending_review
360
361
  ### Editor + Layer sync
361
362
  \`\`\`tsx
362
363
  // editor.tsx
363
- const [storage, setStorage] = useExtensionStorage();
364
- <Toggle label="Show" checked={storage.visible ?? false}
365
- onChange={(v) => setStorage({ ...storage, visible: v })} />
364
+ const [visible, setVisible] = useExtensionStorage("visible", false);
365
+ <Toggle label="Show" checked={visible} onChange={setVisible} />
366
366
 
367
367
  // layer.tsx
368
- const [storage] = useExtensionStorage();
369
- if (!storage.visible) return null;
370
- return <Text content={storage.text} />;
368
+ const [visible] = useExtensionStorage("visible", false);
369
+ const [text] = useExtensionStorage("text", "");
370
+ if (!visible) return null;
371
+ return <Text content={text} />;
371
372
  \`\`\`
372
373
 
373
374
  ### Live events
374
375
  \`\`\`tsx
375
- // eventType is LumioEventType \u2014 autocomplete shows all 58 valid event strings
376
- const event = useLumioEvent("twitch:follower");
377
- // event updates automatically when a new follower event arrives
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
378
380
  \`\`\`
379
381
 
380
- **All 58 event types** (\`LumioEventType\`):
382
+ **All 62 event types** (\`LumioEventType\`):
381
383
 
382
- Twitch (23): \`twitch:follower\`, \`twitch:subscribe\`, \`twitch:gift_sub\`, \`twitch:resub\`, \`twitch:cheer\`, \`twitch:raid\`, \`twitch:reward\`, \`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
+ 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\`
383
385
 
384
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\`
385
387
 
@@ -391,7 +393,7 @@ StreamElements (1): \`streamelements:tip\`
391
393
 
392
394
  Cross-platform chat (1, fires for every platform): \`chat:message\`
393
395
 
394
- 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\`.
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.
395
397
 
396
398
  ### Server queries
397
399
  \`\`\`tsx
@@ -421,7 +423,7 @@ const anim = useAnimation("slide-in", { duration: 300 });
421
423
  | \`EXTENSION_NOT_FOUND\` | Invalid extensionId in config | Run \`lumio init\` or create via dashboard |
422
424
  | \`EGRESS_BLOCKED\` | Fetch to undeclared host | Add host to \`egress.allowHosts\` in config |
423
425
  | \`STORAGE_QUOTA\` | Exceeded storage limit | Reduce data or request limit increase |
424
- | \`EDITOR_ONLY\` | Mutation called from layer/interactive | Remove \`editorOnly\` or restrict to editor |
426
+ | \`EDITOR_ONLY\` | Mutation called from the layer | Remove \`editorOnly\` or restrict to editor |
425
427
  | \`SUPERVISOR_BLOCKED\` | Supervisor rejected a postMessage | Check message origin matches the extension's runtime origin (\`<label>.runtime.ext.lumio.vision\`) |
426
428
  | \`WORKER_CREATION_FAILED\` | Web Worker could not be created | Verify server functions bundle is valid JS |
427
429
  | \`STORAGE_DOM_EXCEPTION\` | localStorage/sessionStorage access | Use \`useExtensionStorage()\` instead (storage blocked) |
@@ -816,18 +818,18 @@ import { IconPicker, IconDisplay } from "@zaflun/lumio-sdk";
816
818
  import type { IconValue } from "@zaflun/lumio-sdk";
817
819
 
818
820
  // In editor.tsx \u2014 let the user pick an icon
819
- const [storage, setStorage] = useExtensionStorage();
821
+ const [icon, setIcon] = useExtensionStorage<IconValue | null>("icon", null);
820
822
 
821
823
  <IconPicker
822
- value={storage.icon ?? null}
823
- onChange={(icon) => setStorage({ ...storage, icon })}
824
+ value={icon}
825
+ onChange={setIcon}
824
826
  sources={["lucide", "emoji", "emote", "upload"]}
825
827
  size="md"
826
828
  placeholder="Choose an icon..."
827
829
  />
828
830
 
829
831
  // In layer.tsx \u2014 render the selected icon
830
- <IconDisplay icon={storage.icon ?? null} size={32} />
832
+ <IconDisplay icon={icon} size={32} />
831
833
  \`\`\`
832
834
 
833
835
  **IconPickerProps:**
@@ -950,9 +952,12 @@ import { queryRows, insertRow, patchRow, deleteRow, arg, filter } from "@zaflun/
950
952
  // Read all rows
951
953
  export const getItems = queryRows("items");
952
954
 
953
- // Read with filter
955
+ // Read with filter, order and paging
954
956
  export const getDoneItems = queryRows("items", {
955
957
  filter: filter("done", "=", true),
958
+ orderBy: "created_at",
959
+ orderDir: "desc",
960
+ limit: arg("limit"),
956
961
  });
957
962
 
958
963
  // Insert a row (editorOnly: only callable from editor surface)
@@ -975,8 +980,10 @@ export const removeItem = deleteRow("items", arg("id"), { editorOnly: true });
975
980
 
976
981
  - \`arg("name")\` \u2014 reads from the client-provided arguments
977
982
  - \`identity()\` \u2014 returns \`{ userId, accountId }\` of the caller
978
- - \`secret("KEY")\` \u2014 reads a server-side secret (never exposed to client)
979
- - \`filter(field, op, value)\` \u2014 query filter (=, !=, >, <, >=, <=, LIKE, IN)
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)
980
987
 
981
988
  ## Handler-Based Functions
982
989
 
@@ -1006,45 +1013,48 @@ export const submitScore = mutation({
1006
1013
  editorOnly: false,
1007
1014
  });
1008
1015
 
1009
- // External API call (requires egress.allowHosts in config)
1010
- // Egress allowlist is enforced by the Supervisor \u2014 requests to undeclared hosts are rejected before reaching the network
1011
- export const fetchScores = action({
1012
- 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() },
1013
1019
  handler: async (ctx, args) => {
1014
- const apiKey = ctx.secrets.get("ESPN_API_KEY");
1015
- const res = await ctx.fetch(
1016
- \`https://site.api.espn.com/apis/v2/sports/\${args.sport}/\${args.league}/scoreboard\`,
1017
- { headers: { "Authorization": \`Bearer \${apiKey}\` } }
1018
- );
1019
- 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 });
1020
1023
  },
1021
1024
  });
1022
1025
  \`\`\`
1023
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
+
1024
1035
  ### Handler Context (\`ctx\`)
1025
1036
 
1026
- Handler-based functions run inside a V8 isolate Web Worker. The \`ctx\` object provides the full sandbox API:
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:
1027
1038
 
1028
1039
  | Property | Description |
1029
1040
  |----------|-------------|
1030
- | \`ctx.db.get(table, id)\` | Read a single row by ID from extension DB schema |
1031
- | \`ctx.db.insert(table, data)\` | Insert row |
1032
- | \`ctx.db.patch(table, id, data)\` | Update row |
1033
- | \`ctx.db.delete(table, id)\` | Delete row |
1034
- | \`ctx.fetch(url, options?)\` | HTTP request (egress allowlist enforced by Supervisor) |
1035
- | \`ctx.secrets.get(key)\` | Read server-side secret (set via dashboard or \`lumio secrets\`) |
1036
- | \`ctx.auth\` | \`{ userId, accountId, target }\` \u2014 caller identity from extension token |
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 |
1037
1048
 
1038
1049
  ### Sandbox Limits
1039
1050
 
1040
1051
  | Limit | Value |
1041
1052
  |-------|-------|
1042
1053
  | Memory | 256 MB per invocation |
1043
- | CPU timeout | 10 seconds |
1054
+ | Wall-clock timeout | 5 seconds per call |
1044
1055
  | Filesystem | No access |
1045
- | Network | Only \`ctx.fetch()\` via egress allowlist (Supervisor-enforced) |
1056
+ | Network | None for server functions (\`ctx.fetch\` refuses every host) |
1046
1057
  | Dynamic code | Blocked (\`eval\`, \`Function\`, \`import()\`) |
1047
- | Storage APIs | localStorage/sessionStorage blocked (separate origin) |
1048
1058
 
1049
1059
  ### Rate Limits
1050
1060
 
@@ -1145,7 +1155,8 @@ export default defineAutomationTrigger({
1145
1155
  const lastCheck = await ctx.cache.get("last_check") ?? "1970-01-01T00:00:00Z";
1146
1156
  const res = await ctx.fetch(
1147
1157
  \`https://api.example.com/events?since=\${lastCheck}\`,
1148
- { headers: { "Authorization": \`Bearer \${ctx.secrets.get("API_KEY")}\` } }
1158
+ // Automation nodes get no secrets yet: the token comes from the node config.
1159
+ { headers: { "Authorization": \`Bearer \${ctx.nodeConfig.apiToken}\` } }
1149
1160
  );
1150
1161
  const events = await res.json();
1151
1162
  await ctx.cache.set("last_check", new Date().toISOString());
@@ -1179,7 +1190,7 @@ export default defineAutomationLogic({
1179
1190
  | \`ctx.inputData\` | Action, Logic | Data from the upstream node |
1180
1191
  | \`ctx.fetch(url, options?)\` | All | HTTP request (egress allowlist enforced) |
1181
1192
  | \`ctx.cache.get(key)\` / \`ctx.cache.set(key, value, ttl?)\` | All | Per-installation KV cache (Redis, 1h default TTL) |
1182
- | \`ctx.secrets.get(key)\` | All | Server-side secret |
1193
+ | \`ctx.secrets.get(key)\` | All | Not wired for automation nodes yet: always \`null\` |
1183
1194
  | \`ctx.db\` | All | Extension DB schema access |
1184
1195
  | \`ctx.auth\` | All | \`{ accountId }\` \u2014 account identity |
1185
1196
  | \`ctx.defer()\` | Action | Mark execution as async (returns 202, completes in background) |
@@ -1444,7 +1455,6 @@ Automation nodes always generate \`server/handler.ts\` (not \`server/functions.t
1444
1455
  | Blank | editor + layer | No | Minimal starting point |
1445
1456
  | Widget | editor + layer | Yes | Widget with storage |
1446
1457
  | Full-Stack | editor + layer | Yes | Complete extension with server functions |
1447
- | Interactive Game | editor + layer + interactive | Yes | Viewer interaction game |
1448
1458
  | Bot Module | server-only | Yes | Chat command module |
1449
1459
  | Automation Action | server + editor | Yes | Automation workflow action node |
1450
1460
  | Automation Trigger | server + editor | Yes | Automation workflow trigger node |
@@ -1461,9 +1471,8 @@ lumio dev
1461
1471
 
1462
1472
  # Output:
1463
1473
  # Lumio Dev Server running at http://localhost:5173
1464
- # Editor: http://localhost:5173/editor
1465
- # Layer: http://localhost:5173/layer
1466
- # Interactive: http://localhost:5173/interactive
1474
+ # Editor: http://localhost:5173/editor
1475
+ # Layer: http://localhost:5173/layer
1467
1476
  #
1468
1477
  # Mock events available. Press 'e' to emit a test event.
1469
1478
  # Press 'q' to quit.
@@ -1507,6 +1516,7 @@ lumio build
1507
1516
  # Output for widgets:
1508
1517
  # Built: dist/editor.js
1509
1518
  # Built: dist/layer.js
1519
+ # Built: dist/server.js + dist/functions.json (if server: true)
1510
1520
  # Compiled: dist/styles.css (if Tailwind detected)
1511
1521
  # Archive: dist/source.tar.gz
1512
1522
  #
@@ -1517,10 +1527,11 @@ lumio build
1517
1527
  \`\`\`
1518
1528
 
1519
1529
  Build steps:
1520
- 1. UI targets (\`editor\`, \`layer\`, \`interactive\`, \`designer\`) \u2014 esbuild IIFE bundles
1521
- 2. Server handler (\`server: true\`) \u2014 compiles \`server/handler.ts\` to \`dist/server.js\` (ESM)
1522
- 3. CSS \u2014 Tailwind v4 auto-detected and compiled, or plain CSS copied
1523
- 4. Source archive \u2014 \`dist/source.tar.gz\`
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\`
1524
1535
 
1525
1536
  Warns if \`CHANGELOG.md\` is missing.
1526
1537
 
@@ -1617,25 +1628,27 @@ lumio logs --filter fetchScores
1617
1628
  lumio logs --level error
1618
1629
  \`\`\`
1619
1630
 
1620
- ## lumio secrets
1631
+ ## lumio env
1621
1632
 
1622
- Manage server-side secrets used by handler-based server functions via \`ctx.secrets.get("KEY")\`.
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).
1623
1634
 
1624
1635
  \`\`\`bash
1625
- # Set a secret
1626
- lumio secrets set ESPN_API_KEY abc123xyz
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
1627
1641
 
1628
1642
  # List secret keys (values are never shown)
1629
- lumio secrets list
1643
+ lumio env list
1630
1644
  # Output:
1631
- # ESPN_API_KEY (set 2026-05-19)
1632
- # WEATHER_TOKEN (set 2026-04-01)
1645
+ # ESPN_API_KEY (created 2026-05-19T00:00:00Z)
1633
1646
 
1634
1647
  # Delete a secret
1635
- lumio secrets delete ESPN_API_KEY
1648
+ lumio env delete ESPN_API_KEY
1636
1649
  \`\`\`
1637
1650
 
1638
- Secrets are stored encrypted on the Lumio platform and injected into the V8 isolate at runtime. They are never exposed to the frontend or to other extensions.
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.
1639
1652
 
1640
1653
  ## lumio team
1641
1654
 
@@ -1711,7 +1724,7 @@ AuthContext::ExtensionToken {
1711
1724
  extension_id: Uuid,
1712
1725
  install_id: Uuid,
1713
1726
  account_id: Uuid,
1714
- target: "editor" | "layer" | "interactive",
1727
+ target: "editor" | "layer",
1715
1728
  }
1716
1729
  \`\`\`
1717
1730
 
@@ -1752,7 +1765,7 @@ Widget tokens are suitable for persistent, public-facing embeds where session re
1752
1765
  Server functions marked with \`editorOnly: true\` can only be called from the \`editor\` surface.
1753
1766
 
1754
1767
  \`\`\`typescript
1755
- // Only callable from editor.tsx \u2014 layer.tsx and interactive.tsx get 403
1768
+ // Only callable from editor.tsx \u2014 layer.tsx gets 403
1756
1769
  export const addItem = insertRow("items", { text: arg("text") }, { editorOnly: true });
1757
1770
 
1758
1771
  // Callable from any surface
@@ -1912,8 +1925,8 @@ var bestPracticesContent = `# Best Practices
1912
1925
  ## Performance
1913
1926
 
1914
1927
  ### Minimize renders
1915
- - Use \`useVisionState()\` for individual values instead of \`useExtensionStorage()\` for everything
1916
- - Avoid spreading entire storage on every keystroke \u2014 debounce text inputs
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
1917
1930
  - Use \`useMemo\` and \`useCallback\` to avoid unnecessary re-renders
1918
1931
 
1919
1932
  ### Optimize server function calls
@@ -1938,22 +1951,21 @@ Always use \`useExtensionStorage()\` for state that should be visible on both ed
1938
1951
 
1939
1952
  \`\`\`tsx
1940
1953
  // editor.tsx \u2014 user configures
1941
- const [storage, setStorage] = useExtensionStorage();
1942
- <TextField label="Title" value={storage.title ?? ""}
1943
- onChange={(v) => setStorage({ ...storage, title: v })} />
1954
+ const [title, setTitle] = useExtensionStorage("title", "Default Title");
1955
+ <TextField label="Title" value={title} onChange={setTitle} />
1944
1956
 
1945
1957
  // layer.tsx \u2014 displays result
1946
- const [storage] = useExtensionStorage();
1947
- <Text content={storage.title ?? "Default Title"} variant="heading" />
1958
+ const [title] = useExtensionStorage("title", "Default Title");
1959
+ <Text content={title} variant="heading" />
1948
1960
  \`\`\`
1949
1961
 
1950
1962
  ### Graceful defaults
1951
- Always provide sensible defaults using \`??\` (nullish coalescing). Storage might be empty on first load.
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.
1952
1964
 
1953
1965
  \`\`\`tsx
1954
- const visible = storage.visible ?? true;
1955
- const maxItems = storage.maxItems ?? 10;
1956
- const theme = storage.theme ?? "dark";
1966
+ const [visible] = useExtensionStorage("visible", true);
1967
+ const [maxItems] = useExtensionStorage("maxItems", 10);
1968
+ const [theme] = useExtensionStorage("theme", "dark");
1957
1969
  \`\`\`
1958
1970
 
1959
1971
  ### Loading states
@@ -2081,7 +2093,8 @@ For beta testing before public launch:
2081
2093
  | Mistake | Fix |
2082
2094
  |---------|-----|
2083
2095
  | Direct DOM manipulation | Use SDK components only \u2014 reconciler serializes to \`LumioComponentNode[]\` |
2084
- | \`setStorage(newObj)\` without spread | \`setStorage({ ...storage, key: value })\` |
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 |
2085
2098
  | Polling with setInterval | Use \`useLumioEvent()\` for live data |
2086
2099
  | Large inline images | Use URL-based images via \`Image\` component |
2087
2100
  | Missing \`Lumio.render()\` | Must be called within 10s of page load |
@@ -2217,7 +2230,7 @@ var troubleshootingContent = `# Troubleshooting
2217
2230
  | \`EGRESS_BLOCKED\` | Host not in allowlist | \`ctx.fetch()\` called with undeclared host | Add host to \`egress.allowHosts\` in \`lumio.config.json\` |
2218
2231
  | \`EGRESS_PRIVATE_IP\` | Private IP range blocked | Fetch targeting localhost or private network | Use public HTTPS endpoints only |
2219
2232
  | \`STORAGE_QUOTA\` | Storage quota exceeded | Extension data exceeds allocated storage | Reduce stored data or request limit increase |
2220
- | \`EDITOR_ONLY\` | Function restricted to editor | Mutation with \`editorOnly: true\` called from layer/interactive | Remove \`editorOnly\` flag or call only from editor |
2233
+ | \`EDITOR_ONLY\` | Function restricted to editor | Mutation with \`editorOnly: true\` called from the layer | Remove \`editorOnly\` flag or call only from editor |
2221
2234
  | \`MEMORY_LIMIT\` | V8 isolate memory exceeded (128 MB) | Handler-based function using too much memory | Reduce data processing, stream large datasets |
2222
2235
  | \`CPU_TIMEOUT\` | V8 isolate CPU timeout (5s) | Handler-based function running too long | Optimize logic, break into smaller operations |
2223
2236
  | \`RATE_LIMITED\` | Too many requests (429) | Exceeded per-installation rate limit | Reduce call frequency, batch operations |
@@ -2225,7 +2238,8 @@ var troubleshootingContent = `# Troubleshooting
2225
2238
  | \`SCHEMA_VALIDATION\` | Invalid function arguments | Args do not match \`v.string()\`/\`v.number()\` validators | Check argument types match schema definition |
2226
2239
  | \`TABLE_NOT_FOUND\` | Table does not exist | Referenced table not defined in \`server/schema.ts\` | Add table to \`defineSchema()\` and redeploy |
2227
2240
  | \`PERMISSION_DENIED\` | Missing declared permission | Action requires undeclared permission | Add permission to \`lumio.config.json -> permissions\` |
2228
- | \`SECRET_NOT_FOUND\` | Secret key not set | \`ctx.secrets.get("KEY")\` with unset secret | Set secret via dashboard (\`/dashboard/extensions/{slug}/secrets\`) |
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 |
2229
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 |
2230
2244
  | \`BUNDLE_TOO_LARGE\` | Bundle exceeds size limit | Built bundle exceeds \`max_bundle_size_mb\` | Reduce bundle size or request limit increase |
2231
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 |
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  ROOT_FILES,
3
3
  SKILL_FILES,
4
4
  installSkillFiles
5
- } from "./chunk-TKNV5K6F.js";
5
+ } from "./chunk-XJZ7H56F.js";
6
6
  export {
7
7
  ROOT_FILES,
8
8
  SKILL_FILES,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zaflun/lumio-agent-skill",
3
- "version": "0.5.3",
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",