@pinepaper.studio/mcp-server 1.6.16 → 1.6.18

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 (90) 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 +142 -4
  8. package/README.pt-BR.md +1 -1
  9. package/README.zh-CN.md +1 -1
  10. package/dist/browser/puppeteer-controller.d.ts +61 -1
  11. package/dist/browser/puppeteer-controller.d.ts.map +1 -1
  12. package/dist/browser/puppeteer-controller.js +168 -6
  13. package/dist/browser/puppeteer-controller.js.map +1 -1
  14. package/dist/build-info.json +2 -2
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +75 -10
  18. package/dist/index.js.map +1 -1
  19. package/dist/ontology/ontology-validator.d.ts.map +1 -1
  20. package/dist/ontology/ontology-validator.js +9 -5
  21. package/dist/ontology/ontology-validator.js.map +1 -1
  22. package/dist/ontology/upstream-vocabulary.d.ts +2 -2
  23. package/dist/ontology/upstream-vocabulary.js +2 -2
  24. package/dist/tools/definitions.d.ts.map +1 -1
  25. package/dist/tools/definitions.js +198 -47
  26. package/dist/tools/definitions.js.map +1 -1
  27. package/dist/tools/engine-lists.d.ts +20 -0
  28. package/dist/tools/engine-lists.d.ts.map +1 -0
  29. package/dist/tools/engine-lists.js +86 -0
  30. package/dist/tools/engine-lists.js.map +1 -0
  31. package/dist/tools/engine-methods.d.ts.map +1 -1
  32. package/dist/tools/engine-methods.js +8 -0
  33. package/dist/tools/engine-methods.js.map +1 -1
  34. package/dist/tools/engine-surface.d.ts +1 -1
  35. package/dist/tools/engine-surface.d.ts.map +1 -1
  36. package/dist/tools/engine-surface.js +20 -4
  37. package/dist/tools/engine-surface.js.map +1 -1
  38. package/dist/tools/handlers/beats.d.ts +64 -0
  39. package/dist/tools/handlers/beats.d.ts.map +1 -0
  40. package/dist/tools/handlers/beats.js +83 -0
  41. package/dist/tools/handlers/beats.js.map +1 -0
  42. package/dist/tools/handlers/brand-intake.d.ts +95 -0
  43. package/dist/tools/handlers/brand-intake.d.ts.map +1 -0
  44. package/dist/tools/handlers/brand-intake.js +242 -0
  45. package/dist/tools/handlers/brand-intake.js.map +1 -0
  46. package/dist/tools/handlers.d.ts +23 -11
  47. package/dist/tools/handlers.d.ts.map +1 -1
  48. package/dist/tools/handlers.js +265 -14
  49. package/dist/tools/handlers.js.map +1 -1
  50. package/dist/tools/minimal-descriptions.d.ts.map +1 -1
  51. package/dist/tools/minimal-descriptions.js +5 -3
  52. package/dist/tools/minimal-descriptions.js.map +1 -1
  53. package/dist/tools/shape-params.d.ts +2 -2
  54. package/dist/tools/shape-params.js +2 -2
  55. package/dist/tools/stick-vocabulary.d.ts +1 -1
  56. package/dist/tools/stick-vocabulary.js +1 -1
  57. package/dist/tools/toolkits.d.ts.map +1 -1
  58. package/dist/tools/toolkits.js +2 -0
  59. package/dist/tools/toolkits.js.map +1 -1
  60. package/dist/types/code-generator.d.ts +63 -2
  61. package/dist/types/code-generator.d.ts.map +1 -1
  62. package/dist/types/code-generator.js +533 -49
  63. package/dist/types/code-generator.js.map +1 -1
  64. package/dist/types/schemas.d.ts +2199 -640
  65. package/dist/types/schemas.d.ts.map +1 -1
  66. package/dist/types/schemas.js +186 -50
  67. package/dist/types/schemas.js.map +1 -1
  68. package/dist/utils/a11y.d.ts +2 -0
  69. package/dist/utils/a11y.d.ts.map +1 -1
  70. package/dist/utils/a11y.js +1 -1
  71. package/dist/utils/a11y.js.map +1 -1
  72. package/dist/utils/beats.d.ts +58 -0
  73. package/dist/utils/beats.d.ts.map +1 -0
  74. package/dist/utils/beats.js +110 -0
  75. package/dist/utils/beats.js.map +1 -0
  76. package/dist/utils/mcp-log.d.ts +16 -0
  77. package/dist/utils/mcp-log.d.ts.map +1 -0
  78. package/dist/utils/mcp-log.js +10 -0
  79. package/dist/utils/mcp-log.js.map +1 -0
  80. package/dist/utils/progress.d.ts +28 -0
  81. package/dist/utils/progress.d.ts.map +1 -0
  82. package/dist/utils/progress.js +24 -0
  83. package/dist/utils/progress.js.map +1 -0
  84. package/dist/utils/svg-size.d.ts +22 -0
  85. package/dist/utils/svg-size.d.ts.map +1 -0
  86. package/dist/utils/svg-size.js +43 -0
  87. package/dist/utils/svg-size.js.map +1 -0
  88. package/dist/vendor/design/design-style.js +1 -1
  89. package/manifest.json +9 -1
  90. package/package.json +7 -4
@@ -5,8 +5,8 @@
5
5
  * Supports i18n for localized error and success messages.
6
6
  * Supports browser execution via Puppeteer.
7
7
  */
8
- import { codeGenerator } 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, ComposeInputSchema, CropImageInputSchema, PathOpInputSchema, MotionInputSchema, SoundInputSchema, InterchangeInputSchema, StickInputSchema, DesignSystemInputSchema, StoryInputSchema, ChromaKeyInputSchema, RiggingInputSchema, GroupInputSchema, CameraDirectorInputSchema, DetectObjectsInputSchema, ExtractObjectInputSchema, ArrangeInputSchema, CreateSceneInputSchema, GetPerformanceMetricsInputSchema, DiagnosticReportInputSchema, SearchAssetsInputSchema, ImportAssetInputSchema, P5DrawInputSchema,
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,
10
10
  // Diagram schemas
11
11
  CreateDiagramShapeInputSchema, ConnectInputSchema, ConnectPortsInputSchema, AddPortsInputSchema, AutoLayoutInputSchema, GetDiagramShapesInputSchema, UpdateConnectorInputSchema, RemoveConnectorInputSchema, DiagramModeInputSchema,
12
12
  // Agent flow mode schemas
@@ -47,6 +47,7 @@ import { ontologyHandlers } from './handlers/ontology.js';
47
47
  import * as designSystems from '../design/design-systems.js';
48
48
  import { exportHandlers } from './handlers/export.js';
49
49
  import { generateHandlers } from './handlers/generate.js';
50
+ import { beatHandlers } from './handlers/beats.js';
50
51
  import { planCharacter, generateCharacterCode } from './handlers/character.js';
51
52
  import { buildZip } from '../utils/zip.js';
52
53
  import { buildScc } from '../utils/scc.js';
@@ -66,6 +67,7 @@ function getDomainHandlers() {
66
67
  ontologyHandlers,
67
68
  exportHandlers,
68
69
  generateHandlers,
70
+ beatHandlers,
69
71
  ];
70
72
  }
71
73
  /**
@@ -83,13 +85,19 @@ export function getScreenshotMode() {
83
85
  // =============================================================================
84
86
  // EXPORT FILE SAVE HELPERS
85
87
  // =============================================================================
86
- function getExportDir() {
88
+ export function getExportDir() {
87
89
  return process.env.PINEPAPER_EXPORT_DIR || join(tmpdir(), 'pinepaper-exports');
88
90
  }
89
91
  // wav joins them: a minute of 48kHz 16-bit is ~5.8 MB of base64 and the ten
90
92
  // minutes the schema allows is ~77 MB. Deliverable across the bridge, useless
91
93
  // pasted into a response.
92
- export const ALWAYS_SAVE_FORMATS = new Set(['mp4', 'webm', 'gif', 'apng', 'pdf', 'wav', 'srt', 'vtt', 'scc', 'html5-ad', 'playable']);
94
+ // Stills join them too. The threshold below is in CHARACTERS of base64, and a
95
+ // 59 KB 1920x1080 PNG is ~79k of them: under the threshold, so it came back
96
+ // inline, and over the token limit of the client reading it (a Halloween
97
+ // look-frame session, 2026-09-29), while bigger PNGs saved to a file. Same
98
+ // format, two shapes, decided by file size. An image is never useful as
99
+ // base64 in a text result, so png/jpg/webp always save and name the path.
100
+ export const ALWAYS_SAVE_FORMATS = new Set(['mp4', 'webm', 'gif', 'apng', 'pdf', 'wav', 'srt', 'vtt', 'scc', 'html5-ad', 'playable', 'png', 'jpg', 'webp']);
93
101
  // 500_000 was chosen against the bridge's limits, not the CALLER's. A pilot
94
102
  // session hit a 263K-character end_job result — comfortably under this, so it
95
103
  // was returned inline, and over the tool-result limit of the client reading
@@ -581,6 +589,33 @@ function successResult(code, description) {
581
589
  function dataResult(value) {
582
590
  return { content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] };
583
591
  }
592
+ /**
593
+ * A call that only succeeded after the browser session was recovered says so,
594
+ * in the result: the controller has reported `recovered` / `canvasReset` for
595
+ * a while, but nothing read them, so an agent kept using item ids from a
596
+ * scene that was gone (M6).
597
+ */
598
+ function withSessionNote(out, r) {
599
+ if (!r.recovered)
600
+ return out;
601
+ const note = r.relaunched
602
+ ? `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
+ : r.canvasReset
604
+ ? 'The page had reloaded and the canvas came back EMPTY. Item ids from before are gone: rebuild the scene.'
605
+ : 'The page connection was re-established; the scene survived.';
606
+ const first = out.content[0];
607
+ if (first && first.type === 'text') {
608
+ try {
609
+ const obj = JSON.parse(first.text);
610
+ obj.session = { recovered: true, ...(r.relaunched ? { relaunched: true } : {}), ...(r.canvasReset !== undefined ? { canvasReset: r.canvasReset } : {}), note };
611
+ return { ...out, content: [{ type: 'text', text: JSON.stringify(obj, null, 2) }, ...out.content.slice(1)] };
612
+ }
613
+ catch {
614
+ return { ...out, content: [{ type: 'text', text: `${note}\n\n${first.text}` }, ...out.content.slice(1)] };
615
+ }
616
+ }
617
+ return out;
618
+ }
584
619
  function executedResult(code, result, screenshot, description, report) {
585
620
  const resultObj = { success: true, result };
586
621
  if (description)
@@ -738,6 +773,170 @@ function handleValidationError(error, i18n) {
738
773
  *
739
774
  * Default is 'on_request' for optimal performance per best practices.
740
775
  */
776
+ /** Page code: remember the id of every item on the canvas. */
777
+ const MARK_ITEMS_BEFORE_RUN = `(function () {
778
+ if (typeof paper === 'undefined' || !paper.project) return { ok: false };
779
+ const ids = new Set();
780
+ paper.project.getItems({}).forEach(function (i) { ids.add(i.id); });
781
+ window.__ppPreRunIds = ids;
782
+ return { ok: true };
783
+ })();`;
784
+ /** Page code: remove every item that was not there at the mark, outermost first. */
785
+ const REMOVE_ITEMS_CREATED_SINCE_MARK = `(function () {
786
+ const pre = window.__ppPreRunIds;
787
+ window.__ppPreRunIds = null;
788
+ if (!pre || typeof paper === 'undefined' || !paper.project) return { ok: false };
789
+ const fresh = paper.project.getItems({}).filter(function (i) { return !pre.has(i.id); });
790
+ const freshIds = new Set(fresh.map(function (i) { return i.id; }));
791
+ const tops = fresh.filter(function (i) { return !(i.parent && freshIds.has(i.parent.id)); });
792
+ let removed = 0;
793
+ tops.forEach(function (it) {
794
+ try { if (typeof app !== 'undefined' && typeof app.deleteItem === 'function' && app.deleteItem(it)) { removed++; return; } } catch (e) {}
795
+ try { if (it.parent) { it.remove(); removed++; } } catch (e) {}
796
+ });
797
+ const left = paper.project.getItems({}).filter(function (i) { return !pre.has(i.id); }).length;
798
+ try { if (paper.view) paper.view.update(); } catch (e) {}
799
+ return { ok: left === 0, removed: removed, left: left };
800
+ })();`;
801
+ /**
802
+ * Record which canvas items exist before agent-written code runs, so a failed
803
+ * run's leftovers can be told apart from the scene it started with. Returns
804
+ * false when the page could not be marked (the rollback is then skipped and
805
+ * the failure note says nothing about the scene it cannot vouch for).
806
+ */
807
+ async function markItemsBeforeRun(controller) {
808
+ try {
809
+ const r = await controller.executeCode(MARK_ITEMS_BEFORE_RUN, false, { bypassGovernor: true });
810
+ return !!(r.success && r.result?.ok);
811
+ }
812
+ catch {
813
+ return false;
814
+ }
815
+ }
816
+ /**
817
+ * Remove what agent-written code created before it failed, and say so.
818
+ *
819
+ * A run stopped by the loop deadline after drawing 42 strokes left them on
820
+ * the canvas: a half-painted scene. Undo cannot fix that. The governor's one
821
+ * history entry only covers mutations that announce themselves, and raw
822
+ * `new paper.Path(...)` does not (measured: report.mutated false, 30 paths
823
+ * left behind). So the rollback is by identity instead: every item that was
824
+ * not on the canvas before the call goes, through app.deleteItem where the
825
+ * engine knows the item (registry, trackers) and item.remove() otherwise.
826
+ * Edits to items that already existed are NOT reverted; the note says so.
827
+ */
828
+ export async function customCodeRollback(controller, error) {
829
+ let rolledBack = false;
830
+ let removed = 0;
831
+ let left = -1;
832
+ try {
833
+ const r = await controller.executeCode(REMOVE_ITEMS_CREATED_SINCE_MARK, false, { bypassGovernor: true });
834
+ const v = (r.result || {});
835
+ rolledBack = !!(r.success && v.ok);
836
+ removed = v.removed ?? 0;
837
+ left = typeof v.left === 'number' ? v.left : -1;
838
+ }
839
+ catch {
840
+ rolledBack = false;
841
+ }
842
+ const scene = rolledBack
843
+ ? (removed > 0
844
+ ? `The ${removed} item(s) this call created before it stopped were removed. Changes it made to items that already existed are not reverted.`
845
+ : 'This call created no items before it stopped. Changes it made to items that already existed are not reverted.')
846
+ : `Items this call created could not all be removed${left > 0 ? ` (${left} remain)` : ''}: check the canvas before continuing.`;
847
+ const how = /Synchronous execution exceeded|Loop budget exceeded/i.test(error || '')
848
+ ? ' This is execute_custom_code\'s per-call guard, not a limit on the scene: its loops may run about 4 s (and 2,000,000 iterations per loop) so the studio tab stays responsive. Split the work across several calls, each of which gets a fresh budget. For example, paint 10 strokes per call instead of 40 in one.'
849
+ : '';
850
+ return { rolledBack, note: scene + how };
851
+ }
852
+ /**
853
+ * pinepaper_brand_kit from_url: the product's real brand, read from its own
854
+ * site (see handlers/brand-intake.ts). Runs in the MCP server's browser, in an
855
+ * isolated context, so code mode cannot do it and says so.
856
+ */
857
+ async function brandFromUrl(url, shots, options) {
858
+ const effectiveExecMode = options.executionMode ?? getExecutionMode();
859
+ if (effectiveExecMode === 'code' || !options.executeInBrowser) {
860
+ return errorResult(ErrorCodes.EXECUTION_ERROR, 'brand_kit from_url reads the site in the MCP server\'s browser, so it needs browser mode; it cannot run as generated code.', {}, { toolName: 'pinepaper_brand_kit' });
861
+ }
862
+ const controller = options.browserController || getBrowserController();
863
+ if (!controller.connected) {
864
+ try {
865
+ await controller.connect();
866
+ }
867
+ catch (e) {
868
+ return errorResult(ErrorCodes.BROWSER_NOT_CONNECTED, `could not start the browser: ${e instanceof Error ? e.message : String(e)}`, {}, { toolName: 'pinepaper_brand_kit' });
869
+ }
870
+ }
871
+ const host = (() => { try {
872
+ return new URL(url).hostname.replace(/[^a-z0-9.-]/gi, '_');
873
+ }
874
+ catch {
875
+ return 'site';
876
+ } })();
877
+ const outDir = join(getExportDir(), `brand-${host}-${Date.now()}`);
878
+ try {
879
+ const { intakeBrand } = await import('./handlers/brand-intake.js');
880
+ const r = await controller
881
+ .withIsolatedPage((page) => intakeBrand(page, url, outDir, shots));
882
+ const text = [
883
+ `Brand read from ${url}. Files in ${outDir}:`,
884
+ ...r.screenshots.map((f) => `- screenshot: ${f}`),
885
+ r.logo ? `- logo: ${r.logo.file} (from ${r.logo.source})` : '- logo: none found in the header, icons or og:image',
886
+ '',
887
+ 'Review the proposed kit against the screenshots, then apply it with pinepaper_brand_kit { action: "apply", kit } (or "plan" first). Import the logo or a screenshot with pinepaper_import_image { url: <file> }.',
888
+ '',
889
+ JSON.stringify({ success: true, ...r }, null, 2),
890
+ ].join('\n');
891
+ return { content: [{ type: 'text', text }] };
892
+ }
893
+ catch (e) {
894
+ return errorResult(ErrorCodes.EXECUTION_ERROR, `could not read ${url}: ${e instanceof Error ? e.message : String(e)}`, {}, { toolName: 'pinepaper_brand_kit' });
895
+ }
896
+ }
897
+ /**
898
+ * pinepaper_capture_frames with `sheet`: one tiled image of the film, saved
899
+ * to a file (an image is never useful inline, see ALWAYS_SAVE_FORMATS), with
900
+ * the rubric the agent scores it against. Code mode returns the code as usual.
901
+ */
902
+ async function captureSheet(input, options) {
903
+ const code = codeGenerator.generateCaptureSheet(input);
904
+ const description = 'Contact sheet';
905
+ const effectiveExecMode = options.executionMode ?? getExecutionMode();
906
+ if (effectiveExecMode === 'code' || !options.executeInBrowser) {
907
+ return executeOrGenerate(code, description, options, 'pinepaper_capture_frames');
908
+ }
909
+ const controller = options.browserController || getBrowserController();
910
+ if (!controller.connected) {
911
+ try {
912
+ await controller.connect();
913
+ }
914
+ catch {
915
+ return executeOrGenerate(code, description, options, 'pinepaper_capture_frames');
916
+ }
917
+ }
918
+ // Frames wait for video seeks one at a time, so 48 of them can outlast the
919
+ // governor's 10 s default.
920
+ const run = await controller.executeCode(code, false, { governorTimeoutMs: 120_000 });
921
+ const value = (run.result ?? {});
922
+ if (!run.success || value.success === false || typeof value.dataUrl !== 'string') {
923
+ return errorResult(ErrorCodes.EXECUTION_ERROR, run.error || value.error || 'the contact sheet could not be made', { code, governorReport: run.report }, { toolName: 'pinepaper_capture_frames' });
924
+ }
925
+ const format = input.sheet?.format === 'png' ? 'png' : 'jpg';
926
+ const { filePath, fileSize } = await saveExportToFile(value.dataUrl, format, 'sheet');
927
+ const { dataUrl: _d, success: _s, ...facts } = value;
928
+ void _d;
929
+ void _s;
930
+ const result = { success: true, filePath, fileSize, format, ...facts, review: SHEET_RUBRIC };
931
+ return {
932
+ content: [{
933
+ type: 'text',
934
+ text: `Contact sheet saved: ${filePath} (${(fileSize / 1024).toFixed(1)} KB, ${String(facts.tiles)} tiles).\n`
935
+ + 'Open the image and score it against review.criteria (1-10 each), then check review.checklist.\n\n'
936
+ + JSON.stringify(result, null, 2),
937
+ }],
938
+ };
939
+ }
741
940
  export async function executeOrGenerate(code, description, options, toolName,
742
941
  /**
743
942
  * Values too large to live inside the generated code. Written to
@@ -878,6 +1077,10 @@ ${code}
878
1077
  if (shouldTakeScreenshot) {
879
1078
  tracker.startTimer(`${timerId}_screenshot`);
880
1079
  }
1080
+ // Agent-written code is rolled back when it fails part-way; a tool's own
1081
+ // emitter is not (its verdict already describes what it left behind).
1082
+ const rollbackOnFailure = toolName === 'pinepaper_execute_custom_code'
1083
+ && await markItemsBeforeRun(controller);
881
1084
  const result = await controller.executeCode(code, shouldTakeScreenshot, stage ? { stage } : {});
882
1085
  const browserDuration = tracker.endTimer(`${timerId}_browser_execution`);
883
1086
  tracker.recordMetric({
@@ -914,7 +1117,10 @@ ${code}
914
1117
  return errorResult(ErrorCodes.EXECUTION_ERROR, result.error || 'Failed to execute code in browser',
915
1118
  // errorCode surfaces the governor's structured code (PP_ITEM_BUDGET,
916
1119
  // PP_LOOP_BUDGET, PP_TIMEOUT) so the model can correct on the next turn.
917
- { code, errorCode: result.errorCode, governorReport: result.report }, {
1120
+ {
1121
+ code, errorCode: result.errorCode, governorReport: result.report,
1122
+ ...(rollbackOnFailure ? await customCodeRollback(controller, result.error) : {}),
1123
+ }, {
918
1124
  toolName,
919
1125
  canvasState: canvasState || undefined,
920
1126
  });
@@ -934,9 +1140,12 @@ ${code}
934
1140
  const inner = innerFailure(result.result);
935
1141
  if (inner) {
936
1142
  const canvasState = await captureCanvasState(controller);
937
- return errorResult(ErrorCodes.EXECUTION_ERROR, inner, { code, result: result.result, governorReport: result.report }, { toolName, canvasState: canvasState || undefined });
1143
+ return errorResult(ErrorCodes.EXECUTION_ERROR, inner, {
1144
+ code, result: result.result, governorReport: result.report,
1145
+ ...(rollbackOnFailure ? await customCodeRollback(controller, inner) : {}),
1146
+ }, { toolName, canvasState: canvasState || undefined });
938
1147
  }
939
- return executedResult(code, result.result, result.screenshot, description, result.report);
1148
+ return withSessionNote(executedResult(code, result.result, result.screenshot, description, result.report), result);
940
1149
  }
941
1150
  /**
942
1151
  * The emitted code's own verdict, when it reported one and it was a failure.
@@ -1093,6 +1302,8 @@ const IGNORED_ARGUMENT_SCHEMAS = {
1093
1302
  // common slip here (round 7 retest, 1.63).
1094
1303
  pinepaper_create_item: CreateItemInputSchema,
1095
1304
  pinepaper_modify_item: ModifyItemInputSchema,
1305
+ // Its options live in `options` / `spec`; a key beside them was dropped.
1306
+ pinepaper_sound: SoundInputSchema,
1096
1307
  };
1097
1308
  const IGNORED_ARGUMENT_HINTS = {
1098
1309
  pinepaper_create_item: Object.fromEntries(['width', 'height', 'radius', 'color', 'fillColor', 'strokeColor', 'strokeWidth', 'content', 'fontSize', 'fontFamily', 'opacity', 'rotation', 'x', 'y']
@@ -1498,8 +1709,10 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
1498
1709
  }
1499
1710
  case 'pinepaper_capture_frames': {
1500
1711
  const input = CaptureFramesInputSchema.parse(args);
1712
+ if (input.sheet)
1713
+ return captureSheet(input, options);
1501
1714
  const code = codeGenerator.generateCaptureFrames(input);
1502
- const description = `Deterministic capture of ${input.times.length} frame(s)`;
1715
+ const description = `Deterministic capture of ${(input.times ?? []).length} frame(s)`;
1503
1716
  return executeOrGenerate(code, description, options, 'pinepaper_capture_frames');
1504
1717
  }
1505
1718
  case 'pinepaper_instantiate_ontology': {
@@ -1559,6 +1772,8 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
1559
1772
  // ─── 1.6.4: agent surface for the Tier-2 engine features ───
1560
1773
  case 'pinepaper_brand_kit': {
1561
1774
  const input = BrandKitInputSchema.parse(args);
1775
+ if (input.action === 'from_url')
1776
+ return brandFromUrl(input.url, input.screenshots ?? 3, options);
1562
1777
  const code = codeGenerator.generateBrandKit(input);
1563
1778
  return executeOrGenerate(code, `Brand kit: ${input.action}`, options, 'pinepaper_brand_kit');
1564
1779
  }
@@ -1615,6 +1830,11 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
1615
1830
  const code = codeGenerator.generateAudioBeats({ ...input, source: staged ? `__ppStage:${stageKey}` : source });
1616
1831
  return executeOrGenerate(code, `Audio: ${input.action}`, options, 'pinepaper_audio_beats', staged ? { [stageKey]: source } : undefined);
1617
1832
  }
1833
+ case 'pinepaper_query_mutations': {
1834
+ const input = QueryMutationsInputSchema.parse(args);
1835
+ const code = generateQueryMutationsCode(input.itemId, input.property, input.range, input.fps);
1836
+ return executeOrGenerate(code, `Keyframe series: ${input.itemId}`, options, 'pinepaper_query_mutations');
1837
+ }
1618
1838
  case 'pinepaper_accessibility_check': {
1619
1839
  const input = AccessibilityCheckInputSchema.parse(args);
1620
1840
  const code = codeGenerator.generateAccessibilityCheck(input);
@@ -1632,9 +1852,23 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
1632
1852
  const report = {};
1633
1853
  const page = parseCssColor(m.page);
1634
1854
  if (m.texts) {
1855
+ // Samples over time: each text judged at every moment it showed,
1856
+ // reported at its worst. Nothing visible at any sampled time is not
1857
+ // a pass — it was a vacuous one (round 12, 12.9).
1635
1858
  const c = checkContrast(m.texts, page);
1636
- report.contrast = { pass: c.failing.length === 0, checked: c.checked, failing: c.failing, skipped: c.skipped,
1637
- method: 'WCAG 1.4.3 AA (4.5:1, 3:1 for large text) between each text fill and the solid fill of the topmost item under its centre, or the page background' + (c.pageAssumed ? ' (none set, so white was assumed)' : '') + '. Approximate: text over a photo or gradient is not judged.' };
1859
+ const worst = new Map();
1860
+ for (const f of c.failing) {
1861
+ const prev = worst.get(f.id);
1862
+ if (!prev || f.ratio < prev.ratio)
1863
+ worst.set(f.id, f);
1864
+ }
1865
+ const ids = new Set(m.texts.filter((t) => t.fill).map((t) => t.id));
1866
+ const skippedIds = [...new Map(c.skipped.map((x) => [x.id, x])).values()];
1867
+ report.contrast = ids.size === 0
1868
+ ? { pass: null, checked: 0, sampledTimes: m.sampledTimes, skipped: skippedIds,
1869
+ note: 'no text was visible at any sampled time (0 s, each text\'s lifetime, keyframe times, every second of duration), so contrast was not checked — pass the scene duration, or check a frame where the text shows.' }
1870
+ : { pass: worst.size === 0, checked: ids.size, sampledTimes: m.sampledTimes, failing: [...worst.values()], skipped: skippedIds,
1871
+ method: 'WCAG 1.4.3 AA (4.5:1, 3:1 for large text) between each text fill and the solid fill of the topmost item under its centre, or the page background' + (c.pageAssumed ? ' (none set, so white was assumed)' : '') + ', at every sampled time the text showed; the worst is reported. Approximate: text over a photo or gradient is not judged.' };
1638
1872
  }
1639
1873
  if (m.flashUnavailable)
1640
1874
  report.flash = { checked: false, reason: m.flashUnavailable };
@@ -1644,7 +1878,9 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
1644
1878
  frames: m.series.length, fps: m.fps,
1645
1879
  method: 'WCAG 2.3.1 general flash: opposing luminance changes of >= 10% with the darker state below 0.8, over a 3 x 3 grid; more than 3 flashes in any second fails. A heuristic screen, not a certified PSE test (red flashes are not checked).' };
1646
1880
  }
1647
- const pass = Object.values(report).every((r) => r.pass !== false);
1881
+ // A check that could not run (pass: null) is not a pass.
1882
+ const verdicts = Object.values(report).map((r) => r.pass);
1883
+ const pass = verdicts.some((v) => v === false) ? false : verdicts.some((v) => v === null) ? null : true;
1648
1884
  return dataResult({ success: true, pass, ...report });
1649
1885
  }
1650
1886
  case 'pinepaper_render_batch': {
@@ -1982,8 +2218,13 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
1982
2218
  return errorResult(ErrorCodes.BROWSER_NOT_CONNECTED, 'Not connected to PinePaper Studio. Call pinepaper_browser_connect first.');
1983
2219
  }
1984
2220
  try {
1985
- // Refresh the page
1986
- await controller.refreshPage();
2221
+ // Refresh the page (recovers a dead session first; M6)
2222
+ const refreshed = await controller.refreshPage();
2223
+ const sizeNote = refreshed.relaunched
2224
+ ? (refreshed.canvasSizeRestored
2225
+ ? `\n\nThe browser session had been lost, so the browser was relaunched. The canvas size was restored to ${refreshed.canvasSizeRestored.width}x${refreshed.canvasSizeRestored.height}.`
2226
+ : '\n\nThe browser session had been lost, so the browser was relaunched at the default canvas size: set the size again if you need another.')
2227
+ : '';
1987
2228
  // Wait a moment for the page to stabilize
1988
2229
  await new Promise(resolve => setTimeout(resolve, 2000));
1989
2230
  // Take a screenshot to show the result
@@ -1995,7 +2236,7 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
1995
2236
 
1996
2237
  The canvas is now completely empty. The welcome template will not appear (it only shows for first-time visitors).
1997
2238
 
1998
- You can now start creating new items on a clean canvas.`,
2239
+ You can now start creating new items on a clean canvas.${sizeNote}`,
1999
2240
  },
2000
2241
  ];
2001
2242
  if (screenshot) {
@@ -2904,6 +3145,16 @@ You can now start creating new items on a clean canvas.`,
2904
3145
  }
2905
3146
  }
2906
3147
  case 'pinepaper_agent_export': {
3148
+ // Formats a DOOH / projection deliverable asks for that no browser
3149
+ // encoder can make (round 12, 12.12): named, with the route that works,
3150
+ // not a generic enum error.
3151
+ const askedFormat = String(args.format ?? '').toLowerCase();
3152
+ if (/^(prores|prores4444|prores422|hap|hapq|hap-alpha|dnxhd|dnxhr|cineform)$/.test(askedFormat)) {
3153
+ return errorResult(ErrorCodes.INVALID_PARAMS, `${askedFormat} is not supported: the studio encodes in the browser, which has no ${askedFormat.startsWith('hap') ? 'HAP' : askedFormat.startsWith('prores') ? 'ProRes' : askedFormat.toUpperCase()} encoder. `
3154
+ + 'Export a lossless png sequence (pinepaper_interchange export_png_sequence, transparent if you need alpha) and convert it, e.g. '
3155
+ + (askedFormat.startsWith('hap') ? 'ffmpeg -i frame_%05d.png -c:v hap -format hap_q out.mov' : 'ffmpeg -i frame_%05d.png -c:v prores_ks -profile:v 4444 out.mov')
3156
+ + ' — or webm / apng for alpha in the browser.');
3157
+ }
2907
3158
  const input = AgentExportInputSchema.parse(args);
2908
3159
  const code = codeGenerator.generateAgentExport(input);
2909
3160
  const description = `Smart export for ${input.platform} as ${input.format || 'auto'}`;