@pinepaper.studio/mcp-server 1.6.13 → 1.6.14

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 (87) 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 +179 -4
  8. package/README.pt-BR.md +1 -1
  9. package/README.zh-CN.md +1 -1
  10. package/dist/build-info.json +5 -0
  11. package/dist/design/design-systems.js +1 -1
  12. package/dist/design/design-systems.js.map +1 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +3 -2
  15. package/dist/index.js.map +1 -1
  16. package/dist/ontology/upstream-vocabulary.d.ts +23 -0
  17. package/dist/ontology/upstream-vocabulary.d.ts.map +1 -0
  18. package/dist/ontology/upstream-vocabulary.js +1172 -0
  19. package/dist/ontology/upstream-vocabulary.js.map +1 -0
  20. package/dist/ontology/vocabulary.d.ts.map +1 -1
  21. package/dist/ontology/vocabulary.js +19 -0
  22. package/dist/ontology/vocabulary.js.map +1 -1
  23. package/dist/tools/definitions.d.ts.map +1 -1
  24. package/dist/tools/definitions.js +263 -70
  25. package/dist/tools/definitions.js.map +1 -1
  26. package/dist/tools/engine-methods.d.ts.map +1 -1
  27. package/dist/tools/engine-methods.js +23 -2
  28. package/dist/tools/engine-methods.js.map +1 -1
  29. package/dist/tools/engine-surface.d.ts +1 -1
  30. package/dist/tools/engine-surface.d.ts.map +1 -1
  31. package/dist/tools/engine-surface.js +6 -5
  32. package/dist/tools/engine-surface.js.map +1 -1
  33. package/dist/tools/handlers/camera.d.ts.map +1 -1
  34. package/dist/tools/handlers/camera.js +17 -2
  35. package/dist/tools/handlers/camera.js.map +1 -1
  36. package/dist/tools/handlers/font.d.ts.map +1 -1
  37. package/dist/tools/handlers/font.js +48 -3
  38. package/dist/tools/handlers/font.js.map +1 -1
  39. package/dist/tools/handlers/maps.d.ts.map +1 -1
  40. package/dist/tools/handlers/maps.js +16 -0
  41. package/dist/tools/handlers/maps.js.map +1 -1
  42. package/dist/tools/handlers.d.ts +11 -1
  43. package/dist/tools/handlers.d.ts.map +1 -1
  44. package/dist/tools/handlers.js +426 -14
  45. package/dist/tools/handlers.js.map +1 -1
  46. package/dist/tools/minimal-descriptions.d.ts.map +1 -1
  47. package/dist/tools/minimal-descriptions.js +2 -0
  48. package/dist/tools/minimal-descriptions.js.map +1 -1
  49. package/dist/tools/shape-params.d.ts +13 -2
  50. package/dist/tools/shape-params.d.ts.map +1 -1
  51. package/dist/tools/shape-params.js +80 -2
  52. package/dist/tools/shape-params.js.map +1 -1
  53. package/dist/tools/stick-vocabulary.d.ts +1 -1
  54. package/dist/tools/stick-vocabulary.js +1 -1
  55. package/dist/tools/toolkits.d.ts.map +1 -1
  56. package/dist/tools/toolkits.js +2 -0
  57. package/dist/tools/toolkits.js.map +1 -1
  58. package/dist/types/code-generator.d.ts +27 -1
  59. package/dist/types/code-generator.d.ts.map +1 -1
  60. package/dist/types/code-generator.js +2454 -273
  61. package/dist/types/code-generator.js.map +1 -1
  62. package/dist/types/schemas.d.ts +1478 -239
  63. package/dist/types/schemas.d.ts.map +1 -1
  64. package/dist/types/schemas.js +255 -26
  65. package/dist/types/schemas.js.map +1 -1
  66. package/dist/utils/a11y.d.ts +67 -0
  67. package/dist/utils/a11y.d.ts.map +1 -0
  68. package/dist/utils/a11y.js +117 -0
  69. package/dist/utils/a11y.js.map +1 -0
  70. package/dist/utils/ad-package.d.ts +44 -0
  71. package/dist/utils/ad-package.d.ts.map +1 -0
  72. package/dist/utils/ad-package.js +121 -0
  73. package/dist/utils/ad-package.js.map +1 -0
  74. package/dist/utils/scc.d.ts +41 -0
  75. package/dist/utils/scc.d.ts.map +1 -0
  76. package/dist/utils/scc.js +185 -0
  77. package/dist/utils/scc.js.map +1 -0
  78. package/dist/utils/zip.d.ts +7 -0
  79. package/dist/utils/zip.d.ts.map +1 -0
  80. package/dist/utils/zip.js +82 -0
  81. package/dist/utils/zip.js.map +1 -0
  82. package/dist/version.d.ts +3 -0
  83. package/dist/version.d.ts.map +1 -1
  84. package/dist/version.js +31 -1
  85. package/dist/version.js.map +1 -1
  86. package/manifest.json +9 -1
  87. package/package.json +6 -4
@@ -1038,7 +1038,10 @@ export const BlastParamsSchema = z.object({
1038
1038
  // =============================================================================
1039
1039
  // FILTER TYPES
1040
1040
  // =============================================================================
1041
- export const FilterTypeSchema = z.enum([
1041
+ // 'saturate' is what the published JSON schema offered for releases, while
1042
+ // the engine registers 'saturation' and this enum rejected 'saturate' — a
1043
+ // 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([
1042
1045
  'grayscale',
1043
1046
  'sepia',
1044
1047
  'blur',
@@ -1053,7 +1056,7 @@ export const FilterTypeSchema = z.enum([
1053
1056
  'sharpen',
1054
1057
  'emboss',
1055
1058
  'posterize',
1056
- ]).describe('Type of visual filter to apply');
1059
+ ])).describe('Type of visual filter to apply');
1057
1060
  // =============================================================================
1058
1061
  // TOOL INPUT SCHEMAS
1059
1062
  // =============================================================================
@@ -1087,6 +1090,14 @@ export const AddFilterInputSchema = z.object({
1087
1090
  params: z.record(z.unknown()).optional().default({}),
1088
1091
  });
1089
1092
  // Create Item
1093
+ // Item data flags. Advertised on create_item and modify_item for releases while
1094
+ // z.object() stripped them, so {data: {selectable: false}} was accepted and
1095
+ // did nothing. The engine reads all three off item.data.
1096
+ export const ItemDataFlagsSchema = z.object({
1097
+ selectable: z.boolean().optional(),
1098
+ isDraggable: z.boolean().optional(),
1099
+ isDecorative: z.boolean().optional(),
1100
+ });
1090
1101
  export const CreateItemInputSchema = z.object({
1091
1102
  itemType: ItemTypeSchema,
1092
1103
  position: PositionSchema.optional().default({ x: 400, y: 300 }),
@@ -1096,6 +1107,11 @@ export const CreateItemInputSchema = z.object({
1096
1107
  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.'),
1097
1108
  animationDelay: z.number().optional().describe('Loop animation start delay in seconds.'),
1098
1109
  keyframes: z.array(KeyframeSchema).optional().describe('Required when animationType is "keyframe". Inline keyframe array attached at creation.'),
1110
+ data: ItemDataFlagsSchema.optional(),
1111
+ // Also accepted at the top level: it names which point `position` is, so it
1112
+ // is read beside position as often as inside properties — and at the top
1113
+ // level it was stripped without a word (round 7 retest, 1.63).
1114
+ anchor: z.string().optional().describe("Which point position names: 'center' (default), 'top-left', 'top-right', 'bottom-left', 'bottom-right'. Same as properties.anchor."),
1099
1115
  });
1100
1116
  // Light direction for 3D effects
1101
1117
  export const LightDirectionSchema = z.enum([
@@ -1132,6 +1148,7 @@ export const CreateDiagonalStripesInputSchema = z.object({
1132
1148
  export const ModifyItemInputSchema = z.object({
1133
1149
  itemId: z.string().describe('Registry ID of the item'),
1134
1150
  properties: z.record(z.unknown()).describe('Properties to update'),
1151
+ data: ItemDataFlagsSchema.optional(),
1135
1152
  });
1136
1153
  // Delete Item
1137
1154
  export const DeleteItemInputSchema = z.object({
@@ -1254,15 +1271,43 @@ export const ImportMotionCaptureInputSchema = z.object({
1254
1271
  //
1255
1272
  // Region (added FxTool c81781c): when set, the generator draws into a clipped
1256
1273
  // sub-group translated to (x,y) sized (width × height). Region runs imply
1257
- // preserve (won't wipe the canvas) and re-running the same generator replaces
1258
- // its prior region. Different generators' regions coexist. Invalid/zero-size
1274
+ // preserve (won't wipe the canvas). An instance is generator + box, or an
1275
+ // explicit region.id (FxTool 5adfcf1c): the same box or id replaces, another
1276
+ // box adds an instance. (Before 5adfcf1c a second box silently replaced the
1277
+ // first — "drawWaves in a roundedRect draws nothing".) Invalid/zero-size
1259
1278
  // regions fall back to full-canvas FxTool-side.
1279
+ //
1280
+ // Shape (FxTool c68524a2): the clip may be a rounded rect, ellipse, circle,
1281
+ // polygon, star, or an existing path's own geometry ({itemId}, whose bounds
1282
+ // are the box when none is given).
1283
+ export const GENERATOR_REGION_SHAPES = ['rect', 'roundedRect', 'ellipse', 'circle', 'polygon', 'star'];
1260
1284
  export const GeneratorRegionSchema = z.object({
1261
- x: z.number().describe('Region top-left X in canvas coords'),
1262
- y: z.number().describe('Region top-left Y in canvas coords'),
1263
- width: z.number().positive().describe('Region width in pixels'),
1264
- height: z.number().positive().describe('Region height in pixels'),
1265
- }).describe('Optional sub-region {x, y, width, height} the generator draws into.');
1285
+ x: z.number().optional().describe('Region top-left X in canvas coords'),
1286
+ y: z.number().optional().describe('Region top-left Y in canvas coords'),
1287
+ width: z.number().positive().optional().describe('Region width in pixels'),
1288
+ height: z.number().positive().optional().describe('Region height in pixels'),
1289
+ shape: z.union([z.enum(GENERATOR_REGION_SHAPES), z.object({ itemId: z.string().min(1) })]).optional()
1290
+ .describe("Clip shape: 'rect' (default), 'roundedRect' (+radius), 'ellipse', 'circle', 'polygon' (+sides), 'star' (+points, innerRatio), or {itemId} — clip to that path's geometry (the box defaults to its bounds)."),
1291
+ radius: z.number().min(0).optional().describe("roundedRect corner radius (default 20% of the shorter side)."),
1292
+ sides: z.number().int().min(3).max(64).optional().describe('polygon sides (default 6).'),
1293
+ points: z.number().int().min(3).max(64).optional().describe('star points (default 5).'),
1294
+ innerRatio: z.number().min(0.05).max(0.95).optional().describe('star inner radius as a fraction of the outer (default 0.5).'),
1295
+ id: z.string().trim().min(1).max(64).optional().describe('Names this region instance. Without it an instance is the generator plus its box: the same box replaces, a different box adds another. With it, re-running under the same id replaces that instance wherever its box now is.'),
1296
+ }).superRefine((r, ctx) => {
1297
+ const box = [r.x, r.y, r.width, r.height].filter((v) => v !== undefined).length;
1298
+ const fromItem = typeof r.shape === 'object';
1299
+ if (box !== 4 && !(fromItem && box === 0)) {
1300
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: fromItem
1301
+ ? 'give all of x, y, width, height, or none of them (the region is then the item\'s bounds).'
1302
+ : 'a region needs x, y, width and height — unless shape is {itemId}, which takes the item\'s bounds.' });
1303
+ }
1304
+ const own = { radius: 'roundedRect', sides: 'polygon', points: 'star', innerRatio: 'star' };
1305
+ for (const [k, shape] of Object.entries(own)) {
1306
+ if (r[k] !== undefined && r.shape !== shape) {
1307
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: [k], message: `${k} applies to shape '${shape}' only.` });
1308
+ }
1309
+ }
1310
+ }).describe('Optional sub-region the generator draws into, clipped to a shape.');
1266
1311
  export const ExecuteGeneratorInputSchema = z.object({
1267
1312
  generatorName: GeneratorNameSchema,
1268
1313
  params: z.record(z.unknown()).optional().default({}),
@@ -1321,8 +1366,13 @@ export const SetBackgroundColorInputSchema = z.object({
1321
1366
  color: SolidColorSchema.describe('Background color to set (solid colors only)'),
1322
1367
  });
1323
1368
  export const SetCanvasSizeInputSchema = z.object({
1324
- width: z.number().min(100).max(4096).describe('Canvas width (100-4096)'),
1325
- height: z.number().min(100).max(4096).describe('Canvas height (100-4096)'),
1369
+ // 8192, not 4096: the artboard tool takes any size and a 5400x1350 carousel
1370
+ // panorama exported exactly, while this refused it. 8192 stays well inside
1371
+ // the browser's per-side canvas limit.
1372
+ // The refusal NAMES the route: a 728x90 banner refused with a bare "too
1373
+ // small" left a caller to export the preset instead (round 8 BB, 1.48).
1374
+ 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(8192).describe('Canvas width (100-8192)'),
1375
+ 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(8192).describe('Canvas height (100-8192)'),
1326
1376
  preset: z.string().optional().describe('Optional preset name'),
1327
1377
  });
1328
1378
  // Export
@@ -1432,7 +1482,10 @@ export const SceneItemSchema = z.object({
1432
1482
  /** Temporary ID for referencing in relations (e.g., "sun", "earth") */
1433
1483
  name: z.string().describe('Reference name for this item (used in relations)'),
1434
1484
  /** Item type to create */
1435
- itemType: z.enum(['text', 'circle', 'star', 'rectangle', 'triangle', 'polygon', 'ellipse', 'path', 'line', 'arc']),
1485
+ // The canonical list, not a subset. This declared ten types while the tool
1486
+ // hand-built four and turned the other six into a 30px circle; now that it
1487
+ // routes through app.create, every type the engine has works here.
1488
+ itemType: ItemTypeSchema,
1436
1489
  /** Position on canvas */
1437
1490
  position: PositionSchema.optional(),
1438
1491
  /** Item-specific properties */
@@ -1444,7 +1497,11 @@ export const SceneRelationSchema = z.object({
1444
1497
  /** Reference name of the target item */
1445
1498
  target: z.string().describe('Name of the item to relate to'),
1446
1499
  /** Type of relation */
1447
- type: z.enum(['orbits', 'follows', 'attached_to', 'maintains_distance', 'points_at', 'mirrors', 'parallax', 'bounds_to']),
1500
+ // THE DOCS LISTED ~90 AND THIS ACCEPTED 8. A caller following the relation
1501
+ // catalogue — grows_from, say — was rejected by validation before the call
1502
+ // was made, with nothing to suggest the tool simply carried a smaller list
1503
+ // than the documentation it shares. The catalogue is the contract.
1504
+ type: RelationTypeSchema,
1448
1505
  /** Relation-specific parameters */
1449
1506
  params: z.record(z.unknown()).optional().describe('Relation parameters (radius, speed, distance, etc.)'),
1450
1507
  });
@@ -1455,6 +1512,7 @@ export const SceneAnimationSchema = z.object({
1455
1512
  type: z.enum(['bounce', 'breathe', 'fade', 'glow', 'jelly', 'path', 'pulse', 'rotate', 'scrollDown', 'scrollLeft', 'scrollRight', 'scrollUp', 'shake', 'slideLeftRight', 'slideUpDown', 'swing', 'typewriter', 'wobble']),
1456
1513
  /** Animation speed (default: 1.0) */
1457
1514
  speed: z.number().optional(),
1515
+ startTime: z.number().min(0).optional().describe('Seconds before this animation begins. These are ambient LOOPS, so this staggers when each one starts rather than scheduling a one-shot — which is what makes a scene read as choreographed instead of everything moving at once from frame zero.'),
1458
1516
  /** Animation parameters */
1459
1517
  params: z.record(z.unknown()).optional().describe('Animation-specific parameters'),
1460
1518
  });
@@ -1822,12 +1880,31 @@ export const AgentPlatformSchema = z.enum([
1822
1880
  'web',
1823
1881
  'print-a4',
1824
1882
  'print-letter',
1883
+ // Landscape print (round 8 DD, 8.18): the engine has a4-landscape and
1884
+ // letter-landscape at 300 dpi, but no platform reached them, so a
1885
+ // landscape print job had no preset carrying the print DPI.
1886
+ 'print-a4-landscape',
1887
+ 'print-letter-landscape',
1825
1888
  ]).describe('Target platform for export');
1826
1889
  /**
1827
1890
  * Export format types
1828
1891
  */
1829
1892
  export const AgentExportFormatSchema = z.enum([
1830
1893
  'svg', 'png', 'gif', 'mp4', 'webm', 'pdf',
1894
+ // STILLS UNDER A BYTE BUDGET. Ad specs cap an image at ~150 KB, and PNG could
1895
+ // not get there. Rendered as the png path (region included) and re-encoded in
1896
+ // the page at the quality tier's compression. jpg has no alpha — see the tool text.
1897
+ 'jpg', 'webp',
1898
+ // ANIMATION WITH FULL ALPHA (8.12): stepped PNG frames. webm cannot carry
1899
+ // alpha with the studio's muxer, and gif's transparency is 1-bit.
1900
+ 'apng',
1901
+ // ADS (8.21 / 8.24): the studio's standalone widget page, wrapped for a
1902
+ // display network (a zip with ad.size + clickTag, and a backup image) or as
1903
+ // an MRAID playable (one HTML file, CTA through mraid.open).
1904
+ 'html5-ad', 'playable',
1905
+ // CAPTIONS: text items staged with bornAt / ttl, as a subtitle file —
1906
+ // scc is CEA-608 (broadcast, 29.97 drop-frame).
1907
+ 'srt', 'vtt', 'scc',
1831
1908
  // AUDIO-ONLY. The soundtrack on its own, with no frames rendered — so
1832
1909
  // platform dimensions, framing and quality do not apply to it, and no
1833
1910
  // platform preset resolves to it. It has to be asked for by name.
@@ -1836,9 +1913,10 @@ export const AgentExportFormatSchema = z.enum([
1836
1913
  /**
1837
1914
  * Screenshot policy for agent jobs
1838
1915
  */
1839
- export const AgentScreenshotPolicySchema = z.enum([
1840
- 'none', 'on_error', 'on_complete', 'on_request',
1841
- ]).describe('When to take screenshots during agent job');
1916
+ // 'never' is what the published JSON schema offered for releases while this
1917
+ // enum rejected it, so a caller who followed the docs got a validation error.
1918
+ // Accepted and read as 'none'.
1919
+ export const AgentScreenshotPolicySchema = z.preprocess((v) => (v === 'never' ? 'none' : v), z.enum(['none', 'on_error', 'on_complete', 'on_request'])).describe('When to take screenshots during agent job');
1842
1920
  /**
1843
1921
  * Start job input schema
1844
1922
  */
@@ -1861,6 +1939,10 @@ export const AgentStartJobInputSchema = z.object({
1861
1939
  */
1862
1940
  export const AgentEndJobInputSchema = z.object({
1863
1941
  takeScreenshot: z.boolean().optional().default(true).describe('Take final screenshot'),
1942
+ // The JSON schema has always published this name, and z.object() stripped it,
1943
+ // so includeScreenshot:false still took a screenshot. It wins when given —
1944
+ // see generateAgentEndJob.
1945
+ includeScreenshot: z.boolean().optional(),
1864
1946
  analyzeContent: z.boolean().optional().default(true).describe('Analyze content for export recommendations'),
1865
1947
  }).describe('Options for ending an agent job');
1866
1948
  /**
@@ -1914,8 +1996,8 @@ export const AgentBatchOperationSchema = z.object({
1914
1996
  generatorParams: z.record(z.unknown()).optional().describe('Generator parameters'),
1915
1997
  generatorRegion: GeneratorRegionSchema.optional().describe('Optional sub-region {x, y, width, height} for execute_generator'),
1916
1998
  // Set canvas size fields
1917
- width: z.number().min(100).max(4096).optional().describe('Canvas width for set_canvas_size (100-4096)'),
1918
- height: z.number().min(100).max(4096).optional().describe('Canvas height for set_canvas_size (100-4096)'),
1999
+ width: z.number().min(100).max(8192).optional().describe('Canvas width for set_canvas_size (100-8192)'),
2000
+ height: z.number().min(100).max(8192).optional().describe('Canvas height for set_canvas_size (100-8192)'),
1919
2001
  preset: z.string().optional().describe('Canvas preset for set_canvas_size (e.g. instagram, youtube)'),
1920
2002
  // Keyframe animate fields
1921
2003
  keyframes: z.array(z.object({
@@ -2061,12 +2143,100 @@ export const AgentExportInputSchema = z.object({
2061
2143
  framing: z.enum(['canvas', 'camera']).optional().default('canvas').describe('Output framing: "canvas" (full canvas, default) or "camera" (camera_animates first-keyframe viewport — fails if no walkthrough exists). Camera animation still drives motion within the fixed output frame.'),
2062
2144
  duration: z.number().min(0.5).max(VIDEO_MAX_DURATION_S).optional().default(5).describe(`Video duration in seconds for animated formats (mp4/webm/gif). Default 5. Max ${VIDEO_MAX_DURATION_S} for mp4/webm, max ${GIF_MAX_DURATION_S} for gif (GIF is not codec-bounded, so file size scales with frames × dimensions, and it cannot stream to the export store). Static formats (png/svg/pdf) ignore this. Past roughly a minute the export is held in the studio's export store and paged back to a file rather than returned inline; a studio without that store refuses by name and says so.`),
2063
2145
  estimateOnly: z.boolean().optional().default(false).describe('Preflight only: return the estimated file size AND what this scene would lose to this format, rendering nothing. Use before a long or high-quality export to check the size first. Modeled for mp4/webm/gif; png/pdf/svg return confidence "none" because no dimension-based model exists for them; wav is reported as "exact", because uncompressed PCM size is arithmetic rather than a guess.'),
2064
- fps: z.number().int().min(1).max(120).optional().describe('Video only: frames per second, overriding whatever `quality` implies. The tiers set it — draft 15, standard 30, high 60 — so asking for higher quality DOUBLES the frame count and the render time unless you say otherwise. File size is bitrate x duration and does not move with fps, but the picture gets fewer bits per frame at a higher one.'),
2065
- scale: z.number().min(0.1).max(1).optional().describe('Video only: render at this fraction of the platform preset\'s dimensions (0.1-1). The engine derives its encode target from RESOLUTION, so this is the size control — there is no bitrate to set, and halving the frame roughly quarters the pixels and the file. It is also the preview knob: scale 0.5 with quality "draft" is the fast look-check before committing to a full render. Rounded to even dimensions, which H.264 requires.'),
2146
+ // NTSC rates (round 9 FF, 1.75): CTV / OTT spots require 29.97 and it was
2147
+ // refused as "Expected integer", though the engine exports true 30000/1001.
2148
+ // A rational string ('30000/1001') or the NTSC decimals snap to the exact
2149
+ // rational — 29.97 is 30000/1001, not the float 29.97, whose drift over a
2150
+ // spot is a dropped frame.
2151
+ fps: z.preprocess((v) => {
2152
+ if (typeof v === 'string') {
2153
+ const m = /^(\d+)\/(\d+)$/.exec(v.trim());
2154
+ if (m && Number(m[2]) > 0)
2155
+ return Number(m[1]) / Number(m[2]);
2156
+ const n = Number(v);
2157
+ return Number.isFinite(n) ? v === '' ? v : n : v;
2158
+ }
2159
+ return v;
2160
+ }, z.number().min(1).max(120).transform((n) => {
2161
+ const ntsc = { '23.976': 24000 / 1001, '29.97': 30000 / 1001, '59.94': 60000 / 1001, '119.88': 120000 / 1001 };
2162
+ return ntsc[(Math.round(n * 1000) / 1000).toString()] ?? ntsc[(Math.round(n * 100) / 100).toString()] ?? n;
2163
+ })).optional().describe('Video only: frames per second — an integer, an NTSC rate (23.976, 29.97, 59.94; snapped to the exact 24000/1001 …), or a rational string like "30000/1001". Overrides whatever `quality` implies. The tiers set it — draft 15, standard 30, high 60 — so asking for higher quality DOUBLES the frame count and the render time unless you say otherwise. File size is bitrate x duration and does not move with fps, but the picture gets fewer bits per frame at a higher one.'),
2164
+ scale: z.number().min(0.1).max(1).optional().describe('Video only: render at this fraction of the platform preset\'s dimensions (0.1-1). The engine derives its encode target from RESOLUTION unless bitrate is given, so this is the size control; halving the frame roughly quarters the pixels and the file. It is also the preview knob: scale 0.5 with quality "draft" is the fast look-check before committing to a full render. Rounded to even dimensions, which H.264 requires.'),
2066
2165
  sampleRate: z.number().int().positive().optional().describe('wav only: samples per second (default 48000). Ignored by every other format.'),
2067
2166
  bitDepth: z.union([z.literal(16), z.literal(32)]).optional().describe('wav only: 16 (default) or 32-bit float. Ignored by every other format.'),
2167
+ pdf: z.object({
2168
+ paperFormat: z.string().optional().describe("Paper size: 'a4', 'a3', 'letter', … or 'custom' (the canvas's own size, the default)."),
2169
+ orientation: z.enum(['portrait', 'landscape']).optional().describe('Default: from the canvas aspect.'),
2170
+ bleed: z.number().min(0).max(20).optional().describe('Bleed in mm; > 0 includes the bleed area.'),
2171
+ trimMarks: z.boolean().optional().describe('Add crop / trim marks.'),
2172
+ searchableText: z.boolean().optional().describe('An invisible text layer over the artwork, so the PDF is selectable, searchable and screen-readable (default true where the studio supports it; lines no font in the PDF can carry are left out and counted). false omits it.'),
2173
+ dpi: z.number().int().min(72).max(600).optional().describe('Rasterisation DPI (default: the quality tier\'s).'),
2174
+ pages: z.union([
2175
+ z.literal('scenes'),
2176
+ z.array(z.union([z.string(), z.object({ sceneId: z.string(), width: z.number().positive(), height: z.number().positive() })])).min(1),
2177
+ ]).optional().describe("Multi-page: 'scenes' = one page per saved scene in timeline order, or a list of scene ids — or {sceneId, width, height} to give a page its canvas size (scenes do not record one)."),
2178
+ }).optional().describe('pdf only: print options.'),
2179
+ ad: z.object({
2180
+ clickUrl: z.string().url().optional().describe('html5-ad: the clickTag value for testing (networks replace it at serve time). playable: the store / landing URL the CTA opens — required.'),
2181
+ ctaItemId: z.string().min(1).optional().describe('The item whose bounds are the click target. html5-ad: optional (default: the whole ad). playable: required — a playable is interactive, so only the CTA clicks through. The hotspot is where the item is at export time; it does not follow an animated CTA.'),
2182
+ maxBytes: z.number().int().positive().optional().describe('Size budget for the upload (the zip for html5-ad, the HTML for playable). html5-ad defaults to 150000: Google Ads display HTML5 is 150 KB; some uploads allow 600 KB — set it to your network\'s figure. Over budget is a fidelity warning, not a refusal.'),
2183
+ }).optional().describe('html5-ad / playable options.'),
2184
+ transparent: z.boolean().optional().describe('apng only: keep the alpha channel (the default). false fills the background colour.'),
2185
+ loop: z.union([z.boolean(), z.number().int().min(0).max(1000)]).optional().describe('gif / apng: true = loop forever, false / 0 / 1 = play once, n = play n times.'),
2186
+ 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.'),
2187
+ 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.'),
2188
+ 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).'),
2189
+ 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.'),
2190
+ minBitrate: z.number().int().min(100_000).max(200_000_000).optional().describe('mp4 / webm: a floor under the target. If the achieved bitrate misses it, fidelity warns and names the re-encode a delivery spec needs.'),
2191
+ bitrateMode: z.enum(['constant', 'variable']).optional().describe("mp4 / webm: 'constant' (the default with broadcast) or 'variable'."),
2192
+ time: z.number().min(0).optional().describe('Stills only (png / jpg / webp / svg / pdf): render the scene at this time in seconds. Without it a still is taken at wherever the playhead happens to be, so two identical runs can differ.'),
2193
+ region: z.object({
2194
+ x: z.number(), y: z.number(),
2195
+ width: z.number().positive(), height: z.number().positive(),
2196
+ outputWidth: z.number().int().positive().max(8192).optional(),
2197
+ outputHeight: z.number().int().positive().max(8192).optional(),
2198
+ excludeForeign: z.boolean().optional().describe('Default true: an item that reaches into the region but is centred outside it (a neighbouring card\'s overflow) is left out of this image; result.excludedItems names them. false renders everything that touches the region.'),
2199
+ }).optional().describe('png / jpg / webp only: export just this canvas region (canvas coordinates, top-left x/y) — carousel slices, crops. Output is the region\'s size unless outputWidth/outputHeight say otherwise; a different aspect is covered, not stretched.'),
2068
2200
  }).describe('Smart export options')
2069
2201
  .superRefine((val, ctx) => {
2202
+ if (val.time !== undefined && ['mp4', 'webm', 'gif', 'apng', 'wav', 'srt', 'vtt', 'scc'].includes(String(val.format))) {
2203
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['time'], message: "time picks the moment a STILL is taken; a video or audio export runs from 0 for its duration. Drop time, or export png / jpg / webp / svg / pdf." });
2204
+ }
2205
+ if (val.broadcast && val.format !== 'mp4') {
2206
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['broadcast'], message: 'broadcast is an mp4 setting (BT.709 H.264). Export format: "mp4".' });
2207
+ }
2208
+ if (val.broadcastHeadroom !== undefined && !val.broadcast) {
2209
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['broadcastHeadroom'], message: 'broadcastHeadroom applies only with broadcast: true (mp4).' });
2210
+ }
2211
+ for (const k of ['bitrate', 'minBitrate', 'bitrateMode']) {
2212
+ if (val[k] !== undefined && val.format !== 'mp4' && val.format !== 'webm') {
2213
+ 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.` });
2214
+ }
2215
+ }
2216
+ if (val.bitrate !== undefined && val.minBitrate !== undefined && val.minBitrate > val.bitrate) {
2217
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['minBitrate'], message: 'minBitrate is above bitrate; the floor would override the target. Drop one, or lower minBitrate.' });
2218
+ }
2219
+ if (val.ad !== undefined && val.format !== 'html5-ad' && val.format !== 'playable') {
2220
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['ad'], message: 'ad options apply to format "html5-ad" or "playable".' });
2221
+ }
2222
+ if (val.format === 'playable' && (!val.ad?.ctaItemId || !val.ad?.clickUrl)) {
2223
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['ad'], message: 'a playable needs ad.ctaItemId (the install button — only it clicks through, so the rest stays playable) and ad.clickUrl (the store URL it opens).' });
2224
+ }
2225
+ if (val.transparent !== undefined && val.format !== 'apng') {
2226
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['transparent'], message: 'transparent is an apng setting here: png keeps alpha already, and mp4 / webm / gif cannot carry it. For animation with alpha, export format "apng".' });
2227
+ }
2228
+ if (val.loop !== undefined && val.format !== 'gif' && val.format !== 'apng') {
2229
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['loop'], message: "loop is a gif / apng setting. A video cannot carry a loop flag — for a seamless loop, key t = 0 and t = duration to the same state and export exactly that duration." });
2230
+ }
2231
+ if (val.maxBytes !== undefined && val.format !== 'gif') {
2232
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['maxBytes'], message: "maxBytes is a gif budget. For mp4 / webm use scale and quality; for stills use jpg / webp with a lower quality." });
2233
+ }
2234
+ if (val.pdf !== undefined && val.format !== 'pdf') {
2235
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['pdf'], message: "pdf options apply only to format: 'pdf'." });
2236
+ }
2237
+ if (val.region !== undefined && !['png', 'jpg', 'webp'].includes(String(val.format))) {
2238
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['region'], message: "region applies only to a still — format 'png', 'jpg' or 'webp'. It renders one image of that part of the canvas." });
2239
+ }
2070
2240
  // wav carries no picture, so the visual knobs are not merely ignored — a
2071
2241
  // caller who set them believes something about the output that is not
2072
2242
  // true. Said once, rather than silently dropped.
@@ -2519,6 +2689,7 @@ export const ImportImageInputSchema = z.object({
2519
2689
  maxWidth: z.number().optional().describe('Maximum width constraint in pixels'),
2520
2690
  maxHeight: z.number().optional().describe('Maximum height constraint in pixels'),
2521
2691
  mask: ImageMaskSchema.optional().describe('Optional mask shape to apply to the image'),
2692
+ smoothing: z.enum(['off', 'low', 'medium', 'high']).optional().describe("Resampling when the image is scaled. 'off' = nearest-neighbour, hard pixel edges (pixel art). Default 'low'."),
2522
2693
  }).describe('Import image input');
2523
2694
  // =============================================================================
2524
2695
  // ONTOLOGY SCHEMAS
@@ -2970,6 +3141,25 @@ export const AudioBeatsInputSchema = z.object({
2970
3141
  sensitivity: z.number().optional(),
2971
3142
  minGap: z.number().optional(),
2972
3143
  });
3144
+ // ACCESSIBILITY CHECK: text contrast (WCAG 1.4.3) and flashing (2.3.1).
3145
+ export const AccessibilityCheckInputSchema = z.object({
3146
+ checks: z.array(z.enum(['contrast', 'flash'])).min(1).optional().describe("Which checks (default both)."),
3147
+ duration: z.number().positive().max(120).optional().describe('flash: seconds of the timeline to sample (default 5).'),
3148
+ fps: z.number().int().min(10).max(60).optional().describe('flash: samples per second (default 20; a flash rate is only seen below half of it).'),
3149
+ });
3150
+ // RENDER BATCH (data-driven creative): one scene, many rows, one export per
3151
+ // row. The handler runs each row through modify_item / template_params and
3152
+ // then agent_export, so every per-tool check applies to every row.
3153
+ export const RenderBatchRowSchema = z.object({
3154
+ id: z.string().min(1).max(64).optional().describe('A name for this row, echoed in its result.'),
3155
+ changes: z.record(z.string(), z.record(z.unknown())).optional().describe('itemId → properties, applied with modify_item (content, fillColor, fit, …).'),
3156
+ template: z.object({ templateId: z.string().min(1), params: z.record(z.unknown()) }).optional().describe('Apply a template\'s params (template_params apply).'),
3157
+ }).refine((r) => r.changes !== undefined || r.template !== undefined, { message: 'a row needs changes and / or template.' });
3158
+ export const RenderBatchInputSchema = z.object({
3159
+ rows: z.array(RenderBatchRowSchema).min(1).max(100).describe('One entry per creative (max 100).'),
3160
+ export: z.record(z.unknown()).describe('agent_export options used for every row (format, platform, quality, duration, ad, …).'),
3161
+ estimateOnly: z.boolean().optional().describe('Validate the rows and return the plan without rendering.'),
3162
+ });
2973
3163
  export const TemplateParamsInputSchema = z.object({
2974
3164
  action: z.enum(['get', 'apply']),
2975
3165
  templateId: z.string(),
@@ -3000,8 +3190,13 @@ export const ImageFilterInputSchema = z.object({
3000
3190
  preserveShading: z.number().min(0).max(1).optional().describe('recolor_palette: how much of the original luminance detail survives (default 1).'),
3001
3191
  });
3002
3192
  export const LassoInputSchema = z.object({
3003
- action: z.enum(['activate', 'apply']),
3193
+ action: z.enum(['cut', 'activate', 'apply']),
3004
3194
  itemId: z.string().optional(),
3195
+ points: z.array(z.union([
3196
+ z.tuple([z.number(), z.number()]),
3197
+ z.object({ x: z.number(), y: z.number() }),
3198
+ ])).optional(),
3199
+ keepOriginal: z.boolean().optional(),
3005
3200
  });
3006
3201
  export const CutoutStyleInputSchema = z.object({
3007
3202
  action: z.enum(['apply', 'list']),
@@ -3124,6 +3319,11 @@ export const ExportWidgetInputSchema = z.object({
3124
3319
  export const ExportWidgetHtmlInputSchema = z.object({
3125
3320
  title: z.string().optional(),
3126
3321
  download: z.boolean().optional(),
3322
+ // Accessibility / localisation (round 8 EE, 8.20): the engine writes
3323
+ // <html lang="en"> with no dir and no text alternative for the canvas.
3324
+ lang: z.string().regex(/^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/).optional().describe('BCP 47 language tag for the page, e.g. "ar", "hi", "zh-Hans".'),
3325
+ dir: z.enum(['ltr', 'rtl', 'auto']).optional().describe('Text direction for the page.'),
3326
+ alt: z.string().max(500).optional().describe('Text alternative for the animation: the canvas gets role="img" and this as its aria-label, and the page a meta description.'),
3127
3327
  });
3128
3328
  // =============================================================================
3129
3329
  // DATA VISUALIZATION (CHARTS)
@@ -3270,7 +3470,11 @@ export const LintSceneInputSchema = z.object({
3270
3470
  export const MediaInputSchema = z.object({
3271
3471
  action: z.enum(['upload_video', 'upload_audio', 'list', 'remove', 'set_playback_rate', 'set_clip', 'set_time_remap', 'speed_ramp', 'match_cut', 'apply_track_matte', 'stop_live_matte'])
3272
3472
  .describe("'upload_video' / 'upload_audio' (from a URL) · 'list' media · 'remove' by id · 'set_playback_rate' · 'set_clip' (re-trim) · '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'"),
3273
- url: z.string().url().optional().describe('Media URL — required for upload_video / upload_audio (fetched then imported).'),
3473
+ // Not .url(): a bare absolute path is not a URL, and 48f3a2a made the server
3474
+ // read local files — so `/Users/…/broll1.mp4` was advertised and then rejected
3475
+ // by the schema before anything could read it. resolveMediaSource validates
3476
+ // what it is given and names the problem.
3477
+ 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://).'),
3274
3478
  id: z.string().optional().describe('Media id — required for remove / set_playback_rate / set_clip.'),
3275
3479
  rate: z.number().min(0.25).max(4).optional().describe('Playback rate 0.25–4 — required for set_playback_rate.'),
3276
3480
  inPoint: z.number().min(0).optional().describe('Clip in-point in media-time seconds — required for set_clip.'),
@@ -3513,10 +3717,20 @@ export const GameInputSchema = z.object({
3513
3717
  rows: z.number().int().positive(),
3514
3718
  cellSize: z.number().positive().optional().describe('World units per cell (default 1).'),
3515
3719
  origin: PositionSchema.optional().describe('World position of cell (0,0).'),
3516
- blocked: z.array(z.number()).optional().describe('Flat cols×rows array, 1 = blocked.'),
3720
+ blocked: z.array(z.number()).optional().describe('A MASK, not a list of indices: one entry per cell, cols*rows long, row-major, non-zero meaning blocked. The engine reads blocked[y * cols + x].'),
3517
3721
  obstacles: z.array(z.object({ x: z.number(), y: z.number(), width: z.number(), height: z.number() }))
3518
3722
  .optional().describe('World-space rects to block — pass item bounds and the walls block the board.'),
3519
- }).optional().describe('pathfind: the board.'),
3723
+ }).optional().describe('pathfind: the board.')
3724
+ // A MASK AND AN INDEX LIST LOOK IDENTICAL TO A TYPE CHECKER.
3725
+ //
3726
+ // `blocked` is read as blocked[y * cols + x], so passing the INDICES of the
3727
+ // blocked cells — the other obvious reading — blocks cells 0..n-1 by their
3728
+ // values instead, and the answer comes back as a plausible "no path" with
3729
+ // nothing wrong on the face of it. The length tells the two apart for free.
3730
+ .refine((g) => !g?.blocked || g.blocked.length === g.cols * g.rows, {
3731
+ message: 'grid.blocked must have exactly cols*rows entries — one per cell, non-zero meaning blocked. A list of blocked cell INDICES is a different thing and produces a wrong board rather than an error.',
3732
+ path: ['blocked'],
3733
+ }),
3520
3734
  start: PositionSchema.optional().describe('pathfind: start, WORLD coordinates.'),
3521
3735
  goal: PositionSchema.optional().describe('pathfind: goal, WORLD coordinates.'),
3522
3736
  diagonal: z.boolean().optional().describe('pathfind: 8-way movement (default true). Never cuts corners.'),
@@ -3582,6 +3796,13 @@ export const World3DInputSchema = z.object({
3582
3796
  segments: z.number().optional().describe('extrude_path: segments along the sweep · lathe_path: segments around the revolution.'),
3583
3797
  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.'),
3584
3798
  flatness: z.number().optional().describe('Curve flattening tolerance when the path is sampled into rings — smaller is finer and heavier.'),
3799
+ // 5.64: these were read by the generator (65a1a21) but not declared here,
3800
+ // so z.object() stripped them before it ran — every mesh came out the
3801
+ // default grey. Declared, and tested through the handler this time.
3802
+ color: z.union([z.string(), z.array(z.number())]).optional().describe('Mesh colour: hex or [r,g,b] (0..255 or 0..1). Default light grey.'),
3803
+ metalness: z.number().min(0).max(1).optional(),
3804
+ roughness: z.number().min(0).max(1).optional(),
3805
+ uniforms: z.record(z.string(), z.unknown()).optional().describe('Raw shader uniforms (diffuse, metalness, roughness …) — win over color / metalness / roughness.'),
3585
3806
  x: z.number().optional().describe('Placement in world space.'),
3586
3807
  y: z.number().optional().describe('Placement in world space (omit to let it sit where the terrain puts it).'),
3587
3808
  z: z.number().optional().describe('Placement in world space.'),
@@ -3815,9 +4036,9 @@ export const SoundInputSchema = z.object({
3815
4036
  'list_instruments', 'list_percussion', 'list_sfx',
3816
4037
  'play_tone', 'play_chord', 'chord_frequencies', 'play_percussion', 'play_sfx',
3817
4038
  'play_spec', 'from_text', 'play_from_text',
3818
- 'create', 'timbre_from_path', 'set_placement', 'remove', 'stop_all',
4039
+ 'create', 'sequence', 'timbre_from_path', 'set_placement', 'remove', 'stop_all',
3819
4040
  'define_instrument', 'define_percussion', 'define_sfx', 'render_soundtrack',
3820
- ]).describe("Catalogues: 'list_instruments' · 'list_percussion' · 'list_sfx'. Play: 'play_tone' · 'play_chord' · 'play_percussion' · 'play_sfx' · 'play_spec' · 'play_from_text'. Read without playing: 'chord_frequencies' · 'from_text' (a plain-language description resolved to a spec) · 'timbre_from_path'. Canvas: 'create' (a sound drawn AS a waveform path) · 'set_placement' · 'remove' · 'stop_all'. Define: 'define_instrument' · 'define_percussion' · 'define_sfx' — the six built-in instruments are a starting set, not a claim that music contains six; anything missing can be defined here and is then first-class, listed by the list_* actions and playable by name. Render: 'render_soundtrack' — mix every placed sound to a WAV file offline, with no Web Audio and no playback."),
4041
+ ]).describe("Catalogues: 'list_instruments' · 'list_percussion' · 'list_sfx'. Play: 'play_tone' · 'play_chord' · 'play_percussion' · 'play_sfx' · 'play_spec' · 'play_from_text'. Read without playing: 'chord_frequencies' · 'from_text' (a plain-language description resolved to a spec) · 'timbre_from_path'. Canvas: 'create' (a sound drawn AS a waveform path) · 'sequence' (MANY cues in ONE call — a 112-cue music bed was 112 calls) · 'set_placement' · 'remove' · 'stop_all'. Define: 'define_instrument' · 'define_percussion' · 'define_sfx' — the six built-in instruments are a starting set, not a claim that music contains six; anything missing can be defined here and is then first-class, listed by the list_* actions and playable by name. Render: 'render_soundtrack' — mix every placed sound to a WAV file offline, with no Web Audio and no playback."),
3821
4042
  note: z.string().optional().describe("play_tone: scientific pitch, e.g. 'A4' or 'C#3'."),
3822
4043
  root: z.string().optional().describe("play_chord / chord_frequencies: the root note, e.g. 'C4'."),
3823
4044
  chord: z.string().optional().default('major').describe("play_chord / chord_frequencies: the chord kind, e.g. 'major', 'minor', 'maj7', 'dim'. Call the engine rather than guessing at exotic names."),
@@ -3849,6 +4070,14 @@ export const SoundInputSchema = z.object({
3849
4070
  itemId: z.string().optional().describe('timbre_from_path: the path whose SHAPE becomes the harmonic content. set_placement / remove: the sound item.'),
3850
4071
  samples: z.number().int().positive().optional().describe('timbre_from_path: how many points to sample along the path (default 256). More is a finer timbre and a slower read.'),
3851
4072
  placement: z.record(z.string(), z.unknown()).optional().describe('set_placement: where and when the sound sits on the timeline.'),
4073
+ cues: z.array(z.object({
4074
+ t: z.number().min(0).describe('When this cue sounds, in seconds.'),
4075
+ spec: z.record(z.string(), z.unknown()).optional().describe('The synthesis spec, as create takes it.'),
4076
+ preset: z.string().optional().describe("A named instrument, percussion or sfx instead of a spec — whatever the list_* actions report."),
4077
+ note: z.string().optional().describe("Pitch for a preset instrument, e.g. 'A4'."),
4078
+ duration: z.number().positive().optional().describe('Seconds this cue lasts.'),
4079
+ visual: z.record(z.string(), z.unknown()).optional().describe('Per-cue waveform drawing options, as create takes them.'),
4080
+ })).optional().describe('sequence: every cue in one call. Each is created and placed at its own t, so a music bed is ONE call rather than one per cue — a pilot spent 112 calls on a bed this expresses in a single array.'),
3852
4081
  visual: z.object({
3853
4082
  width: z.number().optional().describe('Waveform path width in canvas units (default 220).'),
3854
4083
  height: z.number().optional().describe('Waveform path height in canvas units (default 64).'),