@pinepaper.studio/mcp-server 1.6.5 → 1.6.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.de.md +20 -1
  2. package/README.es.md +20 -1
  3. package/README.fr.md +20 -1
  4. package/README.hi.md +18 -1
  5. package/README.ja.md +19 -1
  6. package/README.ko.md +18 -1
  7. package/README.md +177 -5
  8. package/README.pt-BR.md +20 -1
  9. package/README.zh-CN.md +18 -1
  10. package/assets/edit-whenever.svg +75 -0
  11. package/dist/agent/export-manager.d.ts.map +1 -1
  12. package/dist/agent/export-manager.js +2 -2
  13. package/dist/agent/export-manager.js.map +1 -1
  14. package/dist/agent/session-manager.d.ts.map +1 -1
  15. package/dist/agent/session-manager.js.map +1 -1
  16. package/dist/assets/repositories/fontawesome.d.ts.map +1 -1
  17. package/dist/assets/repositories/fontawesome.js.map +1 -1
  18. package/dist/assets/repositories/openclipart.d.ts.map +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +0 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/ontology/design-graph.d.ts.map +1 -1
  23. package/dist/ontology/design-graph.js +0 -1
  24. package/dist/ontology/design-graph.js.map +1 -1
  25. package/dist/ontology/vocabulary.d.ts.map +1 -1
  26. package/dist/ontology/vocabulary.js +119 -0
  27. package/dist/ontology/vocabulary.js.map +1 -1
  28. package/dist/taxonomy/classifier.js +1 -1
  29. package/dist/taxonomy/classifier.js.map +1 -1
  30. package/dist/taxonomy/error-recovery.d.ts.map +1 -1
  31. package/dist/taxonomy/matcher.d.ts.map +1 -1
  32. package/dist/taxonomy/matcher.js.map +1 -1
  33. package/dist/tools/definitions.d.ts +1 -1
  34. package/dist/tools/definitions.d.ts.map +1 -1
  35. package/dist/tools/definitions.js +906 -34
  36. package/dist/tools/definitions.js.map +1 -1
  37. package/dist/tools/handlers.d.ts.map +1 -1
  38. package/dist/tools/handlers.js +71 -7
  39. package/dist/tools/handlers.js.map +1 -1
  40. package/dist/tools/minimal-descriptions.d.ts.map +1 -1
  41. package/dist/tools/minimal-descriptions.js +11 -2
  42. package/dist/tools/minimal-descriptions.js.map +1 -1
  43. package/dist/tools/toolkits.d.ts.map +1 -1
  44. package/dist/tools/toolkits.js +14 -0
  45. package/dist/tools/toolkits.js.map +1 -1
  46. package/dist/types/code-generator.d.ts +46 -2
  47. package/dist/types/code-generator.d.ts.map +1 -1
  48. package/dist/types/code-generator.js +1004 -24
  49. package/dist/types/code-generator.js.map +1 -1
  50. package/dist/types/schemas.d.ts +2266 -215
  51. package/dist/types/schemas.d.ts.map +1 -1
  52. package/dist/types/schemas.js +553 -50
  53. package/dist/types/schemas.js.map +1 -1
  54. package/manifest.json +37 -1
  55. package/package.json +17 -7
@@ -98,6 +98,18 @@ export const ItemTypeSchema = z.enum([
98
98
  'diamond',
99
99
  'arrow',
100
100
  'heart',
101
+ // RENDER-TIME SURFACES. These two are not Paper items and never enter the
102
+ // scene tree: they are drawn per pixel by the cloud rasterizer, straight from
103
+ // the scene document. They are here because a capability nobody can name is a
104
+ // capability nobody has — the cloud has drawn shaders and fields for months
105
+ // and no tool has ever offered them, so the only way to reach one was to
106
+ // hand-write a scene file.
107
+ //
108
+ // In the browser they degrade to a flat plate of their own colour (see
109
+ // generateCreateItemCode) so a composition built with one still reads
110
+ // locally; the shaded surface appears in a cloud render.
111
+ 'shader',
112
+ 'field',
101
113
  ]);
102
114
  // Text item properties
103
115
  export const TextPropertiesSchema = z.object({
@@ -182,14 +194,32 @@ export const ArcPropertiesSchema = z.object({
182
194
  // =============================================================================
183
195
  // ANIMATION TYPES
184
196
  // =============================================================================
197
+ // The loop animations the engine's driver can actually run.
198
+ //
199
+ // Pinned to the engine by animation-type-parity.test.ts. It offered 'slide',
200
+ // which has NEVER existed — the real ones are slideLeftRight and slideUpDown.
201
+ // The engine stored the unknown value and the driver's switch fell through, so
202
+ // the item sat still while its data claimed a slide, and nothing anywhere said
203
+ // so. It also hid twelve types that do work.
185
204
  export const SimpleAnimationTypeSchema = z.enum([
186
- 'pulse',
187
- 'rotate',
188
205
  'bounce',
206
+ 'breathe',
189
207
  'fade',
190
- 'wobble',
191
- 'slide',
208
+ 'glow',
209
+ 'jelly',
210
+ 'path',
211
+ 'pulse',
212
+ 'rotate',
213
+ 'scrollDown',
214
+ 'scrollLeft',
215
+ 'scrollRight',
216
+ 'scrollUp',
217
+ 'shake',
218
+ 'slideLeftRight',
219
+ 'slideUpDown',
220
+ 'swing',
192
221
  'typewriter',
222
+ 'wobble',
193
223
  ]).describe('Simple loop animation type');
194
224
  // Keyframe schema
195
225
  export const KeyframeSchema = z.object({
@@ -200,7 +230,20 @@ export const KeyframeSchema = z.object({
200
230
  // =============================================================================
201
231
  // RELATION TYPES
202
232
  // =============================================================================
233
+ // The relation vocabulary the engine can actually run.
234
+ //
235
+ // Held in step with the engine by relation-parity.test.ts, against a fixture of
236
+ // the registry's own map. This list is a hard gate: a name missing from it is
237
+ // REJECTED at validation, so for 95 live relations the engine could do it and
238
+ // no agent could name it — and for a model those are the same condition. The
239
+ // whole interactive vocabulary sat on the wrong side of that line.
240
+ //
241
+ // Relations a dedicated tool emits (deform_*, effect_*, geo_*, bone_*, …) are
242
+ // deliberately absent: the agent authors those THROUGH that tool, and a second
243
+ // name here would be a worse way to do the same thing. The test file holds that
244
+ // exclusion list, with the reason.
203
245
  export const RelationTypeSchema = z.enum([
246
+ // --- Motion, spatial and transform ---
204
247
  'orbits',
205
248
  'follows',
206
249
  'attached_to',
@@ -209,61 +252,99 @@ export const RelationTypeSchema = z.enum([
209
252
  'mirrors',
210
253
  'parallax',
211
254
  'bounds_to',
255
+ 'anchored_in_world',
256
+ 'spring_follow',
257
+ 'repels',
258
+ 'wiggle',
259
+ // --- Animation and sequencing ---
212
260
  'animates',
213
- // Manim-inspired animation relations
214
261
  'grows_from',
215
262
  'staggered_with',
216
263
  'indicates',
217
264
  'circumscribes',
218
265
  'wave_through',
219
- 'camera_follows',
220
- 'camera_animates',
221
266
  'morphs_to',
222
- // Pair-by-index group morph; same call shape works for any two paper.Groups
223
267
  'group_morphs_to',
224
- // Self-relation; item moves along a custom-drawn path stored in params
225
268
  'moves_along_path',
226
- // Geometric construction constraints (Layer 2) — the source is RE-DERIVED each
227
- // frame from its anchor item(s), so dragging an anchor updates it live
228
- // (GeoGebra-style). Anchor A is the relation target; extra anchors ride in params.
229
- 'is_midpoint_of', // source = midpoint(target, params.other)
230
- 'lies_on_line', // source on line target→params.other at fraction params.t
231
- 'is_centroid_of', // source = centroid(target, ...params.others)
232
- 'is_circumcenter_of', // source = circumcenter(target, params.other1, params.other2)
233
- 'concentric_with', // source shares the target's center
234
- 'construction_reveal', // self-relation: opacity 0→1 at params.revealAt over params.fadeIn (timeline-driven)
235
- // Procedural / deterministic property binding (Expression IR — FxTool S10 G1).
236
- // params.signal:true routes through the pure, seed/frame-deterministic signal
237
- // interpreter (replay-stable), else the per-frame compute fallback runs.
238
- 'driven_by', // source property = target property * multiplier + offset (+clamp); drives color/stroke/opacity/scale/pos. Needs a target.
239
- 'time_expression', // self-relation: source property driven by a math expression f(t, v) — t=time, v=params.baseValue
240
- // Event-driven scene chains (FxTool S11). The source is a pp:event (make one
241
- // with pinepaper_event); firing it runs these reactions. on_event_fire_after
242
- // chains events over canvas/wall time; on_event_add_relation mutates the graph
243
- // (the scene evolves itself). Kick a chain off with pinepaper_event pulse.
244
- 'on_event_fire_after', // event→event: when source fires, pulse target after params.delay (timeline: wall|canvas)
245
- 'on_event_add_relation', // event→item: add params.type relation from item to params.target when the event fires
246
- 'on_event_remove_relation', // event→item: remove params.type relation from item to params.target
247
- 'on_event_set_color', // event→item: set fill/stroke to params.color
248
- 'on_event_set_property', // event→item: set item[params.property] = params.value
249
- 'on_event_set_visibility', // event→item: show/hide (params.visible, default true)
250
- // Structural layout relations (FxTool S12-E1) — STATIC composition as graph edges.
251
- // Placement is derived from the TARGET'S BOUNDS and re-derived each frame, so
252
- // dragging or resizing the target moves the dependent live. Use these instead of
253
- // hardcoding x/y when the intent is "on", "under", "next to", "inside".
254
- 'on_top_of', // source bottom edge rests on target top edge (params: gap, align, overhang)
255
- 'below', // source top edge rests on target bottom edge (params: gap, align, overhang)
256
- 'beside', // source flanks the target left/right (params: side, gap, align)
257
- 'inside', // source anchored inside the target's bounds (params: anchor, padding)
258
- 'centered_on', // source center = target center + (offsetX, offsetY)
259
- 'aligned_with', // source matches target on ONE axis only — the other stays free (params: axis, offset)
269
+ 'construction_reveal',
270
+ 'triggers_animation',
271
+ 'syncs_with',
272
+ 'tours',
273
+ 'synced_to_audio',
274
+ 'expresses',
275
+ // --- Camera ---
276
+ 'camera_follows',
277
+ 'camera_animates',
278
+ // --- Geometry ---
279
+ 'is_midpoint_of',
280
+ 'lies_on_line',
281
+ 'is_centroid_of',
282
+ 'is_circumcenter_of',
283
+ 'concentric_with',
284
+ // --- Procedural / expression ---
285
+ 'driven_by',
286
+ 'time_expression',
287
+ // --- Input triggers — a user action FIRES an event channel ---
288
+ 'on_click_fire',
289
+ 'on_pointer_enter_fire',
290
+ 'on_pointer_exit_fire',
291
+ 'on_key_fire',
292
+ // --- Event reactions — an event fires and something happens ---
293
+ 'on_event_fire_after',
294
+ 'on_event_fire_if',
295
+ 'on_event_add_relation',
296
+ 'on_event_remove_relation',
297
+ 'on_event_set_color',
298
+ 'on_event_set_property',
299
+ 'on_event_set_property_from_template',
300
+ 'on_event_set_visibility',
301
+ 'on_event_set_active',
302
+ 'on_event_set_data',
303
+ 'on_event_increment',
304
+ 'on_event_toggle',
305
+ 'on_event_store_set',
306
+ 'on_event_store_increment',
307
+ // --- Proximity — the pointer entering or leaving an item ---
308
+ 'on_enter_set_color',
309
+ 'on_enter_set_property',
310
+ 'on_enter_set_property_from_template',
311
+ 'on_enter_set_visibility',
312
+ 'on_enter_set_data',
313
+ 'on_enter_increment',
314
+ 'on_enter_toggle',
315
+ 'on_exit_set_color',
316
+ 'on_exit_set_property',
317
+ 'on_exit_set_property_from_template',
318
+ 'on_exit_set_visibility',
319
+ 'on_exit_set_data',
320
+ 'on_exit_increment',
321
+ 'on_exit_toggle',
322
+ // --- Exclusive state — at most one member active ---
323
+ 'exclusive_group',
324
+ 'menubar_group',
325
+ 'restores_from',
326
+ // --- Structural and diagram ---
327
+ 'on_top_of',
328
+ 'below',
329
+ 'beside',
330
+ 'inside',
331
+ 'centered_on',
332
+ 'aligned_with',
333
+ 'part_of',
334
+ 'connects_to',
335
+ 'attached_to_tail',
336
+ 'head_points_to',
260
337
  ]).describe('Type of relationship between items');
261
338
  // Relation parameter schemas
262
339
  export const OrbitsParamsSchema = z.object({
263
340
  radius: z.number().optional().default(100).describe('Orbit radius'),
264
341
  speed: z.number().optional().default(1).describe('Rotation speed'),
265
342
  direction: z.enum(['clockwise', 'counterclockwise']).optional().default('counterclockwise'),
266
- phase: z.number().optional().default(0).describe('Starting angle offset'),
343
+ // The one angle in the relation vocabulary that is NOT degrees. Kept because
344
+ // changing it outright would silently re-time every scene that sets it — a 57x
345
+ // error of exactly the kind the "angles are DEGREES" convention exists to stop.
346
+ phase: z.number().optional().default(0).describe('Starting angle in RADIANS — the documented exception to the degrees convention, kept for existing scenes. Prefer phaseDegrees.'),
347
+ phaseDegrees: z.number().optional().describe('Starting angle in DEGREES, matching the convention every other angle uses. Takes precedence over phase when set.'),
267
348
  });
268
349
  export const FollowsParamsSchema = z.object({
269
350
  offset: PointArraySchema.optional().default([0, 0]).describe('Position offset from target'),
@@ -308,8 +389,19 @@ export const GrowsFromParamsSchema = z.object({
308
389
  });
309
390
  export const StaggeredWithParamsSchema = z.object({
310
391
  index: z.number().describe('0-based position in the stagger sequence'),
311
- stagger: z.number().optional().default(0.1).describe('Delay between items in seconds'),
312
- effect: z.enum(['fadeIn', 'fadeOut', 'growIn', 'slideIn', 'popIn']).optional().default('fadeIn').describe('Stagger effect type'),
392
+ stagger: z.number().optional().default(0.1).describe('Seconds between neighbours. Ignored when amount is set.'),
393
+ // Shape params (pp:Stagger). The plain index x stagger path is taken first and
394
+ // unchanged; these only engage when a shape is actually asked for AND count is
395
+ // known — there is no "end" to measure from without it, so an origin like
396
+ // 'center' would have nothing to mean.
397
+ count: z.number().optional().describe('How many items are in the stagger. Required by any origin other than "start".'),
398
+ from: z.enum(['start', 'end', 'center', 'edges', 'random']).optional().default('start').describe('Where the stagger begins — what turns a delay into a direction. Requires count.'),
399
+ amount: z.number().optional().describe('Total seconds for the WHOLE sequence, split among the items. Overrides stagger.'),
400
+ grid: z.string().optional().describe('Rows and columns as "rows,cols" (or "auto") for a 2D stagger. Requires count.'),
401
+ axis: z.enum(['x', 'y']).optional().describe('Restrict a grid stagger to one axis.'),
402
+ distributeEase: z.enum(['linear', 'easeIn', 'easeOut', 'easeInOut', 'easeInCubic', 'easeOutCubic']).optional().default('linear')
403
+ .describe("Curve that distributes the START TIMES — not the items' own animation curve."),
404
+ effect: z.enum(['fadeIn', 'fadeOut', 'growIn', 'slideIn', 'popIn', 'typewriter']).optional().default('fadeIn').describe('Stagger effect type'),
313
405
  });
314
406
  export const IndicatesParamsSchema = z.object({
315
407
  scale: z.number().optional().default(1.3).describe('Max scale during indication'),
@@ -856,6 +948,10 @@ export const EffectTypeSchema = z.enum([
856
948
  'heatmap',
857
949
  'liquid_metal',
858
950
  'gem_smoke',
951
+ 'electric_arc',
952
+ 'vortex',
953
+ 'rain_veil',
954
+ 'caustics',
859
955
  ]).describe('Visual effect type (particle effects + shader auras)');
860
956
  export const SparkleParamsSchema = z.object({
861
957
  color: ColorSchema.optional().default('#fbbf24').describe('Sparkle color'),
@@ -923,7 +1019,7 @@ export const CreateItemInputSchema = z.object({
923
1019
  itemType: ItemTypeSchema,
924
1020
  position: PositionSchema.optional().default({ x: 400, y: 300 }),
925
1021
  properties: z.record(z.unknown()).optional().default({}),
926
- animationType: z.string().optional().describe('Inline animation to apply on creation. Loop presets (pulse, rotate, bounce, fade, wobble, slide, typewriter) or "keyframe" with keyframes array. Equivalent to a follow-up pinepaper_animate / pinepaper_keyframe_animate call.'),
1022
+ animationType: z.string().optional().describe('Inline animation to apply on creation. Loop presets (bounce, breathe, fade, glow, jelly, path, pulse, rotate, scrollDown, scrollLeft, scrollRight, scrollUp, shake, slideLeftRight, slideUpDown, swing, typewriter, wobble) or "keyframe" with keyframes array. Equivalent to a follow-up pinepaper_animate / pinepaper_keyframe_animate call.'),
927
1023
  animationSpeed: z.number().optional().describe('Speed multiplier for loop animations (default: 1.0). Ignored when animationType is "keyframe".'),
928
1024
  animationIntensity: z.number().optional().describe('Loop animation amplitude (0.1 = ±10%, default 0.15). Drives pulse/wobble/bounce/breathe amplitude; honored by SVG/SMIL/widget export.'),
929
1025
  animationDelay: z.number().optional().describe('Loop animation start delay in seconds.'),
@@ -1007,6 +1103,28 @@ export const KeyframeAnimateInputSchema = z.object({
1007
1103
  clipInPoint: z.number().optional().describe('Skip the first N seconds of keyframe data — clip head trim.'),
1008
1104
  clipOutPoint: z.number().optional().describe('Stop at N seconds into the keyframe data — clip tail trim. Default: lastKeyframeTime.'),
1009
1105
  });
1106
+ // Motion Capture (BVH)
1107
+ //
1108
+ // FxTool exposes two distinct operations and the difference is the whole point:
1109
+ // importBVH builds a NEW skeleton shaped like the capture file
1110
+ // retargetBVH drives an EXISTING rig — proportions stay yours, only MOTION comes from the clip
1111
+ // Angles transfer as bind-pose deltas (target.rest + (source(t) - source(rest))), so a T-posed CMU
1112
+ // rest is not slammed onto a rig with a relaxed stance. Bone matching carries the BVH child-joint
1113
+ // shift ("LeftLeg" is the thigh); anything unmatched is REPORTED, never silently half-driven.
1114
+ export const ImportMotionCaptureInputSchema = z.object({
1115
+ bvh: z.string().describe('BVH clip text (CMU / Mixamo export). Required.'),
1116
+ mode: z.enum(['import', 'retarget']).optional().default('import')
1117
+ .describe("'import' builds a new skeleton from the file; 'retarget' drives an existing rig named by skeletonId — use retarget when the character already exists, so its proportions are preserved and only the motion is taken from the capture."),
1118
+ skeletonId: z.string().optional()
1119
+ .describe("Rig to drive. REQUIRED when mode='retarget'; ignored for 'import'."),
1120
+ fps: z.number().optional().describe('Pose sampling rate. CMU records at 120fps and nobody wants 120 poses a second; 0 keeps every frame. Default 15.'),
1121
+ height: z.number().optional().describe('Skeleton height on the canvas in px (import mode). Default 320.'),
1122
+ position: z.object({ x: z.number(), y: z.number() }).optional()
1123
+ .describe('Where the ROOT joint stands (import mode). Defaults to canvas centre, 70% down.'),
1124
+ name: z.string().optional().describe('Name for the created skeleton (import mode).'),
1125
+ boneMap: z.record(z.string(), z.string()).optional()
1126
+ .describe('BVH edge name → this rig\'s bone name. Overrides the alias table when a clip uses non-standard joint names; check unmatchedSource/unmatchedTarget in the result to see what needs mapping.'),
1127
+ });
1010
1128
  // Execute Generator
1011
1129
  //
1012
1130
  // Region (added FxTool c81781c): when set, the generator draws into a clipped
@@ -1042,7 +1160,13 @@ export const GetItemsInputSchema = z.object({
1042
1160
  });
1043
1161
  // Play Timeline
1044
1162
  export const PlayTimelineInputSchema = z.object({
1045
- action: z.enum(['play', 'pause', 'stop', 'seek']),
1163
+ action: z.enum([
1164
+ 'play', 'pause', 'stop', 'seek',
1165
+ // pp:TimeScale — the rate the scene clock runs at.
1166
+ 'set_time_scale', 'get_time_scale',
1167
+ // pp:InputDrivenPlayback — progress driven by an input instead of a clock.
1168
+ 'get_progress', 'set_progress', 'bind_scroll', 'unbind_scroll', 'list_scrub_anchors',
1169
+ ]),
1046
1170
  duration: z.number().optional().describe('Duration for play action'),
1047
1171
  loop: z.boolean().optional(),
1048
1172
  time: z.number().optional().describe('Time to seek to'),
@@ -1050,6 +1174,22 @@ export const PlayTimelineInputSchema = z.object({
1050
1174
  .boolean()
1051
1175
  .optional()
1052
1176
  .describe('For seek: evaluate the scene at the exact time via app.sceneAt(t) — ticks keyframes + relations + generators deterministically (same t → same frame), not just the keyframe state. Use before a screenshot for a reproducible frame.'),
1177
+ // Deliberately unclamped: 0 freezes the clock without stopping playback, and a
1178
+ // negative rate runs the scene backwards. Both are things the engine supports
1179
+ // and a min(0) would quietly forbid.
1180
+ rate: z.number().optional()
1181
+ .describe('set_time_scale: 1 is real time, 0.5 half speed, 0 freezes without stopping, negative runs backwards. Export is unaffected — a scene watched at 0.5x still exports its real duration.'),
1182
+ progress: z.number().min(0).max(1).optional()
1183
+ .describe('set_progress: how far through the timeline, 0..1 — the unit a scrubber or a progress bar actually has.'),
1184
+ scroll: z.object({
1185
+ elementId: z.string().optional().describe('DOM id of the element to track (defaults to the canvas)'),
1186
+ start: z.string().optional().describe("Anchor where the range begins, e.g. 'top bottom' (the moment the element appears). See list_scrub_anchors."),
1187
+ end: z.string().optional().describe("Anchor where the range ends, e.g. 'bottom top' (the moment it is gone)."),
1188
+ scrub: z.number().min(0).optional()
1189
+ .describe('Seconds of smoothing — the played position chases the scroll instead of snapping to it (default 0.3). 0 snaps.'),
1190
+ range: z.array(z.number()).length(2).optional()
1191
+ .describe('[from, to] in seconds — drive only a sub-range of the timeline instead of the whole thing.'),
1192
+ }).optional().describe('bind_scroll options'),
1053
1193
  });
1054
1194
  // Canvas Control
1055
1195
  export const SetBackgroundColorInputSchema = z.object({
@@ -1187,7 +1327,7 @@ export const SceneAnimationSchema = z.object({
1187
1327
  /** Reference name of the item to animate */
1188
1328
  target: z.string().describe('Name of the item to animate'),
1189
1329
  /** Type of animation */
1190
- type: z.enum(['pulse', 'rotate', 'bounce', 'fade', 'wobble', 'slide', 'typewriter']),
1330
+ type: z.enum(['bounce', 'breathe', 'fade', 'glow', 'jelly', 'path', 'pulse', 'rotate', 'scrollDown', 'scrollLeft', 'scrollRight', 'scrollUp', 'shake', 'slideLeftRight', 'slideUpDown', 'swing', 'typewriter', 'wobble']),
1191
1331
  /** Animation speed (default: 1.0) */
1192
1332
  speed: z.number().optional(),
1193
1333
  /** Animation parameters */
@@ -1346,6 +1486,7 @@ export const CreateDiagramShapeInputSchema = z.object({
1346
1486
  * Connect items input
1347
1487
  */
1348
1488
  export const ConnectInputSchema = z.object({
1489
+ id: z.string().optional().describe('Stable connector ID. Assign your own and reuse it as `connectorId` in update/remove — the engine otherwise mints a Date.now()-based ID no caller can predict'),
1349
1490
  sourceItemId: z.string().describe('Registry ID of source item'),
1350
1491
  targetItemId: z.string().describe('Registry ID of target item'),
1351
1492
  routing: ConnectorRoutingSchema.optional().default('orthogonal'),
@@ -1363,6 +1504,7 @@ export const ConnectInputSchema = z.object({
1363
1504
  * Connect specific ports input
1364
1505
  */
1365
1506
  export const ConnectPortsInputSchema = z.object({
1507
+ id: z.string().optional().describe('Stable connector ID. Assign your own and reuse it as `connectorId` in update/remove — the engine otherwise mints a Date.now()-based ID no caller can predict'),
1366
1508
  sourceItemId: z.string().describe('Registry ID of source item'),
1367
1509
  sourcePort: PortPositionSchema.describe('Port position on source'),
1368
1510
  targetItemId: z.string().describe('Registry ID of target item'),
@@ -2208,6 +2350,29 @@ export const QueryOntologyInputSchema = z.object({
2208
2350
  category: z.string().optional(),
2209
2351
  includeAbstract: z.boolean().optional().default(false),
2210
2352
  });
2353
+ // Capabilities query schema: discover and match capabilities across text styles, text effects, generators, deforms, relations, etc.
2354
+ export const QueryCapabilitiesInputSchema = z.object({
2355
+ action: z.enum(['list', 'choose', 'coverage', 'find']).default('list')
2356
+ .describe("'list' (all capabilities or filtered by kind) · 'choose' (context/mood-weighted recommendation) · 'coverage' (breakdown of indexable capabilities) · 'find' (lookup by key)"),
2357
+ kind: z.union([
2358
+ z.string(),
2359
+ z.array(z.string()),
2360
+ ]).optional().describe("Filter by capability kind: 'style'|'effect'|'deform'|'entrance'|'animation'|'generator'|'collage'|'palette'|'mask'|'cutout'|'relation'"),
2361
+ mood: z.union([
2362
+ z.string(),
2363
+ z.array(z.string()),
2364
+ ]).optional().describe("For choose: target feeling ('reveal'|'triumphant'|'failure'|'calm'|'technical'|'energetic')"),
2365
+ subject: z.string().optional().describe('For choose: contextual subject keywords / prompt text to score against capability definitions'),
2366
+ avoid: z.union([
2367
+ z.string(),
2368
+ z.array(z.string()),
2369
+ ]).optional().describe('For choose: mood terms or feelings to penalize'),
2370
+ seed: z.string().optional().describe('For choose: deterministic seed string for reproducible selection'),
2371
+ key: z.string().optional().describe('For find: exact capability key to look up'),
2372
+ exclude: z.array(z.string()).optional().describe('For choose: list of capability keys to exclude from recommendation'),
2373
+ warm: z.boolean().optional()
2374
+ .describe('Load the lazy registries before answering (default true). The generator registry and the rigging/blending/deform relation rules only exist once touched, so a cold answer omits them — pass false only to re-read what is already resident, cheaply.'),
2375
+ });
2211
2376
  // =============================================================================
2212
2377
  // SCENE MANAGEMENT SCHEMAS
2213
2378
  // =============================================================================
@@ -2232,6 +2397,175 @@ export const ScenePlaybackInputSchema = z.object({
2232
2397
  index: z.number().optional(),
2233
2398
  });
2234
2399
  // =============================================================================
2400
+ // RELATIVE TIMING, STAGGERS, FLIP (GSAP's vocabulary, PinePaper's engine)
2401
+ // =============================================================================
2402
+ // pp:TimelinePosition — saying WHEN relative to something else.
2403
+ //
2404
+ // The position spec is a number or one of the string forms; it stays loose on
2405
+ // purpose, because the grammar is open ("intro+=0.5" names a caller's label) and
2406
+ // an enum here could only ever be a stale subset of it. list_forms is the
2407
+ // discoverable list.
2408
+ export const SequenceInputSchema = z.object({
2409
+ action: z.enum(['place', 'resolve', 'list_forms']).default('place')
2410
+ .describe("'place' threads a run of clips, each resolved against the ones before it · 'resolve' answers a single position · 'list_forms' returns the accepted grammar with what each form means"),
2411
+ clips: z.array(z.object({
2412
+ id: z.string().optional().describe('Your own name for the clip — echoed back on the placement'),
2413
+ duration: z.number().min(0).describe('How long the clip runs, in seconds'),
2414
+ position: z.union([z.number(), z.string()]).optional()
2415
+ .describe('Where it starts. Omit to append at the end of the timeline so far.'),
2416
+ label: z.string().optional().describe('Name this moment so later clips can position against it. A label marks where the clip STARTS.'),
2417
+ })).optional().describe('place: the run of clips, in order'),
2418
+ position: z.union([z.number(), z.string()]).optional().describe('resolve: the single position spec to resolve'),
2419
+ context: z.object({
2420
+ timelineEnd: z.number().optional().describe('Where the timeline currently ends — what a bare "+=1" counts from'),
2421
+ prevStart: z.number().optional().describe('Start of the most recently added clip — what "<" means'),
2422
+ prevEnd: z.number().optional().describe('End of the most recently added clip — what ">" means'),
2423
+ insertDuration: z.number().optional().describe('Duration of the clip being placed — the basis for a "+=25%" / "-=25%" percentage'),
2424
+ }).optional().describe('resolve: what the spec is measured against'),
2425
+ labels: z.record(z.string(), z.number()).optional().describe('Named moments in seconds, addressable as "name" or "name+=0.5"'),
2426
+ startAt: z.number().optional().describe('place: where the run begins (default 0)'),
2427
+ });
2428
+ // pp:Stagger — the shape of a delay across many items.
2429
+ const StaggerOptsSchema = z.object({
2430
+ // `each` and `amount` are ALTERNATIVES, not a pair: each fixes the gap between
2431
+ // neighbours so more items make a longer sequence; amount fixes the total so
2432
+ // more items crowd together.
2433
+ each: z.number().min(0).optional().describe('Seconds between neighbours (default 0.1). More items ⇒ a longer sequence.'),
2434
+ amount: z.number().min(0).optional().describe('Total seconds for the WHOLE run, split among the items. Overrides each — more items ⇒ they crowd together.'),
2435
+ from: z.union([
2436
+ z.enum(['start', 'end', 'center', 'edges', 'random']),
2437
+ z.number(),
2438
+ z.array(z.number()).length(2),
2439
+ ]).optional().describe("Where the stagger begins: a named origin, an item index, or [fx, fy] fractions for a grid ([0.5, 0.5] is the centre)."),
2440
+ grid: z.array(z.number()).length(2).optional().describe('[rows, columns] for a 2D stagger. Items are read row-major.'),
2441
+ axis: z.enum(['x', 'y']).optional().describe('Restrict a grid stagger to one axis.'),
2442
+ ease: z.enum(['linear', 'easeIn', 'easeOut', 'easeInOut', 'easeInCubic', 'easeOutCubic']).optional()
2443
+ .describe("Curve that distributes the START TIMES — not the items' own animation curve."),
2444
+ seed: z.number().optional().describe("from: 'random' — same seed, same shuffle (default 1)."),
2445
+ });
2446
+ export const StaggerInputSchema = z.object({
2447
+ action: z.enum(['apply', 'preview', 'list_origins']).default('apply')
2448
+ .describe("'apply' writes the delays onto real items · 'preview' returns the delays for a count without touching the canvas · 'list_origins' names the origins"),
2449
+ itemIds: z.array(z.string()).optional().describe('apply: registry ids IN THE ORDER the stagger should follow (row-major for a grid)'),
2450
+ count: z.number().int().min(0).optional().describe('preview: how many items to compute delays for'),
2451
+ opts: StaggerOptsSchema.optional().describe('Shape of the stagger'),
2452
+ });
2453
+ // pp:Flip — a transition DERIVED from a change rather than described.
2454
+ export const FlipInputSchema = z.object({
2455
+ action: z.enum(['record', 'apply']).default('record')
2456
+ .describe("'record' captures where things are NOW · 'apply' animates from that record to wherever they have since been moved"),
2457
+ itemIds: z.array(z.string()).optional()
2458
+ .describe('record: which items to capture (defaults to everything on the item layer, which is the usual case for a re-layout)'),
2459
+ duration: z.number().min(0).optional().describe('apply: seconds the transition runs (engine default)'),
2460
+ easing: z.string().optional().describe('apply: named easing for the generated keyframes'),
2461
+ enter: z.string().optional().describe('apply: entrance preset for items that appeared during the change, so they animate in rather than popping'),
2462
+ stagger: StaggerOptsSchema.optional().describe("apply: stagger the transition across the items, in the items' own order"),
2463
+ });
2464
+ // Scene graph: interactive multi-node story/quiz state graphs (cards, answers, mutex, event wiring)
2465
+ //
2466
+ // The node and opts fields below mirror what the engine's compiler actually
2467
+ // reads (js/core/SceneGraph.js). zod strips unknown keys, so a field missing
2468
+ // here is not "passed through" — it is silently dropped on the way to an engine
2469
+ // that would have honoured it. `next`, `duration`, `outcome` and `subject` were
2470
+ // each unreachable for that reason.
2471
+ const SceneGraphNodeSchema = z.object({
2472
+ id: z.string().describe('Unique node id (e.g., "q1", "win", "lose")'),
2473
+ prompt: z.string().optional().describe('Prompt or headline text for question cards'),
2474
+ text: z.string().optional().describe('Text for message/end cards'),
2475
+ kind: z.enum(['node', 'card', 'end']).optional()
2476
+ .describe("'node'/'card' (question or content) or 'end' (terminal/outcome). Anything that is not 'end' is a card."),
2477
+ answers: z.array(z.object({
2478
+ text: z.string().describe('Answer button text'),
2479
+ correct: z.boolean().optional().describe('Whether this is the correct answer (increments score when scoreItemId given)'),
2480
+ to: z.string().min(1).describe('Destination node id when clicked'),
2481
+ })).optional().describe('Interactive answer choices (for question cards)'),
2482
+ next: z.string().min(1).optional()
2483
+ .describe('Unconditional destination — a linear story beat that auto-advances instead of waiting for a click. Use INSTEAD of answers, not alongside a lone fake button.'),
2484
+ duration: z.number().positive().optional()
2485
+ .describe('Seconds this card holds before `next` fires (default 3). Ignored without `next`.'),
2486
+ outcome: z.enum(['win', 'lose']).optional()
2487
+ .describe('Force the outcome of a terminal node. Without it the engine infers win/lose from the wording, which only works in English.'),
2488
+ });
2489
+ export const SceneGraphInputSchema = z.object({
2490
+ action: z.enum(['create', 'validate']).default('create')
2491
+ .describe("'create' builds the graph on the canvas · 'validate' runs the same check WITHOUT drawing anything, returning errors, warnings, unreachable nodes and cycles"),
2492
+ graph: z.object({
2493
+ start: z.string().optional().describe('Node id of the initial active card (defaults to the first node)'),
2494
+ nodes: z.array(SceneGraphNodeSchema).min(1).describe('Array of scene graph nodes'),
2495
+ })
2496
+ // Structural integrity, checked HERE because in code mode there is no engine
2497
+ // to catch it: the snippet is handed to the caller and fails silently later.
2498
+ // Duplicate ids in particular are invisible engine-side — `indexNodes` keys a
2499
+ // Map by id, so the second node quietly replaces the first.
2500
+ .superRefine((graph, ctx) => {
2501
+ const seen = new Set();
2502
+ graph.nodes.forEach((node, i) => {
2503
+ if (seen.has(node.id)) {
2504
+ ctx.addIssue({
2505
+ code: z.ZodIssueCode.custom,
2506
+ path: ['nodes', i, 'id'],
2507
+ message: `duplicate node id "${node.id}" — the later node silently replaces the earlier one`,
2508
+ });
2509
+ }
2510
+ seen.add(node.id);
2511
+ });
2512
+ const has = (id) => seen.has(id);
2513
+ if (graph.start !== undefined && !has(graph.start)) {
2514
+ ctx.addIssue({
2515
+ code: z.ZodIssueCode.custom,
2516
+ path: ['start'],
2517
+ message: `start node "${graph.start}" does not exist`,
2518
+ });
2519
+ }
2520
+ graph.nodes.forEach((node, i) => {
2521
+ (node.answers || []).forEach((answer, a) => {
2522
+ if (!has(answer.to)) {
2523
+ ctx.addIssue({
2524
+ code: z.ZodIssueCode.custom,
2525
+ path: ['nodes', i, 'answers', a, 'to'],
2526
+ message: `node "${node.id}" points at "${answer.to}", which does not exist — clicking it would do nothing`,
2527
+ });
2528
+ }
2529
+ });
2530
+ if (node.next !== undefined && !has(node.next)) {
2531
+ ctx.addIssue({
2532
+ code: z.ZodIssueCode.custom,
2533
+ path: ['nodes', i, 'next'],
2534
+ message: `node "${node.id}" advances to "${node.next}", which does not exist`,
2535
+ });
2536
+ }
2537
+ // Mirrors the engine's own refusal: a non-terminal card with no edge is
2538
+ // a dead end the player can never leave.
2539
+ if (node.kind !== 'end' && !node.answers?.length && node.next === undefined) {
2540
+ ctx.addIssue({
2541
+ code: z.ZodIssueCode.custom,
2542
+ path: ['nodes', i],
2543
+ message: `node "${node.id}" is not terminal but has no way out — give it answers, a next, or kind: 'end'`,
2544
+ });
2545
+ }
2546
+ });
2547
+ })
2548
+ .describe('The declarative scene graph definition'),
2549
+ opts: z.object({
2550
+ width: z.number().optional().describe('Layout width (defaults to view width or canvas width)'),
2551
+ height: z.number().optional().describe('Layout height (defaults to view height or canvas height)'),
2552
+ centerX: z.number().optional().describe('Center X position for card stack'),
2553
+ centerY: z.number().optional().describe('Center Y position for card stack'),
2554
+ groupName: z.string().optional().describe('Prefix / seed namespace for generated IDs and effect selection'),
2555
+ scoreItemId: z.string().optional().describe('Optional scoreboard text item id to increment on correct answers'),
2556
+ subject: z.string().optional()
2557
+ .describe("Extra context folded into each card's text-effect selection — the theme of the piece, e.g. 'space exploration'."),
2558
+ plain: z.boolean().optional().describe('Opt out of rich effects/entrances for a bare structural graph'),
2559
+ style: z.object({
2560
+ panelColor: z.string().optional().describe('Background panel fill color (default #1E1B4B)'),
2561
+ textColor: z.string().optional().describe('Headline/prompt text color (default #F8FAFC)'),
2562
+ answerColor: z.string().optional().describe('Answer text color (default #93C5FD)'),
2563
+ headlineSize: z.number().optional().describe('Font size for headline/prompt (default 34)'),
2564
+ bodySize: z.number().optional().describe('Font size for body/answers (default 24)'),
2565
+ }).optional().describe('Visual styling options for generated cards and text'),
2566
+ }).optional().describe('Layout, style, and scoring options'),
2567
+ });
2568
+ // =============================================================================
2235
2569
  // SELECTION, TRANSFORM & HISTORY TOOLS
2236
2570
  // =============================================================================
2237
2571
  export const SelectionInputSchema = z.object({
@@ -2364,6 +2698,24 @@ export const ComposeInputSchema = z.object({
2364
2698
  audio: z.string().optional(),
2365
2699
  /** With audio: snap to an even tempo grid instead of raw onsets. */
2366
2700
  grid: z.boolean().optional(),
2701
+ /**
2702
+ * WHICH DESIGN LANGUAGE. Resolves the craft — gutter, margin, hue budget,
2703
+ * type scale, alignment discipline — so those move together instead of being
2704
+ * chosen one at a time. A default, not a handcuff: an explicit `craft` still
2705
+ * wins. naive | playful | poster | editorial | technical.
2706
+ */
2707
+ register: z.string().optional().describe('Design register — naive, playful, poster, editorial, technical. Sets the craft; an explicit `craft` still overrides it.'),
2708
+ level: z.number().int().min(1).max(4).optional().describe('Craft level 1-4 WITHIN the register (sketch, competent, refined, art-directed). Climbing never widens a budget — restraint is the direction of craft.'),
2709
+ /**
2710
+ * WHAT MAKES THE MARKS. Only media a whole composition can be rendered in
2711
+ * are accepted: `vector` (what composing already produces) and `thread`
2712
+ * (needlepainting, stitched after the arrangement). The rest are refused
2713
+ * with the medium's own reason rather than composed as flat shapes in its
2714
+ * colours.
2715
+ */
2716
+ medium: z.enum(['vector', 'thread']).optional().describe("Render the composition in a medium. 'thread' stitches every closed path; photographs and text CANNOT be stitched and come back in `medium.skipped` with the reason, so read that count — a composition can render completely and be entirely unstitched."),
2717
+ stitch: z.enum(['longAndShort', 'satin', 'seed', 'stem']).optional().describe('Which stitch, when medium is thread. Default longAndShort.'),
2718
+ stitchBudget: z.number().int().min(200).max(20000).optional().describe('Total marks across the composition (default 6000). The stitch is scaled to fit rather than the fill being truncated — a half-stitched shape looks broken, coarser thread does not.'),
2367
2719
  });
2368
2720
  export const AudioBeatsInputSchema = z.object({
2369
2721
  action: z.enum(['analyze', 'animate_to_beat']),
@@ -2760,6 +3112,93 @@ export const TextStyleInputSchema = z.object({
2760
3112
  })
2761
3113
  .refine((v) => v.action !== 'apply_style' || (!!v.itemId && !!v.styleKey), { message: 'apply_style requires itemId and styleKey', path: ['styleKey'] })
2762
3114
  .refine((v) => v.action !== 'set_font_axes' || (!!v.itemId && !!v.axes && Object.keys(v.axes).length > 0), { message: 'set_font_axes requires itemId and at least one axis', path: ['axes'] });
3115
+ /**
3116
+ * pinepaper_text_effect — the 37 character-level text effects
3117
+ * (terminaltexteffects' vocabulary, reimplemented in FxTool from source).
3118
+ *
3119
+ * The contract that separates this from `pinepaper_text_style`, and the one
3120
+ * thing a caller must know: this tool EXPLODES the text item into one item per
3121
+ * character and REMOVES the original. `text_style` ADOPTS the text's registry
3122
+ * id; this DESTROYS it. Relations, keyframes and agent handles pointing at the
3123
+ * source id die with it — `keepSource: true` leaves the original in place if
3124
+ * they matter.
3125
+ *
3126
+ * `effect` is a free string, not an enum: the engine owns the vocabulary, its
3127
+ * own error names every valid key, and `action: 'list'` is authoritative. An
3128
+ * enum here would pin the server to today's 37 and go stale on the 38th.
3129
+ *
3130
+ * The planner is pure and emits KEYFRAMES, so the result is ordinary animated
3131
+ * items — it scrubs, survives undo/session restore, and exports through the
3132
+ * existing MP4/SMIL/Lottie paths rather than needing a per-exporter animator.
3133
+ */
3134
+ /**
3135
+ * The MEDIUM axis — what makes the marks, as distinct from which design
3136
+ * language (register) and how well executed (level).
3137
+ *
3138
+ * Every medium declares a fidelity, and the tool refuses an `absent` one with
3139
+ * the reason rather than producing flat shapes in its colours. Ask
3140
+ * `list_media` before promising a medium in prose.
3141
+ */
3142
+ export const DesignMediumInputSchema = z.object({
3143
+ action: z.enum(['list_media', 'resolve', 'list_stitches', 'apply_thread', 'apply_hatch', 'list_flow_fields', 'list_hatch_options'])
3144
+ .describe("'list_media' (7 media with fidelity + limitation) · 'resolve' (can this medium be made here, and how honestly) · 'list_stitches' · 'apply_thread' (render an item in thread) · 'apply_hatch' (rule an item with hatching — value through line density) · 'list_flow_fields' · 'list_hatch_options'"),
3145
+ medium: z.string().optional().describe("resolve: medium key — vector, thread, hatch, watercolor, ink, cutPaper, charcoal, oil, encaustic. Call list_media for the live set with each one's fidelity; the catalogue grows."),
3146
+ itemId: z.string().optional().describe('apply_thread / apply_hatch: a closed path, compound path, or a group of them. Its own silhouette is the region and its fill is the ink colour.'),
3147
+ stitch: z.enum(['longAndShort', 'satin', 'seed', 'stem']).optional()
3148
+ .describe("apply_thread: default longAndShort (the needlepainting fill). 'satin' spans the shape edge to edge — right for a narrow shape, wrong for a round one. 'seed' is texture. 'stem' is an outline mark and leaves the interior bare."),
3149
+ field: z.object({
3150
+ kind: z.enum(['radial', 'spine', 'constant']),
3151
+ cx: z.number().optional(), cy: z.number().optional(),
3152
+ angle: z.number().optional(),
3153
+ spine: z.array(PositionSchema).optional(),
3154
+ across: z.boolean().optional(),
3155
+ }).optional().describe("apply_thread: the direction field — the thing that makes it needlepainting rather than hatching. Default radial from the shape centre. 'spine' runs stitches along a midrib or feather shaft; 'constant' is flat hatch."),
3156
+ stitchLen: z.number().positive().optional().describe('apply_thread: nominal stitch length px (default 18).'),
3157
+ rowGap: z.number().positive().optional().describe('apply_thread: row spacing px. Defaults from the thread width so the rows abut; setting it wider is what makes a fill read as hatching.'),
3158
+ variance: z.number().min(0).max(1).optional().describe('apply_thread: length jitter 0..1 (default 0.35) — the variance IS the long-and-short shading.'),
3159
+ width: z.number().positive().optional().describe('apply_thread: thread width px (default 2).'),
3160
+ sheen: z.boolean().optional().describe('apply_thread: draw the highlight along each stitch (default true) — thread is not matte.'),
3161
+ color: z.string().optional().describe("apply_thread: thread colour, defaulting to the item's own fill."),
3162
+ seed: z.number().int().optional().describe('apply_thread: PRNG seed (default 1). Same seed stitches the same way.'),
3163
+ count: z.number().int().positive().optional().describe('apply_thread: stitch count for seed/satin.'),
3164
+ // --- Hatching (pp: the p5.brush marks, as vector) ---
3165
+ //
3166
+ // `field` above is the THREAD direction field (radial/spine/constant) and is
3167
+ // a different thing from `flowField` here (hand/curved/waves/…). Separate
3168
+ // names because one steers stitches around a form and the other bends a
3169
+ // ruling line the way a hand would drift; sharing a key would silently apply
3170
+ // one where the other was meant.
3171
+ distance: z.number().positive().optional()
3172
+ .describe('apply_hatch: spacing between lines in px (default 6). THIS IS THE VALUE CONTROL — the same shape at 6px and 3px reads as light and dark with no change of colour.'),
3173
+ angle: z.number().optional().describe('apply_hatch: ruling angle in DEGREES (default 45).'),
3174
+ gradient: z.number().min(0).max(1).optional()
3175
+ .describe('apply_hatch: 0..1 spacing growth per line (default 0, even). Non-zero makes the density fall off across the shape — a shaded ramp rather than a flat tone.'),
3176
+ rand: z.number().min(0).max(1).optional()
3177
+ .describe('apply_hatch: jitter as a fraction of distance (default 0) — takes the mechanical evenness off.'),
3178
+ continuous: z.boolean().optional()
3179
+ .describe('apply_hatch: join the lines into ONE serpentine path (default false) — a pen that never leaves the paper, and a single item instead of many.'),
3180
+ flowField: z.enum(['hand', 'curved', 'zigzag', 'waves', 'seabed', 'spiral', 'columns']).optional()
3181
+ .describe('apply_hatch: bend each line along a flow field. The straight scanline is the ruling; the field is what makes it read as drawn rather than printed. See list_flow_fields.'),
3182
+ t: z.number().optional().describe('apply_hatch: flow-field time offset — advances the field without changing the seed.'),
3183
+ })
3184
+ .refine((v) => v.action !== 'apply_thread' || !!v.itemId, { message: 'apply_thread requires itemId', path: ['itemId'] })
3185
+ .refine((v) => v.action !== 'resolve' || !!v.medium, { message: 'resolve requires medium', path: ['medium'] })
3186
+ .refine((v) => v.action !== 'apply_hatch' || !!v.itemId, { message: 'apply_hatch requires itemId', path: ['itemId'] });
3187
+ export const TextEffectInputSchema = z.object({
3188
+ action: z.enum(['apply', 'list'])
3189
+ .describe("'apply' (explode a text item into animated characters) · 'list' (the 37 effects with key/label/definition, for a picker)"),
3190
+ itemId: z.string().optional().describe('apply: the text item to animate. Must be a text item with non-whitespace content.'),
3191
+ effect: z.string().optional().describe("apply: effect key from 'list' (e.g. scattered, decrypt, matrix, burn, fireworks, rain, blackhole)."),
3192
+ duration: z.number().positive().optional().describe('apply: seconds for the whole effect (default 2.5). Every effect ends AT REST, so this is the settle time.'),
3193
+ seed: z.number().int().optional().describe('apply: PRNG seed (default 1). Same seed + same text = identical animation, so exports are reproducible.'),
3194
+ gradient: z.boolean().optional().describe("apply: false keeps each character's authored fill. Default TRUE — the upstream effects all end by painting the block with a gradient, which is what their reference frames show."),
3195
+ gradientStops: z.array(z.string()).optional().describe('apply: hex colors overriding the effect\'s own gradient.'),
3196
+ gradientDirection: z.enum(['vertical', 'horizontal', 'radial', 'diagonal']).optional().describe('apply: gradient axis across the text block.'),
3197
+ gradientSteps: z.number().int().positive().optional().describe('apply: quantization steps in the gradient ramp.'),
3198
+ keepSource: z.boolean().optional().describe('apply: leave the original text item in place instead of removing it — the escape hatch when relations/keyframes reference its id.'),
3199
+ options: z.record(z.string(), z.unknown()).optional().describe('apply: per-effect knobs, passed through verbatim (each of the 37 has its own). Spread FIRST, so named fields above win on collision.'),
3200
+ })
3201
+ .refine((v) => v.action !== 'apply' || (!!v.itemId && !!v.effect), { message: 'apply requires itemId and effect', path: ['effect'] });
2763
3202
  /**
2764
3203
  * pinepaper_shatter_image — split a raster into a grid of per-tile rasters.
2765
3204
  * Deliberately INERT on its own: the pieces sit exactly where the picture was
@@ -2852,7 +3291,7 @@ export const World3DInputSchema = z.object({
2852
3291
  live: z.boolean().optional().describe('add_actor: re-rasterize the item as it animates — a rigged character PERFORMS in the world instead of standing there as a photograph of itself.'),
2853
3292
  pose: z.record(z.string(), z.unknown()).optional().describe('set_actor_pose: { x?, z?, angle?, … } — the setter a timeline or agent drives.'),
2854
3293
  camera: z.record(z.string(), z.unknown()).optional().describe("set_camera: { mode: 'follow'|'fixed'|'orbit', target?, radius?, speed?, eye?, lookAt? }."),
2855
- object: z.record(z.string(), z.unknown()).optional().describe('add_object: { x, z, height?, color?, y? (defaults to sitting on the terrain) }.'),
3294
+ object: z.record(z.string(), z.unknown()).optional().describe('add_object: { x, z, height?, color?, y?, metalness?, roughness?, emissiveIntensity? } (defaults to sitting on the terrain).'),
2856
3295
  objectId: z.string().optional().describe('remove_object: the object id.'),
2857
3296
  })
2858
3297
  .refine((v) => v.action !== 'configure' || !!v.patch, { message: 'configure requires patch', path: ['patch'] })
@@ -2892,7 +3331,71 @@ export const RiggingInputSchema = z.object({
2892
3331
  'create_skeleton', 'add_bone', 'attach_item', 'create_ik_chain',
2893
3332
  'add_pose_keyframe', 'set_target_path', 'save_pose', 'save_shape_key',
2894
3333
  'import_bvh', 'retarget_bvh', 'import_spine',
3334
+ // ── Pose motion. The engine has had these for releases; nothing could
3335
+ // reach them, which is not the same as their not existing.
3336
+ 'list_skeletons', 'list_bones', 'list_poses', 'load_pose', 'interpolate_poses',
3337
+ 'list_pose_libraries', 'load_pose_library',
3338
+ 'play_pose_sequence', 'stop_pose_sequence', 'stitch_poses',
3339
+ 'apply_pose_transition',
3340
+ 'auto_walk', 'auto_breath', 'auto_idle', 'auto_jump',
3341
+ 'move_root', 'stop_root_track',
3342
+ 'add_secondary_motion', 'skin_path', 'bake_animation',
3343
+ 'list_shape_keys', 'load_shape_key',
2895
3344
  ]).describe('Rigging operation'),
3345
+ // ── Pose motion ────────────────────────────────────────────────────────
3346
+ poseId: z.string().optional().describe('Saved pose id — load_pose.'),
3347
+ poseIdA: z.string().optional().describe('First pose — interpolate_poses.'),
3348
+ poseIdB: z.string().optional().describe('Second pose — interpolate_poses.'),
3349
+ t: z.number().min(0).max(1).optional().describe('Blend 0=A … 1=B — interpolate_poses.'),
3350
+ /**
3351
+ * A pose sequence: keys over normalized time, each naming a SAVED pose or an
3352
+ * inline { boneId: angleDeg } map. The saved-pose form is the point — an
3353
+ * animation that is literally a graph over the poses in the Poses panel.
3354
+ */
3355
+ sequence: z.array(z.object({
3356
+ t: z.number().describe('Time (normalized within the clip unless duration is given).'),
3357
+ pose: z.string().optional().describe('Saved pose id.'),
3358
+ angles: z.record(z.string(), z.number()).optional().describe('Inline { boneId: angleDeg }.'),
3359
+ ease: z.string().optional(),
3360
+ })).optional().describe('Pose keys — play_pose_sequence.'),
3361
+ /**
3362
+ * Clips to STITCH into one continuous performance. Each clip is a sequence
3363
+ * plus how it joins: `repeat` plays it N times, `loop` marks it cyclic (only
3364
+ * a cyclic clip can be phase-matched), `blend` is the seam overlap.
3365
+ */
3366
+ clips: z.array(z.object({
3367
+ keys: z.array(z.object({
3368
+ t: z.number().optional(),
3369
+ pose: z.string().optional(),
3370
+ angles: z.record(z.string(), z.number()).optional(),
3371
+ ease: z.string().optional(),
3372
+ })).optional(),
3373
+ poses: z.array(z.object({
3374
+ pose: z.string().optional(),
3375
+ angles: z.record(z.string(), z.number()).optional(),
3376
+ })).optional().describe('Bare list — spread evenly across the clip.'),
3377
+ duration: z.number().positive().optional(),
3378
+ repeat: z.number().int().min(1).max(200).optional(),
3379
+ loop: z.boolean().optional().describe('Cyclic: can be entered at any phase.'),
3380
+ blend: z.number().min(0).optional().describe('Override the seam overlap into this clip.'),
3381
+ matchPhase: z.boolean().optional(),
3382
+ })).optional().describe('Clips in order — stitch_poses.'),
3383
+ blend: z.number().min(0).optional().describe('Seam overlap in seconds (default 0.25) — stitch_poses.'),
3384
+ blendSteps: z.number().int().min(2).max(64).optional().describe('Keys emitted across a seam (default 6) — stitch_poses.'),
3385
+ matchPhase: z.boolean().optional().describe('Enter a cyclic clip at the phase closest to where the last one ended (default true) — stitch_poses.'),
3386
+ plan: z.boolean().optional().describe('Return the seams WITHOUT installing the result — stitch_poses. Read `residual` per seam: it is the mismatch the blend is hiding, in degrees.'),
3387
+ libraryName: z.string().optional().describe("Stock pose set — load_pose_library. The LIBRARIES are rig shapes: humanoid, quadruped, bird, fish. Loading one saves its poses (idle, walk_00…walk_07, walk_contact_L, run_*, …) against your bones. The walk/run/flap SEQUENCES are transitions, not libraries — play those with apply_pose_transition once the library is loaded. list_pose_libraries returns both lists."),
3388
+ boneMap: z.record(z.string(), z.string()).optional().describe('Bone NAME → bone id — load_pose_library. Omit and it is built from list_bones, which is what you want unless the rig uses non-standard names.'),
3389
+ transitionName: z.string().optional().describe('Named pose transition — apply_pose_transition.'),
3390
+ poseIdMap: z.record(z.string(), z.string()).optional().describe('Transition pose name → saved pose id — apply_pose_transition.'),
3391
+ boneNames: z.array(z.string()).optional().describe('Ordered bone names for a spring chain — add_secondary_motion.'),
3392
+ keyframes: z.array(z.object({
3393
+ t: z.number().describe('SCENE SECONDS, not a 0..1 fraction.'),
3394
+ x: z.number(), y: z.number(), ease: z.string().optional(),
3395
+ })).optional().describe('World root positions over SCENE TIME — move_root. This is what makes a figure travel instead of walking on the spot. `t` is in seconds on the same clock as the pose sequence, so match the last keyframe to the performance duration or the figure arrives early and stands there. Pass options.loop with options.period to cycle it.'),
3396
+ shapeKeyId: z.string().optional().describe('Saved shape key id — load_shape_key.'),
3397
+ weight: z.number().optional().describe('Shape key weight 0..1 (default 1) — load_shape_key.'),
3398
+ options: z.record(z.string(), z.unknown()).optional().describe('Pass-through options for auto_* / bake_animation / move_root / skin_path / play_pose_sequence.'),
2896
3399
  // ── Mocap / rig import (import_bvh, retarget_bvh, import_spine) ──
2897
3400
  // The file CONTENTS travel in the call (MCP has no filesystem): .bvh text for
2898
3401
  // BVH, the exported .json for Spine. import_bvh builds a NEW stick-figure rig