cozyclay 2.0.2 → 2.1.0

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 (256) hide show
  1. package/CHANGELOG.md +56 -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 +2 -3
  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-B41FmPYI.js +1 -0
  26. package/dist/assets/app-Do-Og5Pu.css +1 -0
  27. package/dist/assets/app-DwP-tl2x.js +4920 -0
  28. package/dist/assets/{demo-ByeYp-dY.js → demo-Dt0qZfS7.js} +1 -1
  29. package/dist/assets/{first-shot-handoff-DgT28ZiV.js → first-shot-handoff-BlT33CGf.js} +1 -1
  30. package/dist/assets/{landing-BaXkpFip.js → landing-BSesrCpL.js} +1 -1
  31. package/dist/assets/scene-asset-sync-BCWDJB1d.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-jqMhqL88.js} +1 -1
  34. package/dist/assets/workflow-Br-K7Srs.js +179 -0
  35. package/dist/cozyclay-package.json +1 -1
  36. package/dist/index.html +328 -222
  37. package/dist/sitemap.xml +1 -1
  38. package/dist/workflow/index.html +4 -4
  39. package/mcp/LIVE-PROTOCOL.md +23 -2
  40. package/mcp/README.md +37 -2
  41. package/mcp/ardy-prompts.mjs +1 -1
  42. package/mcp/image-file.mjs +132 -0
  43. package/mcp/live-hub.mjs +2 -2
  44. package/mcp/package.json +1 -0
  45. package/mcp/runtime/package-lock.json +7 -0
  46. package/mcp/runtime/package.json +1 -0
  47. package/mcp/server.mjs +0 -6
  48. package/mcp/tool-handlers.mjs +289 -87
  49. package/package.json +11 -2
  50. package/src/App.jsx +789 -263
  51. package/src/agent-image-client.js +94 -0
  52. package/src/analytics.js +47 -3
  53. package/src/app-stage.jsx +129 -2745
  54. package/src/ardy/auto-physics.js +6 -16
  55. package/src/ardy/collision-blockers.js +3 -7
  56. package/src/ardy/fix-collisions.js +5 -35
  57. package/src/ardy/ik-drag.js +86 -5
  58. package/src/ardy/ik-key-json.js +3 -1
  59. package/src/ardy/ik.js +15 -13
  60. package/src/ardy/motion-calibration.js +1 -1
  61. package/src/ardy/npz.js +0 -8
  62. package/src/ardy/physics-panel.jsx +3 -2
  63. package/src/ardy/physics-review.js +3 -2
  64. package/src/ardy/physics-support.js +1 -1
  65. package/src/ardy/physics-surface.js +1 -1
  66. package/src/ardy/platform-fit-panel.jsx +2 -1
  67. package/src/ardy/playback.js +1 -5
  68. package/src/ardy/range-pin.js +2 -2
  69. package/src/ardy/root-drop.js +0 -1
  70. package/src/ardy/timeline.css +184 -6
  71. package/src/ardy/timeline.jsx +113 -28
  72. package/src/ardy/to-cskel27.js +0 -2
  73. package/src/ardy/waypoints.js +0 -3
  74. package/src/ardy-bridge-contract.js +281 -0
  75. package/src/asset-pane.jsx +19 -9
  76. package/src/auto-color.js +1 -1
  77. package/src/burn-in.js +1 -1
  78. package/src/camera-block.js +1 -1
  79. package/src/camera-follow.js +75 -32
  80. package/src/camera-move.js +0 -6
  81. package/src/camera-tutorial.jsx +6 -7
  82. package/src/character-kind.js +79 -0
  83. package/src/command-bus.js +76 -17
  84. package/src/commands/cast.js +31 -7
  85. package/src/commands/elements/object.js +1 -1
  86. package/src/commands/elements/shot.js +1 -1
  87. package/src/commands/elements.js +1 -1
  88. package/src/commands/export.js +3 -3
  89. package/src/commands/motion.js +2 -2
  90. package/src/commands/objects.js +105 -10
  91. package/src/commands/project.js +15 -2
  92. package/src/commands/sculpt-images.js +53 -0
  93. package/src/commands/shot.js +88 -10
  94. package/src/commands/view.js +10 -4
  95. package/src/coplanar-depth.js +216 -0
  96. package/src/cuts.js +7 -6
  97. package/src/domains/cast.js +60 -43
  98. package/src/domains/motion/motion-intent-codec.js +50 -0
  99. package/src/domains/motion/motion-range-pin-rules.js +23 -0
  100. package/src/domains/motion/motion-video-capture.js +664 -0
  101. package/src/domains/motion.js +253 -742
  102. package/src/domains/objects.js +106 -5
  103. package/src/domains/scenes.js +79 -28
  104. package/src/domains/shots.js +33 -7
  105. package/src/error-boundary.jsx +1 -1
  106. package/src/execution-telemetry.js +5 -5
  107. package/src/facing-marks.js +14 -1
  108. package/src/first-success-guide.jsx +35 -4
  109. package/src/gizmo-claim.js +3 -1
  110. package/src/grid-floor.jsx +1 -4
  111. package/src/hierarchy-model.js +48 -35
  112. package/src/hierarchy-panel.css +1 -0
  113. package/src/hierarchy-panel.jsx +54 -102
  114. package/src/history.js +0 -2
  115. package/src/i2v-motion-client.js +1 -4
  116. package/src/ime.js +6 -0
  117. package/src/keyframe-pack.js +15 -6
  118. package/src/line-edit.js +4 -4
  119. package/src/live-control.js +6 -6
  120. package/src/matte-editor.js +2 -8
  121. package/src/matte.js +0 -19
  122. package/src/mesh-graph-clone.js +16 -2
  123. package/src/mesh-sniff.js +0 -1
  124. package/src/monotone-cubic.js +40 -0
  125. package/src/motion-store.js +4 -4
  126. package/src/motion-trail.js +5 -11
  127. package/src/multimodel-ingest.js +1 -3
  128. package/src/object-catalog.jsx +7 -5
  129. package/src/object-gizmo.jsx +45 -31
  130. package/src/object-path.js +310 -67
  131. package/src/object-travel.js +224 -0
  132. package/src/otio.js +8 -3
  133. package/src/panels/CharacterTransformPanel.jsx +24 -6
  134. package/src/panels/ObjectTransformPanel.jsx +4 -3
  135. package/src/panels/PoseImagePanel.jsx +205 -0
  136. package/src/panels/PromptBlocksPanel.jsx +3 -1
  137. package/src/panels/ProxyFigurePanel.jsx +56 -0
  138. package/src/panels/ReferenceImageField.jsx +1 -1
  139. package/src/panels/RigControlPanel.jsx +24 -3
  140. package/src/panels/RigPanel.jsx +2 -1
  141. package/src/panels/SculptPanel.jsx +405 -0
  142. package/src/panels/SubjectBox.jsx +1 -1
  143. package/src/panels/SubjectsPanel.jsx +0 -2
  144. package/src/panels/VideoCapturePanel.jsx +2 -1
  145. package/src/panels/pose-image.css +42 -0
  146. package/src/panels/pose.css +3 -0
  147. package/src/panels/sculpt.css +162 -0
  148. package/src/plan-diagram.js +184 -0
  149. package/src/planview.jsx +127 -80
  150. package/src/playground.js +3 -5
  151. package/src/pose-fade.js +77 -0
  152. package/src/pose-gizmo.css +23 -0
  153. package/src/pose-gizmo.js +96 -0
  154. package/src/posestudio.jsx +134 -83
  155. package/src/project-browser.css +53 -0
  156. package/src/project-browser.jsx +49 -4
  157. package/src/project.js +55 -24
  158. package/src/props.jsx +177 -36
  159. package/src/proxy-figure.jsx +69 -0
  160. package/src/range-pin-panel.jsx +0 -1
  161. package/src/receipt-findings.js +20 -0
  162. package/src/render-passes.js +0 -1
  163. package/src/resource-status.jsx +1 -1
  164. package/src/root-path.js +48 -0
  165. package/src/route-owner.js +52 -0
  166. package/src/scene-asset-cache.js +1 -2
  167. package/src/scene-assets.js +12 -4
  168. package/src/scene-fbx.js +1 -1
  169. package/src/scene-mesh-cache.js +0 -1
  170. package/src/scene-objects.js +319 -19
  171. package/src/scenes.js +58 -9
  172. package/src/sculpt-blueprint.js +65 -0
  173. package/src/sculpt-compare.js +384 -0
  174. package/src/sculpt-geometry.js +141 -0
  175. package/src/sculpt-mode.js +168 -0
  176. package/src/sculpt-recipe.js +385 -0
  177. package/src/sculpt-run.js +351 -0
  178. package/src/sculpt-stage.jsx +221 -0
  179. package/src/settings-menu.jsx +1 -1
  180. package/src/shell/BoardSlot.jsx +341 -0
  181. package/src/shell/BottomDock.jsx +58 -14
  182. package/src/shell/DetailsSlot.jsx +26 -6
  183. package/src/shell/MenuBar.jsx +78 -6
  184. package/src/shell/OutlinerSlot.jsx +3 -1
  185. package/src/shell/TopBar.jsx +17 -3
  186. package/src/shell/ViewportToolbar.jsx +18 -3
  187. package/src/shell/dock.css +239 -0
  188. package/src/shell/glass-regions.css +3 -2
  189. package/src/shell/log-store.js +4 -4
  190. package/src/shell/mode.css +20 -0
  191. package/src/shell/studio-shell-context.js +4 -1
  192. package/src/shot-authoring.js +69 -6
  193. package/src/stable-items.js +1 -1
  194. package/src/stage-attach-frames.js +169 -0
  195. package/src/stage-canvas-rigs.jsx +402 -0
  196. package/src/stage-capture.jsx +216 -0
  197. package/src/stage-characters.jsx +302 -0
  198. package/src/stage-editor-gizmos.jsx +1229 -0
  199. package/src/stage-labels-ko.js +204 -0
  200. package/src/stage-startup-defaults.js +182 -0
  201. package/src/starter-scenes.js +1 -1
  202. package/src/storyboard.js +24 -5
  203. package/src/studio-actions.js +42 -9
  204. package/src/studio-agent-commands.js +186 -23
  205. package/src/studio-agent-context.js +11 -9
  206. package/src/studio-agent-motion.js +2 -1
  207. package/src/studio-agent-protocol.js +56 -23
  208. package/src/studio-app-binding.js +52 -23
  209. package/src/studio-contact-sheet.js +3 -3
  210. package/src/studio-elements.js +11 -2
  211. package/src/studio-geometry-facts.js +114 -0
  212. package/src/styles.css +92 -9
  213. package/src/ui-scrub.js +4 -4
  214. package/src/ui.jsx +6 -4
  215. package/src/use-asset-shelf.js +60 -0
  216. package/src/use-camera-tutorial.js +50 -0
  217. package/src/use-embed-bridge.js +12 -0
  218. package/src/use-first-success-prompt.js +7 -0
  219. package/src/use-i2v-capture-modal.js +15 -0
  220. package/src/use-sculpt-mode.js +95 -0
  221. package/src/workflow/AgentPanel.jsx +106 -18
  222. package/src/workflow/WorkflowBuilder.jsx +1 -1
  223. package/src/workflow/agent-client.js +113 -45
  224. package/src/workflow/agent-panel.css +35 -0
  225. package/src/workflow/attachment-image.js +1 -1
  226. package/src/workflow/canvas-commands.js +3 -3
  227. package/src/workflow/cozy-scene-node.js +3 -3
  228. package/src/workflow/local-workflow.js +2 -2
  229. package/src/workflow/motion-input.js +3 -3
  230. package/src/workflow/node-schema.js +1 -1
  231. package/src/workflow/scene-asset-sync.js +2 -9
  232. package/src/workflow/shot-prompt-node.js +1 -1
  233. package/src/workflow/video-contract.js +1 -1
  234. package/src/workflow/workflow-resources.js +0 -8
  235. package/src/workflow-send.js +87 -0
  236. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  237. package/tools/check-eslint.mjs +57 -0
  238. package/tools/check-knip.mjs +60 -0
  239. package/tools/check-loc.mjs +54 -0
  240. package/tools/demo-worker/api-client.mjs +7 -13
  241. package/tools/demo-worker/generate.mjs +2 -4
  242. package/tools/eslint-baseline.json +23 -0
  243. package/tools/kimodo/effector-constraints.mjs +4 -4
  244. package/tools/kimodo/generate.mjs +1 -1
  245. package/tools/knip-baseline.json +331 -0
  246. package/tools/loc-baseline.json +126 -0
  247. package/tools/process-supervisor.mjs +1 -1
  248. package/tools/projflow/replay.mjs +4 -4
  249. package/tools/projflow/runner.mjs +3 -3
  250. package/tools/qa/studio-control-count.mjs +2 -0
  251. package/tools/run-tests.mjs +94 -7
  252. package/dist/assets/analytics-MVZOEW8O.js +0 -1
  253. package/dist/assets/app-BxBLC6GT.js +0 -4876
  254. package/dist/assets/app-CJdxvnrl.css +0 -1
  255. package/dist/assets/shot-prompt-hN0j_O-p.js +0 -17
  256. 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
@@ -247,7 +247,6 @@ function patchArguments(flags, verb) {
247
247
  const target = colon < 0 ? { kind: raw } : { kind: raw.slice(0, colon), id: raw.slice(colon + 1) };
248
248
  required(flags, "--set", verb);
249
249
  const set = jsonFlag(flags, "--set");
250
- if (set === null || typeof set !== "object" || Array.isArray(set)) throw new UsageError(`${verb} expects --set '{"path": value}'`);
251
250
  return { ops: [{ target, set }] };
252
251
  }
253
252
 
@@ -320,7 +319,7 @@ async function runVerb({ client, verb, spec, name, flags }) {
320
319
  }
321
320
  };
322
321
  const admitted = async (commandName, args, { context, options = forwarded } = {}) => {
323
- const envelope = admissionEnvelope(commandName, args, context ?? (await command("inspect_studio", { scope: "selection" }))?.context);
322
+ const envelope = admissionEnvelope(commandName, args, context ?? await command("inspect_studio", { scope: "selection" }));
324
323
  try {
325
324
  const value = await command(commandName, envelope, options);
326
325
  if (value?.ok === false) throw receiptFailure(value);
@@ -446,7 +445,7 @@ async function runVerb({ client, verb, spec, name, flags }) {
446
445
  // at and the deadline it needs, which --timeout overrides.
447
446
  const inspected = await command("inspect_studio", { scope: "actions", ids: [name] });
448
447
  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 } });
448
+ return admitted("run_action", { action: name, args }, { context: inspected, options: deadline === undefined ? {} : { timeoutMs: deadline } });
450
449
  }
451
450
 
452
451
  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-DwP-tl2x.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-B41FmPYI.js">
40
+ <link rel="modulepreload" crossorigin href="/assets/first-shot-handoff-BlT33CGf.js">
41
+ <link rel="modulepreload" crossorigin href="/assets/scene-asset-sync-BCWDJB1d.js">
42
+ <link rel="stylesheet" crossorigin href="/assets/scene-asset-sync-DjJ_cJ2R.css">
43
+ <link rel="stylesheet" crossorigin href="/assets/app-Do-Og5Pu.css">
44
44
  </head>
45
45
  <body>
46
46
  <div id="root">