@pinepaper.studio/mcp-server 1.5.3 → 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 (71) 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/index.d.ts.map +1 -1
  8. package/dist/index.js +47 -2
  9. package/dist/index.js.map +1 -1
  10. package/dist/ontology/hints.d.ts +71 -0
  11. package/dist/ontology/hints.d.ts.map +1 -0
  12. package/dist/ontology/hints.js +189 -0
  13. package/dist/ontology/hints.js.map +1 -0
  14. package/dist/ontology/vocabulary.d.ts.map +1 -1
  15. package/dist/ontology/vocabulary.js +34 -0
  16. package/dist/ontology/vocabulary.js.map +1 -1
  17. package/dist/prompts/index.d.ts.map +1 -1
  18. package/dist/prompts/index.js +146 -0
  19. package/dist/prompts/index.js.map +1 -1
  20. package/dist/tools/compact-descriptions.d.ts +3 -0
  21. package/dist/tools/compact-descriptions.d.ts.map +1 -1
  22. package/dist/tools/compact-descriptions.js +17 -8
  23. package/dist/tools/compact-descriptions.js.map +1 -1
  24. package/dist/tools/definitions.d.ts +1 -1
  25. package/dist/tools/definitions.d.ts.map +1 -1
  26. package/dist/tools/definitions.js +1090 -1639
  27. package/dist/tools/definitions.js.map +1 -1
  28. package/dist/tools/handlers/camera.d.ts +19 -0
  29. package/dist/tools/handlers/camera.d.ts.map +1 -0
  30. package/dist/tools/handlers/camera.js +94 -0
  31. package/dist/tools/handlers/camera.js.map +1 -0
  32. package/dist/tools/handlers/export.d.ts +12 -0
  33. package/dist/tools/handlers/export.d.ts.map +1 -0
  34. package/dist/tools/handlers/export.js +30 -0
  35. package/dist/tools/handlers/export.js.map +1 -0
  36. package/dist/tools/handlers/font.d.ts +12 -0
  37. package/dist/tools/handlers/font.d.ts.map +1 -0
  38. package/dist/tools/handlers/font.js +135 -0
  39. package/dist/tools/handlers/font.js.map +1 -0
  40. package/dist/tools/handlers/maps.d.ts +19 -0
  41. package/dist/tools/handlers/maps.d.ts.map +1 -0
  42. package/dist/tools/handlers/maps.js +184 -0
  43. package/dist/tools/handlers/maps.js.map +1 -0
  44. package/dist/tools/handlers/ontology.d.ts +20 -0
  45. package/dist/tools/handlers/ontology.d.ts.map +1 -0
  46. package/dist/tools/handlers/ontology.js +185 -0
  47. package/dist/tools/handlers/ontology.js.map +1 -0
  48. package/dist/tools/handlers/tool-guide.d.ts +14 -0
  49. package/dist/tools/handlers/tool-guide.d.ts.map +1 -0
  50. package/dist/tools/handlers/tool-guide.js +52 -0
  51. package/dist/tools/handlers/tool-guide.js.map +1 -0
  52. package/dist/tools/handlers.d.ts +18 -0
  53. package/dist/tools/handlers.d.ts.map +1 -1
  54. package/dist/tools/handlers.js +205 -603
  55. package/dist/tools/handlers.js.map +1 -1
  56. package/dist/tools/minimal-descriptions.d.ts +4 -0
  57. package/dist/tools/minimal-descriptions.d.ts.map +1 -1
  58. package/dist/tools/minimal-descriptions.js +45 -54
  59. package/dist/tools/minimal-descriptions.js.map +1 -1
  60. package/dist/tools/toolkits.d.ts.map +1 -1
  61. package/dist/tools/toolkits.js +36 -45
  62. package/dist/tools/toolkits.js.map +1 -1
  63. package/dist/types/code-generator.d.ts +36 -1
  64. package/dist/types/code-generator.d.ts.map +1 -1
  65. package/dist/types/code-generator.js +823 -35
  66. package/dist/types/code-generator.js.map +1 -1
  67. package/dist/types/schemas.d.ts +1116 -444
  68. package/dist/types/schemas.d.ts.map +1 -1
  69. package/dist/types/schemas.js +290 -18
  70. package/dist/types/schemas.js.map +1 -1
  71. package/package.json +1 -1
@@ -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,22 +25,39 @@ ApplyAnimatedMaskInputSchema, ApplyCustomMaskInputSchema, RemoveMaskInputSchema,
29
25
  ApplyTemplateInputSchema,
30
26
  // Image import schemas
31
27
  ImportImageInputSchema,
32
- // Tool guide schema
33
- ToolGuideInputSchema,
34
- // Ontology schemas
35
- AnalyzeDesignInputSchema, ValidateDesignInputSchema, QueryOntologyInputSchema,
36
28
  // Scene management schemas
37
29
  ManageScenesInputSchema, ScenePlaybackInputSchema,
38
30
  // New consolidated tool schemas
39
- SelectionInputSchema, TransformInputSchema, HistoryInputSchema, ImageFilterInputSchema, LassoInputSchema, CutoutStyleInputSchema, PrecompInputSchema, ViewInputSchema, BackgroundInputSchema, QueryInputSchema, ErrorCodes, } from '../types/schemas.js';
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';
40
32
  import { ZodError } from 'zod';
41
33
  import { writeFile, mkdir } from 'node:fs/promises';
42
34
  import { tmpdir } from 'node:os';
43
35
  import { join } from 'node:path';
44
36
  import { getBrowserController, resetBrowserController, } from '../browser/puppeteer-controller.js';
37
+ import { sameStudioTarget, validateStudioUrl } from '../browser/url-target.js';
45
38
  import { getPerformanceTracker } from '../metrics/index.js';
46
39
  import { formatErrorContext, captureCanvasState } from '../execution/index.js';
47
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
+ ];
48
61
  /**
49
62
  * Get screenshot mode from environment variable.
50
63
  * Defaults to 'on_request' for optimal performance.
@@ -146,7 +159,7 @@ function screenshotResult(screenshot) {
146
159
  ],
147
160
  };
148
161
  }
149
- function errorResult(code, message, details, context) {
162
+ export function errorResult(code, message, details, context) {
150
163
  // Build error object
151
164
  const errorObj = {
152
165
  success: false,
@@ -173,10 +186,18 @@ function handleValidationError(error, i18n) {
173
186
  const message = i18n
174
187
  ? i18n.getError('validationError', { message: 'Invalid input parameters' })
175
188
  : 'Invalid input parameters';
176
- return errorResult(ErrorCodes.VALIDATION_ERROR, message, error.errors.map((e) => ({
177
- path: e.path.join('.'),
178
- message: e.message,
179
- })));
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);
180
201
  }
181
202
  /**
182
203
  * Execute code in browser if connected, otherwise return generated code.
@@ -192,7 +213,7 @@ function handleValidationError(error, i18n) {
192
213
  *
193
214
  * Default is 'on_request' for optimal performance per best practices.
194
215
  */
195
- async function executeOrGenerate(code, description, options, toolName) {
216
+ export async function executeOrGenerate(code, description, options, toolName) {
196
217
  const { executeInBrowser, browserController, screenshotMode, executionMode } = options;
197
218
  const effectiveExecutionMode = executionMode ?? getExecutionMode();
198
219
  const effectiveScreenshotMode = screenshotMode ?? getScreenshotMode();
@@ -369,7 +390,7 @@ ${code}
369
390
  // =============================================================================
370
391
  // I18N HELPER FUNCTIONS
371
392
  // =============================================================================
372
- function getLocalizedSuccessMessage(i18n, key, params) {
393
+ export function getLocalizedSuccessMessage(i18n, key, params) {
373
394
  if (!i18n) {
374
395
  // Fallback to English template
375
396
  return Object.entries(params).reduce((msg, [k, v]) => msg.replace(`{{${k}}}`, String(v)), key);
@@ -453,6 +474,14 @@ export async function handleToolCall(toolName, args, options = {}) {
453
474
  async function handleToolCallInner(toolName, args, options, tracker, baseTimerId) {
454
475
  const { i18n } = options;
455
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
+ }
456
485
  switch (toolName) {
457
486
  // -----------------------------------------------------------------------
458
487
  // ITEM TOOLS
@@ -493,7 +522,7 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
493
522
  const animProps = ['animationType', 'animationSpeed', 'keyframes', 'animation'];
494
523
  const passedAnimProps = animProps.filter(p => p in (input.properties || {}));
495
524
  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. ` +
525
+ return errorResult(ErrorCodes.INVALID_PROPERTIES, `Animation properties (${passedAnimProps.join(', ')}) cannot be set via pinepaper_modify_item — they will be silently ignored. ` +
497
526
  `Use pinepaper_animate for loop animations (pulse, rotate, bounce, fade, wobble, slide) ` +
498
527
  `or pinepaper_keyframe_animate for timed/sequenced animations with precise control.`);
499
528
  }
@@ -608,6 +637,62 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
608
637
  return executeOrGenerate(code, `Query: ${input.action}`, options, 'pinepaper_query');
609
638
  }
610
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
+ }
695
+ // -----------------------------------------------------------------------
611
696
  // BATCH OPERATION TOOLS
612
697
  // -----------------------------------------------------------------------
613
698
  case 'pinepaper_batch_create': {
@@ -750,168 +835,11 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
750
835
  return executeOrGenerate(code, 'Gets available mask animation presets', options, 'pinepaper_get_mask_animations');
751
836
  }
752
837
  // -----------------------------------------------------------------------
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
- }
838
+ // CAMERA TOOLS — extracted to src/tools/handlers/camera.ts
839
+ // -----------------------------------------------------------------------
840
+ // -----------------------------------------------------------------------
841
+ // FONT TOOLS — extracted to src/tools/handlers/font.ts
842
+ // -----------------------------------------------------------------------
915
843
  // -----------------------------------------------------------------------
916
844
  // QUERY TOOLS
917
845
  // -----------------------------------------------------------------------
@@ -956,7 +884,7 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
956
884
  case 'pinepaper_refresh_page': {
957
885
  const controller = options.browserController || getBrowserController();
958
886
  if (!controller.connected) {
959
- 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.');
960
888
  }
961
889
  try {
962
890
  // Refresh the page
@@ -985,7 +913,7 @@ You can now start creating new items on a clean canvas.`,
985
913
  return { content };
986
914
  }
987
915
  catch (error) {
988
- 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');
989
917
  }
990
918
  }
991
919
  // -----------------------------------------------------------------------
@@ -997,6 +925,15 @@ You can now start creating new items on a clean canvas.`,
997
925
  const description = 'Imports SVG onto the canvas';
998
926
  return executeOrGenerate(code, description, options, 'pinepaper_import_svg');
999
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
+ }
1000
937
  // -----------------------------------------------------------------------
1001
938
  // FILTER TOOLS
1002
939
  // -----------------------------------------------------------------------
@@ -1007,46 +944,43 @@ You can now start creating new items on a clean canvas.`,
1007
944
  return executeOrGenerate(code, description, options, 'pinepaper_add_filter');
1008
945
  }
1009
946
  // -----------------------------------------------------------------------
1010
- // EXPORT TOOLS
947
+ // EXPORT TOOLS — extracted to src/tools/handlers/export.ts
1011
948
  // -----------------------------------------------------------------------
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
949
  // -----------------------------------------------------------------------
1032
950
  // BROWSER CONTROL TOOLS
1033
951
  // -----------------------------------------------------------------------
1034
952
  case 'pinepaper_browser_connect': {
1035
953
  // Default to headless: false — if you explicitly call browser_connect, you likely want to see it
1036
954
  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) {
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) {
1040
972
  return {
1041
- content: [{ type: 'text', text: `Already connected to PinePaper Studio (headless: ${headless}).` }],
973
+ content: [{ type: 'text', text: `Already connected to PinePaper Studio (${controller.studioUrl || 'default'}, headless: ${headless}).` }],
1042
974
  };
1043
975
  }
1044
- // Already connected but different headless mode → reconnect
1045
- if (controller.connected && controller.isHeadless !== headless) {
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)) {
1046
980
  await resetBrowserController();
1047
981
  }
1048
982
  try {
1049
- const newController = controller.connected ? controller : getBrowserController({ headless });
983
+ const newController = controller.connected ? controller : getBrowserController(cfg);
1050
984
  if (!newController.connected)
1051
985
  await newController.connect();
1052
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);
@@ -1054,7 +988,7 @@ You can now start creating new items on a clean canvas.`,
1054
988
  ? canvasSizeResult.result
1055
989
  : { width: 800, height: 600 };
1056
990
  return {
1057
- content: [{ type: 'text', text: `Connected to PinePaper Studio (headless: ${headless}, canvas: ${canvasSize.width}x${canvasSize.height}).` }],
991
+ content: [{ type: 'text', text: `Connected to PinePaper Studio (${newController.studioUrl || 'default'}, headless: ${headless}, canvas: ${canvasSize.width}x${canvasSize.height}).` }],
1058
992
  };
1059
993
  }
1060
994
  catch (error) {
@@ -1089,13 +1023,13 @@ You can now start creating new items on a clean canvas.`,
1089
1023
  case 'pinepaper_browser_screenshot': {
1090
1024
  const controller = getBrowserController();
1091
1025
  if (!controller.connected) {
1092
- 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.');
1093
1027
  }
1094
1028
  const screenshot = await controller.takeScreenshot();
1095
1029
  if (!screenshot) {
1096
1030
  // Capture canvas state for debugging
1097
1031
  const canvasState = await captureCanvasState(controller);
1098
- return errorResult('SCREENSHOT_FAILED', 'Failed to capture screenshot', undefined, {
1032
+ return errorResult(ErrorCodes.SCREENSHOT_FAILED, 'Failed to capture screenshot', undefined, {
1099
1033
  toolName: 'pinepaper_browser_screenshot',
1100
1034
  canvasState: canvasState || undefined,
1101
1035
  });
@@ -1382,54 +1316,8 @@ You can now start creating new items on a clean canvas.`,
1382
1316
  };
1383
1317
  }
1384
1318
  // -----------------------------------------------------------------------
1385
- // TOOL GUIDE (on-demand documentation)
1319
+ // TOOL GUIDE — extracted to src/tools/handlers/tool-guide.ts
1386
1320
  // -----------------------------------------------------------------------
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
1321
  // -----------------------------------------------------------------------
1434
1322
  // ASSET TOOLS
1435
1323
  // -----------------------------------------------------------------------
@@ -1713,6 +1601,37 @@ You can now start creating new items on a clean canvas.`,
1713
1601
  }
1714
1602
  case 'pinepaper_agent_batch_execute': {
1715
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
+ }
1716
1635
  const code = codeGenerator.generateAgentBatchExecute(input);
1717
1636
  const description = `Batch executed ${input.operations.length} operations${input.atomic !== false ? ' (atomic)' : ''}`;
1718
1637
  // Check for out-of-bounds items before execution
@@ -1887,140 +1806,9 @@ You can now start creating new items on a clean canvas.`,
1887
1806
  // -----------------------------------------------------------------------
1888
1807
  // MAP TOOLS
1889
1808
  // -----------------------------------------------------------------------
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
- }
1809
+ // -----------------------------------------------------------------------
1810
+ // MAP TOOLS — extracted to src/tools/handlers/maps.ts
1811
+ // -----------------------------------------------------------------------
2024
1812
  case 'pinepaper_register_item': {
2025
1813
  const code = codeGenerator.generateRegisterItem({
2026
1814
  itemJson: args.itemJson,
@@ -2046,169 +1834,8 @@ You can now start creating new items on a clean canvas.`,
2046
1834
  return executeOrGenerate(code, 'Imports image', options, 'pinepaper_import_image');
2047
1835
  }
2048
1836
  // -----------------------------------------------------------------------
2049
- // ONTOLOGY TOOLS
2050
- // -----------------------------------------------------------------------
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
- }
1837
+ // ONTOLOGY TOOLS — extracted to src/tools/handlers/ontology.ts
2102
1838
  // -----------------------------------------------------------------------
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
1839
  // -----------------------------------------------------------------------
2213
1840
  // UNKNOWN TOOL
2214
1841
  // -----------------------------------------------------------------------
@@ -2216,7 +1843,7 @@ You can now start creating new items on a clean canvas.`,
2216
1843
  const message = i18n
2217
1844
  ? i18n.getError('unknownTool', { toolName })
2218
1845
  : `Unknown tool: ${toolName}`;
2219
- return errorResult('UNKNOWN_TOOL', message, {
1846
+ return errorResult(ErrorCodes.UNKNOWN_TOOL, message, {
2220
1847
  availableTools: [
2221
1848
  'pinepaper_create_item',
2222
1849
  'pinepaper_modify_item',
@@ -2245,12 +1872,7 @@ You can now start creating new items on a clean canvas.`,
2245
1872
  'pinepaper_get_mask_types',
2246
1873
  'pinepaper_get_mask_animations',
2247
1874
  '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',
1875
+ 'pinepaper_camera',
2254
1876
  'pinepaper_get_items',
2255
1877
  'pinepaper_get_relation_stats',
2256
1878
  'pinepaper_set_background_color',
@@ -2259,6 +1881,7 @@ You can now start creating new items on a clean canvas.`,
2259
1881
  'pinepaper_clear_canvas',
2260
1882
  'pinepaper_refresh_page',
2261
1883
  'pinepaper_import_svg',
1884
+ 'pinepaper_import_mermaid',
2262
1885
  'pinepaper_add_filter',
2263
1886
  'pinepaper_export_svg',
2264
1887
  'pinepaper_export_training_data',
@@ -2307,48 +1930,13 @@ You can now start creating new items on a clean canvas.`,
2307
1930
  'pinepaper_get_letter_collage_options',
2308
1931
  // Canvas presets tools
2309
1932
  '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',
1933
+ // Map tools (4 action-dispatched)
1934
+ 'pinepaper_map',
1935
+ 'pinepaper_map_regions',
1936
+ 'pinepaper_map_animation',
1937
+ 'pinepaper_map_data',
2335
1938
  // 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',
1939
+ 'pinepaper_font',
2352
1940
  // Template tools
2353
1941
  'pinepaper_apply_template',
2354
1942
  // Image import tools
@@ -2359,6 +1947,7 @@ You can now start creating new items on a clean canvas.`,
2359
1947
  'pinepaper_tool_guide',
2360
1948
  'pinepaper_set_toolkit',
2361
1949
  // Ontology tools
1950
+ 'pinepaper_get_canvas_ontology',
2362
1951
  'pinepaper_analyze_design',
2363
1952
  'pinepaper_validate_design',
2364
1953
  'pinepaper_query_ontology',
@@ -2376,6 +1965,19 @@ You can now start creating new items on a clean canvas.`,
2376
1965
  'pinepaper_background',
2377
1966
  // Canvas query tools
2378
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',
2379
1981
  ],
2380
1982
  });
2381
1983
  }