stillmade-sdk 0.1.36 → 0.1.38

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.
Files changed (60) hide show
  1. package/AGENTS.md +8 -8
  2. package/IMAGE_DESCRIPTION.md +2 -0
  3. package/MODULES.md +68 -10
  4. package/NATIVE_MCP.md +18 -7
  5. package/PASTE_TO_LLM.md +121 -50
  6. package/PORTABLE.md +7 -3
  7. package/README.md +19 -6
  8. package/REMIX.md +6 -0
  9. package/SDK.md +226 -103
  10. package/TRANSCRIPTION.md +4 -2
  11. package/UI_AESTHETIC.md +2 -0
  12. package/api-index.json +1 -1
  13. package/blocks/narration/package.js +1 -1
  14. package/blocks/narration/versions/1.0.1/package.js +221 -0
  15. package/blocks/native/sm.animated-scene/versions/2.3.9/package.js +769 -0
  16. package/blocks/native/sm.board-editor/versions/2.2.11/package.js +551 -0
  17. package/blocks/native/sm.describe-media/versions/2.1.0/package.js +140 -0
  18. package/blocks/native/sm.panel-strip/versions/2.4.2/package.js +1316 -0
  19. package/blocks/native/sm.photo-notes/versions/1.1.0/package.js +170 -0
  20. package/blocks/native/sm.pipeline/versions/2.2.6/package.js +1095 -0
  21. package/blocks/screen-recorder/package.js +1 -1
  22. package/blocks/screen-recorder/versions/1.0.1/package.js +223 -0
  23. package/package.json +1 -1
  24. package/packages/block-cli/cli.js +91 -35
  25. package/packages/block-cli/dev-host/assets/host.js +35 -32
  26. package/packages/block-cli/dev-host/assets/index.css +1 -1
  27. package/packages/block-cli/dev-server.js +4 -0
  28. package/packages/block-cli/ready-gate.js +19 -4
  29. package/packages/block-cli/templates.js +33 -21
  30. package/packages/block-platform/block-readiness.js +10 -1
  31. package/packages/block-platform/catalog.js +14 -3
  32. package/packages/block-platform/chat-packages.js +6 -0
  33. package/packages/block-platform/flow-inspection.js +3 -1
  34. package/packages/block-platform/project-target.js +57 -0
  35. package/packages/block-platform/project-type-composer.js +4 -3
  36. package/packages/block-platform/project-types.js +5 -2
  37. package/packages/block-sdk/admission.js +4 -1
  38. package/packages/block-sdk/block-tier.js +23 -0
  39. package/packages/block-sdk/builder-contract.js +12 -10
  40. package/packages/block-sdk/capabilities.js +23 -10
  41. package/packages/block-sdk/collaboration-admission.js +3 -0
  42. package/packages/block-sdk/contracts.js +13 -1
  43. package/packages/block-sdk/example-media-data.js +16 -0
  44. package/packages/block-sdk/example-media.js +42 -0
  45. package/packages/block-sdk/first-party-identities.js +8 -0
  46. package/packages/block-sdk/first-party-packages.js +11 -1
  47. package/packages/block-sdk/index.d.ts +6 -2
  48. package/packages/block-sdk/local-admission.js +6 -5
  49. package/packages/block-sdk/media-transform.js +59 -3
  50. package/packages/block-sdk/mock-host.js +23 -2
  51. package/packages/block-sdk/mocks.js +4 -1
  52. package/packages/block-sdk/module-hosted.js +12 -2
  53. package/packages/block-sdk/port-types.d.ts +6 -0
  54. package/packages/block-sdk/port-types.js +32 -10
  55. package/packages/block-sdk/shared-agent-controls.js +25 -2
  56. package/packages/block-sdk/shared-controls.js +17 -3
  57. package/packages/block-sdk/testing.js +46 -3
  58. package/packages/block-sdk/view-design.js +67 -0
  59. package/packages/block-sdk/view-renderer.js +50 -8
  60. package/packages/design-system/block-kit.css +6 -4
@@ -0,0 +1,42 @@
1
+ // Example files a Block's tests and examples can name by address, /api/media/example/<name>: StillMade serves them
2
+ // (server/routes/media.js), the phone and browser check serves them inside its disposable preview, and the SDK's local
3
+ // tests read them when a test gives no mock for them (mock-host.js). November 2026: the guides used these addresses in
4
+ // fixtures but nothing served them, so an image Block's example never opened in the preview or the device check, and a
5
+ // Block built outside StillMade could not ship a real example photo. The bytes are in example-media-data.js
6
+ // (scripts/generate-example-media.mjs), loaded only when one is read.
7
+ export const EXAMPLE_MEDIA_PATH='/api/media/example/';
8
+ export const EXAMPLE_MEDIA=Object.freeze({
9
+ 'photo-1.jpg':{kind:'image',mimeType:'image/jpeg',width:1600,height:1200},
10
+ 'photo-2.jpg':{kind:'image',mimeType:'image/jpeg',width:1200,height:1600},
11
+ 'photo-3.jpg':{kind:'image',mimeType:'image/jpeg',width:1600,height:1200},
12
+ 'photo-4.jpg':{kind:'image',mimeType:'image/jpeg',width:1600,height:1200},
13
+ 'sheet.png':{kind:'image',mimeType:'image/png',width:1300,height:1440},
14
+ 'room.png':{kind:'image',mimeType:'image/png',width:1200,height:900},
15
+ 'kitchen.wav':{kind:'audio',mimeType:'audio/wav',duration:2,sampleRate:24000,channels:1},
16
+ 'hall.wav':{kind:'audio',mimeType:'audio/wav',duration:2,sampleRate:24000,channels:1},
17
+ 'click-120.wav':{kind:'audio',mimeType:'audio/wav',duration:2,sampleRate:24000,channels:1},
18
+ 'audio.mp3':{kind:'audio',mimeType:'audio/mpeg',duration:2},
19
+ 'clip.mp4':{kind:'video',mimeType:'video/mp4',width:640,height:360,duration:2},
20
+ 'notes.txt':{kind:'file',mimeType:'text/plain'},
21
+ 'sales.csv':{kind:'file',mimeType:'text/csv'},
22
+ });
23
+
24
+ // The example's name for an address StillMade serves ("/api/media/example/photo-1.jpg", with or without an origin), else null.
25
+ export function exampleMediaName(url){
26
+ if(typeof url!=='string')return null;
27
+ let path=url;
28
+ if(/^https?:\/\//i.test(url)){try{path=new URL(url).pathname;}catch{return null;}}
29
+ if(!path.startsWith(EXAMPLE_MEDIA_PATH))return null;
30
+ const name=decodeURIComponent(path.slice(EXAMPLE_MEDIA_PATH.length));
31
+ return Object.hasOwn(EXAMPLE_MEDIA,name)?name:null;
32
+ }
33
+
34
+ let data=null;
35
+ // The example's bytes.
36
+ export async function readExampleMedia(name){
37
+ if(!Object.hasOwn(EXAMPLE_MEDIA,name))throw Object.assign(new Error(`There is no example file named ${name}.`),{code:'EXAMPLE_MEDIA_UNKNOWN'});
38
+ data||=import('./example-media-data.js').then(module=>module.EXAMPLE_MEDIA_DATA);
39
+ const text=(await data)[name];
40
+ if(typeof Buffer!=='undefined')return new Uint8Array(Buffer.from(text,'base64'));
41
+ const binary=atob(text),bytes=new Uint8Array(binary.length);for(let index=0;index<binary.length;index++)bytes[index]=binary.charCodeAt(index);return bytes;
42
+ }
@@ -17,6 +17,7 @@ export const FIRST_PARTY_SDK_IDENTITIES=Object.freeze(new Set([
17
17
  "sm.animated-scene@2.3.6",
18
18
  "sm.animated-scene@2.3.7",
19
19
  "sm.animated-scene@2.3.8",
20
+ "sm.animated-scene@2.3.9",
20
21
  "sm.blueprint-assistant@1.0.0",
21
22
  "sm.blueprint@2.0.0",
22
23
  "sm.blueprint@2.1.0",
@@ -34,6 +35,7 @@ export const FIRST_PARTY_SDK_IDENTITIES=Object.freeze(new Set([
34
35
  "sm.board-editor@2.2.0",
35
36
  "sm.board-editor@2.2.1",
36
37
  "sm.board-editor@2.2.10",
38
+ "sm.board-editor@2.2.11",
37
39
  "sm.board-editor@2.2.2",
38
40
  "sm.board-editor@2.2.3",
39
41
  "sm.board-editor@2.2.4",
@@ -57,6 +59,7 @@ export const FIRST_PARTY_SDK_IDENTITIES=Object.freeze(new Set([
57
59
  "sm.continue-if@1.0.0",
58
60
  "sm.describe-media@2.0.0",
59
61
  "sm.describe-media@2.0.1",
62
+ "sm.describe-media@2.1.0",
60
63
  "sm.edit-assistant@2.0.0",
61
64
  "sm.edit-assistant@2.0.1",
62
65
  "sm.edit-assistant@2.0.2",
@@ -112,7 +115,9 @@ export const FIRST_PARTY_SDK_IDENTITIES=Object.freeze(new Set([
112
115
  "sm.panel-strip@2.3.7",
113
116
  "sm.panel-strip@2.4.0",
114
117
  "sm.panel-strip@2.4.1",
118
+ "sm.panel-strip@2.4.2",
115
119
  "sm.photo-notes@1.0.0",
120
+ "sm.photo-notes@1.1.0",
116
121
  "sm.pick-photos@1.0.0",
117
122
  "sm.pipeline@2.0.0",
118
123
  "sm.pipeline@2.1.0",
@@ -122,6 +127,7 @@ export const FIRST_PARTY_SDK_IDENTITIES=Object.freeze(new Set([
122
127
  "sm.pipeline@2.2.3",
123
128
  "sm.pipeline@2.2.4",
124
129
  "sm.pipeline@2.2.5",
130
+ "sm.pipeline@2.2.6",
125
131
  "sm.quality-audit@1.0.0",
126
132
  "sm.quality-audit@1.0.1",
127
133
  "sm.quality-audit@1.0.2",
@@ -177,6 +183,7 @@ export const FIRST_PARTY_SDK_IDENTITIES=Object.freeze(new Set([
177
183
  "sm.write-summary@1.0.0",
178
184
  "sm.youtube@2.0.0",
179
185
  "stillmade.narration@1.0.0",
186
+ "stillmade.narration@1.0.1",
180
187
  "stillmade.occasion-story@1.0.0",
181
188
  "stillmade.occasion-story@1.0.1",
182
189
  "stillmade.occasion-story@1.0.2",
@@ -188,6 +195,7 @@ export const FIRST_PARTY_SDK_IDENTITIES=Object.freeze(new Set([
188
195
  "stillmade.release-visuals@1.0.1",
189
196
  "stillmade.release-visuals@1.0.2",
190
197
  "stillmade.screen-recorder@1.0.0",
198
+ "stillmade.screen-recorder@1.0.1",
191
199
  "stillmade.short-form-finish@1.0.0",
192
200
  "stillmade.short-form-finish@1.0.1",
193
201
  "stillmade.short-form-finish@1.0.2",
@@ -2,7 +2,9 @@ import shortFormFinishV100 from '../../blocks/short-form-finish/versions/1.0.0/p
2
2
  import shortFormFinishV101 from '../../blocks/short-form-finish/versions/1.0.1/package.js';
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
+ import screenRecorderV100 from '../../blocks/screen-recorder/versions/1.0.0/package.js';
5
6
  import screenRecorder from '../../blocks/screen-recorder/package.js';
7
+ import narrationV100 from '../../blocks/narration/versions/1.0.0/package.js';
6
8
  import narration from '../../blocks/narration/package.js';
7
9
  import occasionStoryV100 from '../../blocks/occasion-story/versions/1.0.0/package.js';
8
10
  import releaseVisualsV100 from '../../blocks/release-visuals/versions/1.0.0/package.js';
@@ -88,6 +90,9 @@ import imageEditor from '../../blocks/native/sm.image-editor/versions/2.0.0/pack
88
90
  import imageEditorV201 from '../../blocks/native/sm.image-editor/versions/2.0.1/package.js';
89
91
  import describeMedia from '../../blocks/native/sm.describe-media/versions/2.0.0/package.js';
90
92
  import describeMediaV201 from '../../blocks/native/sm.describe-media/versions/2.0.1/package.js';
93
+ // October 29, 2026: StillMade credits stopped paying for image.describe on September 13, so Describe media 2.1.0
94
+ // and Photo notes 1.1.0 run on media.analyze (credits, per image); the image.describe releases stay for exact pins.
95
+ import describeMediaV210 from '../../blocks/native/sm.describe-media/versions/2.1.0/package.js';
91
96
  import videoAssets from '../../blocks/native/sm.video-assets/versions/2.0.0/package.js';
92
97
  import videoAssetsV210 from '../../blocks/native/sm.video-assets/versions/2.1.0/package.js';
93
98
  import videoAssetsV211 from '../../blocks/native/sm.video-assets/versions/2.1.1/package.js';
@@ -135,6 +140,7 @@ import pipelineV220 from '../../blocks/native/sm.pipeline/versions/2.2.0/package
135
140
  import pipelineV221 from '../../blocks/native/sm.pipeline/versions/2.2.1/package.js';
136
141
  import pipelineV223 from '../../blocks/native/sm.pipeline/versions/2.2.3/package.js';
137
142
  import pipelineV225 from '../../blocks/native/sm.pipeline/versions/2.2.5/package.js';
143
+ import pipelineV226 from '../../blocks/native/sm.pipeline/versions/2.2.6/package.js';
138
144
  import pipelineV224 from '../../blocks/native/sm.pipeline/versions/2.2.4/package.js';
139
145
  import pipelineV222 from '../../blocks/native/sm.pipeline/versions/2.2.2/package.js';
140
146
  import shotPlanAssistant from '../../blocks/native/sm.shot-plan-assistant/versions/1.0.0/package.js';
@@ -163,6 +169,7 @@ import panelStripV236 from '../../blocks/native/sm.panel-strip/versions/2.3.6/pa
163
169
  import panelStripV237 from '../../blocks/native/sm.panel-strip/versions/2.3.7/package.js';
164
170
  import panelStripV240 from '../../blocks/native/sm.panel-strip/versions/2.4.0/package.js';
165
171
  import panelStripV241 from '../../blocks/native/sm.panel-strip/versions/2.4.1/package.js';
172
+ import panelStripV242 from '../../blocks/native/sm.panel-strip/versions/2.4.2/package.js';
166
173
  import boardEditorV210 from '../../blocks/native/sm.board-editor/versions/2.1.0/package.js';
167
174
  import boardEditorV220 from '../../blocks/native/sm.board-editor/versions/2.2.0/package.js';
168
175
  import boardEditorV221 from '../../blocks/native/sm.board-editor/versions/2.2.1/package.js';
@@ -175,6 +182,7 @@ import boardEditorV227 from '../../blocks/native/sm.board-editor/versions/2.2.7/
175
182
  import boardEditorV228 from '../../blocks/native/sm.board-editor/versions/2.2.8/package.js';
176
183
  import boardEditorV229 from '../../blocks/native/sm.board-editor/versions/2.2.9/package.js';
177
184
  import boardEditorV2210 from '../../blocks/native/sm.board-editor/versions/2.2.10/package.js';
185
+ import boardEditorV2211 from '../../blocks/native/sm.board-editor/versions/2.2.11/package.js';
178
186
  import animatedSceneV220 from '../../blocks/native/sm.animated-scene/versions/2.2.0/package.js';
179
187
  import animatedSceneV230 from '../../blocks/native/sm.animated-scene/versions/2.3.0/package.js';
180
188
  import animatedSceneV231 from '../../blocks/native/sm.animated-scene/versions/2.3.1/package.js';
@@ -185,12 +193,14 @@ import animatedSceneV235 from '../../blocks/native/sm.animated-scene/versions/2.
185
193
  import animatedSceneV236 from '../../blocks/native/sm.animated-scene/versions/2.3.6/package.js';
186
194
  import animatedSceneV237 from '../../blocks/native/sm.animated-scene/versions/2.3.7/package.js';
187
195
  import animatedSceneV238 from '../../blocks/native/sm.animated-scene/versions/2.3.8/package.js';
196
+ import animatedSceneV239 from '../../blocks/native/sm.animated-scene/versions/2.3.9/package.js';
188
197
  import blueprintAssistant from '../../blocks/native/sm.blueprint-assistant/versions/1.0.0/package.js';
189
198
  import photoNotes from '../../blocks/native/sm.photo-notes/versions/1.0.0/package.js';
199
+ import photoNotesV110 from '../../blocks/native/sm.photo-notes/versions/1.1.0/package.js';
190
200
  import pickPhotos from '../../blocks/native/sm.pick-photos/versions/1.0.0/package.js';
191
201
 
192
202
  // Exact, independently executable first-party releases. Host workspaces do not
193
203
  // belong here. Changing implementation requires a new immutable version.
194
204
  const freeze=value=>{if(value&&typeof value==='object'&&!Object.isFrozen(value)){Object.values(value).forEach(freeze);Object.freeze(value);}return value;};
195
- 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(automationV110),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)]);
205
+ export const FIRST_PARTY_SDK_PACKAGES = Object.freeze([freeze(productAdVariants),freeze(pickPhotos),freeze(photoNotesV110),freeze(photoNotes),freeze(youtubePublication),freeze(stockSearch),freeze(exportVideoV210),freeze(exportVideo),freeze(exportAudio),freeze(motionGraphics),freeze(audioScore),freeze(shortFormFinish),freeze(screenRecorder),freeze(screenRecorderV100),freeze(narration),freeze(narrationV100),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(panelStripV242),freeze(panelStripV241),freeze(panelStripV240),freeze(panelStripV237),freeze(panelStripV236),freeze(boardEditorV2211),freeze(boardEditorV2210),freeze(boardEditorV229),freeze(animatedSceneV239),freeze(animatedSceneV238),freeze(animatedSceneV237),freeze(pipelineV226),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(automationV110),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(describeMediaV210),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)]);
196
206
  export const firstPartySdkPackage = manifest => FIRST_PARTY_SDK_PACKAGES.find(pkg => pkg.manifest.id === manifest?.id && pkg.manifest.version === manifest?.version);
@@ -49,6 +49,8 @@ export function testPackage(pkg: BlockPackage, options?: { timeoutMs?: number; c
49
49
  export const SDK_VERSION: '0.1.0';
50
50
  export const ROOT_CHAT_ID: 'stillmade.starting-chat';
51
51
  export function stableStringify(value: unknown): string;
52
+ /** Throws when any text or field name in a package holds the NUL character (\u0000), which StillMade cannot store; the message names where it is (tests.1.mocks.files.tiny-1.text). Import review, validate, test, pack and ready run it. */
53
+ export function assertStorableText(value: unknown): void;
52
54
  export function validateManifest(value: unknown): { valid: boolean; errors: { path: string; message: string }[] };
53
55
  export function validateRecipe(recipe: BlockPackage['recipe']): true;
54
56
  export interface RecipeProgress {stepId:string;complete:number;total:number}
@@ -675,9 +677,11 @@ export type MediaTransformRequest =
675
677
  | {operation:'video.cut'; source:SavedMediaRef; keep:TimeRange[]; settings?:RenderSettings}
676
678
  | {operation:'video.concat'; sources:SavedMediaRef[]; settings?:RenderSettings}
677
679
  | {operation:'video.resize'; source:SavedMediaRef; settings:RenderSettings}
678
- | {operation:'image.thumbnail'; source:SavedMediaRef; at?:number; width?:number};
680
+ | {operation:'image.thumbnail'; source:SavedMediaRef; at?:number; width?:number}
681
+ | {operation:'document.text'; source:SavedMediaRef}
682
+ | {operation:'image.compose'; width:number; height:number; background?:string; format?:'png'|'jpeg'; layers:{source:SavedMediaRef; x:number; y:number; width:number; height:number; fit?:'cover'|'contain'}[]; text?:{text:string; x:number; y:number; size?:number; color?:string; background?:string; align?:'left'|'center'|'right'; weight?:'normal'|'bold'; width?:number}[]};
679
683
  export type DecodedAudio = {sampleRate:number; duration:number; samples:Float32Array};
680
- export const MEDIA_TRANSFORM_LIMITS: Readonly<{ranges:500;sources:50;durationSeconds:21600;decodeSamples:201326592;sampleRates:readonly number[];thumbnailWidth:4096}>;
684
+ export const MEDIA_TRANSFORM_LIMITS: Readonly<{ranges:500;sources:50;durationSeconds:21600;decodeSamples:201326592;sampleRates:readonly number[];thumbnailWidth:4096;composeSide:4096;composeLayers:64;composeText:64;documentPages:500;documentCharacters:4000000}>;
681
685
  export const MEDIA_TRANSFORM_OPERATIONS: readonly MediaTransformRequest['operation'][];
682
686
  export function mediaTransformRequest(value: MediaTransformRequest): Required<MediaTransformRequest>;
683
687
  export function mediaTransformSources(request: MediaTransformRequest): SavedMediaRef[];
@@ -19,8 +19,9 @@ function moduleFilesOf(pkg,files){
19
19
  // on the creator's own machine (`moduleExecution:true`), never a server path
20
20
  // admitting someone else's upload (imports, AI authoring, readiness from the API).
21
21
  // host: the stand-in host a module runs against (the CLI's, which answers from tests/mocks.json);
22
- // without one, the recorded stand-ins.
23
- export async function admitLocalPackage(pkg,{timeoutMs=15000,files,moduleExecution=false,host}={}){
22
+ // without one, the recorded stand-ins. hostFor(mocks): the host for a fixture's own mocks (the CLI's
23
+ // reads the files they name from the Block folder); without one, a stand-in host answering from them.
24
+ export async function admitLocalPackage(pkg,{timeoutMs=15000,files,moduleExecution=false,host,hostFor}={}){
24
25
  const moduleFiles=moduleExecution===true&&pkg?.manifest?.runtime==='module'?moduleFilesOf(pkg,files):null;
25
26
  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.');
26
27
  assert(Number.isFinite(timeoutMs)&&timeoutMs>0&&timeoutMs<=15000,'TIMEOUT','Local admission needs a positive budget of at most 15 seconds');
@@ -38,12 +39,12 @@ export async function admitLocalPackage(pkg,{timeoutMs=15000,files,moduleExecuti
38
39
  // hosted calls, media and files; the digests are checked before anything runs.
39
40
  // Loaded only for a module package: the Node executor needs worker threads, which the
40
41
  // App's bundle (this file is part of it) must never pull in statically.
41
- // A fixture that records its own mocks is answered from them.
42
- const executors=moduleFiles?await (async()=>{const [{createNodeModuleExecutor},{createMockHost}]=await Promise.all([import('./module-node.js'),import('./mock-host.js')]);const module=createNodeModuleExecutor({files:moduleFiles,host:host||createMockHost({manifest:pkg.manifest}).host});return {module,moduleFor:fixture=>fixture?.mocks?createNodeModuleExecutor({files:moduleFiles,host:createMockHost({mocks:fixture.mocks,manifest:pkg.manifest}).host}):module};})():null;
42
+ // A fixture that records its own mocks is answered from them, and so is the sample taken from it.
43
+ const executors=moduleFiles?await (async()=>{const [{createNodeModuleExecutor},{createMockHost}]=await Promise.all([import('./module-node.js'),import('./mock-host.js')]);const module=createNodeModuleExecutor({files:moduleFiles,host:host||createMockHost({manifest:pkg.manifest}).host});return {module,moduleFor:fixture=>fixture?.mocks?createNodeModuleExecutor({files:moduleFiles,host:hostFor?hostFor(fixture.mocks):createMockHost({mocks:fixture.mocks,manifest:pkg.manifest}).host}):module};})():null;
43
44
  const module=executors?.module;
44
45
  const report=await admitPackage(pkg,{
45
46
  test:content=>testPackage(content,{timeoutMs:remaining(),...(executors||{})}),
46
- run:(content,input,options)=>{remaining();return runPackageAsync(content,input,{...options,signal:controller.signal,...(module?{module}:{})});},
47
+ run:(content,input,options,fixture)=>{remaining();return runPackageAsync(content,input,{...options,signal:controller.signal,...(module?{module:executors.moduleFor(fixture)}:{})});},
47
48
  });
48
49
  remaining();
49
50
  return module?{...report,execution:'local-stand-ins',liveVerified:false,reviewRequired:true}:report;
@@ -8,8 +8,8 @@ import {FINAL_RENDER_LIMITS} from './portable-media-contracts.js';
8
8
 
9
9
  // decodeSamples bounds decoded PCM (all channels): 192 M samples is 768 MB of
10
10
  // floats, about 100 minutes of 16 kHz stereo or 33 minutes of 48 kHz stereo.
11
- export const MEDIA_TRANSFORM_LIMITS=Object.freeze({ranges:500,sources:50,durationSeconds:21600,decodeSamples:201326592,sampleRates:[8000,16000,22050,24000,32000,44100,48000],thumbnailWidth:4096});
12
- export const MEDIA_TRANSFORM_OPERATIONS=Object.freeze(['audio.decode','audio.extract','audio.normalize','video.cut','video.concat','video.resize','image.thumbnail','timeline.render']);
11
+ export const MEDIA_TRANSFORM_LIMITS=Object.freeze({ranges:500,sources:50,durationSeconds:21600,decodeSamples:201326592,sampleRates:[8000,16000,22050,24000,32000,44100,48000],thumbnailWidth:4096,composeSide:4096,composeLayers:64,composeText:64,documentPages:500,documentCharacters:4000000});
12
+ export const MEDIA_TRANSFORM_OPERATIONS=Object.freeze(['audio.decode','audio.extract','audio.normalize','video.cut','video.concat','video.resize','image.thumbnail','image.compose','document.text','timeline.render']);
13
13
  // timeline.render (October 22, 2026, owner: exporting, captions, transitions and color are things a Block
14
14
  // does, not Blocks): the Editor's own looks, transitions and caption animations, by name.
15
15
  export const TIMELINE_LOOKS=Object.freeze(['None','Cold','Warm','Desaturated','High Contrast','Filmic','Teal and Orange']);
@@ -57,6 +57,13 @@ export function mediaTransformRequest(value){
57
57
  return {operation:value.operation,source:value.source,settings:{fps:30,resolution:1080,quality:'high',...value.settings}};
58
58
  }
59
59
  case 'timeline.render':return timelineRender(value);
60
+ case 'image.compose':return imageCompose(value);
61
+ // document.text (October 28, 2026): the words of a PDF or text file the person gives, page by page, read on the device
62
+ // (a teacher's lesson slides PDF had no way into a Block: no hosted operation reads PDFs).
63
+ case 'document.text':{
64
+ if(!exact(value,['operation','source'])||!ref(value.source,['file','document']))fail('document.text takes a saved file');
65
+ return {operation:value.operation,source:value.source};
66
+ }
60
67
  case 'image.thumbnail':{
61
68
  if(!exact(value,['operation','source'],['at','width'])||!ref(value.source,['video']))fail('image.thumbnail takes a saved video, an optional time and width');
62
69
  const at=value.at??0,width=value.width??1280;
@@ -66,7 +73,7 @@ export function mediaTransformRequest(value){
66
73
  }
67
74
  }
68
75
  /** The saved references a request reads. */
69
- export const mediaTransformSources=request=>request.operation==='video.concat'?request.sources:request.operation==='timeline.render'?[...request.clips,...request.audio].map(item=>item.source):[request.source];
76
+ export const mediaTransformSources=request=>request.operation==='video.concat'?request.sources:request.operation==='image.compose'?request.layers.map(layer=>layer.source):request.operation==='timeline.render'?[...request.clips,...request.audio].map(item=>item.source):[request.source];
70
77
 
71
78
  /** Silent stretches in mono PCM: [{start, end}] seconds below thresholdDb for at least minSilence. */
72
79
  export function silentRanges(samples,sampleRate,{thresholdDb=-40,minSilence=0.5,window=0.02}={}){
@@ -210,6 +217,13 @@ function timelineRender(value){
210
217
  if(clip.look!==undefined&&!TIMELINE_LOOKS.includes(clip.look))fail(`look is one of ${TIMELINE_LOOKS.join(', ')}`);
211
218
  return {source:clip.source,start:clip.start,duration:clip.duration,from:clip.from??0,...(clip.transition?{transition:{type:clip.transition.type,duration:clip.transition.duration??0.5}}:{}),...(clip.look&&clip.look!=='None'?{look:clip.look}:{}),...(clip.mute?{mute:true}:{})};
212
219
  });
220
+ // One track: each clip starts where the one before it ends, overlapping it only by a transition (October 28, 2026:
221
+ // a built clip joiner put every clip at start 0, so the last covered the first and the rest of the video was black; its
222
+ // tests mock the render, so only this check could tell it). Either clip's fade allows that overlap, at most 0.9 of
223
+ // each clip as the Editor clamps a transition; the host renders the overlap as a crossfade (mediaTransformHost.js)
224
+ // (October 29, 2026: a reel that faded out of each photo failed its test, and stacked clips with long fades passed).
225
+ const ordered=[...clips].sort((a,b)=>a.start-b.start),fade=clip=>clip.transition&&clip.transition.type!=='Cut'?clip.transition.duration:0;
226
+ for(let index=1;index<ordered.length;index++){const before=ordered[index-1],clip=ordered[index],end=before.start+before.duration;if(end-clip.start>Math.min(Math.max(fade(before),fade(clip)),0.9*before.duration,0.9*clip.duration)+0.05)fail(`Clips overlap: one starts at ${clip.start} s while the clip before it runs to ${end} s. start is the clip's place on the timeline, so each clip starts where the one before it ends (less the transition between them); from is where it begins in its own file`);}
213
227
  const sounds=audio.map(item=>{
214
228
  if(!exact(item,['source','start','duration'],['from','volume'])||!ref(item.source,['audio','video'])||!span(item)||item.from!==undefined&&!seconds(item.from)||item.volume!==undefined&&!(Number.isFinite(item.volume)&&item.volume>=0&&item.volume<=200))fail('An audio item is {source: a saved audio or video, start, duration, from?, volume? 0 to 200}');
215
229
  return {source:item.source,start:item.start,duration:item.duration,from:item.from??0,volume:item.volume??100};
@@ -224,3 +238,45 @@ function timelineRender(value){
224
238
  });
225
239
  return {operation:'timeline.render',kind,aspect,clips,audio:sounds,captions:lines,settings:{fps:30,resolution:1080,quality:'high',...(value.settings||{})}};
226
240
  }
241
+ // image.compose (October 28, 2026): a Block joins saved images into one picture (side by side, a grid, a
242
+ // contact sheet, a poster with a title). Layers and text are placed in pixels on the canvas, drawn in order.
243
+ const color=value=>/^#[0-9a-fA-F]{6}$/.test(value);
244
+ // A text colour or the box behind it: #rrggbb, #rrggbbaa (see-through) or rgb()/rgba(), kept as hex so every host draws
245
+ // the same (October 29, 2026: a reel's title box was rgba(0,0,0,0.35) and the Block failed its own test).
246
+ const textColor=value=>{
247
+ if(typeof value!=='string')return null;
248
+ if(/^#[0-9a-fA-F]{6}(?:[0-9a-fA-F]{2})?$/.test(value))return value;
249
+ const match=/^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(0|1|0?\.\d+|1\.0+)\s*)?\)$/.exec(value);
250
+ if(!match||match.slice(1,4).some(channel=>Number(channel)>255))return null;
251
+ const hex=number=>number.toString(16).padStart(2,'0'),alpha=match[4]===undefined?255:Math.round(Number(match[4])*255);
252
+ return '#'+match.slice(1,4).map(channel=>hex(Number(channel))).join('')+(alpha===255?'':hex(alpha));
253
+ };
254
+ const place=item=>['x','y','width','height'].every(key=>Number.isFinite(item[key]))&&item.width>0&&item.height>0&&Math.abs(item.x)<=MEDIA_TRANSFORM_LIMITS.composeSide&&Math.abs(item.y)<=MEDIA_TRANSFORM_LIMITS.composeSide&&item.width<=2*MEDIA_TRANSFORM_LIMITS.composeSide&&item.height<=2*MEDIA_TRANSFORM_LIMITS.composeSide;
255
+ function imageCompose(value){
256
+ const side=MEDIA_TRANSFORM_LIMITS.composeSide;
257
+ if(!exact(value,['operation','width','height','layers'],['background','format','text']))fail('image.compose takes width, height, layers and optional background, format and text');
258
+ if(![value.width,value.height].every(size=>Number.isInteger(size)&&size>=16&&size<=side))fail(`width and height are whole pixels from 16 to ${side}`);
259
+ const background=value.background??'#ffffff',format=value.format??'png',text=value.text??[];
260
+ if(!color(background))fail('background is a #rrggbb colour');
261
+ if(!['png','jpeg'].includes(format))fail('format is png or jpeg');
262
+ if(!Array.isArray(value.layers)||!value.layers.length||value.layers.length>MEDIA_TRANSFORM_LIMITS.composeLayers)fail(`Place 1 to ${MEDIA_TRANSFORM_LIMITS.composeLayers} images`);
263
+ if(!Array.isArray(text)||text.length>MEDIA_TRANSFORM_LIMITS.composeText)fail(`Add at most ${MEDIA_TRANSFORM_LIMITS.composeText} pieces of text`);
264
+ const layers=value.layers.map(layer=>{
265
+ if(!exact(layer,['source','x','y','width','height'],['fit'])||!ref(layer.source,['image'])||!place(layer))fail('A layer is {source: a saved image, x, y, width, height, fit?} in pixels on the canvas');
266
+ if(layer.fit!==undefined&&!['cover','contain'].includes(layer.fit))fail('fit is cover or contain');
267
+ return {source:layer.source,x:layer.x,y:layer.y,width:layer.width,height:layer.height,fit:layer.fit??'cover'};
268
+ });
269
+ const lines=text.map(line=>{
270
+ if(!exact(line,['text','x','y'],['size','color','background','align','weight','width'])||typeof line.text!=='string'||!line.text.trim()||line.text.length>200||![line.x,line.y].every(Number.isFinite))fail('A piece of text is {text up to 200 characters, x, y, size?, color?, background?, align?, weight?, width?}');
271
+ // width wraps the words onto lines no wider than it (October 29, 2026: a storyboard's long caption ran into the next frame).
272
+ if(line.width!==undefined&&!(Number.isFinite(line.width)&&line.width>=16&&line.width<=MEDIA_TRANSFORM_LIMITS.composeSide))fail(`width is 16 to ${MEDIA_TRANSFORM_LIMITS.composeSide} pixels: the longest a line of the text may run before it wraps`);
273
+ const size=line.size??48;
274
+ if(!(Number.isFinite(size)&&size>=8&&size<=400))fail('size is 8 to 400 pixels');
275
+ const ink=line.color===undefined?'#000000':textColor(line.color),box=line.background===undefined?undefined:textColor(line.background);
276
+ if(!ink||box===null)fail('color and background are #rrggbb, #rrggbbaa (see-through) or rgb()/rgba() colours');
277
+ if(line.align!==undefined&&!['left','center','right'].includes(line.align))fail('align is left, center or right');
278
+ if(line.weight!==undefined&&!['normal','bold'].includes(line.weight))fail('weight is normal or bold');
279
+ return {text:line.text,x:line.x,y:line.y,size,color:ink,align:line.align??'left',weight:line.weight??'bold',...(box?{background:box}:{}),...(line.width!==undefined?{width:line.width}:{})};
280
+ });
281
+ return {operation:'image.compose',width:value.width,height:value.height,background,format,layers,text:lines};
282
+ }
@@ -5,11 +5,13 @@
5
5
  // returns, checked by the same validators the app uses, unless the Block's
6
6
  // tests/mocks.json records its own. Nothing here contacts a provider or
7
7
  // spends credits, and a pass with stand-ins is never a live verification.
8
+ import {EXAMPLE_MEDIA_PATH,exampleMediaName,readExampleMedia} from './example-media.js';
8
9
  import {assert,plain} from './contracts.js';
9
10
  import {moduleHostedPackage} from './module-hosted.js';
10
11
  import {prepareCapabilityInvocation,validateCapabilityResult,capabilityOutputs,defaultCapabilityExpectations,AUDIO_CAPABILITY_LIMITS} from './capabilities.js';
11
12
  import {createMemoryStorage,storageKey} from './cloud-storage.js';
12
13
  import {mediaTransformRequest,decodeWav} from './media-transform.js';
14
+ import {FILE_FORMATS} from './data-values.js';
13
15
  import {mockPng,mockWav,mockMp4,MOCK_IMAGE_SIZE,MOCK_VIDEO_SIZE} from './mock-media.js';
14
16
  export {mockPng,mockWav,mockMp4} from './mock-media.js';
15
17
 
@@ -119,15 +121,32 @@ export function createMockHost({mocks:given={},readLocalFile,manifest}={}){
119
121
  const url=typeof ref?.url==='string'?ref.url:'';
120
122
  if(media.has(url))return media.get(url).bytes;
121
123
  const named=[ref?.assetId,url].find(key=>key&&recorded('files',key));
124
+ // {base64} carries a small binary file inside the package, so it travels with the pack and the import review (November
125
+ // 2026: a test photo given as {text} carried NUL characters, which no project could save).
126
+ if(named&&plain(mocks.files[named])&&typeof mocks.files[named].base64==='string'){
127
+ const encoded=mocks.files[named].base64;
128
+ assert(encoded.length<=349528&&/^[A-Za-z0-9+/]*={0,2}$/.test(encoded),'MODULE_FILE_READ',`The {base64} file for ${named} must be plain base64 of at most 256 KB.`);
129
+ note('files.read',named,'mocks');
130
+ if(typeof Buffer!=='undefined')return new Uint8Array(Buffer.from(encoded,'base64'));
131
+ const binary=atob(encoded),bytes=new Uint8Array(binary.length);for(let index=0;index<binary.length;index++)bytes[index]=binary.charCodeAt(index);return bytes;
132
+ }
133
+ if(named&&plain(mocks.files[named])){
134
+ assert(!mocks.files[named].text.includes('\u0000'),'MODULE_FILE_READ',`The {text} file for ${named} holds binary bytes (a NUL character), which StillMade cannot store: give the file as {"base64": "..."} instead.`);
135
+ note('files.read',named,'mocks');return new TextEncoder().encode(mocks.files[named].text);
136
+ }
122
137
  if(named){
123
138
  assert(typeof readLocalFile==='function','MODULE_HOST_UNAVAILABLE','Local files need a file reader.');
124
139
  note('files.read',mocks.files[named],'mocks');
125
140
  return new Uint8Array(await readLocalFile(mocks.files[named]));
126
141
  }
127
- fail(`files.read has no local file for ${ref?.assetId||url||'this reference'}. Map its assetId to a file in the Block folder under files in ${MOCKS_FILE}.`,'MODULE_FILE_READ');
142
+ // An example StillMade serves (/api/media/example/photo-1.jpg) reads its real bytes, as in StillMade.
143
+ const example=exampleMediaName(url);
144
+ if(example){note('files.read',url,'example');return await readExampleMedia(example);}
145
+ fail(`files.read has no local file for ${ref?.assetId||url||'this reference'}. Map its assetId under files in ${MOCKS_FILE} to a file in the Block folder, or to {text} for a text file, or use one of StillMade's example files at ${EXAMPLE_MEDIA_PATH} (photo-1.jpg to photo-4.jpg, sheet.png, room.png, kitchen.wav, hall.wav, click-120.wav, audio.mp3, clip.mp4, notes.txt, sales.csv).`,'MODULE_FILE_READ');
128
146
  };
129
147
  const buffer=bytes=>bytes.buffer.slice(bytes.byteOffset,bytes.byteOffset+bytes.byteLength);
130
- const EXT={'image/png':'png','image/jpeg':'jpg','image/webp':'webp','image/gif':'gif','video/mp4':'mp4','video/webm':'webm','audio/mpeg':'mp3','audio/wav':'wav'};
148
+ // Media, then the general files a module Block may write (data-values.js), each saved under its own extension.
149
+ const EXT={'image/png':'png','image/jpeg':'jpg','image/webp':'webp','image/gif':'gif','video/mp4':'mp4','video/webm':'webm','audio/mpeg':'mp3','audio/wav':'wav',...Object.fromEntries(Object.entries(FILE_FORMATS).map(([extension,mime])=>[mime,extension]))};
131
150
  const saveFile=(data,options={})=>{
132
151
  const mediaType=String(options.mediaType||'application/octet-stream'),bytes=new Uint8Array(data.slice(0)),kind=['image','video','audio'].includes(mediaType.split('/')[0])?mediaType.split('/')[0]:'file';
133
152
  const {id,url}=keep(bytes,mediaType,EXT[mediaType]||'bin',`written-${kind}`);
@@ -153,6 +172,8 @@ export function createMockHost({mocks:given={},readLocalFile,manifest}={}){
153
172
  return {sampleRate:checked.sampleRate,duration:samples.length/checked.sampleRate,samples};
154
173
  }
155
174
  if(checked.operation==='image.thumbnail')return reference.image();
175
+ if(checked.operation==='image.compose')return reference.image({width:checked.width,height:checked.height});
176
+ if(checked.operation==='document.text'){const text=new TextDecoder().decode(new Uint8Array(buffer(await readBytes(checked.source))));return {pages:[text],text};}
156
177
  if(checked.operation==='timeline.render')return checked.kind==='audio'?reference.audio({sampleRate:44100}):reference.video();
157
178
  if(checked.operation.startsWith('audio.'))return reference.audio({sampleRate:44100});
158
179
  return reference.video();
@@ -14,7 +14,10 @@ export function validateMocks(value){
14
14
  assert(Array.isArray(value.network),'MOCK_HOST',`${MOCKS_FILE} network is a list of recorded responses`);
15
15
  for(const entry of value.network)assert(plain(entry)&&typeof entry.url==='string'&&/^https:\/\//.test(entry.url)&&(entry.method===undefined||typeof entry.method==='string')&&(entry.status===undefined||Number.isInteger(entry.status)&&entry.status>=100&&entry.status<=599)&&[entry.json!==undefined,typeof entry.text==='string',typeof entry.base64==='string'].filter(Boolean).length<=1,'MOCK_HOST','Each recorded response has an https url, an optional method and status, and at most one of json, text or base64');
16
16
  }
17
- if(Object.hasOwn(value,'files'))for(const [name,path] of Object.entries(value.files))assert(typeof path==='string'&&path&&!path.startsWith('/')&&!path.split(/[\\/]/).includes('..'),'MOCK_HOST',`${MOCKS_FILE} files.${name} is a path inside the Block folder`);
17
+ // A file is a path inside the Block folder, or {text} for a text file given inline (October 28, 2026: the in-app
18
+ // builder has no Block folder, so a Block reading a notes file the person gives it could not be tested), or {base64} for a
19
+ // small binary file, which travels in the pack (November 2026).
20
+ if(Object.hasOwn(value,'files'))for(const [name,path] of Object.entries(value.files))assert(typeof path==='string'&&path&&!path.startsWith('/')&&!path.split(/[\\/]/).includes('..')||plain(path)&&Object.keys(path).length===1&&(typeof path.text==='string'&&path.text.length<=200000||typeof path.base64==='string'&&path.base64.length<=349528),'MOCK_HOST',`${MOCKS_FILE} files.${name} is a path inside the Block folder, {text} with a text file's contents, or {base64} with a small binary file's (at most 256 KB)`);
18
21
  if(Object.hasOwn(value,'project'))assert(plain(value.project),'MOCK_HOST',`${MOCKS_FILE} project is a project snapshot object`);
19
22
  return value;
20
23
  }
@@ -5,6 +5,7 @@
5
5
  // operation in `permissions.capabilities`.
6
6
  import {assert,plain,stableStringify} from './contracts.js';
7
7
  import {defaultCapabilityExpectations} from './capabilities.js';
8
+ import {GENERATION_LIMITS,IMAGE_GENERATION_MODELS} from './generation-catalog.js';
8
9
 
9
10
  const text=(value,max)=>typeof value==='string'&&value.trim().length>0&&value.length<=max;
10
11
  const media=(value,kind)=>plain(value)&&value.kind===kind&&['assetId','versionId','url'].every(key=>typeof value[key]==='string'&&value[key].trim());
@@ -20,7 +21,14 @@ const SHAPES={
20
21
  },
21
22
  'image.generate':request=>{
22
23
  if(!text(request.prompt,32000))fail('image.generate needs a prompt');
23
- return {inputs:{prompt:port('text','Image prompt')},outputs:{result:port('image','Generated image')},capability:{prompt:{$input:'prompt'},output:'result',...(request.settings?{settings:request.settings}:{}),...(request.negativePrompt?{negativePrompt:request.negativePrompt}:{}),...(Number.isInteger(request.seed)?{seed:request.seed}:{})},input:{prompt:request.prompt}};
24
+ // references: saved photos the new picture keeps (the product, person or place), as a capability Block binds them
25
+ // (October 29, 2026: an escape room poster could not keep the room in the person's photos).
26
+ const references=request.references;
27
+ if(references!==undefined&&(!Array.isArray(references)||!references.length||references.length>GENERATION_LIMITS.references||!references.every(value=>media(value,'image'))))fail(`image.generate references are 1 to ${GENERATION_LIMITS.references} saved images`);
28
+ // A model named in settings must take that many photos (GPT-Image 1.5, Kling Image V3 and Seedream 4.5 take none).
29
+ const model=IMAGE_GENERATION_MODELS[request.settings?.model];
30
+ if(references&&model&&references.length>model.references)fail(model.references?`${model.label} takes at most ${model.references} reference photos`:`${model.label} takes no reference photos: choose a model that does, or leave the model to the person`);
31
+ return {inputs:{prompt:port('text','Image prompt'),...(references?{references:port('image[]','Reference images')}:{})},outputs:{result:port('image','Generated image')},capability:{prompt:{$input:'prompt'},output:'result',...(references?{references:{$input:'references'}}:{}),...(request.settings?{settings:request.settings}:{}),...(request.negativePrompt?{negativePrompt:request.negativePrompt}:{}),...(Number.isInteger(request.seed)?{seed:request.seed}:{})},input:{prompt:request.prompt,...(references?{references}:{})}};
24
32
  },
25
33
  'video.generate':request=>{
26
34
  if(!text(request.prompt,32000))fail('video.generate needs a prompt');
@@ -53,8 +61,10 @@ const SHAPES={
53
61
  const list=Array.isArray(request.media)?request.media:[request.media],kind=list[0]?.kind;
54
62
  if(!list.length||!['image','video'].includes(kind)||!list.every(value=>media(value,kind))||(kind==='video'&&list.length!==1)||list.length>4)fail('media.analyze needs one saved video or one to four saved images');
55
63
  if(!text(request.question,2000))fail('media.analyze needs a question');
64
+ // October 30, 2026: detail:'check' is the short photo check, priced per call of up to four photos.
65
+ if(request.detail!==undefined&&(request.detail!=='check'||kind!=='image'))fail('media.analyze detail:"check" checks one to four saved images, never a video');
56
66
  const many=Array.isArray(request.media);
57
- return {inputs:{media:port(kind==='video'?'video':many?'image[]':'image','Media to analyze')},outputs:{result:port('json','Visual evidence')},capability:{media:{$input:'media'},question:request.question,output:'result'},input:{media:many?list:list[0]}};
67
+ return {inputs:{media:port(kind==='video'?'video':many?'image[]':'image','Media to analyze')},outputs:{result:port('json','Visual evidence')},capability:{media:{$input:'media'},question:request.question,...(request.detail?{detail:request.detail}:{}),output:'result'},input:{media:many?list:list[0]}};
58
68
  },
59
69
  'web.fetch':request=>{
60
70
  if(!text(request.url,2048)||!text(request.instruction,4000))fail('web.fetch needs a url and an instruction');
@@ -1,3 +1,9 @@
1
+ /** What a value of each port type is, in one table: the generated declarations, the authoring guide and a missing Block's brief read it. */
2
+ export const PORT_VALUE_SHAPES: Readonly<Record<string, string>>;
3
+ /** Every other structured port type: an object with schemaVersion 1 whose fields its own contract gives. */
4
+ export const STRUCTURED_VALUE_SHAPE: string;
5
+ /** The value shape of one port type (without []): its own, the structured one for another SDK type, or null for an unknown type. */
6
+ export function portValueShape(type: string): string | null;
1
7
  /** Port types with their own TypeScript value type in generated declarations. */
2
8
  export const PORT_VALUE_TYPES: Readonly<Record<string, string>>;
3
9
  /** The TypeScript type of a compact port schema ("number[]", {points: "number[]"}, ["string"]). */
@@ -4,8 +4,30 @@
4
4
  // `run(input, stillmade)`, and every port's value, from the manifest. The file
5
5
  // is self-contained: it needs no package to resolve.
6
6
  import {VIEW_BRIDGE} from './view-bridge.js';
7
+ import {TYPES} from './contracts.js';
8
+ import {FILE_MEDIA_TYPES,DOCUMENT_FORMATS} from './data-values.js';
7
9
 
8
10
  const MEDIA_FIELDS='assetId: string; versionId: string; url: string; mimeType?: string; bytes?: number; name?: string; role?: string';
11
+ const quoted=values=>values.map(value=>`'${value}'`).join(' | ');
12
+ // What a value of each port type is, in one table: these declarations, the authoring guide's type list and a missing
13
+ // Block's brief all read it (October 27, 2026: built Blocks typed a document port text and returned a document object).
14
+ export const PORT_VALUE_SHAPES=Object.freeze({
15
+ text:'a string',number:'a number',integer:'a whole number',boolean:'true or false',json:'any JSON value',object:'a plain JSON object',
16
+ url:'an absolute http or https address string',date:"an ISO 8601 date string ('2026-10-09' or '2026-10-09T14:30')",color:"a hex color string ('#ff8800' or '#ff8800cc')",
17
+ image:`{kind: 'image'; ${MEDIA_FIELDS}; width?: number; height?: number}`,
18
+ video:`{kind: 'video'; ${MEDIA_FIELDS}; width?: number; height?: number; duration?: number}`,
19
+ audio:`{kind: 'audio'; ${MEDIA_FIELDS}; duration?: number; sampleRate?: number; channels?: number}`,
20
+ asset:`{kind: 'image' | 'video' | 'audio' | 'asset'; ${MEDIA_FIELDS}}`,
21
+ file:`{kind: 'file'; assetId: string; versionId: string; mimeType: ${quoted(FILE_MEDIA_TYPES)}; url?: string; name?: string; bytes?: number}`,
22
+ document:`{schemaVersion: 1; format: ${quoted(DOCUMENT_FORMATS)}; text: string; title?: string}`,
23
+ table:'{schemaVersion?: 1; columns: string[]; rows: Array<Record<string, string | number | boolean | null>>}',
24
+ transcript:'{schemaVersion: 1; text: string; words: Array<{text: string; start: number; end: number}>}',
25
+ script:'{schemaVersion: 1; text: string; [key: string]: unknown}',
26
+ });
27
+ /** Every other structured port type: an object with schemaVersion 1 whose fields its own contract gives. */
28
+ export const STRUCTURED_VALUE_SHAPE='{schemaVersion: 1; [key: string]: unknown}';
29
+ /** The value shape of one port type (without []): its own, the structured one for another SDK type, or null for an unknown type. */
30
+ export function portValueShape(type){return Object.hasOwn(PORT_VALUE_SHAPES,type)?PORT_VALUE_SHAPES[type]:TYPES.includes(type)?STRUCTURED_VALUE_SHAPE:null;}
9
31
  /** Port types with their own TypeScript value type. Other structured types are schema-1 documents. */
10
32
  export const PORT_VALUE_TYPES=Object.freeze({
11
33
  text:'string',number:'number',integer:'number',boolean:'boolean',json:'StillMadeJson',object:'Record<string, unknown>',
@@ -46,17 +68,17 @@ export function blockTypeDeclarations(manifest){
46
68
  export {};
47
69
 
48
70
  type StillMadeJson = string | number | boolean | null | StillMadeJson[] | {[key: string]: StillMadeJson};
49
- type StillMadeMedia = {kind: 'image' | 'video' | 'audio' | 'asset'; ${MEDIA_FIELDS}};
71
+ type StillMadeMedia = ${PORT_VALUE_SHAPES.asset};
50
72
  /** A saved image, or decoded RGBA pixels (recipe and JavaScript Blocks). */
51
- type StillMadeImage = {kind: 'image'; ${MEDIA_FIELDS}; width?: number; height?: number} | {width: number; height: number; data: number[]; role?: string};
52
- type StillMadeVideo = {kind: 'video'; ${MEDIA_FIELDS}; width?: number; height?: number; duration?: number};
53
- type StillMadeAudio = {kind: 'audio'; ${MEDIA_FIELDS}; duration?: number; sampleRate?: number; channels?: number};
54
- type StillMadeFile = {kind: 'file'; assetId: string; versionId: string; mimeType: 'application/pdf' | 'text/plain' | 'text/csv' | 'text/markdown' | 'application/json' | 'application/zip'; url?: string; name?: string; bytes?: number};
55
- type StillMadeDocument = {schemaVersion: 1; format: 'markdown' | 'plain'; text: string; title?: string};
56
- type StillMadeTable = {schemaVersion?: 1; columns: string[]; rows: Array<Record<string, string | number | boolean | null>>};
57
- type StillMadeTranscript = {schemaVersion: 1; text: string; words: Array<{text: string; start: number; end: number}>};
58
- type StillMadeScript = {schemaVersion: 1; text: string; [key: string]: unknown};
59
- type StillMadeDocumentValue = {schemaVersion: 1; [key: string]: unknown};
73
+ type StillMadeImage = ${PORT_VALUE_SHAPES.image} | {width: number; height: number; data: number[]; role?: string};
74
+ type StillMadeVideo = ${PORT_VALUE_SHAPES.video};
75
+ type StillMadeAudio = ${PORT_VALUE_SHAPES.audio};
76
+ type StillMadeFile = ${PORT_VALUE_SHAPES.file};
77
+ type StillMadeDocument = ${PORT_VALUE_SHAPES.document};
78
+ type StillMadeTable = ${PORT_VALUE_SHAPES.table};
79
+ type StillMadeTranscript = ${PORT_VALUE_SHAPES.transcript};
80
+ type StillMadeScript = ${PORT_VALUE_SHAPES.script};
81
+ type StillMadeDocumentValue = ${STRUCTURED_VALUE_SHAPE};
60
82
 
61
83
  /** ${manifest.name}: input values. */
62
84
  export interface BlockInputs {
@@ -13,6 +13,28 @@ const label=value=>String(value||'').replace(/[-_]+/g,' ').replace(/\s+/g,' ').t
13
13
 
14
14
  // Read static host-bound controls as data. The parser deliberately does not
15
15
  // execute author JavaScript or attempt to infer fields from arbitrary code.
16
+ // November 1, 2026 (live controller check): Chat set Narration's voice to "Nova", the label it saw, and the screen
17
+ // kept Onyx because its radio value is "nova". A fieldset's radio options are read with their visible labels so a
18
+ // pick is saved as the option's own value.
19
+ function choiceOptions(source=''){
20
+ const options=[];
21
+ for(const match of String(source).matchAll(/<input\b([^>]*)>([\s\S]*?)(?=<input\b|<\/label>|$)/gi)){
22
+ const attrs=attributes(match[1]);
23
+ if((attrs.get('type')||'').toLowerCase()!=='radio'||!attrs.has('value'))continue;
24
+ options.push({value:attrs.get('value'),label:match[2].replace(/<[^>]*>/g,' ').replace(/\s+/g,' ').trim().slice(0,120)});
25
+ }
26
+ return options.slice(0,40);
27
+ }
28
+
29
+ // The option value a pick names: its value, its label, or the label's first word, in any case.
30
+ export function choiceValue(control,value){
31
+ if(!control?.options?.length||typeof value!=='string')return value;
32
+ const wanted=value.trim().toLowerCase();
33
+ const found=control.options.find(option=>option.value.toLowerCase()===wanted)||control.options.find(option=>option.label.toLowerCase()===wanted||option.label.split(' ')[0].toLowerCase()===wanted);
34
+ if(!found)throw new Error(`Choose one of: ${control.options.map(option=>option.value).join(', ')}.`);
35
+ return found.value;
36
+ }
37
+
16
38
  export function sharedAgentControls(view){
17
39
  const html=String(view?.html||'').replace(/<!--[\s\S]*?-->/g,''),controls=[];
18
40
  for(const match of html.matchAll(/<(input|textarea|select|fieldset)\b([^>]*)>/gi)){
@@ -24,7 +46,8 @@ export function sharedAgentControls(view){
24
46
  const type=element==='input'?(['search','email','tel','url'].includes(inputType)?'text':inputType):element==='fieldset'?'choice':element;
25
47
  if(!['text','number','range','checkbox','color','textarea','select','choice'].includes(type))continue;
26
48
  const description=label(attrs.get('aria-label')||attrs.get('title')||key);
27
- controls.push({key,id,type,description,...(Number.isFinite(Number(attrs.get('min')))&&attrs.has('min')?{minimum:Number(attrs.get('min'))}:{}),...(Number.isFinite(Number(attrs.get('max')))&&attrs.has('max')?{maximum:Number(attrs.get('max'))}:{})});
49
+ const options=type==='choice'?choiceOptions(html.slice(match.index+match[0].length,html.indexOf('</fieldset>',match.index)>=0?html.indexOf('</fieldset>',match.index):undefined)):null;
50
+ controls.push({key,id,type,description,...(options?.length?{options}:{}),...(Number.isFinite(Number(attrs.get('min')))&&attrs.has('min')?{minimum:Number(attrs.get('min'))}:{}),...(Number.isFinite(Number(attrs.get('max')))&&attrs.has('max')?{maximum:Number(attrs.get('max'))}:{})});
28
51
  }
29
52
  return controls.slice(0,100);
30
53
  }
@@ -45,7 +68,7 @@ export function parseSharedAgentValue(control,current,value){
45
68
  return value;
46
69
  }
47
70
  if(control?.type==='color'){if(typeof value!=='string'||!/^#[0-9a-f]{6}$/i.test(value))throw new Error('This shared color needs a six-digit hex value.');return value;}
48
- if(['text','textarea','select','choice'].includes(control?.type)){if(typeof value!=='string'||value.length>65536)throw new Error('This shared control needs bounded text.');return value;}
71
+ if(['text','textarea','select','choice'].includes(control?.type)){if(typeof value!=='string'||value.length>65536)throw new Error('This shared control needs bounded text.');return control.type==='choice'?choiceValue(control,value):value;}
49
72
  if(current===null){if(value!==null)throw new Error('This dynamic shared field currently accepts null only.');return null;}
50
73
  if(Array.isArray(current)){if(!Array.isArray(value))throw new Error('Keep this dynamic shared field as an array.');return structuredClone(value);}
51
74
  if(plain(current)){if(!plain(value))throw new Error('Keep this dynamic shared field as an object.');return structuredClone(value);}