cozyclay 2.0.0 → 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 (260) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +17 -2
  3. package/THIRD_PARTY_NOTICES.md +8 -0
  4. package/bin/agent/agent-routes.mjs +219 -38
  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 +6 -5
  19. package/bin/cozyclay.mjs +12 -3
  20. package/bin/live/cli.mjs +2 -3
  21. package/bin/live/client.mjs +1 -1
  22. package/bin/live/studio.mjs +5 -2
  23. package/bin/mcp-runtime.mjs +12 -2
  24. package/bin/telemetry-state.mjs +3 -3
  25. package/dist/app/index.html +6 -6
  26. package/dist/assets/analytics-B41FmPYI.js +1 -0
  27. package/dist/assets/app-Do-Og5Pu.css +1 -0
  28. package/dist/assets/app-DwP-tl2x.js +4920 -0
  29. package/dist/assets/{demo-ByeYp-dY.js → demo-Dt0qZfS7.js} +1 -1
  30. package/dist/assets/{first-shot-handoff-DgT28ZiV.js → first-shot-handoff-BlT33CGf.js} +1 -1
  31. package/dist/assets/{landing-BaXkpFip.js → landing-BSesrCpL.js} +1 -1
  32. package/dist/assets/scene-asset-sync-BCWDJB1d.js +19 -0
  33. package/dist/assets/{shot-prompt-CbqWcWYi.css → scene-asset-sync-DjJ_cJ2R.css} +1 -1
  34. package/dist/assets/{ticket-BcmsnHn5.js → ticket-jqMhqL88.js} +1 -1
  35. package/dist/assets/workflow-Br-K7Srs.js +179 -0
  36. package/dist/cozyclay-package.json +1 -1
  37. package/dist/index.html +328 -222
  38. package/dist/sitemap.xml +5 -5
  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 +801 -259
  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 +196 -3
  72. package/src/ardy/timeline.jsx +117 -29
  73. package/src/ardy/to-cskel27.js +0 -2
  74. package/src/ardy/waypoints.js +1 -4
  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 +43 -24
  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 +16 -3
  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 +89 -32
  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/generation.js +1 -1
  127. package/src/motion-store.js +4 -4
  128. package/src/motion-trail.js +5 -11
  129. package/src/multimodel-ingest.js +1 -3
  130. package/src/object-catalog.jsx +7 -5
  131. package/src/object-gizmo.jsx +45 -31
  132. package/src/object-path.js +310 -67
  133. package/src/object-travel.js +224 -0
  134. package/src/otio.js +8 -3
  135. package/src/panels/CharacterTransformPanel.jsx +24 -6
  136. package/src/panels/ObjectTransformPanel.jsx +4 -3
  137. package/src/panels/PoseImagePanel.jsx +205 -0
  138. package/src/panels/PromptBlocksPanel.jsx +3 -1
  139. package/src/panels/ProxyFigurePanel.jsx +56 -0
  140. package/src/panels/ReferenceImageField.jsx +1 -1
  141. package/src/panels/RigControlPanel.jsx +24 -3
  142. package/src/panels/RigPanel.jsx +2 -1
  143. package/src/panels/SculptPanel.jsx +405 -0
  144. package/src/panels/SubjectBox.jsx +1 -1
  145. package/src/panels/SubjectsPanel.jsx +0 -2
  146. package/src/panels/VideoCapturePanel.jsx +2 -1
  147. package/src/panels/pose-image.css +42 -0
  148. package/src/panels/pose.css +3 -0
  149. package/src/panels/sculpt.css +162 -0
  150. package/src/plan-diagram.js +184 -0
  151. package/src/planview.jsx +127 -80
  152. package/src/playground.js +3 -5
  153. package/src/pose-fade.js +77 -0
  154. package/src/pose-gizmo.css +23 -0
  155. package/src/pose-gizmo.js +96 -0
  156. package/src/posestudio.jsx +134 -83
  157. package/src/project-browser.css +53 -0
  158. package/src/project-browser.jsx +52 -3
  159. package/src/project.js +55 -24
  160. package/src/props.jsx +177 -36
  161. package/src/proxy-figure.jsx +69 -0
  162. package/src/range-pin-panel.jsx +0 -1
  163. package/src/receipt-findings.js +20 -0
  164. package/src/render-passes.js +0 -1
  165. package/src/resource-status.jsx +1 -1
  166. package/src/root-path.js +48 -0
  167. package/src/route-owner.js +52 -0
  168. package/src/scene-asset-cache.js +1 -2
  169. package/src/scene-assets.js +12 -4
  170. package/src/scene-fbx.js +1 -1
  171. package/src/scene-mesh-cache.js +0 -1
  172. package/src/scene-objects.js +319 -19
  173. package/src/scenes.js +62 -17
  174. package/src/sculpt-blueprint.js +65 -0
  175. package/src/sculpt-compare.js +384 -0
  176. package/src/sculpt-geometry.js +141 -0
  177. package/src/sculpt-mode.js +168 -0
  178. package/src/sculpt-recipe.js +385 -0
  179. package/src/sculpt-run.js +351 -0
  180. package/src/sculpt-stage.jsx +221 -0
  181. package/src/settings-menu.jsx +1 -1
  182. package/src/shell/BoardSlot.jsx +341 -0
  183. package/src/shell/BottomDock.jsx +58 -14
  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/TopBar.jsx +17 -3
  188. package/src/shell/ViewportToolbar.jsx +18 -3
  189. package/src/shell/dock.css +239 -0
  190. package/src/shell/glass-regions.css +3 -2
  191. package/src/shell/log-store.js +4 -4
  192. package/src/shell/mode.css +20 -0
  193. package/src/shell/studio-shell-context.js +4 -1
  194. package/src/shot-authoring.js +69 -6
  195. package/src/stable-items.js +1 -1
  196. package/src/stage-attach-frames.js +169 -0
  197. package/src/stage-canvas-rigs.jsx +402 -0
  198. package/src/stage-capture.jsx +216 -0
  199. package/src/stage-characters.jsx +302 -0
  200. package/src/stage-editor-gizmos.jsx +1229 -0
  201. package/src/stage-labels-ko.js +204 -0
  202. package/src/stage-startup-defaults.js +182 -0
  203. package/src/starter-scenes.js +1 -1
  204. package/src/storyboard.js +24 -5
  205. package/src/studio-actions.js +42 -9
  206. package/src/studio-agent-commands.js +186 -23
  207. package/src/studio-agent-context.js +11 -9
  208. package/src/studio-agent-motion.js +2 -1
  209. package/src/studio-agent-protocol.js +56 -23
  210. package/src/studio-app-binding.js +52 -23
  211. package/src/studio-contact-sheet.js +3 -3
  212. package/src/studio-elements.js +11 -2
  213. package/src/studio-geometry-facts.js +114 -0
  214. package/src/styles.css +111 -10
  215. package/src/theme.js +1 -1
  216. package/src/timeline-fps.js +13 -0
  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 +110 -20
  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/kimodo/effector-constraints.mjs +4 -4
  248. package/tools/kimodo/generate.mjs +1 -1
  249. package/tools/knip-baseline.json +331 -0
  250. package/tools/loc-baseline.json +126 -0
  251. package/tools/process-supervisor.mjs +1 -1
  252. package/tools/projflow/replay.mjs +4 -4
  253. package/tools/projflow/runner.mjs +3 -3
  254. package/tools/qa/studio-control-count.mjs +2 -0
  255. package/tools/run-tests.mjs +96 -7
  256. package/dist/assets/analytics-MVZOEW8O.js +0 -1
  257. package/dist/assets/app-C1sdVbg4.css +0 -1
  258. package/dist/assets/app-CA55tcI3.js +0 -4876
  259. package/dist/assets/shot-prompt-Yrt99wZx.js +0 -17
  260. package/dist/assets/workflow-dNPGY9xz.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), {
@@ -6,11 +6,12 @@ import { join } from "node:path";
6
6
  import { openBrowser } from "./open-browser.mjs";
7
7
  import { writeSecureJson } from "./agent/secure-file.mjs";
8
8
  import { readKeys } from "./agent/provider-keys.mjs";
9
+ import { PROVIDERS } from "./agent/providers.mjs";
9
10
 
10
- export const AUTHORIZE_ENDPOINT = "https://auth.openai.com/oauth/authorize";
11
- export const TOKEN_ENDPOINT = "https://auth.openai.com/oauth/token";
12
- export const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
13
- 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";
14
15
  const DEFAULT_FILE = join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "cozyclay", "codex-auth.json");
15
16
  const tokenFile = process.env.COZYCLAY_CODEX_AUTH_FILE || DEFAULT_FILE;
16
17
  let tokens = null;
@@ -51,7 +52,7 @@ const decodeJwt = (value) => { try { return JSON.parse(Buffer.from(value.split("
51
52
  const claims = () => decodeJwt(readTokens()?.id_token || "");
52
53
  function providersConfigured() {
53
54
  const configured = new Set();
54
- for (const name of ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY", "GOOGLE_API_KEY", "OPENROUTER_API_KEY"]) if (process.env[name]?.trim()) configured.add(name);
55
+ for (const provider of PROVIDERS) for (const name of provider.env) if (process.env[name]?.trim()) configured.add(provider.id);
55
56
  for (const [id, value] of Object.entries(readKeys())) if (typeof value === "string" && value.trim()) configured.add(id);
56
57
  return configured.size;
57
58
  }
package/bin/cozyclay.mjs CHANGED
@@ -407,10 +407,13 @@ process.on("SIGTERM", () => shutdown(143));
407
407
 
408
408
  if (opts.motion && kimodoHost && existsSync(BRIDGE)) {
409
409
  try {
410
+ // The package has no three (a devDependency); the bridge runs from the
411
+ // staged MCP runtime, which carries three, tools/ and src/.
412
+ const bridgeRoot = SOURCE_CHECKOUT ? PKG_ROOT : await ensureMcpRuntime();
410
413
  ({ child: bridge, port: bridgePort } = await startBridge({
411
414
  command: process.execPath,
412
- args: [BRIDGE],
413
- cwd: PKG_ROOT,
415
+ args: [join(bridgeRoot, "tools", "ardy", "bridge.mjs")],
416
+ cwd: bridgeRoot,
414
417
  env: process.env,
415
418
  mainPort: opts.port,
416
419
  onSpawn: (child) => {
@@ -431,6 +434,7 @@ if (opts.motion && kimodoHost && existsSync(BRIDGE)) {
431
434
  }));
432
435
  } catch (err) {
433
436
  console.error(`cozyclay: motion generation sidecar failed: ${err.message}`);
437
+ for (const line of err.lines ?? []) console.error(`cozyclay: ${line}`);
434
438
  console.error("cozyclay: studio did not start; set COZYCLAY_BRIDGE_PORT to an available port or use --no-motion.");
435
439
  if (bridge) await terminateOwned(bridge);
436
440
  process.exit(1);
@@ -455,7 +459,12 @@ server = createServer((req, res) => {
455
459
  return;
456
460
  }
457
461
  if (url.pathname.startsWith("/agent/")) {
458
- void agentHandler(req, res, url.pathname).then((handled) => { if (!handled && !res.writableEnded) { res.writeHead(404); res.end(); } }).catch(() => { if (!res.headersSent) { res.writeHead(502); res.end(JSON.stringify({ error: "agent unavailable" })); } });
462
+ void agentHandler(req, res, url.pathname).then((handled) => { if (!handled && !res.writableEnded) { res.writeHead(404); res.end(); } }).catch((error) => {
463
+ // The pane only sees "502 — agent unavailable"; the reason belongs on
464
+ // the launcher's terminal rather than nowhere.
465
+ console.error(`cozyclay: agent request ${url.pathname} failed: ${error?.message ?? error}`);
466
+ if (!res.headersSent) { res.writeHead(502); res.end(JSON.stringify({ error: "agent unavailable" })); }
467
+ });
459
468
  return;
460
469
  }
461
470
  if (url.pathname === "/__cozyclay/telemetry") {
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]]));
@@ -18,8 +18,9 @@ const MCP_RUNTIME = process.env.COZYCLAY_MCP_RUNTIME_DIR || join(
18
18
  function probeMcpRuntime(runtime = MCP_RUNTIME) {
19
19
  const server = join(runtime, "mcp", "server.mjs");
20
20
  if (!existsSync(server)) throw new Error("MCP server is missing");
21
+ if (!existsSync(join(runtime, "tools", "ardy", "bridge.mjs"))) throw new Error("motion bridge is missing");
21
22
  const runtimeRequire = createRequire(join(runtime, "package.json"));
22
- 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);
23
24
  return server;
24
25
  }
25
26
 
@@ -46,6 +47,15 @@ function installMcpRuntime() {
46
47
  // The server publishes its live endpoint file from this module.
47
48
  copyFileSync(join(PKG_ROOT, "bin", "live-endpoint.mjs"), join(staging, "bin", "live-endpoint.mjs"));
48
49
  cpSync(join(PKG_ROOT, "src"), join(staging, "src"), { recursive: true });
50
+ // The motion bridge imports three, which only this runtime resolves, so the
51
+ // package launches it from here; its out/ scratch dirs are never copied.
52
+ for (const dir of ["ardy", "kimodo", "projflow"]) {
53
+ cpSync(join(PKG_ROOT, "tools", dir), join(staging, "tools", dir), { recursive: true, filter: (path) => basename(path) !== "out" });
54
+ }
55
+ copyFileSync(join(PKG_ROOT, "tools", "process-supervisor.mjs"), join(staging, "tools", "process-supervisor.mjs"));
56
+ // gvhmr-floor.mjs loads its FBX rigs from ../../{public,dist}/models.
57
+ const models = join(PKG_ROOT, "dist", "models");
58
+ if (existsSync(models)) cpSync(models, join(staging, "dist", "models"), { recursive: true });
49
59
 
50
60
  const npm = process.platform === "win32" ? "npm.cmd" : "npm";
51
61
  const child = spawn(npm, ["ci", "--no-audit", "--no-fund"], {
@@ -73,7 +83,7 @@ function installMcpRuntime() {
73
83
  } catch {
74
84
  try {
75
85
  renameSync(staging, MCP_RUNTIME);
76
- } catch (error) {
86
+ } catch {
77
87
  // Another first run can publish its complete staging
78
88
  // directory first. Accept that winner only after the
79
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-CA55tcI3.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-Yrt99wZx.js">
42
- <link rel="stylesheet" crossorigin href="/assets/shot-prompt-CbqWcWYi.css">
43
- <link rel="stylesheet" crossorigin href="/assets/app-C1sdVbg4.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">