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.
- package/CHANGELOG.md +193 -0
- package/bin/agent/agent-routes.mjs +82 -72
- package/bin/agent/agent-runner.mjs +10 -3
- package/bin/agent/motion-runtime.mjs +32 -260
- package/bin/agent/providers.mjs +46 -3
- package/bin/agent/session-store.mjs +41 -5
- package/bin/agent/studio-prompt.mjs +1 -1
- package/bin/agent/studio-tools.mjs +48 -11
- package/bin/codex-auth.mjs +2 -1
- package/bin/cozyclay.mjs +38 -11
- package/bin/live/cli.mjs +22 -3
- package/bin/mcp-runtime.mjs +45 -20
- package/bin/telemetry-state.mjs +5 -2
- package/bin/update-check.mjs +15 -0
- package/dist/app/index.html +6 -6
- package/dist/assets/analytics-MVZOEW8O.js +1 -0
- package/dist/assets/app-BxBLC6GT.js +4876 -0
- package/dist/assets/app-CJdxvnrl.css +1 -0
- package/dist/assets/{demo-B-eg1CT8.js → demo-ByeYp-dY.js} +1 -1
- package/dist/assets/{first-shot-handoff-CGgnvNu8.js → first-shot-handoff-DgT28ZiV.js} +1 -1
- package/dist/assets/{landing-IbRp3FnQ.js → landing-BaXkpFip.js} +1 -1
- package/dist/assets/shot-prompt-CbqWcWYi.css +1 -0
- package/dist/assets/shot-prompt-hN0j_O-p.js +17 -0
- package/dist/assets/{ticket-BgIzOBOf.js → ticket-BcmsnHn5.js} +1 -1
- package/dist/assets/workflow-B5XZwVfe.js +179 -0
- package/dist/assets/workflow-DRdwoDBT.css +1 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/fonts/IBM-Plex-Sans-OFL.txt +92 -0
- package/dist/fonts/JetBrains-Mono-OFL.txt +93 -0
- package/dist/fonts/README.md +13 -9
- package/dist/fonts/ibm-plex-sans-400-latin.woff2 +0 -0
- package/dist/fonts/ibm-plex-sans-500-latin.woff2 +0 -0
- package/dist/fonts/ibm-plex-sans-600-latin.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-400-latin.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-500-latin.woff2 +0 -0
- package/dist/index.html +7 -7
- package/dist/privacy/index.html +13 -8
- package/dist/sitemap.xml +2 -2
- package/dist/workflow/index.html +5 -5
- package/mcp/live-hub.mjs +35 -93
- package/mcp/mesh-file.mjs +48 -0
- package/mcp/server.mjs +6 -77
- package/mcp/tool-handlers.mjs +219 -376
- package/package.json +2 -1
- package/src/App.jsx +1275 -9086
- package/src/analytics.js +393 -9
- package/src/app-context.js +211 -0
- package/src/app-stage.jsx +25 -24
- package/src/ardy/auto-fix-panel.css +108 -0
- package/src/ardy/fix-collisions.js +10 -66
- package/src/ardy/ik-drag.js +101 -0
- package/src/ardy/ik-key-json.js +43 -0
- package/src/ardy/ik.js +97 -67
- package/src/ardy/physics-panel.css +2 -0
- package/src/ardy/physics-panel.jsx +17 -5
- package/src/ardy/platform-fit-panel.css +64 -0
- package/src/ardy/platform-fit-panel.jsx +40 -0
- package/src/ardy/platform-fit.js +326 -0
- package/src/ardy/playback.js +166 -20
- package/src/ardy/range-pin.js +299 -0
- package/src/ardy/timeline-coordinates.js +26 -0
- package/src/ardy/timeline.css +1214 -0
- package/src/ardy/timeline.jsx +152 -41
- package/src/ardy/waypoints.js +3 -3
- package/src/asset-pane.css +462 -0
- package/src/asset-pane.jsx +461 -232
- package/src/camera-tutorial.jsx +38 -18
- package/src/command-bus.js +349 -0
- package/src/commands/ai.js +31 -0
- package/src/commands/cast.js +140 -0
- package/src/commands/elements/character.js +13 -0
- package/src/commands/elements/motion.js +14 -0
- package/src/commands/elements/object.js +22 -0
- package/src/commands/elements/scene.js +16 -0
- package/src/commands/elements/shot.js +9 -0
- package/src/commands/elements/stage.js +11 -0
- package/src/commands/elements.js +155 -0
- package/src/commands/export.js +20 -0
- package/src/commands/index.js +40 -0
- package/src/commands/motion.js +231 -0
- package/src/commands/objects.js +174 -0
- package/src/commands/project.js +53 -0
- package/src/commands/scene.js +81 -0
- package/src/commands/shared.js +19 -0
- package/src/commands/shot.js +103 -0
- package/src/commands/stage.js +17 -0
- package/src/commands/view.js +42 -0
- package/src/document-store.js +144 -0
- package/src/domains/cast.js +1007 -0
- package/src/domains/motion.js +3219 -0
- package/src/domains/objects.js +932 -0
- package/src/domains/scenes.js +823 -0
- package/src/domains/shots.js +404 -0
- package/src/domains/stage.js +93 -0
- package/src/dualview.jsx +113 -57
- package/src/facing-marks.js +3 -0
- package/src/first-success-guide.jsx +14 -14
- package/src/grid-view.js +18 -5
- package/src/hierarchy-panel.css +386 -0
- package/src/hierarchy-panel.jsx +88 -19
- package/src/{fal-motion-client.js → i2v-motion-client.js} +13 -13
- package/src/i2v-motion-studio.jsx +180 -0
- package/src/ik-camera.js +3 -0
- package/src/main.jsx +6 -0
- package/src/motion/generation.js +105 -0
- package/src/motion-readiness-ui.jsx +4 -4
- package/src/motion-readiness.js +11 -0
- package/src/motion-trail.js +366 -9
- package/src/object-gizmo.jsx +7 -1
- package/src/panels/CameraPanel.jsx +60 -0
- package/src/panels/CharacterTransformPanel.jsx +48 -0
- package/src/panels/EnvironmentPanel.jsx +40 -0
- package/src/panels/Foldout.jsx +32 -0
- package/src/panels/LightPanel.jsx +24 -0
- package/src/panels/ObjectTransformPanel.jsx +554 -0
- package/src/panels/PosePanel.jsx +97 -0
- package/src/panels/ProjectPanel.jsx +33 -0
- package/src/panels/PromptBlocksPanel.jsx +442 -0
- package/src/panels/PropsPanel.jsx +70 -0
- package/src/panels/ReferenceImageField.jsx +91 -0
- package/src/panels/RigControlPanel.jsx +78 -0
- package/src/panels/RigPanel.jsx +37 -0
- package/src/panels/SubjectBox.jsx +47 -0
- package/src/panels/SubjectsPanel.jsx +39 -0
- package/src/panels/VideoCapturePanel.jsx +180 -0
- package/src/panels/details.css +1171 -0
- package/src/panels/motion.css +197 -0
- package/src/panels/pose.css +39 -0
- package/src/planview.jsx +60 -31
- package/src/posestudio.jsx +59 -4
- package/src/project-browser.css +1073 -0
- package/src/project-browser.jsx +288 -123
- package/src/range-pin-object-transform.js +19 -0
- package/src/range-pin-panel.css +518 -0
- package/src/range-pin-panel.jsx +344 -0
- package/src/result-modal.jsx +5 -5
- package/src/room.jsx +79 -51
- package/src/scenes.js +9 -8
- package/src/semantic-edit.js +2 -0
- package/src/settings-menu.jsx +39 -105
- package/src/shell/BottomDock.jsx +450 -0
- package/src/shell/DetailsSlot.jsx +513 -0
- package/src/shell/LibrarySlot.jsx +86 -0
- package/src/shell/MenuBar.jsx +601 -0
- package/src/shell/OutlinerSlot.jsx +50 -0
- package/src/shell/PreferencesDialog.jsx +538 -0
- package/src/shell/PreferencesSlot.jsx +40 -0
- package/src/shell/StatusBar.jsx +125 -0
- package/src/shell/StudioShell.jsx +92 -0
- package/src/shell/TopBar.jsx +129 -0
- package/src/shell/ViewportToolbar.jsx +656 -0
- package/src/shell/agent-glass.css +296 -0
- package/src/shell/dock.css +93 -0
- package/src/shell/glass-regions.css +985 -0
- package/src/shell/glass.css +687 -0
- package/src/shell/log-store.js +52 -0
- package/src/shell/mode.css +9 -0
- package/src/shell/preferences.css +651 -0
- package/src/shell/shell.css +282 -0
- package/src/shell/studio-shell-context.js +19 -0
- package/src/shell/topbar.css +435 -0
- package/src/shell/viewport.css +451 -0
- package/src/store/authored-intent.js +22 -0
- package/src/store/runtime-adapters.js +92 -0
- package/src/store/scene-stage.js +21 -0
- package/src/store/use-document-store.js +14 -0
- package/src/studio-actions.js +210 -0
- package/src/studio-agent-commands.js +38 -268
- package/src/studio-agent-context.js +38 -9
- package/src/studio-agent-motion.js +122 -378
- package/src/studio-agent-protocol.js +66 -30
- package/src/studio-app-binding.js +379 -0
- package/src/studio-contact-sheet.js +73 -0
- package/src/studio-elements.js +58 -51
- package/src/styles/themes.css +200 -0
- package/src/styles/tokens.css +69 -0
- package/src/styles.css +456 -1748
- package/src/theme.js +44 -0
- package/src/timeline-extent.js +16 -0
- package/src/timeline-fps.js +13 -0
- package/src/trail-key-conflicts.js +39 -0
- package/src/trail-pick.js +59 -0
- package/src/ui.jsx +8 -8
- package/src/use-case-question.jsx +66 -0
- package/src/workflow/AgentPanel.jsx +86 -71
- package/src/workflow/agent-client.js +12 -2
- package/src/workflow/agent-panel.css +34 -32
- package/src/workflow/cozy-scene-node.css +2 -0
- package/src/workflow/workflow.css +2 -0
- package/tools/ardy/bridge.mjs +164 -146
- package/tools/ardy/visual-qa.mjs +5 -5
- package/tools/bench/EXP3.md +61 -0
- package/tools/bench/cclay_bench_extract_incam.py +125 -0
- package/tools/bench/cclay_bench_extract_obs.py +204 -0
- package/tools/bench/cclay_bench_runner.py +42 -0
- package/tools/bench/cube-contact.mjs +162 -0
- package/tools/bench/exp3.mjs +152 -0
- package/tools/bench/extract-bench-lib.mjs +216 -0
- package/tools/bench/extract-bench.mjs +302 -0
- package/tools/bench/fal-generate.mjs +88 -0
- package/tools/bench/fit/README.md +189 -0
- package/tools/bench/fit/camera.mjs +59 -0
- package/tools/bench/fit/contact.mjs +419 -0
- package/tools/bench/fit/footlock.mjs +234 -0
- package/tools/bench/fit/motion.mjs +85 -0
- package/tools/bench/fit/pin.mjs +27 -0
- package/tools/bench/fit/remote.mjs +105 -0
- package/tools/bench/fit-bench.mjs +126 -0
- package/tools/bench/fit-sanity.mjs +45 -0
- package/tools/bench/metrics.mjs +313 -0
- package/tools/bench/obs/depth.mjs +258 -0
- package/tools/bench/obs/extrinsics.mjs +175 -0
- package/tools/bench/obs/fit_mannequin_betas.py +340 -0
- package/tools/bench/obs/ground.mjs +294 -0
- package/tools/bench/obs/heading.mjs +151 -0
- package/tools/bench/obs/ladder.mjs +555 -0
- package/tools/bench/obs/mannequin-betas.json +124 -0
- package/tools/bench/obs/remote.mjs +83 -0
- package/tools/bench/obs/rest_joints.py +52 -0
- package/tools/bench/obs/ybot-targets.mjs +49 -0
- package/tools/bench/obs-bench.mjs +520 -0
- package/tools/bench/score.mjs +343 -0
- package/tools/bench/summarize.mjs +86 -0
- package/tools/dev/pages/privacy.html +13 -8
- package/tools/gt-render/browser.mjs +159 -0
- package/tools/gt-render/camera-math.mjs +276 -0
- package/tools/gt-render/page.mjs +267 -0
- package/tools/gt-render/render.mjs +514 -0
- package/tools/gt-render/scene-box.mjs +31 -0
- package/tools/gt-render/take-transform.mjs +87 -0
- package/tools/qa/css-rule-usage.mjs +283 -0
- package/tools/qa/studio-control-count.mjs +70 -32
- package/tools/run-tests.mjs +60 -3
- package/tools/track/DESIGN.md +513 -0
- package/tools/track/backfill-provenance.mjs +116 -0
- package/tools/track/budget.mjs +8 -0
- package/tools/track/check-rig.mjs +209 -0
- package/tools/track/diagnostics.schema.json +60 -0
- package/tools/track/export-rig.mjs +256 -0
- package/tools/track/fallback.mjs +7 -0
- package/tools/track/fk-parity-fixture.mjs +161 -0
- package/tools/track/gate.mjs +349 -0
- package/tools/track/masks.mjs +318 -0
- package/tools/track/metrics.mjs +201 -0
- package/tools/track/publish-obs.mjs +108 -0
- package/tools/track/py/check_env.py +61 -0
- package/tools/track/py/eval_lr.py +229 -0
- package/tools/track/py/lr_viterbi.py +294 -0
- package/tools/track/py/masks.py +405 -0
- package/tools/track/py/objective.py +382 -0
- package/tools/track/py/rig.py +508 -0
- package/tools/track/py/scene.py +326 -0
- package/tools/track/py/test_joint_indices.py +123 -0
- package/tools/track/py/test_lr_viterbi.py +195 -0
- package/tools/track/py/test_masks.py +229 -0
- package/tools/track/py/test_rig.py +288 -0
- package/tools/track/py/test_scene.py +354 -0
- package/tools/track/py/test_track.py +328 -0
- package/tools/track/py/track.py +411 -0
- package/tools/track/remote.mjs +140 -0
- package/tools/track/rig-dump.mjs +227 -0
- package/tools/track/run-box-tests.mjs +14 -0
- package/tools/track/run-box.mjs +168 -0
- package/tools/track/setup-box.sh +59 -0
- package/tools/track/study-2d.mjs +728 -0
- package/dist/assets/analytics-B1hnH66c.js +0 -1
- package/dist/assets/app-Cu1cexZR.css +0 -1
- package/dist/assets/app-Dxdh8kds.js +0 -4861
- package/dist/assets/shot-prompt-C_g2BHVc.css +0 -1
- package/dist/assets/shot-prompt-HVJRP4Gr.js +0 -17
- package/dist/assets/workflow-CJd-PDtV.js +0 -179
- package/dist/assets/workflow-DsudxKHF.css +0 -1
- package/src/fal-motion-studio.jsx +0 -180
- package/src/scene-history.js +0 -129
- 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
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
"
|
|
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"
|
|
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 === "
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|