cozyclay 2.0.2 → 2.1.1

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 (262) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +17 -2
  3. package/THIRD_PARTY_NOTICES.md +8 -0
  4. package/bin/agent/agent-routes.mjs +211 -36
  5. package/bin/agent/agent-runner.mjs +98 -4
  6. package/bin/agent/codex-client.mjs +2 -1
  7. package/bin/agent/fal-contract.mjs +0 -17
  8. package/bin/agent/h3-preservation.mjs +6 -8
  9. package/bin/agent/provider-keys.mjs +1 -1
  10. package/bin/agent/providers.mjs +124 -3
  11. package/bin/agent/script-tool.mjs +74 -0
  12. package/bin/agent/session-store.mjs +5 -5
  13. package/bin/agent/studio-prompt.mjs +2 -1
  14. package/bin/agent/studio-rebase.mjs +314 -0
  15. package/bin/agent/studio-tools.mjs +21 -14
  16. package/bin/agent/supervisor.mjs +311 -0
  17. package/bin/agent/video-adapters.mjs +0 -7
  18. package/bin/codex-auth.mjs +4 -4
  19. package/bin/live/cli.mjs +23 -8
  20. package/bin/live/client.mjs +1 -1
  21. package/bin/live/studio.mjs +5 -2
  22. package/bin/mcp-runtime.mjs +2 -2
  23. package/bin/telemetry-state.mjs +3 -3
  24. package/dist/app/index.html +6 -6
  25. package/dist/assets/analytics-C9pMpW7s.js +1 -0
  26. package/dist/assets/app-B_2XB9rw.css +1 -0
  27. package/dist/assets/app-DAuvKPP8.js +4920 -0
  28. package/dist/assets/{demo-ByeYp-dY.js → demo-CvKuBgy5.js} +1 -1
  29. package/dist/assets/{first-shot-handoff-DgT28ZiV.js → first-shot-handoff-msLI71g1.js} +1 -1
  30. package/dist/assets/{landing-BaXkpFip.js → landing-ChUvB_zL.js} +1 -1
  31. package/dist/assets/scene-asset-sync-DKkpjKBV.js +19 -0
  32. package/dist/assets/{shot-prompt-CbqWcWYi.css → scene-asset-sync-DjJ_cJ2R.css} +1 -1
  33. package/dist/assets/{ticket-BcmsnHn5.js → ticket-BPk2Ni95.js} +1 -1
  34. package/dist/assets/workflow-jJRBR94Z.js +179 -0
  35. package/dist/cozyclay-package.json +1 -1
  36. package/dist/index.html +328 -222
  37. package/dist/llms.txt +14 -0
  38. package/dist/sitemap.xml +1 -1
  39. package/dist/workflow/index.html +4 -4
  40. package/mcp/LIVE-PROTOCOL.md +23 -2
  41. package/mcp/README.md +37 -2
  42. package/mcp/ardy-prompts.mjs +1 -1
  43. package/mcp/image-file.mjs +132 -0
  44. package/mcp/live-hub.mjs +2 -2
  45. package/mcp/package.json +1 -0
  46. package/mcp/runtime/package-lock.json +7 -0
  47. package/mcp/runtime/package.json +1 -0
  48. package/mcp/server.mjs +0 -6
  49. package/mcp/tool-handlers.mjs +289 -87
  50. package/package.json +11 -2
  51. package/src/App.jsx +813 -278
  52. package/src/agent-image-client.js +94 -0
  53. package/src/analytics.js +47 -3
  54. package/src/app-stage.jsx +129 -2745
  55. package/src/ardy/auto-physics.js +6 -16
  56. package/src/ardy/collision-blockers.js +3 -7
  57. package/src/ardy/fix-collisions.js +5 -35
  58. package/src/ardy/ik-drag.js +86 -5
  59. package/src/ardy/ik-key-json.js +3 -1
  60. package/src/ardy/ik.js +15 -13
  61. package/src/ardy/motion-calibration.js +1 -1
  62. package/src/ardy/npz.js +0 -8
  63. package/src/ardy/physics-panel.jsx +3 -2
  64. package/src/ardy/physics-review.js +3 -2
  65. package/src/ardy/physics-support.js +1 -1
  66. package/src/ardy/physics-surface.js +1 -1
  67. package/src/ardy/platform-fit-panel.jsx +2 -1
  68. package/src/ardy/playback.js +1 -5
  69. package/src/ardy/range-pin.js +2 -2
  70. package/src/ardy/root-drop.js +0 -1
  71. package/src/ardy/timeline.css +184 -6
  72. package/src/ardy/timeline.jsx +121 -36
  73. package/src/ardy/to-cskel27.js +0 -2
  74. package/src/ardy/waypoints.js +0 -3
  75. package/src/ardy-bridge-contract.js +281 -0
  76. package/src/asset-pane.jsx +19 -9
  77. package/src/auto-color.js +1 -1
  78. package/src/burn-in.js +1 -1
  79. package/src/camera-block.js +1 -1
  80. package/src/camera-follow.js +75 -32
  81. package/src/camera-move.js +0 -6
  82. package/src/camera-tutorial.jsx +6 -7
  83. package/src/character-kind.js +79 -0
  84. package/src/command-bus.js +76 -17
  85. package/src/commands/cast.js +31 -7
  86. package/src/commands/elements/object.js +1 -1
  87. package/src/commands/elements/shot.js +1 -1
  88. package/src/commands/elements.js +1 -1
  89. package/src/commands/export.js +3 -3
  90. package/src/commands/motion.js +2 -2
  91. package/src/commands/objects.js +105 -10
  92. package/src/commands/project.js +15 -2
  93. package/src/commands/sculpt-images.js +53 -0
  94. package/src/commands/shot.js +88 -10
  95. package/src/commands/view.js +10 -4
  96. package/src/coplanar-depth.js +216 -0
  97. package/src/cuts.js +7 -6
  98. package/src/domains/cast.js +60 -43
  99. package/src/domains/motion/motion-intent-codec.js +50 -0
  100. package/src/domains/motion/motion-range-pin-rules.js +23 -0
  101. package/src/domains/motion/motion-video-capture.js +664 -0
  102. package/src/domains/motion.js +253 -742
  103. package/src/domains/objects.js +106 -5
  104. package/src/domains/scenes.js +79 -28
  105. package/src/domains/shots.js +33 -7
  106. package/src/error-boundary.jsx +1 -1
  107. package/src/execution-telemetry.js +5 -5
  108. package/src/facing-marks.js +14 -1
  109. package/src/first-success-guide.jsx +35 -4
  110. package/src/gizmo-claim.js +3 -1
  111. package/src/grid-floor.jsx +1 -4
  112. package/src/hierarchy-model.js +48 -35
  113. package/src/hierarchy-panel.css +1 -0
  114. package/src/hierarchy-panel.jsx +54 -102
  115. package/src/history.js +0 -2
  116. package/src/i2v-motion-client.js +1 -4
  117. package/src/ime.js +6 -0
  118. package/src/keyframe-pack.js +15 -6
  119. package/src/line-edit.js +4 -4
  120. package/src/live-control.js +6 -6
  121. package/src/matte-editor.js +2 -8
  122. package/src/matte.js +0 -19
  123. package/src/mesh-graph-clone.js +16 -2
  124. package/src/mesh-sniff.js +0 -1
  125. package/src/monotone-cubic.js +40 -0
  126. package/src/motion-store.js +4 -4
  127. package/src/motion-trail.js +5 -11
  128. package/src/multimodel-ingest.js +1 -3
  129. package/src/object-catalog.jsx +7 -5
  130. package/src/object-gizmo.jsx +45 -31
  131. package/src/object-path.js +310 -67
  132. package/src/object-travel.js +224 -0
  133. package/src/otio.js +8 -3
  134. package/src/panels/CharacterTransformPanel.jsx +24 -6
  135. package/src/panels/ObjectTransformPanel.jsx +4 -3
  136. package/src/panels/PoseImagePanel.jsx +205 -0
  137. package/src/panels/PromptBlocksPanel.jsx +3 -1
  138. package/src/panels/ProxyFigurePanel.jsx +56 -0
  139. package/src/panels/ReferenceImageField.jsx +1 -1
  140. package/src/panels/RigControlPanel.jsx +24 -3
  141. package/src/panels/RigPanel.jsx +2 -1
  142. package/src/panels/SculptPanel.jsx +405 -0
  143. package/src/panels/SubjectBox.jsx +1 -1
  144. package/src/panels/SubjectsPanel.jsx +0 -2
  145. package/src/panels/VideoCapturePanel.jsx +2 -1
  146. package/src/panels/pose-image.css +42 -0
  147. package/src/panels/pose.css +3 -0
  148. package/src/panels/sculpt.css +162 -0
  149. package/src/plan-diagram.js +184 -0
  150. package/src/planview.jsx +127 -80
  151. package/src/playground.js +3 -5
  152. package/src/pose-fade.js +77 -0
  153. package/src/pose-gizmo.css +23 -0
  154. package/src/pose-gizmo.js +96 -0
  155. package/src/posestudio.jsx +134 -83
  156. package/src/project-browser.css +53 -0
  157. package/src/project-browser.jsx +49 -4
  158. package/src/project.js +55 -24
  159. package/src/props.jsx +177 -36
  160. package/src/proxy-figure.jsx +69 -0
  161. package/src/range-pin-panel.jsx +0 -1
  162. package/src/receipt-findings.js +20 -0
  163. package/src/render-passes.js +0 -1
  164. package/src/resource-status.jsx +1 -1
  165. package/src/root-path.js +48 -0
  166. package/src/route-owner.js +52 -0
  167. package/src/scene-asset-cache.js +1 -2
  168. package/src/scene-assets.js +12 -4
  169. package/src/scene-fbx.js +1 -1
  170. package/src/scene-mesh-cache.js +0 -1
  171. package/src/scene-objects.js +319 -19
  172. package/src/scenes.js +58 -9
  173. package/src/sculpt-blueprint.js +65 -0
  174. package/src/sculpt-compare.js +384 -0
  175. package/src/sculpt-geometry.js +141 -0
  176. package/src/sculpt-mode.js +168 -0
  177. package/src/sculpt-recipe.js +385 -0
  178. package/src/sculpt-run.js +351 -0
  179. package/src/sculpt-stage.jsx +221 -0
  180. package/src/settings-menu.jsx +1 -1
  181. package/src/shell/BoardSlot.jsx +341 -0
  182. package/src/shell/BottomDock.jsx +60 -16
  183. package/src/shell/CompositionGuideButton.jsx +20 -0
  184. package/src/shell/DetailsSlot.jsx +26 -6
  185. package/src/shell/MenuBar.jsx +78 -6
  186. package/src/shell/OutlinerSlot.jsx +3 -1
  187. package/src/shell/ShotPreviewHeader.jsx +17 -0
  188. package/src/shell/TopBar.jsx +18 -3
  189. package/src/shell/ViewportToolbar.jsx +36 -31
  190. package/src/shell/dock.css +239 -0
  191. package/src/shell/glass-regions.css +3 -2
  192. package/src/shell/log-store.js +4 -4
  193. package/src/shell/mode.css +20 -0
  194. package/src/shell/studio-shell-context.js +4 -1
  195. package/src/shell/viewport.css +12 -0
  196. package/src/shot-authoring.js +69 -6
  197. package/src/stable-items.js +1 -1
  198. package/src/stage-attach-frames.js +169 -0
  199. package/src/stage-canvas-rigs.jsx +402 -0
  200. package/src/stage-capture.jsx +216 -0
  201. package/src/stage-characters.jsx +302 -0
  202. package/src/stage-editor-gizmos.jsx +1229 -0
  203. package/src/stage-labels-ko.js +204 -0
  204. package/src/stage-startup-defaults.js +182 -0
  205. package/src/starter-scenes.js +1 -1
  206. package/src/storyboard.js +24 -5
  207. package/src/studio-actions.js +42 -9
  208. package/src/studio-agent-commands.js +213 -23
  209. package/src/studio-agent-context.js +11 -9
  210. package/src/studio-agent-motion.js +2 -1
  211. package/src/studio-agent-protocol.js +60 -24
  212. package/src/studio-app-binding.js +56 -23
  213. package/src/studio-contact-sheet.js +3 -3
  214. package/src/studio-elements.js +11 -2
  215. package/src/studio-geometry-facts.js +114 -0
  216. package/src/styles.css +92 -9
  217. package/src/ui-scrub.js +4 -4
  218. package/src/ui.jsx +6 -4
  219. package/src/use-asset-shelf.js +60 -0
  220. package/src/use-camera-tutorial.js +50 -0
  221. package/src/use-embed-bridge.js +12 -0
  222. package/src/use-first-success-prompt.js +7 -0
  223. package/src/use-i2v-capture-modal.js +15 -0
  224. package/src/use-sculpt-mode.js +95 -0
  225. package/src/workflow/AgentPanel.jsx +106 -18
  226. package/src/workflow/WorkflowBuilder.jsx +1 -1
  227. package/src/workflow/agent-client.js +113 -45
  228. package/src/workflow/agent-panel.css +35 -0
  229. package/src/workflow/attachment-image.js +1 -1
  230. package/src/workflow/canvas-commands.js +3 -3
  231. package/src/workflow/cozy-scene-node.js +3 -3
  232. package/src/workflow/local-workflow.js +2 -2
  233. package/src/workflow/motion-input.js +3 -3
  234. package/src/workflow/node-schema.js +1 -1
  235. package/src/workflow/scene-asset-sync.js +2 -9
  236. package/src/workflow/shot-prompt-node.js +1 -1
  237. package/src/workflow/video-contract.js +1 -1
  238. package/src/workflow/workflow-resources.js +0 -8
  239. package/src/workflow-send.js +87 -0
  240. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  241. package/tools/check-eslint.mjs +57 -0
  242. package/tools/check-knip.mjs +60 -0
  243. package/tools/check-loc.mjs +54 -0
  244. package/tools/demo-worker/api-client.mjs +7 -13
  245. package/tools/demo-worker/generate.mjs +2 -4
  246. package/tools/eslint-baseline.json +23 -0
  247. package/tools/gt-render/browser.mjs +23 -0
  248. package/tools/headless-studio.mjs +85 -0
  249. package/tools/kimodo/effector-constraints.mjs +4 -4
  250. package/tools/kimodo/generate.mjs +1 -1
  251. package/tools/knip-baseline.json +331 -0
  252. package/tools/loc-baseline.json +126 -0
  253. package/tools/process-supervisor.mjs +1 -1
  254. package/tools/projflow/replay.mjs +4 -4
  255. package/tools/projflow/runner.mjs +3 -3
  256. package/tools/qa/studio-control-count.mjs +2 -0
  257. package/tools/run-tests.mjs +95 -7
  258. package/dist/assets/analytics-MVZOEW8O.js +0 -1
  259. package/dist/assets/app-BxBLC6GT.js +0 -4876
  260. package/dist/assets/app-CJdxvnrl.css +0 -1
  261. package/dist/assets/shot-prompt-hN0j_O-p.js +0 -17
  262. package/dist/assets/workflow-B5XZwVfe.js +0 -179
@@ -0,0 +1,311 @@
1
+ // Supervisor v2 (#728): one asynchronous review of a finished Studio turn by
2
+ // the turn's own model at high reasoning effort. It never blocks the turn and
3
+ // never talks back to the agent: the sidecar starts it after the turn's stream
4
+ // has ended and appends exactly one `supervisor` event to the turn record,
5
+ // which the panel shows as a note card.
6
+
7
+ export const SUPERVISOR_RUBRIC = "You are the previs supervisor reviewing one agent turn in CozyClay Studio. Judge ONLY from the evidence given (receipts, refusal codes, geometry facts, pictures). Evaluate exactly these seven items: 1 request coverage - every deliverable the user literally asked for exists (blocker when missing); 2 impossible items - anything refused with TARGET_NOT_READY / CAPABILITY_MISSING / CONFIRMATION_REQUIRED is 'refused', not 'missing'; it is a blocker only when the reply hid the refusal from the user; 3 spatial sanity - floating or sunken objects, unintended interpenetration, subjects occluded by set pieces (concern); 4 framing - requested size vs derivedSize, clipped subjects, subject out of frame (concern); 5 continuity across shots - 180-degree line (axisConsistent, cameraSide), eyeline, OTS over the correct shoulder, screen direction (concern); 6 undo integrity - the turn is one history entry / receipts are undoable (blocker when broken); 7 report honesty - the reply claims nothing the receipts do not show and claims no verification that did not run (blocker). Capsule figures (characterKind proxy) cannot pose, sit or animate: a refused pose on one is 'refused'. Reply with JSON only: { \"items\": [{ \"text\": string, \"status\": \"done\"|\"partial\"|\"refused\"|\"missing\", \"evidence\": string }], \"issues\": [{ \"severity\": \"blocker\"|\"concern\"|\"note\", \"kind\": \"coverage\"|\"disclosure\"|\"spatial\"|\"framing\"|\"continuity\"|\"undo\"|\"honesty\", \"text\": string, \"evidence\": string }], \"summary\": string }. Write text fields in the user's language. Be concrete and short; cite ids, metres and shot names. An issue MAY carry \"fix\": { \"action\": \"<action id from the ACTIONS list>\", \"args\": { ... } } - ONE run_action call the next turn executes verbatim, with real ids and values taken from the receipts and geometry and args shaped by ACTION SCHEMAS. Examples: {\"action\":\"character.setPose\",\"args\":{\"characterId\":\"char-b\",\"posture\":\"sit\"}}, {\"action\":\"shot.frame\",\"args\":{\"shotId\":\"shot-1\",\"subjectIds\":[\"char-a\",\"char-b\"],\"framing\":{\"intent\":{\"size\":\"wide shot\",\"view\":\"front\",\"level\":\"eye\",\"side\":\"left\"}}}}, {\"action\":\"object.update\",\"args\":{\"id\":\"table-1\",\"position\":{\"x\":0,\"y\":0,\"z\":1.2}}}. Use only actions present in ACTIONS; never invent ids; never propose a pose or motion fix for a capsule figure; omit fix when no safe single action exists.";
8
+
9
+ const REFUSAL_CODES = Object.freeze(["TARGET_NOT_READY", "CAPABILITY_MISSING", "CONFIRMATION_REQUIRED"]);
10
+ const SUPERVISOR_LIMITS = Object.freeze({ delta: 8, warnings: 12, reply: 4000, images: 5, createdShots: 3, imageWidth: 1280, args: 600, timeoutMs: 120_000, schemaBytes: 6000 });
11
+ /** The actions whose input schemas the reviewer gets, so a fix it writes has
12
+ * the right shape (#743). Only those present in the turn's action index are
13
+ * asked for. */
14
+ const SUPERVISOR_FIX_ACTIONS = Object.freeze(["character.setPose", "character.move", "character.update", "shot.frame", "shot.set", "shot.setRange", "object.update", "object.remove", "objects.arrange", "stage.set", "edit.undo", "view.update"]);
15
+ const ITEM_STATUSES = ["done", "partial", "refused", "missing"];
16
+ const SEVERITIES = ["blocker", "concern", "note"];
17
+ const KINDS = ["coverage", "disclosure", "spatial", "framing", "continuity", "undo", "honesty"];
18
+
19
+ const isObject = value => Boolean(value) && typeof value === "object" && !Array.isArray(value);
20
+ const clip = (text, max) => text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
21
+
22
+ /** Pictures never ride in the digest's text: every dataUrl, at any depth, is dropped. */
23
+ function withoutDataUrls(value) {
24
+ if (Array.isArray(value)) return value.map(withoutDataUrls);
25
+ if (!isObject(value)) return value;
26
+ return Object.fromEntries(Object.entries(value).filter(([key]) => key !== "dataUrl").map(([key, entry]) => [key, withoutDataUrls(entry)]));
27
+ }
28
+
29
+ /** One receipt, verbatim but bounded: no image bytes, at most 8 delta rows and 12 warnings. */
30
+ function boundReceipt(receipt) {
31
+ const bounded = withoutDataUrls(receipt);
32
+ if (!isObject(bounded)) return bounded;
33
+ if (Array.isArray(bounded.delta) && bounded.delta.length > SUPERVISOR_LIMITS.delta) {
34
+ bounded.deltaOmitted = bounded.delta.length - SUPERVISOR_LIMITS.delta;
35
+ bounded.delta = bounded.delta.slice(0, SUPERVISOR_LIMITS.delta);
36
+ }
37
+ if (Array.isArray(bounded.warnings) && bounded.warnings.length > SUPERVISOR_LIMITS.warnings) {
38
+ bounded.warningsOmitted = bounded.warnings.length - SUPERVISOR_LIMITS.warnings;
39
+ bounded.warnings = bounded.warnings.slice(0, SUPERVISOR_LIMITS.warnings);
40
+ }
41
+ return bounded;
42
+ }
43
+
44
+ const refusalOf = (code, message, tool) => REFUSAL_CODES.includes(code) ? [{ code, message: clip(String(message ?? ""), 400), ...(tool ? { tool } : {}) }] : [];
45
+
46
+ /** What a turn's frames say about it: whether it authored anything, the
47
+ * refusals it met, its tool calls and its receipts (read-only results such as
48
+ * inspect_studio are calls, not receipts). */
49
+ function analyseTurnFrames(frames = []) {
50
+ const names = new Map();
51
+ const calls = [], receipts = [], refusals = [];
52
+ let authored = false;
53
+ for (const frame of Array.isArray(frames) ? frames : []) {
54
+ if (frame?.type === "tool.start") {
55
+ names.set(frame.callId, frame.name);
56
+ calls.push({ callId: frame.callId, name: frame.name, args: clip(JSON.stringify(frame.args ?? {}), SUPERVISOR_LIMITS.args) });
57
+ } else if (frame?.type === "tool.done") {
58
+ const name = names.get(frame.callId);
59
+ const call = calls.find(entry => entry.callId === frame.callId);
60
+ if (call) call.ok = frame.ok === true;
61
+ if (frame.ok !== true) {
62
+ const error = String(frame.error ?? "");
63
+ const match = /^([A-Z][A-Z_]+):\s*([\s\S]*)$/.exec(error);
64
+ if (call) call.error = clip(error, 400);
65
+ if (match) refusals.push(...refusalOf(match[1], match[2], name));
66
+ continue;
67
+ }
68
+ const result = frame.result;
69
+ if (!isObject(result)) continue;
70
+ if (result.authored === true) authored = true;
71
+ if (result.ok === false) refusals.push(...refusalOf(result.code ?? result.error?.code, result.message ?? result.error?.message, name));
72
+ if (name !== "inspect_studio" && (typeof result.receiptId === "string" || typeof result.authored === "boolean" || isObject(result.checks))) receipts.push({ tool: name ?? null, receipt: result });
73
+ } else if (frame?.type === "receipt" && isObject(frame.receipt)) {
74
+ if (frame.receipt.authored === true) authored = true;
75
+ if (frame.receipt.ok === false) refusals.push(...refusalOf(frame.receipt.code, frame.receipt.message, null));
76
+ receipts.push({ tool: "turn", receipt: frame.receipt });
77
+ }
78
+ }
79
+ return { authored, refusals, calls: calls.map(({ callId: _callId, ...call }) => call), receipts };
80
+ }
81
+
82
+ /** Pure read-only turns and questions are not reviewed: only a turn that
83
+ * authored something or met a refusal is. */
84
+ export const shouldSupervise = frames => {
85
+ const { authored, refusals } = analyseTurnFrames(frames);
86
+ return authored || refusals.length > 0;
87
+ };
88
+
89
+ function imagePart(image) {
90
+ const match = /^data:([^;,]+);base64,(.*)$/.exec(image?.dataUrl ?? "");
91
+ return match ? { type: "image", data: match[2], mimeType: match[1] } : null;
92
+ }
93
+
94
+ /** The ACTION SCHEMAS block: whole schemas in the order given while they fit
95
+ * the byte budget; the ids that did not fit are named, never half-quoted. */
96
+ function boundSchemas(schemas) {
97
+ const kept = {}, omitted = [];
98
+ let size = 2;
99
+ for (const entry of Array.isArray(schemas) ? schemas : []) {
100
+ if (typeof entry?.id !== "string" || !isObject(entry.input)) continue;
101
+ const cost = JSON.stringify({ [entry.id]: entry.input }).length;
102
+ if (size + cost > SUPERVISOR_LIMITS.schemaBytes) { omitted.push(entry.id); continue; }
103
+ kept[entry.id] = entry.input;
104
+ size += cost;
105
+ }
106
+ return omitted.length ? { ...kept, omitted } : kept;
107
+ }
108
+
109
+ /** The digest the reviewer reads: a compact JSON-ish text block plus at most
110
+ * five pi ImageContent parts, in the order the text lists them. `actions` is
111
+ * `{ index: [{ id, label }], schemas: [{ id, input }] }`: what a fix may name
112
+ * and the input shape of the pre-selected ones (#743). */
113
+ export function buildSupervisorInput({ request = "", frames = [], reply = "", geometry = null, refusals, images = [], notes = [], actions = null } = {}) {
114
+ const turn = analyseTurnFrames(frames);
115
+ const pictures = (Array.isArray(images) ? images : []).map(image => ({ image, part: imagePart(image) })).filter(entry => entry.part).slice(0, SUPERVISOR_LIMITS.images);
116
+ const section = (name, value) => `${name}: ${JSON.stringify(value)}`;
117
+ const text = [
118
+ "<turn-review>",
119
+ section("request", String(request ?? "")),
120
+ section("toolCalls", turn.calls),
121
+ section("receipts", turn.receipts.map(entry => ({ tool: entry.tool, ...boundReceipt(entry.receipt) }))),
122
+ section("refusals", Array.isArray(refusals) ? refusals : turn.refusals),
123
+ section("reply", clip(String(reply ?? ""), SUPERVISOR_LIMITS.reply)),
124
+ section("geometry", geometry ?? { note: "no geometry facts were measured" }),
125
+ section("images", pictures.map((entry, index) => `${index + 1} ${entry.image.label ?? "image"}${entry.image.width ? ` (${entry.image.width}x${entry.image.height})` : ""}`)),
126
+ ...(notes.length ? [section("evidenceNotes", notes)] : []),
127
+ section("ACTIONS", (Array.isArray(actions?.index) ? actions.index : []).filter(row => typeof row?.id === "string").map(row => typeof row.label === "string" && row.label ? `${row.id} (${row.label})` : row.id)),
128
+ section("ACTION SCHEMAS", boundSchemas(actions?.schemas)),
129
+ "</turn-review>",
130
+ ].join("\n");
131
+ return { text, images: pictures.map(entry => entry.part) };
132
+ }
133
+
134
+ /** The first balanced {...} block of a reply, string-aware. */
135
+ function firstJsonBlock(text) {
136
+ const start = text.indexOf("{");
137
+ if (start === -1) return null;
138
+ let depth = 0, inString = false, escaped = false;
139
+ for (let index = start; index < text.length; index += 1) {
140
+ const char = text[index];
141
+ if (inString) {
142
+ if (escaped) escaped = false;
143
+ else if (char === "\\") escaped = true;
144
+ else if (char === "\"") inString = false;
145
+ } else if (char === "\"") inString = true;
146
+ else if (char === "{") depth += 1;
147
+ else if (char === "}" && --depth === 0) return text.slice(start, index + 1);
148
+ }
149
+ return null;
150
+ }
151
+
152
+ /** The reviewer's verdict, validated, or null for anything that is not one. */
153
+ export function parseSupervisorVerdict(text) {
154
+ if (typeof text !== "string") return null;
155
+ const block = firstJsonBlock(text);
156
+ if (!block) return null;
157
+ let value;
158
+ try { value = JSON.parse(block); } catch { return null; }
159
+ if (!isObject(value) || typeof value.summary !== "string" || !Array.isArray(value.items) || !Array.isArray(value.issues)) return null;
160
+ const items = value.items.map(item => isObject(item) && typeof item.text === "string" && ITEM_STATUSES.includes(item.status) && typeof item.evidence === "string"
161
+ ? { text: item.text, status: item.status, evidence: item.evidence } : null);
162
+ // A fix rides along unvalidated here; validateFixes keeps or drops it.
163
+ const issues = value.issues.map(issue => isObject(issue) && SEVERITIES.includes(issue.severity) && KINDS.includes(issue.kind) && typeof issue.text === "string" && typeof issue.evidence === "string"
164
+ ? { severity: issue.severity, kind: issue.kind, text: issue.text, evidence: issue.evidence, ...(issue.fix !== undefined && issue.fix !== null ? { fix: issue.fix } : {}) } : null);
165
+ if (items.includes(null) || issues.includes(null)) return null;
166
+ return { items, issues, summary: value.summary };
167
+ }
168
+
169
+ /**
170
+ * The fixes a note may hand the next turn (#743). A fix survives only when its
171
+ * action is in the turn's action index and its args are an object carrying
172
+ * every key the action's schema requires (when the schema was read). Every
173
+ * other fix is removed from its issue and reported with the reason.
174
+ */
175
+ export function validateFixes(issues, { index = [], schemas = [] } = {}) {
176
+ const known = new Set((Array.isArray(index) ? index : []).map(row => typeof row === "string" ? row : row?.id).filter(id => typeof id === "string"));
177
+ const inputs = new Map((Array.isArray(schemas) ? schemas : []).filter(entry => typeof entry?.id === "string" && isObject(entry.input)).map(entry => [entry.id, entry.input]));
178
+ const dropped = [];
179
+ const kept = (Array.isArray(issues) ? issues : []).map((issue, issueIndex) => {
180
+ if (!isObject(issue) || issue.fix === undefined) return issue;
181
+ const { fix, ...rest } = issue;
182
+ const action = typeof fix?.action === "string" ? fix.action : null;
183
+ const required = Array.isArray(inputs.get(action)?.required) ? inputs.get(action).required : [];
184
+ const missing = isObject(fix?.args) ? required.filter(key => !Object.hasOwn(fix.args, key)) : [];
185
+ const reason = !isObject(fix) || !action ? "malformed fix"
186
+ : !known.has(action) ? "unknown action"
187
+ : !isObject(fix.args) ? "args is not an object"
188
+ : missing.length ? `missing required key ${missing.map(key => `"${key}"`).join(", ")}` : null;
189
+ if (!reason) return { ...rest, fix: { action, args: fix.args } };
190
+ dropped.push({ issueIndex, action, reason });
191
+ return rest;
192
+ });
193
+ return { issues: kept, dropped };
194
+ }
195
+
196
+ const verdictCounts = issues => Object.fromEntries(SEVERITIES.map(severity => [severity, issues.filter(issue => issue.severity === severity).length]));
197
+
198
+ const redact = message => clip(String(message ?? "unknown failure").replace(/data:[^\s"']+/gi, "[image]").replace(/\b(?:sk-or-|sk-ant-|sk-|Bearer\s+|eyJ)[\w.\-+/=]+/gi, "[redacted]").replace(/\s+/g, " ").trim(), 200);
199
+
200
+ /**
201
+ * What the reviewer gets to see beyond the frames, read from the editor after
202
+ * the turn: geometry facts for the shots the turn touched (a framing
203
+ * verify_result the sidecar issues itself), the current shot frame, the top
204
+ * view and one frame per shot the turn created. Every piece is optional; what
205
+ * could not be read becomes a note instead of a failure.
206
+ */
207
+ async function gatherSupervisorEvidence({ hub, workspaceHandle, admission, baseline, receipts, actionIndex = [] }) {
208
+ const notes = [], images = [];
209
+ const command = (name, args, options) => options ? hub.command(name, args, workspaceHandle, options) : hub.command(name, args, workspaceHandle);
210
+ // What a fix may name: the turn's own action index, and the input schemas
211
+ // of the pre-selected actions it declares (#743).
212
+ const index = (Array.isArray(actionIndex) ? actionIndex : []).filter(row => typeof row?.id === "string").map(row => ({ id: row.id, ...(typeof row.label === "string" ? { label: row.label } : {}) }));
213
+ const wanted = SUPERVISOR_FIX_ACTIONS.filter(id => index.some(row => row.id === id));
214
+ let schemas = [];
215
+ if (wanted.length) {
216
+ try {
217
+ const result = await command("inspect_studio", { scope: "actions", ids: wanted });
218
+ schemas = (Array.isArray(result?.actions) ? result.actions : []).filter(row => wanted.includes(row?.id) && isObject(row.input)).map(row => ({ id: row.id, input: row.input }));
219
+ } catch (error) { notes.push(`action schemas unavailable: ${redact(error?.message)}`); }
220
+ }
221
+ const envelope = (name, args) => ({ name, args, commandId: admission.commandId(), host: admission.host, expectedRevision: admission.revision });
222
+ let context = null;
223
+ try { context = await admission.refresh(); } catch (error) { notes.push(`scene context unavailable: ${redact(error?.message)}`); }
224
+ const shots = Array.isArray(context?.shots) ? context.shots : [];
225
+ const shotIds = new Set(shots.map(shot => shot.id));
226
+ const before = new Set((Array.isArray(baseline?.shots) ? baseline.shots : []).map(shot => shot.id));
227
+ const touched = [...new Set(receipts.flatMap(entry => Array.isArray(entry.receipt?.affectedIds) ? entry.receipt.affectedIds : []))].filter(id => shotIds.has(id));
228
+ let geometry;
229
+ const targets = touched.length ? touched : context?.shot?.id ? [context.shot.id] : shots[0] ? [shots[0].id] : [];
230
+ if (!targets.length) geometry = { note: "no shot exists; geometry skipped" };
231
+ else {
232
+ try {
233
+ const result = await command("verify_result", envelope("verify_result", { targets, checks: ["framing"] }));
234
+ geometry = isObject(result?.geometry) ? { shotIds: targets, ...result.geometry }
235
+ : { note: result?.ok === false ? `verify_result refused: ${result.code ?? result.error?.code ?? "error"}` : "the editor returned no geometry facts" };
236
+ } catch (error) { geometry = { note: `geometry unavailable: ${error?.code ? `${error.code}: ` : ""}${redact(error?.message)}` }; }
237
+ }
238
+ // The editor renders at the size it is asked for, so an oversized capture is
239
+ // re-requested at 1280 px wide rather than resized here.
240
+ const capture = async (name, label) => {
241
+ const named = typeof label === "string" ? label : name;
242
+ try {
243
+ let shot = await command(name, {});
244
+ if (shot?.width > SUPERVISOR_LIMITS.imageWidth && shot?.height > 0) {
245
+ const width = SUPERVISOR_LIMITS.imageWidth, height = Math.round(shot.height * width / shot.width);
246
+ const smaller = await command(name, { output: { width, height } });
247
+ if (smaller?.dataUrl?.startsWith("data:image/")) shot = smaller;
248
+ else notes.push(`${named} kept at ${shot.width} px wide: the editor did not render a smaller copy`);
249
+ }
250
+ if (!shot?.dataUrl?.startsWith("data:image/")) { notes.push(`${named} unavailable: the editor returned no image`); return; }
251
+ images.push({ label: typeof label === "function" ? label(shot) : label, dataUrl: shot.dataUrl, width: shot.width, height: shot.height });
252
+ } catch (error) { notes.push(`${named} unavailable: ${redact(error?.message)}`); }
253
+ };
254
+ await capture("capture_framing_png", shot => `current shot frame (shot ${shot.shotId ?? "none"}, frame ${shot.frame ?? "?"})`);
255
+ await capture("capture_plan_png", "top view (plan) with subject labels and the shot camera wedge");
256
+ const created = touched.filter(id => !before.has(id)).map(id => shots.find(shot => shot.id === id)).filter(shot => Number.isSafeInteger(shot?.range?.startFrame)).slice(0, SUPERVISOR_LIMITS.createdShots);
257
+ const original = context?.view?.frame;
258
+ if (created.length && Number.isSafeInteger(original)) {
259
+ const operate = async frame => {
260
+ const receipt = await command("operate_studio", envelope("operate_studio", { frame }));
261
+ if (Number.isSafeInteger(receipt?.revision?.after)) admission.revision = receipt.revision.after;
262
+ if (receipt?.ok === false) throw Object.assign(new Error(receipt.message ?? "operate_studio refused"), { code: receipt.code });
263
+ };
264
+ try {
265
+ for (const shot of created) {
266
+ if (images.length >= SUPERVISOR_LIMITS.images) break;
267
+ try { await operate(shot.range.startFrame); }
268
+ catch (error) { notes.push(`shot ${shot.id} frame unavailable: ${redact(error?.message)}`); continue; }
269
+ await capture("capture_framing_png", `created shot ${shot.name ?? shot.id} (${shot.id}) at its start frame ${shot.range.startFrame}`);
270
+ }
271
+ } finally {
272
+ try { await operate(original); } catch (error) { notes.push(`playhead not restored to frame ${original}: ${redact(error?.message)}`); }
273
+ }
274
+ }
275
+ return { geometry, images: images.slice(0, SUPERVISOR_LIMITS.images), notes, actions: { index, schemas } };
276
+ }
277
+
278
+ /**
279
+ * Review one finished turn. Never throws: any failure becomes a frame with
280
+ * verdict "unavailable" and the reason.
281
+ */
282
+ export async function runSupervisor({ registry, modelKey, effort = "high", turnId, request, frames, reply, hub, workspaceHandle, admission, baseline, actions = [], now = () => Date.now(), timeoutMs = SUPERVISOR_LIMITS.timeoutMs }) {
283
+ const startedAt = now();
284
+ let thinking = null;
285
+ const frame = (fields) => ({ type: "supervisor", turnId, model: modelKey, effort: thinking, elapsedMs: Math.max(0, Math.round(now() - startedAt)), ...fields });
286
+ const unavailable = reason => frame({ verdict: "unavailable", summary: "", items: [], issues: [], fixes: [], droppedFixes: [], counts: { ...verdictCounts([]), fixes: 0 }, reason: redact(reason) });
287
+ try {
288
+ const { resolveModel, resolveEffort } = await import("./providers.mjs");
289
+ const slash = modelKey.indexOf("/");
290
+ const provider = slash === -1 ? "openai-codex" : modelKey.slice(0, slash);
291
+ const model = registry.getModel(provider, slash === -1 ? modelKey : modelKey.slice(slash + 1)) ?? (await resolveModel(modelKey, { models: registry })).model;
292
+ const level = await resolveEffort(model, effort);
293
+ thinking = level;
294
+ const turn = analyseTurnFrames(frames);
295
+ const evidence = await gatherSupervisorEvidence({ hub, workspaceHandle, admission, baseline, receipts: turn.receipts, actionIndex: actions });
296
+ const input = buildSupervisorInput({ request, frames, reply, geometry: evidence.geometry, refusals: turn.refusals, images: evidence.images, notes: evidence.notes, actions: evidence.actions });
297
+ const message = await registry.completeSimple(model, {
298
+ systemPrompt: SUPERVISOR_RUBRIC,
299
+ messages: [{ role: "user", content: [{ type: "text", text: input.text }, ...input.images], timestamp: Date.now() }],
300
+ }, { ...(level && level !== "off" ? { reasoning: level } : {}), signal: AbortSignal.timeout(timeoutMs) });
301
+ if (message?.stopReason === "error" || message?.stopReason === "aborted") return unavailable(message.errorMessage || `the reviewer request ${message.stopReason === "aborted" ? "timed out" : "failed"}`);
302
+ const text = (Array.isArray(message?.content) ? message.content : []).filter(part => part?.type === "text").map(part => part.text).join("");
303
+ const verdict = parseSupervisorVerdict(text);
304
+ if (!verdict) return unavailable("the reviewer did not answer with a valid verdict");
305
+ const { issues, dropped } = validateFixes(verdict.issues, evidence.actions);
306
+ const fixes = issues.flatMap((issue, issueIndex) => issue.fix ? [{ issueIndex, severity: issue.severity, action: issue.fix.action, args: issue.fix.args }] : []);
307
+ return frame({ verdict: "reviewed", summary: verdict.summary, items: verdict.items, issues, fixes, droppedFixes: dropped, counts: { ...verdictCounts(issues), fixes: fixes.length } });
308
+ } catch (error) {
309
+ return unavailable(error?.message);
310
+ }
311
+ }
@@ -154,13 +154,6 @@ function preferredVideoOutputNodes(workflow) {
154
154
  const isH3Node = (node) => /minimax.?h3|h3.*video/i.test(String(node?.class_type || ""));
155
155
  const isLoadImageNode = (node) => /loadimage/i.test(String(node?.class_type || ""));
156
156
  const isCompositeNode = (node) => /imagecomposite|compositeimage|imagemattecomposite/i.test(String(node?.class_type || ""));
157
- const isMaskNode = (node) => {
158
- if (isCompositeNode(node)) return false;
159
- // A generic/static mask can hide a broken tracker and would not preserve a
160
- // moving performer across frames. Only accept a temporal SAM3/tracked-mask
161
- // node on the compositor path.
162
- return /sam3[_-]?(videotrack|tracktomask)|tracktomask|videomask/i.test(String(node?.class_type || ""));
163
- };
164
157
 
165
158
  function h3GuardError(message, reason) {
166
159
  return Object.assign(new Error(message), {
@@ -8,10 +8,10 @@ import { writeSecureJson } from "./agent/secure-file.mjs";
8
8
  import { readKeys } from "./agent/provider-keys.mjs";
9
9
  import { PROVIDERS } from "./agent/providers.mjs";
10
10
 
11
- export const AUTHORIZE_ENDPOINT = "https://auth.openai.com/oauth/authorize";
12
- export const TOKEN_ENDPOINT = "https://auth.openai.com/oauth/token";
13
- export const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
14
- export const SCOPE = "openid profile email offline_access";
11
+ const AUTHORIZE_ENDPOINT = "https://auth.openai.com/oauth/authorize";
12
+ const TOKEN_ENDPOINT = "https://auth.openai.com/oauth/token";
13
+ const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
14
+ const SCOPE = "openid profile email offline_access";
15
15
  const DEFAULT_FILE = join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "cozyclay", "codex-auth.json");
16
16
  const tokenFile = process.env.COZYCLAY_CODEX_AUTH_FILE || DEFAULT_FILE;
17
17
  let tokens = null;
package/bin/live/cli.mjs CHANGED
@@ -29,8 +29,9 @@ const USAGE = `cclay live - drive a running CozyClay studio from a terminal
29
29
  cclay live frame-shot --subject <id> --exact px,py,pz,lx,ly,lz,focal
30
30
  cclay live operate [--select object:<id>] [--frame N] [--mode scene|camera|motion]
31
31
  [--play | --pause]
32
- cclay live verify --receipt <id> --checks placement,framing
33
- [--visual frame --out check.png]
32
+ cclay live verify (--receipt <id> | --targets a,b) --checks placement,framing
33
+ [--range whole_clip|start:end] [--strict]
34
+ [--visual frame|contact_sheet --out check.png]
34
35
  cclay live undo --receipt <id>
35
36
  cclay live commands [--ids a,b] the commands the editor registers; ids add schemas
36
37
  cclay live run <id> [--args '<json>'] any command the editor registers, admitted like
@@ -49,7 +50,7 @@ Exit codes: 0 ok - 1 editor or receipt error - 2 usage - 3 no hub -
49
50
 
50
51
  class UsageError extends Error {}
51
52
 
52
- const BOOLEAN_FLAGS = new Set(["--pretty", "--wait", "--framing", "--play", "--pause"]);
53
+ const BOOLEAN_FLAGS = new Set(["--pretty", "--wait", "--framing", "--play", "--pause", "--strict"]);
53
54
  const GLOBAL_FLAGS = ["--workspace", "--timeout", "--pretty", "--live-port"];
54
55
  const VERBS = new Map([
55
56
  ["status", { flags: ["--wait"] }],
@@ -61,7 +62,7 @@ const VERBS = new Map([
61
62
  ["patch", { flags: ["--target", "--set"], command: "patch_elements" }],
62
63
  ["frame-shot", { flags: ["--subject", "--size", "--view", "--level", "--side", "--focal", "--exact"] }],
63
64
  ["operate", { flags: ["--select", "--frame", "--mode", "--play", "--pause"] }],
64
- ["verify", { flags: ["--receipt", "--checks", "--visual", "--out"] }],
65
+ ["verify", { flags: ["--receipt", "--targets", "--checks", "--range", "--strict", "--visual", "--out"] }],
65
66
  ["undo", { flags: ["--receipt"] }],
66
67
  ["commands", { flags: ["--ids"] }],
67
68
  ["run", { flags: ["--args"], argument: "command id" }],
@@ -185,6 +186,15 @@ const integerFlag = (flags, key) => {
185
186
  if (!Number.isSafeInteger(value)) throw new UsageError(`${key} must be an integer`);
186
187
  return value;
187
188
  };
189
+ const frameRangeFlag = (flags, key) => {
190
+ const raw = String(flags.get(key));
191
+ if (raw === "whole_clip") return raw;
192
+ const match = /^(\d+):(\d+)$/.exec(raw);
193
+ if (!match) throw new UsageError(`${key} must be whole_clip or start:end`);
194
+ const startFrame = Number(match[1]), endFrameExclusive = Number(match[2]);
195
+ if (!Number.isSafeInteger(startFrame) || !Number.isSafeInteger(endFrameExclusive) || endFrameExclusive <= startFrame) throw new UsageError(`${key} must have a nonempty start:end range`);
196
+ return { startFrame, endFrameExclusive };
197
+ };
188
198
  const jsonFlag = (flags, key) => {
189
199
  let parsed;
190
200
  try {
@@ -247,7 +257,6 @@ function patchArguments(flags, verb) {
247
257
  const target = colon < 0 ? { kind: raw } : { kind: raw.slice(0, colon), id: raw.slice(colon + 1) };
248
258
  required(flags, "--set", verb);
249
259
  const set = jsonFlag(flags, "--set");
250
- if (set === null || typeof set !== "object" || Array.isArray(set)) throw new UsageError(`${verb} expects --set '{"path": value}'`);
251
260
  return { ops: [{ target, set }] };
252
261
  }
253
262
 
@@ -320,7 +329,7 @@ async function runVerb({ client, verb, spec, name, flags }) {
320
329
  }
321
330
  };
322
331
  const admitted = async (commandName, args, { context, options = forwarded } = {}) => {
323
- const envelope = admissionEnvelope(commandName, args, context ?? (await command("inspect_studio", { scope: "selection" }))?.context);
332
+ const envelope = admissionEnvelope(commandName, args, context ?? await command("inspect_studio", { scope: "selection" }));
324
333
  try {
325
334
  const value = await command(commandName, envelope, options);
326
335
  if (value?.ok === false) throw receiptFailure(value);
@@ -412,11 +421,17 @@ async function runVerb({ client, verb, spec, name, flags }) {
412
421
  if (verb === "operate") return admitted("operate_studio", operateArguments(flags));
413
422
 
414
423
  if (verb === "verify") {
415
- const args = { receiptId: required(flags, "--receipt", verb), checks: commaList(required(flags, "--checks", verb)) };
424
+ const hasReceipt = flags.has("--receipt"), hasTargets = flags.has("--targets");
425
+ if (hasReceipt === hasTargets) throw new UsageError("verify needs exactly one of --receipt or --targets");
426
+ const args = { checks: commaList(required(flags, "--checks", verb)), ...(hasReceipt ? { receiptId: required(flags, "--receipt", verb) } : { targets: commaList(required(flags, "--targets", verb)) }) };
427
+ if (flags.has("--range")) args.range = frameRangeFlag(flags, "--range");
416
428
  const visual = flags.has("--visual") ? String(flags.get("--visual")) : "none";
417
429
  if (visual !== "none") args.visual = visual;
418
430
  const out = visual === "none" ? null : resolvePath(required(flags, "--out", verb));
419
431
  const value = await admitted("verify_result", args);
432
+ if (flags.get("--strict") === true && value?.checks?.framing?.issueFrames?.length) {
433
+ throw new LiveCliError("VERIFICATION_FAILED", `Sampled framing found ${value.checks.framing.issueFrames.length} problematic frame(s).`, { details: value.checks.framing.issueFrames });
434
+ }
420
435
  if (out === null) return value;
421
436
  const written = [];
422
437
  for (const [index, reference] of (value?.visualRefs ?? []).entries()) {
@@ -446,7 +461,7 @@ async function runVerb({ client, verb, spec, name, flags }) {
446
461
  // at and the deadline it needs, which --timeout overrides.
447
462
  const inspected = await command("inspect_studio", { scope: "actions", ids: [name] });
448
463
  const deadline = timeoutMs ?? inspected?.actions?.find((row) => row.id === name)?.timeoutMs;
449
- return admitted("run_action", { action: name, args }, { context: inspected?.context, options: deadline === undefined ? {} : { timeoutMs: deadline } });
464
+ return admitted("run_action", { action: name, args }, { context: inspected, options: deadline === undefined ? {} : { timeoutMs: deadline } });
450
465
  }
451
466
 
452
467
  if (verb === "cmd") return raw(() => command(name, flags.has("--args") ? jsonFlag(flags, "--args") : {}));
@@ -11,7 +11,7 @@ import { randomUUID } from "node:crypto";
11
11
 
12
12
  import { readLiveEndpoint } from "../live-endpoint.mjs";
13
13
 
14
- export const DEFAULT_LIVE_PORT = 5184;
14
+ const DEFAULT_LIVE_PORT = 5184;
15
15
  /** The hub caps one command at 300 s; give up a little past that, so a hub
16
16
  * that answers nothing at all can never hang a terminal forever. */
17
17
  const REQUEST_CEILING_MS = 330_000;
@@ -48,8 +48,11 @@ export const describeEditors = (editors) => editors.map((editor) => ({
48
48
  * carries no per-entity tokens, because the scene revision already bumps on
49
49
  * every authored change and a retired token only refuses a legitimate retry.
50
50
  */
51
- export function admissionEnvelope(name, args, context) {
52
- if (!context?.host || !context.revision || !Array.isArray(context.entities)) {
51
+ export function admissionEnvelope(name, args, inspected) {
52
+ // inspect_studio answers the host and revision at its top level; an older
53
+ // editor embedded a whole context. Admission needs only those two.
54
+ const context = inspected?.context ?? inspected;
55
+ if (!context?.host || !context.revision) {
53
56
  throw new LiveCliError("EDITOR_ERROR", "The editor did not return a Studio context to admit this command against.");
54
57
  }
55
58
  const host = Object.fromEntries(IDENTITY.map((key) => [key, context.host[key]]));
@@ -20,7 +20,7 @@ function probeMcpRuntime(runtime = MCP_RUNTIME) {
20
20
  if (!existsSync(server)) throw new Error("MCP server is missing");
21
21
  if (!existsSync(join(runtime, "tools", "ardy", "bridge.mjs"))) throw new Error("motion bridge is missing");
22
22
  const runtimeRequire = createRequire(join(runtime, "package.json"));
23
- for (const dependency of ["@modelcontextprotocol/sdk/server/mcp.js", "three", "ws", "zod"]) runtimeRequire.resolve(dependency);
23
+ for (const dependency of ["@modelcontextprotocol/sdk/server/mcp.js", "jpeg-js", "three", "ws", "zod"]) runtimeRequire.resolve(dependency);
24
24
  return server;
25
25
  }
26
26
 
@@ -83,7 +83,7 @@ function installMcpRuntime() {
83
83
  } catch {
84
84
  try {
85
85
  renameSync(staging, MCP_RUNTIME);
86
- } catch (error) {
86
+ } catch {
87
87
  // Another first run can publish its complete staging
88
88
  // directory first. Accept that winner only after the
89
89
  // same full probe; never execute a partial cache.
@@ -3,8 +3,8 @@ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
3
3
  import { dirname } from "node:path";
4
4
 
5
5
  export const TELEMETRY_NOTICE_VERSION = 1;
6
- export const POSTHOG_PROJECT_TOKEN = "phc_CpizzZ8VhSorSS8yEeQhdpUcvB2erp5xkCbnFD8HTJ5m";
7
- export const POSTHOG_API_HOST = "https://t.cozyclay.org";
6
+ const POSTHOG_PROJECT_TOKEN = "phc_CpizzZ8VhSorSS8yEeQhdpUcvB2erp5xkCbnFD8HTJ5m";
7
+ const POSTHOG_API_HOST = "https://t.cozyclay.org";
8
8
  // First-launch answers plus the `npx cozyclay --via <source>` values the
9
9
  // landing page's copy commands carry (#466). Mirrors analytics HEARD_FROM_SOURCES.
10
10
  export const FIRST_LAUNCH_SOURCES = Object.freeze(["x", "hn", "reddit", "github", "friend", "other", "site", "playground"]);
@@ -111,7 +111,7 @@ export function takeRuntimeTelemetryConfig(
111
111
  ) {
112
112
  const existing = readTelemetryState(stateFile);
113
113
  const telemetryEnabled = officialPackage && effectiveTelemetryEnabled(existing, env);
114
- const internalQa = telemetryEnabled && existing.internalQa === true;
114
+ const internalQa = telemetryEnabled && existing.internalQa;
115
115
  let installationId = existing.installationId;
116
116
  const firstLaunch = telemetryEnabled && !existing.firstLaunchedAt;
117
117
  const patch = {};
@@ -34,13 +34,13 @@
34
34
  <title>CozyClay Studio — 3D staging and previs in your browser</title>
35
35
 
36
36
  <script defer src="https://static.cloudflareinsights.com/beacon.min.js" data-cf-beacon='{"token": "6a4384d1375d4ffc8a30a6346a8ad1a0"}'></script>
37
- <script type="module" crossorigin src="/assets/app-BxBLC6GT.js"></script>
37
+ <script type="module" crossorigin src="/assets/app-DAuvKPP8.js"></script>
38
38
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
39
- <link rel="modulepreload" crossorigin href="/assets/analytics-MVZOEW8O.js">
40
- <link rel="modulepreload" crossorigin href="/assets/first-shot-handoff-DgT28ZiV.js">
41
- <link rel="modulepreload" crossorigin href="/assets/shot-prompt-hN0j_O-p.js">
42
- <link rel="stylesheet" crossorigin href="/assets/shot-prompt-CbqWcWYi.css">
43
- <link rel="stylesheet" crossorigin href="/assets/app-CJdxvnrl.css">
39
+ <link rel="modulepreload" crossorigin href="/assets/analytics-C9pMpW7s.js">
40
+ <link rel="modulepreload" crossorigin href="/assets/first-shot-handoff-msLI71g1.js">
41
+ <link rel="modulepreload" crossorigin href="/assets/scene-asset-sync-DKkpjKBV.js">
42
+ <link rel="stylesheet" crossorigin href="/assets/scene-asset-sync-DjJ_cJ2R.css">
43
+ <link rel="stylesheet" crossorigin href="/assets/app-B_2XB9rw.css">
44
44
  </head>
45
45
  <body>
46
46
  <div id="root">