stillmade-sdk 0.1.24 → 0.1.25

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/AGENTS.md CHANGED
@@ -16,6 +16,10 @@ Treat the current SDK (`stillmade.block.json` manifest, `packages/block-sdk` con
16
16
 
17
17
  Before building, check what already exists (`find_existing_blocks` over MCP, or the Marketplace and your library) by the inputs, results and operations the goal needs. Use an installed or reviewed Block that already does it; remix a close one so its license, notices and creator credit stay attached; chain existing Blocks and build only the missing step. Never republish another creator's source without remix lineage: public copies are refused. Build a new Block when nothing fits or the person asks for their own version, and say which similar Blocks you considered.
18
18
 
19
+ ## Three sizes of Block
20
+
21
+ Blocks come in three sizes and the manifest decides which (packages/block-sdk/block-tier.js): an app is a workspace a person works in (Editor, Canvas, Shots), placed in a Type and never wired automatically; a job is one deliverable a person would name (the narration, the captioned clip), built from the hosted operations it needs (`permissions.capabilities`) and reviewed in one place, the unit Chat picks for a Step; an ingredient is one hosted operation with nothing of its own (a declarative capability Block), what a job is made of. When two ingredients would stand in a row to make one deliverable (writing the words, then voicing them), build or pick the job that does both and keep the review point inside it. A job declares a second output when another Block consumes the intermediate (the script beside the voice); split into two jobs only when the intermediate has a life of its own or the halves have different owners or payment.
22
+
19
23
  ## One job per Step
20
24
 
21
25
  A Step is one job: the work between two moments where a person or the outside world must see the result. Start a new Step only at one of three points; everything else is a part of the neighboring Step.
package/PASTE_TO_LLM.md CHANGED
@@ -14,6 +14,10 @@ Treat the current SDK (`stillmade.block.json` manifest, `packages/block-sdk` con
14
14
 
15
15
  Before building, check what already exists (`find_existing_blocks` over MCP, or the Marketplace and your library) by the inputs, results and operations the goal needs. Use an installed or reviewed Block that already does it; remix a close one so its license, notices and creator credit stay attached; chain existing Blocks and build only the missing step. Never republish another creator's source without remix lineage: public copies are refused. Build a new Block when nothing fits or the person asks for their own version, and say which similar Blocks you considered.
16
16
 
17
+ ### Three sizes of Block
18
+
19
+ Blocks come in three sizes and the manifest decides which (packages/block-sdk/block-tier.js): an app is a workspace a person works in (Editor, Canvas, Shots), placed in a Type and never wired automatically; a job is one deliverable a person would name (the narration, the captioned clip), built from the hosted operations it needs (`permissions.capabilities`) and reviewed in one place, the unit Chat picks for a Step; an ingredient is one hosted operation with nothing of its own (a declarative capability Block), what a job is made of. When two ingredients would stand in a row to make one deliverable (writing the words, then voicing them), build or pick the job that does both and keep the review point inside it. A job declares a second output when another Block consumes the intermediate (the script beside the voice); split into two jobs only when the intermediate has a life of its own or the halves have different owners or payment.
20
+
17
21
  ### One job per Step
18
22
 
19
23
  A Step is one job: the work between two moments where a person or the outside world must see the result. Start a new Step only at one of three points; everything else is a part of the neighboring Step.
package/SDK.md CHANGED
@@ -192,6 +192,10 @@ Treat the current SDK (`stillmade.block.json` manifest, `packages/block-sdk` con
192
192
 
193
193
  Before building, check what already exists (`find_existing_blocks` over MCP, or the Marketplace and your library) by the inputs, results and operations the goal needs. Use an installed or reviewed Block that already does it; remix a close one so its license, notices and creator credit stay attached; chain existing Blocks and build only the missing step. Never republish another creator's source without remix lineage: public copies are refused. Build a new Block when nothing fits or the person asks for their own version, and say which similar Blocks you considered.
194
194
 
195
+ ### Three sizes of Block
196
+
197
+ Blocks come in three sizes and the manifest decides which (packages/block-sdk/block-tier.js): an app is a workspace a person works in (Editor, Canvas, Shots), placed in a Type and never wired automatically; a job is one deliverable a person would name (the narration, the captioned clip), built from the hosted operations it needs (`permissions.capabilities`) and reviewed in one place, the unit Chat picks for a Step; an ingredient is one hosted operation with nothing of its own (a declarative capability Block), what a job is made of. When two ingredients would stand in a row to make one deliverable (writing the words, then voicing them), build or pick the job that does both and keep the review point inside it. A job declares a second output when another Block consumes the intermediate (the script beside the voice); split into two jobs only when the intermediate has a life of its own or the halves have different owners or payment.
198
+
195
199
  ### One job per Step
196
200
 
197
201
  A Step is one job: the work between two moments where a person or the outside world must see the result. Start a new Step only at one of three points; everything else is a part of the neighboring Step.
@@ -6908,7 +6912,7 @@ Generated from the SDK source (197 codes). The message is one example; the actua
6908
6912
  | REQUIREMENT | Choose a declared input requirement. | `interactive-requirements`, `package-jobs`, `sandbox-node` and 1 more |
6909
6913
  | RESOLVER | Choose an exact asset version for scoped resolution. | `universal-resolver` |
6910
6914
  | RUNTIME | Sandbox memory configuration was not applied | `javascript` |
6911
- | RUNTIME_RESTRICTED | Local admission supports recipes, isolated JavaScript, declarative capabilities and ComfyUI workflows. Hosted execution still requires account review. | `local-admission`, `portable` |
6915
+ | RUNTIME_RESTRICTED | Portable Blocks support recipe, isolated JavaScript, declarative host capabilities and reviewed ComfyUI workflows. Arbitrary Python and native code are restricted; do not package a substitute that pretends to provide them. | `portable` |
6912
6916
  | RUNTIME_UNAVAILABLE | This host does not provide interactive Block questions. Supply this input before running. | `javascript`, `recipe`, `runtime` |
6913
6917
  | SCHEMA | Numbers must be finite | `contracts`, `semantic-contracts` |
6914
6918
  | SEMANTIC | Resolve meanings from at most 200 pinned Block manifests. | `package-semantics`, `semantic-contracts` |
@@ -7249,6 +7253,10 @@ Treat the current SDK (`stillmade.block.json` manifest, `packages/block-sdk` con
7249
7253
 
7250
7254
  Before building, check what already exists (`find_existing_blocks` over MCP, or the Marketplace and your library) by the inputs, results and operations the goal needs. Use an installed or reviewed Block that already does it; remix a close one so its license, notices and creator credit stay attached; chain existing Blocks and build only the missing step. Never republish another creator's source without remix lineage: public copies are refused. Build a new Block when nothing fits or the person asks for their own version, and say which similar Blocks you considered.
7251
7255
 
7256
+ ### Three sizes of Block
7257
+
7258
+ Blocks come in three sizes and the manifest decides which (packages/block-sdk/block-tier.js): an app is a workspace a person works in (Editor, Canvas, Shots), placed in a Type and never wired automatically; a job is one deliverable a person would name (the narration, the captioned clip), built from the hosted operations it needs (`permissions.capabilities`) and reviewed in one place, the unit Chat picks for a Step; an ingredient is one hosted operation with nothing of its own (a declarative capability Block), what a job is made of. When two ingredients would stand in a row to make one deliverable (writing the words, then voicing them), build or pick the job that does both and keep the review point inside it. A job declares a second output when another Block consumes the intermediate (the script beside the voice); split into two jobs only when the intermediate has a life of its own or the halves have different owners or payment.
7259
+
7252
7260
  ### One job per Step
7253
7261
 
7254
7262
  A Step is one job: the work between two moments where a person or the outside world must see the result. Start a new Step only at one of three points; everything else is a part of the neighboring Step.
@@ -12273,6 +12281,7 @@ The latest version of each Block bundled with the SDK, generated from the packag
12273
12281
  | `sm.whiteboard-assistant` 1.0.0 | Build Whiteboard | task | `board` board-document · project context only; `request` text · primary | `edit` workspace-edit · primary |
12274
12282
  | `sm.write-summary` 1.0.0 | Write summary | task | `data` text (external_data) · primary | `summary` text (summary_report) · primary |
12275
12283
  | `sm.youtube` 2.0.0 | YouTube publication | task | `video` video (finished_video) · primary; `title` text; `description` text; `tags` text; `visibility` text; `publishAt` text; `madeForKids` boolean; `aiGenerated` boolean | `uploadAction` approval (portable_media_action); `publication` json (youtube_publication) |
12284
+ | `stillmade.narration` 1.0.0 | Narration | workspace | `brief` brief (project_brief) · primary; `script` script (narration_script); `tone` text (narration_tone); `rules` text (writing_rules); `voice` text (speech_voice); `speed` number (speech_speed); `take` audio (narration_audio) | `script` script (narration_script); `audio` audio (narration_audio) · primary |
12276
12285
  | `stillmade.occasion-story` 1.0.2 | Occasion Story | task | `assets` asset[] (legacy.asset-list.assets) · primary; `occasion` text (stillmade.occasion_story.occasion); `title` text (stillmade.occasion_story.title); `ending` text (stillmade.occasion_story.ending); `notes` text (stillmade.occasion_story.notes); `selections` text (stillmade.occasion_story.selections); `music` audio[] (stillmade.occasion_story.music); `musicVolume` number (stillmade.occasion_story.musicvolume); `muteVideoAudio` boolean (stillmade.occasion_story.mutevideoaudio); `aspect` text (stillmade.occasion_story.aspect); `disclosure` text (stillmade.occasion_story.disclosure) | `timeline` timeline (legacy.timeline.updatededitor) · primary; `review` json (stillmade.occasion_story.review) |
12277
12286
  | `stillmade.photo-reel-composer` 1.0.2 | Photo Reel Composer | task | `assets` asset[] (legacy.asset-list.assets) · primary; `purpose` text (stillmade.photo_reel.purpose); `title` text (stillmade.photo_reel.title); `captions` text (stillmade.photo_reel.captions); `cta` text (stillmade.photo_reel.cta); `contact` text (stillmade.photo_reel.contact); `demoDisclosure` text (stillmade.photo_reel.demodisclosure); `duration` number (stillmade.photo_reel.duration); `aspect` text (stillmade.photo_reel.aspect); `logo` asset[] (stillmade.photo_reel.logo); `music` asset[] (stillmade.photo_reel.music) | `timeline` timeline (legacy.timeline.updatededitor) · primary; `review` json (stillmade.photo_reel.review); `provenance` json (stillmade.photo_reel.provenance) |
12278
12287
  | `stillmade.product-ad-variants` 1.0.0 | Product Ad Variants | task | `timeline` timeline (legacy.timeline.updatededitor) · primary; `hookA` text (stillmade.product_ad_variants.hooka); `hookB` text (stillmade.product_ad_variants.hookb); `hookC` text (stillmade.product_ad_variants.hookc); `openingA` number (stillmade.product_ad_variants.openinga); `openingB` number (stillmade.product_ad_variants.openingb); `openingC` number (stillmade.product_ad_variants.openingc); `openingDuration` number (stillmade.product_ad_variants.openingduration); `copyApproved` boolean (stillmade.product_ad_variants.copyapproved) | `variantA` timeline (legacy.timeline.updatededitor) · primary; `variantB` timeline (legacy.timeline.updatededitor); `variantC` timeline (legacy.timeline.updatededitor); `review` json (stillmade.product_ad_variants.review) |
@@ -0,0 +1 @@
1
+ export {default} from './versions/1.0.0/package.js';
@@ -0,0 +1,221 @@
1
+ // Generated from the owned versioned source by build-package.mjs.
2
+ export default {
3
+ "manifest": {
4
+ "schemaVersion": 1,
5
+ "sdkVersion": "0.1.0",
6
+ "id": "stillmade.narration",
7
+ "version": "1.0.0",
8
+ "name": "Narration",
9
+ "description": "Turn the facts Chat gathered into spoken words, hear them in a catalog voice or use your own recording, and hand the script and the voiceover on.",
10
+ "kind": "workspace",
11
+ "runtime": "module",
12
+ "license": "MIT",
13
+ "inputs": {
14
+ "brief": {
15
+ "type": "brief",
16
+ "primary": true,
17
+ "required": false,
18
+ "semantic": "project_brief",
19
+ "description": "The facts the voice speaks from: what is shown, its details, who it is for, how long, and how to reach you. Chat fills this in."
20
+ },
21
+ "script": {
22
+ "type": "script",
23
+ "required": false,
24
+ "semantic": "narration_script",
25
+ "sources": [
26
+ "upstream",
27
+ "user"
28
+ ],
29
+ "description": "The words the voice says. Written from the facts when empty; change them before the voice runs."
30
+ },
31
+ "tone": {
32
+ "type": "text",
33
+ "required": false,
34
+ "default": "warm",
35
+ "semantic": "narration_tone",
36
+ "description": "How it sounds: warm, friendly, no-nonsense or luxury."
37
+ },
38
+ "rules": {
39
+ "type": "text",
40
+ "required": false,
41
+ "semantic": "writing_rules",
42
+ "description": "Your own writing rules, such as short sentences or no exclamation marks."
43
+ },
44
+ "voice": {
45
+ "type": "text",
46
+ "required": false,
47
+ "default": "onyx",
48
+ "semantic": "speech_voice",
49
+ "description": "The catalog voice that reads the script."
50
+ },
51
+ "speed": {
52
+ "type": "number",
53
+ "required": false,
54
+ "default": 1,
55
+ "min": 0.5,
56
+ "max": 2,
57
+ "semantic": "speech_speed",
58
+ "description": "How fast the voice reads, from half to double speed."
59
+ },
60
+ "take": {
61
+ "type": "audio",
62
+ "required": false,
63
+ "semantic": "narration_audio",
64
+ "sources": [
65
+ "user"
66
+ ],
67
+ "description": "Your own recording or audio file, used instead of a generated voice."
68
+ }
69
+ },
70
+ "outputs": {
71
+ "script": {
72
+ "type": "script",
73
+ "required": false,
74
+ "semantic": "narration_script",
75
+ "role": "narration_script",
76
+ "description": "The narration, for shots, captions and the voice."
77
+ },
78
+ "audio": {
79
+ "type": "audio",
80
+ "primary": true,
81
+ "required": false,
82
+ "semantic": "narration_audio",
83
+ "role": "narration_audio",
84
+ "description": "The voiceover."
85
+ }
86
+ },
87
+ "permissions": {
88
+ "project": [],
89
+ "capabilities": [
90
+ "text.generate",
91
+ "audio.speech"
92
+ ],
93
+ "network": [],
94
+ "filesystem": [],
95
+ "secrets": []
96
+ },
97
+ "ui": [],
98
+ "features": [
99
+ {
100
+ "id": "write-script",
101
+ "label": "Write the script",
102
+ "does": "Writes spoken narration from the facts in the brief, in the tone you pick, using only those facts.",
103
+ "asks": [
104
+ "write a script for my listing video",
105
+ "turn these car details into a 30 second narration",
106
+ "write the words for a product video"
107
+ ],
108
+ "inputs": [
109
+ "brief",
110
+ "tone",
111
+ "rules"
112
+ ],
113
+ "outputs": [
114
+ "script"
115
+ ]
116
+ },
117
+ {
118
+ "id": "make-voiceover",
119
+ "label": "Make the voiceover",
120
+ "does": "Reads the script in a catalog voice at the speed you choose.",
121
+ "asks": [
122
+ "make a voiceover for this script",
123
+ "read this in a deep voice",
124
+ "narrate my video"
125
+ ],
126
+ "inputs": [
127
+ "script",
128
+ "voice",
129
+ "speed"
130
+ ],
131
+ "outputs": [
132
+ "audio"
133
+ ]
134
+ },
135
+ {
136
+ "id": "own-recording",
137
+ "label": "Use your own recording",
138
+ "does": "Uses an audio file or recording you supply as the voiceover, with the script kept beside it.",
139
+ "asks": [
140
+ "use my own voice recording",
141
+ "I recorded the narration myself"
142
+ ],
143
+ "inputs": [
144
+ "take"
145
+ ],
146
+ "outputs": [
147
+ "audio"
148
+ ]
149
+ }
150
+ ],
151
+ "platforms": {
152
+ "phone": {
153
+ "supported": true
154
+ },
155
+ "browser": {
156
+ "supported": true
157
+ },
158
+ "desktop": {
159
+ "supported": true
160
+ }
161
+ },
162
+ "provenance": {
163
+ "notice": "MIT License\n\nCopyright (c) <year> <copyright holders>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and \nassociated documentation files (the \"Software\"), to deal in the Software without restriction, including \nwithout limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell \ncopies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the \nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial \nportions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT \nLIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO \nEVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER \nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE \nUSE OR OTHER DEALINGS IN THE SOFTWARE.\n"
164
+ }
165
+ },
166
+ "module": {
167
+ "schemaVersion": 1,
168
+ "entry": "main.js",
169
+ "files": {
170
+ "main.js": {
171
+ "sha256": "a5eed2032d2ba5365d2dbb5b4bb4d483f6e7aee8604a004b69cdcacca1d20b32",
172
+ "bytes": 4703,
173
+ "mediaType": "text/javascript"
174
+ }
175
+ }
176
+ },
177
+ "moduleSource": {
178
+ "main.js": "// Narration: one job in two hosted steps. With no script, the facts in the\n// brief become spoken words (text.generate) and the run ends so the person can\n// read and change them. With a script, the words become a voice (audio.speech),\n// or the person's own take is used as it is. Every call is quoted and approved\n// by StillMade; this code never sees a provider, a key or a price.\nconst WORDS_PER_MINUTE = 150;\nconst SPEECH_LIMIT = 3800;\nconst VOICE = /^[a-z][a-z0-9_-]{0,63}$/;\nconst TONES = {\n warm: 'Warm and welcoming, like a trusted local who wants to help.',\n friendly: 'Upbeat and friendly, in plain words and short sentences.',\n 'no-nonsense': 'Direct and factual, with no filler and no hype.',\n luxury: 'Calm, polished and unhurried; confident, never pushy.',\n};\n\nconst text = value => typeof value === 'string' ? value.trim() : '';\n// A brief or script value is {schemaVersion: 1, text}; older briefs carry a topic.\nfunction words(value) {\n if (typeof value === 'string') return value.trim();\n if (!value || typeof value !== 'object') return '';\n for (const key of ['text', 'topic', 'summary']) if (text(value[key])) return text(value[key]);\n return Object.entries(value).filter(([key, item]) => key !== 'schemaVersion' && ['string', 'number'].includes(typeof item)).map(([key, item]) => `${key}: ${item}`).join('\\n');\n}\nfunction targetWords(brief) {\n const seconds = /\\b(\\d{1,3})\\s*(?:s|sec|secs|second|seconds)\\b/i.exec(brief);\n if (seconds) return Math.max(15, Math.round(Number(seconds[1]) / 60 * WORDS_PER_MINUTE));\n const minutes = /\\b(\\d{1,2}|one|two|three)\\s*(?:min|mins|minute|minutes)\\b/i.exec(brief);\n if (minutes) return Math.min(560, ({ one: 1, two: 2, three: 3 }[minutes[1].toLowerCase()] || Number(minutes[1])) * WORDS_PER_MINUTE);\n return 75;\n}\nfunction toneGuidance(tone, rules) {\n const preset = TONES[String(tone || 'warm').toLowerCase()];\n return [preset || text(tone) || TONES.warm, text(rules)].filter(Boolean).join(' ');\n}\n// What the model returns is the script and nothing else: no fences, labels or quotes.\nfunction cleanScript(reply) {\n let script = text(reply).replace(/^```[a-z]*\\s*|\\s*```$/g, '').trim();\n script = script.replace(/^(?:script|narration|voiceover)\\s*:\\s*/i, '').trim();\n if (/^[\"“].*[\"”]$/s.test(script)) script = script.slice(1, -1).trim();\n return script.replace(/[ \\t]+\\n/g, '\\n').replace(/\\n{3,}/g, '\\n\\n');\n}\nconst SYSTEM = 'You write spoken narration for short videos. Use only the facts you are given: never add a price, mileage, phone number, address, web address, award, inspection, warranty or history that is not in them, and never claim to have checked anything. Write for the ear: plain sentences that read well aloud, no headings, lists, stage directions, speaker labels, emojis, markdown or quotation marks. Return the narration only.';\n\nexport default async function run(input, stillmade) {\n const brief = words(input.brief), script = words(input.script), rules = text(input.rules);\n if (!script) {\n if (!brief) throw new Error('Add the facts the voice should speak from, or type the script.');\n stillmade.progress(0.2, 'Writing the script');\n const count = targetWords(brief);\n const prompt = `Facts (use only these):\\n${brief}\\n\\nWrite the spoken narration for a short video about this. Tone: ${toneGuidance(input.tone, rules)} Length: about ${count} words (${Math.round(count / WORDS_PER_MINUTE * 60)} seconds at ${WORDS_PER_MINUTE} words a minute). End with the contact or next step from the facts when there is one.`;\n const reply = await stillmade.hosted.run('text.generate', { prompt, system: SYSTEM, maxTokens: Math.min(1600, Math.max(300, count * 4)) });\n const written = cleanScript(reply);\n if (!written) throw new Error('No script came back. Add more facts and write again.');\n stillmade.progress(1, 'Script written');\n return { script: { schemaVersion: 1, text: written } };\n }\n const result = { script: { schemaVersion: 1, text: script } };\n if (input.take) {\n stillmade.log('Using the supplied recording.');\n return { ...result, audio: input.take };\n }\n if (script.length > SPEECH_LIMIT) throw new Error(`The script is longer than one voice take (${SPEECH_LIMIT.toLocaleString('en-US')} characters). Shorten it, or split it.`);\n const voice = VOICE.test(String(input.voice || '')) ? String(input.voice) : 'onyx';\n const speed = Number.isFinite(input.speed) && input.speed >= 0.5 && input.speed <= 2 ? input.speed : 1;\n stillmade.progress(0.4, 'Making the voice');\n const audio = await stillmade.hosted.run('audio.speech', { text: script, settings: { voice, speed } });\n stillmade.progress(1, 'Voiceover ready');\n return { ...result, audio };\n}\n"
179
+ },
180
+ "view": {
181
+ "runtime": "frame",
182
+ "html": "<main class=\"sm-block nr\">\n <section class=\"sm-stage nr-stage\" id=\"stage\" aria-live=\"polite\">\n <div class=\"sm-empty\" id=\"empty\">\n <span class=\"sm-glyph\" data-glyph=\"audio\" aria-hidden=\"true\"></span>\n <p>No narration yet</p>\n </div>\n <div class=\"nr-take\" id=\"take\" hidden>\n <audio id=\"player\" controls preload=\"metadata\"></audio>\n <p class=\"nr-meta\"><span id=\"length\"></span><span id=\"from\"></span></p>\n </div>\n </section>\n <div class=\"sm-field\">\n <span class=\"sm-label\"><label for=\"facts\">Facts</label><details class=\"sm-hint\"><summary aria-label=\"About the facts\" title=\"About the facts\"></summary><p>What the voice may say: the thing, its details, who it is for, how long, and how to reach you. Chat fills this from your conversation; change it here.</p></details><span class=\"sm-status\" id=\"facts-state\"></span></span>\n <textarea id=\"facts\" rows=\"3\" maxlength=\"6000\" data-stillmade-share=\"facts\" placeholder=\"2021 Toyota RAV4 XLE, $27,995, 31,200 miles, one owner. Sunrise Motors, (555) 201-7788.\"></textarea>\n </div>\n <div class=\"sm-field\">\n <span class=\"sm-label\"><label for=\"words\">Script</label><span class=\"sm-status\" id=\"words-state\"></span></span>\n <textarea id=\"words\" rows=\"7\" maxlength=\"3800\" data-stillmade-share=\"words\" placeholder=\"Written from the facts, or type what the voice says.\"></textarea>\n </div>\n <fieldset class=\"sm-choices\" data-compact id=\"voice\" data-stillmade-share=\"voice\">\n <legend>Voice</legend>\n <label><input type=\"radio\" name=\"voice\" value=\"onyx\" checked><span class=\"sm-glyph\" data-glyph=\"audio\" aria-hidden=\"true\"></span><span>Onyx <small>deep</small></span></label>\n <label><input type=\"radio\" name=\"voice\" value=\"nova\"><span class=\"sm-glyph\" data-glyph=\"audio\" aria-hidden=\"true\"></span><span>Nova <small>bright</small></span></label>\n <label><input type=\"radio\" name=\"voice\" value=\"alloy\"><span class=\"sm-glyph\" data-glyph=\"audio\" aria-hidden=\"true\"></span><span>Alloy <small>even</small></span></label>\n </fieldset>\n <div class=\"sm-actions\">\n <button type=\"button\" class=\"sm-primary\" id=\"action\" data-stillmade-action=\"run\">Write script</button>\n <button type=\"button\" class=\"sm-quiet\" id=\"again\" hidden>Write again</button>\n <p class=\"sm-status\" id=\"status\" role=\"status\"></p>\n <details class=\"sm-hint\" id=\"why\" hidden><summary aria-label=\"Why\" title=\"Why\"></summary><p id=\"why-text\"></p></details>\n </div>\n <details class=\"sm-more\" id=\"more\">\n <summary>More</summary>\n <div class=\"sm-settings\">\n <fieldset class=\"sm-choices\" data-compact id=\"tone\" data-stillmade-share=\"tone\">\n <legend>Tone</legend>\n <label><input type=\"radio\" name=\"tone\" value=\"warm\" checked><span>Warm</span></label>\n <label><input type=\"radio\" name=\"tone\" value=\"friendly\"><span>Friendly</span></label>\n <label><input type=\"radio\" name=\"tone\" value=\"no-nonsense\"><span>No-nonsense</span></label>\n <label><input type=\"radio\" name=\"tone\" value=\"luxury\"><span>Luxury</span></label>\n </fieldset>\n <fieldset class=\"sm-choices\" data-segmented id=\"speed\" data-stillmade-share=\"speed\">\n <legend>Speed</legend>\n <label><input type=\"radio\" name=\"speed\" value=\"0.9\"><span>Slower</span></label>\n <label><input type=\"radio\" name=\"speed\" value=\"1\" checked><span>Normal</span></label>\n <label><input type=\"radio\" name=\"speed\" value=\"1.15\"><span>Faster</span></label>\n </fieldset>\n <div class=\"sm-field\"><label for=\"rules\">Own rules</label><input id=\"rules\" type=\"text\" maxlength=\"300\" data-stillmade-share=\"rules\" placeholder=\"Short sentences, no exclamation marks\"></div>\n </div>\n </details>\n</main>\n",
183
+ "css": "/* The Block kit (injected by the host) carries the layout; this file only draws\n the voiceover on the stage: the player and its two facts. */\n.nr [hidden] { display: none !important; }\n.nr-stage { display: grid; align-content: center; justify-items: stretch; gap: var(--s-2, 8px); min-height: 180px; }\n/* A voiceover is short: on a wide screen the stage hugs the player instead of stretching down the column. */\n@container (min-width: 860px) { .sm-block:not([data-layout]) > .nr-stage { align-self: start; min-height: 220px; } }\n.nr-stage > .sm-empty { justify-self: center; }\n.nr-take { display: grid; gap: var(--s-2, 8px); width: 100%; }\n.nr-take audio { width: 100%; }\n.nr-meta { display: flex; justify-content: space-between; gap: var(--s-3, 12px); margin: 0; color: var(--fg-muted); font-size: var(--fs-body-s, .875rem); font-variant-numeric: tabular-nums; }\n#voice { --sm-tile: 120px; }\n#voice small { color: var(--fg-subtle); font-size: var(--fs-caption, .8125rem); margin-inline-start: 4px; }\n#tone { --sm-tile: 110px; }\n",
184
+ "javascript": "(()=>{\n\"use strict\";\n// The Narration interface on the Block kit: the voiceover on the stage, the\n// facts and the script on the left, the voice as chips, and one button that\n// names the next step: Write script, then Make voiceover, or Use my recording.\n// The status beside it says the state in a few words; the price is StillMade's.\nconst $ = id => document.getElementById(id);\nconst state = { input: {}, outputs: {}, busy: false, previewUrl: null, previewFor: null, factsFromChat: false };\nconst message = error => String(error && error.message || error || 'Something went wrong.');\nconst say = (id, text) => { $(id).textContent = text || ''; };\nconst trimmed = value => String(value || '').trim();\nconst wordCount = text => trimmed(text) ? trimmed(text).split(/\\s+/).length : 0;\nconst clock = seconds => { const whole = Math.max(0, Math.round(seconds)); return Math.floor(whole / 60) + ':' + String(whole % 60).padStart(2, '0'); };\nconst spoken = text => 'about ' + clock(wordCount(text) / 150 * 60);\nconst picked = name => { const node = document.querySelector('input[name=\"' + name + '\"]:checked'); return node ? node.value : ''; };\nconst pick = (name, value) => { const node = document.querySelector('input[name=\"' + name + '\"][value=\"' + value + '\"]'); if (node) node.checked = true; };\n// A brief or script value is {schemaVersion: 1, text}; older briefs carry a topic.\nconst words = value => typeof value === 'string' ? value : value && typeof value === 'object' ? trimmed(value.text || value.topic || value.summary || '') : '';\nconst facts = () => trimmed($('facts').value);\nconst script = () => trimmed($('words').value);\nconst sameWords = (a, b) => trimmed(a).replace(/\\s+/g, ' ') === trimmed(b).replace(/\\s+/g, ' ');\n\nfunction status(text, tone, why) {\n const node = $('status');\n node.textContent = text || '';\n if (tone) node.dataset.state = tone; else delete node.dataset.state;\n $('why').hidden = !why; $('why').open = false; say('why-text', why || '');\n}\n// A field the host shares is written through its own input event, so the edit is kept.\nfunction setField(id, value) {\n const node = $(id);\n if (node.value === value) return;\n node.value = value;\n node.dispatchEvent(new Event('input', { bubbles: true }));\n node.dispatchEvent(new Event('change', { bubbles: true }));\n}\n\n// ── What the person is about to do ──────────────────────────────────────\nfunction phase() {\n if (!script()) return 'write';\n if (state.input.take) return 'take';\n const audio = state.outputs.audio, made = state.outputs.script;\n return audio && made && sameWords(words(made), script()) ? 'ready' : 'voice';\n}\nfunction render() {\n const p = phase(), count = wordCount(script());\n say('words-state', count ? count + ' words · ' + spoken(script()) : '');\n say('facts-state', facts() ? (state.factsFromChat ? 'From Chat' : '') : 'Needed');\n const labels = { write: 'Write script', voice: 'Make voiceover', ready: 'Make voiceover again', take: 'Use my recording' };\n say('action', labels[p]);\n $('again').hidden = p === 'write';\n $('action').disabled = state.busy || (p === 'write' && !facts());\n $('again').disabled = state.busy;\n if (state.busy) return;\n if (p === 'write') status(facts() ? 'Written from the facts' : 'Add the facts first', facts() ? '' : 'waiting', facts() ? 'The script uses only what the facts say; you can change every word before the voice runs.' : 'Chat usually fills the facts from your conversation. Type them here when it has not.');\n else if (p === 'take') status('Your recording will be used', '', 'The audio you supplied becomes the voiceover; nothing is generated.');\n else if (p === 'voice') status(state.outputs.audio ? 'Script changed, so the voice runs again' : 'Script is done, so only the voice runs', '', 'Only the voice is generated now; the words stay as written.');\n else status('Voiceover ready', 'success', '');\n}\n\n// ── The stage: the voiceover, when there is one ───────────────────────────\nasync function showAudio(reference, from) {\n if (!reference) { $('take').hidden = true; $('empty').hidden = false; return; }\n $('empty').hidden = true; $('take').hidden = false;\n say('from', from || '');\n say('length', Number.isFinite(reference.duration) ? clock(reference.duration) : spoken(script()));\n const key = reference.assetId + '/' + reference.versionId;\n if (state.previewFor === key) return;\n if (state.previewUrl) { try { StillMade.releaseMedia(state.previewUrl); } catch {} state.previewUrl = null; }\n state.previewFor = key;\n try {\n const preview = await StillMade.previewMedia(reference);\n if (state.previewFor !== key) { try { StillMade.releaseMedia(preview.url); } catch {} return; }\n state.previewUrl = preview.url; $('player').src = preview.url;\n } catch (error) {\n if (typeof reference.url === 'string' && /^https?:|^\\//.test(reference.url)) $('player').src = reference.url;\n else status(message(error), 'error', '');\n }\n}\n\n// ── Running ───────────────────────────────────────────────────────────────\nfunction values(forVoice) {\n const value = {};\n if (facts()) value.brief = { schemaVersion: 1, text: facts() };\n value.tone = picked('tone') || 'warm';\n const rules = trimmed($('rules').value); if (rules) value.rules = rules;\n if (forVoice) {\n value.script = { schemaVersion: 1, text: script() };\n value.voice = picked('voice') || 'onyx';\n value.speed = Number(picked('speed')) || 1;\n if (state.input.take) value.take = state.input.take;\n }\n return value;\n}\nasync function act(writeAgain) {\n if (state.busy) return;\n const p = writeAgain ? 'write' : phase();\n state.busy = true; render();\n status(p === 'write' ? 'Writing the script' : p === 'take' ? 'Keeping your recording' : 'Making the voice', 'loading', '');\n try {\n const result = await StillMade.run(values(p !== 'write'));\n state.outputs = { ...state.outputs, ...result };\n if (p === 'write') { setField('words', words(result.script)); await showAudio(null); }\n else await showAudio(result.audio, p === 'take' ? 'Your recording' : voiceName());\n } catch (error) {\n status(message(error), 'error', '');\n state.busy = false; render(); return;\n }\n state.busy = false; render();\n}\nconst voiceName = () => { const node = document.querySelector('input[name=\"voice\"]:checked'); return node ? node.parentElement.textContent.trim().split(/\\s+/)[0] : 'Voice'; };\n$('action').addEventListener('click', () => act(false));\n$('again').addEventListener('click', () => act(true));\nfor (const id of ['facts', 'words', 'rules']) $(id).addEventListener('input', () => { if (id === 'facts') state.factsFromChat = false; render(); });\nfor (const name of ['tone', 'voice', 'speed']) for (const node of document.querySelectorAll('input[name=\"' + name + '\"]')) node.addEventListener('change', render);\n\n// ── What the host gives: the Step's inputs and what was saved before ──────\nStillMade.onInput(input => {\n state.input = input || {};\n fillFromInput();\n if (typeof state.input.tone === 'string' && state.input.tone in { warm: 1, friendly: 1, 'no-nonsense': 1, luxury: 1 }) pick('tone', state.input.tone);\n if (typeof state.input.voice === 'string') pick('voice', state.input.voice);\n render();\n});\n// The host's shared fields start from the saved shared state, which can arrive after the\n// Step's input did and leave a bound field empty; what the input gave is put back then.\nfunction fillFromInput() {\n const brief = words(state.input.brief);\n if (brief && !facts()) { setField('facts', brief); state.factsFromChat = true; }\n const given = words(state.input.script);\n if (given && !script()) setField('words', given);\n}\nfunction adopt(snapshot) {\n const outputs = snapshot && snapshot.outputs || {};\n state.outputs = outputs;\n fillFromInput();\n if (outputs.script && !script()) setField('words', words(outputs.script));\n if (outputs.audio) showAudio(outputs.audio, state.input.take ? 'Your recording' : voiceName()); else showAudio(null);\n render();\n}\nif (typeof StillMade.onShared === 'function') StillMade.onShared(adopt);\ntry { if (typeof StillMade.getShared === 'function') { const shared = StillMade.getShared(); if (shared && typeof shared.then === 'function') shared.then(adopt, () => {}); else adopt(shared); } } catch {}\nrender();\n\n})();\n"
185
+ },
186
+ "tests": [
187
+ {
188
+ "name": "Uses the supplied recording as the voiceover and keeps the script beside it",
189
+ "input": {
190
+ "script": {
191
+ "schemaVersion": 1,
192
+ "text": "Looking for an SUV that does it all? Come see this 2021 Toyota RAV4 XLE at Sunrise Motors."
193
+ },
194
+ "take": {
195
+ "kind": "audio",
196
+ "assetId": "own-take-asset",
197
+ "versionId": "own-take-v1",
198
+ "url": "https://media.example/own-take.mp3",
199
+ "mimeType": "audio/mpeg",
200
+ "duration": 34.2,
201
+ "bytes": 548000
202
+ }
203
+ },
204
+ "expected": {
205
+ "script": {
206
+ "schemaVersion": 1,
207
+ "text": "Looking for an SUV that does it all? Come see this 2021 Toyota RAV4 XLE at Sunrise Motors."
208
+ },
209
+ "audio": {
210
+ "kind": "audio",
211
+ "assetId": "own-take-asset",
212
+ "versionId": "own-take-v1",
213
+ "url": "https://media.example/own-take.mp3",
214
+ "mimeType": "audio/mpeg",
215
+ "duration": 34.2,
216
+ "bytes": 548000
217
+ }
218
+ }
219
+ }
220
+ ]
221
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stillmade-sdk",
3
- "version": "0.1.24",
3
+ "version": "0.1.25",
4
4
  "description": "StillMade Block SDK, CLI and examples",
5
5
  "license": "PolyForm-Shield-1.0.0",
6
6
  "type": "module",
@@ -297,9 +297,9 @@ export async function main(rawArgs) {
297
297
  const scan=scanPackage(pkg);if(!scan.passed)throw Object.assign(new Error('Portable admission failed'),{report:scan});
298
298
  const evidence=['validate','pack'].includes(command)?await verifyPortableSources(pkg):null;
299
299
  if(command==='validate')return {valid:true,format:pkg.portable.format,id:pkg.manifest.id,security:scan,execution:'not-run',repositoryEvidence:evidence,...(args.includes('--mobile')?{mobileProfile:mobileProfile()}:{}),...(hosted?{tests:0,fixtureContracts:pkg.tests.length,liveVerified:false,reviewRequired:true}:{})};
300
- if(command==='test'){const report=await admitLocalPackage(pkg);if(!report.passed)throw Object.assign(new Error('Portable admission failed'),{report});return report;}
300
+ if(command==='test'){const report=await admitLocalPackage(pkg,{files:moduleFiles.get(pkg),moduleExecution:true});if(!report.passed)throw Object.assign(new Error('Portable admission failed'),{report});return report;}
301
301
  if(command==='pack'){
302
- const report=await admitLocalPackage(pkg);if(!report.passed)throw Object.assign(new Error('Portable admission failed'),{report});
302
+ const report=await admitLocalPackage(pkg,{files:moduleFiles.get(pkg),moduleExecution:true});if(!report.passed)throw Object.assign(new Error('Portable admission failed'),{report});
303
303
  const output=resolve(extra||`${pkg.manifest.id}-${pkg.manifest.version}.stillmade-block`);
304
304
  if(!output.endsWith('.stillmade-block'))throw new Error('Canonical output must end in .stillmade-block');
305
305
  await writeFile(output,writePortableArchive(pkg,archiveCodec()),{flag:'wx'});return {file:output,digest:report.digest,tests:hosted?0:pkg.tests.length,format:pkg.portable.format,admission:report,repositoryEvidence:evidence,...(hosted?{fixtureContracts:pkg.tests.length,execution:'not-run',liveVerified:false,reviewRequired:true}:{})};
@@ -81,7 +81,7 @@ function rehearsalContext(packages){
81
81
  * @param options.liveReceipt {digest} of a reviewed live hosted run for this exact package
82
82
  * @param options.unsupported {reason,gapClass,primitive} when authoring reported a platform gap
83
83
  */
84
- export async function assessBlockReadiness(pkg,{catalog=FIRST_PARTY_SDK_PACKAGES,admission,platformChecks,sharedEditing,liveReceipt,unsupported,budgetMs=20000,signal}={}){
84
+ export async function assessBlockReadiness(pkg,{catalog=FIRST_PARTY_SDK_PACKAGES,admission,platformChecks,sharedEditing,liveReceipt,unsupported,budgetMs=20000,signal,moduleExecution=false}={}){
85
85
  const deadline=Date.now()+budgetMs,left=()=>deadline-Date.now(),items=new Map(),composition={};
86
86
  const set=(id,state,detail={})=>{if(!items.has(id)||items.get(id).state!=='failed')items.set(id,{id,title:TITLES[id],state,...detail});};
87
87
  const verified=(id,evidence)=>set(id,'verified',{evidence});
@@ -103,7 +103,7 @@ export async function assessBlockReadiness(pkg,{catalog=FIRST_PARTY_SDK_PACKAGES
103
103
 
104
104
  const manifest=pkg.manifest,sourceDigest=await digest(pkg),hosted=!offline(pkg);
105
105
  let report=admission;
106
- if(!report){try{report=await admitLocalPackage(pkg,{timeoutMs:Math.max(1000,Math.min(15000,left()))});}catch(error){
106
+ if(!report){try{report=await admitLocalPackage(pkg,{timeoutMs:Math.max(1000,Math.min(15000,left())),moduleExecution:moduleExecution===true});}catch(error){
107
107
  // Admission running out of this check's own time budget is not a Block defect.
108
108
  const exhausted=error.code==='TIMEOUT'&&/shared budget/.test(error.message||'');
109
109
  report={passed:false,results:[{name:'Admission',passed:false,code:exhausted?'READINESS_BUDGET':error.code,message:message(error)}]};
@@ -2,6 +2,7 @@ import {REVENUE_REVIEW_PROJECT_TYPES} from './revenue-review-project-types.js';
2
2
  import {PRODUCT_AD_VARIANTS_PROJECT_TYPE,PRODUCT_AD_VARIANTS_PROJECT_TYPE_1_0_3,PRIOR_PRODUCT_AD_VARIANTS_PROJECT_TYPE,PREVIOUS_PRODUCT_AD_VARIANTS_PROJECT_TYPE} from './product-ad-variants-project-type.js';
3
3
  import {FIRST_PARTY_SDK_PACKAGES} from '../block-sdk/first-party-packages.js';
4
4
  import {FIRST_REVENUE_PROJECT_TYPES} from './first-revenue-project-types.js';
5
+ import {DEALER_INVENTORY_VIDEO_PROJECT_TYPE} from './dealer-inventory-video-project-type.js';
5
6
  import { SDK_VERSION, ROOT_CHAT_ID } from '../block-sdk/contracts.js';
6
7
  import {SCRIPT_WRITER_BLOCK} from './script-writer.js';
7
8
  import {VIDEO_ASSETS_BLOCK} from './video-assets.js';
@@ -133,16 +134,24 @@ const currentPortableById=new Map();
133
134
  // An unset app connector is a project placeholder, not something to find or install.
134
135
  // Photo notes is the photo check inside Pick best photos, not a Step of its own.
135
136
  export const UNDISCOVERABLE_SDK_BLOCKS=new Set(['sm.connect-app','sm.photo-notes']);
137
+ // Hidden from discovery (October 20, 2026, owner decision): fragments a job now covers (Script
138
+ // Writer and Voiceover are Narration), a second Chat, tools that belong inside their app (the
139
+ // assistants, the Editor's AI edits, references, compare, image editor, scoring) and thin results.
140
+ // Every release stays in BUILTIN_BLOCKS: saved projects and built-in Types keep their pins and run.
141
+ export const HIDDEN_BLOCKS=new Set(['sm.script','sm.voiceover','sm.chat','sm.thumbnail','sm.shot-plan-assistant','sm.blueprint-assistant','sm.animated-scene-assistant','sm.whiteboard-assistant','sm.final-edit','sm.edit-assistant','sm.character','sm.location','sm.style','sm.compare','sm.image-editor','sm.score','sm.research','sm.quality-audit','sm.rough-cut']);
142
+ // Exact releases to hide (id@version) when one release of a shown Block should not be offered; none today.
143
+ export const HIDDEN_RELEASES=new Set([]);
144
+ export const isHiddenBlock=block=>HIDDEN_BLOCKS.has(block.manifest.id)||HIDDEN_RELEASES.has(`${block.manifest.id}@${block.manifest.version}`);
136
145
  // Export video is offered as the original Editor opened at its Export (1.0.0), so
137
146
  // people can still change the cut before rendering (the owner, Oct 15). The
138
147
  // render-only 2.0.0 stays in BUILTIN_BLOCKS for exact project pins and for
139
148
  // planning with explicit candidates; a prepared timeline goes to the portable Editor.
140
149
  const EXPORT_VIDEO_EDITOR=LEGACY_BLOCKS.find(block=>block.manifest.id==='sm.export-video'&&block.manifest.version==='1.0.0');
141
150
  export const DISCOVERED_AS_HOST=new Map([['sm.export-video',EXPORT_VIDEO_EDITOR]]);
142
- for(const block of SDK_BUILTIN_BLOCKS){if(UNDISCOVERABLE_SDK_BLOCKS.has(block.manifest.id)||DISCOVERED_AS_HOST.has(block.manifest.id))continue;const prior=currentPortableById.get(block.manifest.id);if(!prior||newer(block.manifest.version,prior.manifest.version))currentPortableById.set(block.manifest.id,block);}
151
+ for(const block of SDK_BUILTIN_BLOCKS){if(UNDISCOVERABLE_SDK_BLOCKS.has(block.manifest.id)||DISCOVERED_AS_HOST.has(block.manifest.id)||isHiddenBlock(block))continue;const prior=currentPortableById.get(block.manifest.id);if(!prior||newer(block.manifest.version,prior.manifest.version))currentPortableById.set(block.manifest.id,block);}
143
152
  export const DISCOVERABLE_SDK_BUILTIN_BLOCKS = Object.freeze([...currentPortableById.values()]);
144
153
  const ACCEPTED_PORTABLE_RELEASES=[...MAIN_HOST_WORKSPACES].map(id=>FIRST_PARTY_SDK_PACKAGES.find(pkg=>pkg.manifest.id===id&&pkg.manifest.version===CURRENT_PORTABLE_WORKSPACE_VERSIONS[id])).filter(Boolean);
145
- export const DISCOVERABLE_BLOCKS = [STARTING_CHAT_BLOCK,...WORKSPACE_BLOCKS,...ACCEPTED_PORTABLE_RELEASES,...DISCOVERED_AS_HOST.values(),...DISCOVERABLE_SDK_BUILTIN_BLOCKS.filter(block=>!ACCEPTED_PORTABLE_RELEASES.includes(block)).filter(block=>!WORKSPACE_BLOCKS.some(current=>current.manifest.id===block.manifest.id&&current.manifest.version===block.manifest.version))];
154
+ export const DISCOVERABLE_BLOCKS = [STARTING_CHAT_BLOCK,...WORKSPACE_BLOCKS,...ACCEPTED_PORTABLE_RELEASES,...DISCOVERED_AS_HOST.values(),...DISCOVERABLE_SDK_BUILTIN_BLOCKS.filter(block=>!ACCEPTED_PORTABLE_RELEASES.includes(block)).filter(block=>!WORKSPACE_BLOCKS.some(current=>current.manifest.id===block.manifest.id&&current.manifest.version===block.manifest.version))].filter(block=>!isHiddenBlock(block));
146
155
  // Planners pin a finished file to a Block's output port, which the Editor alias has
147
156
  // none of, so planning keeps the newest render-only release in its place. Chat
148
157
  // builds then fold it into the Editor (compact-project.js), so people still get
@@ -179,7 +188,7 @@ const PRIOR_CURRENT_TYPES = currentBuiltinTypes('1.13.0','2.2.3',SCRIPT_WRITER_B
179
188
  const PREVIOUS_PORTABLE_TYPES=currentBuiltinTypes('1.14.0','2.2.4',SCRIPT_WRITER_BLOCK.manifest.version,{'blueprint':'2.1.8','panel-strip':'2.3.6','board-editor':'2.2.9','animated-scene':'2.3.7','editor':'2.2.8'});
180
189
  // 1.16.0: every built-in type ends in the original Editor (1.0), the full video editor.
181
190
  const PORTABLE_EDITOR_TYPES=currentBuiltinTypes('1.15.0','2.2.4','2.1.4',{'voiceover':'2.1.4','shot-plan':'2.1.3','blueprint':'2.1.9','panel-strip':'2.3.7','board-editor':'2.2.10','animated-scene':'2.3.8','editor':'2.2.9'});
182
- export const CURRENT_BUILTIN_TYPES = [...currentBuiltinTypes('1.16.0','2.2.4','2.1.4',{'voiceover':'2.1.4','shot-plan':'2.1.3','blueprint':'2.1.9','panel-strip':'2.3.7','board-editor':'2.2.10','animated-scene':'2.3.8','editor':'1.0.0'}),...REVENUE_REVIEW_PROJECT_TYPES,PRODUCT_AD_VARIANTS_PROJECT_TYPE,...FIRST_REVENUE_PROJECT_TYPES];
191
+ export const CURRENT_BUILTIN_TYPES = [...currentBuiltinTypes('1.16.0','2.2.4','2.1.4',{'voiceover':'2.1.4','shot-plan':'2.1.3','blueprint':'2.1.9','panel-strip':'2.3.7','board-editor':'2.2.10','animated-scene':'2.3.8','editor':'1.0.0'}),...REVENUE_REVIEW_PROJECT_TYPES,PRODUCT_AD_VARIANTS_PROJECT_TYPE,...FIRST_REVENUE_PROJECT_TYPES,DEALER_INVENTORY_VIDEO_PROJECT_TYPE];
183
192
  const HISTORICAL_CURRENT_TYPES=[...PORTABLE_EDITOR_TYPES,PRODUCT_AD_VARIANTS_PROJECT_TYPE_1_0_3,PRIOR_PRODUCT_AD_VARIANTS_PROJECT_TYPE,PREVIOUS_PRODUCT_AD_VARIANTS_PROJECT_TYPE,...PREVIOUS_PORTABLE_TYPES,...PRIOR_CURRENT_TYPES,...SHARED_WORKSPACE_TYPES,...SHARED_CANVAS_TYPES,...SHARED_BLUEPRINT_TYPES,...PRIOR_SHARED_TYPES,...MEDIA_LIBRARY_TYPES,...EDITOR_AUDIO_TYPES,...PLAYABLE_EDITOR_TYPES,...ADVANCED_PORTABLE_TYPES,...PORTABLE_MVP_TYPES,...PRE_PROMOTION_CURRENT_TYPES,...PREVIOUS_BUILTIN_TYPES.filter(type=>flows[type.legacyMode].includes('shot-plan')),...currentBuiltinTypes('1.1.0','1.1.0','1.1.0',PRIOR_HOST_WORKSPACE_VERSIONS)];
184
193
  export const BUILTIN_TYPES = [...CURRENT_BUILTIN_TYPES,...HISTORICAL_CURRENT_TYPES,...LEGACY_TYPES];
185
194
  export const workspaceBlock = (id,version)=>DISCOVERABLE_BLOCKS.find(block=>block.manifest.id===id && (!version || block.manifest.version===version));
@@ -0,0 +1,33 @@
1
+ // Dealer Inventory Video (October 20, 2026): one vehicle listing and its photos
2
+ // become a narrated inventory video in two Steps after Chat. Chat gathers the
3
+ // facts into Narration's brief; Narration writes the words and voices them (one
4
+ // Block, two hosted operations); the photos are cut to the words in one Video
5
+ // Step made of Import media, Shots and Export video. Each release is pinned:
6
+ // adopt a newer Block in a new Type version, never by editing this one.
7
+ import narration from '../../blocks/narration/versions/1.0.0/package.js';
8
+ import mediaImport from '../../blocks/native/sm.import-media/versions/2.0.2/package.js';
9
+ import shots from '../../blocks/native/sm.shot-plan/versions/2.2.1/package.js';
10
+ import exportVideo from '../../blocks/native/sm.export-video/versions/2.1.0/package.js';
11
+
12
+ const description='Turn one vehicle listing and its photos into a narrated inventory video: Chat gathers the facts, Narration writes and voices them, and the photos are cut to the words.';
13
+ export const DEALER_INVENTORY_VIDEO_PROJECT_TYPE=Object.freeze({
14
+ schemaVersion:1,id:'stillmade.dealer-inventory-video',version:'1.0.0',name:'Dealer Inventory Video',description,license:'MIT',startingChat:'native',
15
+ summary:{text:description,origin:'human',generation:'authored',status:'ready'},
16
+ defaults:{aspectRatio:'16:9',brief:'Tell Chat the vehicle: year, make, model and trim, price, mileage, the features worth saying, the dealership name and how to reach it. Narration writes the words from those facts only and reads them in the voice you pick, or uses your own recording. Import the vehicle photos, check the shots, and make the video.'},
17
+ onboarding:[],
18
+ packages:[narration,mediaImport,shots,exportVideo].map(pkg=>structuredClone(pkg)),
19
+ stages:[
20
+ {id:'narration',blockId:narration.manifest.id,version:narration.manifest.version,label:'Narration'},
21
+ {id:'photos',blockId:mediaImport.manifest.id,version:mediaImport.manifest.version,label:'Vehicle photos',config:{include:'Images',duplicates:'Remove duplicates'}},
22
+ {id:'shots',blockId:shots.manifest.id,version:shots.manifest.version,label:'Shots'},
23
+ {id:'video',blockId:exportVideo.manifest.id,version:exportVideo.manifest.version,label:'Video'},
24
+ ],
25
+ connections:[
26
+ {from:{stage:'narration',port:'script'},to:{stage:'shots',port:'narrationScript'}},
27
+ {from:{stage:'photos',port:'assets'},to:{stage:'video',port:'media'}},
28
+ {from:{stage:'narration',port:'audio'},to:{stage:'video',port:'narration'}},
29
+ ],
30
+ // Photos, shots and the render are one Step a person opens: the video.
31
+ groups:[{id:'make-video',label:'Video',stages:['photos','shots','video'],result:{stage:'video',port:'output'}}],
32
+ outcome:{type:'export',dedupe:'material_project_state',minimumDurationSec:5},
33
+ });
@@ -3,6 +3,7 @@
3
3
  // immutable, so the plain wording for first-party Blocks lives here, by Block id.
4
4
  // A Block whose own description is already plain needs no entry.
5
5
  export const PLAIN_BLOCK_DESCRIPTIONS=Object.freeze({
6
+ 'stillmade.narration':'Turn the facts from Chat into a script, hear it in a catalog voice or use your own recording, and pass the script and the voiceover on.',
6
7
  'stillmade.screen-recorder':'Record your screen or a window, keep it on your device, cut out the parts you do not want, and pass it on.',
7
8
  'sm.script':'Write and edit a script with AI. Set a length, add notes, and go back to earlier versions.',
8
9
  'sm.shot-plan':'Turn a script into a list of timed shots, then edit what each shot shows and how long it lasts.',
@@ -37,6 +37,9 @@ const SECTIONS=Object.freeze([
37
37
  rule('reuse-first','both',
38
38
  'Reuse before building: use, remix or chain an existing Block when one fits; build only what is missing.',
39
39
  'Before building, check what already exists (`find_existing_blocks` over MCP, or the Marketplace and your library) by the inputs, results and operations the goal needs. Use an installed or reviewed Block that already does it; remix a close one so its license, notices and creator credit stay attached; chain existing Blocks and build only the missing step. Never republish another creator\'s source without remix lineage: public copies are refused. Build a new Block when nothing fits or the person asks for their own version, and say which similar Blocks you considered.'),
40
+ rule('block-tier','both',
41
+ 'Three sizes: an app a person works in, a job that makes one deliverable from the hosted operations it needs (what Chat picks), an ingredient (one hosted operation) that only a job is made of.',
42
+ 'Blocks come in three sizes and the manifest decides which (packages/block-sdk/block-tier.js): an app is a workspace a person works in (Editor, Canvas, Shots), placed in a Type and never wired automatically; a job is one deliverable a person would name (the narration, the captioned clip), built from the hosted operations it needs (`permissions.capabilities`) and reviewed in one place, the unit Chat picks for a Step; an ingredient is one hosted operation with nothing of its own (a declarative capability Block), what a job is made of. When two ingredients would stand in a row to make one deliverable (writing the words, then voicing them), build or pick the job that does both and keep the review point inside it. A job declares a second output when another Block consumes the intermediate (the script beside the voice); split into two jobs only when the intermediate has a life of its own or the halves have different owners or payment.'),
40
43
  rule('block-scope','both',
41
44
  'One Step per job: start one only where a person works with the result, where finding new items turns into handling each, or where the result leaves StillMade; all else is a part.',
42
45
  'A Step is one job: the work between two moments where a person or the outside world must see the result. Start a new Step only at one of three points; everything else is a part of the neighboring Step.\n\n1. **A person works with the result**: reads, edits, picks or approves it (a workspace Block such as a script, a Canvas or an Editor, or an approval). That is why Chat, Bible, Canvas and Editor are four Steps.\n2. **Finding new things turns into handling each one**: the trigger (a schedule or an app event), the app read, its filter and "only new" are the first job; what is done with each item is the next job. A trigger is a setting of the first job, never a Step of its own.\n3. **The result leaves StillMade**: it is sent, posted, published, paid or written into another app. Consecutive writes to the same app, with the glue that shapes data for them, are one job.\n\nIn a production the Steps are the workspaces a person opens: work feeding the next workspace is its part, else it joins the one before; neighbouring workspaces with one main result type are one Step unless another part feeds the second. A video ends in the Editor (1.0), which has captions and export built in, so Export video is never a Step beside it.\n\nA part that only runs in the desktop app cannot share a Step with cloud parts. Before adding a Step, name which of the three reasons makes it separate; if none applies, make it a part of a neighbor. A Step nobody would ever open must not exist on its own. Prefer an app task\'s own filter (a Gmail query such as `has:attachment filename:png`) to an AI check, and use AI decide only for judgement.\n\nInside one job, each capability stays its own part: a capability Block performs exactly one hosted operation and a connector one app task, so every part keeps its own permission, approval and credit rules, and StillMade groups the parts of one job into one Step (`groups` in the Project Type). Never invent a combined runtime. When one Block can do the whole job with documented primitives (a JavaScript Block doing several local transforms, a workspace owning its whole document), build it as one Block.\n\nExamples: "every new email with an image becomes a generated image" is two Steps, "Gmail · New emails with images" (When, fetch with an image filter, only new) and "Image from email" (AI write, Generate image); listing When, Fetch, Only new, AI decide, Continue only if, AI write and Generate image as seven Steps splits one job into its parts. "A new Meta lead goes to HubSpot" is three Steps: "Meta Ads · New leads", "Qualify lead" and "HubSpot · Add contact".'),
@@ -133,7 +136,7 @@ export const BUILDER_CONTRACT=Object.freeze({
133
136
  gapClasses:GAP_CLASSES,
134
137
  });
135
138
 
136
- const SECTION_TITLES=Object.freeze({'grounded-text':'Only supplied facts','source-of-truth':'Source of truth','reuse-first':'Reuse first','block-scope':'One job per Step','capability-contract':'Define the capability first','feature-map':'Feature map','compose':'Blocks compose','platform-primitives':'Use platform primitives','no-workarounds':'No hidden workarounds','naming':'Name it briefly','ui':'Interface','multiplayer':'Shared editing','external-services':'External services','dependencies':'Dependencies','security':'Security','lifecycle':'Execution lifecycle','errors':'Useful failures','adapt-to-platform':'Adapt to the platform','verification':'What StillMade verifies','project-types':'Project Types','readiness-report':'Readiness report'});
139
+ const SECTION_TITLES=Object.freeze({'grounded-text':'Only supplied facts','source-of-truth':'Source of truth','reuse-first':'Reuse first','block-tier':'Three sizes of Block','block-scope':'One job per Step','capability-contract':'Define the capability first','feature-map':'Feature map','compose':'Blocks compose','platform-primitives':'Use platform primitives','no-workarounds':'No hidden workarounds','naming':'Name it briefly','ui':'Interface','multiplayer':'Shared editing','external-services':'External services','dependencies':'Dependencies','security':'Security','lifecycle':'Execution lifecycle','errors':'Useful failures','adapt-to-platform':'Adapt to the platform','verification':'What StillMade verifies','project-types':'Project Types','readiness-report':'Readiness report'});
137
140
  const inScope=(scope,wanted)=>wanted==='all'||scope==='both'||scope===wanted;
138
141
 
139
142
  /** Render the contract for a model prompt ('compact'), a guide ('full') or an SDK folder ('agents-md'). */
@@ -175,6 +175,7 @@ export const FIRST_PARTY_SDK_IDENTITIES=Object.freeze(new Set([
175
175
  "sm.whiteboard-assistant@1.0.0",
176
176
  "sm.write-summary@1.0.0",
177
177
  "sm.youtube@2.0.0",
178
+ "stillmade.narration@1.0.0",
178
179
  "stillmade.occasion-story@1.0.0",
179
180
  "stillmade.occasion-story@1.0.1",
180
181
  "stillmade.occasion-story@1.0.2",
@@ -3,6 +3,7 @@ import shortFormFinishV101 from '../../blocks/short-form-finish/versions/1.0.1/p
3
3
  import shortFormFinishV102 from '../../blocks/short-form-finish/versions/1.0.2/package.js';
4
4
  import shortFormFinish from '../../blocks/short-form-finish/package.js';
5
5
  import screenRecorder from '../../blocks/screen-recorder/package.js';
6
+ import narration from '../../blocks/narration/package.js';
6
7
  import occasionStoryV100 from '../../blocks/occasion-story/versions/1.0.0/package.js';
7
8
  import releaseVisualsV100 from '../../blocks/release-visuals/versions/1.0.0/package.js';
8
9
  import occasionStoryV101 from '../../blocks/occasion-story/versions/1.0.1/package.js';
@@ -190,5 +191,5 @@ import pickPhotos from '../../blocks/native/sm.pick-photos/versions/1.0.0/packag
190
191
  // Exact, independently executable first-party releases. Host workspaces do not
191
192
  // belong here. Changing implementation requires a new immutable version.
192
193
  const freeze=value=>{if(value&&typeof value==='object'&&!Object.isFrozen(value)){Object.values(value).forEach(freeze);Object.freeze(value);}return value;};
193
- export const FIRST_PARTY_SDK_PACKAGES = Object.freeze([freeze(productAdVariants),freeze(pickPhotos),freeze(photoNotes),freeze(youtubePublication),freeze(stockSearch),freeze(exportVideoV210),freeze(exportVideo),freeze(exportAudio),freeze(motionGraphics),freeze(audioScore),freeze(shortFormFinish),freeze(screenRecorder),freeze(shortFormFinishV102),freeze(shortFormFinishV101),freeze(shortFormFinishV100),freeze(occasionStory),freeze(occasionStoryV101),freeze(occasionStoryV100),freeze(releaseVisuals),freeze(releaseVisualsV101),freeze(releaseVisualsV100),freeze(photoReelComposer),freeze(photoReelComposerV101),freeze(photoReelComposerV100),freeze(importMediaV202),freeze(importMediaV201),freeze(editorV2212),freeze(editorV2211),freeze(editorV2210),freeze(editorV229),freeze(editorV228),freeze(panelStripV241),freeze(panelStripV240),freeze(panelStripV237),freeze(panelStripV236),freeze(boardEditorV2210),freeze(boardEditorV229),freeze(animatedSceneV238),freeze(animatedSceneV237),freeze(pipelineV225),freeze(pipelineV224),freeze(blueprintV219),freeze(blueprintV218),freeze(videoAssetsV214),freeze(videoAssetsV213),freeze(chatV203),freeze(chatV202),freeze(chatV201),freeze(shotPlanV221),freeze(shotPlanV220),freeze(shotPlanV213),freeze(shotPlanV212),freeze(voiceoverV216),freeze(voiceoverV215),freeze(voiceoverV214),freeze(voiceoverV213),freeze(scriptV215),freeze(scriptV214),freeze(scriptV213),freeze(roughCutV202),freeze(roughCutV201),freeze(finalEditV202),freeze(finalEditV201),freeze(editAssistantV202),freeze(editAssistantV201),freeze(gradeV202),freeze(gradeV201),freeze(transitionV202),freeze(transitionV201),freeze(qualityAuditV102),freeze(qualityAuditV101),freeze(compareV202),freeze(compareV201),freeze(compare),freeze(grade),freeze(transition),freeze(exportTimeline),freeze(captions),freeze(importMedia),freeze(generateImageV210),freeze(generateImage),freeze(thumbnailV201),freeze(thumbnail),freeze(writeSummary),freeze(automationWhen),freeze(automationNewItems),freeze(automationAiDecide),freeze(automationContinueIf),freeze(connectApp),freeze(automationContactFields),freeze(automationAiWrite),freeze(automationMetaCreative),freeze(voiceover),freeze(voiceoverV210),freeze(voiceoverV211),freeze(voiceoverV212),freeze(script),freeze(scriptV210),freeze(scriptV211),freeze(scriptV212),freeze(generateVideoV201),freeze(generateVideo),freeze(chat),freeze(characterV201),freeze(character),freeze(locationV201),freeze(location),freeze(styleV201),freeze(style),freeze(researchV202),freeze(researchV201),freeze(research),freeze(imageEditorV201),freeze(imageEditor),freeze(describeMediaV201),freeze(describeMedia),freeze(videoAssets),freeze(videoAssetsV210),freeze(videoAssetsV211),freeze(videoAssetsV212),freeze(roughCut),freeze(finalEdit),freeze(editAssistant),freeze(transcribeV201),freeze(transcribe),freeze(blueprint),freeze(blueprintV210),freeze(blueprintV211),freeze(blueprintV212),freeze(blueprintV213),freeze(blueprintV214),freeze(blueprintV215),freeze(blueprintV216),freeze(blueprintV217),freeze(shotPlan),freeze(shotPlanV210),freeze(shotPlanV211),freeze(boardEditor),freeze(boardEditorV210),freeze(boardEditorV220),freeze(boardEditorV221),freeze(boardEditorV222),freeze(boardEditorV223),freeze(boardEditorV224),freeze(boardEditorV225),freeze(boardEditorV226),freeze(boardEditorV227),freeze(boardEditorV228),freeze(panelStripV200),freeze(panelStrip),freeze(panelStripV220),freeze(panelStripV230),freeze(panelStripV231),freeze(panelStripV232),freeze(panelStripV233),freeze(panelStripV234),freeze(panelStripV235),freeze(animatedSceneV200),freeze(animatedScene),freeze(animatedSceneV220),freeze(animatedSceneV230),freeze(animatedSceneV231),freeze(animatedSceneV232),freeze(animatedSceneV233),freeze(animatedSceneV234),freeze(animatedSceneV235),freeze(animatedSceneV236),freeze(editor),freeze(editorV210),freeze(editorV220),freeze(editorV221),freeze(editorV222),freeze(editorV223),freeze(editorV224),freeze(editorV225),freeze(editorV226),freeze(editorV227),freeze(pipelineV200),freeze(pipeline),freeze(pipelineV220),freeze(pipelineV221),freeze(pipelineV223),freeze(pipelineV222),freeze(shotPlanAssistant),freeze(animatedSceneAssistant),freeze(blueprintAssistant),freeze(whiteboardAssistant),freeze(qualityAudit)]);
194
+ export const FIRST_PARTY_SDK_PACKAGES = Object.freeze([freeze(productAdVariants),freeze(pickPhotos),freeze(photoNotes),freeze(youtubePublication),freeze(stockSearch),freeze(exportVideoV210),freeze(exportVideo),freeze(exportAudio),freeze(motionGraphics),freeze(audioScore),freeze(shortFormFinish),freeze(screenRecorder),freeze(narration),freeze(shortFormFinishV102),freeze(shortFormFinishV101),freeze(shortFormFinishV100),freeze(occasionStory),freeze(occasionStoryV101),freeze(occasionStoryV100),freeze(releaseVisuals),freeze(releaseVisualsV101),freeze(releaseVisualsV100),freeze(photoReelComposer),freeze(photoReelComposerV101),freeze(photoReelComposerV100),freeze(importMediaV202),freeze(importMediaV201),freeze(editorV2212),freeze(editorV2211),freeze(editorV2210),freeze(editorV229),freeze(editorV228),freeze(panelStripV241),freeze(panelStripV240),freeze(panelStripV237),freeze(panelStripV236),freeze(boardEditorV2210),freeze(boardEditorV229),freeze(animatedSceneV238),freeze(animatedSceneV237),freeze(pipelineV225),freeze(pipelineV224),freeze(blueprintV219),freeze(blueprintV218),freeze(videoAssetsV214),freeze(videoAssetsV213),freeze(chatV203),freeze(chatV202),freeze(chatV201),freeze(shotPlanV221),freeze(shotPlanV220),freeze(shotPlanV213),freeze(shotPlanV212),freeze(voiceoverV216),freeze(voiceoverV215),freeze(voiceoverV214),freeze(voiceoverV213),freeze(scriptV215),freeze(scriptV214),freeze(scriptV213),freeze(roughCutV202),freeze(roughCutV201),freeze(finalEditV202),freeze(finalEditV201),freeze(editAssistantV202),freeze(editAssistantV201),freeze(gradeV202),freeze(gradeV201),freeze(transitionV202),freeze(transitionV201),freeze(qualityAuditV102),freeze(qualityAuditV101),freeze(compareV202),freeze(compareV201),freeze(compare),freeze(grade),freeze(transition),freeze(exportTimeline),freeze(captions),freeze(importMedia),freeze(generateImageV210),freeze(generateImage),freeze(thumbnailV201),freeze(thumbnail),freeze(writeSummary),freeze(automationWhen),freeze(automationNewItems),freeze(automationAiDecide),freeze(automationContinueIf),freeze(connectApp),freeze(automationContactFields),freeze(automationAiWrite),freeze(automationMetaCreative),freeze(voiceover),freeze(voiceoverV210),freeze(voiceoverV211),freeze(voiceoverV212),freeze(script),freeze(scriptV210),freeze(scriptV211),freeze(scriptV212),freeze(generateVideoV201),freeze(generateVideo),freeze(chat),freeze(characterV201),freeze(character),freeze(locationV201),freeze(location),freeze(styleV201),freeze(style),freeze(researchV202),freeze(researchV201),freeze(research),freeze(imageEditorV201),freeze(imageEditor),freeze(describeMediaV201),freeze(describeMedia),freeze(videoAssets),freeze(videoAssetsV210),freeze(videoAssetsV211),freeze(videoAssetsV212),freeze(roughCut),freeze(finalEdit),freeze(editAssistant),freeze(transcribeV201),freeze(transcribe),freeze(blueprint),freeze(blueprintV210),freeze(blueprintV211),freeze(blueprintV212),freeze(blueprintV213),freeze(blueprintV214),freeze(blueprintV215),freeze(blueprintV216),freeze(blueprintV217),freeze(shotPlan),freeze(shotPlanV210),freeze(shotPlanV211),freeze(boardEditor),freeze(boardEditorV210),freeze(boardEditorV220),freeze(boardEditorV221),freeze(boardEditorV222),freeze(boardEditorV223),freeze(boardEditorV224),freeze(boardEditorV225),freeze(boardEditorV226),freeze(boardEditorV227),freeze(boardEditorV228),freeze(panelStripV200),freeze(panelStrip),freeze(panelStripV220),freeze(panelStripV230),freeze(panelStripV231),freeze(panelStripV232),freeze(panelStripV233),freeze(panelStripV234),freeze(panelStripV235),freeze(animatedSceneV200),freeze(animatedScene),freeze(animatedSceneV220),freeze(animatedSceneV230),freeze(animatedSceneV231),freeze(animatedSceneV232),freeze(animatedSceneV233),freeze(animatedSceneV234),freeze(animatedSceneV235),freeze(animatedSceneV236),freeze(editor),freeze(editorV210),freeze(editorV220),freeze(editorV221),freeze(editorV222),freeze(editorV223),freeze(editorV224),freeze(editorV225),freeze(editorV226),freeze(editorV227),freeze(pipelineV200),freeze(pipeline),freeze(pipelineV220),freeze(pipelineV221),freeze(pipelineV223),freeze(pipelineV222),freeze(shotPlanAssistant),freeze(animatedSceneAssistant),freeze(blueprintAssistant),freeze(whiteboardAssistant),freeze(qualityAudit)]);
194
195
  export const firstPartySdkPackage = manifest => FIRST_PARTY_SDK_PACKAGES.find(pkg => pkg.manifest.id === manifest?.id && pkg.manifest.version === manifest?.version);
@@ -7,8 +7,20 @@ import {requiresDesktopExecution} from './desktop-execution.js';
7
7
 
8
8
  // Shared by external portable packaging and managed import. Repository evidence,
9
9
  // account permissions and current quarantine are additional host-side checks.
10
- export async function admitLocalPackage(pkg,{timeoutMs=15000}={}){
11
- assert(['recipe','javascript','capability','comfyui'].includes(pkg?.manifest?.runtime),'RUNTIME_RESTRICTED','Local admission supports recipes, isolated JavaScript, declarative capabilities and ComfyUI workflows. Hosted execution still requires account review.');
10
+ // A module Block's files (October 20, 2026): the folder's files when the caller has
11
+ // them, else the text a repo-shipped package carries beside its descriptor.
12
+ function moduleFilesOf(pkg,files){
13
+ if(files&&typeof files==='object'&&Object.keys(files).length)return files;
14
+ if(pkg?.moduleSource&&typeof pkg.moduleSource==='object')return Object.fromEntries(Object.entries(pkg.moduleSource).map(([path,text])=>[path,new TextEncoder().encode(String(text))]));
15
+ return null;
16
+ }
17
+ // Running a module here means running its code in a Node worker, which is no
18
+ // isolation boundary. Only a caller that owns the code asks for it: the SDK CLI
19
+ // on the creator's own machine (`moduleExecution:true`), never a server path
20
+ // admitting someone else's upload (imports, AI authoring, readiness from the API).
21
+ export async function admitLocalPackage(pkg,{timeoutMs=15000,files,moduleExecution=false}={}){
22
+ const moduleFiles=moduleExecution===true&&pkg?.manifest?.runtime==='module'?moduleFilesOf(pkg,files):null;
23
+ assert(['recipe','javascript','capability','comfyui'].includes(pkg?.manifest?.runtime)||moduleFiles,'RUNTIME_RESTRICTED',pkg?.manifest?.runtime==='module'?'Local admission runs a module Block only on your own machine (the SDK CLI); StillMade runs it in the browser sandbox after review.':'Local admission supports recipes, isolated JavaScript, declarative capabilities and ComfyUI workflows. Hosted execution still requires account review.');
12
24
  assert(Number.isFinite(timeoutMs)&&timeoutMs>0&&timeoutMs<=15000,'TIMEOUT','Local admission needs a positive budget of at most 15 seconds');
13
25
  const deadline=Date.now()+timeoutMs,controller=new AbortController();
14
26
  const remaining=()=>{const value=deadline-Date.now();assert(value>0&&!controller.signal.aborted,'TIMEOUT','Local admission exceeded its shared budget');return value;};
@@ -20,11 +32,16 @@ export async function admitLocalPackage(pkg,{timeoutMs=15000}={}){
20
32
  remaining();
21
33
  return {...scan,digest:sourceDigest,platformChecks,execution:'not-run',liveVerified:false,reviewRequired:true,tests:0,fixtureContracts:scan.passed?pkg.tests.length:0};
22
34
  }
35
+ // A module runs in the SDK's own Node executor with the stand-in host answering its
36
+ // hosted calls, media and files; the digests are checked before anything runs.
37
+ // Loaded only for a module package: the Node executor needs worker threads, which the
38
+ // App's bundle (this file is part of it) must never pull in statically.
39
+ const module=moduleFiles?await (async()=>{const [{createNodeModuleExecutor},{createMockHost}]=await Promise.all([import('./module-node.js'),import('./mock-host.js')]);return createNodeModuleExecutor({files:moduleFiles,host:createMockHost({manifest:pkg.manifest}).host});})():undefined;
23
40
  const report=await admitPackage(pkg,{
24
- test:content=>testPackage(content,{timeoutMs:remaining()}),
25
- run:(content,input,options)=>{remaining();return runPackageAsync(content,input,{...options,signal:controller.signal});},
41
+ test:content=>testPackage(content,{timeoutMs:remaining(),...(module?{module}:{})}),
42
+ run:(content,input,options)=>{remaining();return runPackageAsync(content,input,{...options,signal:controller.signal,...(module?{module}:{})});},
26
43
  });
27
44
  remaining();
28
- return report;
45
+ return module?{...report,execution:'local-stand-ins',liveVerified:false,reviewRequired:true}:report;
29
46
  }finally{clearTimeout(timer);}
30
47
  }
@@ -2,7 +2,7 @@ import {firstPartySdkPackage} from './first-party-packages.js';
2
2
  import {requiresDesktopExecution,validateDesktopExecution} from './desktop-execution.js';
3
3
  import {validatePortableMetadata} from './portable.js';
4
4
  import {initialBlockState,validateBlockState} from './state.js';
5
- import { assert, digest, safeDocument, stableStringify, validateManifest, validateValues } from './contracts.js';
5
+ import { assert, digest, plain, safeDocument, stableStringify, validateManifest, validateValues } from './contracts.js';
6
6
  import { validateRecipe } from './recipe.js';
7
7
  import { validateJavaScript } from './javascript.js';
8
8
  import { runPackageAsync } from './runtime.js';
@@ -14,7 +14,7 @@ import {validateModuleDescriptor,MODULE_LIMITS} from './module-runtime.js';
14
14
  export function validatePackage(pkg) {
15
15
  safeDocument(pkg);
16
16
  if(pkg?.manifest?.runtime==='native-application')return validateNativeApplicationPackage(pkg);
17
- assert(pkg && Object.keys(pkg).every(k => ['manifest', 'recipe', 'code', 'module', 'comfyui', 'capability', 'tests', 'view', 'portable', 'desktop'].includes(k)), 'PACKAGE', 'Unknown package field');
17
+ assert(pkg && Object.keys(pkg).every(k => ['manifest', 'recipe', 'code', 'module', 'moduleSource', 'comfyui', 'capability', 'tests', 'view', 'portable', 'desktop'].includes(k)), 'PACKAGE', 'Unknown package field');
18
18
  if(typeof pkg.manifest?.id==='string'&&pkg.manifest.id.startsWith('sm.'))assert(stableStringify(firstPartySdkPackage(pkg.manifest)??null)===stableStringify(pkg),'FIRST_PARTY_SOURCE','Use the exact published first-party package, or create a remix with a new identity.');
19
19
  const result = validateManifest(pkg.manifest);
20
20
  assert(result.valid, 'MANIFEST', JSON.stringify(result.errors));
@@ -32,8 +32,11 @@ export function validatePackage(pkg) {
32
32
  // Module files are named by digest; StillMade fetches and verifies the bytes before they run.
33
33
  assert(['code','recipe','comfyui','capability'].every(key=>!Object.hasOwn(pkg,key)),'PACKAGE','Module packages contain a module descriptor, not inline code, recipes, ComfyUI workflows or host requests');
34
34
  validateModuleDescriptor(pkg.module);
35
+ // A first-party package may carry its text files beside the descriptor so the app
36
+ // runs it without the content-addressed store; each is still checked against its digest.
37
+ if(Object.hasOwn(pkg,'moduleSource'))assert(plain(pkg.moduleSource)&&Object.entries(pkg.moduleSource).every(([path,text])=>typeof text==='string'&&Object.hasOwn(pkg.module.files,path)&&pkg.module.files[path].mediaType==='text/javascript'),'PACKAGE','moduleSource carries the text of files named in the module descriptor');
35
38
  }
36
- else if (pkg.manifest.runtime === 'javascript') { assert(!Object.hasOwn(pkg,'comfyui')&&!Object.hasOwn(pkg,'capability')&&!Object.hasOwn(pkg,'module'),'PACKAGE','Only matching runtimes can contain ComfyUI workflows, host requests or module files'); validateJavaScript(pkg); }
39
+ else if (pkg.manifest.runtime === 'javascript') { assert(!Object.hasOwn(pkg,'comfyui')&&!Object.hasOwn(pkg,'capability')&&!Object.hasOwn(pkg,'module')&&!Object.hasOwn(pkg,'moduleSource'),'PACKAGE','Only matching runtimes can contain ComfyUI workflows, host requests or module files'); validateJavaScript(pkg); }
37
40
  else { assert(!Object.hasOwn(pkg, 'code')&&!Object.hasOwn(pkg,'comfyui')&&!Object.hasOwn(pkg,'capability')&&!Object.hasOwn(pkg,'module'), 'PACKAGE', 'Recipe packages cannot contain code, ComfyUI workflows or host requests'); validateRecipe(pkg.recipe); }
38
41
  if(pkg.view !== undefined)validateView(pkg.view);
39
42
  assert(Array.isArray(pkg.tests) && pkg.tests.length > 0 && pkg.tests.length <= 50, 'TESTS_REQUIRED', 'Include 1–50 fixture tests');