@pinepaper.studio/mcp-server 1.6.23 → 1.6.25
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 +212 -17
- 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.d.ts +6 -0
- package/dist/browser/puppeteer-controller.d.ts.map +1 -1
- package/dist/browser/puppeteer-controller.js +99 -71
- package/dist/browser/puppeteer-controller.js.map +1 -1
- package/dist/build-info.json +2 -2
- package/dist/cli.js +7 -5
- package/dist/cli.js.map +1 -1
- package/dist/design/design-systems.js +2 -2
- package/dist/design/design-systems.js.map +1 -1
- package/dist/i18n/locales/en.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +63 -12
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.d.ts +5 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +5 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/server.d.ts +86 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +159 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/stdio.d.ts +20 -0
- package/dist/mcp/stdio.d.ts.map +1 -0
- package/dist/mcp/stdio.js +82 -0
- package/dist/mcp/stdio.js.map +1 -0
- package/dist/mcp/types.d.ts +598 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +140 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/metrics/performance-tracker.d.ts.map +1 -1
- package/dist/metrics/performance-tracker.js +4 -3
- package/dist/metrics/performance-tracker.js.map +1 -1
- package/dist/ontology/hints.d.ts.map +1 -1
- package/dist/ontology/hints.js +8 -2
- package/dist/ontology/hints.js.map +1 -1
- package/dist/ontology/upstream-vocabulary.d.ts +2 -2
- package/dist/ontology/upstream-vocabulary.d.ts.map +1 -1
- package/dist/ontology/upstream-vocabulary.js +8 -2
- package/dist/ontology/upstream-vocabulary.js.map +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/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/index.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 +158 -0
- package/dist/tools/batch-macros.js.map +1 -0
- package/dist/tools/choreograph.d.ts.map +1 -1
- package/dist/tools/choreograph.js +2 -0
- package/dist/tools/choreograph.js.map +1 -1
- package/dist/tools/compact-descriptions.d.ts.map +1 -1
- package/dist/tools/compact-descriptions.js +8 -7
- package/dist/tools/compact-descriptions.js.map +1 -1
- package/dist/tools/craft-tools.d.ts +1 -1
- package/dist/tools/craft-tools.d.ts.map +1 -1
- package/dist/tools/create-known-keys.d.ts +2 -0
- package/dist/tools/create-known-keys.d.ts.map +1 -0
- package/dist/tools/create-known-keys.js +24 -0
- package/dist/tools/create-known-keys.js.map +1 -0
- package/dist/tools/definitions.d.ts +3 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +122 -19
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/effect-check.d.ts +1 -1
- package/dist/tools/effect-check.d.ts.map +1 -1
- package/dist/tools/effect-check.js +4 -0
- package/dist/tools/effect-check.js.map +1 -1
- package/dist/tools/engine-lists.d.ts +5 -2
- package/dist/tools/engine-lists.d.ts.map +1 -1
- package/dist/tools/engine-lists.js +10 -2
- package/dist/tools/engine-lists.js.map +1 -1
- package/dist/tools/engine-methods.d.ts.map +1 -1
- package/dist/tools/engine-methods.js +2 -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 +67 -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 +68 -0
- package/dist/tools/export-file-receipt.d.ts.map +1 -0
- package/dist/tools/export-file-receipt.js +171 -0
- package/dist/tools/export-file-receipt.js.map +1 -0
- package/dist/tools/handlers/beats.d.ts +8 -45
- package/dist/tools/handlers/beats.d.ts.map +1 -1
- package/dist/tools/handlers/camera.d.ts +1 -1
- package/dist/tools/handlers/camera.d.ts.map +1 -1
- package/dist/tools/handlers/export.d.ts +1 -1
- package/dist/tools/handlers/export.d.ts.map +1 -1
- package/dist/tools/handlers/export.js +34 -8
- package/dist/tools/handlers/export.js.map +1 -1
- package/dist/tools/handlers/font.d.ts +1 -1
- package/dist/tools/handlers/font.d.ts.map +1 -1
- package/dist/tools/handlers/font.js +8 -7
- package/dist/tools/handlers/font.js.map +1 -1
- package/dist/tools/handlers/generate.d.ts +42 -271
- package/dist/tools/handlers/generate.d.ts.map +1 -1
- package/dist/tools/handlers/generate.js +4 -3
- package/dist/tools/handlers/generate.js.map +1 -1
- package/dist/tools/handlers/maps.d.ts +1 -1
- package/dist/tools/handlers/maps.d.ts.map +1 -1
- package/dist/tools/handlers/ontology.d.ts +1 -1
- package/dist/tools/handlers/ontology.d.ts.map +1 -1
- package/dist/tools/handlers/tool-guide.d.ts +1 -1
- package/dist/tools/handlers/tool-guide.d.ts.map +1 -1
- package/dist/tools/handlers.d.ts +82 -4
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +366 -89
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/lane.d.ts +39 -0
- package/dist/tools/lane.d.ts.map +1 -0
- package/dist/tools/lane.js +247 -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/p5-compat/p5-helpers.d.ts +1 -1
- package/dist/tools/p5-compat/p5-helpers.js +1 -1
- 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 +15 -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 +1041 -0
- package/dist/tools/timeline.js.map +1 -0
- package/dist/tools/toolkits.d.ts +22 -7
- package/dist/tools/toolkits.d.ts.map +1 -1
- package/dist/tools/toolkits.js +113 -21
- package/dist/tools/toolkits.js.map +1 -1
- package/dist/types/code-generator.d.ts +55 -2
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +671 -202
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +5281 -25048
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +81 -41
- package/dist/types/schemas.js.map +1 -1
- package/dist/utils/asset-paths.d.ts.map +1 -1
- package/dist/utils/asset-paths.js +4 -4
- package/dist/utils/asset-paths.js.map +1 -1
- package/dist/utils/env.d.ts +9 -0
- package/dist/utils/env.d.ts.map +1 -0
- package/dist/utils/env.js +11 -0
- package/dist/utils/env.js.map +1 -0
- package/dist/utils/export-dir.d.ts +36 -0
- package/dist/utils/export-dir.d.ts.map +1 -0
- package/dist/utils/export-dir.js +86 -0
- package/dist/utils/export-dir.js.map +1 -0
- package/dist/utils/js-literal.d.ts +4 -0
- package/dist/utils/js-literal.d.ts.map +1 -0
- package/dist/utils/js-literal.js +25 -0
- package/dist/utils/js-literal.js.map +1 -0
- package/dist/utils/mcp-log.d.ts +1 -1
- package/dist/utils/mcp-log.d.ts.map +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +5 -12
- package/dist/version.js.map +1 -1
- package/manifest.json +46 -63
- package/package.json +15 -12
package/dist/tools/handlers.js
CHANGED
|
@@ -5,13 +5,15 @@
|
|
|
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,
|
|
13
15
|
// Agent flow mode schemas
|
|
14
|
-
AgentStartJobInputSchema, AgentEndJobInputSchema, AgentResetInputSchema, AgentBatchExecuteInputSchema, AgentExportInputSchema, ExportStoreInputSchema, AgentAnalyzeInputSchema,
|
|
16
|
+
AgentStartJobInputSchema, AgentEndJobInputSchema, AgentResetInputSchema, AgentBatchExecuteInputSchema, AgentBatchOperationSchema, AgentExportInputSchema, ExportStoreInputSchema, AgentAnalyzeInputSchema,
|
|
15
17
|
// Letter collage schemas
|
|
16
18
|
CreateLetterCollageInputSchema, AnimateLetterCollageInputSchema,
|
|
17
19
|
// Map schemas
|
|
@@ -26,15 +28,16 @@ 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 { exportFileReceiptOrReason, receiptErrorLine } 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
|
-
import { tmpdir } from 'node:os';
|
|
38
41
|
import { join, extname } from 'node:path';
|
|
39
42
|
import { fileURLToPath } from 'node:url';
|
|
40
43
|
import { getBrowserController, resetBrowserController, } from '../browser/puppeteer-controller.js';
|
|
@@ -44,6 +47,7 @@ import { formatErrorContext, captureCanvasState } from '../execution/index.js';
|
|
|
44
47
|
import { FILTER_PARAM_RANGES } from './engine-lists.js';
|
|
45
48
|
import { getSessionManager } from '../agent/session-manager.js';
|
|
46
49
|
import { vocabularyHintForPath, validateBatchVocabulary, detectBatchPropertyTypos } from '../ontology/hints.js';
|
|
50
|
+
import { compileBatchTimelines } from './timeline.js';
|
|
47
51
|
import { cameraHandlers } from './handlers/camera.js';
|
|
48
52
|
import { fontHandlers } from './handlers/font.js';
|
|
49
53
|
import { toolGuideHandlers } from './handlers/tool-guide.js';
|
|
@@ -54,12 +58,15 @@ import { exportHandlers } from './handlers/export.js';
|
|
|
54
58
|
import { generateHandlers } from './handlers/generate.js';
|
|
55
59
|
import { beatHandlers } from './handlers/beats.js';
|
|
56
60
|
import { planCharacter, generateCharacterCode } from './handlers/character.js';
|
|
61
|
+
import { jsLiteral } from '../utils/js-literal.js';
|
|
57
62
|
import { buildZip } from '../utils/zip.js';
|
|
58
63
|
import { buildScc } from '../utils/scc.js';
|
|
59
64
|
import { checkContrast, checkFlashes, parseCssColor } from '../utils/a11y.js';
|
|
60
65
|
import { buildHtml5Ad, buildPlayable, externalRequests, pinSceneBackground } from '../utils/ad-package.js';
|
|
61
66
|
import { safeFetch, checkUrl, UnsafeUrlError } from '../utils/safe-fetch.js';
|
|
62
67
|
import { confineAssetPath } from '../utils/asset-paths.js';
|
|
68
|
+
import { env } from '../utils/env.js';
|
|
69
|
+
import { getExportDir, exportFileLink } from '../utils/export-dir.js';
|
|
63
70
|
/**
|
|
64
71
|
* Registry of per-domain handler maps. Tools listed here short-circuit the
|
|
65
72
|
* main switch at dispatch time (see handleToolCallInner). New domains are
|
|
@@ -82,7 +89,7 @@ function getDomainHandlers() {
|
|
|
82
89
|
* Defaults to 'on_request' for optimal performance.
|
|
83
90
|
*/
|
|
84
91
|
export function getScreenshotMode() {
|
|
85
|
-
const mode =
|
|
92
|
+
const mode = env('PINEPAPER_SCREENSHOT_MODE')?.toLowerCase();
|
|
86
93
|
if (mode === 'always' || mode === 'never' || mode === 'on_request') {
|
|
87
94
|
return mode;
|
|
88
95
|
}
|
|
@@ -92,9 +99,8 @@ export function getScreenshotMode() {
|
|
|
92
99
|
// =============================================================================
|
|
93
100
|
// EXPORT FILE SAVE HELPERS
|
|
94
101
|
// =============================================================================
|
|
95
|
-
export
|
|
96
|
-
|
|
97
|
-
}
|
|
102
|
+
// The export folder, one definition for the writers and for path confinement (utils/export-dir.ts).
|
|
103
|
+
export { getExportDir };
|
|
98
104
|
// wav joins them: a minute of 48kHz 16-bit is ~5.8 MB of base64 and the ten
|
|
99
105
|
// minutes the schema allows is ~77 MB. Deliverable across the bridge, useless
|
|
100
106
|
// pasted into a response.
|
|
@@ -130,10 +136,10 @@ const SAVE_THRESHOLD_BYTES = 180_000; // ~180KB base64 ≈ 135KB decoded
|
|
|
130
136
|
*/
|
|
131
137
|
const EXPORT_GOVERNOR_DEFAULT_MS = 300_000;
|
|
132
138
|
function exportGovernorTimeoutMs() {
|
|
133
|
-
const raw = Number(
|
|
139
|
+
const raw = Number(env('PINEPAPER_EXPORT_TIMEOUT'));
|
|
134
140
|
if (!Number.isFinite(raw) || raw <= 0)
|
|
135
141
|
return EXPORT_GOVERNOR_DEFAULT_MS;
|
|
136
|
-
const protocolFloor = Math.max(Number(
|
|
142
|
+
const protocolFloor = Math.max(Number(env('PINEPAPER_TIMEOUT')) || 0, EXPORT_GOVERNOR_DEFAULT_MS);
|
|
137
143
|
if (raw > protocolFloor) {
|
|
138
144
|
console.error(`[PinePaper] PINEPAPER_EXPORT_TIMEOUT=${raw}ms is above the browser's protocol timeout (${protocolFloor}ms) — ` +
|
|
139
145
|
'raise PINEPAPER_TIMEOUT to match, or an export past that point fails as a protocol error rather than a governor timeout.');
|
|
@@ -372,12 +378,15 @@ async function saveAdExport(r, input, platform) {
|
|
|
372
378
|
const html = buildHtml5Ad(r.adPage, opts);
|
|
373
379
|
const zip = buildZip([{ name: 'index.html', data: html }]);
|
|
374
380
|
files.zip = join(exportDir, `${stamp}.zip`);
|
|
375
|
-
await writeFile(files.zip, zip);
|
|
381
|
+
await writeStep(files.zip, () => writeFile(files.zip, zip));
|
|
382
|
+
await verifySavedFile(files.zip, zip.length);
|
|
376
383
|
uploadBytes = zip.length;
|
|
377
384
|
// The backup image is uploaded BESIDE the zip, not inside it.
|
|
378
385
|
if (r.backupImage && r.backupImage.startsWith('data:')) {
|
|
379
386
|
files.backupImage = join(exportDir, `${stamp}_backup.png`);
|
|
380
|
-
|
|
387
|
+
const backup = bytesForSave(r.backupImage, 'the backup image');
|
|
388
|
+
await writeStep(files.backupImage, () => writeFile(files.backupImage, backup));
|
|
389
|
+
await verifySavedFile(files.backupImage, backup.length);
|
|
381
390
|
}
|
|
382
391
|
else {
|
|
383
392
|
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 +395,9 @@ async function saveAdExport(r, input, platform) {
|
|
|
386
395
|
else {
|
|
387
396
|
const html = buildPlayable(r.adPage, opts);
|
|
388
397
|
files.html = join(exportDir, `${stamp}.html`);
|
|
389
|
-
await writeFile(files.html, html, 'utf-8');
|
|
398
|
+
await writeStep(files.html, () => writeFile(files.html, html, 'utf-8'));
|
|
390
399
|
uploadBytes = Buffer.byteLength(html, 'utf-8');
|
|
400
|
+
await verifySavedFile(files.html, uploadBytes);
|
|
391
401
|
}
|
|
392
402
|
// 150 KB is Google Ads' display HTML5 figure; some uploads allow 600 KB, and
|
|
393
403
|
// playable networks set their own — so it is a default, not a rule.
|
|
@@ -411,27 +421,31 @@ async function saveAdExport(r, input, platform) {
|
|
|
411
421
|
content: [{
|
|
412
422
|
type: 'text',
|
|
413
423
|
text: `Ad saved:\n\n${Object.entries(files).map(([k, v]) => `${k}: ${v}`).join('\n')}\nFormat: ${r.format}\nSize: ${r.adSize.width}x${r.adSize.height}, upload ${(uploadBytes / 1024).toFixed(1)} KB\n\nResult: ${JSON.stringify(cleanResult, null, 2)}`,
|
|
414
|
-
}],
|
|
424
|
+
}, ...Object.values(files).filter((f) => typeof f === 'string').map((f) => exportFileLink(f))],
|
|
415
425
|
};
|
|
416
426
|
}
|
|
417
427
|
async function saveExportToFile(data, format, platform) {
|
|
418
428
|
const exportDir = getExportDir();
|
|
419
|
-
await mkdir(exportDir, { recursive: true });
|
|
429
|
+
await writeStep(exportDir, () => mkdir(exportDir, { recursive: true }));
|
|
420
430
|
const ext = getFileExtension(format);
|
|
421
431
|
const timestamp = Date.now();
|
|
422
432
|
const fileName = `pinepaper_${platform}_${timestamp}.${ext}`;
|
|
423
433
|
const filePath = join(exportDir, fileName);
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
434
|
+
// A data URL is decoded (base64 or percent-encoded); plain text (SVG, captions) is written as UTF-8. Nothing to
|
|
435
|
+
// write is the `export` or `save` step's failure, named — it used to write a 0-byte file and report it saved.
|
|
436
|
+
let buffer;
|
|
437
|
+
try {
|
|
438
|
+
buffer = bytesForSave(data, `the ${format} export`);
|
|
429
439
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
440
|
+
catch (e) {
|
|
441
|
+
if (e instanceof ExportStepError)
|
|
442
|
+
throw new ExportStepError(e.step, filePath, e.reason);
|
|
443
|
+
throw e;
|
|
434
444
|
}
|
|
445
|
+
await writeStep(filePath, () => writeFile(filePath, buffer));
|
|
446
|
+
// Read back: success is a file that exists and holds what was written.
|
|
447
|
+
await verifySavedFile(filePath, buffer.length);
|
|
448
|
+
return { filePath, fileSize: buffer.length };
|
|
435
449
|
}
|
|
436
450
|
/**
|
|
437
451
|
* Page a RETAINED export out of the studio's export store into a local file.
|
|
@@ -459,15 +473,16 @@ async function streamRetainedExportToFile(controller, retained, platform) {
|
|
|
459
473
|
let offset = 0;
|
|
460
474
|
let chunks = 0;
|
|
461
475
|
// Start from empty: appendFile would otherwise extend a same-millisecond file.
|
|
462
|
-
await writeFile(filePath, Buffer.alloc(0));
|
|
476
|
+
await writeStep(filePath, () => writeFile(filePath, Buffer.alloc(0)));
|
|
463
477
|
// Returns the error rather than throwing it, so `throw await abandon(...)`
|
|
464
478
|
// reads as a throw to the type checker and narrows what follows.
|
|
465
|
-
|
|
479
|
+
// Paging the bytes out is the `save` step; a refusal from the file system is the `write` step.
|
|
480
|
+
const abandon = async (why, step = 'save') => {
|
|
466
481
|
try {
|
|
467
482
|
await unlink(filePath);
|
|
468
483
|
}
|
|
469
484
|
catch { /* nothing written yet */ }
|
|
470
|
-
return new
|
|
485
|
+
return new ExportStepError(step, filePath,
|
|
471
486
|
// The promise now names the call that keeps it. It was true before and
|
|
472
487
|
// unreachable: reading a held export mutates nothing, so "it can be paged
|
|
473
488
|
// again" was always correct and there was no door to page it through.
|
|
@@ -546,7 +561,12 @@ async function streamRetainedExportToFile(controller, retained, platform) {
|
|
|
546
561
|
const buf = Buffer.from(chunk.data || '', 'base64');
|
|
547
562
|
if (buf.length === 0 && chunk.eof !== true)
|
|
548
563
|
throw await abandon('the studio returned an empty chunk before the end of the file');
|
|
549
|
-
|
|
564
|
+
try {
|
|
565
|
+
await appendFile(filePath, buf);
|
|
566
|
+
}
|
|
567
|
+
catch (writeError) {
|
|
568
|
+
throw await abandon(`appending ${buf.length} bytes at offset ${offset} failed: ${errorText(writeError, 'the file system gave no reason')}`, 'write');
|
|
569
|
+
}
|
|
550
570
|
offset += buf.length;
|
|
551
571
|
chunks++;
|
|
552
572
|
if (chunk.eof === true)
|
|
@@ -557,6 +577,16 @@ async function streamRetainedExportToFile(controller, retained, platform) {
|
|
|
557
577
|
if (offset !== retained.size) {
|
|
558
578
|
throw await abandon(`reassembled ${offset} bytes but the studio reported ${retained.size}`);
|
|
559
579
|
}
|
|
580
|
+
// An empty export is not a saved file, and neither is one the disk does not hold: read it back BEFORE the
|
|
581
|
+
// studio's copy is released, so a failure here still leaves the bytes recoverable.
|
|
582
|
+
if (offset === 0)
|
|
583
|
+
throw await abandon('the studio held an empty export (0 bytes): there is nothing to save');
|
|
584
|
+
try {
|
|
585
|
+
await verifySavedFile(filePath, offset);
|
|
586
|
+
}
|
|
587
|
+
catch (verifyError) {
|
|
588
|
+
throw await abandon(errorText(verifyError instanceof ExportStepError ? verifyError.reason : verifyError, 'the written file could not be read back'), 'write');
|
|
589
|
+
}
|
|
560
590
|
// Only now is it safe to drop the studio's copy.
|
|
561
591
|
try {
|
|
562
592
|
await controller.executeCode(codeGenerator.generateReleaseExport(retained.exportId), false);
|
|
@@ -572,7 +602,7 @@ async function streamRetainedExportToFile(controller, retained, platform) {
|
|
|
572
602
|
* Defaults to 'puppeteer' for live execution.
|
|
573
603
|
*/
|
|
574
604
|
export function getExecutionMode() {
|
|
575
|
-
const mode =
|
|
605
|
+
const mode = env('PINEPAPER_EXECUTION_MODE')?.toLowerCase();
|
|
576
606
|
if (mode === 'puppeteer' || mode === 'code') {
|
|
577
607
|
return mode;
|
|
578
608
|
}
|
|
@@ -685,10 +715,40 @@ function withSessionNote(out, r) {
|
|
|
685
715
|
}
|
|
686
716
|
return out;
|
|
687
717
|
}
|
|
718
|
+
export const RECEIPTS_META_KEY = 'pinepaper.studio/receipts';
|
|
719
|
+
/** The engine run's receipts (report.receipts: { calls, byDoor, notes }), when any call has something to say. */
|
|
720
|
+
export function engineReceipts(report) {
|
|
721
|
+
const r = report && typeof report === 'object' ? report.receipts : undefined;
|
|
722
|
+
if (!r || !Array.isArray(r.notes) || r.notes.length === 0)
|
|
723
|
+
return null;
|
|
724
|
+
return { calls: Number(r.calls) || 0, byDoor: r.byDoor ?? {}, notes: r.notes };
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* agent_export answered from the export's own result and never read the run's report (AR-1 R2): an export's engine
|
|
728
|
+
* warnings and receipts are now on the reply like every other tool's — a text line an agent reads, and _meta.
|
|
729
|
+
*/
|
|
730
|
+
export function withEngineReport(result, report) {
|
|
731
|
+
if (!report || result.isError)
|
|
732
|
+
return result;
|
|
733
|
+
const receipts = engineReceipts(report);
|
|
734
|
+
const engineWarnings = Array.isArray(report.engineWarnings) ? report.engineWarnings.filter((w) => w && typeof w === 'object') : [];
|
|
735
|
+
if (!receipts && !engineWarnings.length)
|
|
736
|
+
return result;
|
|
737
|
+
const body = { ...(receipts ? { receipts } : {}), ...(engineWarnings.length ? { engineWarnings } : {}) };
|
|
738
|
+
return {
|
|
739
|
+
...result,
|
|
740
|
+
content: [...(result.content ?? []), { type: 'text', text: `ENGINE RECEIPTS: ${JSON.stringify(body)}` }],
|
|
741
|
+
_meta: { ...(result._meta ?? {}), [RECEIPTS_META_KEY]: body },
|
|
742
|
+
};
|
|
743
|
+
}
|
|
688
744
|
function executedResult(code, result, screenshot, description, report) {
|
|
689
745
|
const resultObj = { success: true, result };
|
|
690
746
|
if (description)
|
|
691
747
|
resultObj.description = description;
|
|
748
|
+
// The scene lint after the code ran, when there is something to say.
|
|
749
|
+
const lint = report && Array.isArray(report.lintWarnings) ? report.lintWarnings : [];
|
|
750
|
+
if (lint.length)
|
|
751
|
+
resultObj.lintWarnings = lint;
|
|
692
752
|
// Surface the governor's model-actionable feedback (app.runGenerated report):
|
|
693
753
|
// warnings (e.g. "500 items created one-by-one — prefer a pattern"), item count,
|
|
694
754
|
// and guard trips. Omitted entirely on pre-governor FxTool builds (no report).
|
|
@@ -711,6 +771,11 @@ function executedResult(code, result, screenshot, description, report) {
|
|
|
711
771
|
governor.loopGuardTripped = true;
|
|
712
772
|
if (Object.keys(governor).length)
|
|
713
773
|
resultObj.governor = governor;
|
|
774
|
+
// AR-1 R2: what each engine call did with what it was given (FxTool js/core/Receipt.js) — only when a call
|
|
775
|
+
// renamed, fell back, dropped or warned; a run whose calls all applied as asked adds nothing.
|
|
776
|
+
const receipts = engineReceipts(report);
|
|
777
|
+
if (receipts)
|
|
778
|
+
resultObj.receipts = receipts;
|
|
714
779
|
}
|
|
715
780
|
// THE CODE ECHO WAS 90% OF THE RESPONSE, AND THE AGENT WROTE THE REQUEST.
|
|
716
781
|
//
|
|
@@ -728,7 +793,7 @@ function executedResult(code, result, screenshot, description, report) {
|
|
|
728
793
|
const content = [
|
|
729
794
|
{ type: 'text', text: JSON.stringify(resultObj, null, 2) },
|
|
730
795
|
];
|
|
731
|
-
if (
|
|
796
|
+
if (env('PINEPAPER_ECHO_CODE') === '1') {
|
|
732
797
|
content.push({ type: 'text', text: `Generated code:\n\`\`\`javascript\n${code}\n\`\`\`` });
|
|
733
798
|
}
|
|
734
799
|
if (screenshot) {
|
|
@@ -758,7 +823,7 @@ function screenshotResult(screenshot) {
|
|
|
758
823
|
* error code, not a program, and stripping those would take information away.
|
|
759
824
|
*/
|
|
760
825
|
function redactGeneratedCode(details) {
|
|
761
|
-
if (
|
|
826
|
+
if (env('PINEPAPER_ECHO_CODE') === '1')
|
|
762
827
|
return details;
|
|
763
828
|
if (!details || typeof details !== 'object' || Array.isArray(details))
|
|
764
829
|
return details;
|
|
@@ -777,6 +842,44 @@ function redactGeneratedCode(details) {
|
|
|
777
842
|
},
|
|
778
843
|
};
|
|
779
844
|
}
|
|
845
|
+
/**
|
|
846
|
+
* The studio's scene-lint warnings on an export (FxTool lintScene), one line each, so a centred title that runs
|
|
847
|
+
* out of the frame ("IGNAL" for "SIGNAL") is read on the success path rather than left in the JSON. Empty when none.
|
|
848
|
+
*/
|
|
849
|
+
export function lintLines(exportResult) {
|
|
850
|
+
const w = exportResult?.lintWarnings;
|
|
851
|
+
if (!Array.isArray(w) || !w.length)
|
|
852
|
+
return '';
|
|
853
|
+
return w.map((x) => `LINT: ${String(x?.message ?? x?.code ?? 'scene lint warning')}\n`).join('');
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* The file as ffprobe read it, when it disagrees with what the studio encoded: one line per field. A summary
|
|
857
|
+
* line when they agree; nothing when there is no summary.
|
|
858
|
+
*/
|
|
859
|
+
export function probeLines(receipt) {
|
|
860
|
+
const p = receipt.probe;
|
|
861
|
+
if (!p)
|
|
862
|
+
return '';
|
|
863
|
+
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'}`;
|
|
864
|
+
const c = receipt.probeCheck;
|
|
865
|
+
if (!c)
|
|
866
|
+
return `${head}\n`;
|
|
867
|
+
if (c.tool === null)
|
|
868
|
+
return `${head} (not read back: ${c.reason})\n`;
|
|
869
|
+
if (c.ok)
|
|
870
|
+
return `${head} (ffprobe agrees)\n`;
|
|
871
|
+
if (!c.mismatches?.length)
|
|
872
|
+
return `${head}\nPROBE: ${c.reason ?? 'ffprobe could not confirm the file'}\n`;
|
|
873
|
+
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('');
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* An export that failed at a step, as an error result: `{ step, path, reason }` in the data and in the words.
|
|
877
|
+
* A non-ExportStepError is named at `fallbackStep` with its own message, never "undefined".
|
|
878
|
+
*/
|
|
879
|
+
function exportStepErrorResult(e, fallbackStep, extra = {}, toolName = 'pinepaper_agent_export') {
|
|
880
|
+
const err = e instanceof ExportStepError ? e : new ExportStepError(fallbackStep, null, errorText(e, `the ${fallbackStep} step failed with no reason given`));
|
|
881
|
+
return errorResult(ErrorCodes.EXECUTION_ERROR, err.message, { ...err.toData(), ...extra }, { toolName });
|
|
882
|
+
}
|
|
780
883
|
export function errorResult(code, message, rawDetails, context) {
|
|
781
884
|
// THE GENERATED SCRIPT IS NOT THE ERROR.
|
|
782
885
|
//
|
|
@@ -811,15 +914,19 @@ export function errorResult(code, message, rawDetails, context) {
|
|
|
811
914
|
isError: true,
|
|
812
915
|
};
|
|
813
916
|
}
|
|
814
|
-
function handleValidationError(error, i18n) {
|
|
917
|
+
function handleValidationError(error, i18n, args) {
|
|
815
918
|
const message = i18n
|
|
816
919
|
? i18n.getError('validationError', { message: 'Invalid input parameters' })
|
|
817
920
|
: 'Invalid input parameters';
|
|
818
|
-
const details = error.
|
|
921
|
+
const details = error.issues.map((e) => {
|
|
819
922
|
const path = e.path.join('.');
|
|
820
923
|
const detail = { path, message: e.message };
|
|
821
|
-
|
|
822
|
-
|
|
924
|
+
// zod 4 names a value outside an enum `invalid_value` and no longer carries what was sent; it is read back from the
|
|
925
|
+
// call's own arguments along the issue's path, so the vocabulary hint ("did you mean …") still has a word to match.
|
|
926
|
+
if (e.code === 'invalid_value') {
|
|
927
|
+
let received = args;
|
|
928
|
+
for (const k of e.path)
|
|
929
|
+
received = received && typeof received === 'object' ? received[k] : undefined;
|
|
823
930
|
const hint = vocabularyHintForPath(path, received);
|
|
824
931
|
if (hint)
|
|
825
932
|
detail.vocabulary = hint;
|
|
@@ -979,9 +1086,7 @@ export async function customCodeRollback(controller, error) {
|
|
|
979
1086
|
removed = v.removed ?? 0;
|
|
980
1087
|
left = typeof v.left === 'number' ? v.left : -1;
|
|
981
1088
|
}
|
|
982
|
-
catch {
|
|
983
|
-
rolledBack = false;
|
|
984
|
-
}
|
|
1089
|
+
catch { /* rolledBack stays false: nothing was confirmed removed */ }
|
|
985
1090
|
const scene = rolledBack
|
|
986
1091
|
? (removed > 0
|
|
987
1092
|
? `The ${removed} item(s) this call created before it stopped were removed. Changes it made to items that already existed are not reverted.`
|
|
@@ -1062,23 +1167,34 @@ async function writeResultDataToFile(out, ext) {
|
|
|
1062
1167
|
return out;
|
|
1063
1168
|
}
|
|
1064
1169
|
const r = obj.result;
|
|
1065
|
-
if (!r || r.
|
|
1170
|
+
if (!r || r.success === false)
|
|
1066
1171
|
return out;
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1172
|
+
if (r.data === undefined || r.data === null) {
|
|
1173
|
+
// A result that says in fields that it wrote no file (png-sequence frames left in the page: `file: null` and
|
|
1174
|
+
// `why`) claims no file: it passes as it is. Any other success with nothing to write is the exit-0-without-file
|
|
1175
|
+
// case, named.
|
|
1176
|
+
if (r.file === null && typeof r.why === 'string' && r.why.trim())
|
|
1177
|
+
return out;
|
|
1178
|
+
const err = new ExportStepError('export', null, `the ${ext} export reported success but returned no file data`);
|
|
1179
|
+
return errorResult(ErrorCodes.EXECUTION_ERROR, err.message, { ...err.toData(), result: r });
|
|
1180
|
+
}
|
|
1074
1181
|
const dir = getExportDir();
|
|
1075
|
-
await mkdir(dir, { recursive: true });
|
|
1076
1182
|
const filePath = join(dir, `pinepaper_${String(r.format ?? 'export').replace(/[^a-z0-9-]/gi, '')}_${Date.now()}.${ext}`);
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1183
|
+
// No success without a file that exists and is not empty: each failure names its step, path and reason.
|
|
1184
|
+
try {
|
|
1185
|
+
const bytes = typeof r.data === 'string' ? bytesForSave(r.data, `the ${ext} export`) : Buffer.from(JSON.stringify(r.data), 'utf-8');
|
|
1186
|
+
await writeStep(filePath, async () => { await mkdir(dir, { recursive: true }); await writeFile(filePath, bytes); });
|
|
1187
|
+
await verifySavedFile(filePath, bytes.length);
|
|
1188
|
+
const { data: _d, ...rest } = r;
|
|
1189
|
+
void _d;
|
|
1190
|
+
obj.result = { ...rest, filePath, bytes: bytes.length };
|
|
1191
|
+
return { ...out, content: [{ type: 'text', text: JSON.stringify(obj, null, 2) }, ...out.content.slice(1), exportFileLink(filePath, bytes.length)] };
|
|
1192
|
+
}
|
|
1193
|
+
catch (e) {
|
|
1194
|
+
const err = e instanceof ExportStepError ? new ExportStepError(e.step, e.path ?? filePath, e.reason)
|
|
1195
|
+
: new ExportStepError('write', filePath, errorText(e, 'the file could not be written'));
|
|
1196
|
+
return errorResult(ErrorCodes.EXECUTION_ERROR, err.message, err.toData());
|
|
1197
|
+
}
|
|
1082
1198
|
}
|
|
1083
1199
|
/**
|
|
1084
1200
|
* pinepaper_capture_frames with `sheet`: one tiled image of the film, saved
|
|
@@ -1282,8 +1398,9 @@ ${code}
|
|
|
1282
1398
|
tracker.startTimer(`${timerId}_screenshot`);
|
|
1283
1399
|
}
|
|
1284
1400
|
// 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
|
-
|
|
1401
|
+
// emitter is not (its verdict already describes what it left behind), except
|
|
1402
|
+
// an atomic batch, whose contract is that a refused call changed nothing.
|
|
1403
|
+
const rollbackOnFailure = (toolName === 'pinepaper_execute_custom_code' || options.rollbackCreatedOnFailure === true)
|
|
1287
1404
|
&& await markItemsBeforeRun(controller);
|
|
1288
1405
|
const governorTimeoutMs = LONG_GOVERNOR_BUDGET_MS[toolName];
|
|
1289
1406
|
// NO SILENT SUCCESS (effect-class.ts / effect-check.ts): fingerprint a
|
|
@@ -1308,6 +1425,10 @@ ${code}
|
|
|
1308
1425
|
...(governorTimeoutMs ? { governorTimeoutMs } : {}),
|
|
1309
1426
|
});
|
|
1310
1427
|
const browserDuration = tracker.endTimer(`${timerId}_browser_execution`);
|
|
1428
|
+
// THE SCENE LINT RIDES OUT OF BAND: an emitter that ran app.lintScene() returns it under __ppLint, and
|
|
1429
|
+
// execute_custom_code returns its own value under __ppValue. Both are unwrapped HERE, before any verdict reads the
|
|
1430
|
+
// result, so the code's own value is what is judged and returned, and the lint sits beside it.
|
|
1431
|
+
const lintWarnings = unwrapLint(result);
|
|
1311
1432
|
// THE METRIC RECORDS THE CALL'S VERDICT, NOT THE TRANSPORT'S (gate H2,
|
|
1312
1433
|
// 1.6.19). result.success only says the code RAN; an emitter that ran and
|
|
1313
1434
|
// returned { success: false } is a failed call, and get_performance_metrics
|
|
@@ -1352,6 +1473,8 @@ ${code}
|
|
|
1352
1473
|
// PP_LOOP_BUDGET, PP_TIMEOUT) so the model can correct on the next turn.
|
|
1353
1474
|
{
|
|
1354
1475
|
code, errorCode: result.errorCode, governorReport: result.report,
|
|
1476
|
+
failures: stepFailures(toolName, result.result, result.error || 'Failed to execute code in browser'),
|
|
1477
|
+
...(lintWarnings.length ? { lintWarnings } : {}),
|
|
1355
1478
|
...(rollbackOnFailure ? await customCodeRollback(controller, result.error) : {}),
|
|
1356
1479
|
}, {
|
|
1357
1480
|
toolName,
|
|
@@ -1383,6 +1506,8 @@ ${code}
|
|
|
1383
1506
|
.slice(0, 3);
|
|
1384
1507
|
return withTiming(errorResult(ErrorCodes.EXECUTION_ERROR, said.length ? `${inner} The studio said: ${said.join(' | ')}` : inner, {
|
|
1385
1508
|
code, result: result.result, governorReport: result.report,
|
|
1509
|
+
failures: stepFailures(toolName, result.result, inner),
|
|
1510
|
+
...(lintWarnings.length ? { lintWarnings } : {}),
|
|
1386
1511
|
...(rollbackOnFailure ? await customCodeRollback(controller, inner) : {}),
|
|
1387
1512
|
}, { toolName, canvasState: canvasState || undefined }), timing);
|
|
1388
1513
|
}
|
|
@@ -1434,6 +1559,46 @@ ${code}
|
|
|
1434
1559
|
* string with no `success: true` beside it is a failure too — it is what the
|
|
1435
1560
|
* emitters' catch blocks return.
|
|
1436
1561
|
*/
|
|
1562
|
+
/**
|
|
1563
|
+
* Move an emitter's out-of-band scene lint (`__ppLint`) off its result, and unwrap execute_custom_code's own value
|
|
1564
|
+
* (`__ppValue`). The lint is also put on `run.report.lintWarnings`, where executedResult reads it. Returns the lint.
|
|
1565
|
+
*/
|
|
1566
|
+
export function unwrapLint(run) {
|
|
1567
|
+
const r = run.result;
|
|
1568
|
+
if (!r || typeof r !== 'object' || Array.isArray(r) || !('__ppLint' in r))
|
|
1569
|
+
return [];
|
|
1570
|
+
const lint = Array.isArray(r.__ppLint) ? r.__ppLint : [];
|
|
1571
|
+
if ('__ppValue' in r || r.__ppWrapped === true)
|
|
1572
|
+
run.result = r.__ppValue;
|
|
1573
|
+
else {
|
|
1574
|
+
const { __ppLint: _l, ...rest } = r;
|
|
1575
|
+
void _l;
|
|
1576
|
+
run.result = rest;
|
|
1577
|
+
}
|
|
1578
|
+
if (lint.length)
|
|
1579
|
+
run.report = { ...(run.report ?? {}), lintWarnings: lint };
|
|
1580
|
+
return lint;
|
|
1581
|
+
}
|
|
1582
|
+
/**
|
|
1583
|
+
* Every failed step of a call as `{ step, itemId, reason }`, never a bare message: a batch's own `failures`, a
|
|
1584
|
+
* result that names its step, or the call itself with the item it named.
|
|
1585
|
+
*/
|
|
1586
|
+
export function stepFailures(toolName, value, message) {
|
|
1587
|
+
const v = value;
|
|
1588
|
+
const reasonOf = (x) => (typeof x === 'string' && x.trim() ? x : '');
|
|
1589
|
+
if (v && typeof v === 'object' && Array.isArray(v.failures) && v.failures.length) {
|
|
1590
|
+
return v.failures.map((f) => ({
|
|
1591
|
+
step: reasonOf(f.step) || toolName,
|
|
1592
|
+
itemId: typeof f.itemId === 'string' ? f.itemId : null,
|
|
1593
|
+
reason: reasonOf(f.reason) || reasonOf(f.error) || message,
|
|
1594
|
+
}));
|
|
1595
|
+
}
|
|
1596
|
+
return [{
|
|
1597
|
+
step: (v && typeof v === 'object' && reasonOf(v.step)) || toolName,
|
|
1598
|
+
itemId: v && typeof v === 'object' && typeof v.itemId === 'string' ? v.itemId : null,
|
|
1599
|
+
reason: (v && typeof v === 'object' && (reasonOf(v.reason) || reasonOf(v.error))) || reasonOf(message) || `${toolName} failed with no reason given`,
|
|
1600
|
+
}];
|
|
1601
|
+
}
|
|
1437
1602
|
export function innerFailure(value) {
|
|
1438
1603
|
if (!value || typeof value !== 'object')
|
|
1439
1604
|
return null;
|
|
@@ -1605,6 +1770,15 @@ export const HIDDEN_ARGUMENTS = {
|
|
|
1605
1770
|
pinepaper_query_capabilities: ['query'],
|
|
1606
1771
|
pinepaper_text_effect: ['startAt', 'hideAfter'],
|
|
1607
1772
|
};
|
|
1773
|
+
/**
|
|
1774
|
+
* Nested keys (paths with [] for any array index) a handler accepts that its published schema closes over without
|
|
1775
|
+
* listing. Shrink-only, held exact by nested-ignored-coverage.test.ts; publishing one means deleting it here.
|
|
1776
|
+
*/
|
|
1777
|
+
export const NESTED_HIDDEN = {
|
|
1778
|
+
pinepaper_create_item: ['keyframes[].hold', 'keyframes[].interpolation'],
|
|
1779
|
+
pinepaper_create_scene: ['animations[].startTime'],
|
|
1780
|
+
pinepaper_keyframe_animate: ['keyframes[].hold', 'keyframes[].interpolation'],
|
|
1781
|
+
};
|
|
1608
1782
|
/** Tools whose schema passes every key through: nothing is dropped. */
|
|
1609
1783
|
export const PASSTHROUGH_ARGUMENT_TOOLS = new Set(['pinepaper_equation_path', 'pinepaper_ui_kit']);
|
|
1610
1784
|
const IGNORED_ARGUMENT_HINTS = {
|
|
@@ -1635,8 +1809,9 @@ export function ignoredArgumentKeys(toolName, args, published) {
|
|
|
1635
1809
|
return [];
|
|
1636
1810
|
const known = new Set([...(published ?? []), ...(HIDDEN_ARGUMENTS[toolName] ?? [])]);
|
|
1637
1811
|
let schema = IGNORED_ARGUMENT_SCHEMAS[toolName];
|
|
1638
|
-
|
|
1639
|
-
|
|
1812
|
+
// zod 4: a preprocess or transform is a ZodPipe (preprocess: transform → object; .transform: object → transform).
|
|
1813
|
+
while (schema instanceof z.ZodPipe)
|
|
1814
|
+
schema = (schema._def.in instanceof z.ZodObject || !(schema._def.out instanceof z.ZodObject) ? schema._def.in : schema._def.out);
|
|
1640
1815
|
if (schema instanceof z.ZodObject)
|
|
1641
1816
|
Object.keys(schema.shape).forEach((k) => known.add(k));
|
|
1642
1817
|
else if (published === undefined)
|
|
@@ -1651,13 +1826,66 @@ export function ignoredArgumentsNote(toolName, args, published) {
|
|
|
1651
1826
|
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.`
|
|
1652
1827
|
+ (hints.length ? ` ${[...new Set(hints)].join(' ')}` : '');
|
|
1653
1828
|
}
|
|
1654
|
-
/** The same check, against the tool's published schema. */
|
|
1829
|
+
/** The same check, against the tool's published schema, and below the top level (AR-1 R2: nested-ignored.ts). */
|
|
1655
1830
|
export async function ignoredArgumentsNoteFor(toolName, args) {
|
|
1656
1831
|
const published = await publishedArguments(toolName);
|
|
1657
|
-
const
|
|
1832
|
+
const top = ignoredArgumentKeys(toolName, args, published);
|
|
1833
|
+
const nested = PASSTHROUGH_ARGUMENT_TOOLS.has(toolName) || published === null ? [] : await nestedIgnoredFor(toolName, args);
|
|
1834
|
+
const ignored = [...top, ...nested];
|
|
1658
1835
|
if (!ignored.length)
|
|
1659
1836
|
return null;
|
|
1660
|
-
|
|
1837
|
+
const topNote = top.length ? ignoredArgumentsNote(toolName, args, published) : '';
|
|
1838
|
+
const nestedNote = nested.length
|
|
1839
|
+
? `IGNORED ARGUMENTS: ${nested.join(', ')} ${nested.length > 1 ? 'are' : 'is'} not a parameter of ${toolName}, so ${nested.length > 1 ? 'they had' : 'it had'} no effect.`
|
|
1840
|
+
: '';
|
|
1841
|
+
return { note: [topNote, nestedNote].filter(Boolean).join(' '), ignored };
|
|
1842
|
+
}
|
|
1843
|
+
async function nestedIgnoredFor(toolName, args) {
|
|
1844
|
+
const { PINEPAPER_TOOLS } = await import('./definitions.js');
|
|
1845
|
+
const tool = PINEPAPER_TOOLS.find((t) => t.name === toolName);
|
|
1846
|
+
if (!tool)
|
|
1847
|
+
return [];
|
|
1848
|
+
const hidden = new Set(NESTED_HIDDEN[toolName] ?? []);
|
|
1849
|
+
const paths = nestedIgnoredPaths(tool.inputSchema, args).filter((p) => !hidden.has(p.replace(/\[\d+\]/g, '[]')));
|
|
1850
|
+
return toolName === 'pinepaper_agent_batch_execute' ? [...paths, ...batchOpIgnoredPaths(args)] : paths;
|
|
1851
|
+
}
|
|
1852
|
+
/**
|
|
1853
|
+
* Batch op keys the batch reads that AgentBatchOperationSchema does not list. The op schema is passthrough (a create
|
|
1854
|
+
* takes shape properties flat), so these arrive intact; batch-op-keys.test.ts fails when the batch code reads a key
|
|
1855
|
+
* that is in neither list, so a working key is never reported. Shrink-only.
|
|
1856
|
+
*/
|
|
1857
|
+
export const BATCH_OP_HIDDEN = {
|
|
1858
|
+
'*': ['_step', 'id'],
|
|
1859
|
+
keyframe_animate: ['params', 'timeUnits', 'timeOffset', 'clipInPoint', 'clipOutPoint'],
|
|
1860
|
+
timeline: ['repeat', 'yoyo', 'anchor'],
|
|
1861
|
+
};
|
|
1862
|
+
/** Hints for the slips that land on a key another op type reads. */
|
|
1863
|
+
const BATCH_OP_HINTS = {
|
|
1864
|
+
'relation.params': 'a relation op takes its parameters as relationOptions',
|
|
1865
|
+
'modify.x': 'a modify op takes changes inside properties', 'modify.y': 'a modify op takes changes inside properties',
|
|
1866
|
+
};
|
|
1867
|
+
/**
|
|
1868
|
+
* A key on a batch op that nothing reads (the passthrough schema let it in, so the nested walk cannot see it). A
|
|
1869
|
+
* `create` folds every op-level key into its properties, which create_item's own property report covers, so only the
|
|
1870
|
+
* other op types are checked here.
|
|
1871
|
+
*/
|
|
1872
|
+
export function batchOpIgnoredPaths(args) {
|
|
1873
|
+
const ops = Array.isArray(args.operations) ? args.operations : [];
|
|
1874
|
+
const shape = Object.keys(AgentBatchOperationSchema.shape);
|
|
1875
|
+
const out = [];
|
|
1876
|
+
ops.forEach((op, i) => {
|
|
1877
|
+
const type = op && typeof op === 'object' && !Array.isArray(op) ? String(op.type) : 'create';
|
|
1878
|
+
if (type === 'create')
|
|
1879
|
+
return;
|
|
1880
|
+
const known = new Set([...shape, ...BATCH_OP_HIDDEN['*'], ...(BATCH_OP_HIDDEN[type] ?? [])]);
|
|
1881
|
+
for (const k of Object.keys(op)) {
|
|
1882
|
+
if (known.has(k))
|
|
1883
|
+
continue;
|
|
1884
|
+
const hint = BATCH_OP_HINTS[`${type}.${k}`];
|
|
1885
|
+
out.push(`operations[${i}].${k}${hint ? ` (${hint})` : ''}`);
|
|
1886
|
+
}
|
|
1887
|
+
});
|
|
1888
|
+
return out.slice(0, 50);
|
|
1661
1889
|
}
|
|
1662
1890
|
export const IGNORED_ARGUMENTS_META_KEY = 'pinepaper.studio/ignoredArguments';
|
|
1663
1891
|
export async function handleToolCall(toolName, args, options = {}) {
|
|
@@ -1798,6 +2026,11 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
|
|
|
1798
2026
|
const code = codeGenerator.generateStagger(input);
|
|
1799
2027
|
return executeOrGenerate(code, `Stagger: ${input.action}`, options, 'pinepaper_stagger');
|
|
1800
2028
|
}
|
|
2029
|
+
case 'pinepaper_pattern': {
|
|
2030
|
+
const input = PatternInputSchema.parse(args);
|
|
2031
|
+
const code = codeGenerator.generatePattern(input);
|
|
2032
|
+
return executeOrGenerate(code, `Pattern: ${input.action}${input.id ? ' ' + input.id : ''}`, options, 'pinepaper_pattern');
|
|
2033
|
+
}
|
|
1801
2034
|
case 'pinepaper_flip': {
|
|
1802
2035
|
const input = FlipInputSchema.parse(args);
|
|
1803
2036
|
const code = codeGenerator.generateFlip(input);
|
|
@@ -2396,7 +2629,7 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
|
|
|
2396
2629
|
const controller = options.browserController || getBrowserController();
|
|
2397
2630
|
if (!controller.connected)
|
|
2398
2631
|
await controller.connect();
|
|
2399
|
-
const r = await controller.executeCode(`(function() { const ids = ${
|
|
2632
|
+
const r = await controller.executeCode(`(function() { const ids = ${jsLiteral(itemIds)}; return ids.filter(function(id) { const e = app.itemRegistry && app.itemRegistry.get(id); return !(e && e.item); }); })();`, false);
|
|
2400
2633
|
if (r.success && Array.isArray(r.result))
|
|
2401
2634
|
missingItems = r.result;
|
|
2402
2635
|
}
|
|
@@ -2840,7 +3073,7 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
|
|
|
2840
3073
|
// Falls back to the PINEPAPER_STUDIO_URL env var, else PuppeteerController's
|
|
2841
3074
|
// built-in prod default. "/editor" + agent params are appended by the controller.
|
|
2842
3075
|
const rawUrl = typeof args.url === 'string' ? args.url.trim() : '';
|
|
2843
|
-
const studioUrl = (rawUrl || (
|
|
3076
|
+
const studioUrl = (rawUrl || (env('PINEPAPER_STUDIO_URL') || '').trim()) || undefined;
|
|
2844
3077
|
// Only http(s) is supported — reject file://, data://, javascript:, etc.
|
|
2845
3078
|
if (studioUrl) {
|
|
2846
3079
|
const v = validateStudioUrl(studioUrl);
|
|
@@ -2993,7 +3226,7 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
|
|
|
2993
3226
|
version: SERVER_VERSION,
|
|
2994
3227
|
toolkit: detectToolkit(),
|
|
2995
3228
|
verbosity: detectVerbosity(),
|
|
2996
|
-
locale:
|
|
3229
|
+
locale: env('PINEPAPER_LOCALE') || 'en',
|
|
2997
3230
|
executionMode: options.executionMode || getExecutionMode(),
|
|
2998
3231
|
screenshotMode: options.screenshotMode || getScreenshotMode(),
|
|
2999
3232
|
};
|
|
@@ -3095,6 +3328,7 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
|
|
|
3095
3328
|
})),
|
|
3096
3329
|
profiles: {
|
|
3097
3330
|
'full+verbose': fullTokens,
|
|
3331
|
+
'core+compact': Math.ceil(Buffer.byteLength(JSON.stringify(getToolsForToolkit(getToolsForVerbosity('compact'), 'core')), 'utf-8') / 4),
|
|
3098
3332
|
'agent+compact': agentCompactTokens,
|
|
3099
3333
|
'agent+minimal': agentMinimalTokens,
|
|
3100
3334
|
},
|
|
@@ -3107,7 +3341,7 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
|
|
|
3107
3341
|
const recs = toolDefsSection.recommendations;
|
|
3108
3342
|
const alternatives = [];
|
|
3109
3343
|
for (const v of ['compact', 'minimal']) {
|
|
3110
|
-
for (const k of ['full', 'agent', 'minimal']) {
|
|
3344
|
+
for (const k of ['full', 'agent', 'core', 'minimal']) {
|
|
3111
3345
|
if (k === toolkit && v === verbosity)
|
|
3112
3346
|
continue;
|
|
3113
3347
|
const t = Math.ceil(Buffer.byteLength(JSON.stringify(getToolsForToolkit(getToolsForVerbosity(v), k)), 'utf-8') / 4);
|
|
@@ -3549,7 +3783,7 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
|
|
|
3549
3783
|
type: 'text',
|
|
3550
3784
|
// The script is not part of the answer here either — same reason
|
|
3551
3785
|
// errors no longer carry it. PINEPAPER_ECHO_CODE=1 brings it back.
|
|
3552
|
-
text: (
|
|
3786
|
+
text: (env('PINEPAPER_ECHO_CODE') === '1'
|
|
3553
3787
|
? `Executed PinePaper code:\n\n\`\`\`javascript\n${code}\n\`\`\`\n\n`
|
|
3554
3788
|
: '')
|
|
3555
3789
|
+ `Full screenshot saved to: ${filePath} (${(fileSize / 1024).toFixed(1)} KB)\n\nResult: ${JSON.stringify(cleanResult, null, 2)}`,
|
|
@@ -3611,8 +3845,26 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
|
|
|
3611
3845
|
})));
|
|
3612
3846
|
}
|
|
3613
3847
|
}
|
|
3848
|
+
let expanded;
|
|
3849
|
+
try {
|
|
3850
|
+
expanded = expandBatchMacros(input.operations);
|
|
3851
|
+
}
|
|
3852
|
+
catch (e) {
|
|
3853
|
+
if (e instanceof BatchMacroError)
|
|
3854
|
+
return errorResult(ErrorCodes.VALIDATION_ERROR, e.message, {}, { toolName: 'pinepaper_agent_batch_execute' });
|
|
3855
|
+
throw e;
|
|
3856
|
+
}
|
|
3857
|
+
// A timeline op compiles on the server: a tween it cannot honour is refused by name here, before the canvas is
|
|
3858
|
+
// touched (skipValidation does not bypass it — there is nothing to send for a tween that did not compile).
|
|
3859
|
+
if (expanded.some((op) => op.type === 'timeline')) {
|
|
3860
|
+
const tl = compileBatchTimelines(expanded);
|
|
3861
|
+
if (tl.errors.length > 0) {
|
|
3862
|
+
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 })));
|
|
3863
|
+
}
|
|
3864
|
+
}
|
|
3614
3865
|
const code = codeGenerator.generateAgentBatchExecute(input);
|
|
3615
|
-
const description = `Batch executed ${input.operations.length} operations${input.atomic !== false ? ' (atomic)' : ''}
|
|
3866
|
+
const description = `Batch executed ${input.operations.length} operations${input.atomic !== false ? ' (atomic)' : ''}`
|
|
3867
|
+
+ (expanded.length !== input.operations.length ? ` (${expanded.length} after expanding ring / grid / orbit / stagger)` : '');
|
|
3616
3868
|
// Check for out-of-bounds items before execution
|
|
3617
3869
|
let boundsWarning = '';
|
|
3618
3870
|
const sizeOp = input.operations.find((op) => op.type === 'set_canvas_size');
|
|
@@ -3620,7 +3872,7 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
|
|
|
3620
3872
|
const cw = sizeOp.width || 1920;
|
|
3621
3873
|
const ch = sizeOp.height || 1080;
|
|
3622
3874
|
const oobItems = [];
|
|
3623
|
-
|
|
3875
|
+
expanded.forEach((op, idx) => {
|
|
3624
3876
|
if (op.type === 'create' && op.position) {
|
|
3625
3877
|
const { x, y } = op.position;
|
|
3626
3878
|
if (x < 0 || y < 0 || x > cw || y > ch) {
|
|
@@ -3632,7 +3884,7 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
|
|
|
3632
3884
|
boundsWarning = `\n\nWARNING: ${oobItems.length} item(s) positioned outside canvas bounds (${cw}x${ch}): ${oobItems.join(', ')}. Items may be clipped or invisible.`;
|
|
3633
3885
|
}
|
|
3634
3886
|
}
|
|
3635
|
-
const batchResult = await executeOrGenerate(code, description, options, 'pinepaper_agent_batch_execute');
|
|
3887
|
+
const batchResult = await executeOrGenerate(code, description, input.atomic !== false ? { ...options, rollbackCreatedOnFailure: true } : options, 'pinepaper_agent_batch_execute');
|
|
3636
3888
|
// Append bounds warning if any
|
|
3637
3889
|
if (boundsWarning && batchResult.content && batchResult.content.length > 0) {
|
|
3638
3890
|
const firstContent = batchResult.content[0];
|
|
@@ -3804,13 +4056,14 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
|
|
|
3804
4056
|
format,
|
|
3805
4057
|
chunkBytes: Number(exportResult.chunkBytes) || undefined,
|
|
3806
4058
|
}, input.platform || 'auto');
|
|
3807
|
-
const
|
|
3808
|
-
|
|
4059
|
+
const receipt = await exportFileReceiptOrReason(filePath, exportResult);
|
|
4060
|
+
const cleanResult = { ...exportResult, data: undefined, filePath, fileSize, chunks, file: receipt };
|
|
4061
|
+
return withEngineReport({
|
|
3809
4062
|
content: [{
|
|
3810
4063
|
type: 'text',
|
|
3811
|
-
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)}`,
|
|
3812
|
-
}],
|
|
3813
|
-
};
|
|
4064
|
+
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${receiptErrorLine(receipt)}${'receiptError' in receipt ? '' : probeLines(receipt)}${lintLines(exportResult)}\nResult: ${JSON.stringify(cleanResult, null, 2)}`,
|
|
4065
|
+
}, exportFileLink(filePath, fileSize)],
|
|
4066
|
+
}, exportBrowserResult.report);
|
|
3814
4067
|
}
|
|
3815
4068
|
catch (streamError) {
|
|
3816
4069
|
// The export is still held — the message says so and names the id,
|
|
@@ -3824,7 +4077,14 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
|
|
|
3824
4077
|
// SCC (CEA-608, 8.37): the studio sent the cues; encoded here.
|
|
3825
4078
|
if (format === 'scc' && Array.isArray(exportResult?.sccCues)) {
|
|
3826
4079
|
const scc = buildScc(exportResult.sccCues);
|
|
3827
|
-
|
|
4080
|
+
let saved;
|
|
4081
|
+
try {
|
|
4082
|
+
saved = await saveExportToFile(scc.text, 'scc', input.platform || 'auto');
|
|
4083
|
+
}
|
|
4084
|
+
catch (saveError) {
|
|
4085
|
+
return exportStepErrorResult(saveError, 'save', { format: 'scc' });
|
|
4086
|
+
}
|
|
4087
|
+
const { filePath, fileSize } = saved;
|
|
3828
4088
|
const warnings = [];
|
|
3829
4089
|
if (scc.truncated)
|
|
3830
4090
|
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.` });
|
|
@@ -3835,10 +4095,22 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
|
|
|
3835
4095
|
const { sccCues: _c, ...rest } = exportResult;
|
|
3836
4096
|
void _c;
|
|
3837
4097
|
const cleanResult = { ...rest, filePath, fileSize, frameRate: '29.97 drop-frame', fidelity: { warnings } };
|
|
3838
|
-
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)}` }] };
|
|
4098
|
+
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)}` }, exportFileLink(filePath, fileSize)] }, exportBrowserResult.report);
|
|
3839
4099
|
}
|
|
3840
4100
|
if ((format === 'html5-ad' || format === 'playable') && typeof exportResult?.adPage === 'string') {
|
|
3841
|
-
|
|
4101
|
+
try {
|
|
4102
|
+
return withEngineReport(await saveAdExport(exportResult, input, input.platform || 'auto'), exportBrowserResult.report);
|
|
4103
|
+
}
|
|
4104
|
+
catch (saveError) {
|
|
4105
|
+
return exportStepErrorResult(saveError, 'save', { format });
|
|
4106
|
+
}
|
|
4107
|
+
}
|
|
4108
|
+
// NO SUCCESS WITHOUT BYTES. Every delivering branch above returned; what is left must carry the file's
|
|
4109
|
+
// data. A success with none (no data field, an empty string) used to fall through to the inline return and
|
|
4110
|
+
// report success with nothing in it. An estimate is the one answer that is not a file.
|
|
4111
|
+
if (!exportResult?.estimateOnly && !(typeof data === 'string' && data.length > 0)) {
|
|
4112
|
+
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}`})`);
|
|
4113
|
+
return errorResult(ErrorCodes.EXECUTION_ERROR, err.message, { ...err.toData(), format, result: exportResult }, { toolName: 'pinepaper_agent_export' });
|
|
3842
4114
|
}
|
|
3843
4115
|
const shouldSaveToFile = data && typeof data === 'string' && (options.saveToFile ||
|
|
3844
4116
|
ALWAYS_SAVE_FORMATS.has(format) ||
|
|
@@ -3846,29 +4118,33 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
|
|
|
3846
4118
|
if (shouldSaveToFile) {
|
|
3847
4119
|
try {
|
|
3848
4120
|
const { filePath, fileSize } = await saveExportToFile(data, format, input.platform || 'auto');
|
|
3849
|
-
const
|
|
4121
|
+
const receipt = await exportFileReceiptOrReason(filePath, exportResult);
|
|
4122
|
+
const cleanResult = { ...exportResult, data: undefined, filePath, fileSize, file: receipt };
|
|
3850
4123
|
// A mix that dropped sounds wrote a perfectly good file that is
|
|
3851
4124
|
// missing tracks. It rides the SUCCESS path, which is exactly why
|
|
3852
4125
|
// it has to be said out loud here rather than left in the JSON.
|
|
3853
4126
|
const dropNote = exportResult.incomplete;
|
|
3854
|
-
return {
|
|
4127
|
+
return withEngineReport({
|
|
3855
4128
|
content: [{
|
|
3856
4129
|
type: 'text',
|
|
3857
4130
|
text: `Export saved to file:\n\nFile: ${filePath}\nFormat: ${format}\nSize: ${(fileSize / 1024).toFixed(1)} KB\nPlatform: ${input.platform}\n`
|
|
3858
4131
|
+ (dropNote ? `DROPPED: ${dropNote}\n` : '')
|
|
4132
|
+
+ receiptErrorLine(receipt)
|
|
4133
|
+
+ ('receiptError' in receipt ? '' : probeLines(receipt))
|
|
4134
|
+
+ lintLines(exportResult)
|
|
3859
4135
|
+ `\nResult: ${JSON.stringify(cleanResult, null, 2)}`,
|
|
3860
|
-
}],
|
|
3861
|
-
};
|
|
4136
|
+
}, exportFileLink(filePath, fileSize)],
|
|
4137
|
+
}, exportBrowserResult.report);
|
|
3862
4138
|
}
|
|
3863
4139
|
catch (saveError) {
|
|
4140
|
+
// A failed save is a failed export, never a success with a note inside it: the step, the path and
|
|
4141
|
+
// the reason, and the data stripped so the error is not the size of the video.
|
|
3864
4142
|
console.error('[PinePaper] Failed to save export to file:', saveError);
|
|
3865
|
-
|
|
3866
|
-
const strippedResult = { ...exportResult, data: `[${format} export data — file save failed: ${saveError instanceof Error ? saveError.message : 'unknown error'}]` };
|
|
3867
|
-
return executedResult(code, strippedResult, exportBrowserResult.screenshot, description);
|
|
4143
|
+
return exportStepErrorResult(saveError, 'save', { format, bytes: typeof data === 'string' ? data.length : 0 });
|
|
3868
4144
|
}
|
|
3869
4145
|
}
|
|
3870
|
-
// Small export — return inline (existing behavior)
|
|
3871
|
-
return executedResult(code, exportResult, exportBrowserResult.screenshot, description);
|
|
4146
|
+
// Small export — return inline (existing behavior). The bytes are in the result, checked non-empty above.
|
|
4147
|
+
return executedResult(code, exportResult, exportBrowserResult.screenshot, description, exportBrowserResult.report);
|
|
3872
4148
|
}
|
|
3873
4149
|
case 'pinepaper_design_styles':
|
|
3874
4150
|
return dataResult({ styles: designSystems.listStyles() });
|
|
@@ -4039,11 +4315,8 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
|
|
|
4039
4315
|
};
|
|
4040
4316
|
}
|
|
4041
4317
|
catch (saveError) {
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
wavBase64: `[${soundtrack.byteLength} bytes of WAV — file save failed: ${saveError instanceof Error ? saveError.message : 'unknown error'}]`,
|
|
4045
|
-
};
|
|
4046
|
-
return executedResult(code, stripped, wavResult.screenshot, soundDescription);
|
|
4318
|
+
// The soundtrack rendered; writing it did not. That is a failed export, said as one.
|
|
4319
|
+
return exportStepErrorResult(saveError, 'save', { format: 'wav', byteLength: soundtrack.byteLength }, 'pinepaper_sound');
|
|
4047
4320
|
}
|
|
4048
4321
|
}
|
|
4049
4322
|
return executeOrGenerate(code, soundDescription, options, 'pinepaper_sound');
|
|
@@ -4292,7 +4565,7 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
|
|
|
4292
4565
|
}
|
|
4293
4566
|
catch (error) {
|
|
4294
4567
|
if (error instanceof ZodError) {
|
|
4295
|
-
return handleValidationError(error, i18n);
|
|
4568
|
+
return handleValidationError(error, i18n, args);
|
|
4296
4569
|
}
|
|
4297
4570
|
// Try to capture canvas state for error context (if browser is connected)
|
|
4298
4571
|
let canvasState = null;
|
|
@@ -4305,6 +4578,10 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
|
|
|
4305
4578
|
catch (_stateError) {
|
|
4306
4579
|
// Ignore errors from canvas state capture
|
|
4307
4580
|
}
|
|
4581
|
+
// An export that failed at a named step keeps its step, path and reason as fields.
|
|
4582
|
+
if (error instanceof ExportStepError) {
|
|
4583
|
+
return errorResult(ErrorCodes.EXECUTION_ERROR, error.message, error.toData(), { toolName, canvasState: canvasState || undefined });
|
|
4584
|
+
}
|
|
4308
4585
|
if (error instanceof Error) {
|
|
4309
4586
|
const message = i18n
|
|
4310
4587
|
? i18n.getError('executionError', { message: error.message })
|