@pinepaper.studio/mcp-server 1.6.17 → 1.6.19

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 (125) hide show
  1. package/README.de.md +1 -1
  2. package/README.es.md +1 -1
  3. package/README.fr.md +1 -1
  4. package/README.hi.md +1 -1
  5. package/README.ja.md +1 -1
  6. package/README.ko.md +1 -1
  7. package/README.md +189 -10
  8. package/README.pt-BR.md +1 -1
  9. package/README.zh-CN.md +1 -1
  10. package/dist/agent/export-manager.js +2 -2
  11. package/dist/agent/session-manager.d.ts +8 -0
  12. package/dist/agent/session-manager.d.ts.map +1 -1
  13. package/dist/agent/session-manager.js +17 -2
  14. package/dist/agent/session-manager.js.map +1 -1
  15. package/dist/assets/asset-manager.d.ts +28 -1
  16. package/dist/assets/asset-manager.d.ts.map +1 -1
  17. package/dist/assets/asset-manager.js +65 -25
  18. package/dist/assets/asset-manager.js.map +1 -1
  19. package/dist/assets/index.d.ts +1 -3
  20. package/dist/assets/index.d.ts.map +1 -1
  21. package/dist/assets/index.js +1 -3
  22. package/dist/assets/index.js.map +1 -1
  23. package/dist/assets/repositories/iconify.js +1 -1
  24. package/dist/assets/repositories/iconify.js.map +1 -1
  25. package/dist/assets/repositories/openclipart.js +1 -1
  26. package/dist/assets/repositories/openclipart.js.map +1 -1
  27. package/dist/assets/repositories/svgrepo.js +2 -2
  28. package/dist/assets/repositories/svgrepo.js.map +1 -1
  29. package/dist/assets/types.d.ts +2 -2
  30. package/dist/assets/types.d.ts.map +1 -1
  31. package/dist/browser/puppeteer-controller.d.ts +86 -1
  32. package/dist/browser/puppeteer-controller.d.ts.map +1 -1
  33. package/dist/browser/puppeteer-controller.js +274 -6
  34. package/dist/browser/puppeteer-controller.js.map +1 -1
  35. package/dist/build-info.json +2 -2
  36. package/dist/cli.js +4 -2
  37. package/dist/cli.js.map +1 -1
  38. package/dist/design/design-systems.d.ts +2 -0
  39. package/dist/design/design-systems.d.ts.map +1 -1
  40. package/dist/design/design-systems.js +2 -0
  41. package/dist/design/design-systems.js.map +1 -1
  42. package/dist/execution/canvas-state.d.ts.map +1 -1
  43. package/dist/execution/canvas-state.js +19 -43
  44. package/dist/execution/canvas-state.js.map +1 -1
  45. package/dist/index.d.ts +2 -0
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +94 -16
  48. package/dist/index.js.map +1 -1
  49. package/dist/ontology/design-graph.js +2 -2
  50. package/dist/ontology/design-graph.js.map +1 -1
  51. package/dist/ontology/ontology-validator.d.ts.map +1 -1
  52. package/dist/ontology/ontology-validator.js +9 -5
  53. package/dist/ontology/ontology-validator.js.map +1 -1
  54. package/dist/ontology/upstream-vocabulary.d.ts +2 -2
  55. package/dist/ontology/upstream-vocabulary.js +2 -2
  56. package/dist/ontology/validator.js +1 -1
  57. package/dist/ontology/validator.js.map +1 -1
  58. package/dist/tools/compact-descriptions.d.ts.map +1 -1
  59. package/dist/tools/compact-descriptions.js +15 -14
  60. package/dist/tools/compact-descriptions.js.map +1 -1
  61. package/dist/tools/definitions.d.ts +1 -1
  62. package/dist/tools/definitions.d.ts.map +1 -1
  63. package/dist/tools/definitions.js +556 -157
  64. package/dist/tools/definitions.js.map +1 -1
  65. package/dist/tools/engine-lists.d.ts +35 -0
  66. package/dist/tools/engine-lists.d.ts.map +1 -0
  67. package/dist/tools/engine-lists.js +186 -0
  68. package/dist/tools/engine-lists.js.map +1 -0
  69. package/dist/tools/engine-methods.d.ts.map +1 -1
  70. package/dist/tools/engine-methods.js +18 -1
  71. package/dist/tools/engine-methods.js.map +1 -1
  72. package/dist/tools/engine-surface.d.ts +1 -1
  73. package/dist/tools/engine-surface.d.ts.map +1 -1
  74. package/dist/tools/engine-surface.js +66 -9
  75. package/dist/tools/engine-surface.js.map +1 -1
  76. package/dist/tools/handlers/brand-intake.d.ts +95 -0
  77. package/dist/tools/handlers/brand-intake.d.ts.map +1 -0
  78. package/dist/tools/handlers/brand-intake.js +242 -0
  79. package/dist/tools/handlers/brand-intake.js.map +1 -0
  80. package/dist/tools/handlers/camera.js +8 -8
  81. package/dist/tools/handlers/camera.js.map +1 -1
  82. package/dist/tools/handlers/export.d.ts.map +1 -1
  83. package/dist/tools/handlers/export.js +77 -5
  84. package/dist/tools/handlers/export.js.map +1 -1
  85. package/dist/tools/handlers/font.js +10 -10
  86. package/dist/tools/handlers/font.js.map +1 -1
  87. package/dist/tools/handlers/generate.d.ts +12 -12
  88. package/dist/tools/handlers/maps.d.ts.map +1 -1
  89. package/dist/tools/handlers/maps.js +23 -3
  90. package/dist/tools/handlers/maps.js.map +1 -1
  91. package/dist/tools/handlers.d.ts +44 -11
  92. package/dist/tools/handlers.d.ts.map +1 -1
  93. package/dist/tools/handlers.js +462 -43
  94. package/dist/tools/handlers.js.map +1 -1
  95. package/dist/tools/minimal-descriptions.d.ts.map +1 -1
  96. package/dist/tools/minimal-descriptions.js +36 -29
  97. package/dist/tools/minimal-descriptions.js.map +1 -1
  98. package/dist/tools/shape-params.d.ts +2 -2
  99. package/dist/tools/shape-params.d.ts.map +1 -1
  100. package/dist/tools/shape-params.js +5 -2
  101. package/dist/tools/shape-params.js.map +1 -1
  102. package/dist/tools/stick-vocabulary.d.ts +1 -1
  103. package/dist/tools/stick-vocabulary.js +1 -1
  104. package/dist/tools/toolkits.d.ts.map +1 -1
  105. package/dist/tools/toolkits.js +5 -0
  106. package/dist/tools/toolkits.js.map +1 -1
  107. package/dist/types/code-generator.d.ts +96 -4
  108. package/dist/types/code-generator.d.ts.map +1 -1
  109. package/dist/types/code-generator.js +1265 -277
  110. package/dist/types/code-generator.js.map +1 -1
  111. package/dist/types/schemas.d.ts +4181 -1757
  112. package/dist/types/schemas.d.ts.map +1 -1
  113. package/dist/types/schemas.js +378 -84
  114. package/dist/types/schemas.js.map +1 -1
  115. package/dist/utils/mcp-log.d.ts +16 -0
  116. package/dist/utils/mcp-log.d.ts.map +1 -0
  117. package/dist/utils/mcp-log.js +10 -0
  118. package/dist/utils/mcp-log.js.map +1 -0
  119. package/dist/utils/progress.d.ts +28 -0
  120. package/dist/utils/progress.d.ts.map +1 -0
  121. package/dist/utils/progress.js +24 -0
  122. package/dist/utils/progress.js.map +1 -0
  123. package/dist/vendor/design/design-style.js +1 -1
  124. package/manifest.json +22 -2
  125. package/package.json +7 -4
@@ -8,6 +8,7 @@
8
8
  * 3. TypeScript type inference
9
9
  */
10
10
  import { z } from 'zod';
11
+ import { ENGINE_EASING_NAMES, DESIGN_MEDIA, DESIGN_MEDIA_APPLY, FILTER_TYPES, LINEAGE_KINDS, STYLED_SCENE_STYLES } from '../tools/engine-lists.js';
11
12
  // =============================================================================
12
13
  // COMMON SCHEMAS
13
14
  // =============================================================================
@@ -70,14 +71,77 @@ export const BlendModeSchema = z.enum([
70
71
  'difference',
71
72
  'exclusion',
72
73
  ]).describe('Blend mode for compositing with background');
73
- export const EasingSchema = z.enum([
74
- 'linear',
75
- 'easeIn',
76
- 'easeOut',
77
- 'easeInOut',
78
- 'bounce',
79
- 'elastic',
80
- ]).describe('Easing function for animation');
74
+ /**
75
+ * Every easing name the engine's table accepts: GENERATED from FxTool's
76
+ * EASING_NAMES (scripts/sync-engine-lists.mjs, checked in prepublishOnly).
77
+ * Keyframes, masks, relations and the camera all resolve a name through that
78
+ * one table, so every easing enum here is this list.
79
+ */
80
+ export const KEYFRAME_EASINGS = ENGINE_EASING_NAMES;
81
+ /** One line per easing, for tool descriptions and pinepaper_get_available_easings. */
82
+ export const EASING_DESCRIPTIONS = {
83
+ linear: 'Constant speed',
84
+ easeIn: 'Slow start, fast end',
85
+ easeOut: 'Fast start, slow end',
86
+ easeInOut: 'Slow start and end',
87
+ easeInCubic: 'Slow start, stronger than easeIn',
88
+ easeOutCubic: 'Slow end, stronger than easeOut',
89
+ easeInOutCubic: 'Slow start and end, stronger than easeInOut',
90
+ bounce: 'Bounces to rest at the end (same as easeOutBounce)',
91
+ easeOutBounce: 'Bounces to rest at the end',
92
+ easeInBounce: 'Bounces at the start, then moves',
93
+ elastic: 'Overshoots and oscillates at the end (same as easeOutElastic)',
94
+ easeOutElastic: 'Overshoots and oscillates at the end',
95
+ easeInElastic: 'Oscillates at the start, then moves',
96
+ easeOutBack: 'Overshoots the target slightly, then settles',
97
+ easeInBack: 'Pulls back first, then moves',
98
+ easeInOutBack: 'Pulls back first and overshoots at the end',
99
+ spring: 'Damped spring, ζ≈1: settles with no overshoot',
100
+ springSnappy: 'Damped spring, ζ≈0.69: quick, one ~5% overshoot',
101
+ springPlayful: 'Damped spring, ζ≈0.45: ~21% overshoot, visible wobbles',
102
+ easeInQuad: 'Quadratic slow start (same as easeIn)',
103
+ easeOutQuad: 'Quadratic slow end (same as easeOut)',
104
+ easeInOutQuad: 'Quadratic slow start and end (same as easeInOut)',
105
+ easeInSine: 'Gentle slow start (sine)',
106
+ easeOutSine: 'Gentle slow end (sine)',
107
+ easeInOutSine: 'Gentle slow start and end (sine)',
108
+ easeInQuart: 'Slow start, stronger than cubic (t⁴)',
109
+ easeOutQuart: 'Slow end, stronger than cubic',
110
+ easeInOutQuart: 'Slow start and end, stronger than cubic',
111
+ easeInQuint: 'Very slow start (t⁵)',
112
+ easeOutQuint: 'Very slow end',
113
+ easeInOutQuint: 'Very slow start and end',
114
+ easeInExpo: 'Near-still start, then fast (the standard expo curve)',
115
+ easeOutExpo: 'Fast, then a long near-still end (the standard expo curve)',
116
+ easeInOutExpo: 'Near-still at both ends, fast in the middle',
117
+ easeInCirc: 'Slow start, sharp finish (circular)',
118
+ easeOutCirc: 'Sharp start, slow finish (circular)',
119
+ easeInOutCirc: 'Circular slow start and end',
120
+ };
121
+ export const EasingSchema = z.enum(KEYFRAME_EASINGS).describe('Easing function for animation');
122
+ /**
123
+ * A keyframe's easing: a name from the table, or a custom cubic Bézier
124
+ * [x1, y1, x2, y2]. The interpolator runs the array natively, the validator
125
+ * accepts it, and every export path handles it (SMIL clamps y into keySplines;
126
+ * CSS and Lottie take the raw curve). Keyframe tracks only — keyframe_animate,
127
+ * camera_animate and inline keyframes — not masks or relations (fxtool-d7).
128
+ */
129
+ export const BezierEasingSchema = z.tuple([z.number(), z.number(), z.number(), z.number()])
130
+ .refine(([x1, , x2]) => x1 >= 0 && x1 <= 1 && x2 >= 0 && x2 <= 1, {
131
+ message: 'cubic-bezier x1 and x2 must be within 0..1 (y1 and y2 may exceed it for overshoot)',
132
+ })
133
+ .describe('Custom cubic-bezier [x1, y1, x2, y2]');
134
+ export const KeyframeEasingSchema = z.union([EasingSchema, BezierEasingSchema]);
135
+ /** JSON-Schema form of KeyframeEasingSchema, for tool definitions. */
136
+ export const KEYFRAME_EASING_JSON_SCHEMA = {
137
+ oneOf: [
138
+ { type: 'string', enum: [...KEYFRAME_EASINGS] },
139
+ {
140
+ type: 'array', items: { type: 'number' }, minItems: 4, maxItems: 4,
141
+ description: 'or a custom cubic-bezier [x1, y1, x2, y2] — x in 0..1, y may exceed 0..1 for overshoot',
142
+ },
143
+ ],
144
+ };
81
145
  // =============================================================================
82
146
  // ITEM TYPES
83
147
  // =============================================================================
@@ -251,7 +315,7 @@ export const SimpleAnimationTypeSchema = z.enum([
251
315
  export const KeyframeSchema = z.object({
252
316
  time: z.number().describe('Time in seconds for this keyframe'),
253
317
  properties: z.record(z.unknown()).describe('Property values at this keyframe'),
254
- easing: EasingSchema.optional().default('linear'),
318
+ easing: KeyframeEasingSchema.optional().default('linear'),
255
319
  });
256
320
  // =============================================================================
257
321
  // RELATION TYPES
@@ -1041,22 +1105,10 @@ export const BlastParamsSchema = z.object({
1041
1105
  // 'saturate' is what the published JSON schema offered for releases, while
1042
1106
  // the engine registers 'saturation' and this enum rejected 'saturate' — a
1043
1107
  // caller who followed the docs got a validation error. Accepted and mapped.
1044
- export const FilterTypeSchema = z.preprocess((v) => (v === 'saturate' ? 'saturation' : v), z.enum([
1045
- 'grayscale',
1046
- 'sepia',
1047
- 'blur',
1048
- 'brightness',
1049
- 'contrast',
1050
- 'saturation',
1051
- 'invert',
1052
- 'noise',
1053
- 'vignette',
1054
- 'vintage',
1055
- 'colorOverlay',
1056
- 'sharpen',
1057
- 'emboss',
1058
- 'posterize',
1059
- ])).describe('Type of visual filter to apply');
1108
+ // GENERATED list (FilterSystem.js registerFilter, scripts/sync-engine-lists.mjs).
1109
+ // The hand copy here hid six of twenty: dither, halftoneCMYK, halftoneDots,
1110
+ // edgeDetect, hsl and colorTint. 'saturate' stays accepted as 'saturation'.
1111
+ export const FilterTypeSchema = z.preprocess((v) => (v === 'saturate' ? 'saturation' : v), z.enum(FILTER_TYPES)).describe('Type of visual filter to apply');
1060
1112
  // =============================================================================
1061
1113
  // TOOL INPUT SCHEMAS
1062
1114
  // =============================================================================
@@ -1377,9 +1429,12 @@ export const SetCanvasSizeInputSchema = z.object({
1377
1429
  // not manage it.
1378
1430
  // The refusal NAMES the route: a 728x90 banner refused with a bare "too
1379
1431
  // small" left a caller to export the preset instead (round 8 BB, 1.48).
1380
- width: z.number().min(100, "a canvas side under 100 px is not supported. For small assets (a 728x90 or 320x50 banner, a 28 px emote) design at 2-4x — e.g. 1456x180 — and export with agent_export region {x:0, y:0, width, height, outputWidth:728, outputHeight:90}. For VIDEO at a small size, design at 2x and export with agent_export scale: 0.5.").max(16384).describe('Canvas width (100-16384; above 8192 Safari / iOS may fail)'),
1381
- height: z.number().min(100, "a canvas side under 100 px is not supported. For small assets (a 728x90 or 320x50 banner, a 28 px emote) design at 2-4x — e.g. 1456x180 — and export with agent_export region {x:0, y:0, width, height, outputWidth:728, outputHeight:90}. For VIDEO at a small size, design at 2x and export with agent_export scale: 0.5.").max(16384).describe('Canvas height (100-16384; above 8192 Safari / iOS may fail)'),
1382
- preset: z.string().optional().describe('Optional preset name'),
1432
+ width: z.number().min(100, "a canvas side under 100 px is not supported. For small assets (a 728x90 or 320x50 banner, a 28 px emote) design at 2-4x — e.g. 1456x180 — and export with agent_export region {x:0, y:0, width, height, outputWidth:728, outputHeight:90}. For VIDEO at a small size, design at 2x and export with agent_export scale: 0.5.").max(16384).optional().describe('Canvas width (100-16384; above 8192 Safari / iOS may fail)'),
1433
+ height: z.number().min(100, "a canvas side under 100 px is not supported. For small assets (a 728x90 or 320x50 banner, a 28 px emote) design at 2-4x — e.g. 1456x180 — and export with agent_export region {x:0, y:0, width, height, outputWidth:728, outputHeight:90}. For VIDEO at a small size, design at 2x and export with agent_export scale: 0.5.").max(16384).optional().describe('Canvas height (100-16384; above 8192 Safari / iOS may fail)'),
1434
+ preset: z.string().optional().describe('A named size instead of width/height; an unknown name is refused with the known ones listed.'),
1435
+ }).refine((v) => !!v.preset || (v.width !== undefined && v.height !== undefined), {
1436
+ // {preset} alone was refused for a missing width (1.6.19 gate).
1437
+ message: 'pass width and height, or a preset', path: ['width'],
1383
1438
  });
1384
1439
  // Export
1385
1440
  export const ExportSVGInputSchema = z.object({
@@ -1457,12 +1512,12 @@ export const SetToolkitInputSchema = z.object({
1457
1512
  // =============================================================================
1458
1513
  export const SearchAssetsInputSchema = z.object({
1459
1514
  query: z.string().describe('Search term (e.g., "rocket", "heart", "user icon")'),
1460
- repository: z.enum(['all', 'svgrepo', 'openclipart', 'iconify', 'fontawesome']).optional().describe('Which repository to search (default: all)'),
1515
+ repository: z.enum(['all', 'iconify', 'fontawesome']).optional().describe('Which repository to search (default: all)'),
1461
1516
  limit: z.number().min(1).max(50).optional().describe('Max results to return (default: 10)'),
1462
1517
  includeSvgContent: z.boolean().optional().describe('Include actual SVG content for each result (default: false). When true, fetches and returns the SVG string for each asset, enabling AI to display or describe the icons.'),
1463
1518
  });
1464
1519
  export const ImportAssetInputSchema = z.object({
1465
- assetId: z.string().optional().describe('Asset ID from search results (e.g., "svgrepo_12345")'),
1520
+ assetId: z.string().optional().describe('Asset ID from search results (e.g., "iconify_mdi_rocket")'),
1466
1521
  url: z.string().optional().describe('Direct URL to SVG (alternative to assetId)'),
1467
1522
  position: z.object({
1468
1523
  x: z.number(),
@@ -1674,8 +1729,23 @@ export const CreateDiagramShapeInputSchema = z.object({
1674
1729
  /**
1675
1730
  * Connect items input
1676
1731
  */
1677
- export const ConnectInputSchema = z.object({
1678
- 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'),
1732
+ // strokeColor / strokeWidth are what a model guesses (Paper's names); they
1733
+ // were stripped and the connector drew in the default colour (gate run 3).
1734
+ export const ConnectInputSchema = z.preprocess((v) => {
1735
+ if (!v || typeof v !== 'object' || Array.isArray(v))
1736
+ return v;
1737
+ const o = { ...v };
1738
+ if (o.lineColor === undefined && o.strokeColor !== undefined) {
1739
+ o.lineColor = o.strokeColor;
1740
+ delete o.strokeColor;
1741
+ }
1742
+ if (o.lineWidth === undefined && o.strokeWidth !== undefined) {
1743
+ o.lineWidth = o.strokeWidth;
1744
+ delete o.strokeWidth;
1745
+ }
1746
+ return o;
1747
+ }, z.object({
1748
+ id: z.string().optional().describe('Requested connector id. The studio may not honour it (the result says idHonoured): always use the connectorId the result returns for update/remove.'),
1679
1749
  sourceItemId: z.string().describe('Registry ID of source item'),
1680
1750
  targetItemId: z.string().describe('Registry ID of target item'),
1681
1751
  routing: ConnectorRoutingSchema.optional().default('orthogonal'),
@@ -1688,12 +1758,12 @@ export const ConnectInputSchema = z.object({
1688
1758
  curvature: z.number().min(0.1).max(1.0).optional().default(0.5).describe('Curve intensity for curved routing'),
1689
1759
  boltEnabled: z.boolean().optional().default(true).describe('Enable animated bolt effect'),
1690
1760
  boltColor: z.string().optional().default('#fbbf24').describe('Bolt animation color'),
1691
- });
1761
+ }));
1692
1762
  /**
1693
1763
  * Connect specific ports input
1694
1764
  */
1695
1765
  export const ConnectPortsInputSchema = z.object({
1696
- 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'),
1766
+ id: z.string().optional().describe('Requested connector id. The studio may not honour it (the result says idHonoured): always use the connectorId the result returns for update/remove.'),
1697
1767
  sourceItemId: z.string().describe('Registry ID of source item'),
1698
1768
  sourcePort: PortPositionSchema.describe('Port position on source'),
1699
1769
  targetItemId: z.string().describe('Registry ID of target item'),
@@ -1760,16 +1830,34 @@ export const GetDiagramShapesInputSchema = z.object({
1760
1830
  /**
1761
1831
  * Update connector input
1762
1832
  */
1833
+ // Paper.js's own names are what a model guesses for a connector's style, and
1834
+ // an unknown key was STRIPPED, so {strokeColor} changed nothing and reported
1835
+ // success (gate run 3). The Paper names are mapped; anything else is refused
1836
+ // with the keys that work.
1837
+ const CONNECTOR_STYLE_ALIASES = { strokeColor: 'lineColor', color: 'lineColor', strokeWidth: 'lineWidth', width: 'lineWidth' };
1838
+ const CONNECTOR_STYLE_KEYS = ['lineColor', 'lineWidth', 'headStyle', 'tailStyle', 'routing', 'lineStyle'];
1763
1839
  export const UpdateConnectorInputSchema = z.object({
1764
1840
  connectorId: z.string().describe('Connector ID to update'),
1765
- style: z.object({
1841
+ style: z.preprocess((v) => {
1842
+ if (!v || typeof v !== 'object' || Array.isArray(v))
1843
+ return v;
1844
+ const out = {};
1845
+ for (const [k, val] of Object.entries(v)) {
1846
+ if (k === 'dashArray') {
1847
+ out.lineStyle = Array.isArray(val) && val.length ? 'dashed' : 'solid';
1848
+ continue;
1849
+ }
1850
+ out[CONNECTOR_STYLE_ALIASES[k] ?? k] = val;
1851
+ }
1852
+ return out;
1853
+ }, z.object({
1766
1854
  lineColor: z.string().optional(),
1767
1855
  lineWidth: z.number().optional(),
1768
1856
  headStyle: ArrowHeadStyleSchema.optional(),
1769
1857
  tailStyle: ArrowHeadStyleSchema.optional(),
1770
1858
  routing: ConnectorRoutingSchema.optional(),
1771
1859
  lineStyle: ConnectorLineStyleSchema.optional(),
1772
- }).optional(),
1860
+ }).strict(`a connector's style takes ${CONNECTOR_STYLE_KEYS.join(', ')} (strokeColor, strokeWidth and dashArray are accepted for lineColor, lineWidth and lineStyle)`)).optional(),
1773
1861
  label: z.string().optional().describe('Update connector label'),
1774
1862
  labelPosition: z.number().min(0).max(1).optional().describe('Label position along path (0-1)'),
1775
1863
  });
@@ -1821,9 +1909,7 @@ export const MaskPresetSchema = z.enum([
1821
1909
  export const MaskTypeSchema = z.enum([
1822
1910
  'rectangle', 'circle', 'ellipse', 'star', 'triangle', 'hexagon', 'heart', 'rounded',
1823
1911
  ]).describe('Mask shape type');
1824
- export const MaskEasingSchema = z.enum([
1825
- 'linear', 'easeIn', 'easeOut', 'easeInOut', 'bounce', 'elastic',
1826
- ]).describe('Mask animation easing function');
1912
+ export const MaskEasingSchema = z.enum(KEYFRAME_EASINGS).describe('Mask animation easing function');
1827
1913
  export const MaskKeyframePropertiesSchema = z.object({
1828
1914
  x: z.number().optional(),
1829
1915
  y: z.number().optional(),
@@ -1977,10 +2063,8 @@ export const AgentBatchOperationSchema = z.object({
1977
2063
  type: AgentBatchOperationTypeSchema,
1978
2064
  // Create operation fields
1979
2065
  itemType: z.string().optional().describe('Item type for create operations'),
1980
- position: z.object({
1981
- x: z.number(),
1982
- y: z.number(),
1983
- }).optional().describe('Position for create operations'),
2066
+ // PositionSchema, as create_item takes: [x, y] was refused here (1.6.19 gate).
2067
+ position: PositionSchema.optional().describe('Position for create operations: {x, y} or [x, y]'),
1984
2068
  properties: z.record(z.unknown()).optional().describe('Properties for create/modify operations'),
1985
2069
  // Modify/Animate/Delete/Keyframe/Mask/Effect target
1986
2070
  itemId: z.string().optional().describe('Target item ID or $N reference'),
@@ -2109,6 +2193,19 @@ export const GIF_MAX_DURATION_S = 15;
2109
2193
  * out-of-memory — it turns into a message naming the ceiling and what to do.
2110
2194
  */
2111
2195
  export const VIDEO_MAX_DURATION_S = 600;
2196
+ /**
2197
+ * pinepaper_styled_scene (FxTool K2): one declarative composition drawn in a
2198
+ * built-in style, as ONE canvas item that redraws itself every exported frame.
2199
+ * The styles are GENERATED (STYLED_SCENE_STYLES); the engine also resolves its
2200
+ * own aliases ('watercolour', 'papercut'), so style is a string here and an
2201
+ * unknown one is refused by the engine with the known list.
2202
+ */
2203
+ export const StyledSceneInputSchema = z.object({
2204
+ style: z.string().optional().describe(`The look: ${STYLED_SCENE_STYLES.join(' | ')} (default watercolor). The engine also takes its aliases (watercolour, papercut, 1bit…).`),
2205
+ duration: z.number().positive().max(VIDEO_MAX_DURATION_S).optional().describe('Seconds the scene runs (default the composition\'s, 8). Export for this long.'),
2206
+ id: z.string().optional().describe('Scene id (default "styled-scene"). Calling again with the same id REPLACES that scene, e.g. to change style.'),
2207
+ spec: z.record(z.string(), z.unknown()).optional().describe("The composition, every key optional (omitted keys take the 'valley' preset): preset, size [W, H], duration, transition {name, start, length}, camera {zoom: [from, to], pan}, palette, sun, clouds, ridges, foreground, river, settlements, structure, pylons, vegetation, effects {steam, ripples, scars}, hud {label}."),
2208
+ });
2112
2209
  /**
2113
2210
  * Smart export input schema
2114
2211
  */
@@ -2193,6 +2290,12 @@ export const AgentExportInputSchema = z.object({
2193
2290
  maxBytes: z.number().int().positive().optional().describe('gif only: a size budget in bytes (email wants <= 1 MB). Over it, the GIF is re-encoded smaller — frame size scaled from the overshoot — at most twice; the result reports each attempt and whether the budget was met.'),
2194
2291
  broadcast: z.boolean().optional().describe('mp4 only: broadcast-safe — BT.709, limited range (samples 16-235), tagged bt709, constant bitrate with an 8 Mbps floor at 720p and up (4 below). result.video reports what the encoder did.'),
2195
2292
  frames: z.number().int().min(1).max(36_000).optional().describe('Animated formats: export exactly this many frames; it takes the place of duration (duration x fps frames is rounded UP for mp4 / webm / gif). For frame-exact segments to assemble.'),
2293
+ // FxTool #55 (motion-course G2): N sub-frames per frame, averaged across
2294
+ // the shutter, so fast motion smears the way a camera's does.
2295
+ motionBlur: z.object({
2296
+ subframes: z.number().int().min(1).max(32).optional().describe('Renders per frame (default 8; 1 = no blur). Export time scales with it.'),
2297
+ shutter: z.number().min(0).max(1).optional().describe('Fraction of the frame interval the shutter is open (default 0.5, a 180° shutter).'),
2298
+ }).optional().describe('mp4 / webm: real motion blur. Each frame is rendered subframes times across the shutter and averaged, so fast movement smears instead of strobing. Costs subframes× the render time: run estimateOnly first, which reports renders and renderCost.'),
2196
2299
  deterministic: z.boolean().optional().describe('mp4 / webm: byte-identical files for the same scene, so an export can be checksummed or cached by hash. The container\'s creation / modification timestamps are pinned; nothing about the pixels changes.'),
2197
2300
  broadcastHeadroom: z.number().int().min(0).max(40).optional().describe('mp4 with broadcast only (default 12 with broadcast): luma codes kept clear at both ends of 16-235, so encoder ringing stays legal. 12 measured clean on hard edges; 4 cuts out-of-range samples ~100x, 12 ~1000x; never to zero, so a legaliser pass is still required (result fidelity names it).'),
2198
2301
  bitrate: z.number().int().min(100_000).max(200_000_000).optional().describe('mp4 / webm: target bits per second, replacing the quality-derived one. The browser encoder treats it as a CEILING: simple content comes out lower, and result.video.achievedBitrate says what it was.'),
@@ -2220,7 +2323,7 @@ export const AgentExportInputSchema = z.object({
2220
2323
  if (val.frames !== undefined && !['mp4', 'webm', 'gif', 'apng'].includes(String(val.format))) {
2221
2324
  ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['frames'], message: 'frames applies to animated formats: mp4, webm, gif, apng.' });
2222
2325
  }
2223
- for (const k of ['bitrate', 'minBitrate', 'bitrateMode', 'deterministic']) {
2326
+ for (const k of ['bitrate', 'minBitrate', 'bitrateMode', 'deterministic', 'motionBlur']) {
2224
2327
  if (val[k] !== undefined && val.format !== 'mp4' && val.format !== 'webm') {
2225
2328
  ctx.addIssue({ code: z.ZodIssueCode.custom, path: [k], message: `${k} is a video setting: mp4 or webm. A gif's size is set by maxBytes and scale.` });
2226
2329
  }
@@ -2523,6 +2626,7 @@ export const HighlightRegionsInputSchema = z.object({
2523
2626
  strokeColor: z.string().optional().describe('Highlight stroke color'),
2524
2627
  stroke: z.string().optional().describe("The engine's own name for the stroke."),
2525
2628
  strokeWidth: z.number().optional().describe('Highlight stroke width'),
2629
+ opacity: z.number().min(0).max(1).optional().describe('0..1: a TINT over the land. The colour is pre-blended over the region\'s land fill and drawn opaque, so the export shows exactly the fill reported. Hex colours only.'),
2526
2630
  animate: z.boolean().optional().describe('Animate the highlight'),
2527
2631
  }).optional().describe('Highlight options'),
2528
2632
  }).describe('Highlight regions input');
@@ -2588,12 +2692,24 @@ export const GetRegionAtPointInputSchema = z.object({
2588
2692
  y: z.number().describe('Canvas Y coordinate'),
2589
2693
  }).describe('Get region at point input');
2590
2694
  // Map Animation Schemas
2591
- export const MapRegionKeyframeSchema = z.object({
2695
+ // The engine reads fillColor OR color, plus easing (MapSystem.animateRegions);
2696
+ // requiring fillColor refused keyframes written with color (gate D17, 1.6.19).
2697
+ // A keyframe may nest its values under `properties`, the shape every other
2698
+ // keyframe tool takes; it is flattened rather than refused (gate run 2).
2699
+ export const MapRegionKeyframeSchema = z.preprocess((k) => {
2700
+ if (!k || typeof k !== 'object' || Array.isArray(k))
2701
+ return k;
2702
+ const { properties, ...rest } = k;
2703
+ return properties && typeof properties === 'object' && !Array.isArray(properties) ? { ...properties, ...rest } : k;
2704
+ }, z.object({
2592
2705
  time: z.number().describe('Time in seconds'),
2593
- fillColor: z.string().describe('Fill color at this keyframe'),
2706
+ fillColor: z.string().optional().describe('Fill color at this keyframe (or color)'),
2707
+ color: z.string().optional().describe('Same as fillColor'),
2594
2708
  strokeColor: z.string().optional().describe('Stroke color (optional)'),
2595
2709
  opacity: z.number().optional().describe('Opacity 0-1 (optional)'),
2596
- }).describe('Map region keyframe');
2710
+ easing: z.string().optional().describe('Easing into this keyframe'),
2711
+ }).refine((k) => !!(k.fillColor || k.color), { message: 'each keyframe needs fillColor (or color)', path: ['fillColor'] }))
2712
+ .describe('Map region keyframe');
2597
2713
  export const WaveDirectionSchema = z.enum(['horizontal', 'vertical', 'radial']).describe('Wave animation direction');
2598
2714
  export const AnimateMapRegionsInputSchema = z.object({
2599
2715
  duration: z.number().optional().default(5).describe('Total animation duration in seconds'),
@@ -2603,9 +2719,10 @@ export const AnimateMapRegionsInputSchema = z.object({
2603
2719
  export const AnimateMapWaveInputSchema = z.object({
2604
2720
  duration: z.number().optional().default(10).describe('Total wave duration in seconds'),
2605
2721
  loop: z.boolean().optional().default(true).describe('Loop the animation'),
2606
- colors: z.array(z.string()).optional().describe('Array of colors for the wave'),
2722
+ colors: z.array(z.string()).optional().describe('Colors the wave cycles through (at least 2)'),
2607
2723
  waveDirection: WaveDirectionSchema.optional().default('horizontal').describe('Direction of wave effect'),
2608
- }).describe('Animate map wave input');
2724
+ waveSpeed: z.number().positive().optional().describe('Speed of the wave (default 1)'),
2725
+ }).describe('Animate map wave input: the wave runs across ALL regions of the map');
2609
2726
  export const StopMapAnimationsInputSchema = z.object({
2610
2727
  regions: z.array(z.string()).optional().describe('Specific region IDs to stop (omit for all)'),
2611
2728
  resetColors: z.boolean().optional().default(true).describe('Reset regions to default colors'),
@@ -3000,8 +3117,10 @@ export const TransformInputSchema = z.object({
3000
3117
  // count is already the thing the consolidation backlog complains about, and
3001
3118
  // `pinepaper_media` / `pinepaper_transform` set the precedent.
3002
3119
  export const BrandKitInputSchema = z.object({
3003
- /** 'plan' reports what WOULD change without touching the scene. */
3004
- action: z.enum(['plan', 'apply']),
3120
+ /** 'plan' reports what WOULD change without touching the scene. 'from_url' reads a site into a proposed kit. */
3121
+ action: z.enum(['plan', 'apply', 'from_url']),
3122
+ url: z.string().url().optional().describe("from_url: the product's site (http/https). Read in an isolated browser context: the studio is not navigated."),
3123
+ screenshots: z.number().int().min(1).max(6).optional().describe('from_url: screenshots taken down the page (default 3).'),
3005
3124
  // Spelled out rather than a loose record. A first version typed this as
3006
3125
  // `z.record(...)` described as a "role → colour map", which is NOT the shape
3007
3126
  // the engine takes — every call was rejected with "name is required;
@@ -3022,9 +3141,11 @@ export const BrandKitInputSchema = z.object({
3022
3141
  heading: z.string().optional(),
3023
3142
  body: z.string().optional(),
3024
3143
  }).optional(),
3025
- }),
3144
+ }).optional(),
3026
3145
  selectionOnly: z.boolean().optional(),
3027
- });
3146
+ })
3147
+ .refine((v) => v.action === 'from_url' || !!v.kit, { message: 'plan and apply require kit (from_url proposes one)', path: ['kit'] })
3148
+ .refine((v) => v.action !== 'from_url' || (!!v.url && /^https?:\/\//i.test(v.url)), { message: 'from_url requires an http(s) url', path: ['url'] });
3028
3149
  export const ComponentInputSchema = z.object({
3029
3150
  action: z.enum(['define', 'list', 'instantiate', 'set_override', 'sync', 'update_from_instance', 'detach']),
3030
3151
  itemIds: z.array(z.string()).optional(),
@@ -3063,7 +3184,9 @@ export const CommentInputSchema = z.object({
3063
3184
  export const ProvenanceInputSchema = z.object({
3064
3185
  action: z.enum(['get', 'lineage', 'dependents', 'record']),
3065
3186
  itemId: z.string(),
3066
- kind: z.string().optional(),
3187
+ // GENERATED (LINEAGE_KINDS): the default was 'derived', which the engine
3188
+ // refuses, and the docs named three kinds that do not exist (1.6.19 gate).
3189
+ kind: z.enum(LINEAGE_KINDS).optional().describe('record: how the item came from sourceRef (default derivedFrom).'),
3067
3190
  sourceRef: z.string().optional(),
3068
3191
  meta: z.record(z.string(), z.unknown()).optional(),
3069
3192
  });
@@ -3461,11 +3584,69 @@ export const ValidateSceneInputSchema = z.object({
3461
3584
  });
3462
3585
  // Deterministic headless frame capture (S3) — evaluate the scene at each time via
3463
3586
  // sceneAt(t) with Math.random seeded once around the whole sequence, then snapshot.
3587
+ /**
3588
+ * A contact sheet: many frames tiled into ONE small image, saved to a file,
3589
+ * so an agent can look at its own film (G3 critique loop). Built on the same
3590
+ * deterministic capture; the times come from `times`, or from one of the
3591
+ * modes below.
3592
+ */
3593
+ /**
3594
+ * pinepaper_export_scene / pinepaper_import_scene (D28): a restorable save.
3595
+ * The default export is a summary for inspection; `full` writes the engine's
3596
+ * project document (captureProjectDocument) to a file.
3597
+ */
3598
+ /**
3599
+ * pinepaper_place_on_surface (D22, FxTool #56): a UI onto a screen in a photo,
3600
+ * corner-pinned, matted and lit, baked into the photo's pixels.
3601
+ */
3602
+ const PointPair = z.tuple([z.number(), z.number()]);
3603
+ export const PlaceOnSurfaceInputSchema = z.object({
3604
+ photoId: z.string().describe('The photo: an image item (or a group holding one).'),
3605
+ sourceId: z.string().describe('The UI to place: any canvas item or group, or an image item.'),
3606
+ quad: z.tuple([PointPair, PointPair, PointPair, PointPair]).optional().describe('The screen\'s four corners TL, TR, BR, BL. Omit to detect it (the largest bright, neutral region).'),
3607
+ quadSpace: z.enum(['image', 'canvas']).optional().describe("quad's coordinates: 'image' (default) photo pixels, or 'canvas'."),
3608
+ occlusion: z.enum(['luma', 'none']).optional().describe("'luma' (default) keeps a thumb or hand in front of the screen; 'none' covers it."),
3609
+ key: z.number().optional().describe('Brightness above which the photo counts as screen (default: found from the photo).'),
3610
+ erode: z.number().min(0).optional().describe('Pull the matte in from the bezel, px (default 2).'),
3611
+ matteBlur: z.number().min(0).optional().describe('Soften the matte edge (default 0.8).'),
3612
+ soften: z.number().min(0).optional().describe('Soften the UI to the photo\'s focus (default 0.5).'),
3613
+ glare: z.number().min(0).max(1).optional().describe('Glass glare kept over the UI (default 0.10).'),
3614
+ spill: z.number().min(0).max(1).optional().describe('Screen light spilling onto the surroundings (default 0.35).'),
3615
+ spillRadius: z.number().min(0).optional().describe('Spill reach, px (default 40).'),
3616
+ tint: z.tuple([z.number(), z.number(), z.number()]).optional().describe('Multiply the UI by [r, g, b], 0..1, to match the photo\'s white balance.'),
3617
+ slot: z.string().optional().describe("Name for this screen (default 'screen'). Placing again in the same slot REPLACES it; several screens in one photo use different slots."),
3618
+ hideSource: z.boolean().optional().describe('Hide the UI item after placing it (default true).'),
3619
+ });
3620
+ export const ExportSceneInputSchema = z.object({
3621
+ full: z.boolean().optional().describe('Save a RESTORABLE project document to a file (keyframes, relations, rigging, scene chains), not the summary. Restore it with pinepaper_import_scene.'),
3622
+ name: z.string().optional().describe('full: a name recorded in the document.'),
3623
+ });
3624
+ export const ImportSceneInputSchema = z.object({
3625
+ path: z.string().min(1).describe('The project document a full pinepaper_export_scene saved (absolute path, or relative to the server working directory).'),
3626
+ strict: z.boolean().optional().describe('Refuse a document with validation problems (default true). false loads anyway and returns the problems as warnings.'),
3627
+ });
3628
+ export const CaptureSheetSchema = z.object({
3629
+ every: z.number().positive().optional().describe('One frame every N seconds from 0 to duration (needs duration).'),
3630
+ duration: z.number().positive().optional().describe('Scene length in seconds, for every and loopSeam.'),
3631
+ strip: z.object({
3632
+ at: z.number().min(0).describe('Centre time in seconds.'),
3633
+ count: z.number().int().min(2).max(24).default(8),
3634
+ step: z.number().positive().default(1 / 30).describe('Seconds between frames (default one frame at 30 fps).'),
3635
+ }).optional().describe('Consecutive frames around a moment: to check a transition or a stutter.'),
3636
+ loopSeam: z.boolean().optional().describe('The last frame next to the first (needs duration): does the loop join without a jump?'),
3637
+ tileWidth: z.number().int().min(120).max(1080).default(360).describe('Width of each tile in px. 360 is phone width: read every word at this size.'),
3638
+ columns: z.number().int().min(1).max(12).optional().describe('Tiles per row (default: as square as the count allows, at most 6).'),
3639
+ format: z.enum(['jpeg', 'png']).default('jpeg'),
3640
+ labels: z.boolean().default(true).describe('Stamp each tile with its time.'),
3641
+ });
3464
3642
  export const CaptureFramesInputSchema = z.object({
3465
- times: z.array(z.number()).min(1).describe('Capture times in seconds, rendered in order.'),
3643
+ times: z.array(z.number()).min(1).optional().describe('Capture times in seconds, rendered in order. Required unless sheet gives every, strip or loopSeam.'),
3466
3644
  seed: z.number().optional().describe('Seed Math.random once around the whole sequence so random generators/particles are reproducible (default 0).'),
3467
3645
  includeDataUrls: z.boolean().optional().describe('Include each frame as a PNG data URL (large — token-heavy). Default false → returns a cheap per-frame hash + byte size only.'),
3468
- });
3646
+ sheet: CaptureSheetSchema.optional().describe('Return ONE tiled image (saved to a file) instead of per-frame hashes, plus a critique rubric.'),
3647
+ })
3648
+ .refine((v) => !!v.times || !!(v.sheet && (v.sheet.every || v.sheet.strip || v.sheet.loopSeam)), { message: 'give times, or sheet with every, strip or loopSeam', path: ['times'] })
3649
+ .refine((v) => !v.sheet || !(v.sheet.every || v.sheet.loopSeam) || !!v.sheet.duration || !!v.times, { message: 'sheet.every and sheet.loopSeam need sheet.duration (the scene length in seconds)', path: ['sheet', 'duration'] });
3469
3650
  // Ontology→scene compiler (S12-E3). Compiles a pp:-namespaced JSON-LD design graph
3470
3651
  // (typed nodes + structural edges like pp:onTopOf) into a real scene. Compilation
3471
3652
  // runs server-side (deterministic, testable); only the resulting create/addRelation
@@ -3476,7 +3657,7 @@ export const InstantiateOntologyInputSchema = z.object({
3476
3657
  canvas: z.object({
3477
3658
  width: z.number().positive(),
3478
3659
  height: z.number().positive(),
3479
- }).optional().describe('Canvas size used to tile component roots (default 1080×1080).'),
3660
+ }).optional().describe('Canvas size to lay out for (default: the live canvas).'),
3480
3661
  defaultGeometry: z.record(z.string(), z.object({
3481
3662
  width: z.number(),
3482
3663
  height: z.number(),
@@ -3492,14 +3673,20 @@ export const LintSceneInputSchema = z.object({
3492
3673
  // Media (video/audio) via window.PinePaperAgent. Agent-facing surface is URL-based
3493
3674
  // (the agent can't hand over a File). Uploaded media are first-class canvas items.
3494
3675
  export const MediaInputSchema = z.object({
3495
- action: z.enum(['upload_video', 'upload_audio', 'list', 'remove', 'set_playback_rate', 'set_clip', 'split', 'add_transition', 'set_time_remap', 'speed_ramp', 'match_cut', 'apply_track_matte', 'stop_live_matte'])
3676
+ action: z.enum(['upload_video', 'upload_audio', 'list', 'remove', 'set_playback_rate', 'set_clip', 'split', 'add_transition', 'set_time_remap', 'speed_ramp', 'match_cut', 'apply_track_matte', 'stop_live_matte', 'analyze_reference', 'remake_from_reference'])
3496
3677
  .describe("'upload_video' / 'upload_audio' (from a URL) · 'list' media · 'remove' by id · 'set_playback_rate' · 'set_clip' (re-trim; with timeOffset also moves the clip) · 'split' (cut a clip in two at canvas time `at`; the first half keeps its id) · 'add_transition' (crossfade or dip between fromItemId, which ends at the cut, and toItemId, which starts there) · 'set_time_remap' (canvas-time→source-time curve: ramps, freezes, reverse) · 'speed_ramp' ({duration, speed} segments — the human way to say a remap) · 'match_cut' (cut between two shots aligning the SUBJECT via on-device detection) · 'apply_track_matte' (an item's alpha driven by another item's luma/alpha — type-filled-with-footage; live:true keeps it tracking as the matte animates) · 'stop_live_matte'"),
3497
3678
  // Not .url(): a bare absolute path is not a URL, and 48f3a2a made the server
3498
3679
  // read local files — so `/Users/…/broll1.mp4` was advertised and then rejected
3499
3680
  // by the schema before anything could read it. resolveMediaSource validates
3500
3681
  // what it is given and names the problem.
3501
3682
  url: z.string().min(1).optional().describe('Required for upload_video / upload_audio: an http(s) URL, a data: URL, or a local file path (absolute, relative to the server, or file://).'),
3502
- id: z.string().optional().describe('Media id — required for remove / set_playback_rate / set_clip / split.'),
3683
+ id: z.string().optional().describe('Media id — required for remove / set_playback_rate / set_clip / split / analyze_reference.'),
3684
+ // G4 reference intake (FxTool #52): measure a reference video's FORMAT and
3685
+ // build on it. Nothing of its footage, words or sound is copied.
3686
+ texts: z.array(z.string()).optional().describe('remake_from_reference: your words, one per shot of the reference (a short list is reused across shots).'),
3687
+ duration: z.number().positive().optional().describe('remake_from_reference: total length in seconds (default the reference\'s own).'),
3688
+ clear: z.boolean().optional().describe('remake_from_reference: empty the canvas first (media survives).'),
3689
+ thumbnails: z.boolean().optional().describe('analyze_reference: include a small image per shot (default false: they are inline images and token-heavy).'),
3503
3690
  rate: z.number().min(0.25).max(4).optional().describe('Playback rate 0.25–4 — required for set_playback_rate.'),
3504
3691
  inPoint: z.number().min(0).optional().describe('Clip in-point in media-time seconds — required for set_clip.'),
3505
3692
  outPoint: z.number().min(0).optional().describe('Clip out-point in media-time seconds — required for set_clip.'),
@@ -3549,6 +3736,8 @@ export const MediaInputSchema = z.object({
3549
3736
  atPlayhead: z.boolean().optional().describe('upload_video / upload_audio: start the clip at the current playback time (instead of timeOffset).'),
3550
3737
  })
3551
3738
  .refine((v) => !(v.action === 'upload_video' || v.action === 'upload_audio') || !!v.url, { message: 'upload requires url', path: ['url'] })
3739
+ .refine((v) => v.action !== 'analyze_reference' || !!v.id, { message: 'analyze_reference requires id: the uploaded reference clip (upload_video it first)', path: ['id'] })
3740
+ .refine((v) => v.action !== 'remake_from_reference' || (!!v.texts && v.texts.length > 0), { message: 'remake_from_reference requires texts: your own words, one per shot', path: ['texts'] })
3552
3741
  .refine((v) => !(v.action === 'remove' || v.action === 'set_playback_rate' || v.action === 'set_clip' || v.action === 'split') || !!v.id, { message: 'this action requires id', path: ['id'] })
3553
3742
  .refine((v) => v.action !== 'split' || (v.at !== undefined && v.at >= 0), { message: 'split requires at (canvas seconds) — where to cut', path: ['at'] })
3554
3743
  .refine((v) => v.action !== 'add_transition' || (!!v.fromItemId && !!v.toItemId), { message: 'add_transition requires fromItemId (the clip ending at the cut) and toItemId (the clip starting at it)', path: ['fromItemId'] })
@@ -3581,7 +3770,26 @@ export const TextStyleInputSchema = z.object({
3581
3770
  scale: z.number().optional().describe('Size multiplier (default 1).'),
3582
3771
  strokeColor: z.string().optional().describe("Ink colour (default '#ffffff')."),
3583
3772
  strokeWidth: z.number().optional().describe('Pen width (default 2).'),
3584
- }).optional().describe('cursive: where and how it is written. The result is a STROKED PATH, so pinepaper_animate\'s draw-on and pinepaper_path\'s outline_stroke both apply to it — which is the reason to prefer it over a script font.'),
3773
+ nib: z.union([
3774
+ z.boolean(),
3775
+ z.enum(['monoline', 'broad']),
3776
+ z.object({
3777
+ angle: z.number().optional().describe('Nib angle in degrees.'),
3778
+ width: z.number().positive().optional().describe('Widest stroke, in px.'),
3779
+ contrast: z.number().min(0).max(1).optional().describe('0..1; the hairline is width × (1 − contrast).'),
3780
+ taper: z.number().min(0).max(0.5).optional().describe('0..0.5; thin entry and exit.'),
3781
+ }).strict(),
3782
+ ]).optional().describe("The pen. 'monoline' (or false): one even line. 'broad' (or true): a broad nib, thick across it and hairline along it; an object tunes the broad nib. Omitted: the studio's default pen."),
3783
+ // Draw-on (gate D41): the engine keyframes trimEnd 0→1 itself; these were
3784
+ // stripped, so a handwritten title arrived fully drawn.
3785
+ animate: z.boolean().optional().describe('true writes the words on, stroke by stroke, over `duration` seconds — no keyframe_animate needed (default false: drawn at once).'),
3786
+ duration: z.number().positive().optional().describe('animate: seconds to write the whole line (default 2).'),
3787
+ easing: z.enum(ENGINE_EASING_NAMES).optional().describe("animate: the write-on easing (default 'easeOut')."),
3788
+ timeOffset: z.number().min(0).optional().describe('animate: seconds into the timeline before writing starts (default 0).'),
3789
+ singleStroke: z.boolean().optional().describe('Primary strokes only — drop the secondary strokes (default false).'),
3790
+ showGhost: z.boolean().optional().describe('Show a faint outline of the whole line under the write-on (default false).'),
3791
+ tension: z.number().optional().describe("Curve tension of the pen path (the studio's default when omitted)."),
3792
+ }).strict().optional().describe('cursive: where and how it is written. The result is a STROKED PATH, so pinepaper_animate\'s draw-on and pinepaper_path\'s outline_stroke both apply to it — which is the reason to prefer it over a script font.'),
3585
3793
  collageOptions: z.record(z.string(), z.unknown()).optional().describe('to_collage: style, palette and the rest, same vocabulary as pinepaper_create_letter_collage.'),
3586
3794
  itemId: z.string().optional().describe('Text item id — apply_style / set_font_axes.'),
3587
3795
  styleKey: z.string().optional().describe("apply_style: style name from list_styles (e.g. 'stacked', 'arcade' — the pixel/arcade styles suggest their own face)."),
@@ -3605,7 +3813,13 @@ export const TextStyleInputSchema = z.object({
3605
3813
  }).passthrough().optional().describe('set_font_axes: weight/width/slant, or OpenType tags (wght/wdth/slnt). Unknown axes are passed to the engine and come back in `rejected` rather than being dropped here. All three named axes are animatable (addKeyframe with fontWeight interpolates).'),
3606
3814
  })
3607
3815
  .refine((v) => v.action !== 'apply_style' || (!!v.itemId && !!v.styleKey), { message: 'apply_style requires itemId and styleKey', path: ['styleKey'] })
3608
- .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'] });
3816
+ .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'] })
3817
+ // cursive WRITES new words; with no text it reached the engine and failed as
3818
+ // "Cannot read properties of undefined (reading 'length')" (gate, P2).
3819
+ .refine((v) => v.action !== 'cursive' || (typeof v.text === 'string' && v.text.trim() !== ''), {
3820
+ message: "cursive writes new text from `text`; it does not restyle an existing item. Pass text (the words to write); for a style on an existing text item use apply_style with its itemId.",
3821
+ path: ['text']
3822
+ });
3609
3823
  /**
3610
3824
  * pinepaper_text_effect — the 37 character-level text effects
3611
3825
  * (terminaltexteffects' vocabulary, reimplemented in FxTool from source).
@@ -3634,25 +3848,28 @@ export const TextStyleInputSchema = z.object({
3634
3848
  * `list_media` before promising a medium in prose.
3635
3849
  */
3636
3850
  export const DesignMediumInputSchema = z.object({
3637
- action: z.enum(['list_media', 'resolve', 'list_stitches', 'apply_thread', 'apply_hatch', 'list_flow_fields', 'list_hatch_options'])
3638
- .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'"),
3639
- 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."),
3640
- 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.'),
3851
+ action: z.enum(['list_media', 'resolve', 'list_stitches', 'apply_thread', 'apply_hatch', 'list_flow_fields', 'list_hatch_options', 'apply', 'sdf_stroke'])
3852
+ .describe("'apply' (render an item in ANY medium the engine can make: medium + options — watercolor, oil, cutPaper, thread, hatch) · 'list_media' (every medium 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'"),
3853
+ medium: z.string().optional().describe(`resolve / apply: medium key — ${DESIGN_MEDIA.join(', ')}. apply works for ${Object.keys(DESIGN_MEDIA_APPLY).join(', ')}; a medium the studio cannot apply is refused by the engine with the reason. ink and charcoal, where the studio applies them, restyle the item IN PLACE (same id).`),
3854
+ options: z.record(z.string(), z.unknown()).optional().describe('apply: the medium\'s own options. watercolor: color, brush, water 0..1, opacity, edge 0..1 (the dark rim), angle, bleed, seed, keepSource. oil: color, brush, length, density, toneRange, relief, angle, seed, keepSource. cutPaper: color, facet, jitter, shadow, shadowColor, shadowOpacity, shadowOffset [dx, dy], seed, keepSource. thread / hatch: as apply_thread / apply_hatch.'),
3855
+ itemId: z.string().optional().describe('apply / apply_thread / apply_hatch: a closed path, compound path (holes kept), or a group of them (applied per path). The result takes the source\'s place in the stacking order and the source is hidden; applying again replaces it. One undo step.'),
3641
3856
  stitch: z.enum(THREAD_STITCHES).optional()
3642
3857
  .describe("Which stitch, when the medium is thread. Default longAndShort. These six are THE_STITCH_OPS the engine publishes — call 'list_stitches' for each one's description and its own parameters. An unknown name is refused by the engine rather than quietly stitched as a default fill."),
3643
3858
  field: z.object({
3644
3859
  kind: z.enum(['radial', 'spine', 'constant']),
3645
3860
  cx: z.number().optional(), cy: z.number().optional(),
3646
3861
  angle: z.number().optional(),
3647
- spine: z.array(PositionSchema).optional(),
3862
+ // [x, y] is converted to {x, y} here: the engine's own spine reader only
3863
+ // takes {x, y} and crashed on an array pair when called directly.
3864
+ spine: z.array(PositionSchema).optional().describe('Points along the midrib, each {x, y} or [x, y]'),
3648
3865
  across: z.boolean().optional(),
3649
3866
  }).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."),
3650
3867
  stitchLen: z.number().positive().optional().describe('apply_thread: nominal stitch length px (default 18).'),
3651
3868
  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.'),
3652
3869
  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.'),
3653
- width: z.number().positive().optional().describe('apply_thread: thread width px (default 2).'),
3870
+ width: z.number().positive().optional().describe('apply_thread: thread width px (default 2). sdf_stroke: stroke width px (default 8).'),
3654
3871
  sheen: z.boolean().optional().describe('apply_thread: draw the highlight along each stitch (default true) — thread is not matte.'),
3655
- color: z.string().optional().describe("apply_thread: thread colour, defaulting to the item's own fill."),
3872
+ color: z.string().optional().describe("apply_thread: thread colour, defaulting to the item's own fill. sdf_stroke: stroke colour."),
3656
3873
  seed: z.number().int().optional().describe('apply_thread: PRNG seed (default 1). Same seed stitches the same way.'),
3657
3874
  count: z.number().int().positive().optional().describe('apply_thread: stitch count for seed/satin.'),
3658
3875
  // THE OPTIONS THE ENGINE READS AND NOTHING NAMED. ThreadPainting.paint reads
@@ -3685,10 +3902,78 @@ export const DesignMediumInputSchema = z.object({
3685
3902
  flowField: z.enum(['hand', 'curved', 'zigzag', 'waves', 'seabed', 'spiral', 'columns']).optional()
3686
3903
  .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.'),
3687
3904
  t: z.number().optional().describe('apply_hatch: flow-field time offset — advances the field without changing the seed.'),
3905
+ // --- SDF brush stroke (FxTool D5) ---
3906
+ points: z.array(z.union([
3907
+ z.object({ x: z.number(), y: z.number(), pressure: z.number().min(0).max(1).optional() }),
3908
+ z.tuple([z.number(), z.number()]),
3909
+ z.tuple([z.number(), z.number(), z.number()]),
3910
+ ])).min(1).optional().describe('sdf_stroke: the stroke path in canvas px, each {x, y, pressure?} or [x, y, pressure?]. Pressure 0..1 scales the width at that point.'),
3911
+ softness: z.number().min(0).max(1).optional().describe('sdf_stroke: edge softness 0..1 (default 0.2).'),
3912
+ opacity: z.number().min(0).max(1).optional().describe('sdf_stroke: 0..1 (default 1).'),
3913
+ resolution: z.number().positive().optional().describe('sdf_stroke: bitmap pixels per canvas px (default 2).'),
3914
+ name: z.string().optional().describe('sdf_stroke: item name.'),
3688
3915
  })
3689
3916
  .refine((v) => v.action !== 'apply_thread' || !!v.itemId, { message: 'apply_thread requires itemId', path: ['itemId'] })
3690
3917
  .refine((v) => v.action !== 'resolve' || !!v.medium, { message: 'resolve requires medium', path: ['medium'] })
3691
- .refine((v) => v.action !== 'apply_hatch' || !!v.itemId, { message: 'apply_hatch requires itemId', path: ['itemId'] });
3918
+ .refine((v) => v.action !== 'sdf_stroke' || (Array.isArray(v.points) && v.points.length > 0), { message: 'sdf_stroke requires points', path: ['points'] })
3919
+ .refine((v) => v.action !== 'apply_hatch' || !!v.itemId, { message: 'apply_hatch requires itemId', path: ['itemId'] })
3920
+ .refine((v) => v.action !== 'apply' || (!!v.itemId && !!v.medium), { message: 'apply requires itemId and medium', path: ['medium'] })
3921
+ .refine((v) => v.action !== 'apply' || !v.medium || DESIGN_MEDIA.includes(v.medium), { message: `unknown medium; the engine knows ${DESIGN_MEDIA.join(', ')}`, path: ['medium'] });
3922
+ /**
3923
+ * pinepaper_relight — a lighting pass over the 3D world layer (FxTool D5).
3924
+ * CPU-backed; normals from the world's luminance (no depth). Applied in the
3925
+ * preview and in PNG / PDF / MP4 / WebM; not GIF / APNG; not saved with the
3926
+ * project. Lights are validated by the engine, which names every error.
3927
+ */
3928
+ const RelightColorSchema = z.union([z.string(), z.tuple([z.number(), z.number(), z.number()])]);
3929
+ export const RelightInputSchema = z.object({
3930
+ action: z.enum(['set', 'get', 'clear', 'list_lights'])
3931
+ .describe("'set' (install or replace the rig) · 'get' (the current rig) · 'clear' (remove it) · 'list_lights' (the light types)"),
3932
+ lights: z.array(z.discriminatedUnion('type', [
3933
+ z.object({
3934
+ type: z.literal('point'),
3935
+ x: z.number(), y: z.number(),
3936
+ z: z.number().optional().describe('Height above the picture, px (default 120).'),
3937
+ radius: z.number().positive().optional().describe('Falloff radius, px (default 400).'),
3938
+ color: RelightColorSchema.optional(),
3939
+ intensity: z.number().min(0).optional().describe('Default 1.'),
3940
+ specular: z.number().min(0).optional().describe('Shininess exponent (default 32).'),
3941
+ }),
3942
+ z.object({
3943
+ type: z.literal('directional'),
3944
+ dir: z.tuple([z.number(), z.number(), z.number()]).describe('[dx, dy, dz]; [0, 0, -1] faces the viewer.'),
3945
+ color: RelightColorSchema.optional(),
3946
+ intensity: z.number().min(0).optional(),
3947
+ }),
3948
+ ])).max(8).optional().describe('set: up to 8 lights. Coordinates are export-frame px. Colours are #rrggbb or [r, g, b].'),
3949
+ ambient: z.number().min(0).optional().describe('set: ambient light (default 0.2).'),
3950
+ strength: z.number().positive().optional().describe('set: normal-map strength (default 2).'),
3951
+ enabled: z.boolean().optional().describe('set: false keeps the rig but turns it off.'),
3952
+ preview: z.union([z.boolean(), z.object({ maxSide: z.number().min(64).max(4096) })]).optional()
3953
+ .describe('set: show it in the live preview (capped-resolution overlay); {maxSide} sets the cap.'),
3954
+ }).refine((v) => v.action !== 'set' || (Array.isArray(v.lights) && v.lights.length > 0) || v.enabled === false, { message: 'set requires at least one light (or enabled: false)', path: ['lights'] });
3955
+ /**
3956
+ * pinepaper_shader_graph — validated JSON pixel graphs (FxTool D5), CPU-backed.
3957
+ * Linear chains only; the engine names every error (unknown type, duplicate,
3958
+ * dangling edge, cycle, branching).
3959
+ */
3960
+ const ShaderGraphJsonSchema = z.object({
3961
+ id: z.string().optional(),
3962
+ nodes: z.array(z.object({ id: z.string(), type: z.string(), params: z.record(z.string(), z.unknown()).optional() })).min(1),
3963
+ edges: z.array(z.object({ from: z.string(), to: z.string() })).optional(),
3964
+ });
3965
+ export const ShaderGraphInputSchema = z.object({
3966
+ action: z.enum(['node_types', 'validate', 'create', 'get', 'list', 'remove', 'apply'])
3967
+ .describe("'node_types' (the node vocabulary + params) · 'validate' (check a graph, store nothing) · 'create' (validate + store) · 'get' / 'remove' (by graphId) · 'list' · 'apply' (run a graph over an item or the scene)"),
3968
+ graph: ShaderGraphJsonSchema.optional().describe('validate / create / apply: {id?, nodes: [{id, type, params?}], edges: [{from, to}]} — a linear chain.'),
3969
+ graphId: z.string().optional().describe('get / remove / apply: a stored graph id.'),
3970
+ target: z.string().optional().describe("apply: an itemId, or 'scene'."),
3971
+ keepSource: z.boolean().optional().describe('apply to an item: keep the source visible (default: hidden).'),
3972
+ asItem: z.boolean().optional().describe("apply to 'scene': add the result as an image item instead of returning it."),
3973
+ })
3974
+ .refine((v) => !['validate', 'create'].includes(v.action) || !!v.graph, { message: 'validate / create require graph', path: ['graph'] })
3975
+ .refine((v) => !['get', 'remove'].includes(v.action) || !!v.graphId, { message: 'get / remove require graphId', path: ['graphId'] })
3976
+ .refine((v) => v.action !== 'apply' || (!!v.target && (!!v.graph || !!v.graphId)), { message: "apply requires target (an itemId or 'scene') and graph or graphId", path: ['target'] });
3692
3977
  export const TextEffectInputSchema = z.object({
3693
3978
  action: z.enum(['apply', 'list'])
3694
3979
  .describe("'apply' (explode a text item into animated characters) · 'list' (the 37 effects with key/label/definition, for a picker)"),
@@ -3810,7 +4095,7 @@ export const World3DInputSchema = z.object({
3810
4095
  .describe("'create' (a preset world) · 'describe' (every parameter with type/range — CALL THIS before configure) · 'configure' (live deep-merge patch, schema-validated) · actor ops · 'set_camera' (follow|fixed|orbit) · object ops · 'remove_world' · MESH AUTHORING: 'extrude_path' / 'lathe_path' turn a canvas path into real geometry, 'list_meshes', 'remove_mesh' · 'add_light' / 'set_light' / 'remove_light' / 'list_lights' (at most 8 point lights) · 'add_material' / 'set_material' / 'remove_material' / 'list_materials'"),
3811
4096
  spec: z.union([z.string(), z.record(z.string(), z.unknown())]).optional().describe("create: a preset id ('forest', 'snowMountain', …) or a full world spec object."),
3812
4097
  character: z.union([z.boolean(), z.record(z.string(), z.unknown())]).optional().describe('create: include the walkable character (true/config).'),
3813
- patch: z.record(z.string(), z.unknown()).optional().describe('configure: partial world spec, deep-merged and validated — a wrong key errors naming the right one.'),
4098
+ patch: z.record(z.string(), z.unknown()).optional().describe('configure: partial world spec, deep-merged and validated — a wrong key errors naming the right one. Colours (env.zenith, env.horizon, env.fogColor, env.sunColor, env.ambient, env.lowColor … env.propColorB) take hex ("#03050c") or [r, g, b] in 0..1.'),
3814
4099
  actorId: z.string().optional().describe('Actor id — add_actor (optional, generated if omitted) / remove_actor / set_actor_pose / set_camera target.'),
3815
4100
  x: z.number().optional().describe('World-space x — add_actor / add_object.'),
3816
4101
  z: z.number().optional().describe('World-space z — add_actor / add_object.'),
@@ -3825,8 +4110,12 @@ export const World3DInputSchema = z.object({
3825
4110
  pathId: z.string().optional().describe("extrude_path / lathe_path: the canvas path to turn into geometry. Extrude sweeps it along depth; lathe revolves its profile around the Y axis. The path is unchanged — the mesh is new."),
3826
4111
  mesh: z.object({
3827
4112
  id: z.string().optional().describe('Mesh id (generated if omitted).'),
3828
- depth: z.number().optional().describe('extrude_path: how far to sweep, in world units.'),
3829
- unitsPerPixel: z.number().optional().describe('Canvas pixels → world units. The whole scale relationship between the drawing and the world.'),
4113
+ depth: z.number().optional().describe('extrude_path: how far to sweep, in WORLD units (default 1). Not pixels: the outline is canvas px × unitsPerPixel.'),
4114
+ unitsPerPixel: z.number().optional().describe('Canvas pixels → world units (default 0.01, so a 200 px outline is 2 world units). The whole scale relationship between the drawing and the world.'),
4115
+ // FxTool #49 (7c50c838). Declared here, or z.object() strips them before
4116
+ // the emitter's spread can pass them on (the 5.64 colour bug, again).
4117
+ upright: z.boolean().optional().describe('extrude_path: stand the drawing up (canvas up = world up, extruded along Z) — a sign, a letter, a cut-out. Default false: flat, a floor plan in XZ extruded along Y.'),
4118
+ placement: z.enum(['centered', 'canvas']).optional().describe("extrude_path: 'centered' (default) centres the mesh on x/z with its BASE at y, so it stands on the ground at the point you give. 'canvas' keeps the outline's canvas position (× unitsPerPixel), the older behaviour."),
3830
4119
  caps: z.boolean().optional().describe('extrude_path: close the two ends. A capped extrude is a solid; an uncapped one is a ribbon.'),
3831
4120
  segments: z.number().optional().describe('extrude_path: segments along the sweep · lathe_path: segments around the revolution.'),
3832
4121
  arc: z.number().optional().describe('lathe_path: how far round to revolve, in degrees. 360 is a full solid of revolution; less leaves it open.'),
@@ -3853,7 +4142,7 @@ export const World3DInputSchema = z.object({
3853
4142
  x: z.number().optional().describe('World-space x (default 0).'),
3854
4143
  y: z.number().optional().describe('World-space y (default 3 — above the ground, not on it).'),
3855
4144
  z: z.number().optional().describe('World-space z (default 0).'),
3856
- color: z.array(z.number()).length(3).optional().describe('Linear RGB, each component ≥ 0 (default [1, 0.86, 0.62], a warm lamp).'),
4145
+ color: z.union([z.string(), z.array(z.number()).length(3)]).optional().describe('Hex ("#ffdca0") or linear RGB [r, g, b], each component ≥ 0 and may exceed 1 for a brighter light (default [1, 0.86, 0.62], a warm lamp).'),
3857
4146
  intensity: z.number().optional().describe('Brightness (default 6).'),
3858
4147
  range: z.number().optional().describe('How far the light reaches, in world units (default 20).'),
3859
4148
  }).optional().describe('add_light / set_light: the light spec. At most 8 point lights exist at once — the shader array is fixed-size, so a ninth is refused by name rather than ignored.'),
@@ -3861,26 +4150,26 @@ export const World3DInputSchema = z.object({
3861
4150
  // --- Materials ------------------------------------------------------------
3862
4151
  material: z.object({
3863
4152
  id: z.string().optional().describe('Material id (generated if omitted).'),
3864
- color: z.array(z.number()).length(3).optional().describe('Base colour, linear RGB (default [0.8, 0.8, 0.82]).'),
3865
- emissive: z.array(z.number()).length(3).optional().describe('Light the surface emits regardless of the scene (default [0, 0, 0]).'),
4153
+ color: z.union([z.string(), z.array(z.number()).length(3)]).optional().describe('Base colour: hex ("#e9e4d8") or linear RGB [r, g, b] in 0..1 (default [0.8, 0.8, 0.82]).'),
4154
+ emissive: z.union([z.string(), z.array(z.number()).length(3)]).optional().describe('Light the surface emits regardless of the scene: hex or [r, g, b] in 0..1 (default [0, 0, 0]).'),
3866
4155
  metalness: z.number().min(0).max(1).optional().describe('0 dielectric → 1 metal.'),
3867
4156
  roughness: z.number().min(0).max(1).optional().describe('0 mirror → 1 fully diffuse. Floored at 0.045: a true zero is a numerically unstable mirror.'),
3868
4157
  emissiveIntensity: z.number().min(0).optional().describe('Scales `emissive` before the frame is built, so it reaches every path that draws emissive.'),
3869
4158
  clearcoat: z.number().min(0).max(1).optional().describe('A lacquer layer over the base — car paint, varnished wood.'),
3870
4159
  clearcoatRoughness: z.number().min(0).max(1).optional().describe("The coat's own roughness. NOT floored like the base, so a mirror-smooth lacquer stays expressible."),
3871
- sheenColor: z.array(z.number()).length(3).optional().describe('Retroreflective sheen colour — cloth, velvet.'),
4160
+ sheenColor: z.union([z.string(), z.array(z.number()).length(3)]).optional().describe('Retroreflective sheen colour — cloth, velvet: hex or [r, g, b] in 0..1.'),
3872
4161
  sheenRoughness: z.number().min(0).max(1).optional().describe('How broad the sheen lobe is.'),
3873
4162
  }).optional().describe("add_material / set_material: a NAMED, SHARED surface referenced by many objects, so one edit restyles all of them. metalness and roughness reach the shader on the MESH path only. There are no aoMapIntensity / normalScale / envMapIntensity knobs: each scales a map that does not exist, and a knob that scales nothing is worse than a missing one."),
3874
4163
  materialId: z.string().optional().describe('set_material / remove_material: the material id.'),
3875
4164
  // --- Import ---------------------------------------------------------------
3876
- source: z.string().optional().describe('import_obj: the OBJ file TEXT. import_gltf: a URL, or a data: URI of the .glb bytes.'),
4165
+ source: z.string().optional().describe('import_obj: the OBJ file TEXT. import_gltf: the .gltf JSON TEXT with its buffers embedded. A URL, data: URI or file path is NOT fetched (the string is parsed as JSON), so a .glb cannot be imported here.'),
3877
4166
  importOptions: z.record(z.string(), z.unknown()).optional().describe('import_obj / import_gltf: placement and scale options, passed through to the engine.'),
3878
4167
  clip: z.string().optional().describe('set_mesh_clip: which imported animation clip a skinned mesh plays.'),
3879
4168
  crossfade: z.number().optional().describe('set_mesh_clip: seconds to blend out of the clip that was playing. The blend happens in LOCAL pose space — a palette cannot be blended after the fact.'),
3880
4169
  // --- Navigation, picking and the camera ----------------------------------
3881
4170
  navTarget: z.enum(['2d', '3d']).optional().describe("set_nav_target: which layer the pointer drives — '2d' the Paper canvas, '3d' the world."),
3882
- point: z.object({ x: z.number(), y: z.number(), z: z.number().optional() }).optional()
3883
- .describe('ground_height / canvas_to_ground / world_to_canvas: the point to convert. ground_height and canvas_to_ground read x and y (canvas) or x and z (world); world_to_canvas takes a world point.'),
4171
+ point: z.object({ x: z.number(), y: z.number().optional(), z: z.number().optional() }).optional()
4172
+ .describe('The point to convert. ground_height: {x, z} in WORLD coordinates (the terrain height there; y is read as z if z is absent). canvas_to_ground: {x, y} in CANVAS pixels. world_to_canvas: {x, y, z?} in world coordinates.'),
3884
4173
  origin: z.array(z.number()).length(3).optional().describe('raycast: ray origin in world space, [x, y, z].'),
3885
4174
  direction: z.array(z.number()).length(3).optional().describe('raycast: ray direction in world space, [x, y, z]. Need not be normalised.'),
3886
4175
  multiplier: z.number().optional().describe('dolly_camera: greater than 1 moves away, less than 1 moves closer.'),
@@ -3898,6 +4187,10 @@ export const World3DInputSchema = z.object({
3898
4187
  .describe('set_mesh_instances: one entry per copy, six numbers each — x, y, z, scale, rotY (RADIANS), and a free variant a shader may read for per-instance colour or phase.'),
3899
4188
  })
3900
4189
  .refine((v) => v.action !== 'configure' || !!v.patch, { message: 'configure requires patch', path: ['patch'] })
4190
+ // ground_height reads a WORLD (x, z); y is accepted in z's place for older callers.
4191
+ .refine((v) => v.action !== 'ground_height' || (!!v.point && (v.point.z !== undefined || v.point.y !== undefined)), { message: 'ground_height requires point {x, z} (world coordinates)', path: ['point'] })
4192
+ .refine((v) => v.action !== 'canvas_to_ground' || (!!v.point && v.point.y !== undefined), { message: 'canvas_to_ground requires point {x, y} (canvas coordinates)', path: ['point'] })
4193
+ .refine((v) => v.action !== 'world_to_canvas' || (!!v.point && v.point.y !== undefined), { message: 'world_to_canvas requires point {x, y, z?} (world coordinates)', path: ['point'] })
3901
4194
  .refine((v) => !['remove_actor', 'set_actor_pose'].includes(v.action) || !!v.actorId, { message: 'this action requires actorId', path: ['actorId'] })
3902
4195
  .refine((v) => v.action !== 'set_actor_pose' || !!v.pose, { message: 'set_actor_pose requires pose', path: ['pose'] })
3903
4196
  .refine((v) => v.action !== 'set_camera' || !!v.camera, { message: 'set_camera requires camera', path: ['camera'] })
@@ -3982,7 +4275,7 @@ export const DesignSystemInputSchema = z.object({
3982
4275
  export const StickInputSchema = z.object({
3983
4276
  action: z.enum(['figure', 'set']).describe("'figure' a posed, walking or travelling stick figure · 'set' the floor, wall and objects it stands on and among."),
3984
4277
  id: z.string().optional().describe('Prefix for the created items.'),
3985
- kind: z.string().optional().describe("figure: which figure. set: which piece of the scene kit ('floor', 'wall', a named object) — list-free, because the kit is vendored and grows there."),
4278
+ kind: z.string().optional().describe("figure: which figure. set: which piece — room (floor + wall; options groundY, wall, floor), tabletop (object + surfaceY), chair (facing), table, counter, door, shelf (items), window. An unknown kind or tabletop object is refused with the engine's current list."),
3986
4279
  at: z.object({ x: z.number().optional(), y: z.number().optional() }).optional().describe('Where it goes.'),
3987
4280
  scale: z.number().optional().describe('Size multiplier.'),
3988
4281
  facing: z.enum(['left', 'right']).optional().describe("set: which way a CHAIR faces. The figure has no facing — it is ignored on action 'figure', which refuses it by name rather than dropping it."),
@@ -4048,7 +4341,7 @@ export const InterchangeInputSchema = z.object({
4048
4341
  .describe("'export_lottie' (JSON) · 'export_dotlottie' (the zipped .lottie) · 'import_lottie' (a Lottie or .lottie back onto the canvas) · 'export_glb' (the perspective 3D objects — needs some to exist) · 'export_bvh' (a skeleton's motion — needs a rig) · 'export_png_sequence' (one file per frame)."),
4049
4342
  options: z.record(z.string(), z.unknown()).optional().describe('Format options, passed through to the exporter: frame range, dimensions, fps, naming.'),
4050
4343
  data: z.union([z.string(), z.record(z.string(), z.unknown())]).optional()
4051
- .describe('import_lottie: the Lottie JSON — an object, a JSON string, or a URL.'),
4344
+ .describe('import_lottie: the Lottie JSON — an object or a JSON string. A URL or a file path is NOT fetched: read the file and pass its contents.'),
4052
4345
  skeletonId: z.string().optional().describe('export_bvh: which skeleton to write. A BVH is one skeleton\'s motion, so this is not optional in practice.'),
4053
4346
  })
4054
4347
  .refine((v) => v.action !== 'import_lottie' || v.data !== undefined, { message: 'import_lottie requires data', path: ['data'] })
@@ -4144,7 +4437,7 @@ export const SoundInputSchema = z.object({
4144
4437
  * pinepaper_motion — the generators' Animation knob, pointed at anything.
4145
4438
  *
4146
4439
  * The same engine every generator's own animation runs on, reachable for any
4147
- * group or list of items. Two kinds: a GROUP motion (drift, sway, rotate,
4440
+ * group or list of items. Two kinds: a GROUP motion (driftX/Y, swayX/Y, rotate,
4148
4441
  * pulse, wave, bounce) moves the target as one; a FIELD motion (ripple,
4149
4442
  * breathe, undulate) sweeps a crest through the children from an origin, with
4150
4443
  * a chosen waveform. The field kind is the one nothing else here can do.
@@ -4157,7 +4450,7 @@ export const MotionInputSchema = z.object({
4157
4450
  action: z.enum(['list', 'apply']).describe("'list' every motion, waveform and origin the engine offers · 'apply' one to a target."),
4158
4451
  itemId: z.string().optional().describe('apply: a group or single item to move.'),
4159
4452
  itemIds: z.array(z.string()).optional().describe('apply: several items, wrapped in a new group that becomes the motion host. The members keep their own identity and ids.'),
4160
- motion: z.string().optional().describe("apply: the motion name — a GROUP motion (drift, sway, rotate, pulse, wave, bounce) moves the target as one; a FIELD motion (ripple, breathe, undulate) sweeps a crest through its children. Call 'list' for the engine's own set rather than guessing."),
4453
+ motion: z.string().optional().describe("apply: the motion name — a GROUP motion (driftX, driftY, swayX, swayY, rotate, pulse, wave, bounce) moves the target as one; a FIELD motion (ripple, breathe, undulate) sweeps a crest through its children. Call 'list' for the engine's own set rather than guessing."),
4161
4454
  speed: z.number().optional().describe('apply: cycles per second, roughly. Higher is faster.'),
4162
4455
  intensity: z.number().optional().describe('apply: amplitude. 0.15 is the usual default elsewhere in this surface.'),
4163
4456
  waveform: z.enum(['sine', 'triangle', 'square', 'sawtooth', 'spike']).optional()
@@ -4356,7 +4649,8 @@ export const RiggingInputSchema = z.object({
4356
4649
  duration: z.number().positive().optional().describe('Seconds to traverse the path (default 1) — set_target_path.'),
4357
4650
  loop: z.boolean().optional().describe('Cycle the path — set_target_path.'),
4358
4651
  })
4359
- .refine((v) => ['create_skeleton', 'import_bvh', 'import_spine'].includes(v.action) || !!v.skeletonId, { message: 'this action requires skeletonId', path: ['skeletonId'] })
4652
+ // Listing ALL skeletons or pose libraries needs no skeleton (gate D6, 1.6.19).
4653
+ .refine((v) => ['create_skeleton', 'import_bvh', 'import_spine', 'list_skeletons', 'list_pose_libraries'].includes(v.action) || !!v.skeletonId, { message: 'this action requires skeletonId', path: ['skeletonId'] })
4360
4654
  .refine((v) => v.action !== 'add_bone' || v.skeletonId != null, { message: 'add_bone requires skeletonId', path: ['skeletonId'] })
4361
4655
  .refine((v) => v.action !== 'attach_item' || (!!v.boneId && !!v.itemId), { message: 'attach_item requires boneId and itemId', path: ['boneId'] })
4362
4656
  .refine((v) => v.action !== 'create_ik_chain' || (!!v.boneIds && v.boneIds.length >= 2), { message: 'create_ik_chain requires boneIds (≥2)', path: ['boneIds'] })