stillmade-sdk 0.1.37 → 0.1.39

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 (32) hide show
  1. package/MODULES.md +29 -11
  2. package/PASTE_TO_LLM.md +7 -6
  3. package/SDK.md +16 -15
  4. package/UI_AESTHETIC.md +6 -0
  5. package/blocks/narration/package.js +1 -1
  6. package/blocks/narration/versions/1.0.1/package.js +221 -0
  7. package/blocks/native/sm.animated-scene/versions/2.3.9/package.js +769 -0
  8. package/blocks/native/sm.board-editor/versions/2.2.11/package.js +551 -0
  9. package/blocks/native/sm.panel-strip/versions/2.4.2/package.js +1316 -0
  10. package/blocks/native/sm.pipeline/versions/2.2.6/package.js +1095 -0
  11. package/blocks/screen-recorder/package.js +1 -1
  12. package/blocks/screen-recorder/versions/1.0.1/package.js +223 -0
  13. package/package.json +1 -1
  14. package/packages/block-cli/cli.js +28 -17
  15. package/packages/block-cli/dev-host/assets/host.js +16 -16
  16. package/packages/block-cli/dev-host/assets/index.css +1 -1
  17. package/packages/block-cli/dev-server.js +14 -2
  18. package/packages/block-cli/ready-gate.js +3 -2
  19. package/packages/block-platform/catalog.js +1 -1
  20. package/packages/block-platform/motion-starters.js +14 -0
  21. package/packages/block-platform/project-type-starters.js +20 -7
  22. package/packages/block-sdk/capabilities.js +1 -1
  23. package/packages/block-sdk/example-media-data.js +2 -1
  24. package/packages/block-sdk/example-media.js +1 -0
  25. package/packages/block-sdk/first-party-identities.js +6 -0
  26. package/packages/block-sdk/first-party-packages.js +7 -1
  27. package/packages/block-sdk/media-transform.js +17 -4
  28. package/packages/block-sdk/mock-host.js +24 -5
  29. package/packages/block-sdk/module-hosted.js +12 -4
  30. package/packages/block-sdk/semantic-relationships.js +6 -0
  31. package/packages/block-sdk/testing.js +5 -3
  32. package/packages/block-sdk/view-design.js +56 -1
@@ -4,7 +4,7 @@
4
4
  // executed and checked exactly like one. The module Block must list the
5
5
  // operation in `permissions.capabilities`.
6
6
  import {assert,plain,stableStringify} from './contracts.js';
7
- import {defaultCapabilityExpectations} from './capabilities.js';
7
+ import {defaultCapabilityExpectations,MEDIA_ANALYZE_LIMITS} from './capabilities.js';
8
8
  import {GENERATION_LIMITS,IMAGE_GENERATION_MODELS} from './generation-catalog.js';
9
9
 
10
10
  const text=(value,max)=>typeof value==='string'&&value.trim().length>0&&value.length<=max;
@@ -47,8 +47,11 @@ const SHAPES={
47
47
  return {inputs:{prompt:port('text','Sound effect prompt')},outputs:{result:port('audio','Generated sound effect')},capability:{prompt:{$input:'prompt'},output:'result',...(request.durationSec?{settings:{durationSec:request.durationSec}}:{})},input:{prompt:request.prompt}};
48
48
  },
49
49
  'audio.transcribe':request=>{
50
- if(!media(request.audio,'audio'))fail('audio.transcribe needs a saved audio reference');
51
- return {inputs:{audio:port('audio','Audio to transcribe'),language:{type:'text',description:'Language code or auto',required:false,default:'auto'}},outputs:{result:port('transcript','Timed transcript')},capability:{audio:{$input:'audio'},language:{$input:'language'},output:'result'},input:{audio:request.audio,language:typeof request.language==='string'?request.language:'auto'}};
50
+ // A video's sound is transcribed as a capability Block's is (November 1, 2026: a sermon clip Block had to extract the
51
+ // audio first because only audio was accepted here).
52
+ const kind=request.audio?.kind==='video'?'video':'audio';
53
+ if(!media(request.audio,kind))fail('audio.transcribe needs a saved audio or video reference');
54
+ return {inputs:{audio:port(kind,kind==='video'?'Video whose sound to transcribe':'Audio to transcribe'),language:{type:'text',description:'Language code or auto',required:false,default:'auto'}},outputs:{result:port('transcript','Timed transcript')},capability:{audio:{$input:'audio'},language:{$input:'language'},output:'result'},input:{audio:request.audio,language:typeof request.language==='string'?request.language:'auto'}};
52
55
  },
53
56
  'image.describe':request=>{
54
57
  const images=Array.isArray(request.images)?request.images:[request.image];
@@ -63,8 +66,12 @@ const SHAPES={
63
66
  if(!text(request.question,2000))fail('media.analyze needs a question');
64
67
  // October 30, 2026: detail:'check' is the short photo check, priced per call of up to four photos.
65
68
  if(request.detail!==undefined&&(request.detail!=='check'||kind!=='image'))fail('media.analyze detail:"check" checks one to four saved images, never a video');
69
+ // range: one part of a video looked at closely, as a capability Block binds it (November 1, 2026: a sermon clip Block
70
+ // could only look at the whole sermon at once).
71
+ const range=request.range;
72
+ if(range!==undefined&&!(kind==='video'&&plain(range)&&Object.keys(range).sort().join(',')==='end,start'&&Number.isFinite(range.start)&&Number.isFinite(range.end)&&range.start>=0&&range.end>range.start&&range.end-range.start<=MEDIA_ANALYZE_LIMITS.range+1e-6))fail(`media.analyze range is {start, end} in seconds of one video, at most ${MEDIA_ANALYZE_LIMITS.range} seconds apart`);
66
73
  const many=Array.isArray(request.media);
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]}};
74
+ return {inputs:{media:port(kind==='video'?'video':many?'image[]':'image','Media to analyze'),...(range?{start:port('number','Start of the range, in seconds'),end:port('number','End of the range, in seconds')}:{})},outputs:{result:port('json','Visual evidence')},capability:{media:{$input:'media'},question:request.question,...(range?{range:{start:{$input:'start'},end:{$input:'end'}}}:{}),...(request.detail?{detail:request.detail}:{}),output:'result'},input:{media:many?list:list[0],...(range?{start:range.start,end:range.end}:{})}};
68
75
  },
69
76
  'web.fetch':request=>{
70
77
  if(!text(request.url,2048)||!text(request.instruction,4000))fail('web.fetch needs a url and an instruction');
@@ -105,6 +112,7 @@ export function isModuleHostedPackage(content){
105
112
  const input=content.tests?.[0]?.input||{},request={};
106
113
  // The request: the capability's own fields, each input reference replaced by the call's value.
107
114
  for(const [key,value] of Object.entries(content.capability))if(!['schemaVersion','operation','output'].includes(key))request[key]=plain(value)&&typeof value.$input==='string'?input[value.$input]:value;
115
+ if(plain(request.range))request.range={start:input[request.range.start?.$input],end:input[request.range.end?.$input]};
108
116
  if(Number.isFinite(request.settings?.durationSec))request.durationSec=request.settings.durationSec;
109
117
  if(Array.isArray(request.image)){request.images=request.image;delete request.image;}
110
118
  const source={id:manifest.id.slice(0,-'.hosted'.length),version:manifest.version,name:manifest.name.slice(0,-suffix.length),license:manifest.license,...(manifest.provenance?{provenance:manifest.provenance}:{}),permissions:{capabilities:[operation]}};
@@ -15,6 +15,12 @@ export const STANDARD_SEMANTIC_RELATIONSHIPS=Object.freeze([
15
15
  Object.freeze({name:'legacy.timeline.updatededitor',parents:Object.freeze(['timeline'])}),
16
16
  // A narration is audio that can be transcribed.
17
17
  Object.freeze({name:'narration_audio',parents:Object.freeze(['source_audio'])}),
18
+ // Generated, edited and rendered videos are videos to work from, as generated and processed images are images
19
+ // (November 1, 2026: a new Block taking any video a Step hands on had no meaning that accepts them all).
20
+ Object.freeze({name:'generated_video',parents:Object.freeze(['source_video'])}),
21
+ Object.freeze({name:'edited_video',parents:Object.freeze(['source_video'])}),
22
+ Object.freeze({name:'rendered_video',parents:Object.freeze(['source_video'])}),
23
+ Object.freeze({name:'rendered_audio',parents:Object.freeze(['source_audio'])}),
18
24
  ]);
19
25
 
20
26
  const parents=new Map(STANDARD_SEMANTIC_RELATIONSHIPS.map(entry=>[entry.name,entry.parents]));
@@ -36,9 +36,11 @@ function fixtureExpectedMessage(outputs,fixture,error){
36
36
  // (assetId local-written-file-N, versionId, url, bytes) that no model can predict. A module fixture compares it by
37
37
  // shape: an expected {kind, mimeType, name}, with those fields left out or given, matches when kind, mimeType and name
38
38
  // match. Every other value is compared exactly.
39
- const WRITTEN_KINDS=['file','image','video','audio'],HOST_MADE=['assetId','versionId','url','bytes'];
39
+ // A written file a Block passes on in an asset output carries kind 'asset' and matches the same way (November 1, 2026:
40
+ // a sermon clips Block's JSON in its asset[] output failed every test on the stand-in assetId).
41
+ const WRITTEN_KINDS=['file','image','video','audio'],SHAPE_KINDS=[...WRITTEN_KINDS,'asset'],HOST_MADE=['assetId','versionId','url','bytes'];
40
42
  const standInWritten=value=>plain(value)&&/^local-written-(?:file|image|video|audio)-\d+$/.test(String(value.assetId));
41
- const writtenShape=value=>plain(value)&&WRITTEN_KINDS.includes(value.kind)&&typeof value.mimeType==='string'&&Object.keys(value).every(key=>['kind','mimeType','name',...HOST_MADE].includes(key));
43
+ const writtenShape=value=>plain(value)&&SHAPE_KINDS.includes(value.kind)&&typeof value.mimeType==='string'&&Object.keys(value).every(key=>['kind','mimeType','name',...HOST_MADE].includes(key));
42
44
  // expected, with each reference that matches a stand-in-written output by shape replaced by that output.
43
45
  function matchWritten(actual,expected){
44
46
  if(standInWritten(actual))return writtenShape(expected)&&['kind','mimeType','name'].every(key=>expected[key]===actual[key])?actual:expected;
@@ -50,7 +52,7 @@ function matchWritten(actual,expected){
50
52
  // for the host-made fields it leaves out.
51
53
  function withStandInHostFields(outputs,expected){
52
54
  if(!plain(outputs)||!plain(expected))return expected;
53
- const fill=(value,type)=>type.endsWith('[]')?(Array.isArray(value)?value.map(item=>fill(item,type.slice(0,-2))):value):WRITTEN_KINDS.includes(type)&&value?.kind===type&&writtenShape(value)?{assetId:`local-written-${type}-1`,versionId:'v1',...value}:value;
55
+ const fill=(value,type)=>type.endsWith('[]')?(Array.isArray(value)?value.map(item=>fill(item,type.slice(0,-2))):value):writtenShape(value)&&(WRITTEN_KINDS.includes(type)&&value.kind===type||type==='asset'&&value.kind==='asset')?{assetId:`local-written-${WRITTEN_KINDS.includes(type)?type:'file'}-1`,versionId:'v1',...value}:value;
54
56
  return Object.fromEntries(Object.entries(expected).map(([name,value])=>[name,Object.hasOwn(outputs,name)?fill(value,String(outputs[name]?.type)):value]));
55
57
  }
56
58
  export function validatePackage(pkg) {
@@ -27,7 +27,7 @@ export const BLOCK_KIT_CLASSES=Object.freeze({
27
27
  // The design principles for every Block interface, in priority order.
28
28
  export const BLOCK_INTERFACE_PRINCIPLES=Object.freeze([
29
29
  'Function first: unless the person asks for a design direction, build from the Block kit (`sm-block`, `sm-stage`, `sm-choices`, `sm-actions`, `sm-more`) and ship little or no CSS of your own. A direction the person names wins over the kit.',
30
- 'Show the thing being made: the preview, media or result gets the largest area (`sm-stage`). Never a JSON dump, source editor, manifest or diagnostic panel unless the task is about source.',
30
+ 'Show the thing being made: the preview, media or result gets the largest area (`sm-stage`). A picture or clip the Block makes fills it in its own shape, as large as fits; several fill it as a grid, each big enough to watch. Small tiles are for the files a person gives the Block, never for what it makes. Never a JSON dump, source editor, manifest or diagnostic panel unless the task is about source.',
31
31
  'One obvious next action (`sm-primary`); peers are quiet buttons or named icons. Seven or fewer choices in view.',
32
32
  'The action button names what it does ("Write", "Generate image", "Make video"), never "Run", "Run step", "Submit" or "Review cost". Show no credit numbers: StillMade puts the price beside the verb on its own run button ("Write · 1 credit"), with a chip for the model, quality or count when they can change. Pressing it runs at that price; a confirmation appears only when the server price is higher.',
33
33
  'Pictures over words: choices people see or hear are thumbnails, swatches, samples or glyphs (`sm-choices`), not word lists. Icons carry an accessible name and tooltip; never emoji.',
@@ -42,6 +42,7 @@ export const BLOCK_INTERFACE_PRINCIPLES=Object.freeze([
42
42
  'Status sits beside the action it describes (`sm-status`), in a few words.',
43
43
  'Keep the page in normal scroll flow; no sticky media or control strips unless the editing task needs them.',
44
44
  'Use the width: from 860px wide the kit puts the controls on the left at a reading width (300–380px) and the result (`sm-stage`) fills the right, in its real shape; under that everything stacks with the result first and the action within reach. Extra width goes to the result, never to wider text fields. Before there is a result, the stage previews what will be made from the inputs. `data-layout="stack"` opts out.',
45
+ 'Size with the kit, never fixed sizes: no fixed pixel widths or heights for the workspace or stage, no 100vh, no position:fixed panels. StillMade zooms a Block\'s screen to fill the space it has (up to 150%, never down to a phone layout on a computer), so a Block laid out with the kit\'s spacing fills its Step without empty space.',
45
46
  'Settings sit side by side (`sm-settings`), next to what they change. Three or four short options are one segmented row (`data-segmented`), not a row of big tiles.',
46
47
  'Lists fill the width at a small size: input files are small tiles (`sm-gallery data-size="small"`), many items are one line each (`sm-list`): a thumbnail, one line of text, a short fact. Tapping an item opens all of its fields in place. Extra width means more items per row, never bigger cards.',
47
48
  'On a phone chip and segment rows scroll sideways instead of wrapping; long lists show their first three with "N more" (`data-peek`) and long text five lines until tapped (`sm-clamp`).',
@@ -135,6 +136,58 @@ function topLevelRows(html){
135
136
  return rows;
136
137
  }
137
138
 
139
+ // November 1, 2026 (owner, zoom plan): a screen held to a narrow width leaves most of a wide Step empty, and StillMade's
140
+ // zoom cannot fill it (it stops before the kit's two-column layout from 860px would change). The narrowest fixed or
141
+ // maximum width on what holds the whole screen (html, body, the outermost element, a lone wrapper inside it), from its
142
+ // inline style or a stylesheet rule outside a small-screen @media block; null when nothing holds it.
143
+ const KIT_TWO_COLUMNS=860;
144
+ function narrowScreen(view){
145
+ const html=String(view?.html||''),lower=html.toLowerCase(),holders=[{tag:'html',classes:[],id:''},{tag:'body',classes:[],id:''}];
146
+ const width=declarations=>{let narrowest=null;for(const match of String(declarations).matchAll(/(?:^|[;{\s])(?:max-width|width)\s*:\s*(\d{1,5}(?:\.\d+)?)(px|rem|em)\b/gi)){const px=Number(match[1])*(match[2].toLowerCase()==='px'?1:16);if(px>=200&&px<KIT_TWO_COLUMNS)narrowest=Math.min(narrowest??Infinity,px);}return narrowest;};
147
+ // The outermost elements, and the children of a lone outermost one; a lone child is a wrapper too.
148
+ // The last '>' found is reused while it is still ahead, and attributes are read from a bounded slice, so broken tags
149
+ // ("<a<a<a…>") stay linear.
150
+ const top=[],inner=[];let depth=0,end=-1;
151
+ for(let at=lower.indexOf('<');at!==-1;at=lower.indexOf('<',at+1)){
152
+ if(end<at)end=lower.indexOf('>',at);if(end===-1)break;
153
+ const tag=/^<(\/?)([a-z][a-z0-9-]*)/.exec(lower.slice(at,Math.min(end+1,at+40)));
154
+ if(!tag)continue;
155
+ const [,closing,name]=tag;
156
+ if(closing){depth=Math.max(0,depth-1);if(depth===0&&top.length>1)break;continue;}
157
+ if(['script','style','template'].includes(name)){const close=lower.indexOf(`</${name}`,end);if(close===-1)break;at=close;continue;}
158
+ const attributes=html.slice(at,Math.min(end,at+2000)),element={tag:name,classes:(/\sclass\s*=\s*["']([^"']*)["']/i.exec(attributes)?.[1]||'').split(/\s+/).filter(Boolean),id:/\sid\s*=\s*["']([^"']*)["']/i.exec(attributes)?.[1]||'',style:/\sstyle\s*=\s*["']([^"']*)["']/i.exec(attributes)?.[1]||''};
159
+ if(depth===0)top.push(element);else if(depth===1&&top.length===1)inner.push(element);
160
+ if(!VOID_TAGS.has(name)&&lower[end-1]!=='/')depth++;
161
+ }
162
+ holders.push(...top.filter(()=>top.length===1),...(inner.length===1?inner:[]));
163
+ for(const element of holders.slice(2)){const px=width(element.style);if(px)return {px,where:`the style on <${element.tag}>`};}
164
+ const holds=selector=>{
165
+ const last=selector.trim().split(/[\s>+~]+/).pop().replace(/^:root$/i,'html'),parts=/^([a-z][a-z0-9-]*|\*)?((?:[.#][\w-]+)*)$/i.exec(last);
166
+ if(!parts||!parts[1]&&!parts[2])return false;
167
+ const classes=[...parts[2].matchAll(/\.([\w-]+)/g)].map(match=>match[1]),id=/#([\w-]+)/.exec(parts[2])?.[1]||'';
168
+ return holders.some(element=>(!parts[1]||parts[1]==='*'||parts[1].toLowerCase()===element.tag)&&classes.every(name=>element.classes.includes(name))&&(!id||id===element.id));
169
+ };
170
+ // One pass over the stylesheet, comments skipped: each rule with the @media blocks around it.
171
+ const css=String(view?.css||''),around=[];let prelude='',narrowest=null;
172
+ for(let index=0;index<css.length;index++){
173
+ const char=css[index];
174
+ if(char==='/'&&css[index+1]==='*'){const close=css.indexOf('*/',index+2);if(close===-1)break;index=close+1;continue;}
175
+ if(char==='{'){
176
+ const head=prelude.trim();prelude='';
177
+ if(head.startsWith('@')){around.push(head);continue;}
178
+ const close=css.indexOf('}',index);if(close===-1)break;
179
+ const smallOnly=around.some(rule=>/^@media\b/i.test(rule)&&/max-width/i.test(rule));
180
+ const px=!smallOnly&&head.split(',').some(holds)?width(css.slice(index+1,close)):null;
181
+ if(px&&(!narrowest||px<narrowest.px))narrowest={px,where:head.slice(0,60)};
182
+ index=close;continue;
183
+ }
184
+ if(char==='}'){around.pop();prelude='';continue;}
185
+ if(char===';'&&!around.length){prelude='';continue;}
186
+ prelude+=char;
187
+ }
188
+ return narrowest;
189
+ }
190
+
138
191
  export function reviewViewDesign(view){
139
192
  if(!view)return {passed:true,issues:[],warnings:[],principles:BLOCK_INTERFACE_PRINCIPLES};
140
193
  const html=view.html||'',javascript=view.javascript||'',primary=html.replace(/<details\b[\s\S]*?<\/details>/gi,'');
@@ -157,6 +210,8 @@ export function reviewViewDesign(view){
157
210
  if(rareTool)warnings.push(`"${rareTool.slice(0,40)}" is its own section. Rare tools (update, schedule, history, reload) are named icon buttons beside the action (.sm-icon in .sm-actions), never sections under the work.`);
158
211
  const rows=topLevelRows(primary);
159
212
  if(rows>COMPACT_ROW_LIMIT)warnings.push(`The workspace has ${rows} visible rows besides the stage and may not fit one laptop screen (an estimate: this check counts rows and cannot measure height). Put short settings side by side in .sm-settings and secondary ones in a closed .sm-more.`);
213
+ const narrow=narrowScreen(view);
214
+ if(narrow)warnings.push(`The screen is held to ${Math.round(narrow.px)}px wide (${narrow.where}), so a wide Step shows mostly empty space, and StillMade's zoom cannot fill it. Drop the fixed or maximum width: the Block kit lays out the controls and the result across the width.`);
160
215
  warnings.push(...blockKitProblems(view).map(problem=>problem.message));
161
216
  return {passed:issues.length===0,issues,warnings,principles:BLOCK_INTERFACE_PRINCIPLES};
162
217
  }