cozyclay 1.9.0 → 2.0.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.
- package/CHANGELOG.md +171 -0
- package/bin/agent/agent-routes.mjs +74 -70
- 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/cozyclay.mjs +26 -8
- package/bin/live/cli.mjs +22 -3
- package/bin/mcp-runtime.mjs +35 -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-C1sdVbg4.css +1 -0
- package/dist/assets/app-CA55tcI3.js +4876 -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-Yrt99wZx.js +17 -0
- package/dist/assets/{ticket-BgIzOBOf.js → ticket-BcmsnHn5.js} +1 -1
- package/dist/assets/workflow-DRdwoDBT.css +1 -0
- package/dist/assets/workflow-dNPGY9xz.js +179 -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 +6 -6
- package/dist/workflow/index.html +5 -5
- package/mcp/LIVE-PROTOCOL.md +1 -1
- 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 +224 -377
- package/package.json +2 -1
- package/src/App.jsx +1273 -9075
- 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/collision-blockers.js +9 -3
- package/src/ardy/fix-collisions.js +10 -66
- package/src/ardy/ground.js +4 -4
- package/src/ardy/ik-drag.js +101 -0
- package/src/ardy/ik-key-json.js +43 -0
- package/src/ardy/ik.js +122 -76
- 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 +1199 -0
- package/src/ardy/timeline.jsx +149 -41
- package/src/ardy/waypoints.js +2 -2
- package/src/asset-pane.css +462 -0
- package/src/asset-pane.jsx +461 -232
- 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 +817 -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-model.js +24 -16
- package/src/hierarchy-panel.css +386 -0
- package/src/hierarchy-panel.jsx +150 -21
- 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/otio.js +2 -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 +284 -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/scene-objects.js +37 -1
- package/src/scenes.js +5 -0
- 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 +41 -271
- package/src/studio-agent-context.js +39 -10
- package/src/studio-agent-motion.js +122 -373
- package/src/studio-agent-protocol.js +67 -31
- 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 +460 -1742
- package/src/theme.js +44 -0
- package/src/timeline-extent.js +16 -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 +82 -69
- 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/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -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/morphgs/assets/gen_truth.py +39 -0
- package/tools/morphgs/assets/mesh_ori_rig.txt +27 -0
- package/tools/morphgs/assets/playback-check.mjs +30 -0
- package/tools/morphgs/demo-gate.sh +26 -0
- package/tools/morphgs/fbx2morphgs.mjs +68 -0
- package/tools/morphgs/morphgs-to-cskel27.mjs +105 -0
- package/tools/morphgs/patches/preprocess_src-none-mode.patch +76 -0
- package/tools/morphgs/setup-on-cluster.sh +126 -0
- package/tools/qa/css-rule-usage.mjs +283 -0
- package/tools/qa/studio-control-count.mjs +70 -32
- package/tools/run-tests.mjs +134 -12
- 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-BWusbqgO.js +0 -4861
- package/dist/assets/app-qKDo4PBX.css +0 -1
- package/dist/assets/shot-prompt-CHUtw6af.js +0 -17
- package/dist/assets/shot-prompt-C_g2BHVc.css +0 -1
- package/dist/assets/workflow-CVwzMhz2.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/mcp/tool-handlers.mjs
CHANGED
|
@@ -28,10 +28,10 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
28
28
|
|
|
29
29
|
import { z } from "zod";
|
|
30
30
|
|
|
31
|
-
import {
|
|
31
|
+
import { MAX_COMMAND_TIMEOUT_MS } from "./live-hub.mjs";
|
|
32
32
|
import { readMeshFromPath } from "./mesh-file.mjs";
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
33
|
+
import { DEFAULT_POSE } from "../src/poses.js";
|
|
34
|
+
import { generationArgs } from '../src/motion/generation.js';
|
|
35
35
|
import { BLOCK_MAX_SECONDS, PROMPT_GUIDE, normalizePhases, splitLongBeat, tileClipFrames } from "./ardy-prompts.mjs";
|
|
36
36
|
|
|
37
37
|
import {
|
|
@@ -99,6 +99,7 @@ const liveWorkspaceTools = new Set([
|
|
|
99
99
|
"set_camera", "frame_shot", "add_character", "place_character", "remove_character",
|
|
100
100
|
"focus_character", "place_object", "import_mesh", "group_objects", "set_prompt_blocks", "generate_motion", "update_object",
|
|
101
101
|
"remove_object", "apply_batch", "add_scene", "switch_scene", "open_project", "capture_frame", "load_motion",
|
|
102
|
+
"studio_commands", "studio_run",
|
|
102
103
|
]);
|
|
103
104
|
const MAX_CAPTURE_BYTES = 1_000_000;
|
|
104
105
|
const CAPTURE_ARTIFACT_TTL_MS = 10 * 60_000;
|
|
@@ -166,8 +167,41 @@ const TOOL_ANNOTATIONS = Object.freeze({
|
|
|
166
167
|
switch_scene: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
167
168
|
open_project: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
168
169
|
save_project: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
170
|
+
studio_commands: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
171
|
+
// Whatever the editor registers: some commands delete, export or spend a
|
|
172
|
+
// paid generation, and those answer CONFIRMATION_REQUIRED instead of running.
|
|
173
|
+
studio_run: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
169
174
|
});
|
|
170
175
|
|
|
176
|
+
/** The document identity a Studio command is admitted in. */
|
|
177
|
+
const STUDIO_IDENTITY_KEYS = ["workspaceId", "documentEpoch", "sceneId", "sceneEpoch"];
|
|
178
|
+
|
|
179
|
+
/** One registered editor command through the editor's own bus, admitted at
|
|
180
|
+
* the open document and its current revision like the agent's run_action. Its
|
|
181
|
+
* declaration (read from the editor, never from this server) sets the hub
|
|
182
|
+
* deadline unless the caller gives one. */
|
|
183
|
+
const executeStudioCommand = async ({ action, args, expectedRevision, commandId, timeoutMs, confirmationToken, inspected }) => {
|
|
184
|
+
const workspaceHandle = liveWorkspace.getStore();
|
|
185
|
+
inspected ??= await liveHub.command("inspect_studio", { scope: "actions", ids: [action] }, workspaceHandle);
|
|
186
|
+
const context = inspected?.context;
|
|
187
|
+
if (!context?.host || !Number.isSafeInteger(context.revision?.scene)) throw new Error("The editor did not return a Studio context to admit this command against.");
|
|
188
|
+
const declared = inspected.actions?.find((row) => row.id === action);
|
|
189
|
+
const receipt = await liveHub.command("run_action", {
|
|
190
|
+
name: "run_action",
|
|
191
|
+
args: { action, args: typeof args === "function" ? await args(context) : args ?? {}, ...(confirmationToken ? { confirmationToken } : {}) },
|
|
192
|
+
commandId: commandId ?? randomUUID(),
|
|
193
|
+
host: Object.fromEntries(STUDIO_IDENTITY_KEYS.map((key) => [key, context.host[key]])),
|
|
194
|
+
expectedRevision: expectedRevision ?? context.revision.scene,
|
|
195
|
+
}, workspaceHandle, { timeoutMs: timeoutMs ?? (declared?.timeoutMs === undefined ? undefined : Math.min(MAX_COMMAND_TIMEOUT_MS, declared.timeoutMs + (declared.generation ? 5000 : 0))) });
|
|
196
|
+
return receipt;
|
|
197
|
+
};
|
|
198
|
+
// A refusal is the editor's receipt: its code and recovery are the answer.
|
|
199
|
+
const studioResult = (...receipts) => ({
|
|
200
|
+
content: receipts.map(receipt => ({ type: "text", text: JSON.stringify(receipt) })),
|
|
201
|
+
...(receipts.some(receipt => receipt.ok === false) ? { isError: true } : {}),
|
|
202
|
+
});
|
|
203
|
+
const runStudioCommand = async options => studioResult(await executeStudioCommand(options));
|
|
204
|
+
|
|
171
205
|
const scene = () => activeScene(state.doc.scenes, state.doc.activeSceneId);
|
|
172
206
|
const stage = () => scene().stage;
|
|
173
207
|
|
|
@@ -189,6 +223,13 @@ const findCharacter = (ref) => {
|
|
|
189
223
|
return null;
|
|
190
224
|
};
|
|
191
225
|
|
|
226
|
+
// Resolve legacy letters and slots from the complete live cast inside the
|
|
227
|
+
// inspected admission. A missing id is refused by the command, not hidden here.
|
|
228
|
+
const liveCharacterId = async ref => {
|
|
229
|
+
await refreshLiveDescription();
|
|
230
|
+
return findCharacter(ref)?.id ?? ref;
|
|
231
|
+
};
|
|
232
|
+
|
|
192
233
|
/** The studio labels the cast A, B, C… by position. */
|
|
193
234
|
const letterFor = (character) => String.fromCharCode(65 + cast().indexOf(character));
|
|
194
235
|
|
|
@@ -245,31 +286,6 @@ const framing = () => {
|
|
|
245
286
|
|
|
246
287
|
const currentShot = () => deriveShot(state.camera, subject(), fov(), undefined, filmback());
|
|
247
288
|
|
|
248
|
-
const appliedLiveMutation = async (name, args) => {
|
|
249
|
-
const workspaceHandle = liveWorkspace.getStore();
|
|
250
|
-
const value = await liveHub.command(name, args, workspaceHandle);
|
|
251
|
-
try {
|
|
252
|
-
if (!await refreshLiveDescription(workspaceHandle)) throw new Error("Live editor disconnected before verification.");
|
|
253
|
-
} catch (error) {
|
|
254
|
-
throw new LiveMutationUncertainError(`Live editor accepted ${name}, but its state could not be verified: ${error.message} The mutation may have been applied. Do not retry it; describe the scene before choosing a recovery action.`);
|
|
255
|
-
}
|
|
256
|
-
return value;
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
const requireLiveSceneParity = (name, document, result) => {
|
|
260
|
-
const expected = document.scenes.map(({ id, name: sceneName }) => ({ id, name: sceneName }));
|
|
261
|
-
const received = Array.isArray(result?.scenes) ? result.scenes : [];
|
|
262
|
-
const sameScenes = received.length === expected.length && received.every((scene, index) =>
|
|
263
|
-
scene?.id === expected[index].id && scene?.name === expected[index].name,
|
|
264
|
-
);
|
|
265
|
-
if (sameScenes && result?.activeSceneId === document.activeSceneId) return;
|
|
266
|
-
throw new LiveMutationUncertainError(
|
|
267
|
-
`Live editor accepted ${name}, but did not confirm the complete scene list and active scene ` +
|
|
268
|
-
`(expected ${JSON.stringify(expected)} active ${document.activeSceneId}; received ${JSON.stringify(received)} active ${result?.activeSceneId ?? "none"}). ` +
|
|
269
|
-
"The mutation may have been applied. Do not retry it; describe the scene before choosing a recovery action.",
|
|
270
|
-
);
|
|
271
|
-
};
|
|
272
|
-
|
|
273
289
|
const modelById = (id) =>
|
|
274
290
|
[...VIDEO_MODELS, ...IMAGE_MODELS].find((m) => m.id === id) ?? null;
|
|
275
291
|
|
|
@@ -409,7 +425,8 @@ function sceneReport({ characterCursor = 0, objectCursor = 0, limit = 50 } = {})
|
|
|
409
425
|
const size = objectSize(object);
|
|
410
426
|
lines.push(
|
|
411
427
|
` ${object.id} ${object.name} at x ${round(object.x)}, y ${round(object.y)}, z ${round(object.z)}` +
|
|
412
|
-
` yaw ${round(object.rot, 1)}deg size ${round(size.width)}x${round(size.height)}x${round(size.depth)}m
|
|
428
|
+
` yaw ${round(object.rot, 1)}deg size ${round(size.width)}x${round(size.height)}x${round(size.depth)}m` +
|
|
429
|
+
`${object.hidden ? " hidden" : ""}`,
|
|
413
430
|
` rotX: ${round(object.rotX ?? 0, 1)} rotZ: ${round(object.rotZ ?? 0, 1)} color: ${object.color ?? null} parent: ${object.parent ?? null}`,
|
|
414
431
|
);
|
|
415
432
|
if (object.path?.points?.length >= 2) {
|
|
@@ -469,13 +486,24 @@ function shotReport() {
|
|
|
469
486
|
|
|
470
487
|
/* --------------------------------- tools --------------------------------- */
|
|
471
488
|
|
|
489
|
+
const studioAliasTools = new Set([
|
|
490
|
+
"set_camera", "frame_shot", "place_object", "update_object", "remove_object",
|
|
491
|
+
"import_mesh", "group_objects", "add_scene", "switch_scene", "apply_batch", "open_project",
|
|
492
|
+
"add_character", "place_character", "remove_character", "set_prompt_blocks", "load_motion", "generate_motion",
|
|
493
|
+
]);
|
|
494
|
+
const studioAdmissionSchema = {
|
|
495
|
+
expectedRevision: z.number().int().min(0).optional().describe("scene revision to admit against; defaults to the inspected revision"),
|
|
496
|
+
commandId: z.string().min(1).max(120).optional().describe("idempotency key; reuse with the original expectedRevision to replay its receipt"),
|
|
497
|
+
timeoutMs: z.number().int().min(1).max(MAX_COMMAND_TIMEOUT_MS).optional().describe("hub deadline; defaults to the editor command declaration"),
|
|
498
|
+
};
|
|
499
|
+
|
|
472
500
|
/** One tool, as the registry hands it out: everything server.mjs needs to
|
|
473
501
|
* register it, and everything a direct caller needs to run it. */
|
|
474
502
|
const tool = (name, config, handler) => ({
|
|
475
503
|
name,
|
|
476
504
|
title: config.title,
|
|
477
505
|
description: config.description,
|
|
478
|
-
inputSchema: config.inputSchema,
|
|
506
|
+
inputSchema: studioAliasTools.has(name) ? { ...config.inputSchema, ...studioAdmissionSchema } : config.inputSchema,
|
|
479
507
|
annotations: TOOL_ANNOTATIONS[name],
|
|
480
508
|
live: liveWorkspaceTools.has(name),
|
|
481
509
|
handler,
|
|
@@ -488,14 +516,9 @@ const tool = (name, config, handler) => ({
|
|
|
488
516
|
* @param {Promise<string>} [deps.projectRootPromise] the resolved directory
|
|
489
517
|
* `open_project`/`save_project` confine themselves to; the owner resolves it
|
|
490
518
|
* because doing so chdirs the process.
|
|
491
|
-
* @param {object} [deps.motionJobs] the MotionJobRegistry `generate_motion`
|
|
492
|
-
* records work in.
|
|
493
|
-
* @param {(job: object) => Promise<void>} [deps.publishMotionJob] delivers a
|
|
494
|
-
* finished job to its workspace; the owner holds it because the live hub's
|
|
495
|
-
* reconnect and cancel paths publish jobs too.
|
|
496
519
|
* @returns {Array<{name: string, title: string, description: string, inputSchema: object, annotations: object, live: boolean, handler: Function}>}
|
|
497
520
|
*/
|
|
498
|
-
export const createToolHandlers = ({ projectRootPromise
|
|
521
|
+
export const createToolHandlers = ({ projectRootPromise } = {}) => {
|
|
499
522
|
const resolveProjectPath = async (path, { existing }) => {
|
|
500
523
|
if (!path.endsWith(".cclayproject")) throw new Error("Project path must end in .cclayproject.");
|
|
501
524
|
const root = await projectRootPromise;
|
|
@@ -674,17 +697,19 @@ export const createToolHandlers = ({ projectRootPromise, motionJobs, publishMoti
|
|
|
674
697
|
look_at_z: z.number().optional().describe("world z of an explicit aim point; give all three to aim the lens"),
|
|
675
698
|
},
|
|
676
699
|
},
|
|
677
|
-
async ({ x, y, z: zPos, focal_mm, look_at_x, look_at_y, look_at_z }) => {
|
|
700
|
+
async ({ x, y, z: zPos, focal_mm, look_at_x, look_at_y, look_at_z, ...admission }) => {
|
|
678
701
|
if (liveHub?.connected) {
|
|
679
702
|
try {
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
703
|
+
return await runStudioCommand({ ...admission, action: "shot.frame", args: context => {
|
|
704
|
+
const camera = context.camera;
|
|
705
|
+
const position = { x: x ?? camera.position.x, y: y ?? camera.position.y, z: zPos ?? camera.position.z };
|
|
706
|
+
// Preserve direction when no complete explicit aim point was given.
|
|
707
|
+
const lookAt = [look_at_x, look_at_y, look_at_z].every(value => value !== undefined)
|
|
708
|
+
? { x: look_at_x, y: look_at_y, z: look_at_z }
|
|
709
|
+
: Object.fromEntries(["x", "y", "z"].map(axis => [axis, position[axis] + camera.lookAt[axis] - camera.position[axis]]));
|
|
710
|
+
return { subjectIds: [state.focusLocked ? state.focus : context.activeCharacterId],
|
|
711
|
+
framing: { exact: { position, lookAt, focalMm: focal_mm ?? camera.focalMm } } };
|
|
712
|
+
} });
|
|
688
713
|
} catch (error) {
|
|
689
714
|
return liveError(error);
|
|
690
715
|
}
|
|
@@ -729,9 +754,11 @@ export const createToolHandlers = ({ projectRootPromise, motionJobs, publishMoti
|
|
|
729
754
|
focal_mm: z.number().min(8).max(300).default(35).describe("lens to frame with"),
|
|
730
755
|
},
|
|
731
756
|
},
|
|
732
|
-
async ({ size, view, level, side, focal_mm }) => {
|
|
757
|
+
async ({ size, view, level, side, focal_mm, ...admission }) => {
|
|
758
|
+
let inspected;
|
|
733
759
|
if (liveHub?.connected) {
|
|
734
760
|
try {
|
|
761
|
+
inspected = await liveHub.command("inspect_studio", { scope: "actions", ids: ["shot.frame"] }, liveWorkspace.getStore());
|
|
735
762
|
await refreshLiveDescription();
|
|
736
763
|
} catch (error) {
|
|
737
764
|
return liveError(error);
|
|
@@ -804,19 +831,13 @@ export const createToolHandlers = ({ projectRootPromise, motionJobs, publishMoti
|
|
|
804
831
|
};
|
|
805
832
|
if (liveHub?.connected) {
|
|
806
833
|
try {
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
await appliedLiveMutation("set_camera", {
|
|
815
|
-
...nextCamera,
|
|
816
|
-
lookAtX: s.x,
|
|
817
|
-
lookAtY: FRAMING_PIVOT_Y,
|
|
818
|
-
lookAtZ: s.z,
|
|
819
|
-
});
|
|
834
|
+
return await runStudioCommand({ ...admission, action: "shot.frame", inspected, args: {
|
|
835
|
+
subjectIds: [(findCharacter(state.focus) ?? cast()[0]).id],
|
|
836
|
+
framing: { exact: {
|
|
837
|
+
position: { x: nextCamera.x, y: nextCamera.y, z: nextCamera.z },
|
|
838
|
+
lookAt: { x: s.x, y: FRAMING_PIVOT_Y, z: s.z }, focalMm: lensMm,
|
|
839
|
+
} },
|
|
840
|
+
} });
|
|
820
841
|
} catch (error) {
|
|
821
842
|
return liveError(error);
|
|
822
843
|
}
|
|
@@ -851,13 +872,12 @@ export const createToolHandlers = ({ projectRootPromise, motionJobs, publishMoti
|
|
|
851
872
|
.describe("which mannequin to use"),
|
|
852
873
|
},
|
|
853
874
|
},
|
|
854
|
-
async ({ subject: desc, x, z: zPos, facing, model }) => {
|
|
875
|
+
async ({ subject: desc, x, z: zPos, facing, model, ...admission }) => {
|
|
855
876
|
if (liveHub?.connected) {
|
|
856
877
|
try {
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
return text(`Added ${result?.id ?? "character"}.\n\n${sceneReport()}`);
|
|
878
|
+
return await runStudioCommand({ ...admission, action: "character.add", args: {
|
|
879
|
+
character: { subject: desc, x, z: zPos, rot: facing, model, pose: DEFAULT_POSE },
|
|
880
|
+
} });
|
|
861
881
|
} catch (error) {
|
|
862
882
|
return liveError(error);
|
|
863
883
|
}
|
|
@@ -900,11 +920,12 @@ export const createToolHandlers = ({ projectRootPromise, motionJobs, publishMoti
|
|
|
900
920
|
hidden: z.boolean().optional().describe("hide without removing from the cast"),
|
|
901
921
|
},
|
|
902
922
|
},
|
|
903
|
-
async ({ character, x, z: zPos, y, facing, subject: desc, hidden }) => {
|
|
923
|
+
async ({ character, x, z: zPos, y, facing, subject: desc, hidden, ...admission }) => {
|
|
904
924
|
if (liveHub?.connected) {
|
|
905
925
|
try {
|
|
906
|
-
await
|
|
907
|
-
|
|
926
|
+
return await runStudioCommand({ ...admission, action: "character.update", args: async () => ({
|
|
927
|
+
characterId: await liveCharacterId(character), patch: { x, y, z: zPos, rot: facing, subject: desc, hidden },
|
|
928
|
+
}) });
|
|
908
929
|
} catch (error) {
|
|
909
930
|
return liveError(error);
|
|
910
931
|
}
|
|
@@ -930,11 +951,12 @@ export const createToolHandlers = ({ projectRootPromise, motionJobs, publishMoti
|
|
|
930
951
|
character: z.string().describe('which character — letter, slot number or id'),
|
|
931
952
|
},
|
|
932
953
|
},
|
|
933
|
-
async ({ character }) => {
|
|
954
|
+
async ({ character, ...admission }) => {
|
|
934
955
|
if (liveHub?.connected) {
|
|
935
956
|
try {
|
|
936
|
-
await
|
|
937
|
-
|
|
957
|
+
return await runStudioCommand({ ...admission, action: "character.remove", args: async () => ({
|
|
958
|
+
characterId: await liveCharacterId(character),
|
|
959
|
+
}) });
|
|
938
960
|
} catch (error) {
|
|
939
961
|
return liveError(error);
|
|
940
962
|
}
|
|
@@ -1000,11 +1022,10 @@ export const createToolHandlers = ({ projectRootPromise, motionJobs, publishMoti
|
|
|
1000
1022
|
.describe("object id to attach to — the parent then carries this object when it moves"),
|
|
1001
1023
|
},
|
|
1002
1024
|
},
|
|
1003
|
-
async ({ kind, x, z: zPos, y, facing, name, parent }) => {
|
|
1025
|
+
async ({ kind, x, z: zPos, y, facing, name, parent, ...admission }) => {
|
|
1004
1026
|
if (liveHub?.connected) {
|
|
1005
1027
|
try {
|
|
1006
|
-
|
|
1007
|
-
return text(`Placed object as ${result?.id ?? "unknown"}.\n\n${sceneReport()}`);
|
|
1028
|
+
return await runStudioCommand({ ...admission, action: "object.add", args: { kind, placement: { x, z: zPos, y, rot: facing }, name, parent } });
|
|
1008
1029
|
} catch (error) {
|
|
1009
1030
|
return liveError(error);
|
|
1010
1031
|
}
|
|
@@ -1052,7 +1073,7 @@ export const createToolHandlers = ({ projectRootPromise, motionJobs, publishMoti
|
|
|
1052
1073
|
height: z.number().positive().optional().describe("standing height in metres; omitted uses the fitted size"),
|
|
1053
1074
|
},
|
|
1054
1075
|
},
|
|
1055
|
-
async ({ path, clay, name, x, z: zPos, y, facing, height }) => {
|
|
1076
|
+
async ({ path, clay, name, x, z: zPos, y, facing, height, ...admission }) => {
|
|
1056
1077
|
if (!liveHub?.connected) {
|
|
1057
1078
|
return liveError(new Error(noLiveEditor("import_mesh requires a connected CozyClay editor.")));
|
|
1058
1079
|
}
|
|
@@ -1065,7 +1086,7 @@ export const createToolHandlers = ({ projectRootPromise, motionJobs, publishMoti
|
|
|
1065
1086
|
const liveArgs = {
|
|
1066
1087
|
name: typeof name === "string" && name.trim() ? name.trim() : mesh.name,
|
|
1067
1088
|
mimeType: mesh.mimeType,
|
|
1068
|
-
|
|
1089
|
+
source: `data:${mesh.mimeType};base64,${Buffer.from(mesh.bytes).toString("base64")}`,
|
|
1069
1090
|
placeAs: "mesh",
|
|
1070
1091
|
};
|
|
1071
1092
|
if (clay === true) liveArgs.clay = true;
|
|
@@ -1077,10 +1098,7 @@ export const createToolHandlers = ({ projectRootPromise, motionJobs, publishMoti
|
|
|
1077
1098
|
if (facing !== undefined) liveArgs.rot = facing;
|
|
1078
1099
|
if (height !== undefined) liveArgs.height = height;
|
|
1079
1100
|
try {
|
|
1080
|
-
|
|
1081
|
-
return text(
|
|
1082
|
-
`Placed ${liveArgs.name} as ${result?.objectId ?? "unknown"} (${result?.assetId ?? "unknown"}).\n\n${sceneReport()}`,
|
|
1083
|
-
);
|
|
1101
|
+
return await runStudioCommand({ ...admission, action: "asset.import", args: liveArgs });
|
|
1084
1102
|
} catch (error) {
|
|
1085
1103
|
return liveError(error);
|
|
1086
1104
|
}
|
|
@@ -1101,16 +1119,10 @@ export const createToolHandlers = ({ projectRootPromise, motionJobs, publishMoti
|
|
|
1101
1119
|
children: z.array(z.string()).min(1).describe("object ids to attach or detach"),
|
|
1102
1120
|
},
|
|
1103
1121
|
},
|
|
1104
|
-
async ({ parent, children }) => {
|
|
1122
|
+
async ({ parent, children, ...admission }) => {
|
|
1105
1123
|
if (liveHub?.connected) {
|
|
1106
1124
|
try {
|
|
1107
|
-
await
|
|
1108
|
-
return text(
|
|
1109
|
-
(parent === null
|
|
1110
|
-
? `Detached ${children.length} object(s).`
|
|
1111
|
-
: `Grouped ${children.length} object(s) under ${parent} — move ${parent} and they follow.`) +
|
|
1112
|
-
`\n\n${sceneReport()}`,
|
|
1113
|
-
);
|
|
1125
|
+
return await runStudioCommand({ ...admission, action: parent === null ? "object.ungroup" : "object.group", args: parent === null ? { children } : { parent, children } });
|
|
1114
1126
|
} catch (error) {
|
|
1115
1127
|
return liveError(error);
|
|
1116
1128
|
}
|
|
@@ -1135,7 +1147,7 @@ export const createToolHandlers = ({ projectRootPromise, motionJobs, publishMoti
|
|
|
1135
1147
|
description:
|
|
1136
1148
|
"Write Prompt Blocks onto the timeline WITHOUT generating — the beats and their frame " +
|
|
1137
1149
|
"ranges, so a schedule can be read and revised before any GPU time is spent. Hit " +
|
|
1138
|
-
"'
|
|
1150
|
+
"the studio's top-bar Generate Motion button, or call generate_motion, when it reads right.\n\n" +
|
|
1139
1151
|
PROMPT_GUIDE,
|
|
1140
1152
|
inputSchema: {
|
|
1141
1153
|
beats: z
|
|
@@ -1155,7 +1167,7 @@ export const createToolHandlers = ({ projectRootPromise, motionJobs, publishMoti
|
|
|
1155
1167
|
.describe("beats in order; each one becomes a contiguous block"),
|
|
1156
1168
|
},
|
|
1157
1169
|
},
|
|
1158
|
-
async ({ beats }) => {
|
|
1170
|
+
async ({ beats, ...admission }) => {
|
|
1159
1171
|
if (!liveHub?.connected) {
|
|
1160
1172
|
return text("Prompt Blocks live on the studio timeline — open the editor and try again.");
|
|
1161
1173
|
}
|
|
@@ -1163,12 +1175,10 @@ export const createToolHandlers = ({ projectRootPromise, motionJobs, publishMoti
|
|
|
1163
1175
|
// The timeline runs on a 24 fps production clock.
|
|
1164
1176
|
const TIMELINE_FPS = 24;
|
|
1165
1177
|
let cursor = 0;
|
|
1166
|
-
let chained = 0;
|
|
1167
1178
|
const blocks = [];
|
|
1168
1179
|
for (const [i, textValue] of normalized.texts.entries()) {
|
|
1169
1180
|
const whole = beats[Math.min(normalized.sources[i], beats.length - 1)].seconds ?? 3;
|
|
1170
1181
|
const spans = splitLongBeat(whole);
|
|
1171
|
-
if (spans.length > 1) chained += spans.length - 1;
|
|
1172
1182
|
for (const span of spans) {
|
|
1173
1183
|
const frames = Math.max(1, Math.round(span * TIMELINE_FPS));
|
|
1174
1184
|
blocks.push({ startFrame: cursor, endFrame: cursor + frames, text: textValue });
|
|
@@ -1176,24 +1186,12 @@ export const createToolHandlers = ({ projectRootPromise, motionJobs, publishMoti
|
|
|
1176
1186
|
}
|
|
1177
1187
|
}
|
|
1178
1188
|
try {
|
|
1179
|
-
await
|
|
1189
|
+
return await runStudioCommand({ ...admission, action: "character.setPromptBlocks", args: context => ({
|
|
1190
|
+
characterId: context.activeCharacterId, blocks,
|
|
1191
|
+
}) });
|
|
1180
1192
|
} catch (error) {
|
|
1181
1193
|
return liveError(error);
|
|
1182
1194
|
}
|
|
1183
|
-
// Report the normalised text itself, not blocks[i]: a long beat becomes
|
|
1184
|
-
// several blocks, so block indices run ahead of phase indices and quoting
|
|
1185
|
-
// blocks[i] would attribute one beat's edits to another beat's wording.
|
|
1186
|
-
const rewrites = normalized.notes
|
|
1187
|
-
.map((notes, i) => (notes.length ? ` ${i + 1}. ${normalized.texts[i]} ← ${notes.join("; ")}` : null))
|
|
1188
|
-
.filter(Boolean);
|
|
1189
|
-
return text(
|
|
1190
|
-
`${blocks.length} block(s) on the timeline (${(cursor / TIMELINE_FPS).toFixed(1)}s total):\n` +
|
|
1191
|
-
blocks.map((b) => ` ${b.startFrame}-${b.endFrame}f ${b.text}`).join("\n") +
|
|
1192
|
-
(chained > 0 ? `\n (${chained} block(s) chained to keep every block within ${BLOCK_MAX_SECONDS}s)` : "") +
|
|
1193
|
-
(rewrites.length ? `\n\nRewritten for ARDY:\n${rewrites.join("\n")}` : "") +
|
|
1194
|
-
(normalized.dropped > 0 ? `\n (${normalized.dropped} beat(s) past the 8-phase limit were dropped)` : "") +
|
|
1195
|
-
"\n\nGenerate them from the studio's Prompt Blocks panel, or with generate_motion.",
|
|
1196
|
-
);
|
|
1197
1195
|
},
|
|
1198
1196
|
),
|
|
1199
1197
|
|
|
@@ -1215,23 +1213,10 @@ export const createToolHandlers = ({ projectRootPromise, motionJobs, publishMoti
|
|
|
1215
1213
|
if (!motionUrlPattern.test(args.url)) {
|
|
1216
1214
|
throw new Error(`Unsupported motion url "${args.url}". Use /ardy/motions/<id> or /ardy/assembled/<name>.npz.`);
|
|
1217
1215
|
}
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
let characterId;
|
|
1223
|
-
if (args.character !== undefined) {
|
|
1224
|
-
try {
|
|
1225
|
-
await refreshLiveDescription();
|
|
1226
|
-
} catch (error) {
|
|
1227
|
-
return liveError(error);
|
|
1228
|
-
}
|
|
1229
|
-
const target = findCharacter(args.character);
|
|
1230
|
-
if (!target) return text(`No character "${args.character}". ${castHint()}`);
|
|
1231
|
-
characterId = target.id;
|
|
1232
|
-
}
|
|
1233
|
-
await liveHub.command("load_motion", { url: args.url, prompt: args.prompt ?? "", ...(characterId ? { characterId } : {}) }, workspaceHandle);
|
|
1234
|
-
return text(`Motion installed from ${args.url}${characterId ? ` onto ${characterId}` : ""}.`);
|
|
1216
|
+
return runStudioCommand({ ...args, action: 'motion.replace', args: async context => ({
|
|
1217
|
+
characterId: args.character === undefined ? context.activeCharacterId : await liveCharacterId(args.character),
|
|
1218
|
+
url: args.url, prompt: args.prompt ?? '',
|
|
1219
|
+
}) });
|
|
1235
1220
|
},
|
|
1236
1221
|
),
|
|
1237
1222
|
|
|
@@ -1240,9 +1225,8 @@ export const createToolHandlers = ({ projectRootPromise, motionJobs, publishMoti
|
|
|
1240
1225
|
{
|
|
1241
1226
|
title: "Generate character motion (Kimodo)",
|
|
1242
1227
|
description:
|
|
1243
|
-
"
|
|
1244
|
-
"
|
|
1245
|
-
"the MCP model does not poll task state. A completed event loads the active character's timeline.\n\n" +
|
|
1228
|
+
"Alias for motion.generate in the editor. Returns its bus receipt; started jobs carry jobId for studio_run job.await or job.cancel. " +
|
|
1229
|
+
"The installed take is undoable with studio_run edit.undo.\n\n" +
|
|
1246
1230
|
PROMPT_GUIDE,
|
|
1247
1231
|
inputSchema: {
|
|
1248
1232
|
phases: z
|
|
@@ -1289,203 +1273,16 @@ export const createToolHandlers = ({ projectRootPromise, motionJobs, publishMoti
|
|
|
1289
1273
|
),
|
|
1290
1274
|
},
|
|
1291
1275
|
},
|
|
1292
|
-
async ({ phases
|
|
1293
|
-
|
|
1294
|
-
const
|
|
1295
|
-
const
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
// Keep notes beside the prompt they belong to: a blank beat normalises to
|
|
1303
|
-
// "" and is dropped, which would otherwise slide every later note onto the
|
|
1304
|
-
// wrong prompt in the rewrite report.
|
|
1305
|
-
const kept = normalized.texts
|
|
1306
|
-
.map((t, i) => ({ text: t, notes: normalized.notes[i], source: normalized.sources[i] }))
|
|
1307
|
-
.filter((p) => p.text);
|
|
1308
|
-
const prompts = kept.map((p) => p.text);
|
|
1309
|
-
// One complete beat is a legitimate clip — ARDY's own examples are single
|
|
1310
|
-
// prompts ("A person walks in a circle."), so a valid one-phase request must
|
|
1311
|
-
// not fail merely for being one phase. Only an empty request is refused.
|
|
1312
|
-
if (prompts.length === 0) return text("Give at least one motion beat.");
|
|
1313
|
-
|
|
1314
|
-
// ARDY Core is 20 fps; segments must tile 0..clipFrames exactly, and each
|
|
1315
|
-
// one needs at least 3 frames.
|
|
1316
|
-
const ARDY_FPS =
|
|
1317
|
-
(process.env.CCLAY_MOTION_BACKEND || "kimodo").trim().toLowerCase() === "kimodo" ? 24 : 20;
|
|
1318
|
-
let segments;
|
|
1319
|
-
let clipFrames;
|
|
1320
|
-
let chained = 0;
|
|
1321
|
-
if (timed) {
|
|
1322
|
-
// A beat that became several blocks shares its time between them, so an
|
|
1323
|
-
// explicit "6 seconds of walking" stays 6 seconds however it was phrased.
|
|
1324
|
-
const pieceCount = kept.reduce((acc, piece) => {
|
|
1325
|
-
acc[piece.source] = (acc[piece.source] ?? 0) + 1;
|
|
1326
|
-
return acc;
|
|
1327
|
-
}, {});
|
|
1328
|
-
segments = [];
|
|
1329
|
-
let cursor = 0;
|
|
1330
|
-
for (const { text: prompt, source } of kept) {
|
|
1331
|
-
const whole = phaseSeconds[source] ?? seconds / phases.length;
|
|
1332
|
-
const share = whole / pieceCount[source];
|
|
1333
|
-
// The studio caps a block at BLOCK_MAX_SECONDS and refuses to generate
|
|
1334
|
-
// a longer one, so a long beat becomes consecutive blocks here rather
|
|
1335
|
-
// than something the UI would reject.
|
|
1336
|
-
const spans = splitLongBeat(share);
|
|
1337
|
-
if (spans.length > 1) chained += spans.length - 1;
|
|
1338
|
-
for (const span of spans) {
|
|
1339
|
-
const startFrame = cursor;
|
|
1340
|
-
cursor += Math.max(3, Math.round(span * ARDY_FPS));
|
|
1341
|
-
segments.push({ startFrame, endFrame: cursor, prompt });
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
clipFrames = cursor;
|
|
1345
|
-
} else {
|
|
1346
|
-
clipFrames = Math.floor(seconds * ARDY_FPS);
|
|
1347
|
-
// Even division must also respect the cap: enough blocks that no single
|
|
1348
|
-
// one exceeds it, distributed evenly across the clip.
|
|
1349
|
-
const perPhase = seconds / prompts.length;
|
|
1350
|
-
const piecesPer = Math.ceil(perPhase / BLOCK_MAX_SECONDS);
|
|
1351
|
-
if (piecesPer > 1) chained = prompts.length * (piecesPer - 1);
|
|
1352
|
-
const total = prompts.length * piecesPer;
|
|
1353
|
-
const per = Math.floor(clipFrames / total);
|
|
1354
|
-
segments = [];
|
|
1355
|
-
for (const [i, prompt] of prompts.entries()) {
|
|
1356
|
-
for (let k = 0; k < piecesPer; k += 1) {
|
|
1357
|
-
const index = i * piecesPer + k;
|
|
1358
|
-
segments.push({
|
|
1359
|
-
startFrame: index * per,
|
|
1360
|
-
endFrame: index === total - 1 ? clipFrames : (index + 1) * per,
|
|
1361
|
-
prompt,
|
|
1362
|
-
});
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
|
-
}
|
|
1366
|
-
const clipSeconds = clipFrames / ARDY_FPS;
|
|
1367
|
-
// A single beat has no sequence to chain: send it as a plain prompt;
|
|
1368
|
-
// composite physical wording remains intact in the normalized text.
|
|
1369
|
-
// Long single prompts are the exception: split the actual clip length
|
|
1370
|
-
// into equal Kimodo blocks so every wire segment stays within the cap.
|
|
1371
|
-
const singlePromptNeedsChain = prompts.length === 1 && clipSeconds > BLOCK_MAX_SECONDS;
|
|
1372
|
-
if (prompts.length === 1) {
|
|
1373
|
-
if (singlePromptNeedsChain) {
|
|
1374
|
-
segments = tileClipFrames(clipFrames, clipSeconds).map((b) => ({ ...b, prompt: prompts[0] }));
|
|
1375
|
-
chained = segments.length - 1;
|
|
1376
|
-
} else {
|
|
1377
|
-
chained = 0;
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
const rewrites = kept
|
|
1381
|
-
.map(({ text: prompt, notes }, i) => (notes.length ? ` ${i + 1}. ${prompt} ← ${notes.join("; ")}` : null))
|
|
1382
|
-
.filter(Boolean);
|
|
1383
|
-
const chainNote = chained > 0 ? `\n (${chained} block(s) chained to keep every block within ${BLOCK_MAX_SECONDS}s)` : "";
|
|
1384
|
-
// A drop is reported on its own account: a caller can send nine already-perfect
|
|
1385
|
-
// phases, which produces no rewrite and no chaining yet still loses the ninth
|
|
1386
|
-
// to the schema's 8-phase limit, and silence there would hide a lost beat.
|
|
1387
|
-
const dropNote =
|
|
1388
|
-
normalized.dropped > 0 ? `\n (${normalized.dropped} beat(s) past the 8-phase limit were dropped)` : "";
|
|
1389
|
-
const promptNote =
|
|
1390
|
-
rewrites.length || chainNote || dropNote
|
|
1391
|
-
? (rewrites.length ? `\n\nRewritten for ARDY:\n${rewrites.join("\n")}` : "\n") + dropNote + chainNote
|
|
1392
|
-
: "";
|
|
1393
|
-
|
|
1394
|
-
if (!liveHub?.connected) return text(noLiveEditor("generate_motion requires a connected CozyClay editor so completion can be delivered over its live socket."));
|
|
1395
|
-
const workspaceHandle = liveWorkspace.getStore() ?? liveHub.resolveWorkspace("generate_motion");
|
|
1396
|
-
const workspaceId = liveHub.workspaceId(workspaceHandle);
|
|
1397
|
-
// Reusing an existing take is installation, not generation demand. The
|
|
1398
|
-
// shared lifecycle sanitizes before the targeted live transport sees it;
|
|
1399
|
-
// disconnected events are omitted, never reconstructed on reconnect.
|
|
1400
|
-
const motionRequest = motion_url ? null : startMotionRequest({ surface: "mcp", input_mode: "prompt" }, {
|
|
1401
|
-
capture: (event, props) => liveHub?.sendEvent(workspaceId, "motion_telemetry", { event, props }),
|
|
1402
|
-
});
|
|
1403
|
-
try {
|
|
1404
|
-
await refreshLiveDescription();
|
|
1405
|
-
} catch (error) {
|
|
1406
|
-
return liveError(error);
|
|
1407
|
-
}
|
|
1408
|
-
const targetCharacterId = stage().characters.find((character) => character.id === state.focus)?.id ?? stage().characters[0]?.id ?? null;
|
|
1409
|
-
let job;
|
|
1410
|
-
try {
|
|
1411
|
-
job = motionJobs.create(workspaceId);
|
|
1412
|
-
job.motionRequest = motionRequest;
|
|
1413
|
-
} catch (error) {
|
|
1414
|
-
return liveError(error);
|
|
1415
|
-
}
|
|
1416
|
-
const body =
|
|
1417
|
-
prompts.length === 1 && !singlePromptNeedsChain
|
|
1418
|
-
? { prompt: prompts[0], duration: clipSeconds, posePin: false }
|
|
1419
|
-
: { prompt: prompts.join(" "), duration: clipSeconds, segments, posePin: false };
|
|
1420
|
-
if (seed !== undefined) body.seed = seed;
|
|
1421
|
-
|
|
1422
|
-
// Abort tears down the HTTP stream. The bridge owns the child process group
|
|
1423
|
-
// and kills it on disconnect, so cancellation stops generator work before a
|
|
1424
|
-
// terminal cancelled event is sent; it never reaches editor installation.
|
|
1425
|
-
const run = async () => {
|
|
1426
|
-
if (job.status === "cancelled") return;
|
|
1427
|
-
const controller = new AbortController();
|
|
1428
|
-
const deadline = setTimeout(() => controller.abort(new Error("Motion generation exceeded the 5 minute deadline.")), 5 * 60_000);
|
|
1429
|
-
deadline.unref?.();
|
|
1430
|
-
job.cancel = () => {
|
|
1431
|
-
controller.abort();
|
|
1432
|
-
motionRequest?.fail(null, "aborted");
|
|
1433
|
-
};
|
|
1434
|
-
try {
|
|
1435
|
-
let motionUrl = motion_url;
|
|
1436
|
-
if (!motionUrl) {
|
|
1437
|
-
let health;
|
|
1438
|
-
try {
|
|
1439
|
-
const response = await fetch(`${bridge}/ardy/health`, {
|
|
1440
|
-
signal: AbortSignal.any([controller.signal, AbortSignal.timeout(5_000)]),
|
|
1441
|
-
});
|
|
1442
|
-
const payload = await response.json();
|
|
1443
|
-
health = { ...payload, ok: response.ok && payload?.ok !== false };
|
|
1444
|
-
} catch (error) {
|
|
1445
|
-
if (controller.signal.aborted) throw error;
|
|
1446
|
-
health = { ok: false };
|
|
1447
|
-
}
|
|
1448
|
-
if (job.status === "cancelled") return;
|
|
1449
|
-
// Readiness controls execution independently of best-effort telemetry.
|
|
1450
|
-
const reason = motionPreflightReason(health, { body });
|
|
1451
|
-
motionRequest?.preflight(health, { body });
|
|
1452
|
-
if (reason) {
|
|
1453
|
-
motionJobs.transition(job, "failed", { message: `Motion generation preflight blocked: ${reason}.` });
|
|
1454
|
-
await publishMotionJob(job);
|
|
1455
|
-
return;
|
|
1456
|
-
}
|
|
1457
|
-
motionJobs.transition(job, "running");
|
|
1458
|
-
motionRequest?.start();
|
|
1459
|
-
const res = await fetch(`${bridge}/ardy/generate`, {
|
|
1460
|
-
method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), signal: controller.signal,
|
|
1461
|
-
});
|
|
1462
|
-
motionUrl = await readMotionStream(res);
|
|
1463
|
-
}
|
|
1464
|
-
if (job.status === "cancelled") return;
|
|
1465
|
-
if (typeof motionUrl !== "string") throw new Error("Generation ended without a motion.");
|
|
1466
|
-
if (!motionUrlPattern.test(motionUrl)) throw new Error("Generator returned an invalid motion URL.");
|
|
1467
|
-
motionRequest?.succeed();
|
|
1468
|
-
motionJobs.transition(job, "completed", {
|
|
1469
|
-
motionUrl, prompt: prompts.join(" "), blocks: segments, drop, targetCharacterId,
|
|
1470
|
-
summary: `${clipSeconds.toFixed(1)}s / ${clipFrames} frames${promptNote}`,
|
|
1471
|
-
});
|
|
1472
|
-
await publishMotionJob(job);
|
|
1473
|
-
} catch (error) {
|
|
1474
|
-
if (job.status === "cancelled" || controller.signal.aborted || error?.name === "AbortError") {
|
|
1475
|
-
motionRequest?.fail(error, "aborted");
|
|
1476
|
-
if (job.status !== "cancelled") motionJobs.transition(job, "cancelled", { message: "Generation cancelled before editor delivery." });
|
|
1477
|
-
} else {
|
|
1478
|
-
motionRequest?.fail(error);
|
|
1479
|
-
motionJobs.transition(job, "failed", { message: error instanceof Error ? error.message : "Motion generation failed." });
|
|
1480
|
-
}
|
|
1481
|
-
await publishMotionJob(job);
|
|
1482
|
-
} finally {
|
|
1483
|
-
clearTimeout(deadline);
|
|
1484
|
-
job.cancel = null;
|
|
1485
|
-
}
|
|
1486
|
-
};
|
|
1487
|
-
queueMicrotask(run);
|
|
1488
|
-
return text(JSON.stringify(motionJobs.task(job)));
|
|
1276
|
+
async ({ phases, seconds = 9, seed, motion_url, drop, ...admission }) => {
|
|
1277
|
+
if (!liveHub?.connected) return text(noLiveEditor("Motion requires a connected CozyClay editor."));
|
|
1278
|
+
const normalized = normalizePhases(phases.map(phase => typeof phase === 'string' ? phase : phase.text));
|
|
1279
|
+
const beats = normalized.texts.map((text, index) => ({ text, seconds: phases[normalized.sources[index]]?.seconds ?? seconds / phases.length })).filter(beat => beat.text);
|
|
1280
|
+
return runStudioCommand({ ...admission, action: motion_url ? 'motion.replace' : 'motion.generate', args: context => {
|
|
1281
|
+
const generated = generationArgs({ characterId: context.activeCharacterId, source: { kind: 'generate', beats, ...(seed === undefined ? {} : { seed }) } });
|
|
1282
|
+
return motion_url ? { characterId: generated.characterId, url: motion_url, prompt: beats.map(beat => beat.text).join(' '),
|
|
1283
|
+
blocks: generated.blocks.map(({ startFrame, endFrame, text: prompt }) => ({ startFrame, endFrame, prompt })), ...(drop ? { drop } : {}) }
|
|
1284
|
+
: { ...generated, ...(drop ? { drop } : {}) };
|
|
1285
|
+
} });
|
|
1489
1286
|
},
|
|
1490
1287
|
),
|
|
1491
1288
|
|
|
@@ -1495,7 +1292,7 @@ export const createToolHandlers = ({ projectRootPromise, motionJobs, publishMoti
|
|
|
1495
1292
|
title: "Move, rotate or scale an object",
|
|
1496
1293
|
description:
|
|
1497
1294
|
"Unlike place_object, update_object changes an existing prop instead of adding one. " +
|
|
1498
|
-
"Every field is optional; omitted fields are left alone. Transforms go through the same clamp/snap path the studio's gizmo uses.",
|
|
1295
|
+
"Every field is optional; omitted fields are left alone. Transforms go through the same clamp/snap path the studio's gizmo uses. hidden shows or hides the prop without deleting it.",
|
|
1499
1296
|
inputSchema: {
|
|
1500
1297
|
id: z.string().describe("object id from place_object or describe_scene"),
|
|
1501
1298
|
x: z.number().optional(),
|
|
@@ -1530,9 +1327,10 @@ export const createToolHandlers = ({ projectRootPromise, motionJobs, publishMoti
|
|
|
1530
1327
|
.describe("travel path; null clears it and the object stands still again"),
|
|
1531
1328
|
height: z.number().positive().optional().describe("cutout or mesh height in metres"),
|
|
1532
1329
|
clay: z.boolean().optional().describe("mesh only: replace file materials with matte clay"),
|
|
1330
|
+
hidden: z.boolean().optional().describe("true hides the prop without deleting it"),
|
|
1533
1331
|
},
|
|
1534
1332
|
},
|
|
1535
|
-
async ({ id, x, y, z: zPos, facing, tilt, roll, scale, scale_x, scale_y, scale_z, color, name, path, height, clay }) => {
|
|
1333
|
+
async ({ id, x, y, z: zPos, facing, tilt, roll, scale, scale_x, scale_y, scale_z, color, name, path, height, clay, hidden, ...admission }) => {
|
|
1536
1334
|
const travelPath = path === null
|
|
1537
1335
|
? null
|
|
1538
1336
|
: path
|
|
@@ -1540,14 +1338,11 @@ export const createToolHandlers = ({ projectRootPromise, motionJobs, publishMoti
|
|
|
1540
1338
|
: undefined;
|
|
1541
1339
|
if (liveHub?.connected) {
|
|
1542
1340
|
try {
|
|
1543
|
-
await
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
...(clay !== undefined ? { clay } : {}),
|
|
1549
|
-
});
|
|
1550
|
-
return text(`Updated ${id}.\n\n${sceneReport()}`);
|
|
1341
|
+
return await runStudioCommand({ ...admission, action: "object.update", args: { id, patch: {
|
|
1342
|
+
x, y, z: zPos, rot: facing, rotX: tilt, rotZ: roll,
|
|
1343
|
+
scaleX: scale_x ?? scale, scaleY: scale_y ?? scale, scaleZ: scale_z ?? scale,
|
|
1344
|
+
color, name, path: travelPath, height, clay, hidden,
|
|
1345
|
+
} } });
|
|
1551
1346
|
} catch (error) {
|
|
1552
1347
|
return liveError(error);
|
|
1553
1348
|
}
|
|
@@ -1575,6 +1370,7 @@ export const createToolHandlers = ({ projectRootPromise, motionJobs, publishMoti
|
|
|
1575
1370
|
if (name !== undefined) patch.name = name;
|
|
1576
1371
|
if (height !== undefined) patch.height = height;
|
|
1577
1372
|
if (clay !== undefined) patch.clay = clay;
|
|
1373
|
+
if (hidden !== undefined) patch.hidden = hidden;
|
|
1578
1374
|
sc.objects = updateSceneObject(sc.objects, id, patch);
|
|
1579
1375
|
return text(`Updated ${id}.\n\n${sceneReport()}`);
|
|
1580
1376
|
},
|
|
@@ -1587,11 +1383,10 @@ export const createToolHandlers = ({ projectRootPromise, motionJobs, publishMoti
|
|
|
1587
1383
|
description: "Take a prop out of the set.",
|
|
1588
1384
|
inputSchema: { id: z.string().describe("object id") },
|
|
1589
1385
|
},
|
|
1590
|
-
async ({ id }) => {
|
|
1386
|
+
async ({ id, ...admission }) => {
|
|
1591
1387
|
if (liveHub?.connected) {
|
|
1592
1388
|
try {
|
|
1593
|
-
await
|
|
1594
|
-
return text(`Removed ${id}.\n\n${sceneReport()}`);
|
|
1389
|
+
return await runStudioCommand({ ...admission, action: "object.remove", args: { ids: [id] } });
|
|
1595
1390
|
} catch (error) {
|
|
1596
1391
|
return liveError(error);
|
|
1597
1392
|
}
|
|
@@ -1635,18 +1430,12 @@ export const createToolHandlers = ({ projectRootPromise, motionJobs, publishMoti
|
|
|
1635
1430
|
label: z.string().min(1).default("MCP batch").describe("the single editor undo entry name"),
|
|
1636
1431
|
},
|
|
1637
1432
|
},
|
|
1638
|
-
async ({ ops, atomic, stopOnError, label }) => {
|
|
1433
|
+
async ({ ops, atomic, stopOnError, label, ...admission }) => {
|
|
1639
1434
|
if (!liveHub?.connected) return text(noLiveEditor("apply_batch requires a connected CozyClay editor."));
|
|
1640
1435
|
try {
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
const failure = failed[0];
|
|
1645
|
-
const summary = result?.rolledBack
|
|
1646
|
-
? `Batch rolled back after failure at operation ${failure?.index ?? "unknown"}.`
|
|
1647
|
-
: `Applied ${applied.length} operation(s).`;
|
|
1648
|
-
const detail = failure ? ` Failure at operation ${failure.index}: ${failure.error}.` : "";
|
|
1649
|
-
return text(`${summary}${detail}\n\n${sceneReport()}`);
|
|
1436
|
+
// #446: the v1 alias is objects-only, so objects.batch (not a
|
|
1437
|
+
// generic run.batch) preserves its options and single undo entry.
|
|
1438
|
+
return await runStudioCommand({ ...admission, action: "objects.batch", args: { ops, atomic, stopOnError, label } });
|
|
1650
1439
|
} catch (error) {
|
|
1651
1440
|
return liveError(error);
|
|
1652
1441
|
}
|
|
@@ -1776,30 +1565,30 @@ export const createToolHandlers = ({ projectRootPromise, motionJobs, publishMoti
|
|
|
1776
1565
|
{
|
|
1777
1566
|
title: "Add a scene",
|
|
1778
1567
|
description:
|
|
1779
|
-
"Add another scene to the project and make it active.
|
|
1780
|
-
"
|
|
1781
|
-
"without one, this changes MCP memory only.",
|
|
1568
|
+
"Add another scene to the project and make it active. A connected editor runs scene.create then scene.rename " +
|
|
1569
|
+
"and returns both bus receipts; without one, this changes MCP memory only.",
|
|
1782
1570
|
inputSchema: { name: z.string().default("SCENE 02").describe("scene name") },
|
|
1783
1571
|
},
|
|
1784
|
-
async ({ name }) => {
|
|
1572
|
+
async ({ name, ...admission }) => {
|
|
1785
1573
|
if (liveHub?.connected) {
|
|
1574
|
+
let created;
|
|
1786
1575
|
try {
|
|
1787
|
-
await
|
|
1576
|
+
created = await executeStudioCommand({ ...admission, action: "scene.create" });
|
|
1577
|
+
if (!created.ok) return studioResult(created);
|
|
1578
|
+
// Creation is a document boundary; naming is a separate retained edit.
|
|
1579
|
+
const renamed = await executeStudioCommand({ ...admission, action: "scene.rename",
|
|
1580
|
+
commandId: admission.commandId ? createHash("sha256").update(`${admission.commandId}:rename`).digest("hex") : undefined,
|
|
1581
|
+
args: { sceneId: created.host.sceneId, name }, expectedRevision: created.revision.after });
|
|
1582
|
+
return studioResult(created, renamed);
|
|
1788
1583
|
} catch (error) {
|
|
1789
|
-
|
|
1584
|
+
const failure = liveError(error);
|
|
1585
|
+
if (created) failure.content.unshift(...studioResult(created).content);
|
|
1586
|
+
return failure;
|
|
1790
1587
|
}
|
|
1791
1588
|
}
|
|
1792
1589
|
const document = JSON.parse(JSON.stringify(state.doc));
|
|
1793
1590
|
document.scenes = addScene(document.scenes, name);
|
|
1794
1591
|
document.activeSceneId = document.scenes[document.scenes.length - 1].id;
|
|
1795
|
-
if (liveHub?.connected) {
|
|
1796
|
-
try {
|
|
1797
|
-
const live = await appliedLiveMutation("load_scenes", { document });
|
|
1798
|
-
requireLiveSceneParity("add_scene", document, live);
|
|
1799
|
-
} catch (error) {
|
|
1800
|
-
return liveError(error);
|
|
1801
|
-
}
|
|
1802
|
-
}
|
|
1803
1592
|
state.doc = document;
|
|
1804
1593
|
return text(`Added "${scene().name}".\n\n${sceneReport()}`);
|
|
1805
1594
|
},
|
|
@@ -1810,14 +1599,18 @@ export const createToolHandlers = ({ projectRootPromise, motionJobs, publishMoti
|
|
|
1810
1599
|
{
|
|
1811
1600
|
title: "Switch the active scene",
|
|
1812
1601
|
description:
|
|
1813
|
-
"Make a different scene active.
|
|
1814
|
-
"through that workspace's load_scenes command before success is reported; without one, this changes MCP memory only.",
|
|
1602
|
+
"Make a different scene active through scene.switch and return its bus receipt. Without a connected editor, this changes MCP memory only.",
|
|
1815
1603
|
inputSchema: { name: z.string().describe("scene name to switch to") },
|
|
1816
1604
|
},
|
|
1817
|
-
async ({ name }) => {
|
|
1605
|
+
async ({ name, ...admission }) => {
|
|
1818
1606
|
if (liveHub?.connected) {
|
|
1819
1607
|
try {
|
|
1820
|
-
await
|
|
1608
|
+
return await runStudioCommand({ ...admission, action: "scene.switch", args: async () => {
|
|
1609
|
+
await refreshLiveDescription();
|
|
1610
|
+
const target = state.doc.scenes.find(row => row.name.toLowerCase() === name.toLowerCase());
|
|
1611
|
+
if (!target) throw new Error(`No scene "${name}".`);
|
|
1612
|
+
return { sceneId: target.id };
|
|
1613
|
+
} });
|
|
1821
1614
|
} catch (error) {
|
|
1822
1615
|
return liveError(error);
|
|
1823
1616
|
}
|
|
@@ -1828,14 +1621,6 @@ export const createToolHandlers = ({ projectRootPromise, motionJobs, publishMoti
|
|
|
1828
1621
|
}
|
|
1829
1622
|
const document = JSON.parse(JSON.stringify(state.doc));
|
|
1830
1623
|
document.activeSceneId = target.id;
|
|
1831
|
-
if (liveHub?.connected) {
|
|
1832
|
-
try {
|
|
1833
|
-
const live = await appliedLiveMutation("load_scenes", { document });
|
|
1834
|
-
requireLiveSceneParity("switch_scene", document, live);
|
|
1835
|
-
} catch (error) {
|
|
1836
|
-
return liveError(error);
|
|
1837
|
-
}
|
|
1838
|
-
}
|
|
1839
1624
|
state.doc = document;
|
|
1840
1625
|
return text(`Switched to "${scene().name}".\n\n${sceneReport()}`);
|
|
1841
1626
|
},
|
|
@@ -1846,10 +1631,13 @@ export const createToolHandlers = ({ projectRootPromise, motionJobs, publishMoti
|
|
|
1846
1631
|
{
|
|
1847
1632
|
title: "Open a .cclayproject file",
|
|
1848
1633
|
description:
|
|
1849
|
-
"Load a project authored in the CozyClay studio (or saved here).
|
|
1850
|
-
inputSchema: {
|
|
1634
|
+
"Load a project authored in the CozyClay studio (or saved here). Replacing scene ids in a connected editor requires a Studio-issued confirmationToken; returns the load_scenes bus receipt.",
|
|
1635
|
+
inputSchema: {
|
|
1636
|
+
path: z.string().describe("path to a .cclayproject file"),
|
|
1637
|
+
confirmationToken: z.string().min(1).max(120).optional().describe("token issued by the Studio UI for this exact scene document"),
|
|
1638
|
+
},
|
|
1851
1639
|
},
|
|
1852
|
-
async ({ path }) => {
|
|
1640
|
+
async ({ path, ...admission }) => {
|
|
1853
1641
|
let full;
|
|
1854
1642
|
let raw;
|
|
1855
1643
|
try {
|
|
@@ -1875,8 +1663,14 @@ export const createToolHandlers = ({ projectRootPromise, motionJobs, publishMoti
|
|
|
1875
1663
|
const nextDocument = scenes.document;
|
|
1876
1664
|
if (liveHub?.connected) {
|
|
1877
1665
|
try {
|
|
1878
|
-
const
|
|
1879
|
-
|
|
1666
|
+
const loaded = await executeStudioCommand({ ...admission, action: "load_scenes", args: { document: nextDocument } });
|
|
1667
|
+
if (loaded.ok) {
|
|
1668
|
+
state.name = result.project.name;
|
|
1669
|
+
state.focus = null;
|
|
1670
|
+
state.focusLocked = false;
|
|
1671
|
+
state.markedFraming = null;
|
|
1672
|
+
}
|
|
1673
|
+
return studioResult(loaded);
|
|
1880
1674
|
} catch (error) {
|
|
1881
1675
|
return liveError(error);
|
|
1882
1676
|
}
|
|
@@ -1968,5 +1762,58 @@ export const createToolHandlers = ({ projectRootPromise, motionJobs, publishMoti
|
|
|
1968
1762
|
return text(`Saved "${state.name}" to ${full} (${state.doc.scenes.length} scene(s)).`);
|
|
1969
1763
|
},
|
|
1970
1764
|
),
|
|
1765
|
+
|
|
1766
|
+
tool(
|
|
1767
|
+
"studio_commands",
|
|
1768
|
+
{
|
|
1769
|
+
title: "List Studio commands",
|
|
1770
|
+
description:
|
|
1771
|
+
"List every command the connected editor registers, for studio_run: id, label, kind (mutation, transient, job, document), " +
|
|
1772
|
+
"exposure, whether it is available now (the reason when not), and timeoutMs or generation where declared. The list carries " +
|
|
1773
|
+
"no schemas; pass ids to read those commands' full declarations, description and input schema included.",
|
|
1774
|
+
inputSchema: {
|
|
1775
|
+
ids: z.array(z.string().min(1).max(120)).min(1).max(32).optional().describe("command ids whose full declarations to read"),
|
|
1776
|
+
},
|
|
1777
|
+
},
|
|
1778
|
+
async ({ ids }) => {
|
|
1779
|
+
if (!liveHub?.connected) return liveError(new Error(noLiveEditor("studio_commands requires a connected CozyClay editor.")));
|
|
1780
|
+
try {
|
|
1781
|
+
const listed = await liveHub.command("inspect_studio", { scope: "actions", ...(ids ? { ids } : {}) }, liveWorkspace.getStore());
|
|
1782
|
+
return text(JSON.stringify({ actions: listed?.actions ?? [] }));
|
|
1783
|
+
} catch (error) {
|
|
1784
|
+
return liveError(error);
|
|
1785
|
+
}
|
|
1786
|
+
},
|
|
1787
|
+
),
|
|
1788
|
+
|
|
1789
|
+
tool(
|
|
1790
|
+
"studio_run",
|
|
1791
|
+
{
|
|
1792
|
+
title: "Run a Studio command",
|
|
1793
|
+
description:
|
|
1794
|
+
"Run one command the connected editor registers, by id, through the editor's command bus: the same door its UI and agent use. " +
|
|
1795
|
+
"args must match the command's input schema (read it with studio_commands and ids). The command is admitted at the open " +
|
|
1796
|
+
"document and its current revision unless expectedRevision is given, and answers the editor's JSON receipt: status, summary, " +
|
|
1797
|
+
"affectedIds, undo, and output for a job. A refusal is a receipt with ok false and its code: STALE_SCENE means read again and " +
|
|
1798
|
+
"re-issue; CONFIRMATION_REQUIRED means obtain the user's approval in Studio and pass its confirmationToken. Reusing a commandId returns the receipt of " +
|
|
1799
|
+
"that earlier call instead of running again.",
|
|
1800
|
+
inputSchema: {
|
|
1801
|
+
action: z.string().min(1).max(120).describe("command id, as studio_commands lists it"),
|
|
1802
|
+
args: z.record(z.string(), z.unknown()).default({}).describe("the command's arguments, matching its input schema"),
|
|
1803
|
+
expectedRevision: z.number().int().min(0).optional().describe("the scene revision the command is admitted at; defaults to the current one"),
|
|
1804
|
+
commandId: z.string().min(1).max(120).optional().describe("idempotency key; a repeated id answers the first call's receipt"),
|
|
1805
|
+
timeoutMs: z.number().int().min(1).max(MAX_COMMAND_TIMEOUT_MS).optional().describe("how long the hub waits; defaults to the command's declared timeout"),
|
|
1806
|
+
confirmationToken: z.string().min(1).max(120).optional().describe("Studio UI approval token for this exact command; MCP cannot mint one"),
|
|
1807
|
+
},
|
|
1808
|
+
},
|
|
1809
|
+
async (args) => {
|
|
1810
|
+
if (!liveHub?.connected) return liveError(new Error(noLiveEditor("studio_run requires a connected CozyClay editor.")));
|
|
1811
|
+
try {
|
|
1812
|
+
return await runStudioCommand(args);
|
|
1813
|
+
} catch (error) {
|
|
1814
|
+
return liveError(error);
|
|
1815
|
+
}
|
|
1816
|
+
},
|
|
1817
|
+
),
|
|
1971
1818
|
];
|
|
1972
1819
|
};
|