@pinepaper.studio/mcp-server 1.6.7 → 1.6.8

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 (164) hide show
  1. package/README.de.md +9 -5
  2. package/README.es.md +9 -5
  3. package/README.fr.md +9 -5
  4. package/README.hi.md +8 -5
  5. package/README.ja.md +8 -5
  6. package/README.ko.md +8 -4
  7. package/README.md +171 -17
  8. package/README.pt-BR.md +9 -5
  9. package/README.zh-CN.md +6 -4
  10. package/dist/character/data/concepts.json +1 -0
  11. package/dist/character/data/depictions.json +1 -0
  12. package/dist/character/data/styles.json +1 -0
  13. package/dist/character/duration-scale.d.ts +13 -0
  14. package/dist/character/duration-scale.d.ts.map +1 -0
  15. package/dist/character/duration-scale.js +13 -0
  16. package/dist/character/duration-scale.js.map +1 -0
  17. package/dist/character/path-bbox.d.ts +82 -0
  18. package/dist/character/path-bbox.d.ts.map +1 -0
  19. package/dist/character/path-bbox.js +496 -0
  20. package/dist/character/path-bbox.js.map +1 -0
  21. package/dist/character/perform.d.ts +149 -0
  22. package/dist/character/perform.d.ts.map +1 -0
  23. package/dist/character/perform.js +626 -0
  24. package/dist/character/perform.js.map +1 -0
  25. package/dist/character/resolve.d.ts +51 -0
  26. package/dist/character/resolve.d.ts.map +1 -0
  27. package/dist/character/resolve.js +239 -0
  28. package/dist/character/resolve.js.map +1 -0
  29. package/dist/design/design-systems.d.ts +126 -0
  30. package/dist/design/design-systems.d.ts.map +1 -0
  31. package/dist/design/design-systems.js +240 -0
  32. package/dist/design/design-systems.js.map +1 -0
  33. package/dist/execution/canvas-state.d.ts +7 -0
  34. package/dist/execution/canvas-state.d.ts.map +1 -1
  35. package/dist/execution/canvas-state.js +20 -8
  36. package/dist/execution/canvas-state.js.map +1 -1
  37. package/dist/index.d.ts +20 -0
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +12 -4
  40. package/dist/index.js.map +1 -1
  41. package/dist/ontology/design-graph.d.ts +9 -0
  42. package/dist/ontology/design-graph.d.ts.map +1 -1
  43. package/dist/ontology/design-graph.js +98 -3
  44. package/dist/ontology/design-graph.js.map +1 -1
  45. package/dist/ontology/types.d.ts +42 -0
  46. package/dist/ontology/types.d.ts.map +1 -1
  47. package/dist/ontology/vocabulary.d.ts.map +1 -1
  48. package/dist/ontology/vocabulary.js +20 -0
  49. package/dist/ontology/vocabulary.js.map +1 -1
  50. package/dist/tools/compact-descriptions.d.ts.map +1 -1
  51. package/dist/tools/compact-descriptions.js +25 -0
  52. package/dist/tools/compact-descriptions.js.map +1 -1
  53. package/dist/tools/definitions.d.ts +1 -1
  54. package/dist/tools/definitions.d.ts.map +1 -1
  55. package/dist/tools/definitions.js +343 -24
  56. package/dist/tools/definitions.js.map +1 -1
  57. package/dist/tools/engine-methods.d.ts +18 -0
  58. package/dist/tools/engine-methods.d.ts.map +1 -0
  59. package/dist/tools/engine-methods.js +239 -0
  60. package/dist/tools/engine-methods.js.map +1 -0
  61. package/dist/tools/handlers/character.d.ts +121 -0
  62. package/dist/tools/handlers/character.d.ts.map +1 -0
  63. package/dist/tools/handlers/character.js +315 -0
  64. package/dist/tools/handlers/character.js.map +1 -0
  65. package/dist/tools/handlers.d.ts +2 -0
  66. package/dist/tools/handlers.d.ts.map +1 -1
  67. package/dist/tools/handlers.js +316 -16
  68. package/dist/tools/handlers.js.map +1 -1
  69. package/dist/tools/minimal-descriptions.d.ts.map +1 -1
  70. package/dist/tools/minimal-descriptions.js +13 -2
  71. package/dist/tools/minimal-descriptions.js.map +1 -1
  72. package/dist/tools/toolkits.d.ts.map +1 -1
  73. package/dist/tools/toolkits.js +7 -0
  74. package/dist/tools/toolkits.js.map +1 -1
  75. package/dist/types/code-generator.d.ts +96 -1
  76. package/dist/types/code-generator.d.ts.map +1 -1
  77. package/dist/types/code-generator.js +1200 -12
  78. package/dist/types/code-generator.js.map +1 -1
  79. package/dist/types/schemas.d.ts +6792 -151
  80. package/dist/types/schemas.d.ts.map +1 -1
  81. package/dist/types/schemas.js +587 -17
  82. package/dist/types/schemas.js.map +1 -1
  83. package/dist/vendor/design/art-deco-generator.d.ts +52 -0
  84. package/dist/vendor/design/art-deco-generator.d.ts.map +1 -0
  85. package/dist/vendor/design/art-deco-generator.js +144 -0
  86. package/dist/vendor/design/art-deco-generator.js.map +1 -0
  87. package/dist/vendor/design/art-nouveau-generator.d.ts +46 -0
  88. package/dist/vendor/design/art-nouveau-generator.d.ts.map +1 -0
  89. package/dist/vendor/design/art-nouveau-generator.js +134 -0
  90. package/dist/vendor/design/art-nouveau-generator.js.map +1 -0
  91. package/dist/vendor/design/bauhaus-generator.d.ts +53 -0
  92. package/dist/vendor/design/bauhaus-generator.d.ts.map +1 -0
  93. package/dist/vendor/design/bauhaus-generator.js +126 -0
  94. package/dist/vendor/design/bauhaus-generator.js.map +1 -0
  95. package/dist/vendor/design/brutalist-generator.d.ts +53 -0
  96. package/dist/vendor/design/brutalist-generator.d.ts.map +1 -0
  97. package/dist/vendor/design/brutalist-generator.js +148 -0
  98. package/dist/vendor/design/brutalist-generator.js.map +1 -0
  99. package/dist/vendor/design/claymorphism-generator.d.ts +46 -0
  100. package/dist/vendor/design/claymorphism-generator.d.ts.map +1 -0
  101. package/dist/vendor/design/claymorphism-generator.js +135 -0
  102. package/dist/vendor/design/claymorphism-generator.js.map +1 -0
  103. package/dist/vendor/design/corporate-memphis-generator.d.ts +47 -0
  104. package/dist/vendor/design/corporate-memphis-generator.d.ts.map +1 -0
  105. package/dist/vendor/design/corporate-memphis-generator.js +144 -0
  106. package/dist/vendor/design/corporate-memphis-generator.js.map +1 -0
  107. package/dist/vendor/design/cyberpunk-generator.d.ts +52 -0
  108. package/dist/vendor/design/cyberpunk-generator.d.ts.map +1 -0
  109. package/dist/vendor/design/cyberpunk-generator.js +162 -0
  110. package/dist/vendor/design/cyberpunk-generator.js.map +1 -0
  111. package/dist/vendor/design/de-stijl-generator.d.ts +50 -0
  112. package/dist/vendor/design/de-stijl-generator.d.ts.map +1 -0
  113. package/dist/vendor/design/de-stijl-generator.js +154 -0
  114. package/dist/vendor/design/de-stijl-generator.js.map +1 -0
  115. package/dist/vendor/design/design-style.d.ts +3 -0
  116. package/dist/vendor/design/design-style.d.ts.map +1 -0
  117. package/dist/vendor/design/design-style.js +12 -0
  118. package/dist/vendor/design/design-style.js.map +1 -0
  119. package/dist/vendor/design/design-systems-dtcg.d.ts +59 -0
  120. package/dist/vendor/design/design-systems-dtcg.d.ts.map +1 -0
  121. package/dist/vendor/design/design-systems-dtcg.js +571 -0
  122. package/dist/vendor/design/design-systems-dtcg.js.map +1 -0
  123. package/dist/vendor/design/frutiger-aero-generator.d.ts +46 -0
  124. package/dist/vendor/design/frutiger-aero-generator.d.ts.map +1 -0
  125. package/dist/vendor/design/frutiger-aero-generator.js +133 -0
  126. package/dist/vendor/design/frutiger-aero-generator.js.map +1 -0
  127. package/dist/vendor/design/memphis-generator.d.ts +56 -0
  128. package/dist/vendor/design/memphis-generator.d.ts.map +1 -0
  129. package/dist/vendor/design/memphis-generator.js +158 -0
  130. package/dist/vendor/design/memphis-generator.js.map +1 -0
  131. package/dist/vendor/design/mid-century-upa-generator.d.ts +46 -0
  132. package/dist/vendor/design/mid-century-upa-generator.d.ts.map +1 -0
  133. package/dist/vendor/design/mid-century-upa-generator.js +137 -0
  134. package/dist/vendor/design/mid-century-upa-generator.js.map +1 -0
  135. package/dist/vendor/design/neumorphic-generator.d.ts +53 -0
  136. package/dist/vendor/design/neumorphic-generator.d.ts.map +1 -0
  137. package/dist/vendor/design/neumorphic-generator.js +182 -0
  138. package/dist/vendor/design/neumorphic-generator.js.map +1 -0
  139. package/dist/vendor/design/op-art-generator.d.ts +45 -0
  140. package/dist/vendor/design/op-art-generator.d.ts.map +1 -0
  141. package/dist/vendor/design/op-art-generator.js +116 -0
  142. package/dist/vendor/design/op-art-generator.js.map +1 -0
  143. package/dist/vendor/design/pixel-art-generator.d.ts +45 -0
  144. package/dist/vendor/design/pixel-art-generator.d.ts.map +1 -0
  145. package/dist/vendor/design/pixel-art-generator.js +143 -0
  146. package/dist/vendor/design/pixel-art-generator.js.map +1 -0
  147. package/dist/vendor/design/pop-art-generator.d.ts +53 -0
  148. package/dist/vendor/design/pop-art-generator.d.ts.map +1 -0
  149. package/dist/vendor/design/pop-art-generator.js +164 -0
  150. package/dist/vendor/design/pop-art-generator.js.map +1 -0
  151. package/dist/vendor/design/risograph-generator.d.ts +53 -0
  152. package/dist/vendor/design/risograph-generator.d.ts.map +1 -0
  153. package/dist/vendor/design/risograph-generator.js +170 -0
  154. package/dist/vendor/design/risograph-generator.js.map +1 -0
  155. package/dist/vendor/design/swiss-style-generator.d.ts +55 -0
  156. package/dist/vendor/design/swiss-style-generator.d.ts.map +1 -0
  157. package/dist/vendor/design/swiss-style-generator.js +147 -0
  158. package/dist/vendor/design/swiss-style-generator.js.map +1 -0
  159. package/dist/vendor/design/vaporwave-generator.d.ts +51 -0
  160. package/dist/vendor/design/vaporwave-generator.d.ts.map +1 -0
  161. package/dist/vendor/design/vaporwave-generator.js +139 -0
  162. package/dist/vendor/design/vaporwave-generator.js.map +1 -0
  163. package/manifest.json +29 -1
  164. package/package.json +13 -5
@@ -6,7 +6,7 @@
6
6
  * Supports browser execution via Puppeteer.
7
7
  */
8
8
  import { codeGenerator } from '../types/code-generator.js';
9
- import { CreateItemInputSchema, ModifyItemInputSchema, DeleteItemInputSchema, AddRelationInputSchema, RemoveRelationInputSchema, QueryRelationsInputSchema, AnimateItemInputSchema, ImportMotionCaptureInputSchema, KeyframeAnimateInputSchema, ExecuteGeneratorInputSchema, ApplyEffectInputSchema, GetItemsInputSchema, PlayTimelineInputSchema, SetBackgroundColorInputSchema, SetCanvasSizeInputSchema, ImportSVGInputSchema, ImportMermaidInputSchema, AddFilterInputSchema, CreateGlossySphereInputSchema, CreateDiagonalStripesInputSchema, BatchCreateInputSchema, BatchModifyInputSchema, CreateGridInputSchema, GeometryInputSchema, EquationPathInputSchema, EventInputSchema, ConstructionSequenceInputSchema, ValidateSceneInputSchema, CaptureFramesInputSchema, InstantiateOntologyInputSchema, LintSceneInputSchema, MediaInputSchema, TextStyleInputSchema, TextEffectInputSchema, DesignMediumInputSchema, ShatterImageInputSchema, ImportLayeredCharacterInputSchema, GameInputSchema, World3DInputSchema, BrandKitInputSchema, ComponentInputSchema, ArtboardInputSchema, CommentInputSchema, ProvenanceInputSchema, SceneDiffInputSchema, AudioBeatsInputSchema, TemplateParamsInputSchema, ComposeInputSchema, CropImageInputSchema, ChromaKeyInputSchema, RiggingInputSchema, GroupInputSchema, CameraDirectorInputSchema, DetectObjectsInputSchema, ExtractObjectInputSchema, ArrangeInputSchema, CreateSceneInputSchema, GetPerformanceMetricsInputSchema, DiagnosticReportInputSchema, SearchAssetsInputSchema, ImportAssetInputSchema, P5DrawInputSchema,
9
+ import { CreateItemInputSchema, ModifyItemInputSchema, DeleteItemInputSchema, AddRelationInputSchema, RemoveRelationInputSchema, QueryRelationsInputSchema, AnimateItemInputSchema, ImportMotionCaptureInputSchema, KeyframeAnimateInputSchema, ExecuteGeneratorInputSchema, ApplyEffectInputSchema, GetItemsInputSchema, PlayTimelineInputSchema, SetBackgroundColorInputSchema, SetCanvasSizeInputSchema, ImportSVGInputSchema, ImportMermaidInputSchema, AddFilterInputSchema, CreateGlossySphereInputSchema, CreateDiagonalStripesInputSchema, BatchCreateInputSchema, BatchModifyInputSchema, CreateGridInputSchema, GeometryInputSchema, EquationPathInputSchema, EventInputSchema, ConstructionSequenceInputSchema, ValidateSceneInputSchema, CaptureFramesInputSchema, InstantiateOntologyInputSchema, LintSceneInputSchema, MediaInputSchema, TextStyleInputSchema, TextEffectInputSchema, DesignMediumInputSchema, ShatterImageInputSchema, ImportLayeredCharacterInputSchema, CharacterInputSchema, GameInputSchema, World3DInputSchema, BrandKitInputSchema, ComponentInputSchema, ArtboardInputSchema, CommentInputSchema, ProvenanceInputSchema, SceneDiffInputSchema, AudioBeatsInputSchema, TemplateParamsInputSchema, ComposeInputSchema, CropImageInputSchema, PathOpInputSchema, MotionInputSchema, SoundInputSchema, InterchangeInputSchema, StickInputSchema, DesignSystemInputSchema, StoryInputSchema, ChromaKeyInputSchema, RiggingInputSchema, GroupInputSchema, CameraDirectorInputSchema, DetectObjectsInputSchema, ExtractObjectInputSchema, ArrangeInputSchema, CreateSceneInputSchema, GetPerformanceMetricsInputSchema, DiagnosticReportInputSchema, SearchAssetsInputSchema, ImportAssetInputSchema, P5DrawInputSchema,
10
10
  // Diagram schemas
11
11
  CreateDiagramShapeInputSchema, ConnectInputSchema, ConnectPortsInputSchema, AddPortsInputSchema, AutoLayoutInputSchema, GetDiagramShapesInputSchema, UpdateConnectorInputSchema, RemoveConnectorInputSchema, DiagramModeInputSchema,
12
12
  // Agent flow mode schemas
@@ -29,7 +29,7 @@ ManageScenesInputSchema, ScenePlaybackInputSchema, SceneGraphInputSchema, Sequen
29
29
  // New consolidated tool schemas
30
30
  SelectionInputSchema, TransformInputSchema, HistoryInputSchema, ImageFilterInputSchema, LassoInputSchema, CutoutStyleInputSchema, PrecompInputSchema, BackgroundInputSchema, QueryInputSchema, QueryCapabilitiesInputSchema, DeformInputSchema, SpriteSheetInputSchema, InteractionInputSchema, ExportWidgetInputSchema, ExportWidgetHtmlInputSchema, CreateChartInputSchema, MagicInputSchema, PhysicsInputSchema, MeasurementInputSchema, ErrorCodes, } from '../types/schemas.js';
31
31
  import { ZodError } from 'zod';
32
- import { writeFile, mkdir } from 'node:fs/promises';
32
+ import { writeFile, mkdir, appendFile, unlink } from 'node:fs/promises';
33
33
  import { tmpdir } from 'node:os';
34
34
  import { join } from 'node:path';
35
35
  import { getBrowserController, resetBrowserController, } from '../browser/puppeteer-controller.js';
@@ -43,20 +43,24 @@ import { fontHandlers } from './handlers/font.js';
43
43
  import { toolGuideHandlers } from './handlers/tool-guide.js';
44
44
  import { mapHandlers } from './handlers/maps.js';
45
45
  import { ontologyHandlers } from './handlers/ontology.js';
46
+ import * as designSystems from '../design/design-systems.js';
46
47
  import { exportHandlers } from './handlers/export.js';
48
+ import { planCharacter, generateCharacterCode } from './handlers/character.js';
47
49
  /**
48
50
  * Registry of per-domain handler maps. Tools listed here short-circuit the
49
51
  * main switch at dispatch time (see handleToolCallInner). New domains are
50
52
  * folded in incrementally as they are migrated out of the monolithic switch.
51
53
  */
52
- const DOMAIN_HANDLERS = [
53
- cameraHandlers,
54
- fontHandlers,
55
- toolGuideHandlers,
56
- mapHandlers,
57
- ontologyHandlers,
58
- exportHandlers,
59
- ];
54
+ function getDomainHandlers() {
55
+ return [
56
+ cameraHandlers,
57
+ fontHandlers,
58
+ toolGuideHandlers,
59
+ mapHandlers,
60
+ ontologyHandlers,
61
+ exportHandlers,
62
+ ];
63
+ }
60
64
  /**
61
65
  * Get screenshot mode from environment variable.
62
66
  * Defaults to 'on_request' for optimal performance.
@@ -75,10 +79,13 @@ export function getScreenshotMode() {
75
79
  function getExportDir() {
76
80
  return process.env.PINEPAPER_EXPORT_DIR || join(tmpdir(), 'pinepaper-exports');
77
81
  }
78
- const ALWAYS_SAVE_FORMATS = new Set(['mp4', 'webm', 'gif', 'pdf']);
82
+ // wav joins them: a minute of 48kHz 16-bit is ~5.8 MB of base64 and the ten
83
+ // minutes the schema allows is ~77 MB. Deliverable across the bridge, useless
84
+ // pasted into a response.
85
+ export const ALWAYS_SAVE_FORMATS = new Set(['mp4', 'webm', 'gif', 'pdf', 'wav']);
79
86
  const SAVE_THRESHOLD_BYTES = 500_000; // ~500KB base64 ≈ 375KB decoded
80
- function getFileExtension(format) {
81
- const extMap = { mp4: 'mp4', webm: 'webm', gif: 'gif', pdf: 'pdf', png: 'png', svg: 'svg' };
87
+ export function getFileExtension(format) {
88
+ const extMap = { mp4: 'mp4', webm: 'webm', gif: 'gif', pdf: 'pdf', png: 'png', svg: 'svg', wav: 'wav' };
82
89
  return extMap[format] || format;
83
90
  }
84
91
  async function saveExportToFile(data, format, platform) {
@@ -100,6 +107,78 @@ async function saveExportToFile(data, format, platform) {
100
107
  return { filePath, fileSize: Buffer.byteLength(data, 'utf-8') };
101
108
  }
102
109
  }
110
+ /**
111
+ * Page a RETAINED export out of the studio's export store into a local file.
112
+ *
113
+ * The bytes never cross the bridge as one string. The studio holds the encoded
114
+ * file in its origin-private store and hands back an id; this reads it a chunk
115
+ * at a time and appends each one, so at most one chunk (4 MB by default) is in
116
+ * memory on either side. The agent still made one tool call and still gets one
117
+ * filePath — the paging is entirely inside the tool, because an agent-facing
118
+ * pager would only expose a transport limit the agent cannot act on.
119
+ *
120
+ * On failure the export is deliberately LEFT HELD: the store is the only copy,
121
+ * so the id and the byte count reached go back in the error and the bytes stay
122
+ * recoverable. `releaseExport` runs only once the whole file is written.
123
+ */
124
+ async function streamRetainedExportToFile(controller, retained, platform) {
125
+ const exportDir = getExportDir();
126
+ await mkdir(exportDir, { recursive: true });
127
+ const ext = getFileExtension(retained.format);
128
+ const fileName = `pinepaper_${platform}_${Date.now()}.${ext}`;
129
+ const filePath = join(exportDir, fileName);
130
+ const chunkBytes = retained.chunkBytes && retained.chunkBytes > 0 ? retained.chunkBytes : 4 * 1024 * 1024;
131
+ let offset = 0;
132
+ let chunks = 0;
133
+ // Start from empty: appendFile would otherwise extend a same-millisecond file.
134
+ await writeFile(filePath, Buffer.alloc(0));
135
+ // Returns the error rather than throwing it, so `throw await abandon(...)`
136
+ // reads as a throw to the type checker and narrows what follows.
137
+ const abandon = async (why) => {
138
+ try {
139
+ await unlink(filePath);
140
+ }
141
+ catch { /* nothing written yet */ }
142
+ return new Error(`${why} (export "${retained.exportId}" is still held in the studio, ${offset} of ${retained.size} bytes read; ` +
143
+ `it can be paged again, and is released only once a whole file is written)`);
144
+ };
145
+ for (;;) {
146
+ const code = codeGenerator.generateReadExportChunk(retained.exportId, offset, Math.min(chunkBytes, retained.size - offset));
147
+ const run = await controller.executeCode(code, false);
148
+ if (!run.success)
149
+ throw await abandon(`reading the export failed: ${run.error || 'unknown error'}`);
150
+ const chunk = run.result;
151
+ if (!chunk || chunk.ok !== true) {
152
+ // An evicted id is its own failure: a later export reclaimed the space.
153
+ // Re-exporting is the caller's decision, never this loop's.
154
+ throw await abandon(chunk?.evicted
155
+ ? `the export was evicted mid-read: ${chunk.reason}`
156
+ : `the studio refused the read: ${chunk?.reason || 'no reason given'}`);
157
+ }
158
+ const buf = Buffer.from(chunk.data || '', 'base64');
159
+ if (buf.length === 0 && chunk.eof !== true)
160
+ throw await abandon('the studio returned an empty chunk before the end of the file');
161
+ await appendFile(filePath, buf);
162
+ offset += buf.length;
163
+ chunks++;
164
+ if (chunk.eof === true)
165
+ break;
166
+ if (offset >= retained.size)
167
+ break;
168
+ }
169
+ if (offset !== retained.size) {
170
+ throw await abandon(`reassembled ${offset} bytes but the studio reported ${retained.size}`);
171
+ }
172
+ // Only now is it safe to drop the studio's copy.
173
+ try {
174
+ await controller.executeCode(codeGenerator.generateReleaseExport(retained.exportId), false);
175
+ }
176
+ catch (releaseError) {
177
+ // A held export costs quota, not correctness — the file is already whole.
178
+ console.error('[PinePaper] Export written but releaseExport failed:', releaseError);
179
+ }
180
+ return { filePath, fileSize: offset, chunks };
181
+ }
103
182
  /**
104
183
  * Get execution mode from environment variable.
105
184
  * Defaults to 'puppeteer' for live execution.
@@ -130,6 +209,17 @@ function successResult(code, description) {
130
209
  }
131
210
  return { content };
132
211
  }
212
+ /**
213
+ * A plain data answer.
214
+ *
215
+ * successResult() fences its payload as "Generated PinePaper code", which is
216
+ * right for an emitter and wrong for a tool that computes its answer here —
217
+ * a caller reading design tokens should not be handed something that looks
218
+ * like a script to paste.
219
+ */
220
+ function dataResult(value) {
221
+ return { content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] };
222
+ }
133
223
  function executedResult(code, result, screenshot, description, report) {
134
224
  const resultObj = { success: true, result };
135
225
  if (description)
@@ -503,7 +593,7 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
503
593
  // Domain-module dispatch: tools migrated to per-domain handler files
504
594
  // short-circuit before the main switch. See DOMAIN_HANDLERS for the
505
595
  // current registry.
506
- for (const domainMap of DOMAIN_HANDLERS) {
596
+ for (const domainMap of getDomainHandlers()) {
507
597
  const handler = domainMap[toolName];
508
598
  if (handler)
509
599
  return await handler(args, options);
@@ -739,6 +829,28 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
739
829
  // -----------------------------------------------------------------------
740
830
  // BATCH OPERATION TOOLS
741
831
  // -----------------------------------------------------------------------
832
+ case 'pinepaper_character': {
833
+ const input = CharacterInputSchema.parse(args);
834
+ const plan = planCharacter(input);
835
+ if (!plan.ok) {
836
+ // A refusal that names what WOULD have worked. The caller cannot see
837
+ // the design graph, and a bare "no" is how a model came to ask for a
838
+ // concept that has never had a drawing.
839
+ return {
840
+ content: [{ type: 'text', text: `pinepaper_character: ${plan.reason}` }],
841
+ isError: true,
842
+ };
843
+ }
844
+ const code = generateCharacterCode(plan, {
845
+ generateCreateItem: (i) => codeGenerator.generateCreateItem(i),
846
+ generateKeyframeAnimate: (i) => codeGenerator.generateKeyframeAnimate(i),
847
+ }, {
848
+ createItem: (a) => CreateItemInputSchema.parse(a),
849
+ keyframeAnimate: (a) => KeyframeAnimateInputSchema.parse(a),
850
+ });
851
+ const description = `Places ${input.concept} as ${plan.parts.length} parts with ${plan.tracks} keyframe tracks`;
852
+ return executeOrGenerate(code, description, options, 'pinepaper_character');
853
+ }
742
854
  case 'pinepaper_batch_create': {
743
855
  const input = BatchCreateInputSchema.parse(args);
744
856
  const code = codeGenerator.generateBatchCreate(input);
@@ -947,7 +1059,7 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
947
1059
  const input = AddRelationInputSchema.parse(args);
948
1060
  const code = codeGenerator.generateAddRelation(input);
949
1061
  const description = getLocalizedSuccessMessage(i18n, 'relationAdded', {
950
- relationType: input.relationType,
1062
+ relationType: input.relationType ?? `preset:${input.presetId}`,
951
1063
  sourceId: input.sourceId,
952
1064
  targetId: input.targetId || 'self',
953
1065
  });
@@ -1944,16 +2056,52 @@ You can now start creating new items on a clean canvas.`,
1944
2056
  const exportResult = exportBrowserResult.result;
1945
2057
  const format = exportResult?.format || input.format || 'png';
1946
2058
  const data = exportResult?.data;
2059
+ // RETAINED: the studio held the encoded file in its export store rather
2060
+ // than returning bytes, because base64 of a long video is a bigger
2061
+ // string than the video and it would cross the bridge as one value.
2062
+ // Page it into a file here. mp4/webm were always going to be written to
2063
+ // a file anyway (ALWAYS_SAVE_FORMATS), so the agent's result is the
2064
+ // same shape it has always been — a filePath — just reachable at sizes
2065
+ // that used to run out of memory.
2066
+ if (exportResult?.retained === true && typeof exportResult.exportId === 'string') {
2067
+ try {
2068
+ const { filePath, fileSize, chunks } = await streamRetainedExportToFile(controller, {
2069
+ exportId: exportResult.exportId,
2070
+ size: Number(exportResult.size) || 0,
2071
+ format,
2072
+ chunkBytes: Number(exportResult.chunkBytes) || undefined,
2073
+ }, input.platform || 'auto');
2074
+ const cleanResult = { ...exportResult, data: undefined, filePath, fileSize, chunks };
2075
+ return {
2076
+ content: [{
2077
+ type: 'text',
2078
+ text: `Export saved to file:\n\nFile: ${filePath}\nFormat: ${format}\nSize: ${(fileSize / 1024).toFixed(1)} KB\nPlatform: ${input.platform}\nPaged out of the studio's export store in ${chunks} chunk${chunks === 1 ? '' : 's'}\n\nResult: ${JSON.stringify(cleanResult, null, 2)}`,
2079
+ }],
2080
+ };
2081
+ }
2082
+ catch (streamError) {
2083
+ // The export is still held — the message says so and names the id,
2084
+ // because the bytes are recoverable and re-exporting is not free.
2085
+ const canvasState = await captureCanvasState(controller);
2086
+ return errorResult(ErrorCodes.EXECUTION_ERROR, streamError instanceof Error ? streamError.message : 'Paging the export out of the store failed', { code, exportId: exportResult.exportId, size: exportResult.size }, { toolName: 'pinepaper_agent_export', canvasState: canvasState || undefined });
2087
+ }
2088
+ }
1947
2089
  const shouldSaveToFile = data && typeof data === 'string' && (ALWAYS_SAVE_FORMATS.has(format) ||
1948
2090
  data.length > SAVE_THRESHOLD_BYTES);
1949
2091
  if (shouldSaveToFile) {
1950
2092
  try {
1951
2093
  const { filePath, fileSize } = await saveExportToFile(data, format, input.platform || 'auto');
1952
2094
  const cleanResult = { ...exportResult, data: undefined, filePath, fileSize };
2095
+ // A mix that dropped sounds wrote a perfectly good file that is
2096
+ // missing tracks. It rides the SUCCESS path, which is exactly why
2097
+ // it has to be said out loud here rather than left in the JSON.
2098
+ const dropNote = exportResult.incomplete;
1953
2099
  return {
1954
2100
  content: [{
1955
2101
  type: 'text',
1956
- text: `Export saved to file:\n\nFile: ${filePath}\nFormat: ${format}\nSize: ${(fileSize / 1024).toFixed(1)} KB\nPlatform: ${input.platform}\n\nResult: ${JSON.stringify(cleanResult, null, 2)}`,
2102
+ text: `Export saved to file:\n\nFile: ${filePath}\nFormat: ${format}\nSize: ${(fileSize / 1024).toFixed(1)} KB\nPlatform: ${input.platform}\n`
2103
+ + (dropNote ? `DROPPED: ${dropNote}\n` : '')
2104
+ + `\nResult: ${JSON.stringify(cleanResult, null, 2)}`,
1957
2105
  }],
1958
2106
  };
1959
2107
  }
@@ -1967,6 +2115,158 @@ You can now start creating new items on a clean canvas.`,
1967
2115
  // Small export — return inline (existing behavior)
1968
2116
  return executedResult(code, exportResult, exportBrowserResult.screenshot, description);
1969
2117
  }
2118
+ case 'pinepaper_design_styles':
2119
+ return dataResult({ styles: designSystems.listStyles() });
2120
+ case 'pinepaper_design_systems':
2121
+ case 'pinepaper_design_system': {
2122
+ const rawArgs = { ...args };
2123
+ if (!rawArgs.action && rawArgs.systemName) {
2124
+ if (rawArgs.systemName === 'all') {
2125
+ rawArgs.action = 'list_systems';
2126
+ }
2127
+ else {
2128
+ rawArgs.action = 'get_system';
2129
+ rawArgs.systemId = String(rawArgs.systemName);
2130
+ }
2131
+ }
2132
+ if (!rawArgs.action && rawArgs.style && !rawArgs.title) {
2133
+ rawArgs.action = 'list_styles';
2134
+ }
2135
+ const input = DesignSystemInputSchema.parse(rawArgs);
2136
+ // Answered HERE, not in the browser — these are decisions over
2137
+ // vendored data, and only `compose` with draw:true needs the canvas.
2138
+ switch (input.action) {
2139
+ case 'list_systems':
2140
+ return dataResult({ systems: designSystems.listSystems() });
2141
+ case 'get_system': {
2142
+ const sys = designSystems.getSystem(input.systemId, input.tokenType);
2143
+ if (!sys) {
2144
+ return errorResult(ErrorCodes.INVALID_PARAMS, `no design system "${input.systemId}" — call action 'list_systems' for the ids`, { systemId: input.systemId }, { toolName: 'pinepaper_design_system' });
2145
+ }
2146
+ return dataResult(sys);
2147
+ }
2148
+ case 'list_easings':
2149
+ return dataResult({ easings: designSystems.listEasings(input.authoredOnly) });
2150
+ case 'list_styles':
2151
+ return dataResult({ styles: designSystems.listStyles() });
2152
+ case 'compose': {
2153
+ let scene;
2154
+ try {
2155
+ scene = designSystems.compose(input.style, {
2156
+ title: input.title, subtitle: input.subtitle, body: input.body,
2157
+ width: input.width, height: input.height, variant: input.variant,
2158
+ });
2159
+ }
2160
+ catch (composeError) {
2161
+ return errorResult(ErrorCodes.EXECUTION_ERROR, composeError instanceof Error ? composeError.message : 'the style could not compose', { style: input.style }, { toolName: 'pinepaper_design_system' });
2162
+ }
2163
+ if (!scene) {
2164
+ return errorResult(ErrorCodes.INVALID_PARAMS, `"${input.style}" cannot compose — it is a style this surface can name but not build. ` +
2165
+ `Call action 'list_styles' and use one marked composable.`, { style: input.style }, { toolName: 'pinepaper_design_system' });
2166
+ }
2167
+ const ops = designSystems.sceneToOps(scene);
2168
+ if (input.draw === false) {
2169
+ return dataResult({ scene, ops, drawn: false });
2170
+ }
2171
+ const code = codeGenerator.generateDesignCompose(scene, ops, scene.style);
2172
+ return executeOrGenerate(code, `Composed a ${scene.style} scene (${ops.length} items)`, options, 'pinepaper_design_system');
2173
+ }
2174
+ }
2175
+ break;
2176
+ }
2177
+ case 'pinepaper_stick_figure':
2178
+ case 'pinepaper_stick': {
2179
+ const input = StickInputSchema.parse(args);
2180
+ return executeOrGenerate(codeGenerator.generateStick(input), `Stick ${input.action}`, options, 'pinepaper_stick');
2181
+ }
2182
+ case 'pinepaper_story': {
2183
+ const input = StoryInputSchema.parse(args);
2184
+ return executeOrGenerate(codeGenerator.generateStory(input), `Story: ${input.action}`, options, 'pinepaper_story');
2185
+ }
2186
+ case 'pinepaper_interchange': {
2187
+ const input = InterchangeInputSchema.parse(args);
2188
+ const code = codeGenerator.generateInterchange(input);
2189
+ return executeOrGenerate(code, `Interchange: ${input.action}`, options, 'pinepaper_interchange');
2190
+ }
2191
+ case 'pinepaper_sound': {
2192
+ const input = SoundInputSchema.parse(args);
2193
+ const code = codeGenerator.generateSound(input);
2194
+ const soundDescription = `Sound: ${input.action}`;
2195
+ // A RENDERED SOUNDTRACK IS A FILE, not a response field. 48kHz 16-bit
2196
+ // mono is ~96 KB/s, so a minute is ~5.8 MB of base64 and the ten
2197
+ // minutes the schema allows is ~77 MB — deliverable across the bridge,
2198
+ // useless pasted into a conversation. This mirrors what agent_export
2199
+ // already does for video and PDF, and reuses the same helper rather
2200
+ // than growing a second copy of the save path.
2201
+ if (input.action === 'render_soundtrack') {
2202
+ const execMode = options.executionMode ?? getExecutionMode();
2203
+ if (execMode === 'code' || !options.executeInBrowser) {
2204
+ return executeOrGenerate(code, soundDescription, options, 'pinepaper_sound');
2205
+ }
2206
+ const soundController = options.browserController || getBrowserController();
2207
+ if (!soundController.connected) {
2208
+ try {
2209
+ await soundController.connect();
2210
+ }
2211
+ catch {
2212
+ return executeOrGenerate(code, soundDescription, options, 'pinepaper_sound');
2213
+ }
2214
+ }
2215
+ const wavResult = await soundController.executeCode(code, false);
2216
+ if (!wavResult.success) {
2217
+ const canvasState = await captureCanvasState(soundController);
2218
+ return errorResult(ErrorCodes.EXECUTION_ERROR, wavResult.error || 'Rendering the soundtrack failed', { code }, { toolName: 'pinepaper_sound', canvasState: canvasState || undefined });
2219
+ }
2220
+ const soundtrack = wavResult.result;
2221
+ // A refusal is a real answer — "no sounds are placed" travels intact
2222
+ // rather than becoming an empty file.
2223
+ if (!soundtrack?.success || !soundtrack.wavBase64) {
2224
+ return executedResult(code, soundtrack, wavResult.screenshot, soundDescription);
2225
+ }
2226
+ try {
2227
+ const { filePath, fileSize } = await saveExportToFile(`data:audio/wav;base64,${soundtrack.wavBase64}`, 'wav', 'soundtrack');
2228
+ const clean = { ...soundtrack, wavBase64: undefined, filePath, fileSize };
2229
+ return {
2230
+ content: [{
2231
+ type: 'text',
2232
+ text: `Soundtrack rendered to file:\n\nFile: ${filePath}\n`
2233
+ + `Size: ${(fileSize / 1024).toFixed(1)} KB\n`
2234
+ + `Duration: ${soundtrack.duration}s at ${soundtrack.sampleRate} Hz\n`
2235
+ + `Mixed: ${soundtrack.placed} sound(s)\n`
2236
+ // The drop count rides on the success path, because a file
2237
+ // that wrote fine while missing sounds is the case nobody
2238
+ // goes looking for.
2239
+ + (soundtrack.dropped ? `DROPPED: ${soundtrack.incomplete}\n` : '')
2240
+ + `\nResult: ${JSON.stringify(clean, null, 2)}`,
2241
+ }],
2242
+ };
2243
+ }
2244
+ catch (saveError) {
2245
+ const stripped = {
2246
+ ...soundtrack,
2247
+ wavBase64: `[${soundtrack.byteLength} bytes of WAV — file save failed: ${saveError instanceof Error ? saveError.message : 'unknown error'}]`,
2248
+ };
2249
+ return executedResult(code, stripped, wavResult.screenshot, soundDescription);
2250
+ }
2251
+ }
2252
+ return executeOrGenerate(code, soundDescription, options, 'pinepaper_sound');
2253
+ }
2254
+ case 'pinepaper_motion': {
2255
+ const input = MotionInputSchema.parse(args);
2256
+ const code = codeGenerator.generateMotion(input);
2257
+ const described = input.action === 'list'
2258
+ ? 'Listed the generator motion catalogue'
2259
+ : `Applied motion ${input.motion}`;
2260
+ return executeOrGenerate(code, described, options, 'pinepaper_motion');
2261
+ }
2262
+ case 'pinepaper_path': {
2263
+ const input = PathOpInputSchema.parse(args);
2264
+ const code = codeGenerator.generatePathOp(input);
2265
+ const described = input.action === 'boolean'
2266
+ ? `Path boolean (${input.op}) over ${input.itemIds?.length ?? 0} items`
2267
+ : `Path ${input.action}${input.itemId ? ` on ${input.itemId}` : ''}`;
2268
+ return executeOrGenerate(code, described, options, 'pinepaper_path');
2269
+ }
1970
2270
  case 'pinepaper_agent_analyze': {
1971
2271
  const input = AgentAnalyzeInputSchema.parse(args);
1972
2272
  const code = codeGenerator.generateAgentAnalyze(input);