creator-editing-studio 1.4.0 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,7 +21,7 @@ import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js';
21
21
  import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
22
22
  import {registerAppResource, registerAppTool, RESOURCE_MIME_TYPE} from '@modelcontextprotocol/ext-apps/server';
23
23
  import {z} from 'zod';
24
- import {buildManifest, checkPage, compileEdit, configFor, HERE, PAGE_LIMIT, PUBLIC, playerHtml, PROXY_TIERS, SRC, STUDIO} from './lib.mjs';
24
+ import {buildManifest, checkPage, compileEdit, configFor, HERE, PAGE_LIMIT, PUBLIC, playerHtml, playerJs, PROXY_TIERS, shellHtml, SRC, STUDIO} from './lib.mjs';
25
25
 
26
26
  const CONFIG = join(HERE, 'current.json');
27
27
  const cfg = () => JSON.parse(readFileSync(CONFIG, 'utf8'));
@@ -86,6 +86,9 @@ let lastBuilt = null;
86
86
  let readyBuilt = null;
87
87
  let pageHashes = {};
88
88
  const deltaFor = (b) => b && {...b.data, files: Object.fromEntries(Object.entries(b.data.files).filter(([k]) => pageHashes[k] !== b.hashes[k]))};
89
+ /** What every result carries since the shell (lib.mjs shellHtml): the edit, all its files, and the player itself. */
90
+ let shell = '';
91
+ const lpFor = (b) => b && {...deltaFor(b), ui: playerJs(uiHtml)};
89
92
  /** The newest preview_edit call. Each page carries the open that made it, so old players can see they are old. */
90
93
  let opens = 0;
91
94
  let latest = null;
@@ -124,9 +127,7 @@ function checkNote(report) {
124
127
 
125
128
  /** The page the app loads at startup, before any open: no video, only a line saying the preview opens below. */
126
129
  async function startupPage() {
127
- if (!uiHtml) [uiHtml, uiSig] = [await playerHtml(), 'built'];
128
- const data = {open: null, version: 0, canUndo: false, project: '', edit: '', composition: '', error: 'Opening the preview… it appears in the next card below.', code: null, files: {}};
129
- return uiHtml.replace('<head>', `<head><script>window.__LP_DATA=${JSON.stringify(data)}</script>`);
130
+ return (shell ||= await shellHtml());
130
131
  }
131
132
 
132
133
  /** The player page with the current edit and its footage inside it. `extra`: more files under assets/ to pack. */
@@ -168,6 +169,7 @@ async function page(extra = []) {
168
169
  const mb = (html.length / 1e6).toFixed(1);
169
170
  error = `This edit's preview is ${mb} MB even with small copies — the chat can only take ${PAGE_LIMIT / 1e6} MB. Use fewer cutout frames or shorter clips in the edit, then open the preview again.`;
170
171
  code = null;
172
+ lastBuilt = {data: {open: latest, version, canUndo: false, project: c.project, edit: c.edit, composition: c.composition, error, code, files: {}}, hashes: {}};
171
173
  html = uiHtml.replace('<head>', `<head><script>window.__LP_DATA=${JSON.stringify({open: latest, version, canUndo: false, project: c.project, edit: c.edit, composition: c.composition, error, code, files: {}}).replace(/</g, '\\u003c')}</script>`);
172
174
  }
173
175
  const mb = (html.length / 1e6).toFixed(1);
@@ -202,8 +204,9 @@ const addPage = (n) =>
202
204
  text: await (async () => {
203
205
  // Before any open, the only fetch is the app's own at startup, and it keeps that copy for the first open of
204
206
  // the chat — so it must never be a video (2026-11-05: a different video showed twice for the edit asked for).
205
- const html = !opens ? await startupPage() : (ready ?? (await page()));
206
- pageHashes = !opens ? {} : ((ready ? readyBuilt : lastBuilt)?.hashes ?? {});
207
+ // Always the shell: the player and the edit come inside each preview_edit result (lib.mjs shellHtml).
208
+ const html = await startupPage();
209
+ pageHashes = {};
207
210
  fetched.add(n);
208
211
  appendFileSync(join(HERE, '.reads.log'), `${new Date().toISOString()} FETCHED by the chat: slot ${n}, ${(html.length / 1e6).toFixed(1)}MB\n`);
209
212
  return html;
@@ -260,7 +263,55 @@ const previewTool = registerAppTool(
260
263
  addPage(slot);
261
264
  previewTool.update({_meta: uiMeta(slot)});
262
265
  }, 50);
263
- return {content: [{type: 'text', text: `Live preview of ${c.project} (version ${v}) sent to the chat, with this version's edit inside the result.${checkNote(report)} Before telling the person it's ready, check read_widget_context says "${c.project} v${v}".`}], structuredContent: {project: c.project, version: v, check: report, open: latest}, _meta: {lp: deltaFor(readyBuilt)}};
266
+ return {content: [{type: 'text', text: `Live preview of ${c.project} (version ${v}) sent to the chat, with this version's edit inside the result.${checkNote(report)} Before telling the person it's ready, check read_widget_context says "${c.project} v${v}".`}], structuredContent: {project: c.project, version: v, check: report, open: latest}, _meta: {lp: lpFor(readyBuilt)}};
267
+ },
268
+ );
269
+
270
+ /*
271
+ * SAVING IS A TOOL, NOT A NOTE (2026-11-06). The person's on-video changes (fonts, colours, icons, moves) used to
272
+ * reach the edit only if Claude copied a JSON out of a hidden note into tweaks.json — nothing checked it. Now the
273
+ * player calls this itself when the chat lets it (an app call), and otherwise asks Claude to call it with the JSON;
274
+ * either way it answers a fingerprint the player (and Claude) compare with what was sent, and the next card shows
275
+ * "your changes are kept" or "didn't save" (ui/app.tsx). Same fingerprint both sides: fnv() over the JSON text.
276
+ */
277
+ const fnv = (t) => {
278
+ let h = 0x811c9dc5;
279
+ for (let i = 0; i < t.length; i++) h = Math.imul(h ^ t.charCodeAt(i), 0x01000193) >>> 0;
280
+ return h.toString(36);
281
+ };
282
+ registerAppTool(
283
+ server,
284
+ 'preview_save',
285
+ {
286
+ title: 'Save the preview adjustments',
287
+ description:
288
+ "Write the person's live-preview adjustments (fonts, colours, icons, moves, replaced files) into the video's tweaks.json — the render reads the same file. " +
289
+ 'Call it when a message from the live preview asks you to, passing the JSON exactly as given. It answers a fingerprint: it must equal the one the message names; if not, call it again with the exact JSON.',
290
+ inputSchema: {
291
+ tweaks: z.string().describe('The adjustments JSON, exactly as the live preview gave it.'),
292
+ video: z.string().optional().describe('The video folder name; defaults to the one the preview last showed.'),
293
+ },
294
+ _meta: {ui: {resourceUri: uriFor(0), visibility: ['model', 'app']}},
295
+ },
296
+ async ({tweaks, video}) => {
297
+ const project = video ?? cfg().project;
298
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(project ?? '')) return {content: [{type: 'text', text: `"${project}" is not a video name.`}], isError: true};
299
+ let parsed;
300
+ try {
301
+ parsed = JSON.parse(tweaks);
302
+ } catch (e) {
303
+ return {content: [{type: 'text', text: `Not saved: that is not valid JSON (${e.message}). Pass the adjustments exactly as the preview gave them.`}], isError: true};
304
+ }
305
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {content: [{type: 'text', text: 'Not saved: the adjustments must be a JSON object.'}], isError: true};
306
+ const dir = videoDir(project);
307
+ if (!existsSync(dir)) return {content: [{type: 'text', text: `Not saved: there is no src/videos/${project}.`}], isError: true};
308
+ const text = JSON.stringify(parsed);
309
+ writeFileSync(join(dir, 'tweaks.json'), JSON.stringify(parsed, null, 2) + '\n');
310
+ const hash = fnv(text);
311
+ const count = Object.keys(parsed).length;
312
+ log(`saved ${count} adjusted piece(s) for ${project}: #${hash}`);
313
+ appendFileSync(join(HERE, '.reads.log'), `${new Date().toISOString()} SAVED ${project} #${hash} (${count})\n`);
314
+ return {content: [{type: 'text', text: `Saved ${count} adjusted piece(s) to src/videos/${project}/tweaks.json — fingerprint #${hash}.`}], structuredContent: {project, hash, count}};
264
315
  },
265
316
  );
266
317