@pinepaper.studio/mcp-server 1.6.18 → 1.6.19

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 (108) hide show
  1. package/README.de.md +1 -1
  2. package/README.es.md +1 -1
  3. package/README.fr.md +1 -1
  4. package/README.hi.md +1 -1
  5. package/README.ja.md +1 -1
  6. package/README.ko.md +1 -1
  7. package/README.md +97 -10
  8. package/README.pt-BR.md +1 -1
  9. package/README.zh-CN.md +1 -1
  10. package/dist/agent/export-manager.js +2 -2
  11. package/dist/agent/session-manager.d.ts +8 -0
  12. package/dist/agent/session-manager.d.ts.map +1 -1
  13. package/dist/agent/session-manager.js +17 -2
  14. package/dist/agent/session-manager.js.map +1 -1
  15. package/dist/assets/asset-manager.d.ts +28 -1
  16. package/dist/assets/asset-manager.d.ts.map +1 -1
  17. package/dist/assets/asset-manager.js +65 -25
  18. package/dist/assets/asset-manager.js.map +1 -1
  19. package/dist/assets/index.d.ts +1 -3
  20. package/dist/assets/index.d.ts.map +1 -1
  21. package/dist/assets/index.js +1 -3
  22. package/dist/assets/index.js.map +1 -1
  23. package/dist/assets/repositories/iconify.js +1 -1
  24. package/dist/assets/repositories/iconify.js.map +1 -1
  25. package/dist/assets/repositories/openclipart.js +1 -1
  26. package/dist/assets/repositories/openclipart.js.map +1 -1
  27. package/dist/assets/repositories/svgrepo.js +2 -2
  28. package/dist/assets/repositories/svgrepo.js.map +1 -1
  29. package/dist/assets/types.d.ts +2 -2
  30. package/dist/assets/types.d.ts.map +1 -1
  31. package/dist/browser/puppeteer-controller.d.ts +25 -0
  32. package/dist/browser/puppeteer-controller.d.ts.map +1 -1
  33. package/dist/browser/puppeteer-controller.js +107 -1
  34. package/dist/browser/puppeteer-controller.js.map +1 -1
  35. package/dist/build-info.json +2 -2
  36. package/dist/cli.js +4 -2
  37. package/dist/cli.js.map +1 -1
  38. package/dist/design/design-systems.d.ts +2 -0
  39. package/dist/design/design-systems.d.ts.map +1 -1
  40. package/dist/design/design-systems.js +2 -0
  41. package/dist/design/design-systems.js.map +1 -1
  42. package/dist/execution/canvas-state.d.ts.map +1 -1
  43. package/dist/execution/canvas-state.js +19 -43
  44. package/dist/execution/canvas-state.js.map +1 -1
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +20 -7
  47. package/dist/index.js.map +1 -1
  48. package/dist/ontology/design-graph.js +2 -2
  49. package/dist/ontology/design-graph.js.map +1 -1
  50. package/dist/ontology/upstream-vocabulary.d.ts +2 -2
  51. package/dist/ontology/upstream-vocabulary.js +2 -2
  52. package/dist/ontology/validator.js +1 -1
  53. package/dist/ontology/validator.js.map +1 -1
  54. package/dist/tools/compact-descriptions.d.ts.map +1 -1
  55. package/dist/tools/compact-descriptions.js +15 -14
  56. package/dist/tools/compact-descriptions.js.map +1 -1
  57. package/dist/tools/definitions.d.ts +1 -1
  58. package/dist/tools/definitions.d.ts.map +1 -1
  59. package/dist/tools/definitions.js +422 -126
  60. package/dist/tools/definitions.js.map +1 -1
  61. package/dist/tools/engine-lists.d.ts +17 -2
  62. package/dist/tools/engine-lists.d.ts.map +1 -1
  63. package/dist/tools/engine-lists.js +102 -2
  64. package/dist/tools/engine-lists.js.map +1 -1
  65. package/dist/tools/engine-methods.d.ts.map +1 -1
  66. package/dist/tools/engine-methods.js +14 -1
  67. package/dist/tools/engine-methods.js.map +1 -1
  68. package/dist/tools/engine-surface.d.ts +1 -1
  69. package/dist/tools/engine-surface.d.ts.map +1 -1
  70. package/dist/tools/engine-surface.js +49 -8
  71. package/dist/tools/engine-surface.js.map +1 -1
  72. package/dist/tools/handlers/camera.js +8 -8
  73. package/dist/tools/handlers/camera.js.map +1 -1
  74. package/dist/tools/handlers/export.d.ts.map +1 -1
  75. package/dist/tools/handlers/export.js +77 -5
  76. package/dist/tools/handlers/export.js.map +1 -1
  77. package/dist/tools/handlers/font.js +10 -10
  78. package/dist/tools/handlers/font.js.map +1 -1
  79. package/dist/tools/handlers/generate.d.ts +12 -12
  80. package/dist/tools/handlers/maps.d.ts.map +1 -1
  81. package/dist/tools/handlers/maps.js +23 -3
  82. package/dist/tools/handlers/maps.js.map +1 -1
  83. package/dist/tools/handlers.d.ts +21 -0
  84. package/dist/tools/handlers.d.ts.map +1 -1
  85. package/dist/tools/handlers.js +240 -37
  86. package/dist/tools/handlers.js.map +1 -1
  87. package/dist/tools/minimal-descriptions.d.ts.map +1 -1
  88. package/dist/tools/minimal-descriptions.js +32 -25
  89. package/dist/tools/minimal-descriptions.js.map +1 -1
  90. package/dist/tools/shape-params.d.ts +2 -2
  91. package/dist/tools/shape-params.d.ts.map +1 -1
  92. package/dist/tools/shape-params.js +5 -2
  93. package/dist/tools/shape-params.js.map +1 -1
  94. package/dist/tools/stick-vocabulary.d.ts +1 -1
  95. package/dist/tools/stick-vocabulary.js +1 -1
  96. package/dist/tools/toolkits.d.ts.map +1 -1
  97. package/dist/tools/toolkits.js +5 -0
  98. package/dist/tools/toolkits.js.map +1 -1
  99. package/dist/types/code-generator.d.ts +57 -3
  100. package/dist/types/code-generator.d.ts.map +1 -1
  101. package/dist/types/code-generator.js +1007 -253
  102. package/dist/types/code-generator.js.map +1 -1
  103. package/dist/types/schemas.d.ts +1842 -890
  104. package/dist/types/schemas.d.ts.map +1 -1
  105. package/dist/types/schemas.js +229 -51
  106. package/dist/types/schemas.js.map +1 -1
  107. package/manifest.json +22 -2
  108. package/package.json +1 -1
@@ -6,7 +6,7 @@
6
6
  * Supports browser execution via Puppeteer.
7
7
  */
8
8
  import { codeGenerator, generateQueryMutationsCode, SHEET_RUBRIC } from '../types/code-generator.js';
9
- import { CreateItemInputSchema, ModifyItemInputSchema, DeleteItemInputSchema, AddRelationInputSchema, RemoveRelationInputSchema, QueryRelationsInputSchema, AnimateItemInputSchema, ImportMotionCaptureInputSchema, KeyframeAnimateInputSchema, ExecuteGeneratorInputSchema, ApplyEffectInputSchema, GetItemsInputSchema, PlayTimelineInputSchema, SetBackgroundColorInputSchema, SetCanvasSizeInputSchema, ImportSVGInputSchema, ImportMermaidInputSchema, AddFilterInputSchema, CreateGlossySphereInputSchema, CreateDiagonalStripesInputSchema, BatchCreateInputSchema, BatchModifyInputSchema, CreateGridInputSchema, GeometryInputSchema, EquationPathInputSchema, EventInputSchema, ConstructionSequenceInputSchema, ValidateSceneInputSchema, CaptureFramesInputSchema, InstantiateOntologyInputSchema, LintSceneInputSchema, MediaInputSchema, TextStyleInputSchema, TextEffectInputSchema, DesignMediumInputSchema, ShatterImageInputSchema, ImportLayeredCharacterInputSchema, CharacterInputSchema, GameInputSchema, World3DInputSchema, BrandKitInputSchema, ComponentInputSchema, ArtboardInputSchema, CommentInputSchema, ProvenanceInputSchema, SceneDiffInputSchema, AudioBeatsInputSchema, TemplateParamsInputSchema, RenderBatchInputSchema, AccessibilityCheckInputSchema, QueryMutationsInputSchema, ComposeInputSchema, CropImageInputSchema, PathOpInputSchema, MotionInputSchema, SoundInputSchema, InterchangeInputSchema, StickInputSchema, DesignSystemInputSchema, StoryInputSchema, ChromaKeyInputSchema, RiggingInputSchema, GroupInputSchema, CameraDirectorInputSchema, DetectObjectsInputSchema, ExtractObjectInputSchema, ArrangeInputSchema, CreateSceneInputSchema, GetPerformanceMetricsInputSchema, DiagnosticReportInputSchema, SearchAssetsInputSchema, ImportAssetInputSchema, P5DrawInputSchema,
9
+ import { CreateItemInputSchema, ModifyItemInputSchema, DeleteItemInputSchema, AddRelationInputSchema, RemoveRelationInputSchema, QueryRelationsInputSchema, AnimateItemInputSchema, ImportMotionCaptureInputSchema, KeyframeAnimateInputSchema, ExecuteGeneratorInputSchema, ApplyEffectInputSchema, GetItemsInputSchema, PlayTimelineInputSchema, SetBackgroundColorInputSchema, SetCanvasSizeInputSchema, ImportSVGInputSchema, ImportMermaidInputSchema, AddFilterInputSchema, CreateGlossySphereInputSchema, CreateDiagonalStripesInputSchema, BatchCreateInputSchema, BatchModifyInputSchema, CreateGridInputSchema, GeometryInputSchema, EquationPathInputSchema, EventInputSchema, ConstructionSequenceInputSchema, ValidateSceneInputSchema, CaptureFramesInputSchema, InstantiateOntologyInputSchema, LintSceneInputSchema, MediaInputSchema, TextStyleInputSchema, TextEffectInputSchema, DesignMediumInputSchema, RelightInputSchema, ShaderGraphInputSchema, ShatterImageInputSchema, ImportLayeredCharacterInputSchema, CharacterInputSchema, GameInputSchema, World3DInputSchema, BrandKitInputSchema, ComponentInputSchema, ArtboardInputSchema, CommentInputSchema, ProvenanceInputSchema, SceneDiffInputSchema, AudioBeatsInputSchema, TemplateParamsInputSchema, RenderBatchInputSchema, AccessibilityCheckInputSchema, QueryMutationsInputSchema, ComposeInputSchema, CropImageInputSchema, PathOpInputSchema, MotionInputSchema, SoundInputSchema, InterchangeInputSchema, StickInputSchema, DesignSystemInputSchema, StoryInputSchema, ChromaKeyInputSchema, RiggingInputSchema, GroupInputSchema, CameraDirectorInputSchema, DetectObjectsInputSchema, ExtractObjectInputSchema, ArrangeInputSchema, CreateSceneInputSchema, GetPerformanceMetricsInputSchema, DiagnosticReportInputSchema, SearchAssetsInputSchema, ImportAssetInputSchema, P5DrawInputSchema,
10
10
  // Diagram schemas
11
11
  CreateDiagramShapeInputSchema, ConnectInputSchema, ConnectPortsInputSchema, AddPortsInputSchema, AutoLayoutInputSchema, GetDiagramShapesInputSchema, UpdateConnectorInputSchema, RemoveConnectorInputSchema, DiagramModeInputSchema,
12
12
  // Agent flow mode schemas
@@ -27,7 +27,7 @@ ImportImageInputSchema,
27
27
  // Scene management schemas
28
28
  ManageScenesInputSchema, ScenePlaybackInputSchema, SceneGraphInputSchema, SequenceInputSchema, StaggerInputSchema, FlipInputSchema,
29
29
  // New consolidated tool schemas
30
- SelectionInputSchema, TransformInputSchema, HistoryInputSchema, ImageFilterInputSchema, LassoInputSchema, CutoutStyleInputSchema, PrecompInputSchema, BackgroundInputSchema, QueryInputSchema, QueryCapabilitiesInputSchema, DeformInputSchema, SpriteSheetInputSchema, InteractionInputSchema, ExportWidgetInputSchema, ExportWidgetHtmlInputSchema, CreateChartInputSchema, MagicInputSchema, PhysicsInputSchema, MeasurementInputSchema, ErrorCodes, } from '../types/schemas.js';
30
+ SelectionInputSchema, TransformInputSchema, HistoryInputSchema, ImageFilterInputSchema, LassoInputSchema, CutoutStyleInputSchema, PrecompInputSchema, BackgroundInputSchema, QueryInputSchema, QueryCapabilitiesInputSchema, DeformInputSchema, SpriteSheetInputSchema, InteractionInputSchema, ExportWidgetInputSchema, ExportWidgetHtmlInputSchema, CreateChartInputSchema, MagicInputSchema, PhysicsInputSchema, MeasurementInputSchema, PlaceOnSurfaceInputSchema, StyledSceneInputSchema, ErrorCodes, } from '../types/schemas.js';
31
31
  import { ZodError, z } from 'zod';
32
32
  import { writeFile, mkdir, appendFile, unlink, readFile, stat } from 'node:fs/promises';
33
33
  import { tmpdir } from 'node:os';
@@ -37,6 +37,7 @@ import { getBrowserController, resetBrowserController, } from '../browser/puppet
37
37
  import { sameStudioTarget, validateStudioUrl } from '../browser/url-target.js';
38
38
  import { getPerformanceTracker } from '../metrics/index.js';
39
39
  import { formatErrorContext, captureCanvasState } from '../execution/index.js';
40
+ import { FILTER_PARAM_RANGES } from './engine-lists.js';
40
41
  import { getSessionManager } from '../agent/session-manager.js';
41
42
  import { vocabularyHintForPath, validateBatchVocabulary, detectBatchPropertyTypos } from '../ontology/hints.js';
42
43
  import { cameraHandlers } from './handlers/camera.js';
@@ -560,10 +561,37 @@ export function getExecutionMode() {
560
561
  // Default to 'puppeteer' for live browser execution
561
562
  return 'puppeteer';
562
563
  }
564
+ /** Where a generated script travels for a host to record, beside the agent-visible text. */
565
+ /** Relation types registered with register_custom_relation in this server session (gate D2). */
566
+ export const CUSTOM_RELATION_NAMES = new Set();
567
+ /**
568
+ * Tools whose browser work legitimately outlasts the governor's 10 s default.
569
+ * The first detection call downloads and initialises a model, which on a cold
570
+ * page took longer than 10 s and died as a timeout (gate C10, 1.6.19).
571
+ */
572
+ const LONG_GOVERNOR_BUDGET_MS = {
573
+ pinepaper_detect_objects: 120_000,
574
+ pinepaper_extract_object: 120_000,
575
+ };
576
+ export const CODE_META_KEY = 'pinepaper.studio/code';
563
577
  // =============================================================================
564
578
  // RESULT HELPERS
565
579
  // =============================================================================
566
- function successResult(code, description) {
580
+ function successResult(code, description, deferred = false) {
581
+ if (deferred) {
582
+ return {
583
+ content: [{
584
+ type: 'text',
585
+ text: JSON.stringify({
586
+ success: true,
587
+ status: 'recorded',
588
+ step: description || 'canvas step',
589
+ note: 'Recorded on the scene. It runs in the studio when the scene renders or exports, not now; keep building, then render.',
590
+ }, null, 2),
591
+ }],
592
+ _meta: { [CODE_META_KEY]: code },
593
+ };
594
+ }
567
595
  const content = [
568
596
  {
569
597
  type: 'text',
@@ -576,7 +604,7 @@ function successResult(code, description) {
576
604
  text: `\n${description}`,
577
605
  });
578
606
  }
579
- return { content };
607
+ return { content, _meta: { [CODE_META_KEY]: code } };
580
608
  }
581
609
  /**
582
610
  * A plain data answer.
@@ -589,6 +617,18 @@ function successResult(code, description) {
589
617
  function dataResult(value) {
590
618
  return { content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] };
591
619
  }
620
+ /**
621
+ * Where a call's time went (O1, per-request trace). In the result's _meta, the
622
+ * spec's slot for metadata, under one namespaced key, so it adds nothing to
623
+ * the text a model reads. browserMs includes the screenshot when one was taken.
624
+ * index.ts adds toolMs, the whole call, for every tool.
625
+ */
626
+ export const TIMING_META_KEY = 'pinepaper.studio/timing';
627
+ export function withTiming(out, t) {
628
+ const meta = (out._meta ?? {});
629
+ const prev = (meta[TIMING_META_KEY] ?? {});
630
+ return { ...out, _meta: { ...meta, [TIMING_META_KEY]: { ...prev, ...t } } };
631
+ }
592
632
  /**
593
633
  * A call that only succeeded after the browser session was recovered says so,
594
634
  * in the result: the controller has reported `recovered` / `canvasReset` for
@@ -601,7 +641,7 @@ function withSessionNote(out, r) {
601
641
  const note = r.relaunched
602
642
  ? `The browser session had been lost; the browser was relaunched and this call ran on an EMPTY canvas${r.canvasSizeRestored ? ` (size restored to ${r.canvasSizeRestored.width}x${r.canvasSizeRestored.height})` : ' at the default size'}. Item ids from before are gone: rebuild the scene.`
603
643
  : r.canvasReset
604
- ? 'The page had reloaded and the canvas came back EMPTY. Item ids from before are gone: rebuild the scene.'
644
+ ? 'The studio page had reloaded or navigated away, and the canvas came back EMPTY. Item ids from before are gone: rebuild the scene.'
605
645
  : 'The page connection was re-established; the scene survived.';
606
646
  const first = out.content[0];
607
647
  if (first && first.type === 'text') {
@@ -894,6 +934,40 @@ async function brandFromUrl(url, shots, options) {
894
934
  return errorResult(ErrorCodes.EXECUTION_ERROR, `could not read ${url}: ${e instanceof Error ? e.message : String(e)}`, {}, { toolName: 'pinepaper_brand_kit' });
895
935
  }
896
936
  }
937
+ /**
938
+ * If an executed result carries `data` (a data URL, or JSON/text), write it to
939
+ * a file in the export dir and replace it with filePath + bytes.
940
+ */
941
+ async function writeResultDataToFile(out, ext) {
942
+ const first = out.content?.[0];
943
+ if (out.isError || !first || first.type !== 'text')
944
+ return out;
945
+ let obj;
946
+ try {
947
+ obj = JSON.parse(first.text);
948
+ }
949
+ catch {
950
+ return out;
951
+ }
952
+ const r = obj.result;
953
+ if (!r || r.data === undefined || r.data === null)
954
+ return out;
955
+ let bytes;
956
+ if (typeof r.data === 'string' && r.data.startsWith('data:'))
957
+ bytes = Buffer.from(r.data.slice(r.data.indexOf(',') + 1), 'base64');
958
+ else if (typeof r.data === 'string')
959
+ bytes = Buffer.from(r.data, 'utf-8');
960
+ else
961
+ bytes = Buffer.from(JSON.stringify(r.data), 'utf-8');
962
+ const dir = getExportDir();
963
+ await mkdir(dir, { recursive: true });
964
+ const filePath = join(dir, `pinepaper_${String(r.format ?? 'export').replace(/[^a-z0-9-]/gi, '')}_${Date.now()}.${ext}`);
965
+ await writeFile(filePath, bytes);
966
+ const { data: _d, ...rest } = r;
967
+ void _d;
968
+ obj.result = { ...rest, filePath, bytes: bytes.length };
969
+ return { ...out, content: [{ type: 'text', text: JSON.stringify(obj, null, 2) }, ...out.content.slice(1)] };
970
+ }
897
971
  /**
898
972
  * pinepaper_capture_frames with `sheet`: one tiled image of the film, saved
899
973
  * to a file (an image is never useful inline, see ALWAYS_SAVE_FORMATS), with
@@ -998,7 +1072,7 @@ The code is ready to use. Each subsequent tool call will also generate code you
998
1072
  timestamp: Date.now(),
999
1073
  success: true,
1000
1074
  });
1001
- return successResult(code, description);
1075
+ return successResult(code, description, options.deferred === true);
1002
1076
  }
1003
1077
  const controller = browserController || getBrowserController();
1004
1078
  // Auto-connect in agent mode if not connected
@@ -1081,18 +1155,29 @@ ${code}
1081
1155
  // emitter is not (its verdict already describes what it left behind).
1082
1156
  const rollbackOnFailure = toolName === 'pinepaper_execute_custom_code'
1083
1157
  && await markItemsBeforeRun(controller);
1084
- const result = await controller.executeCode(code, shouldTakeScreenshot, stage ? { stage } : {});
1158
+ const governorTimeoutMs = LONG_GOVERNOR_BUDGET_MS[toolName];
1159
+ const result = await controller.executeCode(code, shouldTakeScreenshot, {
1160
+ ...(stage ? { stage } : {}),
1161
+ ...(governorTimeoutMs ? { governorTimeoutMs } : {}),
1162
+ });
1085
1163
  const browserDuration = tracker.endTimer(`${timerId}_browser_execution`);
1164
+ // THE METRIC RECORDS THE CALL'S VERDICT, NOT THE TRANSPORT'S (gate H2,
1165
+ // 1.6.19). result.success only says the code RAN; an emitter that ran and
1166
+ // returned { success: false } is a failed call, and get_performance_metrics
1167
+ // reported "Success Rate: 100.0%" over exactly such a failure.
1168
+ const innerVerdict = result.success ? innerFailure(result.result) : null;
1086
1169
  tracker.recordMetric({
1087
1170
  toolName,
1088
1171
  phase: 'browser_execution',
1089
1172
  duration: browserDuration,
1090
1173
  timestamp: Date.now(),
1091
- success: result.success,
1092
- error: result.error,
1174
+ success: result.success && !innerVerdict,
1175
+ error: result.error || innerVerdict || undefined,
1093
1176
  });
1177
+ let screenshotMs;
1094
1178
  if (shouldTakeScreenshot) {
1095
1179
  const screenshotDuration = tracker.endTimer(`${timerId}_screenshot`);
1180
+ screenshotMs = screenshotDuration;
1096
1181
  tracker.recordMetric({
1097
1182
  toolName,
1098
1183
  phase: 'screenshot',
@@ -1108,13 +1193,14 @@ ${code}
1108
1193
  phase: 'total',
1109
1194
  duration: totalDuration,
1110
1195
  timestamp: Date.now(),
1111
- success: result.success,
1112
- error: result.error,
1196
+ success: result.success && !innerVerdict,
1197
+ error: result.error || innerVerdict || undefined,
1113
1198
  });
1199
+ const timing = { browserMs: Math.round(browserDuration), ...(screenshotMs !== undefined ? { screenshotMs: Math.round(screenshotMs) } : {}) };
1114
1200
  if (!result.success) {
1115
1201
  // Capture canvas state for error context
1116
1202
  const canvasState = await captureCanvasState(controller);
1117
- return errorResult(ErrorCodes.EXECUTION_ERROR, result.error || 'Failed to execute code in browser',
1203
+ return withTiming(errorResult(ErrorCodes.EXECUTION_ERROR, result.error || 'Failed to execute code in browser',
1118
1204
  // errorCode surfaces the governor's structured code (PP_ITEM_BUDGET,
1119
1205
  // PP_LOOP_BUDGET, PP_TIMEOUT) so the model can correct on the next turn.
1120
1206
  {
@@ -1123,7 +1209,7 @@ ${code}
1123
1209
  }, {
1124
1210
  toolName,
1125
1211
  canvasState: canvasState || undefined,
1126
- });
1212
+ }), timing);
1127
1213
  }
1128
1214
  // TWO VERDICTS. `result.success` says the CODE RAN. The emitted code has its
1129
1215
  // own verdict, and almost every emitter in this repo returns
@@ -1137,15 +1223,24 @@ ${code}
1137
1223
  // outer success and the agent kept building on a rig that did not exist.
1138
1224
  // pinepaper_agent_export got this check in 1.6.9; it belongs HERE, where
1139
1225
  // every tool inherits it.
1140
- const inner = innerFailure(result.result);
1226
+ const inner = innerVerdict;
1141
1227
  if (inner) {
1142
1228
  const canvasState = await captureCanvasState(controller);
1143
- return errorResult(ErrorCodes.EXECUTION_ERROR, inner, {
1229
+ return withTiming(errorResult(ErrorCodes.EXECUTION_ERROR, inner, {
1144
1230
  code, result: result.result, governorReport: result.report,
1145
1231
  ...(rollbackOnFailure ? await customCodeRollback(controller, inner) : {}),
1146
- }, { toolName, canvasState: canvasState || undefined });
1232
+ }, { toolName, canvasState: canvasState || undefined }), timing);
1147
1233
  }
1148
- return withSessionNote(executedResult(code, result.result, result.screenshot, description, result.report), result);
1234
+ // Session item counts come from the governor's own count of what this call
1235
+ // created (gate H3: the session stats read 0 with items on the canvas).
1236
+ const created = result.report?.items?.created;
1237
+ if (typeof created === 'number' && created > 0) {
1238
+ try {
1239
+ getSessionManager().noteItemsCreated(created);
1240
+ }
1241
+ catch { /* no session */ }
1242
+ }
1243
+ return withTiming(withSessionNote(executedResult(code, result.result, result.screenshot, description, result.report), result), timing);
1149
1244
  }
1150
1245
  /**
1151
1246
  * The emitted code's own verdict, when it reported one and it was a failure.
@@ -1703,6 +1798,28 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
1703
1798
  }
1704
1799
  case 'pinepaper_validate_scene': {
1705
1800
  const input = ValidateSceneInputSchema.parse(args);
1801
+ // The validator reads { from, to, relation } on an addRelation op; the
1802
+ // add_relation tool's own names (source, target, relationType) are what
1803
+ // an agent reaches for, and the tool's example used them, so every
1804
+ // example op failed SOURCE_NOT_FOUND "undefined" (gate D8, 1.6.19).
1805
+ if (Array.isArray(input.ops)) {
1806
+ input.ops = input.ops.map((op) => {
1807
+ const o = { ...op };
1808
+ if (o.from === undefined && o.source !== undefined) {
1809
+ o.from = o.source;
1810
+ delete o.source;
1811
+ }
1812
+ if (o.to === undefined && o.target !== undefined) {
1813
+ o.to = o.target;
1814
+ delete o.target;
1815
+ }
1816
+ if (o.relation === undefined && o.relationType !== undefined) {
1817
+ o.relation = o.relationType;
1818
+ delete o.relationType;
1819
+ }
1820
+ return o;
1821
+ });
1822
+ }
1706
1823
  const code = codeGenerator.generateValidateScene(input);
1707
1824
  const description = input.ops ? `Pre-validate ${input.ops.length} proposed op(s)` : 'Audit the live scene';
1708
1825
  return executeOrGenerate(code, description, options, 'pinepaper_validate_scene');
@@ -1770,6 +1887,23 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
1770
1887
  return executeOrGenerate(code, `Text effect: ${input.action}`, options, 'pinepaper_text_effect');
1771
1888
  }
1772
1889
  // ─── 1.6.4: agent surface for the Tier-2 engine features ───
1890
+ case 'pinepaper_relight': {
1891
+ const input = RelightInputSchema.parse(args);
1892
+ return executeOrGenerate(codeGenerator.generateRelight(input), `Relight: ${input.action}`, options, 'pinepaper_relight');
1893
+ }
1894
+ case 'pinepaper_shader_graph': {
1895
+ const input = ShaderGraphInputSchema.parse(args);
1896
+ return executeOrGenerate(codeGenerator.generateShaderGraph(input), `Shader graph: ${input.action}`, options, 'pinepaper_shader_graph');
1897
+ }
1898
+ case 'pinepaper_styled_scene': {
1899
+ const input = StyledSceneInputSchema.parse(args);
1900
+ return executeOrGenerate(codeGenerator.generateStyledScene(input), `Styled scene${input.style ? `: ${input.style}` : ''}`, options, 'pinepaper_styled_scene');
1901
+ }
1902
+ case 'pinepaper_place_on_surface': {
1903
+ const input = PlaceOnSurfaceInputSchema.parse(args);
1904
+ const code = codeGenerator.generatePlaceOnSurface(input);
1905
+ return executeOrGenerate(code, 'Place a UI onto a screen in a photo', options, 'pinepaper_place_on_surface');
1906
+ }
1773
1907
  case 'pinepaper_brand_kit': {
1774
1908
  const input = BrandKitInputSchema.parse(args);
1775
1909
  if (input.action === 'from_url')
@@ -2031,8 +2165,17 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
2031
2165
  // RELATION TOOLS
2032
2166
  // -----------------------------------------------------------------------
2033
2167
  case 'pinepaper_add_relation': {
2034
- const input = AddRelationInputSchema.parse(args);
2035
- const code = codeGenerator.generateAddRelation(input);
2168
+ // A relation registered with register_custom_relation this session is
2169
+ // a valid type here (gate D2, 1.6.19: the enum refused the name just
2170
+ // registered). Only those names; an unknown one is still refused.
2171
+ const rt = args.relationType;
2172
+ // The schema's refinements check presence, not which type, so a custom
2173
+ // name is validated with a built-in stand-in and then put back.
2174
+ const isCustom = typeof rt === 'string' && CUSTOM_RELATION_NAMES.has(rt);
2175
+ const input = AddRelationInputSchema.parse(isCustom ? { ...args, relationType: 'orbits' } : args);
2176
+ if (isCustom)
2177
+ input.relationType = rt;
2178
+ const code = codeGenerator.generateAddRelation(input, isCustom);
2036
2179
  const description = getLocalizedSuccessMessage(i18n, 'relationAdded', {
2037
2180
  relationType: input.relationType ?? `preset:${input.presetId}`,
2038
2181
  sourceId: input.sourceId,
@@ -2056,6 +2199,7 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
2056
2199
  }
2057
2200
  case 'pinepaper_register_custom_relation': {
2058
2201
  const input = RegisterCustomRelationInputSchema.parse(args);
2202
+ CUSTOM_RELATION_NAMES.add(input.name);
2059
2203
  const code = codeGenerator.generateRegisterCustomRelation(input);
2060
2204
  return executeOrGenerate(code, `Registers custom relation type: ${input.name}`, options, 'pinepaper_register_custom_relation');
2061
2205
  }
@@ -2197,10 +2341,9 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
2197
2341
  case 'pinepaper_set_canvas_size': {
2198
2342
  const input = SetCanvasSizeInputSchema.parse(args);
2199
2343
  const code = codeGenerator.generateSetCanvasSize(input);
2200
- const description = getLocalizedSuccessMessage(i18n, 'canvasSizeSet', {
2201
- width: input.width,
2202
- height: input.height,
2203
- });
2344
+ const description = input.preset && input.width === undefined
2345
+ ? `Set canvas size to preset ${input.preset}`
2346
+ : getLocalizedSuccessMessage(i18n, 'canvasSizeSet', { width: input.width ?? '', height: input.height ?? '' });
2204
2347
  return executeOrGenerate(code, description, options, 'pinepaper_set_canvas_size');
2205
2348
  }
2206
2349
  case 'pinepaper_get_canvas_size': {
@@ -2306,6 +2449,15 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
2306
2449
  // -----------------------------------------------------------------------
2307
2450
  case 'pinepaper_add_filter': {
2308
2451
  const input = AddFilterInputSchema.parse(args);
2452
+ // Out-of-range parameters are refused with the engine's own range
2453
+ // (gate A, 1.6.19: dither levels 99 was accepted and quietly clamped).
2454
+ const ranges = FILTER_PARAM_RANGES[input.filterType] ?? {};
2455
+ for (const [k, v] of Object.entries((input.params ?? {}))) {
2456
+ const r = ranges[k];
2457
+ if (r && typeof v === 'number' && (v < r[0] || v > r[1])) {
2458
+ return errorResult(ErrorCodes.VALIDATION_ERROR, `${input.filterType} ${k} must be ${r[0]}..${r[1]} (got ${v}).`, { filterType: input.filterType, param: k, range: r });
2459
+ }
2460
+ }
2309
2461
  const code = codeGenerator.generateAddFilter(input.filterType, input.params);
2310
2462
  const description = `Adds ${input.filterType} filter to the canvas`;
2311
2463
  return executeOrGenerate(code, description, options, 'pinepaper_add_filter');
@@ -2516,7 +2668,19 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
2516
2668
  const perfTracker = getPerformanceTracker();
2517
2669
  const recentMetrics = perfTracker.exportMetrics('json', { limit: metricsLimit });
2518
2670
  const stats = perfTracker.getStats();
2519
- metricsSection = { entries: recentMetrics, stats };
2671
+ // exportMetrics('json') returns a JSON STRING; embedded as-is it reached
2672
+ // the report as one escaped string, not an array (gate H3).
2673
+ let entries = recentMetrics;
2674
+ if (typeof recentMetrics === 'string') {
2675
+ try {
2676
+ entries = JSON.parse(recentMetrics);
2677
+ }
2678
+ catch { /* keep the string */ }
2679
+ }
2680
+ if (entries && typeof entries === 'object' && !Array.isArray(entries) && Array.isArray(entries.metrics)) {
2681
+ entries = entries.metrics;
2682
+ }
2683
+ metricsSection = { entries, stats };
2520
2684
  }
2521
2685
  catch { /* metrics not available */ }
2522
2686
  }
@@ -2571,17 +2735,25 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
2571
2735
  },
2572
2736
  recommendations: [],
2573
2737
  };
2574
- // Add actionable recommendations
2738
+ // RECOMMEND ONLY WHAT SAVES (gate H4, 1.6.19). This said "switch to
2739
+ // agent" on full, which saves under 1% (agent omits three small
2740
+ // tools). Each alternative is measured against the active set and
2741
+ // offered only when it saves at least 10%, with the saving stated.
2575
2742
  const recs = toolDefsSection.recommendations;
2576
- if (toolkit === 'full') {
2577
- recs.push(`Switch to PINEPAPER_TOOLKIT=agent to reduce to ~${agentCompactTokens.toLocaleString()} tokens/turn (agent+compact)`);
2578
- }
2579
- if (verbosity === 'verbose') {
2580
- recs.push(`Set PINEPAPER_VERBOSITY=compact to reduce tool definitions overhead`);
2581
- }
2582
- if (verbosity !== 'minimal') {
2583
- recs.push(`Set PINEPAPER_VERBOSITY=minimal for maximum savings (~${agentMinimalTokens.toLocaleString()} tokens with agent toolkit) — use pinepaper_tool_guide for on-demand docs`);
2743
+ const alternatives = [];
2744
+ for (const v of ['compact', 'minimal']) {
2745
+ for (const k of ['full', 'agent', 'minimal']) {
2746
+ if (k === toolkit && v === verbosity)
2747
+ continue;
2748
+ const t = Math.ceil(Buffer.byteLength(JSON.stringify(getToolsForToolkit(getToolsForVerbosity(v), k)), 'utf-8') / 4);
2749
+ alternatives.push({ label: `${k}+${v}`, how: `PINEPAPER_TOOLKIT=${k} PINEPAPER_VERBOSITY=${v}`, tokens: t });
2750
+ }
2584
2751
  }
2752
+ alternatives
2753
+ .filter((alt) => alt.tokens <= activeTokens * 0.9)
2754
+ .sort((x, y) => x.tokens - y.tokens)
2755
+ .slice(0, 3)
2756
+ .forEach((alt) => recs.push(`${alt.how}: ~${alt.tokens.toLocaleString()} tokens/turn (${Math.round((1 - alt.tokens / activeTokens) * 100)}% less)${alt.label.startsWith('minimal+') ? ' — a small core set; switch profiles at runtime with pinepaper_set_toolkit' : ''}`));
2585
2757
  if (activeTokens > 20000) {
2586
2758
  recs.push(`Tool definitions alone consume ~${activeTokens.toLocaleString()} tokens per API turn — this is a major context budget item`);
2587
2759
  }
@@ -2700,11 +2872,19 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
2700
2872
  // ASSET TOOLS
2701
2873
  // -----------------------------------------------------------------------
2702
2874
  case 'pinepaper_search_assets': {
2875
+ // Asked for by name, a removed repository says why rather than failing
2876
+ // the enum check with a bare "invalid option".
2877
+ {
2878
+ const { REMOVED_REPOSITORIES, REMOVED_REPOSITORY_REASON } = await import('../assets/index.js');
2879
+ if (typeof args.repository === 'string' && REMOVED_REPOSITORIES.includes(args.repository)) {
2880
+ return errorResult(ErrorCodes.INVALID_PARAMS, `${REMOVED_REPOSITORY_REASON} Search repository 'all', 'iconify' or 'fontawesome'.`, { repository: args.repository }, { toolName: 'pinepaper_search_assets' });
2881
+ }
2882
+ }
2703
2883
  const input = SearchAssetsInputSchema.parse(args);
2704
2884
  const { getAssetManager } = await import('../assets/index.js');
2705
2885
  const assetManager = getAssetManager();
2706
2886
  try {
2707
- const results = await assetManager.search(input.query, input.repository || 'all', input.limit || 10);
2887
+ const { results, sources } = await assetManager.searchWithStatus(input.query, input.repository || 'all', input.limit || 10);
2708
2888
  // If includeSvgContent is true, fetch SVG content for each result
2709
2889
  let resultsWithContent = [];
2710
2890
  if (input.includeSvgContent) {
@@ -2776,6 +2956,7 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
2776
2956
  query: input.query,
2777
2957
  repository: input.repository || 'all',
2778
2958
  count: resultsWithContent.length,
2959
+ sources,
2779
2960
  includeSvgContent: input.includeSvgContent || false,
2780
2961
  attributionRequired: requiresAttribution.length > 0,
2781
2962
  attributionNotice,
@@ -2836,6 +3017,15 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
2836
3017
  svg = body;
2837
3018
  metadata = { source: 'url', url: input.url };
2838
3019
  }
3020
+ // `color` was accepted and never applied, so a recoloured icon
3021
+ // rendered black (gate D15, 1.6.19). A monochrome icon draws in
3022
+ // currentColor (stroke sets) or in the default fill (fill sets):
3023
+ // both take the colour.
3024
+ if (input.color) {
3025
+ const c = input.color.replace(/"/g, '');
3026
+ svg = svg.replace(/currentColor/g, c);
3027
+ svg = svg.replace(/<svg\b([^>]*)>/i, (m, attrs) => (/\sfill\s*=/.test(attrs) ? m : `<svg${attrs} fill="${c}">`));
3028
+ }
2839
3029
  // Import the SVG onto canvas using existing import_svg tool
2840
3030
  const code = codeGenerator.generateImportSVG(svg, undefined, input.position, input.scale);
2841
3031
  // A URL import has no catalogue metadata (no title, no licence): this
@@ -3366,10 +3556,18 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
3366
3556
  `Call action 'list_styles' and use one marked composable.`, { style: input.style }, { toolName: 'pinepaper_design_system' });
3367
3557
  }
3368
3558
  const ops = designSystems.sceneToOps(scene);
3559
+ // Some vendored styles lay out at their OWN fixed size and ignore
3560
+ // width/height (swiss_typographic is always 1280x720), so a caller
3561
+ // asking for 1080x1080 got a resized canvas (gate run 3). Say so.
3562
+ const sizeIgnored = (input.width !== undefined && input.width !== scene.width)
3563
+ || (input.height !== undefined && input.height !== scene.height);
3564
+ const sizeWarning = sizeIgnored
3565
+ ? `"${scene.style}" lays out at its own size, ${scene.width}x${scene.height}; the requested ${input.width ?? '?'}x${input.height ?? '?'} was not used`
3566
+ : undefined;
3369
3567
  if (input.draw === false) {
3370
- return dataResult({ scene, ops, drawn: false });
3568
+ return dataResult({ scene, ops, drawn: false, ...(sizeWarning ? { warning: sizeWarning } : {}) });
3371
3569
  }
3372
- const code = codeGenerator.generateDesignCompose(scene, ops, scene.style);
3570
+ const code = codeGenerator.generateDesignCompose(scene, ops, scene.style, sizeWarning);
3373
3571
  return executeOrGenerate(code, `Composed a ${scene.style} scene (${ops.length} items)`, options, 'pinepaper_design_system');
3374
3572
  }
3375
3573
  }
@@ -3387,7 +3585,12 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
3387
3585
  case 'pinepaper_interchange': {
3388
3586
  const input = InterchangeInputSchema.parse(args);
3389
3587
  const code = codeGenerator.generateInterchange(input);
3390
- return executeOrGenerate(code, `Interchange: ${input.action}`, options, 'pinepaper_interchange');
3588
+ const out = await executeOrGenerate(code, `Interchange: ${input.action}`, options, 'pinepaper_interchange');
3589
+ // AN EXPORT IS A FILE (gate D7, 1.6.19): .lottie, GLB and the PNG-sequence
3590
+ // zip came back as byte-index objects or inline data URLs, and a Lottie
3591
+ // JSON can be large. Written out; the result names the path.
3592
+ const ext = { export_lottie: 'json', export_dotlottie: 'lottie', export_glb: 'glb', export_png_sequence: 'zip', export_bvh: 'bvh' };
3593
+ return ext[input.action] ? await writeResultDataToFile(out, ext[input.action]) : out;
3391
3594
  }
3392
3595
  case 'pinepaper_sound': {
3393
3596
  const input = SoundInputSchema.parse(args);