@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.
- package/README.de.md +1 -1
- package/README.es.md +1 -1
- package/README.fr.md +1 -1
- package/README.hi.md +1 -1
- package/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +142 -4
- package/README.pt-BR.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/browser/puppeteer-controller.d.ts +61 -1
- package/dist/browser/puppeteer-controller.d.ts.map +1 -1
- package/dist/browser/puppeteer-controller.js +168 -6
- package/dist/browser/puppeteer-controller.js.map +1 -1
- package/dist/build-info.json +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +75 -10
- package/dist/index.js.map +1 -1
- package/dist/ontology/ontology-validator.d.ts.map +1 -1
- package/dist/ontology/ontology-validator.js +9 -5
- package/dist/ontology/ontology-validator.js.map +1 -1
- package/dist/ontology/upstream-vocabulary.d.ts +2 -2
- package/dist/ontology/upstream-vocabulary.js +2 -2
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +198 -47
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/engine-lists.d.ts +20 -0
- package/dist/tools/engine-lists.d.ts.map +1 -0
- package/dist/tools/engine-lists.js +86 -0
- package/dist/tools/engine-lists.js.map +1 -0
- package/dist/tools/engine-methods.d.ts.map +1 -1
- package/dist/tools/engine-methods.js +8 -0
- package/dist/tools/engine-methods.js.map +1 -1
- package/dist/tools/engine-surface.d.ts +1 -1
- package/dist/tools/engine-surface.d.ts.map +1 -1
- package/dist/tools/engine-surface.js +20 -4
- package/dist/tools/engine-surface.js.map +1 -1
- package/dist/tools/handlers/beats.d.ts +64 -0
- package/dist/tools/handlers/beats.d.ts.map +1 -0
- package/dist/tools/handlers/beats.js +83 -0
- package/dist/tools/handlers/beats.js.map +1 -0
- package/dist/tools/handlers/brand-intake.d.ts +95 -0
- package/dist/tools/handlers/brand-intake.d.ts.map +1 -0
- package/dist/tools/handlers/brand-intake.js +242 -0
- package/dist/tools/handlers/brand-intake.js.map +1 -0
- package/dist/tools/handlers.d.ts +23 -11
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +265 -14
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/minimal-descriptions.d.ts.map +1 -1
- package/dist/tools/minimal-descriptions.js +5 -3
- package/dist/tools/minimal-descriptions.js.map +1 -1
- package/dist/tools/shape-params.d.ts +2 -2
- package/dist/tools/shape-params.js +2 -2
- package/dist/tools/stick-vocabulary.d.ts +1 -1
- package/dist/tools/stick-vocabulary.js +1 -1
- package/dist/tools/toolkits.d.ts.map +1 -1
- package/dist/tools/toolkits.js +2 -0
- package/dist/tools/toolkits.js.map +1 -1
- package/dist/types/code-generator.d.ts +63 -2
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +533 -49
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +2199 -640
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +186 -50
- package/dist/types/schemas.js.map +1 -1
- package/dist/utils/a11y.d.ts +2 -0
- package/dist/utils/a11y.d.ts.map +1 -1
- package/dist/utils/a11y.js +1 -1
- package/dist/utils/a11y.js.map +1 -1
- package/dist/utils/beats.d.ts +58 -0
- package/dist/utils/beats.d.ts.map +1 -0
- package/dist/utils/beats.js +110 -0
- package/dist/utils/beats.js.map +1 -0
- package/dist/utils/mcp-log.d.ts +16 -0
- package/dist/utils/mcp-log.d.ts.map +1 -0
- package/dist/utils/mcp-log.js +10 -0
- package/dist/utils/mcp-log.js.map +1 -0
- package/dist/utils/progress.d.ts +28 -0
- package/dist/utils/progress.d.ts.map +1 -0
- package/dist/utils/progress.js +24 -0
- package/dist/utils/progress.js.map +1 -0
- package/dist/utils/svg-size.d.ts +22 -0
- package/dist/utils/svg-size.d.ts.map +1 -0
- package/dist/utils/svg-size.js +43 -0
- package/dist/utils/svg-size.js.map +1 -0
- package/dist/vendor/design/design-style.js +1 -1
- package/manifest.json +9 -1
- package/package.json +7 -4
package/dist/tools/handlers.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
{
|
|
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, {
|
|
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
|
-
|
|
1637
|
-
|
|
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
|
-
|
|
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'}`;
|