cozyclay 1.10.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. package/CHANGELOG.md +193 -0
  2. package/bin/agent/agent-routes.mjs +82 -72
  3. package/bin/agent/agent-runner.mjs +10 -3
  4. package/bin/agent/motion-runtime.mjs +32 -260
  5. package/bin/agent/providers.mjs +46 -3
  6. package/bin/agent/session-store.mjs +41 -5
  7. package/bin/agent/studio-prompt.mjs +1 -1
  8. package/bin/agent/studio-tools.mjs +48 -11
  9. package/bin/codex-auth.mjs +2 -1
  10. package/bin/cozyclay.mjs +38 -11
  11. package/bin/live/cli.mjs +22 -3
  12. package/bin/mcp-runtime.mjs +45 -20
  13. package/bin/telemetry-state.mjs +5 -2
  14. package/bin/update-check.mjs +15 -0
  15. package/dist/app/index.html +6 -6
  16. package/dist/assets/analytics-MVZOEW8O.js +1 -0
  17. package/dist/assets/app-BxBLC6GT.js +4876 -0
  18. package/dist/assets/app-CJdxvnrl.css +1 -0
  19. package/dist/assets/{demo-B-eg1CT8.js → demo-ByeYp-dY.js} +1 -1
  20. package/dist/assets/{first-shot-handoff-CGgnvNu8.js → first-shot-handoff-DgT28ZiV.js} +1 -1
  21. package/dist/assets/{landing-IbRp3FnQ.js → landing-BaXkpFip.js} +1 -1
  22. package/dist/assets/shot-prompt-CbqWcWYi.css +1 -0
  23. package/dist/assets/shot-prompt-hN0j_O-p.js +17 -0
  24. package/dist/assets/{ticket-BgIzOBOf.js → ticket-BcmsnHn5.js} +1 -1
  25. package/dist/assets/workflow-B5XZwVfe.js +179 -0
  26. package/dist/assets/workflow-DRdwoDBT.css +1 -0
  27. package/dist/cozyclay-package.json +1 -1
  28. package/dist/fonts/IBM-Plex-Sans-OFL.txt +92 -0
  29. package/dist/fonts/JetBrains-Mono-OFL.txt +93 -0
  30. package/dist/fonts/README.md +13 -9
  31. package/dist/fonts/ibm-plex-sans-400-latin.woff2 +0 -0
  32. package/dist/fonts/ibm-plex-sans-500-latin.woff2 +0 -0
  33. package/dist/fonts/ibm-plex-sans-600-latin.woff2 +0 -0
  34. package/dist/fonts/jetbrains-mono-400-latin.woff2 +0 -0
  35. package/dist/fonts/jetbrains-mono-500-latin.woff2 +0 -0
  36. package/dist/index.html +7 -7
  37. package/dist/privacy/index.html +13 -8
  38. package/dist/sitemap.xml +2 -2
  39. package/dist/workflow/index.html +5 -5
  40. package/mcp/live-hub.mjs +35 -93
  41. package/mcp/mesh-file.mjs +48 -0
  42. package/mcp/server.mjs +6 -77
  43. package/mcp/tool-handlers.mjs +219 -376
  44. package/package.json +2 -1
  45. package/src/App.jsx +1275 -9086
  46. package/src/analytics.js +393 -9
  47. package/src/app-context.js +211 -0
  48. package/src/app-stage.jsx +25 -24
  49. package/src/ardy/auto-fix-panel.css +108 -0
  50. package/src/ardy/fix-collisions.js +10 -66
  51. package/src/ardy/ik-drag.js +101 -0
  52. package/src/ardy/ik-key-json.js +43 -0
  53. package/src/ardy/ik.js +97 -67
  54. package/src/ardy/physics-panel.css +2 -0
  55. package/src/ardy/physics-panel.jsx +17 -5
  56. package/src/ardy/platform-fit-panel.css +64 -0
  57. package/src/ardy/platform-fit-panel.jsx +40 -0
  58. package/src/ardy/platform-fit.js +326 -0
  59. package/src/ardy/playback.js +166 -20
  60. package/src/ardy/range-pin.js +299 -0
  61. package/src/ardy/timeline-coordinates.js +26 -0
  62. package/src/ardy/timeline.css +1214 -0
  63. package/src/ardy/timeline.jsx +152 -41
  64. package/src/ardy/waypoints.js +3 -3
  65. package/src/asset-pane.css +462 -0
  66. package/src/asset-pane.jsx +461 -232
  67. package/src/camera-tutorial.jsx +38 -18
  68. package/src/command-bus.js +349 -0
  69. package/src/commands/ai.js +31 -0
  70. package/src/commands/cast.js +140 -0
  71. package/src/commands/elements/character.js +13 -0
  72. package/src/commands/elements/motion.js +14 -0
  73. package/src/commands/elements/object.js +22 -0
  74. package/src/commands/elements/scene.js +16 -0
  75. package/src/commands/elements/shot.js +9 -0
  76. package/src/commands/elements/stage.js +11 -0
  77. package/src/commands/elements.js +155 -0
  78. package/src/commands/export.js +20 -0
  79. package/src/commands/index.js +40 -0
  80. package/src/commands/motion.js +231 -0
  81. package/src/commands/objects.js +174 -0
  82. package/src/commands/project.js +53 -0
  83. package/src/commands/scene.js +81 -0
  84. package/src/commands/shared.js +19 -0
  85. package/src/commands/shot.js +103 -0
  86. package/src/commands/stage.js +17 -0
  87. package/src/commands/view.js +42 -0
  88. package/src/document-store.js +144 -0
  89. package/src/domains/cast.js +1007 -0
  90. package/src/domains/motion.js +3219 -0
  91. package/src/domains/objects.js +932 -0
  92. package/src/domains/scenes.js +823 -0
  93. package/src/domains/shots.js +404 -0
  94. package/src/domains/stage.js +93 -0
  95. package/src/dualview.jsx +113 -57
  96. package/src/facing-marks.js +3 -0
  97. package/src/first-success-guide.jsx +14 -14
  98. package/src/grid-view.js +18 -5
  99. package/src/hierarchy-panel.css +386 -0
  100. package/src/hierarchy-panel.jsx +88 -19
  101. package/src/{fal-motion-client.js → i2v-motion-client.js} +13 -13
  102. package/src/i2v-motion-studio.jsx +180 -0
  103. package/src/ik-camera.js +3 -0
  104. package/src/main.jsx +6 -0
  105. package/src/motion/generation.js +105 -0
  106. package/src/motion-readiness-ui.jsx +4 -4
  107. package/src/motion-readiness.js +11 -0
  108. package/src/motion-trail.js +366 -9
  109. package/src/object-gizmo.jsx +7 -1
  110. package/src/panels/CameraPanel.jsx +60 -0
  111. package/src/panels/CharacterTransformPanel.jsx +48 -0
  112. package/src/panels/EnvironmentPanel.jsx +40 -0
  113. package/src/panels/Foldout.jsx +32 -0
  114. package/src/panels/LightPanel.jsx +24 -0
  115. package/src/panels/ObjectTransformPanel.jsx +554 -0
  116. package/src/panels/PosePanel.jsx +97 -0
  117. package/src/panels/ProjectPanel.jsx +33 -0
  118. package/src/panels/PromptBlocksPanel.jsx +442 -0
  119. package/src/panels/PropsPanel.jsx +70 -0
  120. package/src/panels/ReferenceImageField.jsx +91 -0
  121. package/src/panels/RigControlPanel.jsx +78 -0
  122. package/src/panels/RigPanel.jsx +37 -0
  123. package/src/panels/SubjectBox.jsx +47 -0
  124. package/src/panels/SubjectsPanel.jsx +39 -0
  125. package/src/panels/VideoCapturePanel.jsx +180 -0
  126. package/src/panels/details.css +1171 -0
  127. package/src/panels/motion.css +197 -0
  128. package/src/panels/pose.css +39 -0
  129. package/src/planview.jsx +60 -31
  130. package/src/posestudio.jsx +59 -4
  131. package/src/project-browser.css +1073 -0
  132. package/src/project-browser.jsx +288 -123
  133. package/src/range-pin-object-transform.js +19 -0
  134. package/src/range-pin-panel.css +518 -0
  135. package/src/range-pin-panel.jsx +344 -0
  136. package/src/result-modal.jsx +5 -5
  137. package/src/room.jsx +79 -51
  138. package/src/scenes.js +9 -8
  139. package/src/semantic-edit.js +2 -0
  140. package/src/settings-menu.jsx +39 -105
  141. package/src/shell/BottomDock.jsx +450 -0
  142. package/src/shell/DetailsSlot.jsx +513 -0
  143. package/src/shell/LibrarySlot.jsx +86 -0
  144. package/src/shell/MenuBar.jsx +601 -0
  145. package/src/shell/OutlinerSlot.jsx +50 -0
  146. package/src/shell/PreferencesDialog.jsx +538 -0
  147. package/src/shell/PreferencesSlot.jsx +40 -0
  148. package/src/shell/StatusBar.jsx +125 -0
  149. package/src/shell/StudioShell.jsx +92 -0
  150. package/src/shell/TopBar.jsx +129 -0
  151. package/src/shell/ViewportToolbar.jsx +656 -0
  152. package/src/shell/agent-glass.css +296 -0
  153. package/src/shell/dock.css +93 -0
  154. package/src/shell/glass-regions.css +985 -0
  155. package/src/shell/glass.css +687 -0
  156. package/src/shell/log-store.js +52 -0
  157. package/src/shell/mode.css +9 -0
  158. package/src/shell/preferences.css +651 -0
  159. package/src/shell/shell.css +282 -0
  160. package/src/shell/studio-shell-context.js +19 -0
  161. package/src/shell/topbar.css +435 -0
  162. package/src/shell/viewport.css +451 -0
  163. package/src/store/authored-intent.js +22 -0
  164. package/src/store/runtime-adapters.js +92 -0
  165. package/src/store/scene-stage.js +21 -0
  166. package/src/store/use-document-store.js +14 -0
  167. package/src/studio-actions.js +210 -0
  168. package/src/studio-agent-commands.js +38 -268
  169. package/src/studio-agent-context.js +38 -9
  170. package/src/studio-agent-motion.js +122 -378
  171. package/src/studio-agent-protocol.js +66 -30
  172. package/src/studio-app-binding.js +379 -0
  173. package/src/studio-contact-sheet.js +73 -0
  174. package/src/studio-elements.js +58 -51
  175. package/src/styles/themes.css +200 -0
  176. package/src/styles/tokens.css +69 -0
  177. package/src/styles.css +456 -1748
  178. package/src/theme.js +44 -0
  179. package/src/timeline-extent.js +16 -0
  180. package/src/timeline-fps.js +13 -0
  181. package/src/trail-key-conflicts.js +39 -0
  182. package/src/trail-pick.js +59 -0
  183. package/src/ui.jsx +8 -8
  184. package/src/use-case-question.jsx +66 -0
  185. package/src/workflow/AgentPanel.jsx +86 -71
  186. package/src/workflow/agent-client.js +12 -2
  187. package/src/workflow/agent-panel.css +34 -32
  188. package/src/workflow/cozy-scene-node.css +2 -0
  189. package/src/workflow/workflow.css +2 -0
  190. package/tools/ardy/bridge.mjs +164 -146
  191. package/tools/ardy/visual-qa.mjs +5 -5
  192. package/tools/bench/EXP3.md +61 -0
  193. package/tools/bench/cclay_bench_extract_incam.py +125 -0
  194. package/tools/bench/cclay_bench_extract_obs.py +204 -0
  195. package/tools/bench/cclay_bench_runner.py +42 -0
  196. package/tools/bench/cube-contact.mjs +162 -0
  197. package/tools/bench/exp3.mjs +152 -0
  198. package/tools/bench/extract-bench-lib.mjs +216 -0
  199. package/tools/bench/extract-bench.mjs +302 -0
  200. package/tools/bench/fal-generate.mjs +88 -0
  201. package/tools/bench/fit/README.md +189 -0
  202. package/tools/bench/fit/camera.mjs +59 -0
  203. package/tools/bench/fit/contact.mjs +419 -0
  204. package/tools/bench/fit/footlock.mjs +234 -0
  205. package/tools/bench/fit/motion.mjs +85 -0
  206. package/tools/bench/fit/pin.mjs +27 -0
  207. package/tools/bench/fit/remote.mjs +105 -0
  208. package/tools/bench/fit-bench.mjs +126 -0
  209. package/tools/bench/fit-sanity.mjs +45 -0
  210. package/tools/bench/metrics.mjs +313 -0
  211. package/tools/bench/obs/depth.mjs +258 -0
  212. package/tools/bench/obs/extrinsics.mjs +175 -0
  213. package/tools/bench/obs/fit_mannequin_betas.py +340 -0
  214. package/tools/bench/obs/ground.mjs +294 -0
  215. package/tools/bench/obs/heading.mjs +151 -0
  216. package/tools/bench/obs/ladder.mjs +555 -0
  217. package/tools/bench/obs/mannequin-betas.json +124 -0
  218. package/tools/bench/obs/remote.mjs +83 -0
  219. package/tools/bench/obs/rest_joints.py +52 -0
  220. package/tools/bench/obs/ybot-targets.mjs +49 -0
  221. package/tools/bench/obs-bench.mjs +520 -0
  222. package/tools/bench/score.mjs +343 -0
  223. package/tools/bench/summarize.mjs +86 -0
  224. package/tools/dev/pages/privacy.html +13 -8
  225. package/tools/gt-render/browser.mjs +159 -0
  226. package/tools/gt-render/camera-math.mjs +276 -0
  227. package/tools/gt-render/page.mjs +267 -0
  228. package/tools/gt-render/render.mjs +514 -0
  229. package/tools/gt-render/scene-box.mjs +31 -0
  230. package/tools/gt-render/take-transform.mjs +87 -0
  231. package/tools/qa/css-rule-usage.mjs +283 -0
  232. package/tools/qa/studio-control-count.mjs +70 -32
  233. package/tools/run-tests.mjs +60 -3
  234. package/tools/track/DESIGN.md +513 -0
  235. package/tools/track/backfill-provenance.mjs +116 -0
  236. package/tools/track/budget.mjs +8 -0
  237. package/tools/track/check-rig.mjs +209 -0
  238. package/tools/track/diagnostics.schema.json +60 -0
  239. package/tools/track/export-rig.mjs +256 -0
  240. package/tools/track/fallback.mjs +7 -0
  241. package/tools/track/fk-parity-fixture.mjs +161 -0
  242. package/tools/track/gate.mjs +349 -0
  243. package/tools/track/masks.mjs +318 -0
  244. package/tools/track/metrics.mjs +201 -0
  245. package/tools/track/publish-obs.mjs +108 -0
  246. package/tools/track/py/check_env.py +61 -0
  247. package/tools/track/py/eval_lr.py +229 -0
  248. package/tools/track/py/lr_viterbi.py +294 -0
  249. package/tools/track/py/masks.py +405 -0
  250. package/tools/track/py/objective.py +382 -0
  251. package/tools/track/py/rig.py +508 -0
  252. package/tools/track/py/scene.py +326 -0
  253. package/tools/track/py/test_joint_indices.py +123 -0
  254. package/tools/track/py/test_lr_viterbi.py +195 -0
  255. package/tools/track/py/test_masks.py +229 -0
  256. package/tools/track/py/test_rig.py +288 -0
  257. package/tools/track/py/test_scene.py +354 -0
  258. package/tools/track/py/test_track.py +328 -0
  259. package/tools/track/py/track.py +411 -0
  260. package/tools/track/remote.mjs +140 -0
  261. package/tools/track/rig-dump.mjs +227 -0
  262. package/tools/track/run-box-tests.mjs +14 -0
  263. package/tools/track/run-box.mjs +168 -0
  264. package/tools/track/setup-box.sh +59 -0
  265. package/tools/track/study-2d.mjs +728 -0
  266. package/dist/assets/analytics-B1hnH66c.js +0 -1
  267. package/dist/assets/app-Cu1cexZR.css +0 -1
  268. package/dist/assets/app-Dxdh8kds.js +0 -4861
  269. package/dist/assets/shot-prompt-C_g2BHVc.css +0 -1
  270. package/dist/assets/shot-prompt-HVJRP4Gr.js +0 -17
  271. package/dist/assets/workflow-CJd-PDtV.js +0 -179
  272. package/dist/assets/workflow-DsudxKHF.css +0 -1
  273. package/src/fal-motion-studio.jsx +0 -180
  274. package/src/scene-history.js +0 -129
  275. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
package/mcp/live-hub.mjs CHANGED
@@ -7,98 +7,19 @@ import { mcpToolCategory } from "../src/execution-telemetry.js";
7
7
  import { WebSocket, WebSocketServer } from "ws";
8
8
 
9
9
  export const DEFAULT_COMMAND_TIMEOUT_MS = 5_000;
10
- /** A per-call override may extend a command, never past this ceiling: a hub
11
- * that waits longer than this can no longer tell a slow editor from a dead one. */
10
+ /** A per-call override may extend a non-motion command, never past this ceiling. */
12
11
  export const MAX_COMMAND_TIMEOUT_MS = 300_000;
13
12
  export const DEFAULT_HEARTBEAT_MS = 15_000;
14
13
  export const RUN_WORKFLOW_TIMEOUT_MS = 180_000;
15
14
  export const LOAD_MOTION_TIMEOUT_MS = 30_000;
16
15
  export const IMPORT_ASSET_TIMEOUT_MS = 30_000;
17
16
  export const CAPTURE_FRAME_TIMEOUT_MS = 30_000;
18
- export const MOTION_JOB_TTL_MS = 10 * 60_000;
19
- export const MOTION_JOB_POLL_INTERVAL_MS = 0;
20
- export const MAX_ACTIVE_MOTION_JOBS = 2;
21
- export const MAX_ACTIVE_MOTION_JOBS_PER_WORKSPACE = 1;
22
-
23
- const terminalMotionStatuses = new Set(["completed", "failed", "cancelled", "expired"]);
24
-
25
- /** MCP-internal job retention for push-only motion work. Its clock is injected
26
- * so expiry is deterministic without a polling loop or timing-based test. */
27
- export class MotionJobRegistry {
28
- constructor({ clock = () => Date.now(), ttlMs = MOTION_JOB_TTL_MS } = {}) {
29
- this.clock = clock;
30
- this.ttlMs = ttlMs;
31
- this.jobs = new Map();
32
- }
33
-
34
- create(workspaceId) {
35
- this.cleanup();
36
- const active = [...this.jobs.values()].filter((job) => !terminalMotionStatuses.has(job.status));
37
- if (active.length >= MAX_ACTIVE_MOTION_JOBS) throw new Error(`Motion job capacity reached (${MAX_ACTIVE_MOTION_JOBS} active globally).`);
38
- if (active.filter((job) => job.workspaceId === workspaceId).length >= MAX_ACTIVE_MOTION_JOBS_PER_WORKSPACE) {
39
- throw new Error(`This workspace already has an active motion job.`);
40
- }
41
- const now = this.clock();
42
- const job = {
43
- taskId: randomUUID(), workspaceId, status: "queued", createdAt: now,
44
- lastUpdatedAt: now, ttlMs: this.ttlMs, pollIntervalMs: MOTION_JOB_POLL_INTERVAL_MS,
45
- cancel: null, expiresAt: null, outcome: null,
46
- deliveredWorkspaceIds: new Set(),
47
- installationStates: new Map(),
48
- };
49
- this.jobs.set(job.taskId, job);
50
- return job;
51
- }
52
-
53
- task(job) {
54
- return {
55
- taskId: job.taskId, status: job.status, createdAt: job.createdAt,
56
- lastUpdatedAt: job.lastUpdatedAt, ttlMs: job.ttlMs, pollIntervalMs: job.pollIntervalMs,
57
- };
58
- }
59
-
60
- transition(job, status, outcome = null) {
61
- this.cleanup();
62
- job.status = status;
63
- job.lastUpdatedAt = this.clock();
64
- job.outcome = outcome;
65
- job.expiresAt = terminalMotionStatuses.has(status) ? job.lastUpdatedAt + job.ttlMs : null;
66
- if (job.expiresAt !== null) {
67
- const timer = setTimeout(() => this.cleanup(), job.ttlMs);
68
- timer.unref?.();
69
- }
70
- return this.task(job);
71
- }
72
-
73
- cancel(taskId, workspaceId) {
74
- this.cleanup();
75
- const job = this.jobs.get(taskId);
76
- if (!job || job.workspaceId !== workspaceId || terminalMotionStatuses.has(job.status)) return null;
77
- job.cancel?.();
78
- return this.transition(job, "cancelled", { message: "Generation cancelled before editor delivery." });
79
- }
80
-
81
- forWorkspace(workspaceId) {
82
- this.cleanup();
83
- return [...this.jobs.values()].filter((job) =>
84
- job.workspaceId === workspaceId && terminalMotionStatuses.has(job.status) && job.status !== "expired" && !job.deliveredWorkspaceIds.has(workspaceId));
85
- }
86
-
87
- cleanup() {
88
- const now = this.clock();
89
- for (const job of this.jobs.values()) {
90
- if (job.expiresAt === null || now < job.expiresAt) continue;
91
- if (job.status === "expired") {
92
- this.jobs.delete(job.taskId);
93
- continue;
94
- }
95
- job.status = "expired";
96
- job.lastUpdatedAt = now;
97
- job.outcome = { message: "Motion job outcome expired before this workspace reconnected." };
98
- job.expiresAt = now + job.ttlMs;
99
- }
100
- }
101
- }
17
+ export const STUDIO_COMMAND_TIMEOUT_MS = 30_000;
18
+ const studioCommands = new Set([
19
+ "inspect_studio", "operate_studio", "arrange_objects", "arrange_characters", "patch_elements",
20
+ "frame_shot", "verify_result", "undo_edit", "read_studio_context", "resolve_studio_image",
21
+ "capture_framing_png", "reconcile_studio_command", "run_action",
22
+ ]);
102
23
 
103
24
  const mutationCommands = new Set([
104
25
  "set_camera",
@@ -114,8 +35,8 @@ const mutationCommands = new Set([
114
35
  "set_prompt_blocks",
115
36
  "load_motion",
116
37
  "load_scenes",
117
- "operate_studio", "arrange_objects", "arrange_characters", "patch_elements", "frame_shot", "undo_edit",
118
- "commit_motion_candidate", "import_asset",
38
+ "operate_studio", "arrange_objects", "arrange_characters", "patch_elements", "frame_shot", "undo_edit", "run_action",
39
+ "import_asset",
119
40
  ]);
120
41
 
121
42
  export class LiveMutationUncertainError extends Error {
@@ -217,8 +138,7 @@ export class LiveHub {
217
138
  }
218
139
 
219
140
  static commandTimeoutMs(name) {
220
- if (name === "load_motion" || name === "prepare_motion_install") return LOAD_MOTION_TIMEOUT_MS;
221
- if (name === "verify_motion_candidate" || name === "repair_motion_candidate") return 60_000;
141
+ if (name === "load_motion") return LOAD_MOTION_TIMEOUT_MS;
222
142
  // A 32 MiB mesh as a data URL will not decode, store and stand in 5 s.
223
143
  if (name === "import_asset") return IMPORT_ASSET_TIMEOUT_MS;
224
144
  // Close two-person shots (OTS) raycast two skinned rigs over a full-frame
@@ -226,6 +146,7 @@ export class LiveHub {
226
146
  if (name === "capture_frame") return CAPTURE_FRAME_TIMEOUT_MS;
227
147
  // A workflow run captures a frame and may generate an image upstream.
228
148
  if (name === "run_workflow") return RUN_WORKFLOW_TIMEOUT_MS;
149
+ if (studioCommands.has(name)) return STUDIO_COMMAND_TIMEOUT_MS;
229
150
  return DEFAULT_COMMAND_TIMEOUT_MS;
230
151
  }
231
152
 
@@ -284,6 +205,7 @@ export class LiveHub {
284
205
  let appliedEmitted = false;
285
206
  let failure = null;
286
207
  let description;
208
+ let studioContext;
287
209
  let mutation;
288
210
  let acknowledgedMutation = false;
289
211
  const emitApplied = () => {
@@ -294,14 +216,30 @@ export class LiveHub {
294
216
  const observer = {
295
217
  hub: this, handle,
296
218
  before(command) {
297
- if (!LiveHub.commandMayMutate(command) || command === "load_motion") return false;
219
+ if (!LiveHub.commandMayMutate(command) || command === "load_motion" || command === "run_action") return false;
298
220
  mutation = { name: command, before: mutationState(command, description), acknowledged: false };
299
221
  description = undefined;
300
222
  return mutation.before === undefined;
301
223
  },
302
224
  baseline(value) { if (mutation) mutation.before = mutationState(mutation.name, value); },
303
225
  receipt(command, value) {
304
- if (command === "describe") {
226
+ if (command === "inspect_studio") {
227
+ studioContext = value?.context;
228
+ } else if (command === "run_action") {
229
+ // The bus attests application itself. No legacy describe diff or
230
+ // post-ack read may turn an admitted refusal into uncertainty.
231
+ if (value?.ok === false) {
232
+ failure = value.mutated === "unknown" || value.code === "UNCERTAIN_APPLY" ? "uncertain" : "failed";
233
+ } else if (value?.ok === true) {
234
+ acknowledgedMutation = true;
235
+ // A journal replay has already reached its after revision; it
236
+ // does not count as another application on this invocation.
237
+ if (studioContext && ((value.authored && value.revision.after > studioContext.revision.scene) ||
238
+ (value.nextHost && ["workspaceId", "documentEpoch", "sceneId", "sceneEpoch"].some(key => value.nextHost[key] !== studioContext.host[key])))) applied = true;
239
+ if (value.output?.rolledBack === true || value.output?.failed?.length > 0) failure = "failed";
240
+ }
241
+ studioContext = undefined;
242
+ } else if (command === "describe") {
305
243
  if (mutation?.acknowledged && !mutation.rolledBack) {
306
244
  const after = mutationState(mutation.name, value);
307
245
  if (mutation.before !== undefined && after !== undefined && !isDeepStrictEqual(mutation.before, after)) applied = true;
@@ -336,6 +274,7 @@ export class LiveHub {
336
274
  emitApplied();
337
275
  // Never retain scene data with an asynchronous motion job.
338
276
  description = undefined;
277
+ studioContext = undefined;
339
278
  mutation = null;
340
279
  });
341
280
  emit("mcp:tool_requested", { request_id: requestId, tool_category: category });
@@ -389,6 +328,8 @@ export class LiveHub {
389
328
  if (!socket || socket.readyState !== WebSocket.OPEN) throw hubError("STALE_HANDLE", `Unknown or stale live workspace handle "${handle}".`);
390
329
 
391
330
  const id = randomUUID();
331
+ // The caller's per-call deadline (a controller frame's timeoutMs, or a
332
+ // command's declared one), capped at the ceiling; else the name's default.
392
333
  const bound = Number.isFinite(timeoutMs) && timeoutMs > 0
393
334
  ? Math.min(timeoutMs, MAX_COMMAND_TIMEOUT_MS)
394
335
  : LiveHub.commandTimeoutMs(name);
@@ -401,7 +342,8 @@ export class LiveHub {
401
342
  : hubError("TIMEOUT", message);
402
343
  reject(error);
403
344
  }, bound);
404
- this.pending.set(id, { name, socket, resolve, reject, timer });
345
+ // Each waiting frame keeps the deadline it runs under.
346
+ this.pending.set(id, { name, socket, resolve, reject, timer, timeoutMs: bound });
405
347
  try {
406
348
  socket.send(JSON.stringify({ type: "cmd", id, name, args }));
407
349
  } catch (error) {
@@ -0,0 +1,48 @@
1
+ import { constants as fsConstants } from "node:fs";
2
+ import { open as openFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { basename, join, resolve } from "node:path";
5
+
6
+ import { ASSET_MAX_SOURCE_BYTES } from "../src/scene-assets.js";
7
+ import { classifyMeshBytes, meshMimeForKind } from "../src/mesh-sniff.js";
8
+
9
+ export function normalizeMeshPath(raw, { home = homedir() } = {}) {
10
+ if (typeof raw !== "string" || !raw.trim()) throw new Error("A file path is required.");
11
+ let path = raw.trim();
12
+ if (path.startsWith("file:")) {
13
+ try {
14
+ const url = new URL(path);
15
+ if (url.protocol === "file:") path = decodeURIComponent(url.pathname);
16
+ } catch {
17
+ path = path.replace(/^file:\/\//, "");
18
+ }
19
+ }
20
+ if (path === "~") path = home;
21
+ else if (path.startsWith("~/")) path = join(home, path.slice(2));
22
+ return resolve(path);
23
+ }
24
+
25
+ export async function readMeshFromPath(raw) {
26
+ const path = normalizeMeshPath(raw);
27
+ let file;
28
+ try {
29
+ file = await openFile(path, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
30
+ } catch (error) {
31
+ throw new Error(`Could not read that file: ${error.message}`);
32
+ }
33
+ try {
34
+ const stat = await file.stat();
35
+ if (stat.isDirectory()) throw new Error("That path is a directory, not a 3D model.");
36
+ if (stat.size > ASSET_MAX_SOURCE_BYTES) {
37
+ throw new Error(`That model is too large to import — larger than ${Math.round(ASSET_MAX_SOURCE_BYTES / (1024 * 1024))} MB`);
38
+ }
39
+ const bytes = await file.readFile();
40
+ const kind = classifyMeshBytes(bytes);
41
+ if (kind === "fbx-old") throw new Error("That FBX file is too old to import");
42
+ const mimeType = meshMimeForKind(kind);
43
+ if (!kind || !mimeType) throw new Error("That file is not a 3D model");
44
+ return { path, name: basename(path), bytes, kind, mimeType };
45
+ } finally {
46
+ await file.close().catch(() => {});
47
+ }
48
+ }
package/mcp/server.mjs CHANGED
@@ -48,7 +48,7 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
48
48
  import { ErrorCode, InitializeRequestSchema, LATEST_PROTOCOL_VERSION, McpError } from "@modelcontextprotocol/sdk/types.js";
49
49
  import { z } from "zod";
50
50
 
51
- import { LiveMutationUncertainError, MotionJobRegistry, startLiveHub } from "./live-hub.mjs";
51
+ import { startLiveHub } from "./live-hub.mjs";
52
52
  import {
53
53
  cleanupCaptureArtifacts,
54
54
  createToolHandlers,
@@ -61,7 +61,6 @@ import {
61
61
 
62
62
  /* ------------------------------ process ---------------------------------- */
63
63
 
64
- const motionJobs = new MotionJobRegistry();
65
64
  sweepCaptureArtifacts();
66
65
  process.once("exit", cleanupCaptureArtifacts);
67
66
  for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => { cleanupCaptureArtifacts(); process.exit(128 + (signal === "SIGINT" ? 2 : 15)); });
@@ -71,73 +70,6 @@ const projectRootPromise = realpath(configuredProjectRoot).then((root) => {
71
70
  return root;
72
71
  });
73
72
 
74
- /* ----------------------------- motion jobs ------------------------------- */
75
-
76
- const motionJobEvent = (job) => ({
77
- ...motionJobs.task(job),
78
- ...(job.outcome === null ? {} : { outcome: job.outcome }),
79
- });
80
-
81
- const sendMotionJobEvent = (job) => {
82
- try {
83
- if (liveHub?.sendEvent(job.workspaceId, "motion_job", motionJobEvent(job)) > 0) {
84
- job.deliveredWorkspaceIds.add(job.workspaceId);
85
- }
86
- } catch (error) {
87
- console.error(`[motion_job] lifecycle event delivery failed: ${error instanceof Error ? error.message : String(error)}`);
88
- }
89
- };
90
-
91
- const publishMotionJob = async (job) => {
92
- if (job.deliveredWorkspaceIds.has(job.workspaceId)) return;
93
- const outcome = job.outcome;
94
- if (!liveHub || job.status !== "completed" || typeof outcome?.motionUrl !== "string") {
95
- sendMotionJobEvent(job);
96
- return;
97
- }
98
- const installationState = job.installationStates.get(job.workspaceId);
99
- if (installationState === "installing") return;
100
- if (installationState === "installed") {
101
- sendMotionJobEvent(job);
102
- return;
103
- }
104
- const handle = liveHub.handleForWorkspaceId(job.workspaceId);
105
- if (!handle) return;
106
- job.installationStates.set(job.workspaceId, "installing");
107
- try {
108
- await liveHub.command("load_motion", {
109
- url: outcome.motionUrl,
110
- prompt: outcome.prompt ?? "",
111
- blocks: outcome.blocks ?? [],
112
- drop: outcome.drop ?? null,
113
- characterId: outcome.targetCharacterId,
114
- }, handle);
115
- } catch (error) {
116
- const uncertain = error instanceof LiveMutationUncertainError;
117
- job.installationStates.set(job.workspaceId, uncertain ? "uncertain" : "failed");
118
- motionJobs.transition(job, "failed", {
119
- message: uncertain
120
- ? "The editor connection was lost while installing the completed take. Installation may have applied, so it will not be retried."
121
- : `The editor rejected the completed take: ${error instanceof Error ? error.message : String(error)}`,
122
- });
123
- sendMotionJobEvent(job);
124
- return;
125
- }
126
- job.installationStates.set(job.workspaceId, "installed");
127
- // Generation already succeeded. Application is a separate, acknowledged
128
- // editor mutation; rejected or uncertain installation never reaches here.
129
- job.motionRequest?.apply();
130
- sendMotionJobEvent(job);
131
- };
132
-
133
- const cancelMotionJob = ({ workspaceId, payload }) => {
134
- if (typeof payload.taskId !== "string") return;
135
- const task = motionJobs.cancel(payload.taskId, workspaceId);
136
- if (!task) return;
137
- const job = motionJobs.jobs.get(task.taskId);
138
- if (job) void publishMotionJob(job);
139
- };
140
-
141
73
  /* ------------------------------- server ---------------------------------- */
142
74
 
143
75
  /** Tool registrations are counted as they happen so the HTTP status page can
@@ -152,7 +84,9 @@ const server = new McpServer(
152
84
  "vocabulary (shot size, angle, lens) and render it into an AI image/video prompt. " +
153
85
  "Call describe_scene first to see the current state. Coordinates are metres: x is right, " +
154
86
  "z is toward the camera's default position, y is height above the floor. Rotations are " +
155
- "degrees of yaw. Save with save_project to a .cclayproject file the CozyClay studio opens.",
87
+ "degrees of yaw. Save with save_project to a .cclayproject file the CozyClay studio opens. " +
88
+ "With an editor connected, studio_commands lists every command it registers and studio_run runs one " +
89
+ "through the editor's own command bus, answering its receipt.",
156
90
  },
157
91
  );
158
92
  server.server.setRequestHandler(InitializeRequestSchema, async (request) => {
@@ -213,7 +147,7 @@ const serveRegistryTool = (name, args, workspaceHandle) => {
213
147
  // The tools themselves live in tool-handlers.mjs so an in-process agent can run
214
148
  // them without an MCP server; registration order is the registry's order, which
215
149
  // is the order tools/list reports.
216
- for (const handler of createToolHandlers({ projectRootPromise, motionJobs, publishMotionJob })) registerTool(handler);
150
+ for (const handler of createToolHandlers({ projectRootPromise })) registerTool(handler);
217
151
 
218
152
  /* -------------------------------- start ---------------------------------- */
219
153
 
@@ -234,12 +168,7 @@ const configureLiveHub = (hub, token) => {
234
168
  // only place its token exists.
235
169
  publishLiveEndpoint({ port: hub.port, token, owner: "mcp" });
236
170
  hub.server?.once("close", () => removeLiveEndpoint(hub.port));
237
- hub.onEvent = ({ workspaceId, name, payload }) => {
238
- if (name === "motion_job_cancel") cancelMotionJob({ workspaceId, payload });
239
- };
240
- hub.onWorkspaceConnected = ({ workspaceId }) => {
241
- for (const job of motionJobs.forWorkspace(workspaceId)) void publishMotionJob(job);
242
- };
171
+
243
172
  };
244
173
 
245
174
  // stdio is the default because that is how an MCP client launches a local