stillmade-sdk 0.1.31 → 0.1.32

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/PASTE_TO_LLM.md CHANGED
@@ -162,7 +162,7 @@ Every Block lists its separate tools in manifest.features, one entry per tool or
162
162
 
163
163
  ### Port meanings
164
164
 
165
- For each newly created Block, declare the meaning of every input and output in its manifest contract, not just a physical type. Each input needs semantic, required:boolean and a short plain-language description. Each output needs semantic and a short description. Use lowercase snake_case or dotted semantic names, such as company_profile or email_subject; different meanings are not interchangeable merely because both are text. If an existing role is present, semantic must equal that role. Reuse an exact existing meaning only when the information actually has that meaning. Describe structured fields with schema when their shape matters: for example {name:"string",website:"string",industry:"string?"}. Supported schema fields are string/text, number, integer, boolean, object, json, arrays of those types, and optional fields with ?. Declare sources only when a requirement restricts where its answer may come from: upstream, project, adapter, chat, default, user or external. Do not mark a credential as ordinary text or put secrets in defaults. Contracts describe information; they never authorize network access, model calls, project edits, credit spending or new runtime APIs. Keep existing imported, installed and base Block contracts unchanged; do not silently relabel old published contracts to make a connection appear compatible. New contracts must match actual code and fixtures.
165
+ For each newly created Block, declare the meaning of every input and output in its manifest contract, not just a physical type. Each input needs semantic, required:boolean and a short plain-language description. Each output needs semantic and a short description. Use lowercase snake_case or dotted semantic names, such as company_profile or email_subject; different meanings are not interchangeable merely because both are text. If an existing role is present, semantic must equal that role. Reuse an exact existing meaning only when the information actually has that meaning. Describe structured fields with schema when their shape matters, and always on a json or object output (import refuses one without it): for example {name:"string",website:"string",industry:"string?"}. Supported schema fields are string/text, number, integer, boolean, object, json, arrays of those types, and optional fields with ?. A value of a structured type (transcript, script, brief, shot-plan and the like) is an object with schemaVersion:1, in code and fixtures; a transcript as the Transcription Block hands it on is {schemaVersion:1,text:"...",words:[{text:"Today",start:0,end:0.4}]}, times in seconds. Declare sources only when a requirement restricts where its answer may come from: upstream, project, adapter, chat, default, user or external. Do not mark a credential as ordinary text or put secrets in defaults. Contracts describe information; they never authorize network access, model calls, project edits, credit spending or new runtime APIs. Keep existing imported, installed and base Block contracts unchanged; do not silently relabel old published contracts to make a connection appear compatible. New contracts must match actual code and fixtures.
166
166
  Custom inheritance is persisted in manifest.semantics, an array of {name,description?,parents?}. Use namespaced custom names, at most 64 definitions per Block, eight parents per definition, and 500 description characters. Never redefine unnamespaced standard meanings, create cycles, or combine conflicting parent sets. Preserve effective inherited meanings of all existing base inputs, outputs and accepts declarations; adding or changing a parent is a contract change even when port labels stay identical. New independent ports may declare new meanings.
167
167
  In isolated computation code, StillMade.emit(outputName,value) collects declared final outputs without a return object; emit each name once and never combine it with a returned output object or job.pending result. StillMade.resolveRequirement(inputName) reads the fixed host-validated declared input snapshot as {resolved:true,requirement,value}, or {resolved:false,requirement} for an absent optional input. StillMade.getProjectContext() returns only context fields explicitly bound in manifest.inputs with their matching context.FIELD.read permissions; getProjectContext(field) reads one declared field and rejects undeclared fields. These snapshot readers do not run the resolver again, request Chat/user answers, read extra project data, expose credentials, write projects or grant permissions. StillMade.validate(value,schema) checks JSON values against SDK semantic schemas and returns a boolean; it does not replace host output validation, prove semantic meaning or authorize execution. These methods are not available in view.javascript.
168
168
 
package/SDK.md CHANGED
@@ -7401,7 +7401,7 @@ Every Block lists its separate tools in manifest.features, one entry per tool or
7401
7401
 
7402
7402
  ### Port meanings
7403
7403
 
7404
- For each newly created Block, declare the meaning of every input and output in its manifest contract, not just a physical type. Each input needs semantic, required:boolean and a short plain-language description. Each output needs semantic and a short description. Use lowercase snake_case or dotted semantic names, such as company_profile or email_subject; different meanings are not interchangeable merely because both are text. If an existing role is present, semantic must equal that role. Reuse an exact existing meaning only when the information actually has that meaning. Describe structured fields with schema when their shape matters: for example {name:"string",website:"string",industry:"string?"}. Supported schema fields are string/text, number, integer, boolean, object, json, arrays of those types, and optional fields with ?. Declare sources only when a requirement restricts where its answer may come from: upstream, project, adapter, chat, default, user or external. Do not mark a credential as ordinary text or put secrets in defaults. Contracts describe information; they never authorize network access, model calls, project edits, credit spending or new runtime APIs. Keep existing imported, installed and base Block contracts unchanged; do not silently relabel old published contracts to make a connection appear compatible. New contracts must match actual code and fixtures.
7404
+ For each newly created Block, declare the meaning of every input and output in its manifest contract, not just a physical type. Each input needs semantic, required:boolean and a short plain-language description. Each output needs semantic and a short description. Use lowercase snake_case or dotted semantic names, such as company_profile or email_subject; different meanings are not interchangeable merely because both are text. If an existing role is present, semantic must equal that role. Reuse an exact existing meaning only when the information actually has that meaning. Describe structured fields with schema when their shape matters, and always on a json or object output (import refuses one without it): for example {name:"string",website:"string",industry:"string?"}. Supported schema fields are string/text, number, integer, boolean, object, json, arrays of those types, and optional fields with ?. A value of a structured type (transcript, script, brief, shot-plan and the like) is an object with schemaVersion:1, in code and fixtures; a transcript as the Transcription Block hands it on is {schemaVersion:1,text:"...",words:[{text:"Today",start:0,end:0.4}]}, times in seconds. Declare sources only when a requirement restricts where its answer may come from: upstream, project, adapter, chat, default, user or external. Do not mark a credential as ordinary text or put secrets in defaults. Contracts describe information; they never authorize network access, model calls, project edits, credit spending or new runtime APIs. Keep existing imported, installed and base Block contracts unchanged; do not silently relabel old published contracts to make a connection appear compatible. New contracts must match actual code and fixtures.
7405
7405
  Custom inheritance is persisted in manifest.semantics, an array of {name,description?,parents?}. Use namespaced custom names, at most 64 definitions per Block, eight parents per definition, and 500 description characters. Never redefine unnamespaced standard meanings, create cycles, or combine conflicting parent sets. Preserve effective inherited meanings of all existing base inputs, outputs and accepts declarations; adding or changing a parent is a contract change even when port labels stay identical. New independent ports may declare new meanings.
7406
7406
  In isolated computation code, StillMade.emit(outputName,value) collects declared final outputs without a return object; emit each name once and never combine it with a returned output object or job.pending result. StillMade.resolveRequirement(inputName) reads the fixed host-validated declared input snapshot as {resolved:true,requirement,value}, or {resolved:false,requirement} for an absent optional input. StillMade.getProjectContext() returns only context fields explicitly bound in manifest.inputs with their matching context.FIELD.read permissions; getProjectContext(field) reads one declared field and rejects undeclared fields. These snapshot readers do not run the resolver again, request Chat/user answers, read extra project data, expose credentials, write projects or grant permissions. StillMade.validate(value,schema) checks JSON values against SDK semantic schemas and returns a boolean; it does not replace host output validation, prove semantic meaning or authorize execution. These methods are not available in view.javascript.
7407
7407
 
@@ -12323,7 +12323,7 @@ Every Block lists its separate tools in manifest.features, one entry per tool or
12323
12323
 
12324
12324
  ### Port meanings
12325
12325
 
12326
- For each newly created Block, declare the meaning of every input and output in its manifest contract, not just a physical type. Each input needs semantic, required:boolean and a short plain-language description. Each output needs semantic and a short description. Use lowercase snake_case or dotted semantic names, such as company_profile or email_subject; different meanings are not interchangeable merely because both are text. If an existing role is present, semantic must equal that role. Reuse an exact existing meaning only when the information actually has that meaning. Describe structured fields with schema when their shape matters: for example {name:"string",website:"string",industry:"string?"}. Supported schema fields are string/text, number, integer, boolean, object, json, arrays of those types, and optional fields with ?. Declare sources only when a requirement restricts where its answer may come from: upstream, project, adapter, chat, default, user or external. Do not mark a credential as ordinary text or put secrets in defaults. Contracts describe information; they never authorize network access, model calls, project edits, credit spending or new runtime APIs. Keep existing imported, installed and base Block contracts unchanged; do not silently relabel old published contracts to make a connection appear compatible. New contracts must match actual code and fixtures.
12326
+ For each newly created Block, declare the meaning of every input and output in its manifest contract, not just a physical type. Each input needs semantic, required:boolean and a short plain-language description. Each output needs semantic and a short description. Use lowercase snake_case or dotted semantic names, such as company_profile or email_subject; different meanings are not interchangeable merely because both are text. If an existing role is present, semantic must equal that role. Reuse an exact existing meaning only when the information actually has that meaning. Describe structured fields with schema when their shape matters, and always on a json or object output (import refuses one without it): for example {name:"string",website:"string",industry:"string?"}. Supported schema fields are string/text, number, integer, boolean, object, json, arrays of those types, and optional fields with ?. A value of a structured type (transcript, script, brief, shot-plan and the like) is an object with schemaVersion:1, in code and fixtures; a transcript as the Transcription Block hands it on is {schemaVersion:1,text:"...",words:[{text:"Today",start:0,end:0.4}]}, times in seconds. Declare sources only when a requirement restricts where its answer may come from: upstream, project, adapter, chat, default, user or external. Do not mark a credential as ordinary text or put secrets in defaults. Contracts describe information; they never authorize network access, model calls, project edits, credit spending or new runtime APIs. Keep existing imported, installed and base Block contracts unchanged; do not silently relabel old published contracts to make a connection appear compatible. New contracts must match actual code and fixtures.
12327
12327
  Custom inheritance is persisted in manifest.semantics, an array of {name,description?,parents?}. Use namespaced custom names, at most 64 definitions per Block, eight parents per definition, and 500 description characters. Never redefine unnamespaced standard meanings, create cycles, or combine conflicting parent sets. Preserve effective inherited meanings of all existing base inputs, outputs and accepts declarations; adding or changing a parent is a contract change even when port labels stay identical. New independent ports may declare new meanings.
12328
12328
  In isolated computation code, StillMade.emit(outputName,value) collects declared final outputs without a return object; emit each name once and never combine it with a returned output object or job.pending result. StillMade.resolveRequirement(inputName) reads the fixed host-validated declared input snapshot as {resolved:true,requirement,value}, or {resolved:false,requirement} for an absent optional input. StillMade.getProjectContext() returns only context fields explicitly bound in manifest.inputs with their matching context.FIELD.read permissions; getProjectContext(field) reads one declared field and rejects undeclared fields. These snapshot readers do not run the resolver again, request Chat/user answers, read extra project data, expose credentials, write projects or grant permissions. StillMade.validate(value,schema) checks JSON values against SDK semantic schemas and returns a boolean; it does not replace host output validation, prove semantic meaning or authorize execution. These methods are not available in view.javascript.
12329
12329
 
@@ -181,7 +181,7 @@ export default {
181
181
  "runtime": "frame",
182
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=\"What it is, its details, who it is for, and how to reach you.\"></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
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 // render() writes the phase's status, so the error goes after it and stays.\n state.busy = false; render();\n status(message(error), 'error', ''); 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"
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
185
  },
186
186
  "tests": [
187
187
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stillmade-sdk",
3
- "version": "0.1.31",
3
+ "version": "0.1.32",
4
4
  "description": "StillMade Block SDK, CLI and examples",
5
5
  "license": "PolyForm-Shield-1.0.0",
6
6
  "type": "module",
@@ -222,7 +222,7 @@ export function validateManifest(manifest, { builtIn = false } = {}) {
222
222
  check(['project', 'network', 'connections', 'filesystem', 'secrets', 'capabilities', 'actions', 'desktop'].includes(key), `permissions.${key}`, 'Unknown permission family');
223
223
  // Network entries may be {origin, auth} objects; every other family is a string list.
224
224
  check(Array.isArray(scopes) && scopes.every(s => typeof s === 'string' || key === 'network' && plain(s)), `permissions.${key}`, 'Expected string array');
225
- if (Array.isArray(scopes)) check(key === 'desktop' ? scopes.length<=DESKTOP_PERMISSIONS.length && new Set(scopes).size===scopes.length && scopes.every(s=>DESKTOP_PERMISSIONS.includes(s)) : key === 'project' ? scopes.every(s => SCOPES.includes(s)) : key === 'actions' ? ['javascript','module'].includes(manifest.runtime)&&scopes.length<=PORTABLE_ACTIONS.length&&new Set(scopes).size===scopes.length&&scopes.every(s=>PORTABLE_ACTIONS.includes(s)) : key === 'capabilities' ? (manifest.runtime === 'module' ? scopes.length >= 1 && scopes.length <= CAPABILITY_RUN_OPERATIONS.length && new Set(scopes).size === scopes.length && scopes.every(s => CAPABILITY_RUN_OPERATIONS.includes(s)) : scopes.length === 1 && (manifest.runtime === 'comfyui' && scopes[0] === 'comfyui.execute' || manifest.runtime === 'capability' && HOSTED_OPERATION_NAMES.includes(scopes[0]))) : key === 'network' && manifest.runtime === 'module' ? networkValid(scopes) : key === 'connections' ? manifest.runtime === 'module' && connectionsValid(scopes) : scopes.length === 0, `permissions.${key}`, key === 'connections' ? 'Unsupported permission: only module Blocks declare connections, up to 8 published adapters by app ID such as app:openapi:notion' : key === 'network' ? (manifest.runtime === 'module' ? `Declare network origins as https origins, optionally with {origin, auth}: ${networkProblem(scopes)}` : 'Unsupported permission: only module Blocks declare network origins; other runtimes reach services through hosted operations and connections') : 'Unsupported permission; Blocks have no ambient filesystem or secrets');
225
+ if (Array.isArray(scopes)) check(key === 'desktop' ? scopes.length<=DESKTOP_PERMISSIONS.length && new Set(scopes).size===scopes.length && scopes.every(s=>DESKTOP_PERMISSIONS.includes(s)) : key === 'project' ? scopes.every(s => SCOPES.includes(s)) : key === 'actions' ? ['javascript','module'].includes(manifest.runtime)&&scopes.length<=PORTABLE_ACTIONS.length&&new Set(scopes).size===scopes.length&&scopes.every(s=>PORTABLE_ACTIONS.includes(s)) : key === 'capabilities' ? (manifest.runtime === 'module' ? scopes.length >= 1 && scopes.length <= CAPABILITY_RUN_OPERATIONS.length && new Set(scopes).size === scopes.length && scopes.every(s => CAPABILITY_RUN_OPERATIONS.includes(s)) : scopes.length === 1 && (manifest.runtime === 'comfyui' && scopes[0] === 'comfyui.execute' || manifest.runtime === 'capability' && HOSTED_OPERATION_NAMES.includes(scopes[0]))) : key === 'network' && manifest.runtime === 'module' ? networkValid(scopes) : key === 'connections' ? manifest.runtime === 'module' && connectionsValid(scopes) : scopes.length === 0, `permissions.${key}`, key === 'connections' ? 'Unsupported permission: only module Blocks declare connections, up to 8 published adapters by app ID such as app:openapi:notion' : key === 'network' ? (manifest.runtime === 'module' ? `Declare network origins as https origins, optionally with {origin, auth}: ${networkProblem(scopes)}` : 'Unsupported permission: only module Blocks declare network origins; other runtimes reach services through hosted operations and connections') : key === 'capabilities' ? (['module','capability','comfyui'].includes(manifest.runtime) ? 'Declare each hosted operation once, from the supported list (a capability or ComfyUI Block declares exactly one)' : 'Unsupported permission: only capability, ComfyUI and module Blocks declare hosted operations. A javascript or recipe Block declares none: remove permissions.capabilities, or make the AI work its own capability Block') : 'Unsupported permission; Blocks have no ambient filesystem or secrets');
226
226
  }
227
227
  if(Object.values(plain(manifest.outputs)?manifest.outputs:{}).some(port=>['workspace-edit','workspace-edit[]'].includes(port?.type))) {
228
228
  check(WORKSPACE_EDIT_FIELDS.some(field=>canProposeWorkspace(manifest,field)), 'permissions.project', 'Workspace edits require matching context.FIELD.read and workspace.FIELD.propose permissions');
@@ -84,7 +84,9 @@ export function semanticPortErrors(port,{key,direction='inputs'}={}) {
84
84
  if(port.semantic!==undefined && direction==='inputs' && (typeof port.required!=='boolean' || typeof port.description!=='string'))errors.push('Describe this input and whether it is required.');
85
85
  if(port.accepts!==undefined && (!Array.isArray(port.accepts) || !port.accepts.every(name=>typeof name==='string' && SEMANTIC_NAME.test(name))))errors.push('Declare valid accepted meanings.');
86
86
  if(port.sources!==undefined && (!Array.isArray(port.sources) || !port.sources.every(source=>REQUIREMENT_SOURCES.includes(source))))errors.push('Choose supported information sources.');
87
- if(port.schema!=null && !semanticSchemaValid(port.schema))errors.push('Describe the required fields with a supported schema.');
87
+ // The format in the message itself, so a builder's repair can follow it (October 20, 2026: three
88
+ // repairs in a row resent a JSON-Schema-style {type,items} the SDK does not read).
89
+ if(port.schema!=null && !semanticSchemaValid(port.schema))errors.push('Describe the required fields with a supported schema: field names mapped to string, text, number, integer, boolean, object or json, a list as "type[]" or [ {fields} ], optional with "?"; for example [{"reference":"string","startSeconds":"number","note":"text?"}]. Not JSON Schema ({"type":…,"items":…}).');
88
90
  if(port.freshnessPolicy!==undefined && (!record(port.freshnessPolicy) || !Object.keys(port.freshnessPolicy).every(name=>name==='maxAgeMs') || !Number.isFinite(port.freshnessPolicy.maxAgeMs) || port.freshnessPolicy.maxAgeMs<0))errors.push('Declare how long this information stays current.');
89
91
  if(port.cardinality!==undefined && !['one','many'].includes(port.cardinality))errors.push('Choose one value or many values.');
90
92
  if(port.batchable!==undefined && typeof port.batchable!=='boolean')errors.push('Declare whether this input supports batches.');