@pinepaper.studio/mcp-server 1.5.3 → 1.5.5

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 (77) hide show
  1. package/README.md +6 -4
  2. package/dist/agent/types.d.ts +16 -16
  3. package/dist/browser/url-target.d.ts +26 -0
  4. package/dist/browser/url-target.d.ts.map +1 -0
  5. package/dist/browser/url-target.js +50 -0
  6. package/dist/browser/url-target.js.map +1 -0
  7. package/dist/cli.js +2 -1
  8. package/dist/cli.js.map +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +48 -4
  11. package/dist/index.js.map +1 -1
  12. package/dist/ontology/hints.d.ts +71 -0
  13. package/dist/ontology/hints.d.ts.map +1 -0
  14. package/dist/ontology/hints.js +189 -0
  15. package/dist/ontology/hints.js.map +1 -0
  16. package/dist/ontology/vocabulary.d.ts.map +1 -1
  17. package/dist/ontology/vocabulary.js +34 -0
  18. package/dist/ontology/vocabulary.js.map +1 -1
  19. package/dist/prompts/index.d.ts.map +1 -1
  20. package/dist/prompts/index.js +146 -0
  21. package/dist/prompts/index.js.map +1 -1
  22. package/dist/tools/compact-descriptions.d.ts +3 -0
  23. package/dist/tools/compact-descriptions.d.ts.map +1 -1
  24. package/dist/tools/compact-descriptions.js +17 -9
  25. package/dist/tools/compact-descriptions.js.map +1 -1
  26. package/dist/tools/definitions.d.ts +1 -1
  27. package/dist/tools/definitions.d.ts.map +1 -1
  28. package/dist/tools/definitions.js +1251 -2327
  29. package/dist/tools/definitions.js.map +1 -1
  30. package/dist/tools/handlers/camera.d.ts +19 -0
  31. package/dist/tools/handlers/camera.d.ts.map +1 -0
  32. package/dist/tools/handlers/camera.js +100 -0
  33. package/dist/tools/handlers/camera.js.map +1 -0
  34. package/dist/tools/handlers/export.d.ts +12 -0
  35. package/dist/tools/handlers/export.d.ts.map +1 -0
  36. package/dist/tools/handlers/export.js +30 -0
  37. package/dist/tools/handlers/export.js.map +1 -0
  38. package/dist/tools/handlers/font.d.ts +12 -0
  39. package/dist/tools/handlers/font.d.ts.map +1 -0
  40. package/dist/tools/handlers/font.js +135 -0
  41. package/dist/tools/handlers/font.js.map +1 -0
  42. package/dist/tools/handlers/maps.d.ts +19 -0
  43. package/dist/tools/handlers/maps.d.ts.map +1 -0
  44. package/dist/tools/handlers/maps.js +184 -0
  45. package/dist/tools/handlers/maps.js.map +1 -0
  46. package/dist/tools/handlers/ontology.d.ts +20 -0
  47. package/dist/tools/handlers/ontology.d.ts.map +1 -0
  48. package/dist/tools/handlers/ontology.js +185 -0
  49. package/dist/tools/handlers/ontology.js.map +1 -0
  50. package/dist/tools/handlers/tool-guide.d.ts +14 -0
  51. package/dist/tools/handlers/tool-guide.d.ts.map +1 -0
  52. package/dist/tools/handlers/tool-guide.js +52 -0
  53. package/dist/tools/handlers/tool-guide.js.map +1 -0
  54. package/dist/tools/handlers.d.ts +18 -0
  55. package/dist/tools/handlers.d.ts.map +1 -1
  56. package/dist/tools/handlers.js +213 -695
  57. package/dist/tools/handlers.js.map +1 -1
  58. package/dist/tools/minimal-descriptions.d.ts +4 -0
  59. package/dist/tools/minimal-descriptions.d.ts.map +1 -1
  60. package/dist/tools/minimal-descriptions.js +45 -66
  61. package/dist/tools/minimal-descriptions.js.map +1 -1
  62. package/dist/tools/toolkits.d.ts.map +1 -1
  63. package/dist/tools/toolkits.js +36 -62
  64. package/dist/tools/toolkits.js.map +1 -1
  65. package/dist/types/code-generator.d.ts +35 -34
  66. package/dist/types/code-generator.d.ts.map +1 -1
  67. package/dist/types/code-generator.js +869 -474
  68. package/dist/types/code-generator.js.map +1 -1
  69. package/dist/types/schemas.d.ts +1047 -988
  70. package/dist/types/schemas.d.ts.map +1 -1
  71. package/dist/types/schemas.js +282 -84
  72. package/dist/types/schemas.js.map +1 -1
  73. package/dist/version.d.ts +11 -0
  74. package/dist/version.d.ts.map +1 -0
  75. package/dist/version.js +18 -0
  76. package/dist/version.js.map +1 -0
  77. package/package.json +1 -1
@@ -6,45 +6,52 @@
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
13
13
  AgentStartJobInputSchema, AgentEndJobInputSchema, AgentResetInputSchema, AgentBatchExecuteInputSchema, AgentExportInputSchema, AgentAnalyzeInputSchema,
14
- // Interactive/Trigger schemas
15
- AddTriggerInputSchema, RemoveTriggerInputSchema, QueryTriggersInputSchema,
16
- // Quiz schemas
17
- CreateQuizInputSchema, GetQuizStateInputSchema, ResetQuizInputSchema,
18
14
  // Letter collage schemas
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,
15
+ CreateLetterCollageInputSchema, AnimateLetterCollageInputSchema, ApplyTemplateInputSchema,
24
16
  // Custom relation/code schemas
25
17
  RegisterCustomRelationInputSchema, ExecuteCustomCodeInputSchema,
26
18
  // Mask schemas
27
19
  ApplyAnimatedMaskInputSchema, ApplyCustomMaskInputSchema, RemoveMaskInputSchema,
28
- // Template schemas
29
- ApplyTemplateInputSchema,
30
20
  // Image import schemas
31
21
  ImportImageInputSchema,
32
- // Tool guide schema
33
- ToolGuideInputSchema,
34
- // Ontology schemas
35
- AnalyzeDesignInputSchema, ValidateDesignInputSchema, QueryOntologyInputSchema,
36
22
  // Scene management schemas
37
23
  ManageScenesInputSchema, ScenePlaybackInputSchema,
38
24
  // New consolidated tool schemas
39
- SelectionInputSchema, TransformInputSchema, HistoryInputSchema, ImageFilterInputSchema, LassoInputSchema, CutoutStyleInputSchema, PrecompInputSchema, ViewInputSchema, BackgroundInputSchema, QueryInputSchema, ErrorCodes, } from '../types/schemas.js';
25
+ SelectionInputSchema, TransformInputSchema, HistoryInputSchema, ImageFilterInputSchema, LassoInputSchema, CutoutStyleInputSchema, PrecompInputSchema, BackgroundInputSchema, QueryInputSchema, DeformInputSchema, SpriteSheetInputSchema, InteractionInputSchema, ExportWidgetInputSchema, ExportWidgetHtmlInputSchema, CreateChartInputSchema, MagicInputSchema, PhysicsInputSchema, MeasurementInputSchema, ErrorCodes, } from '../types/schemas.js';
40
26
  import { ZodError } from 'zod';
41
27
  import { writeFile, mkdir } from 'node:fs/promises';
42
28
  import { tmpdir } from 'node:os';
43
29
  import { join } from 'node:path';
44
30
  import { getBrowserController, resetBrowserController, } from '../browser/puppeteer-controller.js';
31
+ import { sameStudioTarget, validateStudioUrl } from '../browser/url-target.js';
45
32
  import { getPerformanceTracker } from '../metrics/index.js';
46
33
  import { formatErrorContext, captureCanvasState } from '../execution/index.js';
47
34
  import { getSessionManager } from '../agent/session-manager.js';
35
+ import { vocabularyHintForPath, validateBatchVocabulary, detectBatchPropertyTypos } from '../ontology/hints.js';
36
+ import { cameraHandlers } from './handlers/camera.js';
37
+ import { fontHandlers } from './handlers/font.js';
38
+ import { toolGuideHandlers } from './handlers/tool-guide.js';
39
+ import { mapHandlers } from './handlers/maps.js';
40
+ import { ontologyHandlers } from './handlers/ontology.js';
41
+ import { exportHandlers } from './handlers/export.js';
42
+ /**
43
+ * Registry of per-domain handler maps. Tools listed here short-circuit the
44
+ * main switch at dispatch time (see handleToolCallInner). New domains are
45
+ * folded in incrementally as they are migrated out of the monolithic switch.
46
+ */
47
+ const DOMAIN_HANDLERS = [
48
+ cameraHandlers,
49
+ fontHandlers,
50
+ toolGuideHandlers,
51
+ mapHandlers,
52
+ ontologyHandlers,
53
+ exportHandlers,
54
+ ];
48
55
  /**
49
56
  * Get screenshot mode from environment variable.
50
57
  * Defaults to 'on_request' for optimal performance.
@@ -146,7 +153,7 @@ function screenshotResult(screenshot) {
146
153
  ],
147
154
  };
148
155
  }
149
- function errorResult(code, message, details, context) {
156
+ export function errorResult(code, message, details, context) {
150
157
  // Build error object
151
158
  const errorObj = {
152
159
  success: false,
@@ -173,10 +180,18 @@ function handleValidationError(error, i18n) {
173
180
  const message = i18n
174
181
  ? i18n.getError('validationError', { message: 'Invalid input parameters' })
175
182
  : 'Invalid input parameters';
176
- return errorResult(ErrorCodes.VALIDATION_ERROR, message, error.errors.map((e) => ({
177
- path: e.path.join('.'),
178
- message: e.message,
179
- })));
183
+ const details = error.errors.map((e) => {
184
+ const path = e.path.join('.');
185
+ const detail = { path, message: e.message };
186
+ if (e.code === 'invalid_enum_value') {
187
+ const received = e.received;
188
+ const hint = vocabularyHintForPath(path, received);
189
+ if (hint)
190
+ detail.vocabulary = hint;
191
+ }
192
+ return detail;
193
+ });
194
+ return errorResult(ErrorCodes.VALIDATION_ERROR, message, details);
180
195
  }
181
196
  /**
182
197
  * Execute code in browser if connected, otherwise return generated code.
@@ -192,7 +207,7 @@ function handleValidationError(error, i18n) {
192
207
  *
193
208
  * Default is 'on_request' for optimal performance per best practices.
194
209
  */
195
- async function executeOrGenerate(code, description, options, toolName) {
210
+ export async function executeOrGenerate(code, description, options, toolName) {
196
211
  const { executeInBrowser, browserController, screenshotMode, executionMode } = options;
197
212
  const effectiveExecutionMode = executionMode ?? getExecutionMode();
198
213
  const effectiveScreenshotMode = screenshotMode ?? getScreenshotMode();
@@ -369,7 +384,7 @@ ${code}
369
384
  // =============================================================================
370
385
  // I18N HELPER FUNCTIONS
371
386
  // =============================================================================
372
- function getLocalizedSuccessMessage(i18n, key, params) {
387
+ export function getLocalizedSuccessMessage(i18n, key, params) {
373
388
  if (!i18n) {
374
389
  // Fallback to English template
375
390
  return Object.entries(params).reduce((msg, [k, v]) => msg.replace(`{{${k}}}`, String(v)), key);
@@ -453,6 +468,14 @@ export async function handleToolCall(toolName, args, options = {}) {
453
468
  async function handleToolCallInner(toolName, args, options, tracker, baseTimerId) {
454
469
  const { i18n } = options;
455
470
  try {
471
+ // Domain-module dispatch: tools migrated to per-domain handler files
472
+ // short-circuit before the main switch. See DOMAIN_HANDLERS for the
473
+ // current registry.
474
+ for (const domainMap of DOMAIN_HANDLERS) {
475
+ const handler = domainMap[toolName];
476
+ if (handler)
477
+ return await handler(args, options);
478
+ }
456
479
  switch (toolName) {
457
480
  // -----------------------------------------------------------------------
458
481
  // ITEM TOOLS
@@ -493,7 +516,7 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
493
516
  const animProps = ['animationType', 'animationSpeed', 'keyframes', 'animation'];
494
517
  const passedAnimProps = animProps.filter(p => p in (input.properties || {}));
495
518
  if (passedAnimProps.length > 0) {
496
- return errorResult('INVALID_PROPERTIES', `Animation properties (${passedAnimProps.join(', ')}) cannot be set via pinepaper_modify_item — they will be silently ignored. ` +
519
+ return errorResult(ErrorCodes.INVALID_PROPERTIES, `Animation properties (${passedAnimProps.join(', ')}) cannot be set via pinepaper_modify_item — they will be silently ignored. ` +
497
520
  `Use pinepaper_animate for loop animations (pulse, rotate, bounce, fade, wobble, slide) ` +
498
521
  `or pinepaper_keyframe_animate for timed/sequenced animations with precise control.`);
499
522
  }
@@ -589,11 +612,6 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
589
612
  const code = codeGenerator.generatePrecomp(input);
590
613
  return executeOrGenerate(code, `Precomp: ${input.action}`, options, 'pinepaper_precomp');
591
614
  }
592
- case 'pinepaper_view': {
593
- const input = ViewInputSchema.parse(args);
594
- const code = codeGenerator.generateView(input);
595
- return executeOrGenerate(code, `View: ${input.action}`, options, 'pinepaper_view');
596
- }
597
615
  case 'pinepaper_background': {
598
616
  const input = BackgroundInputSchema.parse(args);
599
617
  const code = codeGenerator.generateBackground(input);
@@ -608,6 +626,57 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
608
626
  return executeOrGenerate(code, `Query: ${input.action}`, options, 'pinepaper_query');
609
627
  }
610
628
  // -----------------------------------------------------------------------
629
+ // DEFORMATION, SPRITE SHEETS, STORAGE, INTERACTION
630
+ // -----------------------------------------------------------------------
631
+ case 'pinepaper_deform': {
632
+ const input = DeformInputSchema.parse(args);
633
+ const code = codeGenerator.generateDeform(input);
634
+ return executeOrGenerate(code, `Deform: ${input.action}`, options, 'pinepaper_deform');
635
+ }
636
+ case 'pinepaper_sprite_sheet': {
637
+ const input = SpriteSheetInputSchema.parse(args);
638
+ const code = codeGenerator.generateSpriteSheet(input);
639
+ return executeOrGenerate(code, `Sprite sheet: ${input.action}`, options, 'pinepaper_sprite_sheet');
640
+ }
641
+ case 'pinepaper_interaction': {
642
+ const input = InteractionInputSchema.parse(args);
643
+ const code = codeGenerator.generateInteraction(input);
644
+ return executeOrGenerate(code, `Interaction: ${input.action}`, options, 'pinepaper_interaction');
645
+ }
646
+ case 'pinepaper_export_widget': {
647
+ const input = ExportWidgetInputSchema.parse(args);
648
+ const code = codeGenerator.generateExportWidget(input);
649
+ return executeOrGenerate(code, 'Export widget JSON', options, 'pinepaper_export_widget');
650
+ }
651
+ case 'pinepaper_export_widget_html': {
652
+ const input = ExportWidgetHtmlInputSchema.parse(args);
653
+ const code = codeGenerator.generateExportWidgetHtml(input);
654
+ return executeOrGenerate(code, 'Export widget HTML', options, 'pinepaper_export_widget_html');
655
+ }
656
+ // -----------------------------------------------------------------------
657
+ // DATA VISUALIZATION, MAGIC, PHYSICS, MEASUREMENT
658
+ // -----------------------------------------------------------------------
659
+ case 'pinepaper_create_chart': {
660
+ const input = CreateChartInputSchema.parse(args);
661
+ const code = codeGenerator.generateChart(input);
662
+ return executeOrGenerate(code, `Chart: ${input.action}`, options, 'pinepaper_create_chart');
663
+ }
664
+ case 'pinepaper_magic': {
665
+ const input = MagicInputSchema.parse(args);
666
+ const code = codeGenerator.generateMagic(input);
667
+ return executeOrGenerate(code, `Magic: ${input.action}`, options, 'pinepaper_magic');
668
+ }
669
+ case 'pinepaper_physics': {
670
+ const input = PhysicsInputSchema.parse(args);
671
+ const code = codeGenerator.generatePhysics(input);
672
+ return executeOrGenerate(code, `Physics: ${input.action}`, options, 'pinepaper_physics');
673
+ }
674
+ case 'pinepaper_measurement': {
675
+ const input = MeasurementInputSchema.parse(args);
676
+ const code = codeGenerator.generateMeasurement(input);
677
+ return executeOrGenerate(code, `Measurement: ${input.action}`, options, 'pinepaper_measurement');
678
+ }
679
+ // -----------------------------------------------------------------------
611
680
  // BATCH OPERATION TOOLS
612
681
  // -----------------------------------------------------------------------
613
682
  case 'pinepaper_batch_create': {
@@ -750,168 +819,11 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
750
819
  return executeOrGenerate(code, 'Gets available mask animation presets', options, 'pinepaper_get_mask_animations');
751
820
  }
752
821
  // -----------------------------------------------------------------------
753
- // CAMERA TOOLS
754
- // -----------------------------------------------------------------------
755
- case 'pinepaper_camera_animate': {
756
- const keyframes = args.keyframes;
757
- const duration = args.duration;
758
- const loop = args.loop ?? false;
759
- const delay = args.delay ?? 0;
760
- const keyframesStr = JSON.stringify(keyframes);
761
- 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} });`;
762
- return executeOrGenerate(code, `Animates camera with ${keyframes.length} keyframes over ${duration}s`, options, 'pinepaper_camera_animate');
763
- }
764
- case 'pinepaper_camera_zoom': {
765
- const direction = args.direction;
766
- const level = args.level ?? (direction === 'in' ? 2 : 0.5);
767
- const duration = args.duration ?? 0.5;
768
- const method = direction === 'in' ? 'zoomIn' : 'zoomOut';
769
- const code = `app.camera && app.camera.${method} ? app.camera.${method}(${level}, ${duration}) : null;`;
770
- return executeOrGenerate(code, `Camera zoom ${direction} to ${level}x`, options, 'pinepaper_camera_zoom');
771
- }
772
- case 'pinepaper_camera_pan': {
773
- const direction = args.direction;
774
- const amount = args.amount ?? 100;
775
- const x = args.x;
776
- const y = args.y;
777
- const duration = args.duration ?? 0.5;
778
- let code;
779
- let description;
780
- if (x !== undefined && y !== undefined) {
781
- code = `app.camera && app.camera.panTo ? app.camera.panTo(${x}, ${y}, ${duration}) : null;`;
782
- description = `Camera pan to (${x}, ${y})`;
783
- }
784
- else if (direction) {
785
- const methodMap = { left: 'panLeft', right: 'panRight', up: 'panUp', down: 'panDown' };
786
- const method = methodMap[direction];
787
- code = `app.camera && app.camera.${method} ? app.camera.${method}(${amount}, ${duration}) : null;`;
788
- description = `Camera pan ${direction} by ${amount}px`;
789
- }
790
- else {
791
- code = `// No direction or coordinates specified`;
792
- description = 'Camera pan (no parameters)';
793
- }
794
- return executeOrGenerate(code, description, options, 'pinepaper_camera_pan');
795
- }
796
- case 'pinepaper_camera_move_to': {
797
- const x = args.x;
798
- const y = args.y;
799
- const zoom = args.zoom;
800
- const duration = args.duration ?? 0.5;
801
- const code = `app.camera && app.camera.moveTo ? app.camera.moveTo(${x}, ${y}, ${zoom}, ${duration}) : null;`;
802
- return executeOrGenerate(code, `Camera move to (${x}, ${y}) at ${zoom}x zoom`, options, 'pinepaper_camera_move_to');
803
- }
804
- case 'pinepaper_camera_reset': {
805
- const duration = args.duration ?? 0.5;
806
- const code = `app.camera && app.camera.reset ? app.camera.reset(${duration}) : null;`;
807
- return executeOrGenerate(code, 'Reset camera to default state', options, 'pinepaper_camera_reset');
808
- }
809
- case 'pinepaper_camera_stop': {
810
- const code = `app.camera && app.camera.stop ? app.camera.stop() : null;`;
811
- return executeOrGenerate(code, 'Stop camera animation', options, 'pinepaper_camera_stop');
812
- }
813
- case 'pinepaper_camera_state': {
814
- const code = `app.camera && app.camera.getState ? app.camera.getState() : { zoom: 1, center: [400, 300], isAnimating: false };`;
815
- return executeOrGenerate(code, 'Get current camera state', options, 'pinepaper_camera_state');
816
- }
817
- // -----------------------------------------------------------------------
818
- // FONT TOOLS
819
- // -----------------------------------------------------------------------
820
- case 'pinepaper_font_show_studio': {
821
- const code = `app.fontStudio && app.fontStudio.show ? app.fontStudio.show() : null;`;
822
- return executeOrGenerate(code, 'Opens Font Studio UI', options, 'pinepaper_font_show_studio');
823
- }
824
- case 'pinepaper_font_set_name': {
825
- const { name } = args;
826
- const code = `app.fontStudio.setName(${JSON.stringify(name)});`;
827
- return executeOrGenerate(code, `Set font name to "${name}"`, options, 'pinepaper_font_set_name');
828
- }
829
- case 'pinepaper_font_get_required_chars': {
830
- const { set } = args;
831
- const setArg = set ? JSON.stringify(set) : '"minimum"';
832
- const code = `app.fontStudio.getRequiredChars(${setArg});`;
833
- return executeOrGenerate(code, `Get required characters (${set || 'minimum'} set)`, options, 'pinepaper_font_get_required_chars');
834
- }
835
- case 'pinepaper_font_get_status': {
836
- const code = `app.fontStudio.getStatus();`;
837
- return executeOrGenerate(code, 'Get font completion status', options, 'pinepaper_font_get_status');
838
- }
839
- case 'pinepaper_font_create_glyph': {
840
- const { character, pathId } = args;
841
- const code = `app.fontStudio.createGlyph(${JSON.stringify(character)}, ${JSON.stringify(pathId)});`;
842
- return executeOrGenerate(code, `Create glyph for "${character}" from path ${pathId}`, options, 'pinepaper_font_create_glyph');
843
- }
844
- case 'pinepaper_font_create_space': {
845
- const { width } = args;
846
- const code = width !== undefined
847
- ? `app.fontStudio.createSpace(${width});`
848
- : `app.fontStudio.createSpace();`;
849
- return executeOrGenerate(code, `Create space glyph${width ? ` (width: ${width})` : ''}`, options, 'pinepaper_font_create_space');
850
- }
851
- case 'pinepaper_font_remove_glyph': {
852
- const { character } = args;
853
- const code = `app.fontStudio.removeGlyph(${JSON.stringify(character)});`;
854
- return executeOrGenerate(code, `Remove glyph for "${character}"`, options, 'pinepaper_font_remove_glyph');
855
- }
856
- case 'pinepaper_font_set_metrics': {
857
- const metrics = args;
858
- const code = `app.fontStudio.setMetrics(${JSON.stringify(metrics)});`;
859
- return executeOrGenerate(code, 'Set font metrics', options, 'pinepaper_font_set_metrics');
860
- }
861
- case 'pinepaper_font_export': {
862
- const { download } = args;
863
- const code = download === false
864
- ? `app.fontStudio.export({ download: false });`
865
- : `app.fontStudio.export();`;
866
- return executeOrGenerate(code, 'Export font as OTF', options, 'pinepaper_font_export');
867
- }
868
- case 'pinepaper_font_load_into_document': {
869
- const code = `app.fontStudio.loadIntoDocument();`;
870
- return executeOrGenerate(code, 'Load font into document', options, 'pinepaper_font_load_into_document');
871
- }
872
- case 'pinepaper_font_export_data': {
873
- const { download } = args;
874
- const code = download === false
875
- ? `app.fontStudio.exportData({ download: false });`
876
- : `app.fontStudio.exportData();`;
877
- return executeOrGenerate(code, 'Export font data as JSON', options, 'pinepaper_font_export_data');
878
- }
879
- case 'pinepaper_font_import_data': {
880
- const { data } = args;
881
- const code = `app.fontStudio.importData(${JSON.stringify(data)});`;
882
- return executeOrGenerate(code, 'Import font data from JSON', options, 'pinepaper_font_import_data');
883
- }
884
- case 'pinepaper_font_clear': {
885
- const code = `app.fontStudio.clear();`;
886
- return executeOrGenerate(code, 'Clear all glyphs and reset font', options, 'pinepaper_font_clear');
887
- }
888
- case 'pinepaper_font_remove_overlap': {
889
- const { pathId } = args;
890
- const code = `app.fontStudio.removeOverlap(${JSON.stringify(pathId)});`;
891
- return executeOrGenerate(code, `Remove overlaps from path ${pathId}`, options, 'pinepaper_font_remove_overlap');
892
- }
893
- case 'pinepaper_font_correct_direction': {
894
- const { pathId } = args;
895
- const code = `app.fontStudio.correctDirection(${JSON.stringify(pathId)});`;
896
- return executeOrGenerate(code, `Correct path direction for ${pathId}`, options, 'pinepaper_font_correct_direction');
897
- }
898
- case 'pinepaper_font_cleanup_path': {
899
- const { pathId, removeOverlap, correctDirection, smooth, smoothTolerance } = args;
900
- const optsObj = {};
901
- if (removeOverlap !== undefined)
902
- optsObj.removeOverlap = removeOverlap;
903
- if (correctDirection !== undefined)
904
- optsObj.correctDirection = correctDirection;
905
- if (smooth !== undefined)
906
- optsObj.smooth = smooth;
907
- if (smoothTolerance !== undefined)
908
- optsObj.smoothTolerance = smoothTolerance;
909
- const hasOpts = Object.keys(optsObj).length > 0;
910
- const code = hasOpts
911
- ? `app.fontStudio.cleanupPath(${JSON.stringify(pathId)}, ${JSON.stringify(optsObj)});`
912
- : `app.fontStudio.cleanupPath(${JSON.stringify(pathId)});`;
913
- return executeOrGenerate(code, `Cleanup path ${pathId}`, options, 'pinepaper_font_cleanup_path');
914
- }
822
+ // CAMERA TOOLS — extracted to src/tools/handlers/camera.ts
823
+ // -----------------------------------------------------------------------
824
+ // -----------------------------------------------------------------------
825
+ // FONT TOOLS — extracted to src/tools/handlers/font.ts
826
+ // -----------------------------------------------------------------------
915
827
  // -----------------------------------------------------------------------
916
828
  // QUERY TOOLS
917
829
  // -----------------------------------------------------------------------
@@ -956,7 +868,7 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
956
868
  case 'pinepaper_refresh_page': {
957
869
  const controller = options.browserController || getBrowserController();
958
870
  if (!controller.connected) {
959
- return errorResult('BROWSER_NOT_CONNECTED', 'Not connected to PinePaper Studio. Call pinepaper_browser_connect first.');
871
+ return errorResult(ErrorCodes.BROWSER_NOT_CONNECTED, 'Not connected to PinePaper Studio. Call pinepaper_browser_connect first.');
960
872
  }
961
873
  try {
962
874
  // Refresh the page
@@ -985,7 +897,7 @@ You can now start creating new items on a clean canvas.`,
985
897
  return { content };
986
898
  }
987
899
  catch (error) {
988
- return errorResult('REFRESH_FAILED', error instanceof Error ? error.message : 'Failed to refresh page');
900
+ return errorResult(ErrorCodes.REFRESH_FAILED, error instanceof Error ? error.message : 'Failed to refresh page');
989
901
  }
990
902
  }
991
903
  // -----------------------------------------------------------------------
@@ -997,6 +909,15 @@ You can now start creating new items on a clean canvas.`,
997
909
  const description = 'Imports SVG onto the canvas';
998
910
  return executeOrGenerate(code, description, options, 'pinepaper_import_svg');
999
911
  }
912
+ case 'pinepaper_import_mermaid': {
913
+ const input = ImportMermaidInputSchema.parse(args);
914
+ const code = codeGenerator.generateImportMermaid(input.mermaidText, {
915
+ autoLayout: input.autoLayout,
916
+ clearExisting: input.clearExisting,
917
+ });
918
+ const description = 'Imports a Mermaid diagram onto the canvas';
919
+ return executeOrGenerate(code, description, options, 'pinepaper_import_mermaid');
920
+ }
1000
921
  // -----------------------------------------------------------------------
1001
922
  // FILTER TOOLS
1002
923
  // -----------------------------------------------------------------------
@@ -1007,46 +928,53 @@ You can now start creating new items on a clean canvas.`,
1007
928
  return executeOrGenerate(code, description, options, 'pinepaper_add_filter');
1008
929
  }
1009
930
  // -----------------------------------------------------------------------
1010
- // EXPORT TOOLS
931
+ // EXPORT TOOLS — extracted to src/tools/handlers/export.ts
1011
932
  // -----------------------------------------------------------------------
1012
- case 'pinepaper_export_svg': {
1013
- const code = codeGenerator.generateExportSVG();
1014
- const description = getLocalizedSuccessMessage(i18n, 'exported', {
1015
- format: 'SVG',
1016
- });
1017
- return executeOrGenerate(code, description, options, 'pinepaper_export_svg');
1018
- }
1019
- case 'pinepaper_export_training_data': {
1020
- const input = ExportTrainingDataInputSchema.parse(args);
1021
- const code = codeGenerator.generateExportTrainingData(input.format, input.includeMetadata);
1022
- const description = getLocalizedSuccessMessage(i18n, 'exported', {
1023
- format: input.format?.toUpperCase() || 'JSON',
1024
- });
1025
- return executeOrGenerate(code, description, options, 'pinepaper_export_training_data');
1026
- }
1027
- case 'pinepaper_export_scene': {
1028
- const code = codeGenerator.generateExportScene();
1029
- return executeOrGenerate(code, 'Exports complete scene state', options, 'pinepaper_export_scene');
1030
- }
1031
933
  // -----------------------------------------------------------------------
1032
934
  // BROWSER CONTROL TOOLS
1033
935
  // -----------------------------------------------------------------------
1034
936
  case 'pinepaper_browser_connect': {
1035
937
  // Default to headless: false — if you explicitly call browser_connect, you likely want to see it
1036
938
  const headless = args.headless ?? false;
1037
- const controller = getBrowserController({ headless });
1038
- // Already connected with same headless mode → nothing to do
1039
- if (controller.connected && controller.isHeadless === headless) {
939
+ // Target a custom Studio instance (e.g. local dev at http://localhost:3000).
940
+ // Falls back to the PINEPAPER_STUDIO_URL env var, else PuppeteerController's
941
+ // built-in prod default. "/editor" + agent params are appended by the controller.
942
+ const rawUrl = typeof args.url === 'string' ? args.url.trim() : '';
943
+ const studioUrl = (rawUrl || (process.env.PINEPAPER_STUDIO_URL || '').trim()) || undefined;
944
+ // Only http(s) is supported — reject file://, data://, javascript:, etc.
945
+ if (studioUrl) {
946
+ const v = validateStudioUrl(studioUrl);
947
+ if (!v.ok)
948
+ return errorResult(ErrorCodes.INVALID_INPUT, v.error);
949
+ }
950
+ const cfg = studioUrl ? { headless, studioUrl } : { headless };
951
+ let controller = getBrowserController(cfg);
952
+ // NOT gated on controller.connected. getBrowserController() ignores cfg
953
+ // once the singleton exists, AND the singleton survives disconnect()
954
+ // (pinepaper_browser_disconnect doesn't null it) carrying its old
955
+ // studioUrl. So a stale/disconnected controller pointing at the prod
956
+ // default — which any prior browser_status() or connect() creates —
957
+ // would otherwise silently swallow an explicit url= (e.g. localhost for
958
+ // a dev build). Treat a different requested target as a change even when
959
+ // not currently connected.
960
+ const urlChanged = !!studioUrl &&
961
+ !sameStudioTarget(controller.studioUrl, studioUrl);
962
+ // Already connected, same headless mode, same URL → nothing to do
963
+ if (controller.connected && controller.isHeadless === headless && !urlChanged) {
1040
964
  return {
1041
- content: [{ type: 'text', text: `Already connected to PinePaper Studio (headless: ${headless}).` }],
965
+ content: [{ type: 'text', text: `Already connected to PinePaper Studio (${controller.studioUrl || 'default'}, headless: ${headless}).` }],
1042
966
  };
1043
967
  }
1044
- // Already connected but different headless mode → reconnect
1045
- if (controller.connected && controller.isHeadless !== headless) {
968
+ // Headless mode or target URL differs from the existing singleton → tear
969
+ // it down so the new config takes effect. Runs even when disconnected,
970
+ // because getBrowserController() reuses the stale singleton (and its old
971
+ // config) on the next call otherwise.
972
+ if (urlChanged || (controller.connected && controller.isHeadless !== headless)) {
1046
973
  await resetBrowserController();
974
+ controller = getBrowserController(cfg);
1047
975
  }
1048
976
  try {
1049
- const newController = controller.connected ? controller : getBrowserController({ headless });
977
+ const newController = controller;
1050
978
  if (!newController.connected)
1051
979
  await newController.connect();
1052
980
  const canvasSizeResult = await newController.executeCode(`const size = app.getCanvasSize ? app.getCanvasSize() : { width: 800, height: 600 }; ({ width: size.width || 800, height: size.height || 600 });`, false);
@@ -1054,7 +982,7 @@ You can now start creating new items on a clean canvas.`,
1054
982
  ? canvasSizeResult.result
1055
983
  : { width: 800, height: 600 };
1056
984
  return {
1057
- content: [{ type: 'text', text: `Connected to PinePaper Studio (headless: ${headless}, canvas: ${canvasSize.width}x${canvasSize.height}).` }],
985
+ content: [{ type: 'text', text: `Connected to PinePaper Studio (${newController.studioUrl || 'default'}, headless: ${headless}, canvas: ${canvasSize.width}x${canvasSize.height}).` }],
1058
986
  };
1059
987
  }
1060
988
  catch (error) {
@@ -1089,13 +1017,13 @@ You can now start creating new items on a clean canvas.`,
1089
1017
  case 'pinepaper_browser_screenshot': {
1090
1018
  const controller = getBrowserController();
1091
1019
  if (!controller.connected) {
1092
- return errorResult('BROWSER_NOT_CONNECTED', 'Not connected to PinePaper Studio. Call pinepaper_browser_connect first.');
1020
+ return errorResult(ErrorCodes.BROWSER_NOT_CONNECTED, 'Not connected to PinePaper Studio. Call pinepaper_browser_connect first.');
1093
1021
  }
1094
1022
  const screenshot = await controller.takeScreenshot();
1095
1023
  if (!screenshot) {
1096
1024
  // Capture canvas state for debugging
1097
1025
  const canvasState = await captureCanvasState(controller);
1098
- return errorResult('SCREENSHOT_FAILED', 'Failed to capture screenshot', undefined, {
1026
+ return errorResult(ErrorCodes.SCREENSHOT_FAILED, 'Failed to capture screenshot', undefined, {
1099
1027
  toolName: 'pinepaper_browser_screenshot',
1100
1028
  canvasState: canvasState || undefined,
1101
1029
  });
@@ -1160,8 +1088,9 @@ You can now start creating new items on a clean canvas.`,
1160
1088
  const metricsLimit = input.metricsLimit ?? 100;
1161
1089
  // --- Server info ---
1162
1090
  const { detectToolkitFromEnvironment: detectToolkit, detectVerbosityFromEnvironment: detectVerbosity } = await import('./toolkits.js');
1091
+ const { SERVER_VERSION } = await import('../version.js');
1163
1092
  const serverSection = {
1164
- version: '1.5.1',
1093
+ version: SERVER_VERSION,
1165
1094
  toolkit: detectToolkit(),
1166
1095
  verbosity: detectVerbosity(),
1167
1096
  locale: process.env.PINEPAPER_LOCALE || 'en',
@@ -1382,54 +1311,8 @@ You can now start creating new items on a clean canvas.`,
1382
1311
  };
1383
1312
  }
1384
1313
  // -----------------------------------------------------------------------
1385
- // TOOL GUIDE (on-demand documentation)
1314
+ // TOOL GUIDE — extracted to src/tools/handlers/tool-guide.ts
1386
1315
  // -----------------------------------------------------------------------
1387
- case 'pinepaper_tool_guide': {
1388
- const input = ToolGuideInputSchema.parse(args);
1389
- const { PINEPAPER_TOOLS, AI_AGENT_GUIDE } = await import('./definitions.js');
1390
- const { TOOL_TAGS } = await import('./toolkits.js');
1391
- // No args → full AI Agent Guide
1392
- if (!input.tool && !input.category) {
1393
- return {
1394
- content: [{ type: 'text', text: AI_AGENT_GUIDE }],
1395
- };
1396
- }
1397
- // Specific tool → full verbose description
1398
- if (input.tool) {
1399
- const tool = PINEPAPER_TOOLS.find((t) => t.name === input.tool);
1400
- if (!tool) {
1401
- return {
1402
- content: [{ type: 'text', text: `Unknown tool: ${input.tool}. Use pinepaper_tool_guide with no args to see the AI Agent Guide.` }],
1403
- isError: true,
1404
- };
1405
- }
1406
- return {
1407
- content: [{ type: 'text', text: `# ${tool.name}\n\n${tool.description}` }],
1408
- };
1409
- }
1410
- // Category → all tools in that tag with descriptions
1411
- if (input.category) {
1412
- const toolNames = TOOL_TAGS[input.category];
1413
- if (!toolNames) {
1414
- const available = Object.keys(TOOL_TAGS).join(', ');
1415
- return {
1416
- content: [{ type: 'text', text: `Unknown category: ${input.category}. Available: ${available}` }],
1417
- isError: true,
1418
- };
1419
- }
1420
- const lines = [`# Category: ${input.category}\n`];
1421
- for (const name of toolNames) {
1422
- const tool = PINEPAPER_TOOLS.find((t) => t.name === name);
1423
- if (tool) {
1424
- lines.push(`## ${tool.name}\n${tool.description}\n`);
1425
- }
1426
- }
1427
- return {
1428
- content: [{ type: 'text', text: lines.join('\n') }],
1429
- };
1430
- }
1431
- return { content: [{ type: 'text', text: AI_AGENT_GUIDE }] };
1432
- }
1433
1316
  // -----------------------------------------------------------------------
1434
1317
  // ASSET TOOLS
1435
1318
  // -----------------------------------------------------------------------
@@ -1713,6 +1596,37 @@ You can now start creating new items on a clean canvas.`,
1713
1596
  }
1714
1597
  case 'pinepaper_agent_batch_execute': {
1715
1598
  const input = AgentBatchExecuteInputSchema.parse(args);
1599
+ // Ontology preflight — catch typos in itemType/relationType/
1600
+ // effectType/generatorName before generating JS, so the LLM gets a
1601
+ // structured hint instead of a silent failure inside the browser.
1602
+ // Opt-out via skipValidation: true when the caller knows what they're
1603
+ // doing (e.g. experimental vocabulary).
1604
+ if (!input.skipValidation) {
1605
+ const ops = input.operations;
1606
+ const violations = validateBatchVocabulary(ops);
1607
+ if (violations.length > 0) {
1608
+ 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) => ({
1609
+ path: `operations[${v.opIndex}].${v.field}`,
1610
+ opType: v.opType,
1611
+ vocabulary: v.hint,
1612
+ })));
1613
+ }
1614
+ // Property-key typo detection — narrow scope (Levenshtein ≤2) so
1615
+ // legitimate extras like `gradient` pass through. Catches the
1616
+ // common typo class (radiues vs radius, fontSiz vs fontSize)
1617
+ // before browser execution silently drops them.
1618
+ const typos = detectBatchPropertyTypos(ops);
1619
+ if (typos.length > 0) {
1620
+ 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) => ({
1621
+ path: `operations[${t.opIndex}].properties.${t.property}`,
1622
+ opType: t.opType,
1623
+ itemType: t.itemType,
1624
+ received: t.property,
1625
+ suggestion: t.suggestion,
1626
+ validKeys: t.validKeys,
1627
+ })));
1628
+ }
1629
+ }
1716
1630
  const code = codeGenerator.generateAgentBatchExecute(input);
1717
1631
  const description = `Batch executed ${input.operations.length} operations${input.atomic !== false ? ' (atomic)' : ''}`;
1718
1632
  // Check for out-of-bounds items before execution
@@ -1809,56 +1723,6 @@ You can now start creating new items on a clean canvas.`,
1809
1723
  return executeOrGenerate(code, description, options, 'pinepaper_agent_analyze');
1810
1724
  }
1811
1725
  // -----------------------------------------------------------------------
1812
- // INTERACTIVE TRIGGER TOOLS
1813
- // -----------------------------------------------------------------------
1814
- case 'pinepaper_add_trigger': {
1815
- const input = AddTriggerInputSchema.parse(args);
1816
- const code = codeGenerator.generateAddTrigger(input);
1817
- const description = `Added ${input.event} trigger to ${input.itemId} with ${input.actions.length} actions`;
1818
- return executeOrGenerate(code, description, options, 'pinepaper_add_trigger');
1819
- }
1820
- case 'pinepaper_remove_trigger': {
1821
- const input = RemoveTriggerInputSchema.parse(args);
1822
- const code = codeGenerator.generateRemoveTrigger(input);
1823
- const description = input.removeAll
1824
- ? `Removed all triggers from ${input.itemId}`
1825
- : `Removed ${input.event} trigger from ${input.itemId}`;
1826
- return executeOrGenerate(code, description, options, 'pinepaper_remove_trigger');
1827
- }
1828
- case 'pinepaper_query_triggers': {
1829
- const input = QueryTriggersInputSchema.parse(args);
1830
- const code = codeGenerator.generateQueryTriggers(input);
1831
- const description = input.itemId
1832
- ? `Queried triggers for ${input.itemId}`
1833
- : 'Queried all triggers on canvas';
1834
- return executeOrGenerate(code, description, options, 'pinepaper_query_triggers');
1835
- }
1836
- // -----------------------------------------------------------------------
1837
- // QUIZ/LMS TOOLS
1838
- // -----------------------------------------------------------------------
1839
- case 'pinepaper_create_quiz': {
1840
- const input = CreateQuizInputSchema.parse(args);
1841
- const code = codeGenerator.generateCreateQuiz(input);
1842
- const description = `Created quiz${input.title ? ` "${input.title}"` : ''} with ${input.questions.length} questions`;
1843
- return executeOrGenerate(code, description, options, 'pinepaper_create_quiz');
1844
- }
1845
- case 'pinepaper_get_quiz_state': {
1846
- const input = GetQuizStateInputSchema.parse(args);
1847
- const code = codeGenerator.generateGetQuizState(input);
1848
- const description = input.quizId
1849
- ? `Got state for quiz ${input.quizId}`
1850
- : 'Got state for active quiz';
1851
- return executeOrGenerate(code, description, options, 'pinepaper_get_quiz_state');
1852
- }
1853
- case 'pinepaper_reset_quiz': {
1854
- const input = ResetQuizInputSchema.parse(args);
1855
- const code = codeGenerator.generateResetQuiz(input);
1856
- const description = input.quizId
1857
- ? `Reset quiz ${input.quizId}`
1858
- : 'Reset active quiz';
1859
- return executeOrGenerate(code, description, options, 'pinepaper_reset_quiz');
1860
- }
1861
- // -----------------------------------------------------------------------
1862
1726
  // LETTER COLLAGE TOOLS
1863
1727
  // -----------------------------------------------------------------------
1864
1728
  case 'pinepaper_create_letter_collage': {
@@ -1873,154 +1737,9 @@ You can now start creating new items on a clean canvas.`,
1873
1737
  const description = `Animated collage ${input.collageId} with ${input.animationType}`;
1874
1738
  return executeOrGenerate(code, description, options, 'pinepaper_animate_letter_collage');
1875
1739
  }
1876
- case 'pinepaper_get_letter_collage_options': {
1877
- const code = codeGenerator.generateGetLetterCollageOptions();
1878
- return executeOrGenerate(code, 'Gets available letter collage styles and palettes', options, 'pinepaper_get_letter_collage_options');
1879
- }
1880
- // -----------------------------------------------------------------------
1881
- // CANVAS PRESETS TOOLS
1882
- // -----------------------------------------------------------------------
1883
- case 'pinepaper_get_canvas_presets': {
1884
- const code = codeGenerator.generateGetCanvasPresets();
1885
- return executeOrGenerate(code, 'Gets all available canvas presets', options, 'pinepaper_get_canvas_presets');
1886
- }
1887
- // -----------------------------------------------------------------------
1888
- // MAP TOOLS
1889
- // -----------------------------------------------------------------------
1890
- case 'pinepaper_load_map': {
1891
- const input = LoadMapInputSchema.parse(args);
1892
- const code = codeGenerator.generateLoadMap(input);
1893
- const description = `Loads ${input.mapId} map with ${input.projection || 'default'} projection`;
1894
- return executeOrGenerate(code, description, options, 'pinepaper_load_map');
1895
- }
1896
- case 'pinepaper_highlight_regions': {
1897
- const input = HighlightRegionsInputSchema.parse(args);
1898
- const code = codeGenerator.generateHighlightRegions(input);
1899
- const description = `Highlights ${input.regionIds.length} region(s) on the map`;
1900
- return executeOrGenerate(code, description, options, 'pinepaper_highlight_regions');
1901
- }
1902
- case 'pinepaper_unhighlight_regions': {
1903
- const input = UnhighlightRegionsInputSchema.parse(args);
1904
- const code = codeGenerator.generateUnhighlightRegions(input);
1905
- return executeOrGenerate(code, 'Removes region highlights', options, 'pinepaper_unhighlight_regions');
1906
- }
1907
- case 'pinepaper_apply_data_colors': {
1908
- const input = ApplyDataColorsInputSchema.parse(args);
1909
- const code = codeGenerator.generateApplyDataColors(input);
1910
- const regionCount = Object.keys(input.data).length;
1911
- const description = `Applies choropleth coloring to ${regionCount} region(s)`;
1912
- return executeOrGenerate(code, description, options, 'pinepaper_apply_data_colors');
1913
- }
1914
- case 'pinepaper_add_marker': {
1915
- const input = AddMarkerInputSchema.parse(args);
1916
- const code = codeGenerator.generateAddMarker(input);
1917
- const description = `Adds marker at [${input.lat}, ${input.lon}]${input.label ? `: ${input.label}` : ''}`;
1918
- return executeOrGenerate(code, description, options, 'pinepaper_add_marker');
1919
- }
1920
- case 'pinepaper_add_map_labels': {
1921
- const input = AddMapLabelsInputSchema.parse(args);
1922
- const code = codeGenerator.generateAddMapLabels(input);
1923
- return executeOrGenerate(code, 'Adds labels to map regions', options, 'pinepaper_add_map_labels');
1924
- }
1925
- case 'pinepaper_pan_map': {
1926
- const input = PanMapInputSchema.parse(args);
1927
- const code = codeGenerator.generatePanMap(input);
1928
- const description = `Pans map to [${input.lat}, ${input.lon}]`;
1929
- return executeOrGenerate(code, description, options, 'pinepaper_pan_map');
1930
- }
1931
- case 'pinepaper_zoom_map': {
1932
- const input = ZoomMapInputSchema.parse(args);
1933
- const code = codeGenerator.generateZoomMap(input);
1934
- const description = `Sets map zoom level to ${input.level}`;
1935
- return executeOrGenerate(code, description, options, 'pinepaper_zoom_map');
1936
- }
1937
- case 'pinepaper_export_map': {
1938
- const code = codeGenerator.generateExportMap();
1939
- return executeOrGenerate(code, 'Exports map configuration', options, 'pinepaper_export_map');
1940
- }
1941
- case 'pinepaper_import_custom_map': {
1942
- const input = ImportCustomMapInputSchema.parse(args);
1943
- const code = codeGenerator.generateImportCustomMap(input);
1944
- const description = input.url ? `Imports custom map from URL` : 'Imports custom GeoJSON map';
1945
- return executeOrGenerate(code, description, options, 'pinepaper_import_custom_map');
1946
- }
1947
- case 'pinepaper_get_region_at_point': {
1948
- const input = GetRegionAtPointInputSchema.parse(args);
1949
- const code = codeGenerator.generateGetRegionAtPoint(input);
1950
- const description = `Gets region at point [${input.x}, ${input.y}]`;
1951
- return executeOrGenerate(code, description, options, 'pinepaper_get_region_at_point');
1952
- }
1953
- // -----------------------------------------------------------------------
1954
- // MAP ANIMATION/CSV TOOLS
1955
- // -----------------------------------------------------------------------
1956
- case 'pinepaper_animate_map_regions': {
1957
- const input = AnimateMapRegionsInputSchema.parse(args);
1958
- const code = codeGenerator.generateAnimateMapRegions(input);
1959
- const regionCount = Object.keys(input.regions).length;
1960
- const description = `Animates ${regionCount} map region(s) over ${input.duration || 5}s`;
1961
- return executeOrGenerate(code, description, options, 'pinepaper_animate_map_regions');
1962
- }
1963
- case 'pinepaper_animate_map_wave': {
1964
- const input = AnimateMapWaveInputSchema.parse(args);
1965
- const code = codeGenerator.generateAnimateMapWave(input);
1966
- const description = `Creates ${input.waveDirection || 'horizontal'} wave animation`;
1967
- return executeOrGenerate(code, description, options, 'pinepaper_animate_map_wave');
1968
- }
1969
- case 'pinepaper_stop_map_animations': {
1970
- const input = StopMapAnimationsInputSchema.parse(args);
1971
- const code = codeGenerator.generateStopMapAnimations(input);
1972
- return executeOrGenerate(code, 'Stops map region animations', options, 'pinepaper_stop_map_animations');
1973
- }
1974
- case 'pinepaper_get_animated_map_regions': {
1975
- const code = codeGenerator.generateGetAnimatedMapRegions();
1976
- return executeOrGenerate(code, 'Gets list of animated map regions', options, 'pinepaper_get_animated_map_regions');
1977
- }
1978
- case 'pinepaper_export_map_region_csv': {
1979
- const input = ExportMapRegionCSVInputSchema.parse(args);
1980
- const code = codeGenerator.generateExportMapRegionCSV(input);
1981
- return executeOrGenerate(code, 'Exports map region data as CSV', options, 'pinepaper_export_map_region_csv');
1982
- }
1983
- case 'pinepaper_import_map_region_csv': {
1984
- const input = ImportMapRegionCSVInputSchema.parse(args);
1985
- const code = codeGenerator.generateImportMapRegionCSV(input);
1986
- return executeOrGenerate(code, 'Imports CSV data to update map regions', options, 'pinepaper_import_map_region_csv');
1987
- }
1988
- case 'pinepaper_select_map_regions': {
1989
- const input = SelectMapRegionsInputSchema.parse(args);
1990
- const code = codeGenerator.generateSelectMapRegions(input);
1991
- const description = `Selects ${input.regionIds.length} map region(s)`;
1992
- return executeOrGenerate(code, description, options, 'pinepaper_select_map_regions');
1993
- }
1994
- case 'pinepaper_deselect_map_regions': {
1995
- const input = DeselectMapRegionsInputSchema.parse(args);
1996
- const code = codeGenerator.generateDeselectMapRegions(input);
1997
- return executeOrGenerate(code, 'Deselects map regions', options, 'pinepaper_deselect_map_regions');
1998
- }
1999
- case 'pinepaper_get_highlighted_map_regions': {
2000
- const code = codeGenerator.generateGetHighlightedMapRegions();
2001
- return executeOrGenerate(code, 'Gets list of highlighted map regions', options, 'pinepaper_get_highlighted_map_regions');
2002
- }
2003
- case 'pinepaper_export_map_geojson': {
2004
- const code = codeGenerator.generateExportMapGeoJson({
2005
- includeStyles: args.includeStyles,
2006
- includeMetadata: args.includeMetadata,
2007
- selectedOnly: args.selectedOnly,
2008
- download: args.download,
2009
- filename: args.filename,
2010
- });
2011
- return executeOrGenerate(code, 'Exports map as GeoJSON', options, 'pinepaper_export_map_geojson');
2012
- }
2013
- case 'pinepaper_export_original_map_geojson': {
2014
- const code = codeGenerator.generateExportOriginalMapGeoJson({
2015
- download: args.download,
2016
- filename: args.filename,
2017
- });
2018
- return executeOrGenerate(code, 'Exports original map GeoJSON', options, 'pinepaper_export_original_map_geojson');
2019
- }
2020
- case 'pinepaper_get_map_source_info': {
2021
- const code = codeGenerator.generateGetMapSourceInfo();
2022
- return executeOrGenerate(code, 'Gets map source information', options, 'pinepaper_get_map_source_info');
2023
- }
1740
+ // -----------------------------------------------------------------------
1741
+ // MAP TOOLS — extracted to src/tools/handlers/maps.ts
1742
+ // -----------------------------------------------------------------------
2024
1743
  case 'pinepaper_register_item': {
2025
1744
  const code = codeGenerator.generateRegisterItem({
2026
1745
  itemJson: args.itemJson,
@@ -2030,7 +1749,7 @@ You can now start creating new items on a clean canvas.`,
2030
1749
  return executeOrGenerate(code, 'Registers Paper.js item', options, 'pinepaper_register_item');
2031
1750
  }
2032
1751
  // -----------------------------------------------------------------------
2033
- // TEMPLATE TOOLS
1752
+ // TEMPLATE TOOL
2034
1753
  // -----------------------------------------------------------------------
2035
1754
  case 'pinepaper_apply_template': {
2036
1755
  const parsed = ApplyTemplateInputSchema.parse(args);
@@ -2046,169 +1765,8 @@ You can now start creating new items on a clean canvas.`,
2046
1765
  return executeOrGenerate(code, 'Imports image', options, 'pinepaper_import_image');
2047
1766
  }
2048
1767
  // -----------------------------------------------------------------------
2049
- // ONTOLOGY TOOLS
1768
+ // ONTOLOGY TOOLS — extracted to src/tools/handlers/ontology.ts
2050
1769
  // -----------------------------------------------------------------------
2051
- case 'pinepaper_analyze_design': {
2052
- const input = AnalyzeDesignInputSchema.parse(args);
2053
- const { DesignGraph } = await import('../ontology/index.js');
2054
- const dg = new DesignGraph();
2055
- const graph = dg.extractFromDefinition(input.definition);
2056
- const jsonLd = dg.toJsonLd(graph);
2057
- return {
2058
- content: [{
2059
- type: 'text',
2060
- text: JSON.stringify({
2061
- templateId: graph.templateId,
2062
- templateName: graph.templateName,
2063
- category: graph.category,
2064
- dimensions: graph.dimensions,
2065
- duration: graph.duration,
2066
- nodeCount: graph.nodes.length,
2067
- edgeCount: graph.edges.length,
2068
- nodeTypes: Object.fromEntries(Object.entries(graph.fingerprint?.nodeTypeCounts || {}).sort((a, b) => b[1] - a[1])),
2069
- edgeTypes: Object.fromEntries(Object.entries(graph.fingerprint?.edgeTypeCounts || {}).sort((a, b) => b[1] - a[1])),
2070
- patterns: graph.patterns,
2071
- mathFunctions: graph.mathFunctions,
2072
- generator: graph.generator,
2073
- semantics: graph.semantics,
2074
- fingerprint: graph.fingerprint,
2075
- jsonLd,
2076
- }, null, 2),
2077
- }],
2078
- };
2079
- }
2080
- case 'pinepaper_validate_design': {
2081
- const input = ValidateDesignInputSchema.parse(args);
2082
- const { DesignGraph, KnowledgeGraphValidator } = await import('../ontology/index.js');
2083
- const dg = new DesignGraph();
2084
- const validator = new KnowledgeGraphValidator();
2085
- const definition = input.definition;
2086
- const validation = validator.validateTemplate(definition);
2087
- let quality = null;
2088
- if (validation.valid) {
2089
- const graph = dg.extractFromDefinition(definition);
2090
- quality = validator.scoreQuality(definition, graph);
2091
- }
2092
- return {
2093
- content: [{
2094
- type: 'text',
2095
- text: JSON.stringify({
2096
- validation,
2097
- quality,
2098
- }, null, 2),
2099
- }],
2100
- };
2101
- }
2102
- // -----------------------------------------------------------------------
2103
- // ONTOLOGY QUERY (read-only graph navigation)
2104
- // -----------------------------------------------------------------------
2105
- case 'pinepaper_query_ontology': {
2106
- const input = QueryOntologyInputSchema.parse(args);
2107
- const { PP_VOCABULARY, ITEM_TYPE_MAP, RELATION_TYPE_MAP } = await import('../ontology/vocabulary.js');
2108
- const { DesignGraph } = await import('../ontology/design-graph.js');
2109
- const dg = new DesignGraph();
2110
- let result;
2111
- switch (input.query) {
2112
- case 'list_types': {
2113
- const entries = Object.entries(PP_VOCABULARY.types);
2114
- const filtered = entries.filter(([key, def]) => {
2115
- if (input.category && def.category !== input.category)
2116
- return false;
2117
- if (!input.includeAbstract && def.abstract)
2118
- return false;
2119
- return true;
2120
- });
2121
- result = Object.fromEntries(filtered);
2122
- break;
2123
- }
2124
- case 'list_edges': {
2125
- const entries = Object.entries(PP_VOCABULARY.edges);
2126
- const filtered = input.category
2127
- ? entries.filter(([, def]) => def.category === input.category)
2128
- : entries;
2129
- result = Object.fromEntries(filtered);
2130
- break;
2131
- }
2132
- case 'list_generators': {
2133
- const entries = Object.entries(PP_VOCABULARY.generators);
2134
- const filtered = input.category
2135
- ? entries.filter(([, def]) => def.category === input.category)
2136
- : entries;
2137
- result = Object.fromEntries(filtered);
2138
- break;
2139
- }
2140
- case 'list_effects': {
2141
- result = ['sparkle', 'blast', 'smoke', 'fire', 'rain', 'snow', 'confetti', 'ripple', 'glow', 'electric'];
2142
- break;
2143
- }
2144
- case 'list_patterns': {
2145
- result = PP_VOCABULARY.patterns;
2146
- break;
2147
- }
2148
- case 'list_math_functions': {
2149
- result = PP_VOCABULARY.mathFunctions;
2150
- break;
2151
- }
2152
- case 'type_hierarchy': {
2153
- if (!input.ppType)
2154
- return errorResult('INVALID_INPUT', 'ppType is required for type_hierarchy');
2155
- result = dg.getTypeHierarchy(input.ppType);
2156
- break;
2157
- }
2158
- case 'type_children': {
2159
- if (!input.ppType)
2160
- return errorResult('INVALID_INPUT', 'ppType is required for type_children');
2161
- result = dg.getTypeChildren(input.ppType);
2162
- break;
2163
- }
2164
- case 'type_properties': {
2165
- if (!input.ppType)
2166
- return errorResult('INVALID_INPUT', 'ppType is required for type_properties');
2167
- result = dg.getPropertiesFor(input.ppType);
2168
- break;
2169
- }
2170
- case 'animatable_properties': {
2171
- if (!input.ppType)
2172
- return errorResult('INVALID_INPUT', 'ppType is required for animatable_properties');
2173
- result = dg.getAnimatableProperties(input.ppType);
2174
- break;
2175
- }
2176
- case 'is_subtype': {
2177
- if (!input.typeA || !input.typeB)
2178
- return errorResult('INVALID_INPUT', 'typeA and typeB are required for is_subtype');
2179
- result = { isSubtype: dg.isSubtypeOf(input.typeA, input.typeB) };
2180
- break;
2181
- }
2182
- case 'edge_info': {
2183
- const edgeName = input.relationName || input.ppType;
2184
- if (!edgeName)
2185
- return errorResult('INVALID_INPUT', 'relationName or ppType is required for edge_info');
2186
- const ppEdge = PP_VOCABULARY.edges[edgeName] || PP_VOCABULARY.edges[RELATION_TYPE_MAP[edgeName] || ''];
2187
- result = ppEdge || { error: `Unknown edge: ${edgeName}` };
2188
- break;
2189
- }
2190
- case 'node_type': {
2191
- if (!input.itemType)
2192
- return errorResult('INVALID_INPUT', 'itemType is required for node_type');
2193
- result = { ppType: dg.getNodeType(input.itemType) };
2194
- break;
2195
- }
2196
- case 'edge_type': {
2197
- if (!input.relationName)
2198
- return errorResult('INVALID_INPUT', 'relationName is required for edge_type');
2199
- result = { ppEdge: dg.getEdgeType(input.relationName) };
2200
- break;
2201
- }
2202
- default:
2203
- return errorResult('INVALID_INPUT', `Unknown query: ${input.query}`);
2204
- }
2205
- return {
2206
- content: [{
2207
- type: 'text',
2208
- text: JSON.stringify(result, null, 2),
2209
- }],
2210
- };
2211
- }
2212
1770
  // -----------------------------------------------------------------------
2213
1771
  // UNKNOWN TOOL
2214
1772
  // -----------------------------------------------------------------------
@@ -2216,7 +1774,7 @@ You can now start creating new items on a clean canvas.`,
2216
1774
  const message = i18n
2217
1775
  ? i18n.getError('unknownTool', { toolName })
2218
1776
  : `Unknown tool: ${toolName}`;
2219
- return errorResult('UNKNOWN_TOOL', message, {
1777
+ return errorResult(ErrorCodes.UNKNOWN_TOOL, message, {
2220
1778
  availableTools: [
2221
1779
  'pinepaper_create_item',
2222
1780
  'pinepaper_modify_item',
@@ -2245,12 +1803,7 @@ You can now start creating new items on a clean canvas.`,
2245
1803
  'pinepaper_get_mask_types',
2246
1804
  'pinepaper_get_mask_animations',
2247
1805
  'pinepaper_camera_animate',
2248
- 'pinepaper_camera_zoom',
2249
- 'pinepaper_camera_pan',
2250
- 'pinepaper_camera_move_to',
2251
- 'pinepaper_camera_reset',
2252
- 'pinepaper_camera_stop',
2253
- 'pinepaper_camera_state',
1806
+ 'pinepaper_camera',
2254
1807
  'pinepaper_get_items',
2255
1808
  'pinepaper_get_relation_stats',
2256
1809
  'pinepaper_set_background_color',
@@ -2259,6 +1812,7 @@ You can now start creating new items on a clean canvas.`,
2259
1812
  'pinepaper_clear_canvas',
2260
1813
  'pinepaper_refresh_page',
2261
1814
  'pinepaper_import_svg',
1815
+ 'pinepaper_import_mermaid',
2262
1816
  'pinepaper_add_filter',
2263
1817
  'pinepaper_export_svg',
2264
1818
  'pinepaper_export_training_data',
@@ -2293,64 +1847,16 @@ You can now start creating new items on a clean canvas.`,
2293
1847
  'pinepaper_agent_batch_execute',
2294
1848
  'pinepaper_agent_export',
2295
1849
  'pinepaper_agent_analyze',
2296
- // Interactive trigger tools
2297
- 'pinepaper_add_trigger',
2298
- 'pinepaper_remove_trigger',
2299
- 'pinepaper_query_triggers',
2300
- // Quiz/LMS tools
2301
- 'pinepaper_create_quiz',
2302
- 'pinepaper_get_quiz_state',
2303
- 'pinepaper_reset_quiz',
2304
1850
  // Letter collage tools
2305
1851
  'pinepaper_create_letter_collage',
2306
1852
  'pinepaper_animate_letter_collage',
2307
- 'pinepaper_get_letter_collage_options',
2308
- // Canvas presets tools
2309
- 'pinepaper_get_canvas_presets',
2310
- // Map tools
2311
- 'pinepaper_load_map',
2312
- 'pinepaper_highlight_regions',
2313
- 'pinepaper_unhighlight_regions',
2314
- 'pinepaper_apply_data_colors',
2315
- 'pinepaper_add_marker',
2316
- 'pinepaper_add_map_labels',
2317
- 'pinepaper_pan_map',
2318
- 'pinepaper_zoom_map',
2319
- 'pinepaper_export_map',
2320
- 'pinepaper_import_custom_map',
2321
- 'pinepaper_get_region_at_point',
2322
- // Map animation/CSV tools
2323
- 'pinepaper_animate_map_regions',
2324
- 'pinepaper_animate_map_wave',
2325
- 'pinepaper_stop_map_animations',
2326
- 'pinepaper_get_animated_map_regions',
2327
- 'pinepaper_export_map_region_csv',
2328
- 'pinepaper_import_map_region_csv',
2329
- 'pinepaper_select_map_regions',
2330
- 'pinepaper_deselect_map_regions',
2331
- 'pinepaper_get_highlighted_map_regions',
2332
- 'pinepaper_export_map_geojson',
2333
- 'pinepaper_export_original_map_geojson',
2334
- 'pinepaper_get_map_source_info',
1853
+ // Map tools (4 action-dispatched)
1854
+ 'pinepaper_map',
1855
+ 'pinepaper_map_regions',
1856
+ 'pinepaper_map_animation',
1857
+ 'pinepaper_map_data',
2335
1858
  // Font tools
2336
- 'pinepaper_font_show_studio',
2337
- 'pinepaper_font_set_name',
2338
- 'pinepaper_font_get_required_chars',
2339
- 'pinepaper_font_get_status',
2340
- 'pinepaper_font_create_glyph',
2341
- 'pinepaper_font_create_space',
2342
- 'pinepaper_font_remove_glyph',
2343
- 'pinepaper_font_set_metrics',
2344
- 'pinepaper_font_export',
2345
- 'pinepaper_font_load_into_document',
2346
- 'pinepaper_font_export_data',
2347
- 'pinepaper_font_import_data',
2348
- 'pinepaper_font_clear',
2349
- 'pinepaper_font_remove_overlap',
2350
- 'pinepaper_font_correct_direction',
2351
- 'pinepaper_font_cleanup_path',
2352
- // Template tools
2353
- 'pinepaper_apply_template',
1859
+ 'pinepaper_font',
2354
1860
  // Image import tools
2355
1861
  'pinepaper_import_image',
2356
1862
  // Paper.js direct access tools
@@ -2359,6 +1865,7 @@ You can now start creating new items on a clean canvas.`,
2359
1865
  'pinepaper_tool_guide',
2360
1866
  'pinepaper_set_toolkit',
2361
1867
  // Ontology tools
1868
+ 'pinepaper_get_canvas_ontology',
2362
1869
  'pinepaper_analyze_design',
2363
1870
  'pinepaper_validate_design',
2364
1871
  'pinepaper_query_ontology',
@@ -2372,10 +1879,21 @@ You can now start creating new items on a clean canvas.`,
2372
1879
  'pinepaper_cutout_style',
2373
1880
  // Composition & view tools
2374
1881
  'pinepaper_precomp',
2375
- 'pinepaper_view',
2376
1882
  'pinepaper_background',
2377
1883
  // Canvas query tools
2378
1884
  'pinepaper_query',
1885
+ // Deformation, sprite sheets, interaction
1886
+ 'pinepaper_deform',
1887
+ 'pinepaper_sprite_sheet',
1888
+ 'pinepaper_interaction',
1889
+ // Widget export
1890
+ 'pinepaper_export_widget',
1891
+ 'pinepaper_export_widget_html',
1892
+ // Data visualization, magic, physics, measurement
1893
+ 'pinepaper_create_chart',
1894
+ 'pinepaper_magic',
1895
+ 'pinepaper_physics',
1896
+ 'pinepaper_measurement',
2379
1897
  ],
2380
1898
  });
2381
1899
  }