@pinepaper.studio/mcp-server 1.4.3 → 1.5.1

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 (74) hide show
  1. package/README.md +89 -0
  2. package/dist/__tests__/fixtures/mock-generators.d.ts +147 -0
  3. package/dist/__tests__/fixtures/mock-generators.d.ts.map +1 -1
  4. package/dist/__tests__/fixtures/mock-generators.js +85 -0
  5. package/dist/__tests__/fixtures/mock-generators.js.map +1 -1
  6. package/dist/agent/export-manager.d.ts +77 -0
  7. package/dist/agent/export-manager.d.ts.map +1 -0
  8. package/dist/agent/export-manager.js +465 -0
  9. package/dist/agent/export-manager.js.map +1 -0
  10. package/dist/agent/index.d.ts +9 -0
  11. package/dist/agent/index.d.ts.map +1 -0
  12. package/dist/agent/index.js +12 -0
  13. package/dist/agent/index.js.map +1 -0
  14. package/dist/agent/session-manager.d.ts +109 -0
  15. package/dist/agent/session-manager.d.ts.map +1 -0
  16. package/dist/agent/session-manager.js +311 -0
  17. package/dist/agent/session-manager.js.map +1 -0
  18. package/dist/agent/types.d.ts +385 -0
  19. package/dist/agent/types.d.ts.map +1 -0
  20. package/dist/agent/types.js +141 -0
  21. package/dist/agent/types.js.map +1 -0
  22. package/dist/browser/index.d.ts +1 -1
  23. package/dist/browser/index.d.ts.map +1 -1
  24. package/dist/browser/index.js +1 -1
  25. package/dist/browser/index.js.map +1 -1
  26. package/dist/browser/puppeteer-controller.d.ts +89 -5
  27. package/dist/browser/puppeteer-controller.d.ts.map +1 -1
  28. package/dist/browser/puppeteer-controller.js +412 -11
  29. package/dist/browser/puppeteer-controller.js.map +1 -1
  30. package/dist/cli.js +61 -65
  31. package/dist/cli.js.map +1 -1
  32. package/dist/index.d.ts +6 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +2769 -150
  35. package/dist/index.js.map +1 -1
  36. package/dist/metrics/types.d.ts +3 -1
  37. package/dist/metrics/types.d.ts.map +1 -1
  38. package/dist/prompts/index.d.ts +30 -0
  39. package/dist/prompts/index.d.ts.map +1 -0
  40. package/dist/prompts/index.js +163 -0
  41. package/dist/prompts/index.js.map +1 -0
  42. package/dist/tools/compact-descriptions.d.ts +9 -0
  43. package/dist/tools/compact-descriptions.d.ts.map +1 -0
  44. package/dist/tools/compact-descriptions.js +291 -0
  45. package/dist/tools/compact-descriptions.js.map +1 -0
  46. package/dist/tools/definitions.d.ts +14 -0
  47. package/dist/tools/definitions.d.ts.map +1 -1
  48. package/dist/tools/definitions.js +4954 -1552
  49. package/dist/tools/definitions.js.map +1 -1
  50. package/dist/tools/handlers.d.ts.map +1 -1
  51. package/dist/tools/handlers.js +1302 -19
  52. package/dist/tools/handlers.js.map +1 -1
  53. package/dist/tools/index.d.ts +4 -1
  54. package/dist/tools/index.d.ts.map +1 -1
  55. package/dist/tools/index.js +2 -1
  56. package/dist/tools/index.js.map +1 -1
  57. package/dist/tools/minimal-descriptions.d.ts +9 -0
  58. package/dist/tools/minimal-descriptions.d.ts.map +1 -0
  59. package/dist/tools/minimal-descriptions.js +158 -0
  60. package/dist/tools/minimal-descriptions.js.map +1 -0
  61. package/dist/tools/toolkits.d.ts +51 -0
  62. package/dist/tools/toolkits.d.ts.map +1 -0
  63. package/dist/tools/toolkits.js +336 -0
  64. package/dist/tools/toolkits.js.map +1 -0
  65. package/dist/types/code-generator.d.ts +193 -1
  66. package/dist/types/code-generator.d.ts.map +1 -1
  67. package/dist/types/code-generator.js +2167 -26
  68. package/dist/types/code-generator.js.map +1 -1
  69. package/dist/types/schemas.d.ts +5278 -1141
  70. package/dist/types/schemas.d.ts.map +1 -1
  71. package/dist/types/schemas.js +863 -5
  72. package/dist/types/schemas.js.map +1 -1
  73. package/manifest.json +94 -2
  74. package/package.json +1 -1
@@ -6,13 +6,39 @@
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, SearchAssetsInputSchema, ImportAssetInputSchema, P5DrawInputSchema,
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,
10
10
  // Diagram schemas
11
- CreateDiagramShapeInputSchema, ConnectInputSchema, ConnectPortsInputSchema, AddPortsInputSchema, AutoLayoutInputSchema, GetDiagramShapesInputSchema, UpdateConnectorInputSchema, RemoveConnectorInputSchema, DiagramModeInputSchema, ErrorCodes, } from '../types/schemas.js';
11
+ CreateDiagramShapeInputSchema, ConnectInputSchema, ConnectPortsInputSchema, AddPortsInputSchema, AutoLayoutInputSchema, GetDiagramShapesInputSchema, UpdateConnectorInputSchema, RemoveConnectorInputSchema, DiagramModeInputSchema,
12
+ // Agent flow mode schemas
13
+ AgentStartJobInputSchema, AgentEndJobInputSchema, AgentResetInputSchema, AgentBatchExecuteInputSchema, AgentExportInputSchema, AgentAnalyzeInputSchema,
14
+ // Interactive/Trigger schemas
15
+ AddTriggerInputSchema, RemoveTriggerInputSchema, QueryTriggersInputSchema,
16
+ // Quiz schemas
17
+ CreateQuizInputSchema, GetQuizStateInputSchema, ResetQuizInputSchema,
18
+ // 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,
24
+ // Custom relation/code schemas
25
+ RegisterCustomRelationInputSchema, ExecuteCustomCodeInputSchema,
26
+ // Mask schemas
27
+ ApplyAnimatedMaskInputSchema, ApplyCustomMaskInputSchema, RemoveMaskInputSchema,
28
+ // Template schemas
29
+ ApplyTemplateInputSchema,
30
+ // Image import schemas
31
+ ImportImageInputSchema,
32
+ // Tool guide schema
33
+ ToolGuideInputSchema, ErrorCodes, } from '../types/schemas.js';
12
34
  import { ZodError } from 'zod';
35
+ import { writeFile, mkdir } from 'node:fs/promises';
36
+ import { tmpdir } from 'node:os';
37
+ import { join } from 'node:path';
13
38
  import { getBrowserController, } from '../browser/puppeteer-controller.js';
14
39
  import { getPerformanceTracker } from '../metrics/index.js';
15
40
  import { formatErrorContext, captureCanvasState } from '../execution/index.js';
41
+ import { getSessionManager } from '../agent/session-manager.js';
16
42
  /**
17
43
  * Get screenshot mode from environment variable.
18
44
  * Defaults to 'on_request' for optimal performance.
@@ -25,6 +51,74 @@ export function getScreenshotMode() {
25
51
  // Default to 'on_request' for better performance (best practice)
26
52
  return 'on_request';
27
53
  }
54
+ // =============================================================================
55
+ // SMART DESIGN GUIDANCE — detect if user prompt has specific design direction
56
+ // =============================================================================
57
+ const COLOR_KEYWORDS = /(?:#[0-9a-f]{3,8}|rgb\(|hsl\(|\b(?:red|blue|green|purple|pink|gold|silver|orange|yellow|cyan|magenta|indigo|teal|coral|crimson|navy|maroon|turquoise|violet|amber|emerald|scarlet)\b)/i;
58
+ const STYLE_KEYWORDS = /\b(?:gradient|bokeh|geometric|circuit|wave|sunset|sunrise|minimal|modern|retro|vintage|elegant|playful|abstract|organic|neon|pastel|watercolor|grunge|futuristic|cosmic|dreamy|glossy)\b/i;
59
+ const LAYOUT_KEYWORDS = /\b(?:centered|left-aligned|right-aligned|grid|stacked|scattered|radial|circular|diagonal|symmetrical|horizontal|vertical)\b/i;
60
+ const ANIMATION_KEYWORDS = /\b(?:orbit|follow|pulse|bounce|fade|rotate|wobble|slide|wipe|reveal|parallax|wave|morph|float|glow|sparkle|burst|spiral)\b/i;
61
+ /**
62
+ * Detect if a prompt has enough design specificity to skip guidance injection.
63
+ * Returns true if the prompt is design-specific (skip guidance).
64
+ * Returns false if vague (inject guidance).
65
+ */
66
+ function detectDesignSpecificity(prompt) {
67
+ if (!prompt || prompt.trim().length === 0)
68
+ return false;
69
+ let specificityScore = 0;
70
+ // Color mentions: strong signal of specificity
71
+ if (COLOR_KEYWORDS.test(prompt))
72
+ specificityScore += 2;
73
+ // Style/mood keywords
74
+ if (STYLE_KEYWORDS.test(prompt))
75
+ specificityScore += 2;
76
+ // Layout direction
77
+ if (LAYOUT_KEYWORDS.test(prompt))
78
+ specificityScore += 1;
79
+ // Animation specifics
80
+ if (ANIMATION_KEYWORDS.test(prompt))
81
+ specificityScore += 1;
82
+ // Word count: detailed prompts tend to be longer
83
+ const wordCount = prompt.trim().split(/\s+/).length;
84
+ if (wordCount >= 20)
85
+ specificityScore += 1;
86
+ if (wordCount >= 35)
87
+ specificityScore += 1;
88
+ // Threshold: 3+ = specific enough, skip guidance
89
+ return specificityScore >= 3;
90
+ }
91
+ // =============================================================================
92
+ // EXPORT FILE SAVE HELPERS
93
+ // =============================================================================
94
+ function getExportDir() {
95
+ return process.env.PINEPAPER_EXPORT_DIR || join(tmpdir(), 'pinepaper-exports');
96
+ }
97
+ const ALWAYS_SAVE_FORMATS = new Set(['mp4', 'webm', 'gif', 'pdf']);
98
+ const SAVE_THRESHOLD_BYTES = 500_000; // ~500KB base64 ≈ 375KB decoded
99
+ function getFileExtension(format) {
100
+ const extMap = { mp4: 'mp4', webm: 'webm', gif: 'gif', pdf: 'pdf', png: 'png', svg: 'svg' };
101
+ return extMap[format] || format;
102
+ }
103
+ async function saveExportToFile(data, format, platform) {
104
+ const exportDir = getExportDir();
105
+ await mkdir(exportDir, { recursive: true });
106
+ const ext = getFileExtension(format);
107
+ const timestamp = Date.now();
108
+ const fileName = `pinepaper_${platform}_${timestamp}.${ext}`;
109
+ const filePath = join(exportDir, fileName);
110
+ if (data.startsWith('data:')) {
111
+ const base64Data = data.split(',')[1];
112
+ const buffer = Buffer.from(base64Data, 'base64');
113
+ await writeFile(filePath, buffer);
114
+ return { filePath, fileSize: buffer.length };
115
+ }
116
+ else {
117
+ // Plain text (SVG)
118
+ await writeFile(filePath, data, 'utf-8');
119
+ return { filePath, fileSize: Buffer.byteLength(data, 'utf-8') };
120
+ }
121
+ }
28
122
  /**
29
123
  * Get execution mode from environment variable.
30
124
  * Defaults to 'puppeteer' for live execution.
@@ -153,7 +247,32 @@ async function executeOrGenerate(code, description, options, toolName) {
153
247
  success: true,
154
248
  metadata: { executionMode: 'code' },
155
249
  });
156
- return successResult(code, `${description}\n\n📋 **Code Generation Mode**: Copy the code above and paste it into the PinePaper Studio console to execute.`);
250
+ return {
251
+ content: [
252
+ {
253
+ type: 'text',
254
+ text: `✅ **Code Generated Successfully**
255
+
256
+ **Tool:** ${toolName}
257
+ **Action:** ${description}
258
+
259
+ ## Generated Code
260
+
261
+ \`\`\`javascript
262
+ ${code}
263
+ \`\`\`
264
+
265
+ ## How to Execute
266
+
267
+ 1. Open **PinePaper Studio**: https://pinepaper.studio/editor?agent=1&mode=agent
268
+ 2. Click the **Code Console** button (</> icon) in the toolbar
269
+ 3. Paste the code above into the console
270
+ 4. Click **Run** to execute
271
+
272
+ The code is ready to use. Each subsequent tool call will also generate code you can paste.`,
273
+ },
274
+ ],
275
+ };
157
276
  }
158
277
  if (!executeInBrowser) {
159
278
  const totalDuration = tracker.endTimer(`${timerId}_total`);
@@ -167,17 +286,72 @@ async function executeOrGenerate(code, description, options, toolName) {
167
286
  return successResult(code, description);
168
287
  }
169
288
  const controller = browserController || getBrowserController();
289
+ // Auto-connect in agent mode if not connected
170
290
  if (!controller.connected) {
171
- const totalDuration = tracker.endTimer(`${timerId}_total`);
172
- tracker.recordMetric({
173
- toolName,
174
- phase: 'total',
175
- duration: totalDuration,
176
- timestamp: Date.now(),
177
- success: true,
178
- metadata: { browserConnected: false },
291
+ console.error('[PinePaper] Auto-connecting browser in agent mode...');
292
+ try {
293
+ await controller.connect(); // Will use connectAgent() due to enforced agentMode
294
+ console.error('[PinePaper] Browser auto-connected successfully');
295
+ }
296
+ catch (connectError) {
297
+ const totalDuration = tracker.endTimer(`${timerId}_total`);
298
+ tracker.recordMetric({
299
+ toolName,
300
+ phase: 'total',
301
+ duration: totalDuration,
302
+ timestamp: Date.now(),
303
+ success: false,
304
+ metadata: { browserConnected: false, autoConnectFailed: true, fallbackToCode: true },
305
+ });
306
+ // FALLBACK: Return code for manual paste instead of failing
307
+ const errorMessage = connectError instanceof Error ? connectError.message : 'Unknown error';
308
+ console.error(`[PinePaper] Browser connection failed: ${errorMessage}. Falling back to code-only mode.`);
309
+ return {
310
+ content: [
311
+ {
312
+ type: 'text',
313
+ text: `⚠️ **Browser Connection Failed** - Falling back to code mode
314
+
315
+ **Error:** ${errorMessage}
316
+
317
+ **Don't worry!** You can still use PinePaper by copying the code below and pasting it into the PinePaper Code Console.
318
+
319
+ ## How to use the code manually:
320
+
321
+ 1. Open **PinePaper Studio**: https://pinepaper.studio/editor?agent=1&mode=agent
322
+ 2. Click the **Code Console** button (</> icon) in the toolbar
323
+ 3. Paste the code below into the console
324
+ 4. Click **Run** to execute
325
+
326
+ ---
327
+
328
+ ## Generated Code for: ${toolName}
329
+
330
+ \`\`\`javascript
331
+ ${code}
332
+ \`\`\`
333
+
334
+ ---
335
+
336
+ **${description}**
337
+
338
+ 💡 **Tip:** If browser connection keeps failing, check:
339
+ - Is Chrome/Chromium installed?
340
+ - Are there firewall restrictions?
341
+ - Try setting \`headless: false\` in browser_connect to see the browser window`,
342
+ },
343
+ ],
344
+ };
345
+ }
346
+ }
347
+ // Auto-start agent session if not already active (enforced agent mode)
348
+ const sessionManager = getSessionManager();
349
+ if (!sessionManager.hasActiveJob()) {
350
+ sessionManager.startJob({
351
+ name: 'auto_session',
352
+ screenshotPolicy: 'on_complete',
179
353
  });
180
- return successResult(code, `${description}\n\n⚠️ Browser not connected. Call pinepaper_browser_connect first to execute code live.`);
354
+ console.error('[PinePaper] Auto-started agent session for enforced agent mode');
181
355
  }
182
356
  // Track browser execution time
183
357
  tracker.startTimer(`${timerId}_browser_execution`);
@@ -277,10 +451,43 @@ async function executeToolWithTracking(toolName, args, options, validator, codeG
277
451
  // =============================================================================
278
452
  // TOOL HANDLERS
279
453
  // =============================================================================
454
+ /**
455
+ * Measure the approximate byte size of a CallToolResult payload.
456
+ * Text content is measured directly; image content estimates base64 overhead.
457
+ */
458
+ function measureResponseBytes(result) {
459
+ let bytes = 0;
460
+ if (result.content) {
461
+ for (const item of result.content) {
462
+ if (item.type === 'text') {
463
+ bytes += Buffer.byteLength(item.text, 'utf-8');
464
+ }
465
+ else if (item.type === 'image') {
466
+ // Base64 image data
467
+ bytes += (item.data?.length ?? 0);
468
+ }
469
+ }
470
+ }
471
+ return bytes;
472
+ }
280
473
  export async function handleToolCall(toolName, args, options = {}) {
281
- const { i18n } = options;
282
474
  const tracker = getPerformanceTracker();
283
475
  const baseTimerId = `${toolName}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
476
+ const result = await handleToolCallInner(toolName, args, options, tracker, baseTimerId);
477
+ // Record response payload size for token estimation
478
+ const responseBytes = measureResponseBytes(result);
479
+ tracker.recordMetric({
480
+ toolName,
481
+ phase: 'response_size',
482
+ duration: 0,
483
+ timestamp: Date.now(),
484
+ success: !result.isError,
485
+ responseBytes,
486
+ });
487
+ return result;
488
+ }
489
+ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId) {
490
+ const { i18n } = options;
284
491
  try {
285
492
  switch (toolName) {
286
493
  // -----------------------------------------------------------------------
@@ -318,6 +525,14 @@ export async function handleToolCall(toolName, args, options = {}) {
318
525
  }
319
526
  case 'pinepaper_modify_item': {
320
527
  const input = ModifyItemInputSchema.parse(args);
528
+ // Guard: reject animation properties — they don't work via modify
529
+ const animProps = ['animationType', 'animationSpeed', 'keyframes', 'animation'];
530
+ const passedAnimProps = animProps.filter(p => p in (input.properties || {}));
531
+ if (passedAnimProps.length > 0) {
532
+ return errorResult('INVALID_PROPERTIES', `Animation properties (${passedAnimProps.join(', ')}) cannot be set via pinepaper_modify_item — they will be silently ignored. ` +
533
+ `Use pinepaper_animate for loop animations (pulse, rotate, bounce, fade, wobble, slide) ` +
534
+ `or pinepaper_keyframe_animate for timed/sequenced animations with precise control.`);
535
+ }
321
536
  const code = codeGenerator.generateModifyItem(input);
322
537
  const description = getLocalizedSuccessMessage(i18n, 'itemModified', {
323
538
  itemId: input.itemId,
@@ -386,7 +601,7 @@ export async function handleToolCall(toolName, args, options = {}) {
386
601
  const description = getLocalizedSuccessMessage(i18n, 'relationAdded', {
387
602
  relationType: input.relationType,
388
603
  sourceId: input.sourceId,
389
- targetId: input.targetId,
604
+ targetId: input.targetId || 'self',
390
605
  });
391
606
  return executeOrGenerate(code, description, options, 'pinepaper_add_relation');
392
607
  }
@@ -404,6 +619,16 @@ export async function handleToolCall(toolName, args, options = {}) {
404
619
  const code = codeGenerator.generateQueryRelations(input.itemId, input.relationType, input.direction);
405
620
  return executeOrGenerate(code, `Queries ${input.direction || 'all'} relations for ${input.itemId}`, options, 'pinepaper_query_relations');
406
621
  }
622
+ case 'pinepaper_register_custom_relation': {
623
+ const input = RegisterCustomRelationInputSchema.parse(args);
624
+ const code = codeGenerator.generateRegisterCustomRelation(input);
625
+ return executeOrGenerate(code, `Registers custom relation type: ${input.name}`, options, 'pinepaper_register_custom_relation');
626
+ }
627
+ case 'pinepaper_execute_custom_code': {
628
+ const input = ExecuteCustomCodeInputSchema.parse(args);
629
+ const code = codeGenerator.generateExecuteCustomCode(input);
630
+ return executeOrGenerate(code, input.description || 'Executes custom JavaScript code', options, 'pinepaper_execute_custom_code');
631
+ }
407
632
  // -----------------------------------------------------------------------
408
633
  // ANIMATION TOOLS
409
634
  // -----------------------------------------------------------------------
@@ -454,6 +679,204 @@ export async function handleToolCall(toolName, args, options = {}) {
454
679
  return executeOrGenerate(code, description, options, 'pinepaper_apply_effect');
455
680
  }
456
681
  // -----------------------------------------------------------------------
682
+ // MASK TOOLS
683
+ // -----------------------------------------------------------------------
684
+ case 'pinepaper_apply_animated_mask': {
685
+ const input = ApplyAnimatedMaskInputSchema.parse(args);
686
+ const code = codeGenerator.generateApplyAnimatedMask(input);
687
+ const mode = input.preset && input.keyframes ? 'hybrid' : input.preset ? 'preset' : 'custom';
688
+ return executeOrGenerate(code, `Applies ${mode} animated mask to ${input.itemId}`, options, 'pinepaper_apply_animated_mask');
689
+ }
690
+ case 'pinepaper_apply_custom_mask': {
691
+ const input = ApplyCustomMaskInputSchema.parse(args);
692
+ const code = codeGenerator.generateApplyCustomMask(input);
693
+ return executeOrGenerate(code, `Applies custom ${input.maskType} mask with ${input.keyframes.length} keyframes to ${input.itemId}`, options, 'pinepaper_apply_custom_mask');
694
+ }
695
+ case 'pinepaper_remove_mask': {
696
+ const input = RemoveMaskInputSchema.parse(args);
697
+ const code = codeGenerator.generateRemoveMask(input);
698
+ return executeOrGenerate(code, `Removes mask from ${input.itemId}`, options, 'pinepaper_remove_mask');
699
+ }
700
+ case 'pinepaper_get_animatable_properties': {
701
+ const code = codeGenerator.generateGetAnimatableProperties();
702
+ return executeOrGenerate(code, 'Gets animatable properties for mask types', options, 'pinepaper_get_animatable_properties');
703
+ }
704
+ case 'pinepaper_get_available_easings': {
705
+ const code = codeGenerator.generateGetAvailableEasings();
706
+ return executeOrGenerate(code, 'Gets available easing functions', options, 'pinepaper_get_available_easings');
707
+ }
708
+ case 'pinepaper_get_mask_types': {
709
+ const code = codeGenerator.generateGetMaskTypes();
710
+ return executeOrGenerate(code, 'Gets available mask types', options, 'pinepaper_get_mask_types');
711
+ }
712
+ case 'pinepaper_get_mask_animations': {
713
+ const code = codeGenerator.generateGetMaskAnimations();
714
+ return executeOrGenerate(code, 'Gets available mask animation presets', options, 'pinepaper_get_mask_animations');
715
+ }
716
+ // -----------------------------------------------------------------------
717
+ // CAMERA TOOLS
718
+ // -----------------------------------------------------------------------
719
+ case 'pinepaper_camera_animate': {
720
+ const keyframes = args.keyframes;
721
+ const duration = args.duration;
722
+ const loop = args.loop ?? false;
723
+ const delay = args.delay ?? 0;
724
+ const keyframesStr = JSON.stringify(keyframes);
725
+ const code = `app.camera && app.camera.animate ? app.camera.animate(${keyframesStr}, ${duration}, ${loop}, ${delay}) : app.addRelation('camera', 'camera', 'camera_animates', { keyframes: ${keyframesStr}, duration: ${duration}, loop: ${loop}, delay: ${delay} });`;
726
+ return executeOrGenerate(code, `Animates camera with ${keyframes.length} keyframes over ${duration}s`, options, 'pinepaper_camera_animate');
727
+ }
728
+ case 'pinepaper_camera_zoom': {
729
+ const direction = args.direction;
730
+ const level = args.level ?? (direction === 'in' ? 2 : 0.5);
731
+ const duration = args.duration ?? 0.5;
732
+ const method = direction === 'in' ? 'zoomIn' : 'zoomOut';
733
+ const code = `app.camera && app.camera.${method} ? app.camera.${method}(${level}, ${duration}) : null;`;
734
+ return executeOrGenerate(code, `Camera zoom ${direction} to ${level}x`, options, 'pinepaper_camera_zoom');
735
+ }
736
+ case 'pinepaper_camera_pan': {
737
+ const direction = args.direction;
738
+ const amount = args.amount ?? 100;
739
+ const x = args.x;
740
+ const y = args.y;
741
+ const duration = args.duration ?? 0.5;
742
+ let code;
743
+ let description;
744
+ if (x !== undefined && y !== undefined) {
745
+ code = `app.camera && app.camera.panTo ? app.camera.panTo(${x}, ${y}, ${duration}) : null;`;
746
+ description = `Camera pan to (${x}, ${y})`;
747
+ }
748
+ else if (direction) {
749
+ const methodMap = { left: 'panLeft', right: 'panRight', up: 'panUp', down: 'panDown' };
750
+ const method = methodMap[direction];
751
+ code = `app.camera && app.camera.${method} ? app.camera.${method}(${amount}, ${duration}) : null;`;
752
+ description = `Camera pan ${direction} by ${amount}px`;
753
+ }
754
+ else {
755
+ code = `// No direction or coordinates specified`;
756
+ description = 'Camera pan (no parameters)';
757
+ }
758
+ return executeOrGenerate(code, description, options, 'pinepaper_camera_pan');
759
+ }
760
+ case 'pinepaper_camera_move_to': {
761
+ const x = args.x;
762
+ const y = args.y;
763
+ const zoom = args.zoom;
764
+ const duration = args.duration ?? 0.5;
765
+ const code = `app.camera && app.camera.moveTo ? app.camera.moveTo(${x}, ${y}, ${zoom}, ${duration}) : null;`;
766
+ return executeOrGenerate(code, `Camera move to (${x}, ${y}) at ${zoom}x zoom`, options, 'pinepaper_camera_move_to');
767
+ }
768
+ case 'pinepaper_camera_reset': {
769
+ const duration = args.duration ?? 0.5;
770
+ const code = `app.camera && app.camera.reset ? app.camera.reset(${duration}) : null;`;
771
+ return executeOrGenerate(code, 'Reset camera to default state', options, 'pinepaper_camera_reset');
772
+ }
773
+ case 'pinepaper_camera_stop': {
774
+ const code = `app.camera && app.camera.stop ? app.camera.stop() : null;`;
775
+ return executeOrGenerate(code, 'Stop camera animation', options, 'pinepaper_camera_stop');
776
+ }
777
+ case 'pinepaper_camera_state': {
778
+ const code = `app.camera && app.camera.getState ? app.camera.getState() : { zoom: 1, center: [400, 300], isAnimating: false };`;
779
+ return executeOrGenerate(code, 'Get current camera state', options, 'pinepaper_camera_state');
780
+ }
781
+ // -----------------------------------------------------------------------
782
+ // FONT TOOLS
783
+ // -----------------------------------------------------------------------
784
+ case 'pinepaper_font_show_studio': {
785
+ const code = `app.fontStudio && app.fontStudio.show ? app.fontStudio.show() : null;`;
786
+ return executeOrGenerate(code, 'Opens Font Studio UI', options, 'pinepaper_font_show_studio');
787
+ }
788
+ case 'pinepaper_font_set_name': {
789
+ const { name } = args;
790
+ const code = `app.fontStudio.setName(${JSON.stringify(name)});`;
791
+ return executeOrGenerate(code, `Set font name to "${name}"`, options, 'pinepaper_font_set_name');
792
+ }
793
+ case 'pinepaper_font_get_required_chars': {
794
+ const { set } = args;
795
+ const setArg = set ? JSON.stringify(set) : '"minimum"';
796
+ const code = `app.fontStudio.getRequiredChars(${setArg});`;
797
+ return executeOrGenerate(code, `Get required characters (${set || 'minimum'} set)`, options, 'pinepaper_font_get_required_chars');
798
+ }
799
+ case 'pinepaper_font_get_status': {
800
+ const code = `app.fontStudio.getStatus();`;
801
+ return executeOrGenerate(code, 'Get font completion status', options, 'pinepaper_font_get_status');
802
+ }
803
+ case 'pinepaper_font_create_glyph': {
804
+ const { character, pathId } = args;
805
+ const code = `app.fontStudio.createGlyph(${JSON.stringify(character)}, ${JSON.stringify(pathId)});`;
806
+ return executeOrGenerate(code, `Create glyph for "${character}" from path ${pathId}`, options, 'pinepaper_font_create_glyph');
807
+ }
808
+ case 'pinepaper_font_create_space': {
809
+ const { width } = args;
810
+ const code = width !== undefined
811
+ ? `app.fontStudio.createSpace(${width});`
812
+ : `app.fontStudio.createSpace();`;
813
+ return executeOrGenerate(code, `Create space glyph${width ? ` (width: ${width})` : ''}`, options, 'pinepaper_font_create_space');
814
+ }
815
+ case 'pinepaper_font_remove_glyph': {
816
+ const { character } = args;
817
+ const code = `app.fontStudio.removeGlyph(${JSON.stringify(character)});`;
818
+ return executeOrGenerate(code, `Remove glyph for "${character}"`, options, 'pinepaper_font_remove_glyph');
819
+ }
820
+ case 'pinepaper_font_set_metrics': {
821
+ const metrics = args;
822
+ const code = `app.fontStudio.setMetrics(${JSON.stringify(metrics)});`;
823
+ return executeOrGenerate(code, 'Set font metrics', options, 'pinepaper_font_set_metrics');
824
+ }
825
+ case 'pinepaper_font_export': {
826
+ const { download } = args;
827
+ const code = download === false
828
+ ? `app.fontStudio.export({ download: false });`
829
+ : `app.fontStudio.export();`;
830
+ return executeOrGenerate(code, 'Export font as OTF', options, 'pinepaper_font_export');
831
+ }
832
+ case 'pinepaper_font_load_into_document': {
833
+ const code = `app.fontStudio.loadIntoDocument();`;
834
+ return executeOrGenerate(code, 'Load font into document', options, 'pinepaper_font_load_into_document');
835
+ }
836
+ case 'pinepaper_font_export_data': {
837
+ const { download } = args;
838
+ const code = download === false
839
+ ? `app.fontStudio.exportData({ download: false });`
840
+ : `app.fontStudio.exportData();`;
841
+ return executeOrGenerate(code, 'Export font data as JSON', options, 'pinepaper_font_export_data');
842
+ }
843
+ case 'pinepaper_font_import_data': {
844
+ const { data } = args;
845
+ const code = `app.fontStudio.importData(${JSON.stringify(data)});`;
846
+ return executeOrGenerate(code, 'Import font data from JSON', options, 'pinepaper_font_import_data');
847
+ }
848
+ case 'pinepaper_font_clear': {
849
+ const code = `app.fontStudio.clear();`;
850
+ return executeOrGenerate(code, 'Clear all glyphs and reset font', options, 'pinepaper_font_clear');
851
+ }
852
+ case 'pinepaper_font_remove_overlap': {
853
+ const { pathId } = args;
854
+ const code = `app.fontStudio.removeOverlap(${JSON.stringify(pathId)});`;
855
+ return executeOrGenerate(code, `Remove overlaps from path ${pathId}`, options, 'pinepaper_font_remove_overlap');
856
+ }
857
+ case 'pinepaper_font_correct_direction': {
858
+ const { pathId } = args;
859
+ const code = `app.fontStudio.correctDirection(${JSON.stringify(pathId)});`;
860
+ return executeOrGenerate(code, `Correct path direction for ${pathId}`, options, 'pinepaper_font_correct_direction');
861
+ }
862
+ case 'pinepaper_font_cleanup_path': {
863
+ const { pathId, removeOverlap, correctDirection, smooth, smoothTolerance } = args;
864
+ const optsObj = {};
865
+ if (removeOverlap !== undefined)
866
+ optsObj.removeOverlap = removeOverlap;
867
+ if (correctDirection !== undefined)
868
+ optsObj.correctDirection = correctDirection;
869
+ if (smooth !== undefined)
870
+ optsObj.smooth = smooth;
871
+ if (smoothTolerance !== undefined)
872
+ optsObj.smoothTolerance = smoothTolerance;
873
+ const hasOpts = Object.keys(optsObj).length > 0;
874
+ const code = hasOpts
875
+ ? `app.fontStudio.cleanupPath(${JSON.stringify(pathId)}, ${JSON.stringify(optsObj)});`
876
+ : `app.fontStudio.cleanupPath(${JSON.stringify(pathId)});`;
877
+ return executeOrGenerate(code, `Cleanup path ${pathId}`, options, 'pinepaper_font_cleanup_path');
878
+ }
879
+ // -----------------------------------------------------------------------
457
880
  // QUERY TOOLS
458
881
  // -----------------------------------------------------------------------
459
882
  case 'pinepaper_get_items': {
@@ -565,19 +988,24 @@ You can now start creating new items on a clean canvas.`,
565
988
  });
566
989
  return executeOrGenerate(code, description, options, 'pinepaper_export_training_data');
567
990
  }
991
+ case 'pinepaper_export_scene': {
992
+ const code = codeGenerator.generateExportScene();
993
+ return executeOrGenerate(code, 'Exports complete scene state', options, 'pinepaper_export_scene');
994
+ }
568
995
  // -----------------------------------------------------------------------
569
996
  // BROWSER CONTROL TOOLS
570
997
  // -----------------------------------------------------------------------
571
998
  case 'pinepaper_browser_connect': {
572
999
  const url = args.url || 'https://pinepaper.studio';
573
- const headless = args.headless ?? false;
1000
+ // Default to headless: true for enforced agent mode
1001
+ const headless = args.headless ?? true;
574
1002
  const controller = getBrowserController({ studioUrl: url, headless });
575
1003
  if (controller.connected) {
576
1004
  return {
577
1005
  content: [
578
1006
  {
579
1007
  type: 'text',
580
- text: `Already connected to PinePaper Studio at ${controller.studioUrl}`,
1008
+ text: `Already connected to PinePaper Studio at ${controller.actualUrl}. Do NOT call this tool again — proceed directly with pinepaper_agent_start_job.`,
581
1009
  },
582
1010
  ],
583
1011
  };
@@ -593,8 +1021,9 @@ You can now start creating new items on a clean canvas.`,
593
1021
  const content = [
594
1022
  {
595
1023
  type: 'text',
596
- text: `✅ Connected to PinePaper Studio at ${url}
1024
+ text: `✅ Connected to PinePaper Studio at ${controller.actualUrl}
597
1025
 
1026
+ **Agent mode enabled** (agent=1&mode=agent parameters added automatically)
598
1027
  **Current canvas size: ${canvasSize.width}x${canvasSize.height}**
599
1028
 
600
1029
  ⚠️ IMPORTANT NOTES:
@@ -615,7 +1044,39 @@ Browser is ready. You can now use other pinepaper tools to create and animate gr
615
1044
  return { content };
616
1045
  }
617
1046
  catch (error) {
618
- return errorResult('BROWSER_CONNECTION_ERROR', error instanceof Error ? error.message : 'Failed to connect to browser', { url });
1047
+ const errorMessage = error instanceof Error ? error.message : 'Failed to connect to browser';
1048
+ // Provide helpful fallback instructions instead of just failing
1049
+ return {
1050
+ content: [
1051
+ {
1052
+ type: 'text',
1053
+ text: `⚠️ **Browser Connection Failed**
1054
+
1055
+ **Error:** ${errorMessage}
1056
+
1057
+ ## Don't worry! You can still use PinePaper manually:
1058
+
1059
+ ### Option 1: Manual Browser Mode
1060
+ 1. Open **PinePaper Studio** in your browser: https://pinepaper.studio/editor?agent=1&mode=agent
1061
+ 2. Continue using PinePaper tools - the MCP server will generate code for each action
1062
+ 3. Copy the generated code and paste it into the **Code Console** (</> icon in toolbar)
1063
+
1064
+ ### Option 2: Troubleshoot Browser Connection
1065
+ - **Check Chrome/Chromium**: Puppeteer requires Chrome or Chromium installed
1066
+ - **Try headless: false**: Call \`pinepaper_browser_connect\` with \`headless: false\` to see the browser window
1067
+ - **Check permissions**: Ensure the process has permission to launch browsers
1068
+ - **Firewall**: Check if firewall is blocking browser launch
1069
+
1070
+ ### What happens now?
1071
+ All PinePaper tools will work in **code-only mode**:
1072
+ - Each tool generates JavaScript code
1073
+ - You copy the code and paste it in PinePaper's Code Console
1074
+ - The code executes and creates your graphics
1075
+
1076
+ **The MCP server is fully functional** - just without automatic browser control.`,
1077
+ },
1078
+ ],
1079
+ };
619
1080
  }
620
1081
  }
621
1082
  case 'pinepaper_browser_disconnect': {
@@ -664,7 +1125,8 @@ Browser is ready. You can now use other pinepaper tools to create and animate gr
664
1125
  type: 'text',
665
1126
  text: JSON.stringify({
666
1127
  connected: controller.connected,
667
- studioUrl: controller.studioUrl,
1128
+ studioUrl: controller.actualUrl,
1129
+ agentMode: controller.agentMode,
668
1130
  }, null, 2),
669
1131
  },
670
1132
  ],
@@ -704,6 +1166,286 @@ Browser is ready. You can now use other pinepaper tools to create and animate gr
704
1166
  };
705
1167
  }
706
1168
  // -----------------------------------------------------------------------
1169
+ // DIAGNOSTIC REPORT
1170
+ // -----------------------------------------------------------------------
1171
+ case 'pinepaper_diagnostic_report': {
1172
+ const input = DiagnosticReportInputSchema.parse(args);
1173
+ const includeMetrics = input.includeMetrics !== false;
1174
+ const includeCanvas = input.includeCanvas !== false;
1175
+ const metricsLimit = input.metricsLimit ?? 100;
1176
+ // --- Server info ---
1177
+ const { detectToolkitFromEnvironment: detectToolkit, detectVerbosityFromEnvironment: detectVerbosity } = await import('./toolkits.js');
1178
+ const serverSection = {
1179
+ version: '1.5.1',
1180
+ toolkit: detectToolkit(),
1181
+ verbosity: detectVerbosity(),
1182
+ locale: process.env.PINEPAPER_LOCALE || 'en',
1183
+ executionMode: options.executionMode || getExecutionMode(),
1184
+ screenshotMode: options.screenshotMode || getScreenshotMode(),
1185
+ };
1186
+ // --- Browser state ---
1187
+ let browserSection = { connected: false };
1188
+ try {
1189
+ const ctrl = options.browserController || getBrowserController();
1190
+ browserSection = {
1191
+ connected: ctrl.connected,
1192
+ agentMode: ctrl.agentMode,
1193
+ studioUrl: ctrl.studioUrl,
1194
+ };
1195
+ }
1196
+ catch { /* browser not initialized */ }
1197
+ // --- Session state ---
1198
+ let sessionSection = {};
1199
+ try {
1200
+ const session = getSessionManager();
1201
+ sessionSection = session.getStats();
1202
+ }
1203
+ catch { /* session not initialized */ }
1204
+ // --- Canvas state ---
1205
+ let canvasSection = null;
1206
+ if (includeCanvas) {
1207
+ try {
1208
+ const ctrl = options.browserController || getBrowserController();
1209
+ if (ctrl.connected) {
1210
+ canvasSection = await captureCanvasState(ctrl);
1211
+ }
1212
+ }
1213
+ catch { /* canvas capture failed */ }
1214
+ }
1215
+ // --- Performance metrics ---
1216
+ let metricsSection = null;
1217
+ if (includeMetrics) {
1218
+ try {
1219
+ const perfTracker = getPerformanceTracker();
1220
+ const recentMetrics = perfTracker.exportMetrics('json', { limit: metricsLimit });
1221
+ const stats = perfTracker.getStats();
1222
+ metricsSection = { entries: recentMetrics, stats };
1223
+ }
1224
+ catch { /* metrics not available */ }
1225
+ }
1226
+ // --- Tool definitions overhead (fixed cost per API turn) ---
1227
+ let toolDefsSection = null;
1228
+ try {
1229
+ const { PINEPAPER_TOOLS, getToolsForVerbosity } = await import('./definitions.js');
1230
+ const { getToolsForToolkit } = await import('./toolkits.js');
1231
+ const toolkit = detectToolkit();
1232
+ const verbosity = detectVerbosity();
1233
+ // Measure what actually gets sent to the LLM
1234
+ const activeTools = getToolsForToolkit(getToolsForVerbosity(verbosity), toolkit);
1235
+ const activeJson = JSON.stringify(activeTools);
1236
+ const activeBytes = Buffer.byteLength(activeJson, 'utf-8');
1237
+ const activeTokens = Math.ceil(activeBytes / 4);
1238
+ // Also measure full set for comparison
1239
+ const fullJson = JSON.stringify(PINEPAPER_TOOLS);
1240
+ const fullBytes = Buffer.byteLength(fullJson, 'utf-8');
1241
+ const fullTokens = Math.ceil(fullBytes / 4);
1242
+ // Measure other profiles for comparison recommendations
1243
+ const agentCompactTools = getToolsForToolkit(getToolsForVerbosity('compact'), 'agent');
1244
+ const agentCompactTokens = Math.ceil(Buffer.byteLength(JSON.stringify(agentCompactTools), 'utf-8') / 4);
1245
+ const agentMinimalTools = getToolsForToolkit(getToolsForVerbosity('minimal'), 'agent');
1246
+ const agentMinimalTokens = Math.ceil(Buffer.byteLength(JSON.stringify(agentMinimalTools), 'utf-8') / 4);
1247
+ // Per-tool breakdown of active set (top 10 heaviest)
1248
+ const perToolDef = activeTools.map((t) => ({
1249
+ tool: t.name,
1250
+ bytes: Buffer.byteLength(JSON.stringify(t), 'utf-8'),
1251
+ descBytes: Buffer.byteLength(t.description || '', 'utf-8'),
1252
+ })).sort((a, b) => b.bytes - a.bytes);
1253
+ const totalDescBytes = perToolDef.reduce((s, t) => s + t.descBytes, 0);
1254
+ toolDefsSection = {
1255
+ toolkit,
1256
+ verbosity,
1257
+ activeToolCount: activeTools.length,
1258
+ activeBytes,
1259
+ activeTokens,
1260
+ fullToolCount: PINEPAPER_TOOLS.length,
1261
+ fullBytes,
1262
+ fullTokens,
1263
+ descriptionBytes: totalDescBytes,
1264
+ descriptionPct: ((totalDescBytes / activeBytes) * 100).toFixed(1) + '%',
1265
+ top10: perToolDef.slice(0, 10).map((t) => ({
1266
+ tool: t.tool,
1267
+ bytes: t.bytes,
1268
+ tokens: Math.ceil(t.bytes / 4),
1269
+ })),
1270
+ profiles: {
1271
+ 'full+verbose': fullTokens,
1272
+ 'agent+compact': agentCompactTokens,
1273
+ 'agent+minimal': agentMinimalTokens,
1274
+ },
1275
+ recommendations: [],
1276
+ };
1277
+ // Add actionable recommendations
1278
+ const recs = toolDefsSection.recommendations;
1279
+ if (toolkit === 'full') {
1280
+ recs.push(`Switch to PINEPAPER_TOOLKIT=agent to reduce to ~${agentCompactTokens.toLocaleString()} tokens/turn (agent+compact)`);
1281
+ }
1282
+ if (verbosity === 'verbose') {
1283
+ recs.push(`Set PINEPAPER_VERBOSITY=compact to reduce tool definitions overhead`);
1284
+ }
1285
+ if (verbosity !== 'minimal') {
1286
+ recs.push(`Set PINEPAPER_VERBOSITY=minimal for maximum savings (~${agentMinimalTokens.toLocaleString()} tokens with agent toolkit) — use pinepaper_tool_guide for on-demand docs`);
1287
+ }
1288
+ if (activeTokens > 20000) {
1289
+ recs.push(`Tool definitions alone consume ~${activeTokens.toLocaleString()} tokens per API turn — this is a major context budget item`);
1290
+ }
1291
+ }
1292
+ catch { /* definitions not available */ }
1293
+ // --- Token usage estimate (response payloads) ---
1294
+ let tokenSection = null;
1295
+ try {
1296
+ const perfTracker = getPerformanceTracker();
1297
+ const responseSizeMetrics = perfTracker.getMetrics({ phase: 'response_size' });
1298
+ // Aggregate per-tool response sizes
1299
+ const perTool = new Map();
1300
+ let totalResponseBytes = 0;
1301
+ for (const m of responseSizeMetrics) {
1302
+ const bytes = m.responseBytes ?? 0;
1303
+ totalResponseBytes += bytes;
1304
+ const entry = perTool.get(m.toolName) ?? { calls: 0, totalBytes: 0 };
1305
+ entry.calls++;
1306
+ entry.totalBytes += bytes;
1307
+ perTool.set(m.toolName, entry);
1308
+ }
1309
+ // ~4 chars per token is a common estimate for English text
1310
+ const estimatedTokens = Math.ceil(totalResponseBytes / 4);
1311
+ // Sort by totalBytes descending
1312
+ const perToolSorted = [...perTool.entries()]
1313
+ .sort((a, b) => b[1].totalBytes - a[1].totalBytes)
1314
+ .map(([tool, data]) => ({
1315
+ tool,
1316
+ calls: data.calls,
1317
+ totalBytes: data.totalBytes,
1318
+ estimatedTokens: Math.ceil(data.totalBytes / 4),
1319
+ avgBytesPerCall: Math.round(data.totalBytes / data.calls),
1320
+ }));
1321
+ tokenSection = {
1322
+ totalToolCalls: responseSizeMetrics.length,
1323
+ totalResponseBytes,
1324
+ estimatedResponseTokens: estimatedTokens,
1325
+ perTool: perToolSorted,
1326
+ };
1327
+ }
1328
+ catch { /* metrics not available */ }
1329
+ // --- Assemble report ---
1330
+ const report = {
1331
+ timestamp: new Date().toISOString(),
1332
+ server: serverSection,
1333
+ browser: browserSection,
1334
+ session: sessionSection,
1335
+ canvas: canvasSection,
1336
+ metrics: metricsSection,
1337
+ toolDefinitions: toolDefsSection,
1338
+ tokenUsage: tokenSection,
1339
+ };
1340
+ // --- Save to disk ---
1341
+ const exportDir = getExportDir();
1342
+ await mkdir(exportDir, { recursive: true });
1343
+ const fileName = `pinepaper_diagnostic_${Date.now()}.json`;
1344
+ const filePath = join(exportDir, fileName);
1345
+ const jsonContent = JSON.stringify(report, null, 2);
1346
+ await writeFile(filePath, jsonContent, 'utf-8');
1347
+ // --- Build summary (token budget first — it's what matters most) ---
1348
+ const lines = [
1349
+ `Diagnostic report saved to: ${filePath}`,
1350
+ ``,
1351
+ ];
1352
+ // Token budget section — prominent at top
1353
+ if (toolDefsSection) {
1354
+ const td = toolDefsSection;
1355
+ lines.push(`TOKEN BUDGET`);
1356
+ lines.push(` Tool definitions: ~${td.activeTokens.toLocaleString()} tokens/turn (${td.activeToolCount} tools, ${(td.activeBytes / 1024).toFixed(0)} KB)`);
1357
+ lines.push(` Descriptions are ${td.descriptionPct} of that overhead`);
1358
+ lines.push(` Config: toolkit=${td.toolkit}, verbosity=${td.verbosity}`);
1359
+ if (tokenSection) {
1360
+ const tk = tokenSection;
1361
+ lines.push(` Response payloads: ~${tk.estimatedResponseTokens.toLocaleString()} tokens across ${tk.totalToolCalls} tool calls (${(tk.totalResponseBytes / 1024).toFixed(1)} KB)`);
1362
+ const totalPerTurn = td.activeTokens + Math.ceil(tk.estimatedResponseTokens / Math.max(tk.totalToolCalls, 1));
1363
+ lines.push(` Estimated per-turn cost: ~${totalPerTurn.toLocaleString()} tokens (definitions + avg response)`);
1364
+ }
1365
+ // Top 3 heaviest tool definitions
1366
+ const topDefs = td.top10.slice(0, 3);
1367
+ if (topDefs.length > 0) {
1368
+ lines.push(` Heaviest definitions: ${topDefs.map(t => `${t.tool} (~${t.tokens.toLocaleString()}t)`).join(', ')}`);
1369
+ }
1370
+ // Top 3 heaviest response consumers
1371
+ if (tokenSection) {
1372
+ const tk = tokenSection;
1373
+ const topResp = tk.perTool.slice(0, 3);
1374
+ if (topResp.length > 0) {
1375
+ lines.push(` Heaviest responses: ${topResp.map(t => `${t.tool} (${t.calls}x, ~${t.estimatedTokens.toLocaleString()}t)`).join(', ')}`);
1376
+ }
1377
+ }
1378
+ if (td.recommendations.length > 0) {
1379
+ lines.push(``);
1380
+ lines.push(`RECOMMENDATIONS`);
1381
+ for (const rec of td.recommendations) {
1382
+ lines.push(` - ${rec}`);
1383
+ }
1384
+ }
1385
+ lines.push(``);
1386
+ }
1387
+ // Brief state summary
1388
+ lines.push(`Server: v${serverSection.version} | toolkit=${serverSection.toolkit} | mode=${serverSection.executionMode}`);
1389
+ lines.push(`Browser: ${browserSection.connected ? 'connected' : 'disconnected'}`);
1390
+ lines.push(`Session: ${sessionSection.activeJob ? `active job (${sessionSection.currentJobId})` : 'no active job'} | ${sessionSection.completedJobs ?? 0} completed | ${sessionSection.totalItemsCreated ?? 0} items total`);
1391
+ if (canvasSection) {
1392
+ const cs = canvasSection;
1393
+ lines.push(`Canvas: ${cs.itemCount ?? 0} items | ${JSON.stringify(cs.canvasSize ?? {})}`);
1394
+ }
1395
+ return {
1396
+ content: [{ type: 'text', text: lines.join('\n') }],
1397
+ };
1398
+ }
1399
+ // -----------------------------------------------------------------------
1400
+ // TOOL GUIDE (on-demand documentation)
1401
+ // -----------------------------------------------------------------------
1402
+ case 'pinepaper_tool_guide': {
1403
+ const input = ToolGuideInputSchema.parse(args);
1404
+ const { PINEPAPER_TOOLS, AI_AGENT_GUIDE } = await import('./definitions.js');
1405
+ const { TOOL_TAGS } = await import('./toolkits.js');
1406
+ // No args → full AI Agent Guide
1407
+ if (!input.tool && !input.category) {
1408
+ return {
1409
+ content: [{ type: 'text', text: AI_AGENT_GUIDE }],
1410
+ };
1411
+ }
1412
+ // Specific tool → full verbose description
1413
+ if (input.tool) {
1414
+ const tool = PINEPAPER_TOOLS.find((t) => t.name === input.tool);
1415
+ if (!tool) {
1416
+ return {
1417
+ content: [{ type: 'text', text: `Unknown tool: ${input.tool}. Use pinepaper_tool_guide with no args to see the AI Agent Guide.` }],
1418
+ isError: true,
1419
+ };
1420
+ }
1421
+ return {
1422
+ content: [{ type: 'text', text: `# ${tool.name}\n\n${tool.description}` }],
1423
+ };
1424
+ }
1425
+ // Category → all tools in that tag with descriptions
1426
+ if (input.category) {
1427
+ const toolNames = TOOL_TAGS[input.category];
1428
+ if (!toolNames) {
1429
+ const available = Object.keys(TOOL_TAGS).join(', ');
1430
+ return {
1431
+ content: [{ type: 'text', text: `Unknown category: ${input.category}. Available: ${available}` }],
1432
+ isError: true,
1433
+ };
1434
+ }
1435
+ const lines = [`# Category: ${input.category}\n`];
1436
+ for (const name of toolNames) {
1437
+ const tool = PINEPAPER_TOOLS.find((t) => t.name === name);
1438
+ if (tool) {
1439
+ lines.push(`## ${tool.name}\n${tool.description}\n`);
1440
+ }
1441
+ }
1442
+ return {
1443
+ content: [{ type: 'text', text: lines.join('\n') }],
1444
+ };
1445
+ }
1446
+ return { content: [{ type: 'text', text: AI_AGENT_GUIDE }] };
1447
+ }
1448
+ // -----------------------------------------------------------------------
707
1449
  // ASSET TOOLS
708
1450
  // -----------------------------------------------------------------------
709
1451
  case 'pinepaper_search_assets': {
@@ -894,6 +1636,455 @@ Browser is ready. You can now use other pinepaper tools to create and animate gr
894
1636
  return executeOrGenerate(code, description, options, 'pinepaper_diagram_mode');
895
1637
  }
896
1638
  // -----------------------------------------------------------------------
1639
+ // AGENT FLOW MODE TOOLS
1640
+ // -----------------------------------------------------------------------
1641
+ case 'pinepaper_agent_start_job': {
1642
+ const input = AgentStartJobInputSchema.parse(args);
1643
+ // Guard against pipeline restarts — warn loudly if a job is already active
1644
+ const session = getSessionManager();
1645
+ if (session.hasActiveJob()) {
1646
+ const currentJob = session.getCurrentJob();
1647
+ const itemCount = currentJob?.itemsCreated.length ?? 0;
1648
+ if (itemCount > 0) {
1649
+ // Job has items — this is almost certainly a pipeline restart bug
1650
+ console.error(`[PinePaper] WARNING: Starting new job while ${itemCount} items exist from active job ${currentJob?.jobId}`);
1651
+ const code = codeGenerator.generateAgentStartJob(input);
1652
+ const result = await executeOrGenerate(code, `Restarted job (previous had ${itemCount} items)`, options, 'pinepaper_agent_start_job');
1653
+ // Prepend a strong warning to the result text
1654
+ if (result.content && result.content.length > 0 && result.content[0].type === 'text') {
1655
+ result.content[0].text = `⚠️ WARNING: A job was already active with ${itemCount} items created. Starting a new job will create DUPLICATE items on the canvas. If you already created items, do NOT recreate them — continue from where you left off or call pinepaper_agent_end_job instead.\n\n${result.content[0].text}`;
1656
+ }
1657
+ return result;
1658
+ }
1659
+ }
1660
+ const code = codeGenerator.generateAgentStartJob(input);
1661
+ const descriptionText = `Started agent job${input.name ? ` "${input.name}"` : ''} with ${input.screenshotPolicy || 'on_complete'} screenshot policy`;
1662
+ const result = await executeOrGenerate(code, descriptionText, options, 'pinepaper_agent_start_job');
1663
+ // --- Smart design guidance injection ---
1664
+ if (result.content && result.content.length > 0 && result.content[0].type === 'text') {
1665
+ const prompt = (input.description || input.name || '').toLowerCase();
1666
+ const isDesignSpecific = detectDesignSpecificity(prompt);
1667
+ // Always include workflow hint
1668
+ let guidance = `\n\nNEXT: Call pinepaper_agent_batch_execute with ALL operations in one call:` +
1669
+ `\n 1. set_canvas_size / set_background / execute_generator (canvas setup)` +
1670
+ `\n 2. create (items — text, shapes, etc.)` +
1671
+ `\n 3. animate / keyframe_animate / relation (REQUIRED for animation — add for EACH item)` +
1672
+ `\n 4. play_timeline (REQUIRED — starts playback)` +
1673
+ `\nThen: pinepaper_agent_end_job (returns screenshot for validation)`;
1674
+ // Inject design inspiration when the prompt is vague
1675
+ if (!isDesignSpecific) {
1676
+ guidance += `\n\n─── DESIGN INSPIRATION (your prompt is open-ended — use these for creative direction) ───` +
1677
+ `\nBACKGROUND: Use execute_generator for rich visuals:` +
1678
+ `\n Dreamy/soft: drawBokeh, drawGradientMesh, drawOrganicFlow` +
1679
+ `\n Energetic: drawSunburst, drawSunsetScene, drawGeometricAbstract` +
1680
+ `\n Tech/modern: drawCircuit, drawGrid, drawWindField` +
1681
+ `\n Calm/organic: drawWaves, drawFluidFlow, drawNoiseTexture` +
1682
+ `\nCOLORS: dark bg (#0f172a, #1a1a2e) + bright accents (#f472b6 pink, #818cf8 purple, #fbbf24 gold, #34d399 green)` +
1683
+ `\nCOMPOSITION: generator bg → large decorative shapes (low opacity 0.3-0.5) → main text → small accents on top` +
1684
+ `\nANIMATION: Use "relation" ops for dynamic motion:` +
1685
+ `\n orbits (items circle around center), follows (items trail each other), wave_through (ripple effect)` +
1686
+ `\n Plus: keyframe_animate for timed reveals (fade in, scale up), animate for loops (pulse, rotate, bounce)` +
1687
+ `\n5s TIMING: bg 0s → shapes fade-in 0.5-1.5s → main text 2-3s → accents 3-4s → play_timeline`;
1688
+ }
1689
+ result.content[0].text += guidance;
1690
+ }
1691
+ return result;
1692
+ }
1693
+ case 'pinepaper_agent_end_job': {
1694
+ const input = AgentEndJobInputSchema.parse(args);
1695
+ const code = codeGenerator.generateAgentEndJob(input);
1696
+ const description = 'Ended agent job with summary and recommendations';
1697
+ // In code mode or non-browser mode, return as usual
1698
+ const endJobExecMode = options.executionMode ?? getExecutionMode();
1699
+ if (endJobExecMode === 'code' || !options.executeInBrowser) {
1700
+ return executeOrGenerate(code, description, options, 'pinepaper_agent_end_job');
1701
+ }
1702
+ // Execute in browser and intercept result to save large screenshots to disk
1703
+ const endJobController = options.browserController || getBrowserController();
1704
+ if (!endJobController.connected) {
1705
+ return executeOrGenerate(code, description, options, 'pinepaper_agent_end_job');
1706
+ }
1707
+ // Auto-start agent session if not active
1708
+ const endJobSessionMgr = getSessionManager();
1709
+ if (!endJobSessionMgr.hasActiveJob()) {
1710
+ endJobSessionMgr.startJob({ name: 'auto_session', screenshotPolicy: 'on_complete' });
1711
+ }
1712
+ const endJobBrowserResult = await endJobController.executeCode(code, false);
1713
+ if (!endJobBrowserResult.success) {
1714
+ const canvasState = await captureCanvasState(endJobController);
1715
+ return errorResult(ErrorCodes.EXECUTION_ERROR, endJobBrowserResult.error || 'End job failed', { code }, { toolName: 'pinepaper_agent_end_job', canvasState: canvasState || undefined });
1716
+ }
1717
+ const endJobResult = endJobBrowserResult.result;
1718
+ // Check if result contains a large screenshot data URL
1719
+ if (endJobResult?.screenshot && typeof endJobResult.screenshot === 'string' && endJobResult.screenshot.length > SAVE_THRESHOLD_BYTES) {
1720
+ try {
1721
+ const { filePath, fileSize } = await saveExportToFile(endJobResult.screenshot, 'png', 'screenshot');
1722
+ const cleanResult = { ...endJobResult, screenshot: undefined, screenshotPath: filePath, screenshotSize: fileSize };
1723
+ // Take a smaller Puppeteer viewport screenshot to show the user inline
1724
+ const previewScreenshot = await endJobController.takeScreenshot();
1725
+ const content = [{
1726
+ type: 'text',
1727
+ text: `Executed PinePaper code:\n\n\`\`\`javascript\n${code}\n\`\`\`\n\nFull screenshot saved to: ${filePath} (${(fileSize / 1024).toFixed(1)} KB)\n\nResult: ${JSON.stringify(cleanResult, null, 2)}`,
1728
+ }];
1729
+ if (previewScreenshot) {
1730
+ content.push({
1731
+ type: 'image',
1732
+ data: previewScreenshot,
1733
+ mimeType: 'image/png',
1734
+ });
1735
+ }
1736
+ return { content };
1737
+ }
1738
+ catch (saveError) {
1739
+ console.error('[PinePaper] Failed to save end_job screenshot to file:', saveError);
1740
+ // Fall through to inline result but strip the screenshot to avoid oversized response
1741
+ const strippedResult = { ...endJobResult, screenshot: '[screenshot too large for inline — save failed]' };
1742
+ return executedResult(code, strippedResult, undefined, description);
1743
+ }
1744
+ }
1745
+ return executedResult(code, endJobResult, endJobBrowserResult.screenshot, description);
1746
+ }
1747
+ case 'pinepaper_agent_reset': {
1748
+ const input = AgentResetInputSchema.parse(args);
1749
+ const code = codeGenerator.generateAgentReset(input);
1750
+ const description = `Fast canvas reset${input.canvasPreset ? ` to ${input.canvasPreset} preset` : ''}`;
1751
+ return executeOrGenerate(code, description, options, 'pinepaper_agent_reset');
1752
+ }
1753
+ case 'pinepaper_agent_batch_execute': {
1754
+ const input = AgentBatchExecuteInputSchema.parse(args);
1755
+ const code = codeGenerator.generateAgentBatchExecute(input);
1756
+ const description = `Batch executed ${input.operations.length} operations${input.atomic !== false ? ' (atomic)' : ''}`;
1757
+ // Check for out-of-bounds items before execution
1758
+ let boundsWarning = '';
1759
+ const sizeOp = input.operations.find((op) => op.type === 'set_canvas_size');
1760
+ if (sizeOp) {
1761
+ const cw = sizeOp.width || 1920;
1762
+ const ch = sizeOp.height || 1080;
1763
+ const oobItems = [];
1764
+ input.operations.forEach((op, idx) => {
1765
+ if (op.type === 'create' && op.position) {
1766
+ const { x, y } = op.position;
1767
+ if (x < 0 || y < 0 || x > cw || y > ch) {
1768
+ oobItems.push(`op[${idx}] ${op.itemType || 'item'} at (${x},${y})`);
1769
+ }
1770
+ }
1771
+ });
1772
+ if (oobItems.length > 0) {
1773
+ boundsWarning = `\n\nWARNING: ${oobItems.length} item(s) positioned outside canvas bounds (${cw}x${ch}): ${oobItems.join(', ')}. Items may be clipped or invisible.`;
1774
+ }
1775
+ }
1776
+ const batchResult = await executeOrGenerate(code, description, options, 'pinepaper_agent_batch_execute');
1777
+ // Append bounds warning if any
1778
+ if (boundsWarning && batchResult.content && batchResult.content.length > 0) {
1779
+ const firstContent = batchResult.content[0];
1780
+ if (firstContent.type === 'text') {
1781
+ firstContent.text += boundsWarning;
1782
+ }
1783
+ }
1784
+ return batchResult;
1785
+ }
1786
+ case 'pinepaper_agent_export': {
1787
+ const input = AgentExportInputSchema.parse(args);
1788
+ const code = codeGenerator.generateAgentExport(input);
1789
+ const description = `Smart export for ${input.platform} as ${input.format || 'auto'}`;
1790
+ // In code mode, return generated code as usual
1791
+ const effectiveExecMode = options.executionMode ?? getExecutionMode();
1792
+ if (effectiveExecMode === 'code' || !options.executeInBrowser) {
1793
+ return executeOrGenerate(code, description, options, 'pinepaper_agent_export');
1794
+ }
1795
+ // Execute in browser and intercept result for file saving
1796
+ const controller = options.browserController || getBrowserController();
1797
+ // Auto-connect if needed
1798
+ if (!controller.connected) {
1799
+ console.error('[PinePaper] Auto-connecting browser for export...');
1800
+ try {
1801
+ await controller.connect();
1802
+ }
1803
+ catch (connectError) {
1804
+ // Fall back to executeOrGenerate which handles the fallback gracefully
1805
+ return executeOrGenerate(code, description, options, 'pinepaper_agent_export');
1806
+ }
1807
+ }
1808
+ // Auto-start agent session if not active
1809
+ const exportSessionManager = getSessionManager();
1810
+ if (!exportSessionManager.hasActiveJob()) {
1811
+ exportSessionManager.startJob({ name: 'auto_session', screenshotPolicy: 'on_complete' });
1812
+ }
1813
+ const exportBrowserResult = await controller.executeCode(code, false);
1814
+ if (!exportBrowserResult.success) {
1815
+ const canvasState = await captureCanvasState(controller);
1816
+ return errorResult(ErrorCodes.EXECUTION_ERROR, exportBrowserResult.error || 'Export failed', { code }, { toolName: 'pinepaper_agent_export', canvasState: canvasState || undefined });
1817
+ }
1818
+ const exportResult = exportBrowserResult.result;
1819
+ const format = exportResult?.format || input.format || 'png';
1820
+ const data = exportResult?.data;
1821
+ const shouldSaveToFile = data && typeof data === 'string' && (ALWAYS_SAVE_FORMATS.has(format) ||
1822
+ data.length > SAVE_THRESHOLD_BYTES);
1823
+ if (shouldSaveToFile) {
1824
+ try {
1825
+ const { filePath, fileSize } = await saveExportToFile(data, format, input.platform || 'auto');
1826
+ const cleanResult = { ...exportResult, data: undefined, filePath, fileSize };
1827
+ return {
1828
+ content: [{
1829
+ type: 'text',
1830
+ text: `Export saved to file:\n\nFile: ${filePath}\nFormat: ${format}\nSize: ${(fileSize / 1024).toFixed(1)} KB\nPlatform: ${input.platform}\n\nResult: ${JSON.stringify(cleanResult, null, 2)}`,
1831
+ }],
1832
+ };
1833
+ }
1834
+ catch (saveError) {
1835
+ console.error('[PinePaper] Failed to save export to file:', saveError);
1836
+ // Strip the massive data to prevent oversized response
1837
+ const strippedResult = { ...exportResult, data: `[${format} export data — file save failed: ${saveError instanceof Error ? saveError.message : 'unknown error'}]` };
1838
+ return executedResult(code, strippedResult, exportBrowserResult.screenshot, description);
1839
+ }
1840
+ }
1841
+ // Small export — return inline (existing behavior)
1842
+ return executedResult(code, exportResult, exportBrowserResult.screenshot, description);
1843
+ }
1844
+ case 'pinepaper_agent_analyze': {
1845
+ const input = AgentAnalyzeInputSchema.parse(args);
1846
+ const code = codeGenerator.generateAgentAnalyze(input);
1847
+ const description = 'Analyzed canvas content for export recommendations';
1848
+ return executeOrGenerate(code, description, options, 'pinepaper_agent_analyze');
1849
+ }
1850
+ // -----------------------------------------------------------------------
1851
+ // INTERACTIVE TRIGGER TOOLS
1852
+ // -----------------------------------------------------------------------
1853
+ case 'pinepaper_add_trigger': {
1854
+ const input = AddTriggerInputSchema.parse(args);
1855
+ const code = codeGenerator.generateAddTrigger(input);
1856
+ const description = `Added ${input.event} trigger to ${input.itemId} with ${input.actions.length} actions`;
1857
+ return executeOrGenerate(code, description, options, 'pinepaper_add_trigger');
1858
+ }
1859
+ case 'pinepaper_remove_trigger': {
1860
+ const input = RemoveTriggerInputSchema.parse(args);
1861
+ const code = codeGenerator.generateRemoveTrigger(input);
1862
+ const description = input.removeAll
1863
+ ? `Removed all triggers from ${input.itemId}`
1864
+ : `Removed ${input.event} trigger from ${input.itemId}`;
1865
+ return executeOrGenerate(code, description, options, 'pinepaper_remove_trigger');
1866
+ }
1867
+ case 'pinepaper_query_triggers': {
1868
+ const input = QueryTriggersInputSchema.parse(args);
1869
+ const code = codeGenerator.generateQueryTriggers(input);
1870
+ const description = input.itemId
1871
+ ? `Queried triggers for ${input.itemId}`
1872
+ : 'Queried all triggers on canvas';
1873
+ return executeOrGenerate(code, description, options, 'pinepaper_query_triggers');
1874
+ }
1875
+ // -----------------------------------------------------------------------
1876
+ // QUIZ/LMS TOOLS
1877
+ // -----------------------------------------------------------------------
1878
+ case 'pinepaper_create_quiz': {
1879
+ const input = CreateQuizInputSchema.parse(args);
1880
+ const code = codeGenerator.generateCreateQuiz(input);
1881
+ const description = `Created quiz${input.title ? ` "${input.title}"` : ''} with ${input.questions.length} questions`;
1882
+ return executeOrGenerate(code, description, options, 'pinepaper_create_quiz');
1883
+ }
1884
+ case 'pinepaper_get_quiz_state': {
1885
+ const input = GetQuizStateInputSchema.parse(args);
1886
+ const code = codeGenerator.generateGetQuizState(input);
1887
+ const description = input.quizId
1888
+ ? `Got state for quiz ${input.quizId}`
1889
+ : 'Got state for active quiz';
1890
+ return executeOrGenerate(code, description, options, 'pinepaper_get_quiz_state');
1891
+ }
1892
+ case 'pinepaper_reset_quiz': {
1893
+ const input = ResetQuizInputSchema.parse(args);
1894
+ const code = codeGenerator.generateResetQuiz(input);
1895
+ const description = input.quizId
1896
+ ? `Reset quiz ${input.quizId}`
1897
+ : 'Reset active quiz';
1898
+ return executeOrGenerate(code, description, options, 'pinepaper_reset_quiz');
1899
+ }
1900
+ // -----------------------------------------------------------------------
1901
+ // LETTER COLLAGE TOOLS
1902
+ // -----------------------------------------------------------------------
1903
+ case 'pinepaper_create_letter_collage': {
1904
+ const input = CreateLetterCollageInputSchema.parse(args);
1905
+ const code = codeGenerator.generateCreateLetterCollage(input);
1906
+ const description = `Created ${input.style || 'tile'} letter collage: "${input.text}"`;
1907
+ return executeOrGenerate(code, description, options, 'pinepaper_create_letter_collage');
1908
+ }
1909
+ case 'pinepaper_animate_letter_collage': {
1910
+ const input = AnimateLetterCollageInputSchema.parse(args);
1911
+ const code = codeGenerator.generateAnimateLetterCollage(input);
1912
+ const description = `Animated collage ${input.collageId} with ${input.animationType}`;
1913
+ return executeOrGenerate(code, description, options, 'pinepaper_animate_letter_collage');
1914
+ }
1915
+ case 'pinepaper_get_letter_collage_options': {
1916
+ const code = codeGenerator.generateGetLetterCollageOptions();
1917
+ return executeOrGenerate(code, 'Gets available letter collage styles and palettes', options, 'pinepaper_get_letter_collage_options');
1918
+ }
1919
+ // -----------------------------------------------------------------------
1920
+ // CANVAS PRESETS TOOLS
1921
+ // -----------------------------------------------------------------------
1922
+ case 'pinepaper_get_canvas_presets': {
1923
+ const code = codeGenerator.generateGetCanvasPresets();
1924
+ return executeOrGenerate(code, 'Gets all available canvas presets', options, 'pinepaper_get_canvas_presets');
1925
+ }
1926
+ // -----------------------------------------------------------------------
1927
+ // MAP TOOLS
1928
+ // -----------------------------------------------------------------------
1929
+ case 'pinepaper_load_map': {
1930
+ const input = LoadMapInputSchema.parse(args);
1931
+ const code = codeGenerator.generateLoadMap(input);
1932
+ const description = `Loads ${input.mapId} map with ${input.projection || 'default'} projection`;
1933
+ return executeOrGenerate(code, description, options, 'pinepaper_load_map');
1934
+ }
1935
+ case 'pinepaper_highlight_regions': {
1936
+ const input = HighlightRegionsInputSchema.parse(args);
1937
+ const code = codeGenerator.generateHighlightRegions(input);
1938
+ const description = `Highlights ${input.regionIds.length} region(s) on the map`;
1939
+ return executeOrGenerate(code, description, options, 'pinepaper_highlight_regions');
1940
+ }
1941
+ case 'pinepaper_unhighlight_regions': {
1942
+ const input = UnhighlightRegionsInputSchema.parse(args);
1943
+ const code = codeGenerator.generateUnhighlightRegions(input);
1944
+ return executeOrGenerate(code, 'Removes region highlights', options, 'pinepaper_unhighlight_regions');
1945
+ }
1946
+ case 'pinepaper_apply_data_colors': {
1947
+ const input = ApplyDataColorsInputSchema.parse(args);
1948
+ const code = codeGenerator.generateApplyDataColors(input);
1949
+ const regionCount = Object.keys(input.data).length;
1950
+ const description = `Applies choropleth coloring to ${regionCount} region(s)`;
1951
+ return executeOrGenerate(code, description, options, 'pinepaper_apply_data_colors');
1952
+ }
1953
+ case 'pinepaper_add_marker': {
1954
+ const input = AddMarkerInputSchema.parse(args);
1955
+ const code = codeGenerator.generateAddMarker(input);
1956
+ const description = `Adds marker at [${input.lat}, ${input.lon}]${input.label ? `: ${input.label}` : ''}`;
1957
+ return executeOrGenerate(code, description, options, 'pinepaper_add_marker');
1958
+ }
1959
+ case 'pinepaper_add_map_labels': {
1960
+ const input = AddMapLabelsInputSchema.parse(args);
1961
+ const code = codeGenerator.generateAddMapLabels(input);
1962
+ return executeOrGenerate(code, 'Adds labels to map regions', options, 'pinepaper_add_map_labels');
1963
+ }
1964
+ case 'pinepaper_pan_map': {
1965
+ const input = PanMapInputSchema.parse(args);
1966
+ const code = codeGenerator.generatePanMap(input);
1967
+ const description = `Pans map to [${input.lat}, ${input.lon}]`;
1968
+ return executeOrGenerate(code, description, options, 'pinepaper_pan_map');
1969
+ }
1970
+ case 'pinepaper_zoom_map': {
1971
+ const input = ZoomMapInputSchema.parse(args);
1972
+ const code = codeGenerator.generateZoomMap(input);
1973
+ const description = `Sets map zoom level to ${input.level}`;
1974
+ return executeOrGenerate(code, description, options, 'pinepaper_zoom_map');
1975
+ }
1976
+ case 'pinepaper_export_map': {
1977
+ const code = codeGenerator.generateExportMap();
1978
+ return executeOrGenerate(code, 'Exports map configuration', options, 'pinepaper_export_map');
1979
+ }
1980
+ case 'pinepaper_import_custom_map': {
1981
+ const input = ImportCustomMapInputSchema.parse(args);
1982
+ const code = codeGenerator.generateImportCustomMap(input);
1983
+ const description = input.url ? `Imports custom map from URL` : 'Imports custom GeoJSON map';
1984
+ return executeOrGenerate(code, description, options, 'pinepaper_import_custom_map');
1985
+ }
1986
+ case 'pinepaper_get_region_at_point': {
1987
+ const input = GetRegionAtPointInputSchema.parse(args);
1988
+ const code = codeGenerator.generateGetRegionAtPoint(input);
1989
+ const description = `Gets region at point [${input.x}, ${input.y}]`;
1990
+ return executeOrGenerate(code, description, options, 'pinepaper_get_region_at_point');
1991
+ }
1992
+ // -----------------------------------------------------------------------
1993
+ // MAP ANIMATION/CSV TOOLS
1994
+ // -----------------------------------------------------------------------
1995
+ case 'pinepaper_animate_map_regions': {
1996
+ const input = AnimateMapRegionsInputSchema.parse(args);
1997
+ const code = codeGenerator.generateAnimateMapRegions(input);
1998
+ const regionCount = Object.keys(input.regions).length;
1999
+ const description = `Animates ${regionCount} map region(s) over ${input.duration || 5}s`;
2000
+ return executeOrGenerate(code, description, options, 'pinepaper_animate_map_regions');
2001
+ }
2002
+ case 'pinepaper_animate_map_wave': {
2003
+ const input = AnimateMapWaveInputSchema.parse(args);
2004
+ const code = codeGenerator.generateAnimateMapWave(input);
2005
+ const description = `Creates ${input.waveDirection || 'horizontal'} wave animation`;
2006
+ return executeOrGenerate(code, description, options, 'pinepaper_animate_map_wave');
2007
+ }
2008
+ case 'pinepaper_stop_map_animations': {
2009
+ const input = StopMapAnimationsInputSchema.parse(args);
2010
+ const code = codeGenerator.generateStopMapAnimations(input);
2011
+ return executeOrGenerate(code, 'Stops map region animations', options, 'pinepaper_stop_map_animations');
2012
+ }
2013
+ case 'pinepaper_get_animated_map_regions': {
2014
+ const code = codeGenerator.generateGetAnimatedMapRegions();
2015
+ return executeOrGenerate(code, 'Gets list of animated map regions', options, 'pinepaper_get_animated_map_regions');
2016
+ }
2017
+ case 'pinepaper_export_map_region_csv': {
2018
+ const input = ExportMapRegionCSVInputSchema.parse(args);
2019
+ const code = codeGenerator.generateExportMapRegionCSV(input);
2020
+ return executeOrGenerate(code, 'Exports map region data as CSV', options, 'pinepaper_export_map_region_csv');
2021
+ }
2022
+ case 'pinepaper_import_map_region_csv': {
2023
+ const input = ImportMapRegionCSVInputSchema.parse(args);
2024
+ const code = codeGenerator.generateImportMapRegionCSV(input);
2025
+ return executeOrGenerate(code, 'Imports CSV data to update map regions', options, 'pinepaper_import_map_region_csv');
2026
+ }
2027
+ case 'pinepaper_select_map_regions': {
2028
+ const input = SelectMapRegionsInputSchema.parse(args);
2029
+ const code = codeGenerator.generateSelectMapRegions(input);
2030
+ const description = `Selects ${input.regionIds.length} map region(s)`;
2031
+ return executeOrGenerate(code, description, options, 'pinepaper_select_map_regions');
2032
+ }
2033
+ case 'pinepaper_deselect_map_regions': {
2034
+ const input = DeselectMapRegionsInputSchema.parse(args);
2035
+ const code = codeGenerator.generateDeselectMapRegions(input);
2036
+ return executeOrGenerate(code, 'Deselects map regions', options, 'pinepaper_deselect_map_regions');
2037
+ }
2038
+ case 'pinepaper_get_highlighted_map_regions': {
2039
+ const code = codeGenerator.generateGetHighlightedMapRegions();
2040
+ return executeOrGenerate(code, 'Gets list of highlighted map regions', options, 'pinepaper_get_highlighted_map_regions');
2041
+ }
2042
+ case 'pinepaper_export_map_geojson': {
2043
+ const code = codeGenerator.generateExportMapGeoJson({
2044
+ includeStyles: args.includeStyles,
2045
+ includeMetadata: args.includeMetadata,
2046
+ selectedOnly: args.selectedOnly,
2047
+ download: args.download,
2048
+ filename: args.filename,
2049
+ });
2050
+ return executeOrGenerate(code, 'Exports map as GeoJSON', options, 'pinepaper_export_map_geojson');
2051
+ }
2052
+ case 'pinepaper_export_original_map_geojson': {
2053
+ const code = codeGenerator.generateExportOriginalMapGeoJson({
2054
+ download: args.download,
2055
+ filename: args.filename,
2056
+ });
2057
+ return executeOrGenerate(code, 'Exports original map GeoJSON', options, 'pinepaper_export_original_map_geojson');
2058
+ }
2059
+ case 'pinepaper_get_map_source_info': {
2060
+ const code = codeGenerator.generateGetMapSourceInfo();
2061
+ return executeOrGenerate(code, 'Gets map source information', options, 'pinepaper_get_map_source_info');
2062
+ }
2063
+ case 'pinepaper_register_item': {
2064
+ const code = codeGenerator.generateRegisterItem({
2065
+ itemJson: args.itemJson,
2066
+ itemType: args.itemType,
2067
+ properties: args.properties,
2068
+ });
2069
+ return executeOrGenerate(code, 'Registers Paper.js item', options, 'pinepaper_register_item');
2070
+ }
2071
+ // -----------------------------------------------------------------------
2072
+ // TEMPLATE TOOLS
2073
+ // -----------------------------------------------------------------------
2074
+ case 'pinepaper_apply_template': {
2075
+ const parsed = ApplyTemplateInputSchema.parse(args);
2076
+ const code = codeGenerator.generateApplyTemplate(parsed);
2077
+ return executeOrGenerate(code, 'Applies template', options, 'pinepaper_apply_template');
2078
+ }
2079
+ // -----------------------------------------------------------------------
2080
+ // IMAGE IMPORT TOOLS
2081
+ // -----------------------------------------------------------------------
2082
+ case 'pinepaper_import_image': {
2083
+ const parsed = ImportImageInputSchema.parse(args);
2084
+ const code = codeGenerator.generateImportImage(parsed);
2085
+ return executeOrGenerate(code, 'Imports image', options, 'pinepaper_import_image');
2086
+ }
2087
+ // -----------------------------------------------------------------------
897
2088
  // UNKNOWN TOOL
898
2089
  // -----------------------------------------------------------------------
899
2090
  default: {
@@ -913,12 +2104,28 @@ Browser is ready. You can now use other pinepaper tools to create and animate gr
913
2104
  'pinepaper_add_relation',
914
2105
  'pinepaper_remove_relation',
915
2106
  'pinepaper_query_relations',
2107
+ 'pinepaper_register_custom_relation',
2108
+ 'pinepaper_execute_custom_code',
916
2109
  'pinepaper_animate',
917
2110
  'pinepaper_keyframe_animate',
918
2111
  'pinepaper_play_timeline',
919
2112
  'pinepaper_execute_generator',
920
2113
  'pinepaper_list_generators',
921
2114
  'pinepaper_apply_effect',
2115
+ 'pinepaper_apply_animated_mask',
2116
+ 'pinepaper_apply_custom_mask',
2117
+ 'pinepaper_remove_mask',
2118
+ 'pinepaper_get_animatable_properties',
2119
+ 'pinepaper_get_available_easings',
2120
+ 'pinepaper_get_mask_types',
2121
+ 'pinepaper_get_mask_animations',
2122
+ 'pinepaper_camera_animate',
2123
+ 'pinepaper_camera_zoom',
2124
+ 'pinepaper_camera_pan',
2125
+ 'pinepaper_camera_move_to',
2126
+ 'pinepaper_camera_reset',
2127
+ 'pinepaper_camera_stop',
2128
+ 'pinepaper_camera_state',
922
2129
  'pinepaper_get_items',
923
2130
  'pinepaper_get_relation_stats',
924
2131
  'pinepaper_set_background_color',
@@ -930,11 +2137,13 @@ Browser is ready. You can now use other pinepaper tools to create and animate gr
930
2137
  'pinepaper_add_filter',
931
2138
  'pinepaper_export_svg',
932
2139
  'pinepaper_export_training_data',
2140
+ 'pinepaper_export_scene',
933
2141
  'pinepaper_browser_connect',
934
2142
  'pinepaper_browser_disconnect',
935
2143
  'pinepaper_browser_screenshot',
936
2144
  'pinepaper_browser_status',
937
2145
  'pinepaper_get_performance_metrics',
2146
+ 'pinepaper_diagnostic_report',
938
2147
  'pinepaper_search_assets',
939
2148
  'pinepaper_import_asset',
940
2149
  'pinepaper_p5_draw',
@@ -948,6 +2157,80 @@ Browser is ready. You can now use other pinepaper tools to create and animate gr
948
2157
  'pinepaper_update_connector',
949
2158
  'pinepaper_remove_connector',
950
2159
  'pinepaper_diagram_mode',
2160
+ // Scene tools
2161
+ 'pinepaper_create_scene',
2162
+ // Agent flow mode tools
2163
+ 'pinepaper_agent_start_job',
2164
+ 'pinepaper_agent_end_job',
2165
+ 'pinepaper_agent_reset',
2166
+ 'pinepaper_agent_batch_execute',
2167
+ 'pinepaper_agent_export',
2168
+ 'pinepaper_agent_analyze',
2169
+ // Interactive trigger tools
2170
+ 'pinepaper_add_trigger',
2171
+ 'pinepaper_remove_trigger',
2172
+ 'pinepaper_query_triggers',
2173
+ // Quiz/LMS tools
2174
+ 'pinepaper_create_quiz',
2175
+ 'pinepaper_get_quiz_state',
2176
+ 'pinepaper_reset_quiz',
2177
+ // Letter collage tools
2178
+ 'pinepaper_create_letter_collage',
2179
+ 'pinepaper_animate_letter_collage',
2180
+ 'pinepaper_get_letter_collage_options',
2181
+ // Canvas presets tools
2182
+ 'pinepaper_get_canvas_presets',
2183
+ // Map tools
2184
+ 'pinepaper_load_map',
2185
+ 'pinepaper_highlight_regions',
2186
+ 'pinepaper_unhighlight_regions',
2187
+ 'pinepaper_apply_data_colors',
2188
+ 'pinepaper_add_marker',
2189
+ 'pinepaper_add_map_labels',
2190
+ 'pinepaper_pan_map',
2191
+ 'pinepaper_zoom_map',
2192
+ 'pinepaper_export_map',
2193
+ 'pinepaper_import_custom_map',
2194
+ 'pinepaper_get_region_at_point',
2195
+ // Map animation/CSV tools
2196
+ 'pinepaper_animate_map_regions',
2197
+ 'pinepaper_animate_map_wave',
2198
+ 'pinepaper_stop_map_animations',
2199
+ 'pinepaper_get_animated_map_regions',
2200
+ 'pinepaper_export_map_region_csv',
2201
+ 'pinepaper_import_map_region_csv',
2202
+ 'pinepaper_select_map_regions',
2203
+ 'pinepaper_deselect_map_regions',
2204
+ 'pinepaper_get_highlighted_map_regions',
2205
+ 'pinepaper_export_map_geojson',
2206
+ 'pinepaper_export_original_map_geojson',
2207
+ 'pinepaper_get_map_source_info',
2208
+ // Font tools
2209
+ 'pinepaper_font_show_studio',
2210
+ 'pinepaper_font_set_name',
2211
+ 'pinepaper_font_get_required_chars',
2212
+ 'pinepaper_font_get_status',
2213
+ 'pinepaper_font_create_glyph',
2214
+ 'pinepaper_font_create_space',
2215
+ 'pinepaper_font_remove_glyph',
2216
+ 'pinepaper_font_set_metrics',
2217
+ 'pinepaper_font_export',
2218
+ 'pinepaper_font_load_into_document',
2219
+ 'pinepaper_font_export_data',
2220
+ 'pinepaper_font_import_data',
2221
+ 'pinepaper_font_clear',
2222
+ 'pinepaper_font_remove_overlap',
2223
+ 'pinepaper_font_correct_direction',
2224
+ 'pinepaper_font_cleanup_path',
2225
+ // Template tools
2226
+ 'pinepaper_apply_template',
2227
+ // Image import tools
2228
+ 'pinepaper_import_image',
2229
+ // Paper.js direct access tools
2230
+ 'pinepaper_register_item',
2231
+ // On-demand guide & runtime config
2232
+ 'pinepaper_tool_guide',
2233
+ 'pinepaper_set_toolkit',
951
2234
  ],
952
2235
  });
953
2236
  }