@pinepaper.studio/mcp-server 1.5.1 → 1.5.4

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 (96) hide show
  1. package/README.md +6 -4
  2. package/dist/agent/types.d.ts +16 -16
  3. package/dist/browser/puppeteer-controller.d.ts +5 -1
  4. package/dist/browser/puppeteer-controller.d.ts.map +1 -1
  5. package/dist/browser/puppeteer-controller.js +11 -3
  6. package/dist/browser/puppeteer-controller.js.map +1 -1
  7. package/dist/browser/url-target.d.ts +26 -0
  8. package/dist/browser/url-target.d.ts.map +1 -0
  9. package/dist/browser/url-target.js +50 -0
  10. package/dist/browser/url-target.js.map +1 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +128 -109
  13. package/dist/index.js.map +1 -1
  14. package/dist/ontology/design-graph.d.ts +85 -0
  15. package/dist/ontology/design-graph.d.ts.map +1 -0
  16. package/dist/ontology/design-graph.js +944 -0
  17. package/dist/ontology/design-graph.js.map +1 -0
  18. package/dist/ontology/hints.d.ts +71 -0
  19. package/dist/ontology/hints.d.ts.map +1 -0
  20. package/dist/ontology/hints.js +189 -0
  21. package/dist/ontology/hints.js.map +1 -0
  22. package/dist/ontology/index.d.ts +11 -0
  23. package/dist/ontology/index.d.ts.map +1 -0
  24. package/dist/ontology/index.js +14 -0
  25. package/dist/ontology/index.js.map +1 -0
  26. package/dist/ontology/semantic-vocabulary.d.ts +12 -0
  27. package/dist/ontology/semantic-vocabulary.d.ts.map +1 -0
  28. package/dist/ontology/semantic-vocabulary.js +26 -0
  29. package/dist/ontology/semantic-vocabulary.js.map +1 -0
  30. package/dist/ontology/types.d.ts +255 -0
  31. package/dist/ontology/types.d.ts.map +1 -0
  32. package/dist/ontology/types.js +10 -0
  33. package/dist/ontology/types.js.map +1 -0
  34. package/dist/ontology/validator.d.ts +36 -0
  35. package/dist/ontology/validator.d.ts.map +1 -0
  36. package/dist/ontology/validator.js +374 -0
  37. package/dist/ontology/validator.js.map +1 -0
  38. package/dist/ontology/vocabulary.d.ts +34 -0
  39. package/dist/ontology/vocabulary.d.ts.map +1 -0
  40. package/dist/ontology/vocabulary.js +429 -0
  41. package/dist/ontology/vocabulary.js.map +1 -0
  42. package/dist/prompts/index.d.ts.map +1 -1
  43. package/dist/prompts/index.js +146 -0
  44. package/dist/prompts/index.js.map +1 -1
  45. package/dist/tools/compact-descriptions.d.ts +5 -2
  46. package/dist/tools/compact-descriptions.d.ts.map +1 -1
  47. package/dist/tools/compact-descriptions.js +56 -235
  48. package/dist/tools/compact-descriptions.js.map +1 -1
  49. package/dist/tools/definitions.d.ts +1 -1
  50. package/dist/tools/definitions.d.ts.map +1 -1
  51. package/dist/tools/definitions.js +1842 -1784
  52. package/dist/tools/definitions.js.map +1 -1
  53. package/dist/tools/handlers/camera.d.ts +19 -0
  54. package/dist/tools/handlers/camera.d.ts.map +1 -0
  55. package/dist/tools/handlers/camera.js +94 -0
  56. package/dist/tools/handlers/camera.js.map +1 -0
  57. package/dist/tools/handlers/export.d.ts +12 -0
  58. package/dist/tools/handlers/export.d.ts.map +1 -0
  59. package/dist/tools/handlers/export.js +30 -0
  60. package/dist/tools/handlers/export.js.map +1 -0
  61. package/dist/tools/handlers/font.d.ts +12 -0
  62. package/dist/tools/handlers/font.d.ts.map +1 -0
  63. package/dist/tools/handlers/font.js +135 -0
  64. package/dist/tools/handlers/font.js.map +1 -0
  65. package/dist/tools/handlers/maps.d.ts +19 -0
  66. package/dist/tools/handlers/maps.d.ts.map +1 -0
  67. package/dist/tools/handlers/maps.js +184 -0
  68. package/dist/tools/handlers/maps.js.map +1 -0
  69. package/dist/tools/handlers/ontology.d.ts +20 -0
  70. package/dist/tools/handlers/ontology.d.ts.map +1 -0
  71. package/dist/tools/handlers/ontology.js +185 -0
  72. package/dist/tools/handlers/ontology.js.map +1 -0
  73. package/dist/tools/handlers/tool-guide.d.ts +14 -0
  74. package/dist/tools/handlers/tool-guide.d.ts.map +1 -0
  75. package/dist/tools/handlers/tool-guide.js +52 -0
  76. package/dist/tools/handlers/tool-guide.js.map +1 -0
  77. package/dist/tools/handlers.d.ts +18 -0
  78. package/dist/tools/handlers.d.ts.map +1 -1
  79. package/dist/tools/handlers.js +321 -574
  80. package/dist/tools/handlers.js.map +1 -1
  81. package/dist/tools/minimal-descriptions.d.ts +4 -0
  82. package/dist/tools/minimal-descriptions.d.ts.map +1 -1
  83. package/dist/tools/minimal-descriptions.js +65 -54
  84. package/dist/tools/minimal-descriptions.js.map +1 -1
  85. package/dist/tools/toolkits.d.ts.map +1 -1
  86. package/dist/tools/toolkits.js +67 -46
  87. package/dist/tools/toolkits.js.map +1 -1
  88. package/dist/types/code-generator.d.ts +49 -2
  89. package/dist/types/code-generator.d.ts.map +1 -1
  90. package/dist/types/code-generator.js +1456 -39
  91. package/dist/types/code-generator.js.map +1 -1
  92. package/dist/types/schemas.d.ts +1381 -404
  93. package/dist/types/schemas.d.ts.map +1 -1
  94. package/dist/types/schemas.js +426 -20
  95. package/dist/types/schemas.js.map +1 -1
  96. package/package.json +2 -2
@@ -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, KeyframeAnimateInputSchema, ExecuteGeneratorInputSchema, ApplyEffectInputSchema, GetItemsInputSchema, PlayTimelineInputSchema, SetBackgroundColorInputSchema, SetCanvasSizeInputSchema, ExportTrainingDataInputSchema, ImportSVGInputSchema, AddFilterInputSchema, CreateGlossySphereInputSchema, CreateDiagonalStripesInputSchema, BatchCreateInputSchema, BatchModifyInputSchema, CreateGridInputSchema, CreateSceneInputSchema, GetPerformanceMetricsInputSchema, DiagnosticReportInputSchema, SearchAssetsInputSchema, ImportAssetInputSchema, P5DrawInputSchema,
9
+ import { CreateItemInputSchema, ModifyItemInputSchema, DeleteItemInputSchema, AddRelationInputSchema, RemoveRelationInputSchema, QueryRelationsInputSchema, AnimateItemInputSchema, KeyframeAnimateInputSchema, ExecuteGeneratorInputSchema, ApplyEffectInputSchema, GetItemsInputSchema, PlayTimelineInputSchema, SetBackgroundColorInputSchema, SetCanvasSizeInputSchema, ImportSVGInputSchema, ImportMermaidInputSchema, AddFilterInputSchema, CreateGlossySphereInputSchema, CreateDiagonalStripesInputSchema, BatchCreateInputSchema, BatchModifyInputSchema, CreateGridInputSchema, 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
@@ -17,10 +17,6 @@ AddTriggerInputSchema, RemoveTriggerInputSchema, QueryTriggersInputSchema,
17
17
  CreateQuizInputSchema, GetQuizStateInputSchema, ResetQuizInputSchema,
18
18
  // Letter collage schemas
19
19
  CreateLetterCollageInputSchema, AnimateLetterCollageInputSchema,
20
- // Map schemas
21
- LoadMapInputSchema, HighlightRegionsInputSchema, UnhighlightRegionsInputSchema, ApplyDataColorsInputSchema, AddMarkerInputSchema, AddMapLabelsInputSchema, PanMapInputSchema, ZoomMapInputSchema, ImportCustomMapInputSchema, GetRegionAtPointInputSchema,
22
- // Map animation/CSV schemas
23
- AnimateMapRegionsInputSchema, AnimateMapWaveInputSchema, StopMapAnimationsInputSchema, ExportMapRegionCSVInputSchema, ImportMapRegionCSVInputSchema, SelectMapRegionsInputSchema, DeselectMapRegionsInputSchema,
24
20
  // Custom relation/code schemas
25
21
  RegisterCustomRelationInputSchema, ExecuteCustomCodeInputSchema,
26
22
  // Mask schemas
@@ -29,16 +25,39 @@ ApplyAnimatedMaskInputSchema, ApplyCustomMaskInputSchema, RemoveMaskInputSchema,
29
25
  ApplyTemplateInputSchema,
30
26
  // Image import schemas
31
27
  ImportImageInputSchema,
32
- // Tool guide schema
33
- ToolGuideInputSchema, ErrorCodes, } from '../types/schemas.js';
28
+ // Scene management schemas
29
+ ManageScenesInputSchema, ScenePlaybackInputSchema,
30
+ // New consolidated tool schemas
31
+ SelectionInputSchema, TransformInputSchema, HistoryInputSchema, ImageFilterInputSchema, LassoInputSchema, CutoutStyleInputSchema, PrecompInputSchema, ViewInputSchema, BackgroundInputSchema, QueryInputSchema, DeformInputSchema, SpriteSheetInputSchema, StorageInputSchema, InteractionInputSchema, ExportWidgetInputSchema, ExportWidgetHtmlInputSchema, CreateChartInputSchema, MagicInputSchema, PhysicsInputSchema, MeasurementInputSchema, ErrorCodes, } from '../types/schemas.js';
34
32
  import { ZodError } from 'zod';
35
33
  import { writeFile, mkdir } from 'node:fs/promises';
36
34
  import { tmpdir } from 'node:os';
37
35
  import { join } from 'node:path';
38
- import { getBrowserController, } from '../browser/puppeteer-controller.js';
36
+ import { getBrowserController, resetBrowserController, } from '../browser/puppeteer-controller.js';
37
+ import { sameStudioTarget, validateStudioUrl } from '../browser/url-target.js';
39
38
  import { getPerformanceTracker } from '../metrics/index.js';
40
39
  import { formatErrorContext, captureCanvasState } from '../execution/index.js';
41
40
  import { getSessionManager } from '../agent/session-manager.js';
41
+ import { vocabularyHintForPath, validateBatchVocabulary, detectBatchPropertyTypos } from '../ontology/hints.js';
42
+ import { cameraHandlers } from './handlers/camera.js';
43
+ import { fontHandlers } from './handlers/font.js';
44
+ import { toolGuideHandlers } from './handlers/tool-guide.js';
45
+ import { mapHandlers } from './handlers/maps.js';
46
+ import { ontologyHandlers } from './handlers/ontology.js';
47
+ import { exportHandlers } from './handlers/export.js';
48
+ /**
49
+ * Registry of per-domain handler maps. Tools listed here short-circuit the
50
+ * main switch at dispatch time (see handleToolCallInner). New domains are
51
+ * folded in incrementally as they are migrated out of the monolithic switch.
52
+ */
53
+ const DOMAIN_HANDLERS = [
54
+ cameraHandlers,
55
+ fontHandlers,
56
+ toolGuideHandlers,
57
+ mapHandlers,
58
+ ontologyHandlers,
59
+ exportHandlers,
60
+ ];
42
61
  /**
43
62
  * Get screenshot mode from environment variable.
44
63
  * Defaults to 'on_request' for optimal performance.
@@ -52,43 +71,6 @@ export function getScreenshotMode() {
52
71
  return 'on_request';
53
72
  }
54
73
  // =============================================================================
55
- // SMART DESIGN GUIDANCE — detect if user prompt has specific design direction
56
- // =============================================================================
57
- const COLOR_KEYWORDS = /(?:#[0-9a-f]{3,8}|rgb\(|hsl\(|\b(?:red|blue|green|purple|pink|gold|silver|orange|yellow|cyan|magenta|indigo|teal|coral|crimson|navy|maroon|turquoise|violet|amber|emerald|scarlet)\b)/i;
58
- const STYLE_KEYWORDS = /\b(?:gradient|bokeh|geometric|circuit|wave|sunset|sunrise|minimal|modern|retro|vintage|elegant|playful|abstract|organic|neon|pastel|watercolor|grunge|futuristic|cosmic|dreamy|glossy)\b/i;
59
- const LAYOUT_KEYWORDS = /\b(?:centered|left-aligned|right-aligned|grid|stacked|scattered|radial|circular|diagonal|symmetrical|horizontal|vertical)\b/i;
60
- const ANIMATION_KEYWORDS = /\b(?:orbit|follow|pulse|bounce|fade|rotate|wobble|slide|wipe|reveal|parallax|wave|morph|float|glow|sparkle|burst|spiral)\b/i;
61
- /**
62
- * Detect if a prompt has enough design specificity to skip guidance injection.
63
- * Returns true if the prompt is design-specific (skip guidance).
64
- * Returns false if vague (inject guidance).
65
- */
66
- function detectDesignSpecificity(prompt) {
67
- if (!prompt || prompt.trim().length === 0)
68
- return false;
69
- let specificityScore = 0;
70
- // Color mentions: strong signal of specificity
71
- if (COLOR_KEYWORDS.test(prompt))
72
- specificityScore += 2;
73
- // Style/mood keywords
74
- if (STYLE_KEYWORDS.test(prompt))
75
- specificityScore += 2;
76
- // Layout direction
77
- if (LAYOUT_KEYWORDS.test(prompt))
78
- specificityScore += 1;
79
- // Animation specifics
80
- if (ANIMATION_KEYWORDS.test(prompt))
81
- specificityScore += 1;
82
- // Word count: detailed prompts tend to be longer
83
- const wordCount = prompt.trim().split(/\s+/).length;
84
- if (wordCount >= 20)
85
- specificityScore += 1;
86
- if (wordCount >= 35)
87
- specificityScore += 1;
88
- // Threshold: 3+ = specific enough, skip guidance
89
- return specificityScore >= 3;
90
- }
91
- // =============================================================================
92
74
  // EXPORT FILE SAVE HELPERS
93
75
  // =============================================================================
94
76
  function getExportDir() {
@@ -150,18 +132,13 @@ function successResult(code, description) {
150
132
  return { content };
151
133
  }
152
134
  function executedResult(code, result, screenshot, description) {
135
+ const resultObj = { success: true, result };
136
+ if (description)
137
+ resultObj.description = description;
153
138
  const content = [
154
- {
155
- type: 'text',
156
- text: `Executed PinePaper code:\n\n\`\`\`javascript\n${code}\n\`\`\`\n\nResult: ${JSON.stringify(result, null, 2)}`,
157
- },
139
+ { type: 'text', text: JSON.stringify(resultObj, null, 2) },
140
+ { type: 'text', text: `Generated code:\n\`\`\`javascript\n${code}\n\`\`\`` },
158
141
  ];
159
- if (description) {
160
- content.push({
161
- type: 'text',
162
- text: `\n${description}`,
163
- });
164
- }
165
142
  if (screenshot) {
166
143
  content.push({
167
144
  type: 'image',
@@ -182,7 +159,7 @@ function screenshotResult(screenshot) {
182
159
  ],
183
160
  };
184
161
  }
185
- function errorResult(code, message, details, context) {
162
+ export function errorResult(code, message, details, context) {
186
163
  // Build error object
187
164
  const errorObj = {
188
165
  success: false,
@@ -209,10 +186,18 @@ function handleValidationError(error, i18n) {
209
186
  const message = i18n
210
187
  ? i18n.getError('validationError', { message: 'Invalid input parameters' })
211
188
  : 'Invalid input parameters';
212
- return errorResult(ErrorCodes.VALIDATION_ERROR, message, error.errors.map((e) => ({
213
- path: e.path.join('.'),
214
- message: e.message,
215
- })));
189
+ const details = error.errors.map((e) => {
190
+ const path = e.path.join('.');
191
+ const detail = { path, message: e.message };
192
+ if (e.code === 'invalid_enum_value') {
193
+ const received = e.received;
194
+ const hint = vocabularyHintForPath(path, received);
195
+ if (hint)
196
+ detail.vocabulary = hint;
197
+ }
198
+ return detail;
199
+ });
200
+ return errorResult(ErrorCodes.VALIDATION_ERROR, message, details);
216
201
  }
217
202
  /**
218
203
  * Execute code in browser if connected, otherwise return generated code.
@@ -228,7 +213,7 @@ function handleValidationError(error, i18n) {
228
213
  *
229
214
  * Default is 'on_request' for optimal performance per best practices.
230
215
  */
231
- async function executeOrGenerate(code, description, options, toolName) {
216
+ export async function executeOrGenerate(code, description, options, toolName) {
232
217
  const { executeInBrowser, browserController, screenshotMode, executionMode } = options;
233
218
  const effectiveExecutionMode = executionMode ?? getExecutionMode();
234
219
  const effectiveScreenshotMode = screenshotMode ?? getScreenshotMode();
@@ -405,7 +390,7 @@ ${code}
405
390
  // =============================================================================
406
391
  // I18N HELPER FUNCTIONS
407
392
  // =============================================================================
408
- function getLocalizedSuccessMessage(i18n, key, params) {
393
+ export function getLocalizedSuccessMessage(i18n, key, params) {
409
394
  if (!i18n) {
410
395
  // Fallback to English template
411
396
  return Object.entries(params).reduce((msg, [k, v]) => msg.replace(`{{${k}}}`, String(v)), key);
@@ -489,6 +474,14 @@ export async function handleToolCall(toolName, args, options = {}) {
489
474
  async function handleToolCallInner(toolName, args, options, tracker, baseTimerId) {
490
475
  const { i18n } = options;
491
476
  try {
477
+ // Domain-module dispatch: tools migrated to per-domain handler files
478
+ // short-circuit before the main switch. See DOMAIN_HANDLERS for the
479
+ // current registry.
480
+ for (const domainMap of DOMAIN_HANDLERS) {
481
+ const handler = domainMap[toolName];
482
+ if (handler)
483
+ return await handler(args, options);
484
+ }
492
485
  switch (toolName) {
493
486
  // -----------------------------------------------------------------------
494
487
  // ITEM TOOLS
@@ -529,7 +522,7 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
529
522
  const animProps = ['animationType', 'animationSpeed', 'keyframes', 'animation'];
530
523
  const passedAnimProps = animProps.filter(p => p in (input.properties || {}));
531
524
  if (passedAnimProps.length > 0) {
532
- return errorResult('INVALID_PROPERTIES', `Animation properties (${passedAnimProps.join(', ')}) cannot be set via pinepaper_modify_item — they will be silently ignored. ` +
525
+ return errorResult(ErrorCodes.INVALID_PROPERTIES, `Animation properties (${passedAnimProps.join(', ')}) cannot be set via pinepaper_modify_item — they will be silently ignored. ` +
533
526
  `Use pinepaper_animate for loop animations (pulse, rotate, bounce, fade, wobble, slide) ` +
534
527
  `or pinepaper_keyframe_animate for timed/sequenced animations with precise control.`);
535
528
  }
@@ -571,6 +564,134 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
571
564
  const description = `Creates complete scene with ${itemCount} items, ${relationCount} relations, ${animationCount} animations`;
572
565
  return executeOrGenerate(code, description, options, 'pinepaper_create_scene');
573
566
  }
567
+ case 'pinepaper_manage_scenes': {
568
+ const input = ManageScenesInputSchema.parse(args);
569
+ const code = codeGenerator.generateManageScenes(input);
570
+ return executeOrGenerate(code, `Scene: ${input.action}`, options, 'pinepaper_manage_scenes');
571
+ }
572
+ case 'pinepaper_scene_playback': {
573
+ const input = ScenePlaybackInputSchema.parse(args);
574
+ const code = codeGenerator.generateScenePlayback(input);
575
+ return executeOrGenerate(code, `Scene playback: ${input.action}`, options, 'pinepaper_scene_playback');
576
+ }
577
+ // -----------------------------------------------------------------------
578
+ // SELECTION, TRANSFORM & HISTORY TOOLS
579
+ // -----------------------------------------------------------------------
580
+ case 'pinepaper_selection': {
581
+ const input = SelectionInputSchema.parse(args);
582
+ const code = codeGenerator.generateSelection(input);
583
+ return executeOrGenerate(code, `Selection: ${input.action}`, options, 'pinepaper_selection');
584
+ }
585
+ case 'pinepaper_transform': {
586
+ const input = TransformInputSchema.parse(args);
587
+ const code = codeGenerator.generateTransform(input);
588
+ return executeOrGenerate(code, `Transform: ${input.action}`, options, 'pinepaper_transform');
589
+ }
590
+ case 'pinepaper_history': {
591
+ const input = HistoryInputSchema.parse(args);
592
+ const code = codeGenerator.generateHistory(input);
593
+ return executeOrGenerate(code, `History: ${input.action}`, options, 'pinepaper_history');
594
+ }
595
+ // -----------------------------------------------------------------------
596
+ // IMAGE PROCESSING TOOLS
597
+ // -----------------------------------------------------------------------
598
+ case 'pinepaper_image_filter': {
599
+ const input = ImageFilterInputSchema.parse(args);
600
+ const code = codeGenerator.generateImageFilter(input);
601
+ return executeOrGenerate(code, `Image filter: ${input.action}`, options, 'pinepaper_image_filter');
602
+ }
603
+ case 'pinepaper_lasso': {
604
+ const input = LassoInputSchema.parse(args);
605
+ const code = codeGenerator.generateLasso(input);
606
+ return executeOrGenerate(code, `Lasso: ${input.action}`, options, 'pinepaper_lasso');
607
+ }
608
+ case 'pinepaper_cutout_style': {
609
+ const input = CutoutStyleInputSchema.parse(args);
610
+ const code = codeGenerator.generateCutoutStyle(input);
611
+ return executeOrGenerate(code, `Cutout style: ${input.action}`, options, 'pinepaper_cutout_style');
612
+ }
613
+ // -----------------------------------------------------------------------
614
+ // COMPOSITION & VIEW TOOLS
615
+ // -----------------------------------------------------------------------
616
+ case 'pinepaper_precomp': {
617
+ const input = PrecompInputSchema.parse(args);
618
+ const code = codeGenerator.generatePrecomp(input);
619
+ return executeOrGenerate(code, `Precomp: ${input.action}`, options, 'pinepaper_precomp');
620
+ }
621
+ case 'pinepaper_view': {
622
+ const input = ViewInputSchema.parse(args);
623
+ const code = codeGenerator.generateView(input);
624
+ return executeOrGenerate(code, `View: ${input.action}`, options, 'pinepaper_view');
625
+ }
626
+ case 'pinepaper_background': {
627
+ const input = BackgroundInputSchema.parse(args);
628
+ const code = codeGenerator.generateBackground(input);
629
+ return executeOrGenerate(code, `Background: ${input.action}`, options, 'pinepaper_background');
630
+ }
631
+ // -----------------------------------------------------------------------
632
+ // CANVAS QUERY TOOLS
633
+ // -----------------------------------------------------------------------
634
+ case 'pinepaper_query': {
635
+ const input = QueryInputSchema.parse(args);
636
+ const code = codeGenerator.generateQuery(input);
637
+ return executeOrGenerate(code, `Query: ${input.action}`, options, 'pinepaper_query');
638
+ }
639
+ // -----------------------------------------------------------------------
640
+ // DEFORMATION, SPRITE SHEETS, STORAGE, INTERACTION
641
+ // -----------------------------------------------------------------------
642
+ case 'pinepaper_deform': {
643
+ const input = DeformInputSchema.parse(args);
644
+ const code = codeGenerator.generateDeform(input);
645
+ return executeOrGenerate(code, `Deform: ${input.action}`, options, 'pinepaper_deform');
646
+ }
647
+ case 'pinepaper_sprite_sheet': {
648
+ const input = SpriteSheetInputSchema.parse(args);
649
+ const code = codeGenerator.generateSpriteSheet(input);
650
+ return executeOrGenerate(code, `Sprite sheet: ${input.action}`, options, 'pinepaper_sprite_sheet');
651
+ }
652
+ case 'pinepaper_storage': {
653
+ const input = StorageInputSchema.parse(args);
654
+ const code = codeGenerator.generateStorage(input);
655
+ return executeOrGenerate(code, `Storage: ${input.action}`, options, 'pinepaper_storage');
656
+ }
657
+ case 'pinepaper_interaction': {
658
+ const input = InteractionInputSchema.parse(args);
659
+ const code = codeGenerator.generateInteraction(input);
660
+ return executeOrGenerate(code, `Interaction: ${input.action}`, options, 'pinepaper_interaction');
661
+ }
662
+ case 'pinepaper_export_widget': {
663
+ const input = ExportWidgetInputSchema.parse(args);
664
+ const code = codeGenerator.generateExportWidget(input);
665
+ return executeOrGenerate(code, 'Export widget JSON', options, 'pinepaper_export_widget');
666
+ }
667
+ case 'pinepaper_export_widget_html': {
668
+ const input = ExportWidgetHtmlInputSchema.parse(args);
669
+ const code = codeGenerator.generateExportWidgetHtml(input);
670
+ return executeOrGenerate(code, 'Export widget HTML', options, 'pinepaper_export_widget_html');
671
+ }
672
+ // -----------------------------------------------------------------------
673
+ // DATA VISUALIZATION, MAGIC, PHYSICS, MEASUREMENT
674
+ // -----------------------------------------------------------------------
675
+ case 'pinepaper_create_chart': {
676
+ const input = CreateChartInputSchema.parse(args);
677
+ const code = codeGenerator.generateChart(input);
678
+ return executeOrGenerate(code, `Chart: ${input.action}`, options, 'pinepaper_create_chart');
679
+ }
680
+ case 'pinepaper_magic': {
681
+ const input = MagicInputSchema.parse(args);
682
+ const code = codeGenerator.generateMagic(input);
683
+ return executeOrGenerate(code, `Magic: ${input.action}`, options, 'pinepaper_magic');
684
+ }
685
+ case 'pinepaper_physics': {
686
+ const input = PhysicsInputSchema.parse(args);
687
+ const code = codeGenerator.generatePhysics(input);
688
+ return executeOrGenerate(code, `Physics: ${input.action}`, options, 'pinepaper_physics');
689
+ }
690
+ case 'pinepaper_measurement': {
691
+ const input = MeasurementInputSchema.parse(args);
692
+ const code = codeGenerator.generateMeasurement(input);
693
+ return executeOrGenerate(code, `Measurement: ${input.action}`, options, 'pinepaper_measurement');
694
+ }
574
695
  // -----------------------------------------------------------------------
575
696
  // BATCH OPERATION TOOLS
576
697
  // -----------------------------------------------------------------------
@@ -714,168 +835,11 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
714
835
  return executeOrGenerate(code, 'Gets available mask animation presets', options, 'pinepaper_get_mask_animations');
715
836
  }
716
837
  // -----------------------------------------------------------------------
717
- // CAMERA TOOLS
718
- // -----------------------------------------------------------------------
719
- case 'pinepaper_camera_animate': {
720
- const keyframes = args.keyframes;
721
- const duration = args.duration;
722
- const loop = args.loop ?? false;
723
- const delay = args.delay ?? 0;
724
- const keyframesStr = JSON.stringify(keyframes);
725
- const code = `app.camera && app.camera.animate ? app.camera.animate(${keyframesStr}, ${duration}, ${loop}, ${delay}) : app.addRelation('camera', 'camera', 'camera_animates', { keyframes: ${keyframesStr}, duration: ${duration}, loop: ${loop}, delay: ${delay} });`;
726
- return executeOrGenerate(code, `Animates camera with ${keyframes.length} keyframes over ${duration}s`, options, 'pinepaper_camera_animate');
727
- }
728
- case 'pinepaper_camera_zoom': {
729
- const direction = args.direction;
730
- const level = args.level ?? (direction === 'in' ? 2 : 0.5);
731
- const duration = args.duration ?? 0.5;
732
- const method = direction === 'in' ? 'zoomIn' : 'zoomOut';
733
- const code = `app.camera && app.camera.${method} ? app.camera.${method}(${level}, ${duration}) : null;`;
734
- return executeOrGenerate(code, `Camera zoom ${direction} to ${level}x`, options, 'pinepaper_camera_zoom');
735
- }
736
- case 'pinepaper_camera_pan': {
737
- const direction = args.direction;
738
- const amount = args.amount ?? 100;
739
- const x = args.x;
740
- const y = args.y;
741
- const duration = args.duration ?? 0.5;
742
- let code;
743
- let description;
744
- if (x !== undefined && y !== undefined) {
745
- code = `app.camera && app.camera.panTo ? app.camera.panTo(${x}, ${y}, ${duration}) : null;`;
746
- description = `Camera pan to (${x}, ${y})`;
747
- }
748
- else if (direction) {
749
- const methodMap = { left: 'panLeft', right: 'panRight', up: 'panUp', down: 'panDown' };
750
- const method = methodMap[direction];
751
- code = `app.camera && app.camera.${method} ? app.camera.${method}(${amount}, ${duration}) : null;`;
752
- description = `Camera pan ${direction} by ${amount}px`;
753
- }
754
- else {
755
- code = `// No direction or coordinates specified`;
756
- description = 'Camera pan (no parameters)';
757
- }
758
- return executeOrGenerate(code, description, options, 'pinepaper_camera_pan');
759
- }
760
- case 'pinepaper_camera_move_to': {
761
- const x = args.x;
762
- const y = args.y;
763
- const zoom = args.zoom;
764
- const duration = args.duration ?? 0.5;
765
- const code = `app.camera && app.camera.moveTo ? app.camera.moveTo(${x}, ${y}, ${zoom}, ${duration}) : null;`;
766
- return executeOrGenerate(code, `Camera move to (${x}, ${y}) at ${zoom}x zoom`, options, 'pinepaper_camera_move_to');
767
- }
768
- case 'pinepaper_camera_reset': {
769
- const duration = args.duration ?? 0.5;
770
- const code = `app.camera && app.camera.reset ? app.camera.reset(${duration}) : null;`;
771
- return executeOrGenerate(code, 'Reset camera to default state', options, 'pinepaper_camera_reset');
772
- }
773
- case 'pinepaper_camera_stop': {
774
- const code = `app.camera && app.camera.stop ? app.camera.stop() : null;`;
775
- return executeOrGenerate(code, 'Stop camera animation', options, 'pinepaper_camera_stop');
776
- }
777
- case 'pinepaper_camera_state': {
778
- const code = `app.camera && app.camera.getState ? app.camera.getState() : { zoom: 1, center: [400, 300], isAnimating: false };`;
779
- return executeOrGenerate(code, 'Get current camera state', options, 'pinepaper_camera_state');
780
- }
781
- // -----------------------------------------------------------------------
782
- // FONT TOOLS
783
- // -----------------------------------------------------------------------
784
- case 'pinepaper_font_show_studio': {
785
- const code = `app.fontStudio && app.fontStudio.show ? app.fontStudio.show() : null;`;
786
- return executeOrGenerate(code, 'Opens Font Studio UI', options, 'pinepaper_font_show_studio');
787
- }
788
- case 'pinepaper_font_set_name': {
789
- const { name } = args;
790
- const code = `app.fontStudio.setName(${JSON.stringify(name)});`;
791
- return executeOrGenerate(code, `Set font name to "${name}"`, options, 'pinepaper_font_set_name');
792
- }
793
- case 'pinepaper_font_get_required_chars': {
794
- const { set } = args;
795
- const setArg = set ? JSON.stringify(set) : '"minimum"';
796
- const code = `app.fontStudio.getRequiredChars(${setArg});`;
797
- return executeOrGenerate(code, `Get required characters (${set || 'minimum'} set)`, options, 'pinepaper_font_get_required_chars');
798
- }
799
- case 'pinepaper_font_get_status': {
800
- const code = `app.fontStudio.getStatus();`;
801
- return executeOrGenerate(code, 'Get font completion status', options, 'pinepaper_font_get_status');
802
- }
803
- case 'pinepaper_font_create_glyph': {
804
- const { character, pathId } = args;
805
- const code = `app.fontStudio.createGlyph(${JSON.stringify(character)}, ${JSON.stringify(pathId)});`;
806
- return executeOrGenerate(code, `Create glyph for "${character}" from path ${pathId}`, options, 'pinepaper_font_create_glyph');
807
- }
808
- case 'pinepaper_font_create_space': {
809
- const { width } = args;
810
- const code = width !== undefined
811
- ? `app.fontStudio.createSpace(${width});`
812
- : `app.fontStudio.createSpace();`;
813
- return executeOrGenerate(code, `Create space glyph${width ? ` (width: ${width})` : ''}`, options, 'pinepaper_font_create_space');
814
- }
815
- case 'pinepaper_font_remove_glyph': {
816
- const { character } = args;
817
- const code = `app.fontStudio.removeGlyph(${JSON.stringify(character)});`;
818
- return executeOrGenerate(code, `Remove glyph for "${character}"`, options, 'pinepaper_font_remove_glyph');
819
- }
820
- case 'pinepaper_font_set_metrics': {
821
- const metrics = args;
822
- const code = `app.fontStudio.setMetrics(${JSON.stringify(metrics)});`;
823
- return executeOrGenerate(code, 'Set font metrics', options, 'pinepaper_font_set_metrics');
824
- }
825
- case 'pinepaper_font_export': {
826
- const { download } = args;
827
- const code = download === false
828
- ? `app.fontStudio.export({ download: false });`
829
- : `app.fontStudio.export();`;
830
- return executeOrGenerate(code, 'Export font as OTF', options, 'pinepaper_font_export');
831
- }
832
- case 'pinepaper_font_load_into_document': {
833
- const code = `app.fontStudio.loadIntoDocument();`;
834
- return executeOrGenerate(code, 'Load font into document', options, 'pinepaper_font_load_into_document');
835
- }
836
- case 'pinepaper_font_export_data': {
837
- const { download } = args;
838
- const code = download === false
839
- ? `app.fontStudio.exportData({ download: false });`
840
- : `app.fontStudio.exportData();`;
841
- return executeOrGenerate(code, 'Export font data as JSON', options, 'pinepaper_font_export_data');
842
- }
843
- case 'pinepaper_font_import_data': {
844
- const { data } = args;
845
- const code = `app.fontStudio.importData(${JSON.stringify(data)});`;
846
- return executeOrGenerate(code, 'Import font data from JSON', options, 'pinepaper_font_import_data');
847
- }
848
- case 'pinepaper_font_clear': {
849
- const code = `app.fontStudio.clear();`;
850
- return executeOrGenerate(code, 'Clear all glyphs and reset font', options, 'pinepaper_font_clear');
851
- }
852
- case 'pinepaper_font_remove_overlap': {
853
- const { pathId } = args;
854
- const code = `app.fontStudio.removeOverlap(${JSON.stringify(pathId)});`;
855
- return executeOrGenerate(code, `Remove overlaps from path ${pathId}`, options, 'pinepaper_font_remove_overlap');
856
- }
857
- case 'pinepaper_font_correct_direction': {
858
- const { pathId } = args;
859
- const code = `app.fontStudio.correctDirection(${JSON.stringify(pathId)});`;
860
- return executeOrGenerate(code, `Correct path direction for ${pathId}`, options, 'pinepaper_font_correct_direction');
861
- }
862
- case 'pinepaper_font_cleanup_path': {
863
- const { pathId, removeOverlap, correctDirection, smooth, smoothTolerance } = args;
864
- const optsObj = {};
865
- if (removeOverlap !== undefined)
866
- optsObj.removeOverlap = removeOverlap;
867
- if (correctDirection !== undefined)
868
- optsObj.correctDirection = correctDirection;
869
- if (smooth !== undefined)
870
- optsObj.smooth = smooth;
871
- if (smoothTolerance !== undefined)
872
- optsObj.smoothTolerance = smoothTolerance;
873
- const hasOpts = Object.keys(optsObj).length > 0;
874
- const code = hasOpts
875
- ? `app.fontStudio.cleanupPath(${JSON.stringify(pathId)}, ${JSON.stringify(optsObj)});`
876
- : `app.fontStudio.cleanupPath(${JSON.stringify(pathId)});`;
877
- return executeOrGenerate(code, `Cleanup path ${pathId}`, options, 'pinepaper_font_cleanup_path');
878
- }
838
+ // CAMERA TOOLS — extracted to src/tools/handlers/camera.ts
839
+ // -----------------------------------------------------------------------
840
+ // -----------------------------------------------------------------------
841
+ // FONT TOOLS — extracted to src/tools/handlers/font.ts
842
+ // -----------------------------------------------------------------------
879
843
  // -----------------------------------------------------------------------
880
844
  // QUERY TOOLS
881
845
  // -----------------------------------------------------------------------
@@ -920,7 +884,7 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
920
884
  case 'pinepaper_refresh_page': {
921
885
  const controller = options.browserController || getBrowserController();
922
886
  if (!controller.connected) {
923
- return errorResult('BROWSER_NOT_CONNECTED', 'Not connected to PinePaper Studio. Call pinepaper_browser_connect first.');
887
+ return errorResult(ErrorCodes.BROWSER_NOT_CONNECTED, 'Not connected to PinePaper Studio. Call pinepaper_browser_connect first.');
924
888
  }
925
889
  try {
926
890
  // Refresh the page
@@ -949,7 +913,7 @@ You can now start creating new items on a clean canvas.`,
949
913
  return { content };
950
914
  }
951
915
  catch (error) {
952
- return errorResult('REFRESH_FAILED', error instanceof Error ? error.message : 'Failed to refresh page');
916
+ return errorResult(ErrorCodes.REFRESH_FAILED, error instanceof Error ? error.message : 'Failed to refresh page');
953
917
  }
954
918
  }
955
919
  // -----------------------------------------------------------------------
@@ -961,6 +925,15 @@ You can now start creating new items on a clean canvas.`,
961
925
  const description = 'Imports SVG onto the canvas';
962
926
  return executeOrGenerate(code, description, options, 'pinepaper_import_svg');
963
927
  }
928
+ case 'pinepaper_import_mermaid': {
929
+ const input = ImportMermaidInputSchema.parse(args);
930
+ const code = codeGenerator.generateImportMermaid(input.mermaidText, {
931
+ autoLayout: input.autoLayout,
932
+ clearExisting: input.clearExisting,
933
+ });
934
+ const description = 'Imports a Mermaid diagram onto the canvas';
935
+ return executeOrGenerate(code, description, options, 'pinepaper_import_mermaid');
936
+ }
964
937
  // -----------------------------------------------------------------------
965
938
  // FILTER TOOLS
966
939
  // -----------------------------------------------------------------------
@@ -971,111 +944,57 @@ You can now start creating new items on a clean canvas.`,
971
944
  return executeOrGenerate(code, description, options, 'pinepaper_add_filter');
972
945
  }
973
946
  // -----------------------------------------------------------------------
974
- // EXPORT TOOLS
947
+ // EXPORT TOOLS — extracted to src/tools/handlers/export.ts
975
948
  // -----------------------------------------------------------------------
976
- case 'pinepaper_export_svg': {
977
- const code = codeGenerator.generateExportSVG();
978
- const description = getLocalizedSuccessMessage(i18n, 'exported', {
979
- format: 'SVG',
980
- });
981
- return executeOrGenerate(code, description, options, 'pinepaper_export_svg');
982
- }
983
- case 'pinepaper_export_training_data': {
984
- const input = ExportTrainingDataInputSchema.parse(args);
985
- const code = codeGenerator.generateExportTrainingData(input.format, input.includeMetadata);
986
- const description = getLocalizedSuccessMessage(i18n, 'exported', {
987
- format: input.format?.toUpperCase() || 'JSON',
988
- });
989
- return executeOrGenerate(code, description, options, 'pinepaper_export_training_data');
990
- }
991
- case 'pinepaper_export_scene': {
992
- const code = codeGenerator.generateExportScene();
993
- return executeOrGenerate(code, 'Exports complete scene state', options, 'pinepaper_export_scene');
994
- }
995
949
  // -----------------------------------------------------------------------
996
950
  // BROWSER CONTROL TOOLS
997
951
  // -----------------------------------------------------------------------
998
952
  case 'pinepaper_browser_connect': {
999
- const url = args.url || 'https://pinepaper.studio';
1000
- // Default to headless: true for enforced agent mode
1001
- const headless = args.headless ?? true;
1002
- const controller = getBrowserController({ studioUrl: url, headless });
1003
- if (controller.connected) {
953
+ // Default to headless: false — if you explicitly call browser_connect, you likely want to see it
954
+ const headless = args.headless ?? false;
955
+ // Target a custom Studio instance (e.g. local dev at http://localhost:3000).
956
+ // Falls back to the PINEPAPER_STUDIO_URL env var, else PuppeteerController's
957
+ // built-in prod default. "/editor" + agent params are appended by the controller.
958
+ const rawUrl = typeof args.url === 'string' ? args.url.trim() : '';
959
+ const studioUrl = (rawUrl || (process.env.PINEPAPER_STUDIO_URL || '').trim()) || undefined;
960
+ // Only http(s) is supported — reject file://, data://, javascript:, etc.
961
+ if (studioUrl) {
962
+ const v = validateStudioUrl(studioUrl);
963
+ if (!v.ok)
964
+ return errorResult(ErrorCodes.INVALID_INPUT, v.error);
965
+ }
966
+ const cfg = studioUrl ? { headless, studioUrl } : { headless };
967
+ const controller = getBrowserController(cfg);
968
+ const urlChanged = !!studioUrl && controller.connected &&
969
+ !sameStudioTarget(controller.studioUrl, studioUrl);
970
+ // Already connected, same headless mode, same URL → nothing to do
971
+ if (controller.connected && controller.isHeadless === headless && !urlChanged) {
1004
972
  return {
1005
- content: [
1006
- {
1007
- type: 'text',
1008
- text: `Already connected to PinePaper Studio at ${controller.actualUrl}. Do NOT call this tool again — proceed directly with pinepaper_agent_start_job.`,
1009
- },
1010
- ],
973
+ content: [{ type: 'text', text: `Already connected to PinePaper Studio (${controller.studioUrl || 'default'}, headless: ${headless}).` }],
1011
974
  };
1012
975
  }
976
+ // Connected but headless mode OR target URL changed → tear down so the
977
+ // new config takes effect (getBrowserController ignores config once the
978
+ // singleton exists).
979
+ if (controller.connected && (controller.isHeadless !== headless || urlChanged)) {
980
+ await resetBrowserController();
981
+ }
1013
982
  try {
1014
- await controller.connect();
1015
- // Get canvas size info to help agent understand the workspace
1016
- const canvasSizeResult = await controller.executeCode(`const size = app.getCanvasSize ? app.getCanvasSize() : { width: 800, height: 600 }; ({ width: size.width || 800, height: size.height || 600 });`, false);
983
+ const newController = controller.connected ? controller : getBrowserController(cfg);
984
+ if (!newController.connected)
985
+ await newController.connect();
986
+ const canvasSizeResult = await newController.executeCode(`const size = app.getCanvasSize ? app.getCanvasSize() : { width: 800, height: 600 }; ({ width: size.width || 800, height: size.height || 600 });`, false);
1017
987
  const canvasSize = canvasSizeResult.success
1018
988
  ? canvasSizeResult.result
1019
989
  : { width: 800, height: 600 };
1020
- const screenshot = await controller.takeScreenshot();
1021
- const content = [
1022
- {
1023
- type: 'text',
1024
- text: `✅ Connected to PinePaper Studio at ${controller.actualUrl}
1025
-
1026
- **Agent mode enabled** (agent=1&mode=agent parameters added automatically)
1027
- **Current canvas size: ${canvasSize.width}x${canvasSize.height}**
1028
-
1029
- ⚠️ IMPORTANT NOTES:
1030
- 1. **Welcome Template**: First-time visitors see a welcome template with example items. Use \`pinepaper_clear_canvas\` or \`pinepaper_refresh_page\` to start with an empty canvas.
1031
- 2. **Canvas Size**: For complex designs (cards, invitations, detailed graphics), call \`pinepaper_set_canvas_size\` FIRST to set an appropriate size like 1080x1080 or larger. The default 800x600 may be too small.
1032
- 3. **Verify with Screenshots**: After making changes, use \`pinepaper_browser_screenshot\` to confirm your work.
1033
-
1034
- Browser is ready. You can now use other pinepaper tools to create and animate graphics.`,
1035
- },
1036
- ];
1037
- if (screenshot) {
1038
- content.push({
1039
- type: 'image',
1040
- data: screenshot,
1041
- mimeType: 'image/png',
1042
- });
1043
- }
1044
- return { content };
990
+ return {
991
+ content: [{ type: 'text', text: `Connected to PinePaper Studio (${newController.studioUrl || 'default'}, headless: ${headless}, canvas: ${canvasSize.width}x${canvasSize.height}).` }],
992
+ };
1045
993
  }
1046
994
  catch (error) {
1047
- const errorMessage = error instanceof Error ? error.message : 'Failed to connect to browser';
1048
- // Provide helpful fallback instructions instead of just failing
995
+ const errorMessage = error instanceof Error ? error.message : 'Failed to connect';
1049
996
  return {
1050
- content: [
1051
- {
1052
- type: 'text',
1053
- text: `⚠️ **Browser Connection Failed**
1054
-
1055
- **Error:** ${errorMessage}
1056
-
1057
- ## Don't worry! You can still use PinePaper manually:
1058
-
1059
- ### Option 1: Manual Browser Mode
1060
- 1. Open **PinePaper Studio** in your browser: https://pinepaper.studio/editor?agent=1&mode=agent
1061
- 2. Continue using PinePaper tools - the MCP server will generate code for each action
1062
- 3. Copy the generated code and paste it into the **Code Console** (</> icon in toolbar)
1063
-
1064
- ### Option 2: Troubleshoot Browser Connection
1065
- - **Check Chrome/Chromium**: Puppeteer requires Chrome or Chromium installed
1066
- - **Try headless: false**: Call \`pinepaper_browser_connect\` with \`headless: false\` to see the browser window
1067
- - **Check permissions**: Ensure the process has permission to launch browsers
1068
- - **Firewall**: Check if firewall is blocking browser launch
1069
-
1070
- ### What happens now?
1071
- All PinePaper tools will work in **code-only mode**:
1072
- - Each tool generates JavaScript code
1073
- - You copy the code and paste it in PinePaper's Code Console
1074
- - The code executes and creates your graphics
1075
-
1076
- **The MCP server is fully functional** - just without automatic browser control.`,
1077
- },
1078
- ],
997
+ content: [{ type: 'text', text: `Browser connection failed: ${errorMessage}` }],
1079
998
  };
1080
999
  }
1081
1000
  }
@@ -1104,13 +1023,13 @@ All PinePaper tools will work in **code-only mode**:
1104
1023
  case 'pinepaper_browser_screenshot': {
1105
1024
  const controller = getBrowserController();
1106
1025
  if (!controller.connected) {
1107
- return errorResult('BROWSER_NOT_CONNECTED', 'Not connected to PinePaper Studio. Call pinepaper_browser_connect first.');
1026
+ return errorResult(ErrorCodes.BROWSER_NOT_CONNECTED, 'Not connected to PinePaper Studio. Call pinepaper_browser_connect first.');
1108
1027
  }
1109
1028
  const screenshot = await controller.takeScreenshot();
1110
1029
  if (!screenshot) {
1111
1030
  // Capture canvas state for debugging
1112
1031
  const canvasState = await captureCanvasState(controller);
1113
- return errorResult('SCREENSHOT_FAILED', 'Failed to capture screenshot', undefined, {
1032
+ return errorResult(ErrorCodes.SCREENSHOT_FAILED, 'Failed to capture screenshot', undefined, {
1114
1033
  toolName: 'pinepaper_browser_screenshot',
1115
1034
  canvasState: canvasState || undefined,
1116
1035
  });
@@ -1397,54 +1316,8 @@ All PinePaper tools will work in **code-only mode**:
1397
1316
  };
1398
1317
  }
1399
1318
  // -----------------------------------------------------------------------
1400
- // TOOL GUIDE (on-demand documentation)
1319
+ // TOOL GUIDE — extracted to src/tools/handlers/tool-guide.ts
1401
1320
  // -----------------------------------------------------------------------
1402
- case 'pinepaper_tool_guide': {
1403
- const input = ToolGuideInputSchema.parse(args);
1404
- const { PINEPAPER_TOOLS, AI_AGENT_GUIDE } = await import('./definitions.js');
1405
- const { TOOL_TAGS } = await import('./toolkits.js');
1406
- // No args → full AI Agent Guide
1407
- if (!input.tool && !input.category) {
1408
- return {
1409
- content: [{ type: 'text', text: AI_AGENT_GUIDE }],
1410
- };
1411
- }
1412
- // Specific tool → full verbose description
1413
- if (input.tool) {
1414
- const tool = PINEPAPER_TOOLS.find((t) => t.name === input.tool);
1415
- if (!tool) {
1416
- return {
1417
- content: [{ type: 'text', text: `Unknown tool: ${input.tool}. Use pinepaper_tool_guide with no args to see the AI Agent Guide.` }],
1418
- isError: true,
1419
- };
1420
- }
1421
- return {
1422
- content: [{ type: 'text', text: `# ${tool.name}\n\n${tool.description}` }],
1423
- };
1424
- }
1425
- // Category → all tools in that tag with descriptions
1426
- if (input.category) {
1427
- const toolNames = TOOL_TAGS[input.category];
1428
- if (!toolNames) {
1429
- const available = Object.keys(TOOL_TAGS).join(', ');
1430
- return {
1431
- content: [{ type: 'text', text: `Unknown category: ${input.category}. Available: ${available}` }],
1432
- isError: true,
1433
- };
1434
- }
1435
- const lines = [`# Category: ${input.category}\n`];
1436
- for (const name of toolNames) {
1437
- const tool = PINEPAPER_TOOLS.find((t) => t.name === name);
1438
- if (tool) {
1439
- lines.push(`## ${tool.name}\n${tool.description}\n`);
1440
- }
1441
- }
1442
- return {
1443
- content: [{ type: 'text', text: lines.join('\n') }],
1444
- };
1445
- }
1446
- return { content: [{ type: 'text', text: AI_AGENT_GUIDE }] };
1447
- }
1448
1321
  // -----------------------------------------------------------------------
1449
1322
  // ASSET TOOLS
1450
1323
  // -----------------------------------------------------------------------
@@ -1660,33 +1533,9 @@ All PinePaper tools will work in **code-only mode**:
1660
1533
  const code = codeGenerator.generateAgentStartJob(input);
1661
1534
  const descriptionText = `Started agent job${input.name ? ` "${input.name}"` : ''} with ${input.screenshotPolicy || 'on_complete'} screenshot policy`;
1662
1535
  const result = await executeOrGenerate(code, descriptionText, options, 'pinepaper_agent_start_job');
1663
- // --- Smart design guidance injection ---
1536
+ // Append a 1-line workflow hint
1664
1537
  if (result.content && result.content.length > 0 && result.content[0].type === 'text') {
1665
- const prompt = (input.description || input.name || '').toLowerCase();
1666
- const isDesignSpecific = detectDesignSpecificity(prompt);
1667
- // Always include workflow hint
1668
- let guidance = `\n\nNEXT: Call pinepaper_agent_batch_execute with ALL operations in one call:` +
1669
- `\n 1. set_canvas_size / set_background / execute_generator (canvas setup)` +
1670
- `\n 2. create (items — text, shapes, etc.)` +
1671
- `\n 3. animate / keyframe_animate / relation (REQUIRED for animation — add for EACH item)` +
1672
- `\n 4. play_timeline (REQUIRED — starts playback)` +
1673
- `\nThen: pinepaper_agent_end_job (returns screenshot for validation)`;
1674
- // Inject design inspiration when the prompt is vague
1675
- if (!isDesignSpecific) {
1676
- guidance += `\n\n─── DESIGN INSPIRATION (your prompt is open-ended — use these for creative direction) ───` +
1677
- `\nBACKGROUND: Use execute_generator for rich visuals:` +
1678
- `\n Dreamy/soft: drawBokeh, drawGradientMesh, drawOrganicFlow` +
1679
- `\n Energetic: drawSunburst, drawSunsetScene, drawGeometricAbstract` +
1680
- `\n Tech/modern: drawCircuit, drawGrid, drawWindField` +
1681
- `\n Calm/organic: drawWaves, drawFluidFlow, drawNoiseTexture` +
1682
- `\nCOLORS: dark bg (#0f172a, #1a1a2e) + bright accents (#f472b6 pink, #818cf8 purple, #fbbf24 gold, #34d399 green)` +
1683
- `\nCOMPOSITION: generator bg → large decorative shapes (low opacity 0.3-0.5) → main text → small accents on top` +
1684
- `\nANIMATION: Use "relation" ops for dynamic motion:` +
1685
- `\n orbits (items circle around center), follows (items trail each other), wave_through (ripple effect)` +
1686
- `\n Plus: keyframe_animate for timed reveals (fade in, scale up), animate for loops (pulse, rotate, bounce)` +
1687
- `\n5s TIMING: bg 0s → shapes fade-in 0.5-1.5s → main text 2-3s → accents 3-4s → play_timeline`;
1688
- }
1689
- result.content[0].text += guidance;
1538
+ result.content[0].text += `\n\nNEXT: Call batch_execute with all operations, then end_job.`;
1690
1539
  }
1691
1540
  return result;
1692
1541
  }
@@ -1752,6 +1601,37 @@ All PinePaper tools will work in **code-only mode**:
1752
1601
  }
1753
1602
  case 'pinepaper_agent_batch_execute': {
1754
1603
  const input = AgentBatchExecuteInputSchema.parse(args);
1604
+ // Ontology preflight — catch typos in itemType/relationType/
1605
+ // effectType/generatorName before generating JS, so the LLM gets a
1606
+ // structured hint instead of a silent failure inside the browser.
1607
+ // Opt-out via skipValidation: true when the caller knows what they're
1608
+ // doing (e.g. experimental vocabulary).
1609
+ if (!input.skipValidation) {
1610
+ const ops = input.operations;
1611
+ const violations = validateBatchVocabulary(ops);
1612
+ if (violations.length > 0) {
1613
+ return errorResult(ErrorCodes.VALIDATION_ERROR, `Batch preflight rejected ${violations.length} operation${violations.length > 1 ? 's' : ''} with unknown vocabulary value${violations.length > 1 ? 's' : ''}. Fix the offending fields and retry, or pass skipValidation: true to bypass.`, violations.map((v) => ({
1614
+ path: `operations[${v.opIndex}].${v.field}`,
1615
+ opType: v.opType,
1616
+ vocabulary: v.hint,
1617
+ })));
1618
+ }
1619
+ // Property-key typo detection — narrow scope (Levenshtein ≤2) so
1620
+ // legitimate extras like `gradient` pass through. Catches the
1621
+ // common typo class (radiues vs radius, fontSiz vs fontSize)
1622
+ // before browser execution silently drops them.
1623
+ const typos = detectBatchPropertyTypos(ops);
1624
+ if (typos.length > 0) {
1625
+ return errorResult(ErrorCodes.VALIDATION_ERROR, `Batch preflight detected ${typos.length} likely property-key typo${typos.length > 1 ? 's' : ''}. Each suggestion is a Levenshtein-≤2 match against the schema for that itemType — fix and retry, or pass skipValidation: true to bypass.`, typos.map((t) => ({
1626
+ path: `operations[${t.opIndex}].properties.${t.property}`,
1627
+ opType: t.opType,
1628
+ itemType: t.itemType,
1629
+ received: t.property,
1630
+ suggestion: t.suggestion,
1631
+ validKeys: t.validKeys,
1632
+ })));
1633
+ }
1634
+ }
1755
1635
  const code = codeGenerator.generateAgentBatchExecute(input);
1756
1636
  const description = `Batch executed ${input.operations.length} operations${input.atomic !== false ? ' (atomic)' : ''}`;
1757
1637
  // Check for out-of-bounds items before execution
@@ -1926,140 +1806,9 @@ All PinePaper tools will work in **code-only mode**:
1926
1806
  // -----------------------------------------------------------------------
1927
1807
  // MAP TOOLS
1928
1808
  // -----------------------------------------------------------------------
1929
- case 'pinepaper_load_map': {
1930
- const input = LoadMapInputSchema.parse(args);
1931
- const code = codeGenerator.generateLoadMap(input);
1932
- const description = `Loads ${input.mapId} map with ${input.projection || 'default'} projection`;
1933
- return executeOrGenerate(code, description, options, 'pinepaper_load_map');
1934
- }
1935
- case 'pinepaper_highlight_regions': {
1936
- const input = HighlightRegionsInputSchema.parse(args);
1937
- const code = codeGenerator.generateHighlightRegions(input);
1938
- const description = `Highlights ${input.regionIds.length} region(s) on the map`;
1939
- return executeOrGenerate(code, description, options, 'pinepaper_highlight_regions');
1940
- }
1941
- case 'pinepaper_unhighlight_regions': {
1942
- const input = UnhighlightRegionsInputSchema.parse(args);
1943
- const code = codeGenerator.generateUnhighlightRegions(input);
1944
- return executeOrGenerate(code, 'Removes region highlights', options, 'pinepaper_unhighlight_regions');
1945
- }
1946
- case 'pinepaper_apply_data_colors': {
1947
- const input = ApplyDataColorsInputSchema.parse(args);
1948
- const code = codeGenerator.generateApplyDataColors(input);
1949
- const regionCount = Object.keys(input.data).length;
1950
- const description = `Applies choropleth coloring to ${regionCount} region(s)`;
1951
- return executeOrGenerate(code, description, options, 'pinepaper_apply_data_colors');
1952
- }
1953
- case 'pinepaper_add_marker': {
1954
- const input = AddMarkerInputSchema.parse(args);
1955
- const code = codeGenerator.generateAddMarker(input);
1956
- const description = `Adds marker at [${input.lat}, ${input.lon}]${input.label ? `: ${input.label}` : ''}`;
1957
- return executeOrGenerate(code, description, options, 'pinepaper_add_marker');
1958
- }
1959
- case 'pinepaper_add_map_labels': {
1960
- const input = AddMapLabelsInputSchema.parse(args);
1961
- const code = codeGenerator.generateAddMapLabels(input);
1962
- return executeOrGenerate(code, 'Adds labels to map regions', options, 'pinepaper_add_map_labels');
1963
- }
1964
- case 'pinepaper_pan_map': {
1965
- const input = PanMapInputSchema.parse(args);
1966
- const code = codeGenerator.generatePanMap(input);
1967
- const description = `Pans map to [${input.lat}, ${input.lon}]`;
1968
- return executeOrGenerate(code, description, options, 'pinepaper_pan_map');
1969
- }
1970
- case 'pinepaper_zoom_map': {
1971
- const input = ZoomMapInputSchema.parse(args);
1972
- const code = codeGenerator.generateZoomMap(input);
1973
- const description = `Sets map zoom level to ${input.level}`;
1974
- return executeOrGenerate(code, description, options, 'pinepaper_zoom_map');
1975
- }
1976
- case 'pinepaper_export_map': {
1977
- const code = codeGenerator.generateExportMap();
1978
- return executeOrGenerate(code, 'Exports map configuration', options, 'pinepaper_export_map');
1979
- }
1980
- case 'pinepaper_import_custom_map': {
1981
- const input = ImportCustomMapInputSchema.parse(args);
1982
- const code = codeGenerator.generateImportCustomMap(input);
1983
- const description = input.url ? `Imports custom map from URL` : 'Imports custom GeoJSON map';
1984
- return executeOrGenerate(code, description, options, 'pinepaper_import_custom_map');
1985
- }
1986
- case 'pinepaper_get_region_at_point': {
1987
- const input = GetRegionAtPointInputSchema.parse(args);
1988
- const code = codeGenerator.generateGetRegionAtPoint(input);
1989
- const description = `Gets region at point [${input.x}, ${input.y}]`;
1990
- return executeOrGenerate(code, description, options, 'pinepaper_get_region_at_point');
1991
- }
1992
- // -----------------------------------------------------------------------
1993
- // MAP ANIMATION/CSV TOOLS
1994
- // -----------------------------------------------------------------------
1995
- case 'pinepaper_animate_map_regions': {
1996
- const input = AnimateMapRegionsInputSchema.parse(args);
1997
- const code = codeGenerator.generateAnimateMapRegions(input);
1998
- const regionCount = Object.keys(input.regions).length;
1999
- const description = `Animates ${regionCount} map region(s) over ${input.duration || 5}s`;
2000
- return executeOrGenerate(code, description, options, 'pinepaper_animate_map_regions');
2001
- }
2002
- case 'pinepaper_animate_map_wave': {
2003
- const input = AnimateMapWaveInputSchema.parse(args);
2004
- const code = codeGenerator.generateAnimateMapWave(input);
2005
- const description = `Creates ${input.waveDirection || 'horizontal'} wave animation`;
2006
- return executeOrGenerate(code, description, options, 'pinepaper_animate_map_wave');
2007
- }
2008
- case 'pinepaper_stop_map_animations': {
2009
- const input = StopMapAnimationsInputSchema.parse(args);
2010
- const code = codeGenerator.generateStopMapAnimations(input);
2011
- return executeOrGenerate(code, 'Stops map region animations', options, 'pinepaper_stop_map_animations');
2012
- }
2013
- case 'pinepaper_get_animated_map_regions': {
2014
- const code = codeGenerator.generateGetAnimatedMapRegions();
2015
- return executeOrGenerate(code, 'Gets list of animated map regions', options, 'pinepaper_get_animated_map_regions');
2016
- }
2017
- case 'pinepaper_export_map_region_csv': {
2018
- const input = ExportMapRegionCSVInputSchema.parse(args);
2019
- const code = codeGenerator.generateExportMapRegionCSV(input);
2020
- return executeOrGenerate(code, 'Exports map region data as CSV', options, 'pinepaper_export_map_region_csv');
2021
- }
2022
- case 'pinepaper_import_map_region_csv': {
2023
- const input = ImportMapRegionCSVInputSchema.parse(args);
2024
- const code = codeGenerator.generateImportMapRegionCSV(input);
2025
- return executeOrGenerate(code, 'Imports CSV data to update map regions', options, 'pinepaper_import_map_region_csv');
2026
- }
2027
- case 'pinepaper_select_map_regions': {
2028
- const input = SelectMapRegionsInputSchema.parse(args);
2029
- const code = codeGenerator.generateSelectMapRegions(input);
2030
- const description = `Selects ${input.regionIds.length} map region(s)`;
2031
- return executeOrGenerate(code, description, options, 'pinepaper_select_map_regions');
2032
- }
2033
- case 'pinepaper_deselect_map_regions': {
2034
- const input = DeselectMapRegionsInputSchema.parse(args);
2035
- const code = codeGenerator.generateDeselectMapRegions(input);
2036
- return executeOrGenerate(code, 'Deselects map regions', options, 'pinepaper_deselect_map_regions');
2037
- }
2038
- case 'pinepaper_get_highlighted_map_regions': {
2039
- const code = codeGenerator.generateGetHighlightedMapRegions();
2040
- return executeOrGenerate(code, 'Gets list of highlighted map regions', options, 'pinepaper_get_highlighted_map_regions');
2041
- }
2042
- case 'pinepaper_export_map_geojson': {
2043
- const code = codeGenerator.generateExportMapGeoJson({
2044
- includeStyles: args.includeStyles,
2045
- includeMetadata: args.includeMetadata,
2046
- selectedOnly: args.selectedOnly,
2047
- download: args.download,
2048
- filename: args.filename,
2049
- });
2050
- return executeOrGenerate(code, 'Exports map as GeoJSON', options, 'pinepaper_export_map_geojson');
2051
- }
2052
- case 'pinepaper_export_original_map_geojson': {
2053
- const code = codeGenerator.generateExportOriginalMapGeoJson({
2054
- download: args.download,
2055
- filename: args.filename,
2056
- });
2057
- return executeOrGenerate(code, 'Exports original map GeoJSON', options, 'pinepaper_export_original_map_geojson');
2058
- }
2059
- case 'pinepaper_get_map_source_info': {
2060
- const code = codeGenerator.generateGetMapSourceInfo();
2061
- return executeOrGenerate(code, 'Gets map source information', options, 'pinepaper_get_map_source_info');
2062
- }
1809
+ // -----------------------------------------------------------------------
1810
+ // MAP TOOLS — extracted to src/tools/handlers/maps.ts
1811
+ // -----------------------------------------------------------------------
2063
1812
  case 'pinepaper_register_item': {
2064
1813
  const code = codeGenerator.generateRegisterItem({
2065
1814
  itemJson: args.itemJson,
@@ -2085,13 +1834,16 @@ All PinePaper tools will work in **code-only mode**:
2085
1834
  return executeOrGenerate(code, 'Imports image', options, 'pinepaper_import_image');
2086
1835
  }
2087
1836
  // -----------------------------------------------------------------------
1837
+ // ONTOLOGY TOOLS — extracted to src/tools/handlers/ontology.ts
1838
+ // -----------------------------------------------------------------------
1839
+ // -----------------------------------------------------------------------
2088
1840
  // UNKNOWN TOOL
2089
1841
  // -----------------------------------------------------------------------
2090
1842
  default: {
2091
1843
  const message = i18n
2092
1844
  ? i18n.getError('unknownTool', { toolName })
2093
1845
  : `Unknown tool: ${toolName}`;
2094
- return errorResult('UNKNOWN_TOOL', message, {
1846
+ return errorResult(ErrorCodes.UNKNOWN_TOOL, message, {
2095
1847
  availableTools: [
2096
1848
  'pinepaper_create_item',
2097
1849
  'pinepaper_modify_item',
@@ -2120,12 +1872,7 @@ All PinePaper tools will work in **code-only mode**:
2120
1872
  'pinepaper_get_mask_types',
2121
1873
  'pinepaper_get_mask_animations',
2122
1874
  'pinepaper_camera_animate',
2123
- 'pinepaper_camera_zoom',
2124
- 'pinepaper_camera_pan',
2125
- 'pinepaper_camera_move_to',
2126
- 'pinepaper_camera_reset',
2127
- 'pinepaper_camera_stop',
2128
- 'pinepaper_camera_state',
1875
+ 'pinepaper_camera',
2129
1876
  'pinepaper_get_items',
2130
1877
  'pinepaper_get_relation_stats',
2131
1878
  'pinepaper_set_background_color',
@@ -2134,6 +1881,7 @@ All PinePaper tools will work in **code-only mode**:
2134
1881
  'pinepaper_clear_canvas',
2135
1882
  'pinepaper_refresh_page',
2136
1883
  'pinepaper_import_svg',
1884
+ 'pinepaper_import_mermaid',
2137
1885
  'pinepaper_add_filter',
2138
1886
  'pinepaper_export_svg',
2139
1887
  'pinepaper_export_training_data',
@@ -2159,6 +1907,8 @@ All PinePaper tools will work in **code-only mode**:
2159
1907
  'pinepaper_diagram_mode',
2160
1908
  // Scene tools
2161
1909
  'pinepaper_create_scene',
1910
+ 'pinepaper_manage_scenes',
1911
+ 'pinepaper_scene_playback',
2162
1912
  // Agent flow mode tools
2163
1913
  'pinepaper_agent_start_job',
2164
1914
  'pinepaper_agent_end_job',
@@ -2180,48 +1930,13 @@ All PinePaper tools will work in **code-only mode**:
2180
1930
  'pinepaper_get_letter_collage_options',
2181
1931
  // Canvas presets tools
2182
1932
  'pinepaper_get_canvas_presets',
2183
- // Map tools
2184
- 'pinepaper_load_map',
2185
- 'pinepaper_highlight_regions',
2186
- 'pinepaper_unhighlight_regions',
2187
- 'pinepaper_apply_data_colors',
2188
- 'pinepaper_add_marker',
2189
- 'pinepaper_add_map_labels',
2190
- 'pinepaper_pan_map',
2191
- 'pinepaper_zoom_map',
2192
- 'pinepaper_export_map',
2193
- 'pinepaper_import_custom_map',
2194
- 'pinepaper_get_region_at_point',
2195
- // Map animation/CSV tools
2196
- 'pinepaper_animate_map_regions',
2197
- 'pinepaper_animate_map_wave',
2198
- 'pinepaper_stop_map_animations',
2199
- 'pinepaper_get_animated_map_regions',
2200
- 'pinepaper_export_map_region_csv',
2201
- 'pinepaper_import_map_region_csv',
2202
- 'pinepaper_select_map_regions',
2203
- 'pinepaper_deselect_map_regions',
2204
- 'pinepaper_get_highlighted_map_regions',
2205
- 'pinepaper_export_map_geojson',
2206
- 'pinepaper_export_original_map_geojson',
2207
- 'pinepaper_get_map_source_info',
1933
+ // Map tools (4 action-dispatched)
1934
+ 'pinepaper_map',
1935
+ 'pinepaper_map_regions',
1936
+ 'pinepaper_map_animation',
1937
+ 'pinepaper_map_data',
2208
1938
  // Font tools
2209
- 'pinepaper_font_show_studio',
2210
- 'pinepaper_font_set_name',
2211
- 'pinepaper_font_get_required_chars',
2212
- 'pinepaper_font_get_status',
2213
- 'pinepaper_font_create_glyph',
2214
- 'pinepaper_font_create_space',
2215
- 'pinepaper_font_remove_glyph',
2216
- 'pinepaper_font_set_metrics',
2217
- 'pinepaper_font_export',
2218
- 'pinepaper_font_load_into_document',
2219
- 'pinepaper_font_export_data',
2220
- 'pinepaper_font_import_data',
2221
- 'pinepaper_font_clear',
2222
- 'pinepaper_font_remove_overlap',
2223
- 'pinepaper_font_correct_direction',
2224
- 'pinepaper_font_cleanup_path',
1939
+ 'pinepaper_font',
2225
1940
  // Template tools
2226
1941
  'pinepaper_apply_template',
2227
1942
  // Image import tools
@@ -2231,6 +1946,38 @@ All PinePaper tools will work in **code-only mode**:
2231
1946
  // On-demand guide & runtime config
2232
1947
  'pinepaper_tool_guide',
2233
1948
  'pinepaper_set_toolkit',
1949
+ // Ontology tools
1950
+ 'pinepaper_get_canvas_ontology',
1951
+ 'pinepaper_analyze_design',
1952
+ 'pinepaper_validate_design',
1953
+ 'pinepaper_query_ontology',
1954
+ // Selection, transform & history tools
1955
+ 'pinepaper_selection',
1956
+ 'pinepaper_transform',
1957
+ 'pinepaper_history',
1958
+ // Image processing tools
1959
+ 'pinepaper_image_filter',
1960
+ 'pinepaper_lasso',
1961
+ 'pinepaper_cutout_style',
1962
+ // Composition & view tools
1963
+ 'pinepaper_precomp',
1964
+ 'pinepaper_view',
1965
+ 'pinepaper_background',
1966
+ // Canvas query tools
1967
+ 'pinepaper_query',
1968
+ // Deformation, sprite sheets, storage, interaction
1969
+ 'pinepaper_deform',
1970
+ 'pinepaper_sprite_sheet',
1971
+ 'pinepaper_storage',
1972
+ 'pinepaper_interaction',
1973
+ // Widget export
1974
+ 'pinepaper_export_widget',
1975
+ 'pinepaper_export_widget_html',
1976
+ // Data visualization, magic, physics, measurement
1977
+ 'pinepaper_create_chart',
1978
+ 'pinepaper_magic',
1979
+ 'pinepaper_physics',
1980
+ 'pinepaper_measurement',
2234
1981
  ],
2235
1982
  });
2236
1983
  }