@pinepaper.studio/mcp-server 1.6.22 → 1.6.24
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 +115 -5
- package/README.pt-BR.md +1 -1
- package/README.zh-CN.md +1 -1
- package/SECURITY.md +46 -0
- package/dist/agent/types.d.ts +114 -106
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/browser/puppeteer-controller.js +2 -2
- package/dist/browser/puppeteer-controller.js.map +1 -1
- package/dist/build-info.json +2 -2
- package/dist/design/design-systems.d.ts +50 -0
- package/dist/design/design-systems.d.ts.map +1 -1
- package/dist/design/design-systems.js +220 -35
- package/dist/design/design-systems.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -2
- package/dist/index.js.map +1 -1
- package/dist/ontology/hints.d.ts.map +1 -1
- package/dist/ontology/hints.js +5 -1
- package/dist/ontology/hints.js.map +1 -1
- package/dist/ontology/upstream-vocabulary.d.ts +1 -1
- package/dist/ontology/upstream-vocabulary.js +1 -1
- package/dist/ontology/vocabulary.d.ts.map +1 -1
- package/dist/ontology/vocabulary.js +1 -0
- package/dist/ontology/vocabulary.js.map +1 -1
- package/dist/tools/batch-macros.d.ts +27 -0
- package/dist/tools/batch-macros.d.ts.map +1 -0
- package/dist/tools/batch-macros.js +157 -0
- package/dist/tools/batch-macros.js.map +1 -0
- package/dist/tools/choreograph.d.ts +4 -0
- package/dist/tools/choreograph.d.ts.map +1 -1
- package/dist/tools/choreograph.js +12 -2
- package/dist/tools/choreograph.js.map +1 -1
- package/dist/tools/compact-descriptions.js +3 -3
- package/dist/tools/compact-descriptions.js.map +1 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +122 -15
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/engine-lists.d.ts +2 -2
- package/dist/tools/engine-lists.js +2 -2
- package/dist/tools/engine-methods.d.ts.map +1 -1
- package/dist/tools/engine-methods.js +3 -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 +52 -6
- package/dist/tools/engine-surface.js.map +1 -1
- package/dist/tools/export-file-check.d.ts +31 -0
- package/dist/tools/export-file-check.d.ts.map +1 -0
- package/dist/tools/export-file-check.js +103 -0
- package/dist/tools/export-file-check.js.map +1 -0
- package/dist/tools/export-file-receipt.d.ts +56 -0
- package/dist/tools/export-file-receipt.d.ts.map +1 -0
- package/dist/tools/export-file-receipt.js +151 -0
- package/dist/tools/export-file-receipt.js.map +1 -0
- package/dist/tools/handlers/beats.d.ts +7 -44
- package/dist/tools/handlers/beats.d.ts.map +1 -1
- package/dist/tools/handlers/export.d.ts.map +1 -1
- package/dist/tools/handlers/export.js +31 -6
- package/dist/tools/handlers/export.js.map +1 -1
- package/dist/tools/handlers/generate.d.ts +41 -270
- package/dist/tools/handlers/generate.d.ts.map +1 -1
- package/dist/tools/handlers.d.ts +84 -1
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +389 -82
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/lane.d.ts +34 -0
- package/dist/tools/lane.d.ts.map +1 -0
- package/dist/tools/lane.js +210 -0
- package/dist/tools/lane.js.map +1 -0
- package/dist/tools/minimal-descriptions.d.ts.map +1 -1
- package/dist/tools/minimal-descriptions.js +3 -2
- package/dist/tools/minimal-descriptions.js.map +1 -1
- package/dist/tools/nested-ignored.d.ts +16 -0
- package/dist/tools/nested-ignored.d.ts.map +1 -0
- package/dist/tools/nested-ignored.js +83 -0
- package/dist/tools/nested-ignored.js.map +1 -0
- package/dist/tools/shape-params.d.ts +2 -2
- package/dist/tools/shape-params.d.ts.map +1 -1
- package/dist/tools/shape-params.js +9 -2
- package/dist/tools/shape-params.js.map +1 -1
- package/dist/tools/stick-vocabulary.d.ts +4 -4
- package/dist/tools/stick-vocabulary.d.ts.map +1 -1
- package/dist/tools/stick-vocabulary.js +8 -4
- package/dist/tools/stick-vocabulary.js.map +1 -1
- package/dist/tools/timeline.d.ts +136 -0
- package/dist/tools/timeline.d.ts.map +1 -0
- package/dist/tools/timeline.js +1036 -0
- package/dist/tools/timeline.js.map +1 -0
- package/dist/tools/toolkits.d.ts +1 -1
- package/dist/tools/toolkits.d.ts.map +1 -1
- package/dist/tools/toolkits.js +12 -2
- package/dist/tools/toolkits.js.map +1 -1
- package/dist/types/code-generator.d.ts +66 -3
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +629 -96
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +5278 -24999
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +93 -40
- package/dist/types/schemas.js.map +1 -1
- package/manifest.json +5 -1
- package/package.json +8 -7
package/dist/tools/handlers.js
CHANGED
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
* Supports i18n for localized error and success messages.
|
|
6
6
|
* Supports browser execution via Puppeteer.
|
|
7
7
|
*/
|
|
8
|
+
import { nestedIgnoredPaths } from './nested-ignored.js';
|
|
8
9
|
import { CRAFT_FRAME_JS, generateDrawWeave, generateDrawKnots, generateDrawSashiko, generateDrawSubdivision, generateUiKit, uiKitCallProblems, } from './craft-tools.js';
|
|
9
10
|
import { codeGenerator, generateQueryMutationsCode, SHEET_RUBRIC } from '../types/code-generator.js';
|
|
11
|
+
import { expandBatchMacros, BatchMacroError } from './batch-macros.js';
|
|
10
12
|
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,
|
|
11
13
|
// Diagram schemas
|
|
12
14
|
CreateDiagramShapeInputSchema, ConnectInputSchema, ConnectPortsInputSchema, AddPortsInputSchema, AutoLayoutInputSchema, GetDiagramShapesInputSchema, UpdateConnectorInputSchema, RemoveConnectorInputSchema, DiagramModeInputSchema,
|
|
@@ -26,12 +28,14 @@ ImportImageInputSchema,
|
|
|
26
28
|
// Tool guide schema
|
|
27
29
|
// Ontology schemas
|
|
28
30
|
// Scene management schemas
|
|
29
|
-
ManageScenesInputSchema, ScenePlaybackInputSchema, SceneGraphInputSchema, SequenceInputSchema, StaggerInputSchema, FlipInputSchema,
|
|
31
|
+
ManageScenesInputSchema, ScenePlaybackInputSchema, SceneGraphInputSchema, SequenceInputSchema, StaggerInputSchema, PatternInputSchema, FlipInputSchema,
|
|
30
32
|
// New consolidated tool schemas
|
|
31
33
|
SelectionInputSchema, TransformInputSchema, HistoryInputSchema, ImageFilterInputSchema, LassoInputSchema, CutoutStyleInputSchema, PrecompInputSchema, BackgroundInputSchema, QueryInputSchema, QueryCapabilitiesInputSchema, DeformInputSchema, HandDrawnInputSchema, PenFigureInputSchema, CreateCharacterInputSchema, CharacterActionInputSchema, CharacterPoseInputSchema, SpriteSheetInputSchema, InteractionInputSchema, ExportWidgetInputSchema, ExportWidgetHtmlInputSchema, CreateChartInputSchema, MagicInputSchema, PhysicsInputSchema, MeasurementInputSchema, PlaceOnSurfaceInputSchema, DrawWeaveInputSchema, DrawKnotsInputSchema, DrawSashikoInputSchema, DrawSubdivisionInputSchema, UiKitInputSchema, StyledSceneInputSchema, ErrorCodes, } from '../types/schemas.js';
|
|
32
34
|
import { ZodError, z } from 'zod';
|
|
33
35
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
34
36
|
import { effectClass } from './effect-class.js';
|
|
37
|
+
import { exportFileReceipt } from './export-file-receipt.js';
|
|
38
|
+
import { ExportStepError, errorText, verifySavedFile, writeStep, bytesForSave } from './export-file-check.js';
|
|
35
39
|
import { EFFECT_BEFORE_JS, effectAfterJs, namedItemIds, idMayBeGone, NO_CHANGE_NOTE } from './effect-check.js';
|
|
36
40
|
import { writeFile, mkdir, appendFile, unlink, readFile, stat } from 'node:fs/promises';
|
|
37
41
|
import { tmpdir } from 'node:os';
|
|
@@ -44,6 +48,7 @@ import { formatErrorContext, captureCanvasState } from '../execution/index.js';
|
|
|
44
48
|
import { FILTER_PARAM_RANGES } from './engine-lists.js';
|
|
45
49
|
import { getSessionManager } from '../agent/session-manager.js';
|
|
46
50
|
import { vocabularyHintForPath, validateBatchVocabulary, detectBatchPropertyTypos } from '../ontology/hints.js';
|
|
51
|
+
import { compileBatchTimelines } from './timeline.js';
|
|
47
52
|
import { cameraHandlers } from './handlers/camera.js';
|
|
48
53
|
import { fontHandlers } from './handlers/font.js';
|
|
49
54
|
import { toolGuideHandlers } from './handlers/tool-guide.js';
|
|
@@ -372,12 +377,15 @@ async function saveAdExport(r, input, platform) {
|
|
|
372
377
|
const html = buildHtml5Ad(r.adPage, opts);
|
|
373
378
|
const zip = buildZip([{ name: 'index.html', data: html }]);
|
|
374
379
|
files.zip = join(exportDir, `${stamp}.zip`);
|
|
375
|
-
await writeFile(files.zip, zip);
|
|
380
|
+
await writeStep(files.zip, () => writeFile(files.zip, zip));
|
|
381
|
+
await verifySavedFile(files.zip, zip.length);
|
|
376
382
|
uploadBytes = zip.length;
|
|
377
383
|
// The backup image is uploaded BESIDE the zip, not inside it.
|
|
378
384
|
if (r.backupImage && r.backupImage.startsWith('data:')) {
|
|
379
385
|
files.backupImage = join(exportDir, `${stamp}_backup.png`);
|
|
380
|
-
|
|
386
|
+
const backup = bytesForSave(r.backupImage, 'the backup image');
|
|
387
|
+
await writeStep(files.backupImage, () => writeFile(files.backupImage, backup));
|
|
388
|
+
await verifySavedFile(files.backupImage, backup.length);
|
|
381
389
|
}
|
|
382
390
|
else {
|
|
383
391
|
warnings.push({ code: 'no_backup_image', message: 'no backup image was made (the studio could not capture a frame); display networks require one — export format "png" at the same size.' });
|
|
@@ -386,8 +394,9 @@ async function saveAdExport(r, input, platform) {
|
|
|
386
394
|
else {
|
|
387
395
|
const html = buildPlayable(r.adPage, opts);
|
|
388
396
|
files.html = join(exportDir, `${stamp}.html`);
|
|
389
|
-
await writeFile(files.html, html, 'utf-8');
|
|
397
|
+
await writeStep(files.html, () => writeFile(files.html, html, 'utf-8'));
|
|
390
398
|
uploadBytes = Buffer.byteLength(html, 'utf-8');
|
|
399
|
+
await verifySavedFile(files.html, uploadBytes);
|
|
391
400
|
}
|
|
392
401
|
// 150 KB is Google Ads' display HTML5 figure; some uploads allow 600 KB, and
|
|
393
402
|
// playable networks set their own — so it is a default, not a rule.
|
|
@@ -416,22 +425,26 @@ async function saveAdExport(r, input, platform) {
|
|
|
416
425
|
}
|
|
417
426
|
async function saveExportToFile(data, format, platform) {
|
|
418
427
|
const exportDir = getExportDir();
|
|
419
|
-
await mkdir(exportDir, { recursive: true });
|
|
428
|
+
await writeStep(exportDir, () => mkdir(exportDir, { recursive: true }));
|
|
420
429
|
const ext = getFileExtension(format);
|
|
421
430
|
const timestamp = Date.now();
|
|
422
431
|
const fileName = `pinepaper_${platform}_${timestamp}.${ext}`;
|
|
423
432
|
const filePath = join(exportDir, fileName);
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
433
|
+
// A data URL is decoded (base64 or percent-encoded); plain text (SVG, captions) is written as UTF-8. Nothing to
|
|
434
|
+
// write is the `export` or `save` step's failure, named — it used to write a 0-byte file and report it saved.
|
|
435
|
+
let buffer;
|
|
436
|
+
try {
|
|
437
|
+
buffer = bytesForSave(data, `the ${format} export`);
|
|
429
438
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
439
|
+
catch (e) {
|
|
440
|
+
if (e instanceof ExportStepError)
|
|
441
|
+
throw new ExportStepError(e.step, filePath, e.reason);
|
|
442
|
+
throw e;
|
|
434
443
|
}
|
|
444
|
+
await writeStep(filePath, () => writeFile(filePath, buffer));
|
|
445
|
+
// Read back: success is a file that exists and holds what was written.
|
|
446
|
+
await verifySavedFile(filePath, buffer.length);
|
|
447
|
+
return { filePath, fileSize: buffer.length };
|
|
435
448
|
}
|
|
436
449
|
/**
|
|
437
450
|
* Page a RETAINED export out of the studio's export store into a local file.
|
|
@@ -459,15 +472,16 @@ async function streamRetainedExportToFile(controller, retained, platform) {
|
|
|
459
472
|
let offset = 0;
|
|
460
473
|
let chunks = 0;
|
|
461
474
|
// Start from empty: appendFile would otherwise extend a same-millisecond file.
|
|
462
|
-
await writeFile(filePath, Buffer.alloc(0));
|
|
475
|
+
await writeStep(filePath, () => writeFile(filePath, Buffer.alloc(0)));
|
|
463
476
|
// Returns the error rather than throwing it, so `throw await abandon(...)`
|
|
464
477
|
// reads as a throw to the type checker and narrows what follows.
|
|
465
|
-
|
|
478
|
+
// Paging the bytes out is the `save` step; a refusal from the file system is the `write` step.
|
|
479
|
+
const abandon = async (why, step = 'save') => {
|
|
466
480
|
try {
|
|
467
481
|
await unlink(filePath);
|
|
468
482
|
}
|
|
469
483
|
catch { /* nothing written yet */ }
|
|
470
|
-
return new
|
|
484
|
+
return new ExportStepError(step, filePath,
|
|
471
485
|
// The promise now names the call that keeps it. It was true before and
|
|
472
486
|
// unreachable: reading a held export mutates nothing, so "it can be paged
|
|
473
487
|
// again" was always correct and there was no door to page it through.
|
|
@@ -546,7 +560,12 @@ async function streamRetainedExportToFile(controller, retained, platform) {
|
|
|
546
560
|
const buf = Buffer.from(chunk.data || '', 'base64');
|
|
547
561
|
if (buf.length === 0 && chunk.eof !== true)
|
|
548
562
|
throw await abandon('the studio returned an empty chunk before the end of the file');
|
|
549
|
-
|
|
563
|
+
try {
|
|
564
|
+
await appendFile(filePath, buf);
|
|
565
|
+
}
|
|
566
|
+
catch (writeError) {
|
|
567
|
+
throw await abandon(`appending ${buf.length} bytes at offset ${offset} failed: ${errorText(writeError, 'the file system gave no reason')}`, 'write');
|
|
568
|
+
}
|
|
550
569
|
offset += buf.length;
|
|
551
570
|
chunks++;
|
|
552
571
|
if (chunk.eof === true)
|
|
@@ -557,6 +576,16 @@ async function streamRetainedExportToFile(controller, retained, platform) {
|
|
|
557
576
|
if (offset !== retained.size) {
|
|
558
577
|
throw await abandon(`reassembled ${offset} bytes but the studio reported ${retained.size}`);
|
|
559
578
|
}
|
|
579
|
+
// An empty export is not a saved file, and neither is one the disk does not hold: read it back BEFORE the
|
|
580
|
+
// studio's copy is released, so a failure here still leaves the bytes recoverable.
|
|
581
|
+
if (offset === 0)
|
|
582
|
+
throw await abandon('the studio held an empty export (0 bytes): there is nothing to save');
|
|
583
|
+
try {
|
|
584
|
+
await verifySavedFile(filePath, offset);
|
|
585
|
+
}
|
|
586
|
+
catch (verifyError) {
|
|
587
|
+
throw await abandon(errorText(verifyError instanceof ExportStepError ? verifyError.reason : verifyError, 'the written file could not be read back'), 'write');
|
|
588
|
+
}
|
|
560
589
|
// Only now is it safe to drop the studio's copy.
|
|
561
590
|
try {
|
|
562
591
|
await controller.executeCode(codeGenerator.generateReleaseExport(retained.exportId), false);
|
|
@@ -685,10 +714,40 @@ function withSessionNote(out, r) {
|
|
|
685
714
|
}
|
|
686
715
|
return out;
|
|
687
716
|
}
|
|
717
|
+
export const RECEIPTS_META_KEY = 'pinepaper.studio/receipts';
|
|
718
|
+
/** The engine run's receipts (report.receipts: { calls, byDoor, notes }), when any call has something to say. */
|
|
719
|
+
export function engineReceipts(report) {
|
|
720
|
+
const r = report && typeof report === 'object' ? report.receipts : undefined;
|
|
721
|
+
if (!r || !Array.isArray(r.notes) || r.notes.length === 0)
|
|
722
|
+
return null;
|
|
723
|
+
return { calls: Number(r.calls) || 0, byDoor: r.byDoor ?? {}, notes: r.notes };
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* agent_export answered from the export's own result and never read the run's report (AR-1 R2): an export's engine
|
|
727
|
+
* warnings and receipts are now on the reply like every other tool's — a text line an agent reads, and _meta.
|
|
728
|
+
*/
|
|
729
|
+
export function withEngineReport(result, report) {
|
|
730
|
+
if (!report || result.isError)
|
|
731
|
+
return result;
|
|
732
|
+
const receipts = engineReceipts(report);
|
|
733
|
+
const engineWarnings = Array.isArray(report.engineWarnings) ? report.engineWarnings.filter((w) => w && typeof w === 'object') : [];
|
|
734
|
+
if (!receipts && !engineWarnings.length)
|
|
735
|
+
return result;
|
|
736
|
+
const body = { ...(receipts ? { receipts } : {}), ...(engineWarnings.length ? { engineWarnings } : {}) };
|
|
737
|
+
return {
|
|
738
|
+
...result,
|
|
739
|
+
content: [...(result.content ?? []), { type: 'text', text: `ENGINE RECEIPTS: ${JSON.stringify(body)}` }],
|
|
740
|
+
_meta: { ...(result._meta ?? {}), [RECEIPTS_META_KEY]: body },
|
|
741
|
+
};
|
|
742
|
+
}
|
|
688
743
|
function executedResult(code, result, screenshot, description, report) {
|
|
689
744
|
const resultObj = { success: true, result };
|
|
690
745
|
if (description)
|
|
691
746
|
resultObj.description = description;
|
|
747
|
+
// The scene lint after the code ran, when there is something to say.
|
|
748
|
+
const lint = report && Array.isArray(report.lintWarnings) ? report.lintWarnings : [];
|
|
749
|
+
if (lint.length)
|
|
750
|
+
resultObj.lintWarnings = lint;
|
|
692
751
|
// Surface the governor's model-actionable feedback (app.runGenerated report):
|
|
693
752
|
// warnings (e.g. "500 items created one-by-one — prefer a pattern"), item count,
|
|
694
753
|
// and guard trips. Omitted entirely on pre-governor FxTool builds (no report).
|
|
@@ -711,6 +770,11 @@ function executedResult(code, result, screenshot, description, report) {
|
|
|
711
770
|
governor.loopGuardTripped = true;
|
|
712
771
|
if (Object.keys(governor).length)
|
|
713
772
|
resultObj.governor = governor;
|
|
773
|
+
// AR-1 R2: what each engine call did with what it was given (FxTool js/core/Receipt.js) — only when a call
|
|
774
|
+
// renamed, fell back, dropped or warned; a run whose calls all applied as asked adds nothing.
|
|
775
|
+
const receipts = engineReceipts(report);
|
|
776
|
+
if (receipts)
|
|
777
|
+
resultObj.receipts = receipts;
|
|
714
778
|
}
|
|
715
779
|
// THE CODE ECHO WAS 90% OF THE RESPONSE, AND THE AGENT WROTE THE REQUEST.
|
|
716
780
|
//
|
|
@@ -777,6 +841,44 @@ function redactGeneratedCode(details) {
|
|
|
777
841
|
},
|
|
778
842
|
};
|
|
779
843
|
}
|
|
844
|
+
/**
|
|
845
|
+
* The studio's scene-lint warnings on an export (FxTool lintScene), one line each, so a centred title that runs
|
|
846
|
+
* out of the frame ("IGNAL" for "SIGNAL") is read on the success path rather than left in the JSON. Empty when none.
|
|
847
|
+
*/
|
|
848
|
+
export function lintLines(exportResult) {
|
|
849
|
+
const w = exportResult?.lintWarnings;
|
|
850
|
+
if (!Array.isArray(w) || !w.length)
|
|
851
|
+
return '';
|
|
852
|
+
return w.map((x) => `LINT: ${String(x?.message ?? x?.code ?? 'scene lint warning')}\n`).join('');
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* The file as ffprobe read it, when it disagrees with what the studio encoded: one line per field. A summary
|
|
856
|
+
* line when they agree; nothing when there is no summary.
|
|
857
|
+
*/
|
|
858
|
+
export function probeLines(receipt) {
|
|
859
|
+
const p = receipt.probe;
|
|
860
|
+
if (!p)
|
|
861
|
+
return '';
|
|
862
|
+
const head = `Probe: ${p.container} ${p.codec} ${p.width}x${p.height}${p.fps ? ` @ ${p.fps} fps` : ''}, ${p.durationS} s, ${p.frames} frame${p.frames === 1 ? '' : 's'}`;
|
|
863
|
+
const c = receipt.probeCheck;
|
|
864
|
+
if (!c)
|
|
865
|
+
return `${head}\n`;
|
|
866
|
+
if (c.tool === null)
|
|
867
|
+
return `${head} (not read back: ${c.reason})\n`;
|
|
868
|
+
if (c.ok)
|
|
869
|
+
return `${head} (ffprobe agrees)\n`;
|
|
870
|
+
if (!c.mismatches?.length)
|
|
871
|
+
return `${head}\nPROBE: ${c.reason ?? 'ffprobe could not confirm the file'}\n`;
|
|
872
|
+
return `${head}\n` + c.mismatches.map((m) => `PROBE MISMATCH: ${m.field} — the studio encoded ${JSON.stringify(m.studio)}, ffprobe reads ${JSON.stringify(m.file)}\n`).join('');
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* An export that failed at a step, as an error result: `{ step, path, reason }` in the data and in the words.
|
|
876
|
+
* A non-ExportStepError is named at `fallbackStep` with its own message, never "undefined".
|
|
877
|
+
*/
|
|
878
|
+
function exportStepErrorResult(e, fallbackStep, extra = {}, toolName = 'pinepaper_agent_export') {
|
|
879
|
+
const err = e instanceof ExportStepError ? e : new ExportStepError(fallbackStep, null, errorText(e, `the ${fallbackStep} step failed with no reason given`));
|
|
880
|
+
return errorResult(ErrorCodes.EXECUTION_ERROR, err.message, { ...err.toData(), ...extra }, { toolName });
|
|
881
|
+
}
|
|
780
882
|
export function errorResult(code, message, rawDetails, context) {
|
|
781
883
|
// THE GENERATED SCRIPT IS NOT THE ERROR.
|
|
782
884
|
//
|
|
@@ -811,15 +913,19 @@ export function errorResult(code, message, rawDetails, context) {
|
|
|
811
913
|
isError: true,
|
|
812
914
|
};
|
|
813
915
|
}
|
|
814
|
-
function handleValidationError(error, i18n) {
|
|
916
|
+
function handleValidationError(error, i18n, args) {
|
|
815
917
|
const message = i18n
|
|
816
918
|
? i18n.getError('validationError', { message: 'Invalid input parameters' })
|
|
817
919
|
: 'Invalid input parameters';
|
|
818
|
-
const details = error.
|
|
920
|
+
const details = error.issues.map((e) => {
|
|
819
921
|
const path = e.path.join('.');
|
|
820
922
|
const detail = { path, message: e.message };
|
|
821
|
-
|
|
822
|
-
|
|
923
|
+
// zod 4 names a value outside an enum `invalid_value` and no longer carries what was sent; it is read back from the
|
|
924
|
+
// call's own arguments along the issue's path, so the vocabulary hint ("did you mean …") still has a word to match.
|
|
925
|
+
if (e.code === 'invalid_value') {
|
|
926
|
+
let received = args;
|
|
927
|
+
for (const k of e.path)
|
|
928
|
+
received = received && typeof received === 'object' ? received[k] : undefined;
|
|
823
929
|
const hint = vocabularyHintForPath(path, received);
|
|
824
930
|
if (hint)
|
|
825
931
|
detail.vocabulary = hint;
|
|
@@ -1062,23 +1168,34 @@ async function writeResultDataToFile(out, ext) {
|
|
|
1062
1168
|
return out;
|
|
1063
1169
|
}
|
|
1064
1170
|
const r = obj.result;
|
|
1065
|
-
if (!r || r.
|
|
1171
|
+
if (!r || r.success === false)
|
|
1066
1172
|
return out;
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1173
|
+
if (r.data === undefined || r.data === null) {
|
|
1174
|
+
// A result that says in fields that it wrote no file (png-sequence frames left in the page: `file: null` and
|
|
1175
|
+
// `why`) claims no file: it passes as it is. Any other success with nothing to write is the exit-0-without-file
|
|
1176
|
+
// case, named.
|
|
1177
|
+
if (r.file === null && typeof r.why === 'string' && r.why.trim())
|
|
1178
|
+
return out;
|
|
1179
|
+
const err = new ExportStepError('export', null, `the ${ext} export reported success but returned no file data`);
|
|
1180
|
+
return errorResult(ErrorCodes.EXECUTION_ERROR, err.message, { ...err.toData(), result: r });
|
|
1181
|
+
}
|
|
1074
1182
|
const dir = getExportDir();
|
|
1075
|
-
await mkdir(dir, { recursive: true });
|
|
1076
1183
|
const filePath = join(dir, `pinepaper_${String(r.format ?? 'export').replace(/[^a-z0-9-]/gi, '')}_${Date.now()}.${ext}`);
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1184
|
+
// No success without a file that exists and is not empty: each failure names its step, path and reason.
|
|
1185
|
+
try {
|
|
1186
|
+
const bytes = typeof r.data === 'string' ? bytesForSave(r.data, `the ${ext} export`) : Buffer.from(JSON.stringify(r.data), 'utf-8');
|
|
1187
|
+
await writeStep(filePath, async () => { await mkdir(dir, { recursive: true }); await writeFile(filePath, bytes); });
|
|
1188
|
+
await verifySavedFile(filePath, bytes.length);
|
|
1189
|
+
const { data: _d, ...rest } = r;
|
|
1190
|
+
void _d;
|
|
1191
|
+
obj.result = { ...rest, filePath, bytes: bytes.length };
|
|
1192
|
+
return { ...out, content: [{ type: 'text', text: JSON.stringify(obj, null, 2) }, ...out.content.slice(1)] };
|
|
1193
|
+
}
|
|
1194
|
+
catch (e) {
|
|
1195
|
+
const err = e instanceof ExportStepError ? new ExportStepError(e.step, e.path ?? filePath, e.reason)
|
|
1196
|
+
: new ExportStepError('write', filePath, errorText(e, 'the file could not be written'));
|
|
1197
|
+
return errorResult(ErrorCodes.EXECUTION_ERROR, err.message, err.toData());
|
|
1198
|
+
}
|
|
1082
1199
|
}
|
|
1083
1200
|
/**
|
|
1084
1201
|
* pinepaper_capture_frames with `sheet`: one tiled image of the film, saved
|
|
@@ -1282,8 +1399,9 @@ ${code}
|
|
|
1282
1399
|
tracker.startTimer(`${timerId}_screenshot`);
|
|
1283
1400
|
}
|
|
1284
1401
|
// Agent-written code is rolled back when it fails part-way; a tool's own
|
|
1285
|
-
// emitter is not (its verdict already describes what it left behind)
|
|
1286
|
-
|
|
1402
|
+
// emitter is not (its verdict already describes what it left behind), except
|
|
1403
|
+
// an atomic batch, whose contract is that a refused call changed nothing.
|
|
1404
|
+
const rollbackOnFailure = (toolName === 'pinepaper_execute_custom_code' || options.rollbackCreatedOnFailure === true)
|
|
1287
1405
|
&& await markItemsBeforeRun(controller);
|
|
1288
1406
|
const governorTimeoutMs = LONG_GOVERNOR_BUDGET_MS[toolName];
|
|
1289
1407
|
// NO SILENT SUCCESS (effect-class.ts / effect-check.ts): fingerprint a
|
|
@@ -1308,6 +1426,10 @@ ${code}
|
|
|
1308
1426
|
...(governorTimeoutMs ? { governorTimeoutMs } : {}),
|
|
1309
1427
|
});
|
|
1310
1428
|
const browserDuration = tracker.endTimer(`${timerId}_browser_execution`);
|
|
1429
|
+
// THE SCENE LINT RIDES OUT OF BAND: an emitter that ran app.lintScene() returns it under __ppLint, and
|
|
1430
|
+
// execute_custom_code returns its own value under __ppValue. Both are unwrapped HERE, before any verdict reads the
|
|
1431
|
+
// result, so the code's own value is what is judged and returned, and the lint sits beside it.
|
|
1432
|
+
const lintWarnings = unwrapLint(result);
|
|
1311
1433
|
// THE METRIC RECORDS THE CALL'S VERDICT, NOT THE TRANSPORT'S (gate H2,
|
|
1312
1434
|
// 1.6.19). result.success only says the code RAN; an emitter that ran and
|
|
1313
1435
|
// returned { success: false } is a failed call, and get_performance_metrics
|
|
@@ -1352,6 +1474,8 @@ ${code}
|
|
|
1352
1474
|
// PP_LOOP_BUDGET, PP_TIMEOUT) so the model can correct on the next turn.
|
|
1353
1475
|
{
|
|
1354
1476
|
code, errorCode: result.errorCode, governorReport: result.report,
|
|
1477
|
+
failures: stepFailures(toolName, result.result, result.error || 'Failed to execute code in browser'),
|
|
1478
|
+
...(lintWarnings.length ? { lintWarnings } : {}),
|
|
1355
1479
|
...(rollbackOnFailure ? await customCodeRollback(controller, result.error) : {}),
|
|
1356
1480
|
}, {
|
|
1357
1481
|
toolName,
|
|
@@ -1383,6 +1507,8 @@ ${code}
|
|
|
1383
1507
|
.slice(0, 3);
|
|
1384
1508
|
return withTiming(errorResult(ErrorCodes.EXECUTION_ERROR, said.length ? `${inner} The studio said: ${said.join(' | ')}` : inner, {
|
|
1385
1509
|
code, result: result.result, governorReport: result.report,
|
|
1510
|
+
failures: stepFailures(toolName, result.result, inner),
|
|
1511
|
+
...(lintWarnings.length ? { lintWarnings } : {}),
|
|
1386
1512
|
...(rollbackOnFailure ? await customCodeRollback(controller, inner) : {}),
|
|
1387
1513
|
}, { toolName, canvasState: canvasState || undefined }), timing);
|
|
1388
1514
|
}
|
|
@@ -1434,6 +1560,46 @@ ${code}
|
|
|
1434
1560
|
* string with no `success: true` beside it is a failure too — it is what the
|
|
1435
1561
|
* emitters' catch blocks return.
|
|
1436
1562
|
*/
|
|
1563
|
+
/**
|
|
1564
|
+
* Move an emitter's out-of-band scene lint (`__ppLint`) off its result, and unwrap execute_custom_code's own value
|
|
1565
|
+
* (`__ppValue`). The lint is also put on `run.report.lintWarnings`, where executedResult reads it. Returns the lint.
|
|
1566
|
+
*/
|
|
1567
|
+
export function unwrapLint(run) {
|
|
1568
|
+
const r = run.result;
|
|
1569
|
+
if (!r || typeof r !== 'object' || Array.isArray(r) || !('__ppLint' in r))
|
|
1570
|
+
return [];
|
|
1571
|
+
const lint = Array.isArray(r.__ppLint) ? r.__ppLint : [];
|
|
1572
|
+
if ('__ppValue' in r)
|
|
1573
|
+
run.result = r.__ppValue;
|
|
1574
|
+
else {
|
|
1575
|
+
const { __ppLint: _l, ...rest } = r;
|
|
1576
|
+
void _l;
|
|
1577
|
+
run.result = rest;
|
|
1578
|
+
}
|
|
1579
|
+
if (lint.length)
|
|
1580
|
+
run.report = { ...(run.report ?? {}), lintWarnings: lint };
|
|
1581
|
+
return lint;
|
|
1582
|
+
}
|
|
1583
|
+
/**
|
|
1584
|
+
* Every failed step of a call as `{ step, itemId, reason }`, never a bare message: a batch's own `failures`, a
|
|
1585
|
+
* result that names its step, or the call itself with the item it named.
|
|
1586
|
+
*/
|
|
1587
|
+
export function stepFailures(toolName, value, message) {
|
|
1588
|
+
const v = value;
|
|
1589
|
+
const reasonOf = (x) => (typeof x === 'string' && x.trim() ? x : '');
|
|
1590
|
+
if (v && typeof v === 'object' && Array.isArray(v.failures) && v.failures.length) {
|
|
1591
|
+
return v.failures.map((f) => ({
|
|
1592
|
+
step: reasonOf(f.step) || toolName,
|
|
1593
|
+
itemId: typeof f.itemId === 'string' ? f.itemId : null,
|
|
1594
|
+
reason: reasonOf(f.reason) || reasonOf(f.error) || message,
|
|
1595
|
+
}));
|
|
1596
|
+
}
|
|
1597
|
+
return [{
|
|
1598
|
+
step: (v && typeof v === 'object' && reasonOf(v.step)) || toolName,
|
|
1599
|
+
itemId: v && typeof v === 'object' && typeof v.itemId === 'string' ? v.itemId : null,
|
|
1600
|
+
reason: (v && typeof v === 'object' && (reasonOf(v.reason) || reasonOf(v.error))) || reasonOf(message) || `${toolName} failed with no reason given`,
|
|
1601
|
+
}];
|
|
1602
|
+
}
|
|
1437
1603
|
export function innerFailure(value) {
|
|
1438
1604
|
if (!value || typeof value !== 'object')
|
|
1439
1605
|
return null;
|
|
@@ -1561,15 +1727,23 @@ function measureResponseBytes(result) {
|
|
|
1561
1727
|
return bytes;
|
|
1562
1728
|
}
|
|
1563
1729
|
/**
|
|
1564
|
-
* TOP-LEVEL ARGUMENTS A TOOL DROPPED, SAID OUT LOUD.
|
|
1730
|
+
* TOP-LEVEL ARGUMENTS A TOOL DROPPED, SAID OUT LOUD — FOR EVERY TOOL.
|
|
1565
1731
|
*
|
|
1566
1732
|
* z.object() strips unknown keys, so agent_export {loop: true, seamless: true}
|
|
1567
1733
|
* answered success and exported exactly what it would have without them — the
|
|
1568
|
-
* caller believed they had a loop setting.
|
|
1569
|
-
*
|
|
1570
|
-
*
|
|
1571
|
-
*
|
|
1572
|
-
*
|
|
1734
|
+
* caller believed they had a loop setting. keyframe_animate {mode: 'merge'}
|
|
1735
|
+
* (issue #14) was the same: stripped, success, the track replaced.
|
|
1736
|
+
*
|
|
1737
|
+
* A key is known when the tool's published JSON schema lists it, when the zod
|
|
1738
|
+
* schema in IGNORED_ARGUMENT_SCHEMAS has it, or when HIDDEN_ARGUMENTS names it.
|
|
1739
|
+
* Anything else had no effect, and the reply says so. The published schema is
|
|
1740
|
+
* the contract the caller reads, so it is the right default; the two lists
|
|
1741
|
+
* cover what a handler accepts without advertising.
|
|
1742
|
+
*
|
|
1743
|
+
* ignored-arguments-coverage.test.ts pairs every tool with the zod schemas its
|
|
1744
|
+
* handler parses and the args.x it reads, and fails when a key the handler
|
|
1745
|
+
* accepts is neither published nor in HIDDEN_ARGUMENTS — so a working key is
|
|
1746
|
+
* never reported as ignored — and when an entry here is no longer needed.
|
|
1573
1747
|
*/
|
|
1574
1748
|
const IGNORED_ARGUMENT_SCHEMAS = {
|
|
1575
1749
|
pinepaper_agent_export: AgentExportInputSchema,
|
|
@@ -1580,6 +1754,34 @@ const IGNORED_ARGUMENT_SCHEMAS = {
|
|
|
1580
1754
|
// Its options live in `options` / `spec`; a key beside them was dropped.
|
|
1581
1755
|
pinepaper_sound: SoundInputSchema,
|
|
1582
1756
|
};
|
|
1757
|
+
/**
|
|
1758
|
+
* Keys a handler accepts that its published schema does not list. Shrink-only:
|
|
1759
|
+
* publishing one (or dropping it from the handler) means deleting it here.
|
|
1760
|
+
*/
|
|
1761
|
+
export const HIDDEN_ARGUMENTS = {
|
|
1762
|
+
pinepaper_agent_end_job: ['takeScreenshot'],
|
|
1763
|
+
pinepaper_agent_export: ['includeRecommendations'],
|
|
1764
|
+
pinepaper_agent_reset: ['preserveCanvasSize'],
|
|
1765
|
+
pinepaper_agent_start_job: ['headless', 'canvasSize', 'backgroundColor'],
|
|
1766
|
+
pinepaper_font: ['fontFamily'],
|
|
1767
|
+
pinepaper_import_svg: ['source'],
|
|
1768
|
+
// load takes its options flat as well as under `options`.
|
|
1769
|
+
pinepaper_map: ['quality', 'fillColor', 'strokeColor', 'strokeWidth', 'scale', 'rotate', 'enableHover', 'enableClick', 'hoverFill', 'hoverStroke', 'animate', 'geoJson'],
|
|
1770
|
+
pinepaper_map_regions: ['pulse', 'shape'],
|
|
1771
|
+
pinepaper_query_capabilities: ['query'],
|
|
1772
|
+
pinepaper_text_effect: ['startAt', 'hideAfter'],
|
|
1773
|
+
};
|
|
1774
|
+
/**
|
|
1775
|
+
* Nested keys (paths with [] for any array index) a handler accepts that its published schema closes over without
|
|
1776
|
+
* listing. Shrink-only, held exact by nested-ignored-coverage.test.ts; publishing one means deleting it here.
|
|
1777
|
+
*/
|
|
1778
|
+
export const NESTED_HIDDEN = {
|
|
1779
|
+
pinepaper_create_item: ['keyframes[].hold', 'keyframes[].interpolation'],
|
|
1780
|
+
pinepaper_create_scene: ['animations[].startTime'],
|
|
1781
|
+
pinepaper_keyframe_animate: ['keyframes[].hold', 'keyframes[].interpolation'],
|
|
1782
|
+
};
|
|
1783
|
+
/** Tools whose schema passes every key through: nothing is dropped. */
|
|
1784
|
+
export const PASSTHROUGH_ARGUMENT_TOOLS = new Set(['pinepaper_equation_path', 'pinepaper_ui_kit']);
|
|
1583
1785
|
const IGNORED_ARGUMENT_HINTS = {
|
|
1584
1786
|
pinepaper_create_item: Object.fromEntries(['width', 'height', 'radius', 'color', 'fillColor', 'strokeColor', 'strokeWidth', 'content', 'fontSize', 'fontFamily', 'opacity', 'rotation', 'x', 'y']
|
|
1585
1787
|
.map((k) => [k, 'Shape properties go INSIDE properties: {…}; only itemType, position, anchor, properties, animation*, keyframes and data are top-level.'])),
|
|
@@ -1590,29 +1792,73 @@ const IGNORED_ARGUMENT_HINTS = {
|
|
|
1590
1792
|
seamless: 'A seamless loop is keyed, not a switch: key the animation to the SAME state at t = 0 and at t = duration and export exactly that duration (the export stops one frame before duration, so nothing is doubled at the wrap). A GIF\'s play count is the loop parameter.',
|
|
1591
1793
|
},
|
|
1592
1794
|
};
|
|
1593
|
-
|
|
1795
|
+
let PUBLISHED_ARGUMENTS = null;
|
|
1796
|
+
/** Each tool's published top-level keys; null when its schema allows any key. */
|
|
1797
|
+
export async function publishedArguments(toolName) {
|
|
1798
|
+
if (!PUBLISHED_ARGUMENTS) {
|
|
1799
|
+
const { PINEPAPER_TOOLS } = await import('./definitions.js');
|
|
1800
|
+
PUBLISHED_ARGUMENTS = new Map(PINEPAPER_TOOLS.map((t) => {
|
|
1801
|
+
const js = t.inputSchema;
|
|
1802
|
+
return [t.name, js.additionalProperties === true ? null : new Set(Object.keys(js.properties ?? {}))];
|
|
1803
|
+
}));
|
|
1804
|
+
}
|
|
1805
|
+
return PUBLISHED_ARGUMENTS.get(toolName);
|
|
1806
|
+
}
|
|
1807
|
+
/** The top-level keys of `args` the tool does not read. */
|
|
1808
|
+
export function ignoredArgumentKeys(toolName, args, published) {
|
|
1809
|
+
if (!args || typeof args !== 'object' || PASSTHROUGH_ARGUMENT_TOOLS.has(toolName) || published === null)
|
|
1810
|
+
return [];
|
|
1811
|
+
const known = new Set([...(published ?? []), ...(HIDDEN_ARGUMENTS[toolName] ?? [])]);
|
|
1594
1812
|
let schema = IGNORED_ARGUMENT_SCHEMAS[toolName];
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1813
|
+
// zod 4: a preprocess or transform is a ZodPipe (preprocess: transform → object; .transform: object → transform).
|
|
1814
|
+
while (schema instanceof z.ZodPipe)
|
|
1815
|
+
schema = (schema._def.in instanceof z.ZodObject || !(schema._def.out instanceof z.ZodObject) ? schema._def.in : schema._def.out);
|
|
1816
|
+
if (schema instanceof z.ZodObject)
|
|
1817
|
+
Object.keys(schema.shape).forEach((k) => known.add(k));
|
|
1818
|
+
else if (published === undefined)
|
|
1819
|
+
return []; // neither a schema nor a published contract to judge by
|
|
1820
|
+
return Object.keys(args).filter((k) => !known.has(k));
|
|
1821
|
+
}
|
|
1822
|
+
export function ignoredArgumentsNote(toolName, args, published) {
|
|
1823
|
+
const ignored = ignoredArgumentKeys(toolName, args, published);
|
|
1603
1824
|
if (ignored.length === 0)
|
|
1604
1825
|
return null;
|
|
1605
1826
|
const hints = ignored.map((k) => IGNORED_ARGUMENT_HINTS[toolName]?.[k]).filter(Boolean);
|
|
1606
1827
|
return `IGNORED ARGUMENTS: ${ignored.join(', ')} ${ignored.length > 1 ? 'are' : 'is'} not a parameter of ${toolName}, so ${ignored.length > 1 ? 'they had' : 'it had'} no effect.`
|
|
1607
1828
|
+ (hints.length ? ` ${[...new Set(hints)].join(' ')}` : '');
|
|
1608
1829
|
}
|
|
1830
|
+
/** The same check, against the tool's published schema, and below the top level (AR-1 R2: nested-ignored.ts). */
|
|
1831
|
+
export async function ignoredArgumentsNoteFor(toolName, args) {
|
|
1832
|
+
const published = await publishedArguments(toolName);
|
|
1833
|
+
const top = ignoredArgumentKeys(toolName, args, published);
|
|
1834
|
+
const nested = PASSTHROUGH_ARGUMENT_TOOLS.has(toolName) || published === null ? [] : await nestedIgnoredFor(toolName, args);
|
|
1835
|
+
const ignored = [...top, ...nested];
|
|
1836
|
+
if (!ignored.length)
|
|
1837
|
+
return null;
|
|
1838
|
+
const topNote = top.length ? ignoredArgumentsNote(toolName, args, published) : '';
|
|
1839
|
+
const nestedNote = nested.length
|
|
1840
|
+
? `IGNORED ARGUMENTS: ${nested.join(', ')} ${nested.length > 1 ? 'are' : 'is'} not a parameter of ${toolName}, so ${nested.length > 1 ? 'they had' : 'it had'} no effect.`
|
|
1841
|
+
: '';
|
|
1842
|
+
return { note: [topNote, nestedNote].filter(Boolean).join(' '), ignored };
|
|
1843
|
+
}
|
|
1844
|
+
async function nestedIgnoredFor(toolName, args) {
|
|
1845
|
+
const { PINEPAPER_TOOLS } = await import('./definitions.js');
|
|
1846
|
+
const tool = PINEPAPER_TOOLS.find((t) => t.name === toolName);
|
|
1847
|
+
if (!tool)
|
|
1848
|
+
return [];
|
|
1849
|
+
const hidden = new Set(NESTED_HIDDEN[toolName] ?? []);
|
|
1850
|
+
return nestedIgnoredPaths(tool.inputSchema, args).filter((p) => !hidden.has(p.replace(/\[\d+\]/g, '[]')));
|
|
1851
|
+
}
|
|
1852
|
+
export const IGNORED_ARGUMENTS_META_KEY = 'pinepaper.studio/ignoredArguments';
|
|
1609
1853
|
export async function handleToolCall(toolName, args, options = {}) {
|
|
1610
1854
|
const tracker = getPerformanceTracker();
|
|
1611
1855
|
const baseTimerId = `${toolName}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
1612
1856
|
const result = await callContext.run({ toolName, action: typeof args.action === 'string' ? args.action : undefined }, () => handleToolCallInner(toolName, args, options, tracker, baseTimerId));
|
|
1613
|
-
const
|
|
1614
|
-
if (
|
|
1615
|
-
result.content = [...(result.content ?? []), { type: 'text', text:
|
|
1857
|
+
const ignored = await ignoredArgumentsNoteFor(toolName, args);
|
|
1858
|
+
if (ignored) {
|
|
1859
|
+
result.content = [...(result.content ?? []), { type: 'text', text: ignored.note }];
|
|
1860
|
+
result._meta = { ...(result._meta ?? {}), [IGNORED_ARGUMENTS_META_KEY]: ignored.ignored };
|
|
1861
|
+
}
|
|
1616
1862
|
// Record response payload size for token estimation
|
|
1617
1863
|
const responseBytes = measureResponseBytes(result);
|
|
1618
1864
|
tracker.recordMetric({
|
|
@@ -1742,6 +1988,11 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
|
|
|
1742
1988
|
const code = codeGenerator.generateStagger(input);
|
|
1743
1989
|
return executeOrGenerate(code, `Stagger: ${input.action}`, options, 'pinepaper_stagger');
|
|
1744
1990
|
}
|
|
1991
|
+
case 'pinepaper_pattern': {
|
|
1992
|
+
const input = PatternInputSchema.parse(args);
|
|
1993
|
+
const code = codeGenerator.generatePattern(input);
|
|
1994
|
+
return executeOrGenerate(code, `Pattern: ${input.action}${input.id ? ' ' + input.id : ''}`, options, 'pinepaper_pattern');
|
|
1995
|
+
}
|
|
1745
1996
|
case 'pinepaper_flip': {
|
|
1746
1997
|
const input = FlipInputSchema.parse(args);
|
|
1747
1998
|
const code = codeGenerator.generateFlip(input);
|
|
@@ -3555,8 +3806,26 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
|
|
|
3555
3806
|
})));
|
|
3556
3807
|
}
|
|
3557
3808
|
}
|
|
3809
|
+
let expanded;
|
|
3810
|
+
try {
|
|
3811
|
+
expanded = expandBatchMacros(input.operations);
|
|
3812
|
+
}
|
|
3813
|
+
catch (e) {
|
|
3814
|
+
if (e instanceof BatchMacroError)
|
|
3815
|
+
return errorResult(ErrorCodes.VALIDATION_ERROR, e.message, {}, { toolName: 'pinepaper_agent_batch_execute' });
|
|
3816
|
+
throw e;
|
|
3817
|
+
}
|
|
3818
|
+
// A timeline op compiles on the server: a tween it cannot honour is refused by name here, before the canvas is
|
|
3819
|
+
// touched (skipValidation does not bypass it — there is nothing to send for a tween that did not compile).
|
|
3820
|
+
if (expanded.some((op) => op.type === 'timeline')) {
|
|
3821
|
+
const tl = compileBatchTimelines(expanded);
|
|
3822
|
+
if (tl.errors.length > 0) {
|
|
3823
|
+
return errorResult(ErrorCodes.VALIDATION_ERROR, `The timeline was refused (${tl.errors.length} problem${tl.errors.length > 1 ? 's' : ''}); nothing was run. Fix ${tl.errors.length > 1 ? 'each' : 'it'} and send the batch again.`, tl.errors.map((e) => ({ path: e.path, reason: e.message })));
|
|
3824
|
+
}
|
|
3825
|
+
}
|
|
3558
3826
|
const code = codeGenerator.generateAgentBatchExecute(input);
|
|
3559
|
-
const description = `Batch executed ${input.operations.length} operations${input.atomic !== false ? ' (atomic)' : ''}
|
|
3827
|
+
const description = `Batch executed ${input.operations.length} operations${input.atomic !== false ? ' (atomic)' : ''}`
|
|
3828
|
+
+ (expanded.length !== input.operations.length ? ` (${expanded.length} after expanding ring / grid / orbit / stagger)` : '');
|
|
3560
3829
|
// Check for out-of-bounds items before execution
|
|
3561
3830
|
let boundsWarning = '';
|
|
3562
3831
|
const sizeOp = input.operations.find((op) => op.type === 'set_canvas_size');
|
|
@@ -3564,7 +3833,7 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
|
|
|
3564
3833
|
const cw = sizeOp.width || 1920;
|
|
3565
3834
|
const ch = sizeOp.height || 1080;
|
|
3566
3835
|
const oobItems = [];
|
|
3567
|
-
|
|
3836
|
+
expanded.forEach((op, idx) => {
|
|
3568
3837
|
if (op.type === 'create' && op.position) {
|
|
3569
3838
|
const { x, y } = op.position;
|
|
3570
3839
|
if (x < 0 || y < 0 || x > cw || y > ch) {
|
|
@@ -3576,7 +3845,7 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
|
|
|
3576
3845
|
boundsWarning = `\n\nWARNING: ${oobItems.length} item(s) positioned outside canvas bounds (${cw}x${ch}): ${oobItems.join(', ')}. Items may be clipped or invisible.`;
|
|
3577
3846
|
}
|
|
3578
3847
|
}
|
|
3579
|
-
const batchResult = await executeOrGenerate(code, description, options, 'pinepaper_agent_batch_execute');
|
|
3848
|
+
const batchResult = await executeOrGenerate(code, description, input.atomic !== false ? { ...options, rollbackCreatedOnFailure: true } : options, 'pinepaper_agent_batch_execute');
|
|
3580
3849
|
// Append bounds warning if any
|
|
3581
3850
|
if (boundsWarning && batchResult.content && batchResult.content.length > 0) {
|
|
3582
3851
|
const firstContent = batchResult.content[0];
|
|
@@ -3748,13 +4017,14 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
|
|
|
3748
4017
|
format,
|
|
3749
4018
|
chunkBytes: Number(exportResult.chunkBytes) || undefined,
|
|
3750
4019
|
}, input.platform || 'auto');
|
|
3751
|
-
const
|
|
3752
|
-
|
|
4020
|
+
const receipt = await exportFileReceipt(filePath, exportResult);
|
|
4021
|
+
const cleanResult = { ...exportResult, data: undefined, filePath, fileSize, chunks, file: receipt };
|
|
4022
|
+
return withEngineReport({
|
|
3753
4023
|
content: [{
|
|
3754
4024
|
type: 'text',
|
|
3755
|
-
text: `Export saved to file:\n\nFile: ${filePath}\nFormat: ${format}\nSize: ${(fileSize / 1024).toFixed(1)} KB\nPlatform: ${input.platform}\nPaged out of the studio's export store in ${chunks} chunk${chunks === 1 ? '' : 's'}\n\nResult: ${JSON.stringify(cleanResult, null, 2)}`,
|
|
4025
|
+
text: `Export saved to file:\n\nFile: ${filePath}\nFormat: ${format}\nSize: ${(fileSize / 1024).toFixed(1)} KB\nPlatform: ${input.platform}\nPaged out of the studio's export store in ${chunks} chunk${chunks === 1 ? '' : 's'}\n${probeLines(receipt)}${lintLines(exportResult)}\nResult: ${JSON.stringify(cleanResult, null, 2)}`,
|
|
3756
4026
|
}],
|
|
3757
|
-
};
|
|
4027
|
+
}, exportBrowserResult.report);
|
|
3758
4028
|
}
|
|
3759
4029
|
catch (streamError) {
|
|
3760
4030
|
// The export is still held — the message says so and names the id,
|
|
@@ -3768,7 +4038,14 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
|
|
|
3768
4038
|
// SCC (CEA-608, 8.37): the studio sent the cues; encoded here.
|
|
3769
4039
|
if (format === 'scc' && Array.isArray(exportResult?.sccCues)) {
|
|
3770
4040
|
const scc = buildScc(exportResult.sccCues);
|
|
3771
|
-
|
|
4041
|
+
let saved;
|
|
4042
|
+
try {
|
|
4043
|
+
saved = await saveExportToFile(scc.text, 'scc', input.platform || 'auto');
|
|
4044
|
+
}
|
|
4045
|
+
catch (saveError) {
|
|
4046
|
+
return exportStepErrorResult(saveError, 'save', { format: 'scc' });
|
|
4047
|
+
}
|
|
4048
|
+
const { filePath, fileSize } = saved;
|
|
3772
4049
|
const warnings = [];
|
|
3773
4050
|
if (scc.truncated)
|
|
3774
4051
|
warnings.push({ code: 'caption_truncated', message: `${scc.truncated} caption(s) did not fit two rows of 32 characters and were cut. Split long captions into more cues.` });
|
|
@@ -3779,10 +4056,22 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
|
|
|
3779
4056
|
const { sccCues: _c, ...rest } = exportResult;
|
|
3780
4057
|
void _c;
|
|
3781
4058
|
const cleanResult = { ...rest, filePath, fileSize, frameRate: '29.97 drop-frame', fidelity: { warnings } };
|
|
3782
|
-
return { content: [{ type: 'text', text: `Export saved to file:\n\nFile: ${filePath}\nFormat: scc (CEA-608, CC1, pop-on)\nSize: ${fileSize} bytes\n\nResult: ${JSON.stringify(cleanResult, null, 2)}` }] };
|
|
4059
|
+
return withEngineReport({ content: [{ type: 'text', text: `Export saved to file:\n\nFile: ${filePath}\nFormat: scc (CEA-608, CC1, pop-on)\nSize: ${fileSize} bytes\n\nResult: ${JSON.stringify(cleanResult, null, 2)}` }] }, exportBrowserResult.report);
|
|
3783
4060
|
}
|
|
3784
4061
|
if ((format === 'html5-ad' || format === 'playable') && typeof exportResult?.adPage === 'string') {
|
|
3785
|
-
|
|
4062
|
+
try {
|
|
4063
|
+
return withEngineReport(await saveAdExport(exportResult, input, input.platform || 'auto'), exportBrowserResult.report);
|
|
4064
|
+
}
|
|
4065
|
+
catch (saveError) {
|
|
4066
|
+
return exportStepErrorResult(saveError, 'save', { format });
|
|
4067
|
+
}
|
|
4068
|
+
}
|
|
4069
|
+
// NO SUCCESS WITHOUT BYTES. Every delivering branch above returned; what is left must carry the file's
|
|
4070
|
+
// data. A success with none (no data field, an empty string) used to fall through to the inline return and
|
|
4071
|
+
// report success with nothing in it. An estimate is the one answer that is not a file.
|
|
4072
|
+
if (!exportResult?.estimateOnly && !(typeof data === 'string' && data.length > 0)) {
|
|
4073
|
+
const err = new ExportStepError('export', null, `the studio reported a successful ${format} export but returned no file data (${data === undefined ? 'no data field' : typeof data === 'string' ? 'an empty string' : `a ${data === null ? 'null' : typeof data}`})`);
|
|
4074
|
+
return errorResult(ErrorCodes.EXECUTION_ERROR, err.message, { ...err.toData(), format, result: exportResult }, { toolName: 'pinepaper_agent_export' });
|
|
3786
4075
|
}
|
|
3787
4076
|
const shouldSaveToFile = data && typeof data === 'string' && (options.saveToFile ||
|
|
3788
4077
|
ALWAYS_SAVE_FORMATS.has(format) ||
|
|
@@ -3790,29 +4079,32 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
|
|
|
3790
4079
|
if (shouldSaveToFile) {
|
|
3791
4080
|
try {
|
|
3792
4081
|
const { filePath, fileSize } = await saveExportToFile(data, format, input.platform || 'auto');
|
|
3793
|
-
const
|
|
4082
|
+
const receipt = await exportFileReceipt(filePath, exportResult);
|
|
4083
|
+
const cleanResult = { ...exportResult, data: undefined, filePath, fileSize, file: receipt };
|
|
3794
4084
|
// A mix that dropped sounds wrote a perfectly good file that is
|
|
3795
4085
|
// missing tracks. It rides the SUCCESS path, which is exactly why
|
|
3796
4086
|
// it has to be said out loud here rather than left in the JSON.
|
|
3797
4087
|
const dropNote = exportResult.incomplete;
|
|
3798
|
-
return {
|
|
4088
|
+
return withEngineReport({
|
|
3799
4089
|
content: [{
|
|
3800
4090
|
type: 'text',
|
|
3801
4091
|
text: `Export saved to file:\n\nFile: ${filePath}\nFormat: ${format}\nSize: ${(fileSize / 1024).toFixed(1)} KB\nPlatform: ${input.platform}\n`
|
|
3802
4092
|
+ (dropNote ? `DROPPED: ${dropNote}\n` : '')
|
|
4093
|
+
+ probeLines(receipt)
|
|
4094
|
+
+ lintLines(exportResult)
|
|
3803
4095
|
+ `\nResult: ${JSON.stringify(cleanResult, null, 2)}`,
|
|
3804
4096
|
}],
|
|
3805
|
-
};
|
|
4097
|
+
}, exportBrowserResult.report);
|
|
3806
4098
|
}
|
|
3807
4099
|
catch (saveError) {
|
|
4100
|
+
// A failed save is a failed export, never a success with a note inside it: the step, the path and
|
|
4101
|
+
// the reason, and the data stripped so the error is not the size of the video.
|
|
3808
4102
|
console.error('[PinePaper] Failed to save export to file:', saveError);
|
|
3809
|
-
|
|
3810
|
-
const strippedResult = { ...exportResult, data: `[${format} export data — file save failed: ${saveError instanceof Error ? saveError.message : 'unknown error'}]` };
|
|
3811
|
-
return executedResult(code, strippedResult, exportBrowserResult.screenshot, description);
|
|
4103
|
+
return exportStepErrorResult(saveError, 'save', { format, bytes: typeof data === 'string' ? data.length : 0 });
|
|
3812
4104
|
}
|
|
3813
4105
|
}
|
|
3814
|
-
// Small export — return inline (existing behavior)
|
|
3815
|
-
return executedResult(code, exportResult, exportBrowserResult.screenshot, description);
|
|
4106
|
+
// Small export — return inline (existing behavior). The bytes are in the result, checked non-empty above.
|
|
4107
|
+
return executedResult(code, exportResult, exportBrowserResult.screenshot, description, exportBrowserResult.report);
|
|
3816
4108
|
}
|
|
3817
4109
|
case 'pinepaper_design_styles':
|
|
3818
4110
|
return dataResult({ styles: designSystems.listStyles() });
|
|
@@ -3884,10 +4176,24 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
|
|
|
3884
4176
|
const sizeWarning = sizeIgnored
|
|
3885
4177
|
? `"${scene.style}" lays out at its own size, ${scene.width}x${scene.height}; the requested ${input.width ?? '?'}x${input.height ?? '?'} was not used`
|
|
3886
4178
|
: undefined;
|
|
4179
|
+
// BAUHAUS-SUBTITLE: at a size the style was not laid out for, its
|
|
4180
|
+
// proportions can put text across a shape (bauhaus at 1920x1080
|
|
4181
|
+
// runs the subtitle over the red circle). The layout is vendored;
|
|
4182
|
+
// the result names each collision rather than drawing it as intended.
|
|
4183
|
+
const collisions = designSystems.scalingCollisions(input.style, {
|
|
4184
|
+
title: input.title, subtitle: input.subtitle, body: input.body,
|
|
4185
|
+
width: input.width, height: input.height, variant: input.variant,
|
|
4186
|
+
}, scene);
|
|
4187
|
+
const layoutWarning = collisions.length
|
|
4188
|
+
? `at ${scene.width}x${scene.height} the "${scene.style}" layout runs text onto a shape it does not touch at its own default size: `
|
|
4189
|
+
+ collisions.map((c) => `${c.text} over ${c.shape}`).join(', ')
|
|
4190
|
+
+ '. Compose without width/height (its default size) for the layout as designed, or move the text afterwards with pinepaper_modify_item.'
|
|
4191
|
+
: undefined;
|
|
4192
|
+
const collisionReport = collisions.length ? { layoutCollisions: collisions, layoutWarning } : {};
|
|
3887
4193
|
if (input.draw === false) {
|
|
3888
|
-
return dataResult({ scene, ops, drawn: false, ...(sizeWarning ? { warning: sizeWarning } : {}) });
|
|
4194
|
+
return dataResult({ scene, ops, drawn: false, ...(sizeWarning ? { warning: sizeWarning } : {}), ...collisionReport });
|
|
3889
4195
|
}
|
|
3890
|
-
const code = codeGenerator.generateDesignCompose(scene, ops, scene.style, sizeWarning);
|
|
4196
|
+
const code = codeGenerator.generateDesignCompose(scene, ops, scene.style, sizeWarning, collisionReport);
|
|
3891
4197
|
return executeOrGenerate(code, `Composed a ${scene.style} scene (${ops.length} items)`, options, 'pinepaper_design_system');
|
|
3892
4198
|
}
|
|
3893
4199
|
}
|
|
@@ -3969,11 +4275,8 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
|
|
|
3969
4275
|
};
|
|
3970
4276
|
}
|
|
3971
4277
|
catch (saveError) {
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
wavBase64: `[${soundtrack.byteLength} bytes of WAV — file save failed: ${saveError instanceof Error ? saveError.message : 'unknown error'}]`,
|
|
3975
|
-
};
|
|
3976
|
-
return executedResult(code, stripped, wavResult.screenshot, soundDescription);
|
|
4278
|
+
// The soundtrack rendered; writing it did not. That is a failed export, said as one.
|
|
4279
|
+
return exportStepErrorResult(saveError, 'save', { format: 'wav', byteLength: soundtrack.byteLength }, 'pinepaper_sound');
|
|
3977
4280
|
}
|
|
3978
4281
|
}
|
|
3979
4282
|
return executeOrGenerate(code, soundDescription, options, 'pinepaper_sound');
|
|
@@ -4222,7 +4525,7 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
|
|
|
4222
4525
|
}
|
|
4223
4526
|
catch (error) {
|
|
4224
4527
|
if (error instanceof ZodError) {
|
|
4225
|
-
return handleValidationError(error, i18n);
|
|
4528
|
+
return handleValidationError(error, i18n, args);
|
|
4226
4529
|
}
|
|
4227
4530
|
// Try to capture canvas state for error context (if browser is connected)
|
|
4228
4531
|
let canvasState = null;
|
|
@@ -4235,6 +4538,10 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
|
|
|
4235
4538
|
catch (_stateError) {
|
|
4236
4539
|
// Ignore errors from canvas state capture
|
|
4237
4540
|
}
|
|
4541
|
+
// An export that failed at a named step keeps its step, path and reason as fields.
|
|
4542
|
+
if (error instanceof ExportStepError) {
|
|
4543
|
+
return errorResult(ErrorCodes.EXECUTION_ERROR, error.message, error.toData(), { toolName, canvasState: canvasState || undefined });
|
|
4544
|
+
}
|
|
4238
4545
|
if (error instanceof Error) {
|
|
4239
4546
|
const message = i18n
|
|
4240
4547
|
? i18n.getError('executionError', { message: error.message })
|