@pinepaper.studio/mcp-server 1.6.18 → 1.6.20

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 (127) 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 +179 -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 +3 -3
  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 +27 -11
  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/ontology/vocabulary.d.ts.map +1 -1
  55. package/dist/ontology/vocabulary.js +8 -0
  56. package/dist/ontology/vocabulary.js.map +1 -1
  57. package/dist/tools/choreograph.d.ts +64 -0
  58. package/dist/tools/choreograph.d.ts.map +1 -0
  59. package/dist/tools/choreograph.js +336 -0
  60. package/dist/tools/choreograph.js.map +1 -0
  61. package/dist/tools/compact-descriptions.d.ts.map +1 -1
  62. package/dist/tools/compact-descriptions.js +16 -15
  63. package/dist/tools/compact-descriptions.js.map +1 -1
  64. package/dist/tools/definitions.d.ts +10 -1
  65. package/dist/tools/definitions.d.ts.map +1 -1
  66. package/dist/tools/definitions.js +705 -150
  67. package/dist/tools/definitions.js.map +1 -1
  68. package/dist/tools/effect-check.d.ts +26 -0
  69. package/dist/tools/effect-check.d.ts.map +1 -0
  70. package/dist/tools/effect-check.js +129 -0
  71. package/dist/tools/effect-check.js.map +1 -0
  72. package/dist/tools/effect-class.d.ts +31 -0
  73. package/dist/tools/effect-class.d.ts.map +1 -0
  74. package/dist/tools/effect-class.js +170 -0
  75. package/dist/tools/effect-class.js.map +1 -0
  76. package/dist/tools/engine-lists.d.ts +20 -5
  77. package/dist/tools/engine-lists.d.ts.map +1 -1
  78. package/dist/tools/engine-lists.js +106 -4
  79. package/dist/tools/engine-lists.js.map +1 -1
  80. package/dist/tools/engine-methods.d.ts.map +1 -1
  81. package/dist/tools/engine-methods.js +22 -1
  82. package/dist/tools/engine-methods.js.map +1 -1
  83. package/dist/tools/engine-surface.d.ts +1 -1
  84. package/dist/tools/engine-surface.d.ts.map +1 -1
  85. package/dist/tools/engine-surface.js +232 -13
  86. package/dist/tools/engine-surface.js.map +1 -1
  87. package/dist/tools/handlers/camera.js +8 -8
  88. package/dist/tools/handlers/camera.js.map +1 -1
  89. package/dist/tools/handlers/export.d.ts.map +1 -1
  90. package/dist/tools/handlers/export.js +84 -5
  91. package/dist/tools/handlers/export.js.map +1 -1
  92. package/dist/tools/handlers/font.js +10 -10
  93. package/dist/tools/handlers/font.js.map +1 -1
  94. package/dist/tools/handlers/generate.d.ts +12 -12
  95. package/dist/tools/handlers/maps.d.ts.map +1 -1
  96. package/dist/tools/handlers/maps.js +23 -3
  97. package/dist/tools/handlers/maps.js.map +1 -1
  98. package/dist/tools/handlers.d.ts +44 -0
  99. package/dist/tools/handlers.d.ts.map +1 -1
  100. package/dist/tools/handlers.js +413 -39
  101. package/dist/tools/handlers.js.map +1 -1
  102. package/dist/tools/index.d.ts +2 -2
  103. package/dist/tools/index.d.ts.map +1 -1
  104. package/dist/tools/index.js +2 -2
  105. package/dist/tools/index.js.map +1 -1
  106. package/dist/tools/minimal-descriptions.d.ts.map +1 -1
  107. package/dist/tools/minimal-descriptions.js +39 -26
  108. package/dist/tools/minimal-descriptions.js.map +1 -1
  109. package/dist/tools/shape-params.d.ts +2 -2
  110. package/dist/tools/shape-params.d.ts.map +1 -1
  111. package/dist/tools/shape-params.js +27 -2
  112. package/dist/tools/shape-params.js.map +1 -1
  113. package/dist/tools/stick-vocabulary.d.ts +1 -1
  114. package/dist/tools/stick-vocabulary.js +1 -1
  115. package/dist/tools/toolkits.d.ts.map +1 -1
  116. package/dist/tools/toolkits.js +11 -0
  117. package/dist/tools/toolkits.js.map +1 -1
  118. package/dist/types/code-generator.d.ts +91 -3
  119. package/dist/types/code-generator.d.ts.map +1 -1
  120. package/dist/types/code-generator.js +1544 -276
  121. package/dist/types/code-generator.js.map +1 -1
  122. package/dist/types/schemas.d.ts +3203 -907
  123. package/dist/types/schemas.d.ts.map +1 -1
  124. package/dist/types/schemas.js +393 -53
  125. package/dist/types/schemas.js.map +1 -1
  126. package/manifest.json +46 -2
  127. package/package.json +4 -3
@@ -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, ChoreographInputSchema, EmitterInputSchema, MorphSequenceInputSchema, OriginalCharacterInputSchema, LookInputSchema, RenderHookInputSchema, 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,8 +27,11 @@ 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
+ import { AsyncLocalStorage } from 'node:async_hooks';
33
+ import { effectClass } from './effect-class.js';
34
+ import { EFFECT_BEFORE_JS, effectAfterJs, namedItemIds, idMayBeGone, NO_CHANGE_NOTE } from './effect-check.js';
32
35
  import { writeFile, mkdir, appendFile, unlink, readFile, stat } from 'node:fs/promises';
33
36
  import { tmpdir } from 'node:os';
34
37
  import { join, extname } from 'node:path';
@@ -37,6 +40,7 @@ import { getBrowserController, resetBrowserController, } from '../browser/puppet
37
40
  import { sameStudioTarget, validateStudioUrl } from '../browser/url-target.js';
38
41
  import { getPerformanceTracker } from '../metrics/index.js';
39
42
  import { formatErrorContext, captureCanvasState } from '../execution/index.js';
43
+ import { FILTER_PARAM_RANGES } from './engine-lists.js';
40
44
  import { getSessionManager } from '../agent/session-manager.js';
41
45
  import { vocabularyHintForPath, validateBatchVocabulary, detectBatchPropertyTypos } from '../ontology/hints.js';
42
46
  import { cameraHandlers } from './handlers/camera.js';
@@ -560,10 +564,37 @@ export function getExecutionMode() {
560
564
  // Default to 'puppeteer' for live browser execution
561
565
  return 'puppeteer';
562
566
  }
567
+ /** Where a generated script travels for a host to record, beside the agent-visible text. */
568
+ /** Relation types registered with register_custom_relation in this server session (gate D2). */
569
+ export const CUSTOM_RELATION_NAMES = new Set();
570
+ /**
571
+ * Tools whose browser work legitimately outlasts the governor's 10 s default.
572
+ * The first detection call downloads and initialises a model, which on a cold
573
+ * page took longer than 10 s and died as a timeout (gate C10, 1.6.19).
574
+ */
575
+ const LONG_GOVERNOR_BUDGET_MS = {
576
+ pinepaper_detect_objects: 120_000,
577
+ pinepaper_extract_object: 120_000,
578
+ };
579
+ export const CODE_META_KEY = 'pinepaper.studio/code';
563
580
  // =============================================================================
564
581
  // RESULT HELPERS
565
582
  // =============================================================================
566
- function successResult(code, description) {
583
+ function successResult(code, description, deferred = false) {
584
+ if (deferred) {
585
+ return {
586
+ content: [{
587
+ type: 'text',
588
+ text: JSON.stringify({
589
+ success: true,
590
+ status: 'recorded',
591
+ step: description || 'canvas step',
592
+ note: 'Recorded on the scene. It runs in the studio when the scene renders or exports, not now; keep building, then render.',
593
+ }, null, 2),
594
+ }],
595
+ _meta: { [CODE_META_KEY]: code },
596
+ };
597
+ }
567
598
  const content = [
568
599
  {
569
600
  type: 'text',
@@ -576,7 +607,7 @@ function successResult(code, description) {
576
607
  text: `\n${description}`,
577
608
  });
578
609
  }
579
- return { content };
610
+ return { content, _meta: { [CODE_META_KEY]: code } };
580
611
  }
581
612
  /**
582
613
  * A plain data answer.
@@ -589,6 +620,29 @@ function successResult(code, description) {
589
620
  function dataResult(value) {
590
621
  return { content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] };
591
622
  }
623
+ /**
624
+ * Where a call's time went (O1, per-request trace). In the result's _meta, the
625
+ * spec's slot for metadata, under one namespaced key, so it adds nothing to
626
+ * the text a model reads. browserMs includes the screenshot when one was taken.
627
+ * index.ts adds toolMs, the whole call, for every tool.
628
+ */
629
+ export const TIMING_META_KEY = 'pinepaper.studio/timing';
630
+ export const EFFECT_META_KEY = 'pinepaper.studio/effect';
631
+ /** The tool and action of the call in flight, for executeOrGenerate's effect check. */
632
+ const callContext = new AsyncLocalStorage();
633
+ let READ_ONLY_TOOLS = null;
634
+ async function readOnlyTools() {
635
+ if (!READ_ONLY_TOOLS) {
636
+ const { PINEPAPER_TOOLS } = await import('./definitions.js');
637
+ READ_ONLY_TOOLS = new Set(PINEPAPER_TOOLS.filter((t) => t.annotations?.readOnlyHint).map((t) => t.name));
638
+ }
639
+ return READ_ONLY_TOOLS;
640
+ }
641
+ export function withTiming(out, t) {
642
+ const meta = (out._meta ?? {});
643
+ const prev = (meta[TIMING_META_KEY] ?? {});
644
+ return { ...out, _meta: { ...meta, [TIMING_META_KEY]: { ...prev, ...t } } };
645
+ }
592
646
  /**
593
647
  * A call that only succeeded after the browser session was recovered says so,
594
648
  * in the result: the controller has reported `recovered` / `canvasReset` for
@@ -601,7 +655,7 @@ function withSessionNote(out, r) {
601
655
  const note = r.relaunched
602
656
  ? `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
657
  : r.canvasReset
604
- ? 'The page had reloaded and the canvas came back EMPTY. Item ids from before are gone: rebuild the scene.'
658
+ ? 'The studio page had reloaded or navigated away, and the canvas came back EMPTY. Item ids from before are gone: rebuild the scene.'
605
659
  : 'The page connection was re-established; the scene survived.';
606
660
  const first = out.content[0];
607
661
  if (first && first.type === 'text') {
@@ -804,6 +858,80 @@ const REMOVE_ITEMS_CREATED_SINCE_MARK = `(function () {
804
858
  * false when the page could not be marked (the rollback is then skipped and
805
859
  * the failure note says nothing about the scene it cannot vouch for).
806
860
  */
861
+ /**
862
+ * The studio's job gate (FxTool D92). PinePaperAgent.startJob is the one place
863
+ * a stale studio reloads, and only when the job is about to clear the canvas:
864
+ * it resolves {reloading: true, fromBuild, toBuild} and the PAGE reloads, so
865
+ * nothing running in it survives — the wait has to happen here. Poll the new
866
+ * page through waitForReady, open the job again, then let start_job's own
867
+ * setup (clear, canvas size) run on the fresh page.
868
+ */
869
+ export async function studioJobGate(input, options, wait = (ms) => new Promise((r) => setTimeout(r, ms))) {
870
+ const mode = options.executionMode ?? getExecutionMode();
871
+ if (mode === 'code' || !options.executeInBrowser || options.deferred)
872
+ return {};
873
+ const controller = options.browserController || getBrowserController();
874
+ if (!controller.connected) {
875
+ try {
876
+ await controller.connect();
877
+ }
878
+ catch {
879
+ return {};
880
+ }
881
+ }
882
+ const args = JSON.stringify({ ...(input.name ? { name: input.name } : {}), clearCanvas: input.clearCanvas !== false });
883
+ const START = `(async function () {
884
+ const A = typeof window !== 'undefined' ? window.PinePaperAgent : null;
885
+ if (!A || !A.startJob) return { legacy: true };
886
+ return await A.startJob(${args});
887
+ })();`;
888
+ const call = async (code) => {
889
+ try {
890
+ const r = await controller.executeCode(code, false, { bypassGovernor: true });
891
+ return r.success ? r.result : null;
892
+ }
893
+ catch {
894
+ return null;
895
+ }
896
+ };
897
+ const first = await call(START);
898
+ if (!first || first.legacy || (first.jobOpen === undefined && first.reloading === undefined))
899
+ return {};
900
+ const build = (j) => ({ buildId: j.buildId, liveBuildId: j.liveBuildId, stale: !!j.stale });
901
+ if (first.reloading) {
902
+ const READY = `(async function () {
903
+ const A = typeof window !== 'undefined' ? window.PinePaperAgent : null;
904
+ if (!A || !A.waitForReady) return { ready: false };
905
+ await A.waitForReady();
906
+ return { ready: true };
907
+ })();`;
908
+ let ready = false;
909
+ for (let i = 0; i < 60 && !ready; i++) { // ~30 s: a reload is ~100 ms plus a fresh boot
910
+ await wait(500);
911
+ const r = await call(READY);
912
+ ready = !!(r && r.ready);
913
+ }
914
+ if (!ready)
915
+ return { warning: `the studio was reloading from build ${first.fromBuild} to ${first.toBuild} and did not come back within 30 s; this job may run on the old page.`, build: build(first) };
916
+ const second = await call(START);
917
+ if (!second)
918
+ return { warning: 'the studio reloaded but did not open the job again.', build: build(first) };
919
+ return {
920
+ note: `Studio was on build ${first.fromBuild}, reloaded to ${first.toBuild} before this job.`,
921
+ ...(second.stale ? { warning: `the studio is still on an old build after reloading (${second.buildId}; live ${second.liveBuildId})${second.reason ? ': ' + second.reason : ''}.` } : {}),
922
+ build: build(second),
923
+ };
924
+ }
925
+ if (first.stale) {
926
+ return {
927
+ warning: input.clearCanvas === false
928
+ ? `studio is on old build ${first.buildId} (live: ${first.liveBuildId}); finish, then start a new job to update.`
929
+ : `studio is on old build ${first.buildId} (live: ${first.liveBuildId}) and was not reloaded${first.reason ? ': ' + first.reason : ''}.`,
930
+ build: build(first),
931
+ };
932
+ }
933
+ return { build: build(first) };
934
+ }
807
935
  async function markItemsBeforeRun(controller) {
808
936
  try {
809
937
  const r = await controller.executeCode(MARK_ITEMS_BEFORE_RUN, false, { bypassGovernor: true });
@@ -894,6 +1022,40 @@ async function brandFromUrl(url, shots, options) {
894
1022
  return errorResult(ErrorCodes.EXECUTION_ERROR, `could not read ${url}: ${e instanceof Error ? e.message : String(e)}`, {}, { toolName: 'pinepaper_brand_kit' });
895
1023
  }
896
1024
  }
1025
+ /**
1026
+ * If an executed result carries `data` (a data URL, or JSON/text), write it to
1027
+ * a file in the export dir and replace it with filePath + bytes.
1028
+ */
1029
+ async function writeResultDataToFile(out, ext) {
1030
+ const first = out.content?.[0];
1031
+ if (out.isError || !first || first.type !== 'text')
1032
+ return out;
1033
+ let obj;
1034
+ try {
1035
+ obj = JSON.parse(first.text);
1036
+ }
1037
+ catch {
1038
+ return out;
1039
+ }
1040
+ const r = obj.result;
1041
+ if (!r || r.data === undefined || r.data === null)
1042
+ return out;
1043
+ let bytes;
1044
+ if (typeof r.data === 'string' && r.data.startsWith('data:'))
1045
+ bytes = Buffer.from(r.data.slice(r.data.indexOf(',') + 1), 'base64');
1046
+ else if (typeof r.data === 'string')
1047
+ bytes = Buffer.from(r.data, 'utf-8');
1048
+ else
1049
+ bytes = Buffer.from(JSON.stringify(r.data), 'utf-8');
1050
+ const dir = getExportDir();
1051
+ await mkdir(dir, { recursive: true });
1052
+ const filePath = join(dir, `pinepaper_${String(r.format ?? 'export').replace(/[^a-z0-9-]/gi, '')}_${Date.now()}.${ext}`);
1053
+ await writeFile(filePath, bytes);
1054
+ const { data: _d, ...rest } = r;
1055
+ void _d;
1056
+ obj.result = { ...rest, filePath, bytes: bytes.length };
1057
+ return { ...out, content: [{ type: 'text', text: JSON.stringify(obj, null, 2) }, ...out.content.slice(1)] };
1058
+ }
897
1059
  /**
898
1060
  * pinepaper_capture_frames with `sheet`: one tiled image of the film, saved
899
1061
  * to a file (an image is never useful inline, see ALWAYS_SAVE_FORMATS), with
@@ -998,7 +1160,7 @@ The code is ready to use. Each subsequent tool call will also generate code you
998
1160
  timestamp: Date.now(),
999
1161
  success: true,
1000
1162
  });
1001
- return successResult(code, description);
1163
+ return successResult(code, description, options.deferred === true);
1002
1164
  }
1003
1165
  const controller = browserController || getBrowserController();
1004
1166
  // Auto-connect in agent mode if not connected
@@ -1081,18 +1243,46 @@ ${code}
1081
1243
  // emitter is not (its verdict already describes what it left behind).
1082
1244
  const rollbackOnFailure = toolName === 'pinepaper_execute_custom_code'
1083
1245
  && await markItemsBeforeRun(controller);
1084
- const result = await controller.executeCode(code, shouldTakeScreenshot, stage ? { stage } : {});
1246
+ const governorTimeoutMs = LONG_GOVERNOR_BUDGET_MS[toolName];
1247
+ // NO SILENT SUCCESS (effect-class.ts / effect-check.ts): fingerprint a
1248
+ // mutating call before and after it runs.
1249
+ const ctx = callContext.getStore();
1250
+ const action = ctx && ctx.toolName === toolName ? ctx.action : undefined;
1251
+ const effect = effectClass(toolName, action, (await readOnlyTools()).has(toolName));
1252
+ const checkEffect = effect === 'mutates' && !rollbackOnFailure;
1253
+ let effectCheckMs = 0;
1254
+ let effectArmed = false;
1255
+ if (checkEffect) {
1256
+ const t0 = Date.now();
1257
+ try {
1258
+ const r = await controller.executeCode(EFFECT_BEFORE_JS, false, { bypassGovernor: true });
1259
+ effectArmed = !!(r.success && r.result?.ok);
1260
+ }
1261
+ catch { /* unchecked this time */ }
1262
+ effectCheckMs += Date.now() - t0;
1263
+ }
1264
+ const result = await controller.executeCode(code, shouldTakeScreenshot, {
1265
+ ...(stage ? { stage } : {}),
1266
+ ...(governorTimeoutMs ? { governorTimeoutMs } : {}),
1267
+ });
1085
1268
  const browserDuration = tracker.endTimer(`${timerId}_browser_execution`);
1269
+ // THE METRIC RECORDS THE CALL'S VERDICT, NOT THE TRANSPORT'S (gate H2,
1270
+ // 1.6.19). result.success only says the code RAN; an emitter that ran and
1271
+ // returned { success: false } is a failed call, and get_performance_metrics
1272
+ // reported "Success Rate: 100.0%" over exactly such a failure.
1273
+ const innerVerdict = result.success ? innerFailure(result.result) : null;
1086
1274
  tracker.recordMetric({
1087
1275
  toolName,
1088
1276
  phase: 'browser_execution',
1089
1277
  duration: browserDuration,
1090
1278
  timestamp: Date.now(),
1091
- success: result.success,
1092
- error: result.error,
1279
+ success: result.success && !innerVerdict,
1280
+ error: result.error || innerVerdict || undefined,
1093
1281
  });
1282
+ let screenshotMs;
1094
1283
  if (shouldTakeScreenshot) {
1095
1284
  const screenshotDuration = tracker.endTimer(`${timerId}_screenshot`);
1285
+ screenshotMs = screenshotDuration;
1096
1286
  tracker.recordMetric({
1097
1287
  toolName,
1098
1288
  phase: 'screenshot',
@@ -1108,13 +1298,14 @@ ${code}
1108
1298
  phase: 'total',
1109
1299
  duration: totalDuration,
1110
1300
  timestamp: Date.now(),
1111
- success: result.success,
1112
- error: result.error,
1301
+ success: result.success && !innerVerdict,
1302
+ error: result.error || innerVerdict || undefined,
1113
1303
  });
1304
+ const timing = { browserMs: Math.round(browserDuration), ...(screenshotMs !== undefined ? { screenshotMs: Math.round(screenshotMs) } : {}) };
1114
1305
  if (!result.success) {
1115
1306
  // Capture canvas state for error context
1116
1307
  const canvasState = await captureCanvasState(controller);
1117
- return errorResult(ErrorCodes.EXECUTION_ERROR, result.error || 'Failed to execute code in browser',
1308
+ return withTiming(errorResult(ErrorCodes.EXECUTION_ERROR, result.error || 'Failed to execute code in browser',
1118
1309
  // errorCode surfaces the governor's structured code (PP_ITEM_BUDGET,
1119
1310
  // PP_LOOP_BUDGET, PP_TIMEOUT) so the model can correct on the next turn.
1120
1311
  {
@@ -1123,7 +1314,7 @@ ${code}
1123
1314
  }, {
1124
1315
  toolName,
1125
1316
  canvasState: canvasState || undefined,
1126
- });
1317
+ }), timing);
1127
1318
  }
1128
1319
  // TWO VERDICTS. `result.success` says the CODE RAN. The emitted code has its
1129
1320
  // own verdict, and almost every emitter in this repo returns
@@ -1137,15 +1328,57 @@ ${code}
1137
1328
  // outer success and the agent kept building on a rig that did not exist.
1138
1329
  // pinepaper_agent_export got this check in 1.6.9; it belongs HERE, where
1139
1330
  // every tool inherits it.
1140
- const inner = innerFailure(result.result);
1331
+ const inner = innerVerdict;
1141
1332
  if (inner) {
1142
1333
  const canvasState = await captureCanvasState(controller);
1143
- return errorResult(ErrorCodes.EXECUTION_ERROR, inner, {
1334
+ // THE ENGINE'S OWN REASON, when it gave one. An emitter's refusal is often
1335
+ // generic ("the studio refused the time_expression relation") while the
1336
+ // engine said exactly why in a warning the governor captured ("unknown
1337
+ // property 'opacty' — did you mean 'opacity'?"; D78). Production strips
1338
+ // console.warn, so this is the only place the model can read it.
1339
+ const said = (Array.isArray(result.report?.warnings) ? result.report.warnings : [])
1340
+ .filter((w) => typeof w === 'string' && w.length > 0 && !inner.includes(w))
1341
+ .slice(0, 3);
1342
+ return withTiming(errorResult(ErrorCodes.EXECUTION_ERROR, said.length ? `${inner} The studio said: ${said.join(' | ')}` : inner, {
1144
1343
  code, result: result.result, governorReport: result.report,
1145
1344
  ...(rollbackOnFailure ? await customCodeRollback(controller, inner) : {}),
1146
- }, { toolName, canvasState: canvasState || undefined });
1345
+ }, { toolName, canvasState: canvasState || undefined }), timing);
1346
+ }
1347
+ // Session item counts come from the governor's own count of what this call
1348
+ // created (gate H3: the session stats read 0 with items on the canvas).
1349
+ const created = result.report?.items?.created;
1350
+ if (typeof created === 'number' && created > 0) {
1351
+ try {
1352
+ getSessionManager().noteItemsCreated(created);
1353
+ }
1354
+ catch { /* no session */ }
1355
+ }
1356
+ let noChangeNote = null;
1357
+ let effectMeta = null;
1358
+ if (checkEffect && effectArmed) {
1359
+ const t0 = Date.now();
1360
+ const ids = idMayBeGone(toolName, action) ? [] : namedItemIds(result.result);
1361
+ const verdict = await controller.executeCode(effectAfterJs(ids), false, { bypassGovernor: true })
1362
+ .then((r) => (r.success ? r.result : null))
1363
+ .catch(() => null);
1364
+ effectCheckMs += Date.now() - t0;
1365
+ if (verdict && verdict.ok) {
1366
+ // A CONTRADICTION, not idempotence: the call names an item that is not there.
1367
+ if (verdict.missing && verdict.missing.length) {
1368
+ return withTiming(errorResult(ErrorCodes.EXECUTION_ERROR, `the call reported ${verdict.missing.join(', ')}, which ${verdict.missing.length === 1 ? 'is' : 'are'} not on the canvas: it did not make what it said it made.`, { result: result.result }, { toolName }), { ...timing, effectCheckMs });
1369
+ }
1370
+ const changed = verdict.changed ?? [];
1371
+ effectMeta = { class: effect, changed: changed.length > 0, signals: changed, checked: verdict.checked ?? [] };
1372
+ if (!changed.length)
1373
+ noChangeNote = NO_CHANGE_NOTE(verdict.checked ?? []);
1374
+ }
1147
1375
  }
1148
- return withSessionNote(executedResult(code, result.result, result.screenshot, description, result.report), result);
1376
+ let out = withSessionNote(executedResult(code, result.result, result.screenshot, description, result.report), result);
1377
+ if (noChangeNote)
1378
+ out = { ...out, content: [...(out.content ?? []), { type: 'text', text: noChangeNote }] };
1379
+ if (effectMeta)
1380
+ out = { ...out, _meta: { ...(out._meta ?? {}), [EFFECT_META_KEY]: effectMeta } };
1381
+ return withTiming(out, checkEffect ? { ...timing, effectCheckMs } : timing);
1149
1382
  }
1150
1383
  /**
1151
1384
  * The emitted code's own verdict, when it reported one and it was a failure.
@@ -1334,7 +1567,7 @@ export function ignoredArgumentsNote(toolName, args) {
1334
1567
  export async function handleToolCall(toolName, args, options = {}) {
1335
1568
  const tracker = getPerformanceTracker();
1336
1569
  const baseTimerId = `${toolName}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
1337
- const result = await handleToolCallInner(toolName, args, options, tracker, baseTimerId);
1570
+ const result = await callContext.run({ toolName, action: typeof args.action === 'string' ? args.action : undefined }, () => handleToolCallInner(toolName, args, options, tracker, baseTimerId));
1338
1571
  const ignoredNote = ignoredArgumentsNote(toolName, args);
1339
1572
  if (ignoredNote)
1340
1573
  result.content = [...(result.content ?? []), { type: 'text', text: ignoredNote }];
@@ -1703,6 +1936,28 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
1703
1936
  }
1704
1937
  case 'pinepaper_validate_scene': {
1705
1938
  const input = ValidateSceneInputSchema.parse(args);
1939
+ // The validator reads { from, to, relation } on an addRelation op; the
1940
+ // add_relation tool's own names (source, target, relationType) are what
1941
+ // an agent reaches for, and the tool's example used them, so every
1942
+ // example op failed SOURCE_NOT_FOUND "undefined" (gate D8, 1.6.19).
1943
+ if (Array.isArray(input.ops)) {
1944
+ input.ops = input.ops.map((op) => {
1945
+ const o = { ...op };
1946
+ if (o.from === undefined && o.source !== undefined) {
1947
+ o.from = o.source;
1948
+ delete o.source;
1949
+ }
1950
+ if (o.to === undefined && o.target !== undefined) {
1951
+ o.to = o.target;
1952
+ delete o.target;
1953
+ }
1954
+ if (o.relation === undefined && o.relationType !== undefined) {
1955
+ o.relation = o.relationType;
1956
+ delete o.relationType;
1957
+ }
1958
+ return o;
1959
+ });
1960
+ }
1706
1961
  const code = codeGenerator.generateValidateScene(input);
1707
1962
  const description = input.ops ? `Pre-validate ${input.ops.length} proposed op(s)` : 'Audit the live scene';
1708
1963
  return executeOrGenerate(code, description, options, 'pinepaper_validate_scene');
@@ -1770,6 +2025,47 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
1770
2025
  return executeOrGenerate(code, `Text effect: ${input.action}`, options, 'pinepaper_text_effect');
1771
2026
  }
1772
2027
  // ─── 1.6.4: agent surface for the Tier-2 engine features ───
2028
+ case 'pinepaper_look': {
2029
+ const input = LookInputSchema.parse(args);
2030
+ return executeOrGenerate(codeGenerator.generateLook(input), `Look: ${input.action}`, options, 'pinepaper_look');
2031
+ }
2032
+ case 'pinepaper_original_character': {
2033
+ const input = OriginalCharacterInputSchema.parse(args);
2034
+ return executeOrGenerate(codeGenerator.generateOriginalCharacter(input), `Original character: ${input.action}`, options, 'pinepaper_original_character');
2035
+ }
2036
+ case 'pinepaper_morph_sequence': {
2037
+ const input = MorphSequenceInputSchema.parse(args);
2038
+ return executeOrGenerate(codeGenerator.generateMorphSequence(input), `Morph sequence: ${input.states.length} states`, options, 'pinepaper_morph_sequence');
2039
+ }
2040
+ case 'pinepaper_emitter': {
2041
+ const input = EmitterInputSchema.parse(args);
2042
+ return executeOrGenerate(codeGenerator.generateEmitter(input), `Emitter: ${input.action}`, options, 'pinepaper_emitter');
2043
+ }
2044
+ case 'pinepaper_render_hook': {
2045
+ const input = RenderHookInputSchema.parse(args);
2046
+ return executeOrGenerate(codeGenerator.generateRenderHook(input), `Render hook: ${input.action}`, options, 'pinepaper_render_hook');
2047
+ }
2048
+ case 'pinepaper_choreograph': {
2049
+ const input = ChoreographInputSchema.parse(args);
2050
+ return executeOrGenerate(codeGenerator.generateChoreograph(input), `Choreograph: ${input.beats.map((b) => b.verb).join(' → ')}`, options, 'pinepaper_choreograph');
2051
+ }
2052
+ case 'pinepaper_relight': {
2053
+ const input = RelightInputSchema.parse(args);
2054
+ return executeOrGenerate(codeGenerator.generateRelight(input), `Relight: ${input.action}`, options, 'pinepaper_relight');
2055
+ }
2056
+ case 'pinepaper_shader_graph': {
2057
+ const input = ShaderGraphInputSchema.parse(args);
2058
+ return executeOrGenerate(codeGenerator.generateShaderGraph(input), `Shader graph: ${input.action}`, options, 'pinepaper_shader_graph');
2059
+ }
2060
+ case 'pinepaper_styled_scene': {
2061
+ const input = StyledSceneInputSchema.parse(args);
2062
+ return executeOrGenerate(codeGenerator.generateStyledScene(input), `Styled scene${input.style ? `: ${input.style}` : ''}`, options, 'pinepaper_styled_scene');
2063
+ }
2064
+ case 'pinepaper_place_on_surface': {
2065
+ const input = PlaceOnSurfaceInputSchema.parse(args);
2066
+ const code = codeGenerator.generatePlaceOnSurface(input);
2067
+ return executeOrGenerate(code, 'Place a UI onto a screen in a photo', options, 'pinepaper_place_on_surface');
2068
+ }
1773
2069
  case 'pinepaper_brand_kit': {
1774
2070
  const input = BrandKitInputSchema.parse(args);
1775
2071
  if (input.action === 'from_url')
@@ -2031,12 +2327,21 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
2031
2327
  // RELATION TOOLS
2032
2328
  // -----------------------------------------------------------------------
2033
2329
  case 'pinepaper_add_relation': {
2034
- const input = AddRelationInputSchema.parse(args);
2035
- const code = codeGenerator.generateAddRelation(input);
2330
+ // A relation registered with register_custom_relation this session is
2331
+ // a valid type here (gate D2, 1.6.19: the enum refused the name just
2332
+ // registered). Only those names; an unknown one is still refused.
2333
+ const rt = args.relationType;
2334
+ // The schema's refinements check presence, not which type, so a custom
2335
+ // name is validated with a built-in stand-in and then put back.
2336
+ const isCustom = typeof rt === 'string' && CUSTOM_RELATION_NAMES.has(rt);
2337
+ const input = AddRelationInputSchema.parse(isCustom ? { ...args, relationType: 'orbits' } : args);
2338
+ if (isCustom)
2339
+ input.relationType = rt;
2340
+ const code = codeGenerator.generateAddRelation(input, isCustom);
2036
2341
  const description = getLocalizedSuccessMessage(i18n, 'relationAdded', {
2037
2342
  relationType: input.relationType ?? `preset:${input.presetId}`,
2038
2343
  sourceId: input.sourceId,
2039
- targetId: input.targetId || 'self',
2344
+ targetId: input.targetId || '(no target)',
2040
2345
  });
2041
2346
  return executeOrGenerate(code, description, options, 'pinepaper_add_relation');
2042
2347
  }
@@ -2056,6 +2361,7 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
2056
2361
  }
2057
2362
  case 'pinepaper_register_custom_relation': {
2058
2363
  const input = RegisterCustomRelationInputSchema.parse(args);
2364
+ CUSTOM_RELATION_NAMES.add(input.name);
2059
2365
  const code = codeGenerator.generateRegisterCustomRelation(input);
2060
2366
  return executeOrGenerate(code, `Registers custom relation type: ${input.name}`, options, 'pinepaper_register_custom_relation');
2061
2367
  }
@@ -2197,10 +2503,9 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
2197
2503
  case 'pinepaper_set_canvas_size': {
2198
2504
  const input = SetCanvasSizeInputSchema.parse(args);
2199
2505
  const code = codeGenerator.generateSetCanvasSize(input);
2200
- const description = getLocalizedSuccessMessage(i18n, 'canvasSizeSet', {
2201
- width: input.width,
2202
- height: input.height,
2203
- });
2506
+ const description = input.preset && input.width === undefined
2507
+ ? `Set canvas size to preset ${input.preset}`
2508
+ : getLocalizedSuccessMessage(i18n, 'canvasSizeSet', { width: input.width ?? '', height: input.height ?? '' });
2204
2509
  return executeOrGenerate(code, description, options, 'pinepaper_set_canvas_size');
2205
2510
  }
2206
2511
  case 'pinepaper_get_canvas_size': {
@@ -2306,6 +2611,15 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
2306
2611
  // -----------------------------------------------------------------------
2307
2612
  case 'pinepaper_add_filter': {
2308
2613
  const input = AddFilterInputSchema.parse(args);
2614
+ // Out-of-range parameters are refused with the engine's own range
2615
+ // (gate A, 1.6.19: dither levels 99 was accepted and quietly clamped).
2616
+ const ranges = FILTER_PARAM_RANGES[input.filterType] ?? {};
2617
+ for (const [k, v] of Object.entries((input.params ?? {}))) {
2618
+ const r = ranges[k];
2619
+ if (r && typeof v === 'number' && (v < r[0] || v > r[1])) {
2620
+ return errorResult(ErrorCodes.VALIDATION_ERROR, `${input.filterType} ${k} must be ${r[0]}..${r[1]} (got ${v}).`, { filterType: input.filterType, param: k, range: r });
2621
+ }
2622
+ }
2309
2623
  const code = codeGenerator.generateAddFilter(input.filterType, input.params);
2310
2624
  const description = `Adds ${input.filterType} filter to the canvas`;
2311
2625
  return executeOrGenerate(code, description, options, 'pinepaper_add_filter');
@@ -2516,7 +2830,19 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
2516
2830
  const perfTracker = getPerformanceTracker();
2517
2831
  const recentMetrics = perfTracker.exportMetrics('json', { limit: metricsLimit });
2518
2832
  const stats = perfTracker.getStats();
2519
- metricsSection = { entries: recentMetrics, stats };
2833
+ // exportMetrics('json') returns a JSON STRING; embedded as-is it reached
2834
+ // the report as one escaped string, not an array (gate H3).
2835
+ let entries = recentMetrics;
2836
+ if (typeof recentMetrics === 'string') {
2837
+ try {
2838
+ entries = JSON.parse(recentMetrics);
2839
+ }
2840
+ catch { /* keep the string */ }
2841
+ }
2842
+ if (entries && typeof entries === 'object' && !Array.isArray(entries) && Array.isArray(entries.metrics)) {
2843
+ entries = entries.metrics;
2844
+ }
2845
+ metricsSection = { entries, stats };
2520
2846
  }
2521
2847
  catch { /* metrics not available */ }
2522
2848
  }
@@ -2571,17 +2897,25 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
2571
2897
  },
2572
2898
  recommendations: [],
2573
2899
  };
2574
- // Add actionable recommendations
2900
+ // RECOMMEND ONLY WHAT SAVES (gate H4, 1.6.19). This said "switch to
2901
+ // agent" on full, which saves under 1% (agent omits three small
2902
+ // tools). Each alternative is measured against the active set and
2903
+ // offered only when it saves at least 10%, with the saving stated.
2575
2904
  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`);
2905
+ const alternatives = [];
2906
+ for (const v of ['compact', 'minimal']) {
2907
+ for (const k of ['full', 'agent', 'minimal']) {
2908
+ if (k === toolkit && v === verbosity)
2909
+ continue;
2910
+ const t = Math.ceil(Buffer.byteLength(JSON.stringify(getToolsForToolkit(getToolsForVerbosity(v), k)), 'utf-8') / 4);
2911
+ alternatives.push({ label: `${k}+${v}`, how: `PINEPAPER_TOOLKIT=${k} PINEPAPER_VERBOSITY=${v}`, tokens: t });
2912
+ }
2584
2913
  }
2914
+ alternatives
2915
+ .filter((alt) => alt.tokens <= activeTokens * 0.9)
2916
+ .sort((x, y) => x.tokens - y.tokens)
2917
+ .slice(0, 3)
2918
+ .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
2919
  if (activeTokens > 20000) {
2586
2920
  recs.push(`Tool definitions alone consume ~${activeTokens.toLocaleString()} tokens per API turn — this is a major context budget item`);
2587
2921
  }
@@ -2700,11 +3034,19 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
2700
3034
  // ASSET TOOLS
2701
3035
  // -----------------------------------------------------------------------
2702
3036
  case 'pinepaper_search_assets': {
3037
+ // Asked for by name, a removed repository says why rather than failing
3038
+ // the enum check with a bare "invalid option".
3039
+ {
3040
+ const { REMOVED_REPOSITORIES, REMOVED_REPOSITORY_REASON } = await import('../assets/index.js');
3041
+ if (typeof args.repository === 'string' && REMOVED_REPOSITORIES.includes(args.repository)) {
3042
+ return errorResult(ErrorCodes.INVALID_PARAMS, `${REMOVED_REPOSITORY_REASON} Search repository 'all', 'iconify' or 'fontawesome'.`, { repository: args.repository }, { toolName: 'pinepaper_search_assets' });
3043
+ }
3044
+ }
2703
3045
  const input = SearchAssetsInputSchema.parse(args);
2704
3046
  const { getAssetManager } = await import('../assets/index.js');
2705
3047
  const assetManager = getAssetManager();
2706
3048
  try {
2707
- const results = await assetManager.search(input.query, input.repository || 'all', input.limit || 10);
3049
+ const { results, sources } = await assetManager.searchWithStatus(input.query, input.repository || 'all', input.limit || 10);
2708
3050
  // If includeSvgContent is true, fetch SVG content for each result
2709
3051
  let resultsWithContent = [];
2710
3052
  if (input.includeSvgContent) {
@@ -2776,6 +3118,7 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
2776
3118
  query: input.query,
2777
3119
  repository: input.repository || 'all',
2778
3120
  count: resultsWithContent.length,
3121
+ sources,
2779
3122
  includeSvgContent: input.includeSvgContent || false,
2780
3123
  attributionRequired: requiresAttribution.length > 0,
2781
3124
  attributionNotice,
@@ -2836,6 +3179,15 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
2836
3179
  svg = body;
2837
3180
  metadata = { source: 'url', url: input.url };
2838
3181
  }
3182
+ // `color` was accepted and never applied, so a recoloured icon
3183
+ // rendered black (gate D15, 1.6.19). A monochrome icon draws in
3184
+ // currentColor (stroke sets) or in the default fill (fill sets):
3185
+ // both take the colour.
3186
+ if (input.color) {
3187
+ const c = input.color.replace(/"/g, '');
3188
+ svg = svg.replace(/currentColor/g, c);
3189
+ svg = svg.replace(/<svg\b([^>]*)>/i, (m, attrs) => (/\sfill\s*=/.test(attrs) ? m : `<svg${attrs} fill="${c}">`));
3190
+ }
2839
3191
  // Import the SVG onto canvas using existing import_svg tool
2840
3192
  const code = codeGenerator.generateImportSVG(svg, undefined, input.position, input.scale);
2841
3193
  // A URL import has no catalogue metadata (no title, no licence): this
@@ -2940,11 +3292,20 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
2940
3292
  }
2941
3293
  const code = codeGenerator.generateAgentStartJob(input);
2942
3294
  const descriptionText = `Started agent job${input.name ? ` "${input.name}"` : ''} with ${input.screenshotPolicy || 'on_complete'} screenshot policy`;
3295
+ // A STALE STUDIO IS RELOADED HERE, AND ONLY HERE (D92): the canvas is
3296
+ // about to be cleared anyway. The setup below runs on the new page.
3297
+ const gate = await studioJobGate(input, options);
2943
3298
  const result = await executeOrGenerate(code, descriptionText, options, 'pinepaper_agent_start_job');
2944
3299
  // Append a 1-line workflow hint
2945
3300
  if (result.content && result.content.length > 0 && result.content[0].type === 'text') {
2946
3301
  result.content[0].text += `\n\nNEXT: Call batch_execute with all operations, then end_job.`;
3302
+ if (gate.note)
3303
+ result.content[0].text = `${gate.note}\n\n${result.content[0].text}`;
3304
+ if (gate.warning)
3305
+ result.content[0].text = `⚠️ ${gate.warning}\n\n${result.content[0].text}`;
2947
3306
  }
3307
+ if (gate.build)
3308
+ result._meta = { ...(result._meta ?? {}), 'pinepaper.studio/build': gate.build };
2948
3309
  return result;
2949
3310
  }
2950
3311
  case 'pinepaper_agent_end_job': {
@@ -3366,10 +3727,18 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
3366
3727
  `Call action 'list_styles' and use one marked composable.`, { style: input.style }, { toolName: 'pinepaper_design_system' });
3367
3728
  }
3368
3729
  const ops = designSystems.sceneToOps(scene);
3730
+ // Some vendored styles lay out at their OWN fixed size and ignore
3731
+ // width/height (swiss_typographic is always 1280x720), so a caller
3732
+ // asking for 1080x1080 got a resized canvas (gate run 3). Say so.
3733
+ const sizeIgnored = (input.width !== undefined && input.width !== scene.width)
3734
+ || (input.height !== undefined && input.height !== scene.height);
3735
+ const sizeWarning = sizeIgnored
3736
+ ? `"${scene.style}" lays out at its own size, ${scene.width}x${scene.height}; the requested ${input.width ?? '?'}x${input.height ?? '?'} was not used`
3737
+ : undefined;
3369
3738
  if (input.draw === false) {
3370
- return dataResult({ scene, ops, drawn: false });
3739
+ return dataResult({ scene, ops, drawn: false, ...(sizeWarning ? { warning: sizeWarning } : {}) });
3371
3740
  }
3372
- const code = codeGenerator.generateDesignCompose(scene, ops, scene.style);
3741
+ const code = codeGenerator.generateDesignCompose(scene, ops, scene.style, sizeWarning);
3373
3742
  return executeOrGenerate(code, `Composed a ${scene.style} scene (${ops.length} items)`, options, 'pinepaper_design_system');
3374
3743
  }
3375
3744
  }
@@ -3387,7 +3756,12 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
3387
3756
  case 'pinepaper_interchange': {
3388
3757
  const input = InterchangeInputSchema.parse(args);
3389
3758
  const code = codeGenerator.generateInterchange(input);
3390
- return executeOrGenerate(code, `Interchange: ${input.action}`, options, 'pinepaper_interchange');
3759
+ const out = await executeOrGenerate(code, `Interchange: ${input.action}`, options, 'pinepaper_interchange');
3760
+ // AN EXPORT IS A FILE (gate D7, 1.6.19): .lottie, GLB and the PNG-sequence
3761
+ // zip came back as byte-index objects or inline data URLs, and a Lottie
3762
+ // JSON can be large. Written out; the result names the path.
3763
+ const ext = { export_lottie: 'json', export_dotlottie: 'lottie', export_glb: 'glb', export_png_sequence: 'zip', export_bvh: 'bvh' };
3764
+ return ext[input.action] ? await writeResultDataToFile(out, ext[input.action]) : out;
3391
3765
  }
3392
3766
  case 'pinepaper_sound': {
3393
3767
  const input = SoundInputSchema.parse(args);