@pinepaper.studio/mcp-server 1.6.7 → 1.6.9
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 +9 -5
- package/README.es.md +9 -5
- package/README.fr.md +9 -5
- package/README.hi.md +8 -5
- package/README.ja.md +8 -5
- package/README.ko.md +8 -4
- package/README.md +256 -17
- package/README.pt-BR.md +9 -5
- package/README.zh-CN.md +6 -4
- package/dist/browser/index.d.ts +1 -1
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +1 -1
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/puppeteer-controller.d.ts +86 -2
- package/dist/browser/puppeteer-controller.d.ts.map +1 -1
- package/dist/browser/puppeteer-controller.js +143 -60
- package/dist/browser/puppeteer-controller.js.map +1 -1
- package/dist/character/data/concepts.json +1 -0
- package/dist/character/data/depictions.json +1 -0
- package/dist/character/data/styles.json +1 -0
- package/dist/character/duration-scale.d.ts +13 -0
- package/dist/character/duration-scale.d.ts.map +1 -0
- package/dist/character/duration-scale.js +13 -0
- package/dist/character/duration-scale.js.map +1 -0
- package/dist/character/path-bbox.d.ts +82 -0
- package/dist/character/path-bbox.d.ts.map +1 -0
- package/dist/character/path-bbox.js +496 -0
- package/dist/character/path-bbox.js.map +1 -0
- package/dist/character/perform.d.ts +149 -0
- package/dist/character/perform.d.ts.map +1 -0
- package/dist/character/perform.js +626 -0
- package/dist/character/perform.js.map +1 -0
- package/dist/character/resolve.d.ts +51 -0
- package/dist/character/resolve.d.ts.map +1 -0
- package/dist/character/resolve.js +239 -0
- package/dist/character/resolve.js.map +1 -0
- package/dist/cli.js +12 -0
- package/dist/cli.js.map +1 -1
- package/dist/design/design-systems.d.ts +224 -0
- package/dist/design/design-systems.d.ts.map +1 -0
- package/dist/design/design-systems.js +434 -0
- package/dist/design/design-systems.js.map +1 -0
- package/dist/execution/canvas-state.d.ts +7 -0
- package/dist/execution/canvas-state.d.ts.map +1 -1
- package/dist/execution/canvas-state.js +20 -8
- package/dist/execution/canvas-state.js.map +1 -1
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -4
- package/dist/index.js.map +1 -1
- package/dist/ontology/design-graph.d.ts +9 -0
- package/dist/ontology/design-graph.d.ts.map +1 -1
- package/dist/ontology/design-graph.js +98 -3
- package/dist/ontology/design-graph.js.map +1 -1
- package/dist/ontology/types.d.ts +42 -0
- package/dist/ontology/types.d.ts.map +1 -1
- package/dist/ontology/vocabulary.d.ts.map +1 -1
- package/dist/ontology/vocabulary.js +20 -0
- package/dist/ontology/vocabulary.js.map +1 -1
- package/dist/tools/compact-descriptions.d.ts.map +1 -1
- package/dist/tools/compact-descriptions.js +25 -0
- package/dist/tools/compact-descriptions.js.map +1 -1
- package/dist/tools/definitions.d.ts +1 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +404 -25
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/engine-methods.d.ts +18 -0
- package/dist/tools/engine-methods.d.ts.map +1 -0
- package/dist/tools/engine-methods.js +241 -0
- package/dist/tools/engine-methods.js.map +1 -0
- package/dist/tools/handlers/character.d.ts +121 -0
- package/dist/tools/handlers/character.d.ts.map +1 -0
- package/dist/tools/handlers/character.js +315 -0
- package/dist/tools/handlers/character.js.map +1 -0
- package/dist/tools/handlers.d.ts +2 -0
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +522 -18
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/minimal-descriptions.d.ts.map +1 -1
- package/dist/tools/minimal-descriptions.js +15 -3
- package/dist/tools/minimal-descriptions.js.map +1 -1
- package/dist/tools/toolkits.d.ts.map +1 -1
- package/dist/tools/toolkits.js +8 -0
- package/dist/tools/toolkits.js.map +1 -1
- package/dist/types/code-generator.d.ts +106 -1
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +1273 -18
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +6805 -124
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +627 -17
- package/dist/types/schemas.js.map +1 -1
- package/dist/vendor/design/art-deco-generator.d.ts +52 -0
- package/dist/vendor/design/art-deco-generator.d.ts.map +1 -0
- package/dist/vendor/design/art-deco-generator.js +144 -0
- package/dist/vendor/design/art-deco-generator.js.map +1 -0
- package/dist/vendor/design/art-nouveau-generator.d.ts +46 -0
- package/dist/vendor/design/art-nouveau-generator.d.ts.map +1 -0
- package/dist/vendor/design/art-nouveau-generator.js +134 -0
- package/dist/vendor/design/art-nouveau-generator.js.map +1 -0
- package/dist/vendor/design/bauhaus-generator.d.ts +53 -0
- package/dist/vendor/design/bauhaus-generator.d.ts.map +1 -0
- package/dist/vendor/design/bauhaus-generator.js +126 -0
- package/dist/vendor/design/bauhaus-generator.js.map +1 -0
- package/dist/vendor/design/brutalist-generator.d.ts +53 -0
- package/dist/vendor/design/brutalist-generator.d.ts.map +1 -0
- package/dist/vendor/design/brutalist-generator.js +148 -0
- package/dist/vendor/design/brutalist-generator.js.map +1 -0
- package/dist/vendor/design/claymorphism-generator.d.ts +46 -0
- package/dist/vendor/design/claymorphism-generator.d.ts.map +1 -0
- package/dist/vendor/design/claymorphism-generator.js +135 -0
- package/dist/vendor/design/claymorphism-generator.js.map +1 -0
- package/dist/vendor/design/corporate-memphis-generator.d.ts +47 -0
- package/dist/vendor/design/corporate-memphis-generator.d.ts.map +1 -0
- package/dist/vendor/design/corporate-memphis-generator.js +144 -0
- package/dist/vendor/design/corporate-memphis-generator.js.map +1 -0
- package/dist/vendor/design/cyberpunk-generator.d.ts +52 -0
- package/dist/vendor/design/cyberpunk-generator.d.ts.map +1 -0
- package/dist/vendor/design/cyberpunk-generator.js +162 -0
- package/dist/vendor/design/cyberpunk-generator.js.map +1 -0
- package/dist/vendor/design/de-stijl-generator.d.ts +50 -0
- package/dist/vendor/design/de-stijl-generator.d.ts.map +1 -0
- package/dist/vendor/design/de-stijl-generator.js +154 -0
- package/dist/vendor/design/de-stijl-generator.js.map +1 -0
- package/dist/vendor/design/design-style.d.ts +3 -0
- package/dist/vendor/design/design-style.d.ts.map +1 -0
- package/dist/vendor/design/design-style.js +12 -0
- package/dist/vendor/design/design-style.js.map +1 -0
- package/dist/vendor/design/design-systems-dtcg.d.ts +59 -0
- package/dist/vendor/design/design-systems-dtcg.d.ts.map +1 -0
- package/dist/vendor/design/design-systems-dtcg.js +571 -0
- package/dist/vendor/design/design-systems-dtcg.js.map +1 -0
- package/dist/vendor/design/frutiger-aero-generator.d.ts +46 -0
- package/dist/vendor/design/frutiger-aero-generator.d.ts.map +1 -0
- package/dist/vendor/design/frutiger-aero-generator.js +133 -0
- package/dist/vendor/design/frutiger-aero-generator.js.map +1 -0
- package/dist/vendor/design/memphis-generator.d.ts +56 -0
- package/dist/vendor/design/memphis-generator.d.ts.map +1 -0
- package/dist/vendor/design/memphis-generator.js +158 -0
- package/dist/vendor/design/memphis-generator.js.map +1 -0
- package/dist/vendor/design/mid-century-upa-generator.d.ts +46 -0
- package/dist/vendor/design/mid-century-upa-generator.d.ts.map +1 -0
- package/dist/vendor/design/mid-century-upa-generator.js +137 -0
- package/dist/vendor/design/mid-century-upa-generator.js.map +1 -0
- package/dist/vendor/design/neumorphic-generator.d.ts +53 -0
- package/dist/vendor/design/neumorphic-generator.d.ts.map +1 -0
- package/dist/vendor/design/neumorphic-generator.js +182 -0
- package/dist/vendor/design/neumorphic-generator.js.map +1 -0
- package/dist/vendor/design/op-art-generator.d.ts +45 -0
- package/dist/vendor/design/op-art-generator.d.ts.map +1 -0
- package/dist/vendor/design/op-art-generator.js +116 -0
- package/dist/vendor/design/op-art-generator.js.map +1 -0
- package/dist/vendor/design/pixel-art-generator.d.ts +45 -0
- package/dist/vendor/design/pixel-art-generator.d.ts.map +1 -0
- package/dist/vendor/design/pixel-art-generator.js +143 -0
- package/dist/vendor/design/pixel-art-generator.js.map +1 -0
- package/dist/vendor/design/pop-art-generator.d.ts +53 -0
- package/dist/vendor/design/pop-art-generator.d.ts.map +1 -0
- package/dist/vendor/design/pop-art-generator.js +164 -0
- package/dist/vendor/design/pop-art-generator.js.map +1 -0
- package/dist/vendor/design/risograph-generator.d.ts +53 -0
- package/dist/vendor/design/risograph-generator.d.ts.map +1 -0
- package/dist/vendor/design/risograph-generator.js +170 -0
- package/dist/vendor/design/risograph-generator.js.map +1 -0
- package/dist/vendor/design/swiss-style-generator.d.ts +55 -0
- package/dist/vendor/design/swiss-style-generator.d.ts.map +1 -0
- package/dist/vendor/design/swiss-style-generator.js +147 -0
- package/dist/vendor/design/swiss-style-generator.js.map +1 -0
- package/dist/vendor/design/vaporwave-generator.d.ts +51 -0
- package/dist/vendor/design/vaporwave-generator.d.ts.map +1 -0
- package/dist/vendor/design/vaporwave-generator.js +139 -0
- package/dist/vendor/design/vaporwave-generator.js.map +1 -0
- package/manifest.json +33 -1
- package/package.json +13 -5
package/dist/tools/handlers.js
CHANGED
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
* Supports browser execution via Puppeteer.
|
|
7
7
|
*/
|
|
8
8
|
import { codeGenerator } from '../types/code-generator.js';
|
|
9
|
-
import { CreateItemInputSchema, ModifyItemInputSchema, DeleteItemInputSchema, AddRelationInputSchema, RemoveRelationInputSchema, QueryRelationsInputSchema, AnimateItemInputSchema, ImportMotionCaptureInputSchema, KeyframeAnimateInputSchema, ExecuteGeneratorInputSchema, ApplyEffectInputSchema, GetItemsInputSchema, PlayTimelineInputSchema, SetBackgroundColorInputSchema, SetCanvasSizeInputSchema, ImportSVGInputSchema, ImportMermaidInputSchema, AddFilterInputSchema, CreateGlossySphereInputSchema, CreateDiagonalStripesInputSchema, BatchCreateInputSchema, BatchModifyInputSchema, CreateGridInputSchema, GeometryInputSchema, EquationPathInputSchema, EventInputSchema, ConstructionSequenceInputSchema, ValidateSceneInputSchema, CaptureFramesInputSchema, InstantiateOntologyInputSchema, LintSceneInputSchema, MediaInputSchema, TextStyleInputSchema, TextEffectInputSchema, DesignMediumInputSchema, ShatterImageInputSchema, ImportLayeredCharacterInputSchema, GameInputSchema, World3DInputSchema, BrandKitInputSchema, ComponentInputSchema, ArtboardInputSchema, CommentInputSchema, ProvenanceInputSchema, SceneDiffInputSchema, AudioBeatsInputSchema, TemplateParamsInputSchema, ComposeInputSchema, CropImageInputSchema, ChromaKeyInputSchema, RiggingInputSchema, GroupInputSchema, CameraDirectorInputSchema, DetectObjectsInputSchema, ExtractObjectInputSchema, ArrangeInputSchema, CreateSceneInputSchema, GetPerformanceMetricsInputSchema, DiagnosticReportInputSchema, SearchAssetsInputSchema, ImportAssetInputSchema, P5DrawInputSchema,
|
|
9
|
+
import { CreateItemInputSchema, ModifyItemInputSchema, DeleteItemInputSchema, AddRelationInputSchema, RemoveRelationInputSchema, QueryRelationsInputSchema, AnimateItemInputSchema, ImportMotionCaptureInputSchema, KeyframeAnimateInputSchema, ExecuteGeneratorInputSchema, ApplyEffectInputSchema, GetItemsInputSchema, PlayTimelineInputSchema, SetBackgroundColorInputSchema, SetCanvasSizeInputSchema, ImportSVGInputSchema, ImportMermaidInputSchema, AddFilterInputSchema, CreateGlossySphereInputSchema, CreateDiagonalStripesInputSchema, BatchCreateInputSchema, BatchModifyInputSchema, CreateGridInputSchema, GeometryInputSchema, EquationPathInputSchema, EventInputSchema, ConstructionSequenceInputSchema, ValidateSceneInputSchema, CaptureFramesInputSchema, InstantiateOntologyInputSchema, LintSceneInputSchema, MediaInputSchema, TextStyleInputSchema, TextEffectInputSchema, DesignMediumInputSchema, ShatterImageInputSchema, ImportLayeredCharacterInputSchema, CharacterInputSchema, GameInputSchema, World3DInputSchema, BrandKitInputSchema, ComponentInputSchema, ArtboardInputSchema, CommentInputSchema, ProvenanceInputSchema, SceneDiffInputSchema, AudioBeatsInputSchema, TemplateParamsInputSchema, ComposeInputSchema, CropImageInputSchema, PathOpInputSchema, MotionInputSchema, SoundInputSchema, InterchangeInputSchema, StickInputSchema, DesignSystemInputSchema, StoryInputSchema, ChromaKeyInputSchema, RiggingInputSchema, GroupInputSchema, CameraDirectorInputSchema, DetectObjectsInputSchema, ExtractObjectInputSchema, ArrangeInputSchema, CreateSceneInputSchema, GetPerformanceMetricsInputSchema, DiagnosticReportInputSchema, SearchAssetsInputSchema, ImportAssetInputSchema, P5DrawInputSchema,
|
|
10
10
|
// Diagram schemas
|
|
11
11
|
CreateDiagramShapeInputSchema, ConnectInputSchema, ConnectPortsInputSchema, AddPortsInputSchema, AutoLayoutInputSchema, GetDiagramShapesInputSchema, UpdateConnectorInputSchema, RemoveConnectorInputSchema, DiagramModeInputSchema,
|
|
12
12
|
// Agent flow mode schemas
|
|
13
|
-
AgentStartJobInputSchema, AgentEndJobInputSchema, AgentResetInputSchema, AgentBatchExecuteInputSchema, AgentExportInputSchema, AgentAnalyzeInputSchema,
|
|
13
|
+
AgentStartJobInputSchema, AgentEndJobInputSchema, AgentResetInputSchema, AgentBatchExecuteInputSchema, AgentExportInputSchema, ExportStoreInputSchema, AgentAnalyzeInputSchema,
|
|
14
14
|
// Letter collage schemas
|
|
15
15
|
CreateLetterCollageInputSchema, AnimateLetterCollageInputSchema,
|
|
16
16
|
// Map schemas
|
|
@@ -29,7 +29,7 @@ ManageScenesInputSchema, ScenePlaybackInputSchema, SceneGraphInputSchema, Sequen
|
|
|
29
29
|
// New consolidated tool schemas
|
|
30
30
|
SelectionInputSchema, TransformInputSchema, HistoryInputSchema, ImageFilterInputSchema, LassoInputSchema, CutoutStyleInputSchema, PrecompInputSchema, BackgroundInputSchema, QueryInputSchema, QueryCapabilitiesInputSchema, DeformInputSchema, SpriteSheetInputSchema, InteractionInputSchema, ExportWidgetInputSchema, ExportWidgetHtmlInputSchema, CreateChartInputSchema, MagicInputSchema, PhysicsInputSchema, MeasurementInputSchema, ErrorCodes, } from '../types/schemas.js';
|
|
31
31
|
import { ZodError } from 'zod';
|
|
32
|
-
import { writeFile, mkdir } from 'node:fs/promises';
|
|
32
|
+
import { writeFile, mkdir, appendFile, unlink } from 'node:fs/promises';
|
|
33
33
|
import { tmpdir } from 'node:os';
|
|
34
34
|
import { join } from 'node:path';
|
|
35
35
|
import { getBrowserController, resetBrowserController, } from '../browser/puppeteer-controller.js';
|
|
@@ -43,20 +43,24 @@ import { fontHandlers } from './handlers/font.js';
|
|
|
43
43
|
import { toolGuideHandlers } from './handlers/tool-guide.js';
|
|
44
44
|
import { mapHandlers } from './handlers/maps.js';
|
|
45
45
|
import { ontologyHandlers } from './handlers/ontology.js';
|
|
46
|
+
import * as designSystems from '../design/design-systems.js';
|
|
46
47
|
import { exportHandlers } from './handlers/export.js';
|
|
48
|
+
import { planCharacter, generateCharacterCode } from './handlers/character.js';
|
|
47
49
|
/**
|
|
48
50
|
* Registry of per-domain handler maps. Tools listed here short-circuit the
|
|
49
51
|
* main switch at dispatch time (see handleToolCallInner). New domains are
|
|
50
52
|
* folded in incrementally as they are migrated out of the monolithic switch.
|
|
51
53
|
*/
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
function getDomainHandlers() {
|
|
55
|
+
return [
|
|
56
|
+
cameraHandlers,
|
|
57
|
+
fontHandlers,
|
|
58
|
+
toolGuideHandlers,
|
|
59
|
+
mapHandlers,
|
|
60
|
+
ontologyHandlers,
|
|
61
|
+
exportHandlers,
|
|
62
|
+
];
|
|
63
|
+
}
|
|
60
64
|
/**
|
|
61
65
|
* Get screenshot mode from environment variable.
|
|
62
66
|
* Defaults to 'on_request' for optimal performance.
|
|
@@ -75,10 +79,41 @@ export function getScreenshotMode() {
|
|
|
75
79
|
function getExportDir() {
|
|
76
80
|
return process.env.PINEPAPER_EXPORT_DIR || join(tmpdir(), 'pinepaper-exports');
|
|
77
81
|
}
|
|
78
|
-
|
|
82
|
+
// wav joins them: a minute of 48kHz 16-bit is ~5.8 MB of base64 and the ten
|
|
83
|
+
// minutes the schema allows is ~77 MB. Deliverable across the bridge, useless
|
|
84
|
+
// pasted into a response.
|
|
85
|
+
export const ALWAYS_SAVE_FORMATS = new Set(['mp4', 'webm', 'gif', 'pdf', 'wav']);
|
|
79
86
|
const SAVE_THRESHOLD_BYTES = 500_000; // ~500KB base64 ≈ 375KB decoded
|
|
80
|
-
|
|
81
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Governor budget for the export run itself.
|
|
89
|
+
*
|
|
90
|
+
* FxTool's runGenerated defaults to a 10s async-tail timeout — right for a
|
|
91
|
+
* scene-building script that has no business running longer, and fatal for an
|
|
92
|
+
* export, which renders frame by frame and is capped at 600s of footage. The
|
|
93
|
+
* export died at ten seconds with a PP_TIMEOUT that read like a bug in the
|
|
94
|
+
* scene. This raises the budget rather than bypassing the governor, so the
|
|
95
|
+
* report, the item budget and the seeded PRNG all survive.
|
|
96
|
+
*
|
|
97
|
+
* PINEPAPER_EXPORT_TIMEOUT overrides it. The default matches the floor put on
|
|
98
|
+
* CDP's own per-call timeout (MIN_PROTOCOL_TIMEOUT_MS in
|
|
99
|
+
* puppeteer-controller.ts), so raising this past the default without also
|
|
100
|
+
* raising PINEPAPER_TIMEOUT just moves the execution to a protocol error —
|
|
101
|
+
* which is why that case says so out loud rather than failing obscurely later.
|
|
102
|
+
*/
|
|
103
|
+
const EXPORT_GOVERNOR_DEFAULT_MS = 300_000;
|
|
104
|
+
function exportGovernorTimeoutMs() {
|
|
105
|
+
const raw = Number(process.env.PINEPAPER_EXPORT_TIMEOUT);
|
|
106
|
+
if (!Number.isFinite(raw) || raw <= 0)
|
|
107
|
+
return EXPORT_GOVERNOR_DEFAULT_MS;
|
|
108
|
+
const protocolFloor = Math.max(Number(process.env.PINEPAPER_TIMEOUT) || 0, EXPORT_GOVERNOR_DEFAULT_MS);
|
|
109
|
+
if (raw > protocolFloor) {
|
|
110
|
+
console.error(`[PinePaper] PINEPAPER_EXPORT_TIMEOUT=${raw}ms is above the browser's protocol timeout (${protocolFloor}ms) — ` +
|
|
111
|
+
'raise PINEPAPER_TIMEOUT to match, or an export past that point fails as a protocol error rather than a governor timeout.');
|
|
112
|
+
}
|
|
113
|
+
return raw;
|
|
114
|
+
}
|
|
115
|
+
export function getFileExtension(format) {
|
|
116
|
+
const extMap = { mp4: 'mp4', webm: 'webm', gif: 'gif', pdf: 'pdf', png: 'png', svg: 'svg', wav: 'wav' };
|
|
82
117
|
return extMap[format] || format;
|
|
83
118
|
}
|
|
84
119
|
async function saveExportToFile(data, format, platform) {
|
|
@@ -100,6 +135,140 @@ async function saveExportToFile(data, format, platform) {
|
|
|
100
135
|
return { filePath, fileSize: Buffer.byteLength(data, 'utf-8') };
|
|
101
136
|
}
|
|
102
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Page a RETAINED export out of the studio's export store into a local file.
|
|
140
|
+
*
|
|
141
|
+
* The bytes never cross the bridge as one string. The studio holds the encoded
|
|
142
|
+
* file in its origin-private store and hands back an id; this reads it a chunk
|
|
143
|
+
* at a time and appends each one, so at most one chunk (4 MB by default) is in
|
|
144
|
+
* memory on either side. The agent still made one tool call and still gets one
|
|
145
|
+
* filePath — the paging is entirely inside the tool, because an agent-facing
|
|
146
|
+
* pager would only expose a transport limit the agent cannot act on.
|
|
147
|
+
*
|
|
148
|
+
* On failure the export is deliberately LEFT HELD: the store is the only copy,
|
|
149
|
+
* so the id and the byte count reached go back in the error and the bytes stay
|
|
150
|
+
* recoverable. `releaseExport` runs only once the whole file is written.
|
|
151
|
+
*/
|
|
152
|
+
async function streamRetainedExportToFile(controller, retained, platform) {
|
|
153
|
+
// Reassigned once if the store corrects the size below.
|
|
154
|
+
let corrected = false;
|
|
155
|
+
const exportDir = getExportDir();
|
|
156
|
+
await mkdir(exportDir, { recursive: true });
|
|
157
|
+
const ext = getFileExtension(retained.format);
|
|
158
|
+
const fileName = `pinepaper_${platform}_${Date.now()}.${ext}`;
|
|
159
|
+
const filePath = join(exportDir, fileName);
|
|
160
|
+
const chunkBytes = retained.chunkBytes && retained.chunkBytes > 0 ? retained.chunkBytes : 4 * 1024 * 1024;
|
|
161
|
+
let offset = 0;
|
|
162
|
+
let chunks = 0;
|
|
163
|
+
// Start from empty: appendFile would otherwise extend a same-millisecond file.
|
|
164
|
+
await writeFile(filePath, Buffer.alloc(0));
|
|
165
|
+
// Returns the error rather than throwing it, so `throw await abandon(...)`
|
|
166
|
+
// reads as a throw to the type checker and narrows what follows.
|
|
167
|
+
const abandon = async (why) => {
|
|
168
|
+
try {
|
|
169
|
+
await unlink(filePath);
|
|
170
|
+
}
|
|
171
|
+
catch { /* nothing written yet */ }
|
|
172
|
+
return new Error(
|
|
173
|
+
// The promise now names the call that keeps it. It was true before and
|
|
174
|
+
// unreachable: reading a held export mutates nothing, so "it can be paged
|
|
175
|
+
// again" was always correct and there was no door to page it through.
|
|
176
|
+
`${why} (export "${retained.exportId}" is still held in the studio, ${offset} of ${retained.size} bytes read; ` +
|
|
177
|
+
`page it out with pinepaper_export_store action 'save', exportId "${retained.exportId}" — reading is idempotent, ` +
|
|
178
|
+
'so retrying costs nothing and the export is released only once a whole file is written. Do it BEFORE the next ' +
|
|
179
|
+
'export: a new one evicts the oldest held bytes to make room)');
|
|
180
|
+
};
|
|
181
|
+
// A READ THAT FAILS ON SIZE IS NOT A READ THAT FAILS.
|
|
182
|
+
//
|
|
183
|
+
// A 17 MB memphis chunk died on its FOURTH 4 MB read — three had already
|
|
184
|
+
// landed — with "Failed to write data to data pipe". That is CDP refusing to
|
|
185
|
+
// carry one oversized value across the bridge, not the store refusing to
|
|
186
|
+
// serve it: the render had succeeded and the bytes were sitting right there.
|
|
187
|
+
// The loop abandoned the whole export anyway, and the caller was left with an
|
|
188
|
+
// error that explained the bytes were still held and no way to go get them.
|
|
189
|
+
//
|
|
190
|
+
// So a transport failure halves the request and asks again, down to a floor,
|
|
191
|
+
// before giving up. The offset does not move, so a retry costs one re-read,
|
|
192
|
+
// never a corrupt file. A refusal from the STORE (evicted, out of range) is
|
|
193
|
+
// NOT retried — that answer will not change with a smaller window, and
|
|
194
|
+
// retrying it would only spend the caller's time to reach the same place.
|
|
195
|
+
const MIN_CHUNK_BYTES = 256 * 1024;
|
|
196
|
+
let window = chunkBytes;
|
|
197
|
+
for (;;) {
|
|
198
|
+
const want = Math.min(window, retained.size - offset);
|
|
199
|
+
const code = codeGenerator.generateReadExportChunk(retained.exportId, offset, want);
|
|
200
|
+
// Deliberately governed at the default budget: a chunk read is a slice of a
|
|
201
|
+
// string already in memory. If one of these ever needs ten seconds, the
|
|
202
|
+
// store is wedged and a timeout is the correct answer, not a longer wait.
|
|
203
|
+
const run = await controller.executeCode(code, false);
|
|
204
|
+
if (!run.success) {
|
|
205
|
+
if (want > MIN_CHUNK_BYTES) {
|
|
206
|
+
window = Math.max(MIN_CHUNK_BYTES, Math.floor(want / 2));
|
|
207
|
+
console.error(`[PinePaper] a ${want}-byte read of "${retained.exportId}" failed (${run.error || 'unknown error'}); `
|
|
208
|
+
+ `retrying the same offset at ${window} bytes`);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
throw await abandon(`reading the export failed even at the ${MIN_CHUNK_BYTES}-byte floor: ${run.error || 'unknown error'}`);
|
|
212
|
+
}
|
|
213
|
+
const chunk = run.result;
|
|
214
|
+
if (!chunk || chunk.ok !== true) {
|
|
215
|
+
// A RANGE REFUSAL IS A CORRECTION, and it carries the real size.
|
|
216
|
+
//
|
|
217
|
+
// readExport refuses an out-of-range read by name rather than clamping —
|
|
218
|
+
// deliberately, so a miscounting caller is told instead of handed a short
|
|
219
|
+
// chunk it treats as the tail, which surfaces later as a truncated video
|
|
220
|
+
// with nothing pointing back at the cause. The size this loop is working
|
|
221
|
+
// from came from the export result, and `save` works from a listing; when
|
|
222
|
+
// either disagrees with the file, the refusal is the authority. Take it
|
|
223
|
+
// ONCE — a second disagreement is not a miscount, it is a moving target.
|
|
224
|
+
// Only when it DISAGREES. Every range refusal quotes the real size,
|
|
225
|
+
// including the ones that are refusing for some other reason — so
|
|
226
|
+
// matching the number alone treated an eviction as a miscount, spent the
|
|
227
|
+
// one correction on a no-op re-read, and hid the actual reason for an
|
|
228
|
+
// iteration.
|
|
229
|
+
const stated = /\(size (\d+)\)/.exec(chunk?.reason ?? '');
|
|
230
|
+
if (stated && !corrected && Number(stated[1]) !== retained.size) {
|
|
231
|
+
corrected = true;
|
|
232
|
+
const was = retained.size;
|
|
233
|
+
retained = { ...retained, size: Number(stated[1]) };
|
|
234
|
+
console.error(`[PinePaper] the store reports "${retained.exportId}" as ${retained.size} bytes, not ${was}; `
|
|
235
|
+
+ "taking the store's number and continuing");
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
// An evicted id is its own failure: a later export reclaimed the space.
|
|
239
|
+
// Re-exporting is the caller's decision, never this loop's. The engine
|
|
240
|
+
// records an eviction in browser storage so it survives a reload, but
|
|
241
|
+
// that storage is best-effort — a private window, cleared site data or
|
|
242
|
+
// storage switched off all fall back to memory-only. So `evicted` being
|
|
243
|
+
// ABSENT means unknown, never that the export was not there.
|
|
244
|
+
throw await abandon(chunk?.evicted
|
|
245
|
+
? `the export was evicted mid-read: ${chunk.reason}`
|
|
246
|
+
: `the studio refused the read: ${chunk?.reason || 'no reason given'}`);
|
|
247
|
+
}
|
|
248
|
+
const buf = Buffer.from(chunk.data || '', 'base64');
|
|
249
|
+
if (buf.length === 0 && chunk.eof !== true)
|
|
250
|
+
throw await abandon('the studio returned an empty chunk before the end of the file');
|
|
251
|
+
await appendFile(filePath, buf);
|
|
252
|
+
offset += buf.length;
|
|
253
|
+
chunks++;
|
|
254
|
+
if (chunk.eof === true)
|
|
255
|
+
break;
|
|
256
|
+
if (offset >= retained.size)
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
if (offset !== retained.size) {
|
|
260
|
+
throw await abandon(`reassembled ${offset} bytes but the studio reported ${retained.size}`);
|
|
261
|
+
}
|
|
262
|
+
// Only now is it safe to drop the studio's copy.
|
|
263
|
+
try {
|
|
264
|
+
await controller.executeCode(codeGenerator.generateReleaseExport(retained.exportId), false);
|
|
265
|
+
}
|
|
266
|
+
catch (releaseError) {
|
|
267
|
+
// A held export costs quota, not correctness — the file is already whole.
|
|
268
|
+
console.error('[PinePaper] Export written but releaseExport failed:', releaseError);
|
|
269
|
+
}
|
|
270
|
+
return { filePath, fileSize: offset, chunks };
|
|
271
|
+
}
|
|
103
272
|
/**
|
|
104
273
|
* Get execution mode from environment variable.
|
|
105
274
|
* Defaults to 'puppeteer' for live execution.
|
|
@@ -130,6 +299,17 @@ function successResult(code, description) {
|
|
|
130
299
|
}
|
|
131
300
|
return { content };
|
|
132
301
|
}
|
|
302
|
+
/**
|
|
303
|
+
* A plain data answer.
|
|
304
|
+
*
|
|
305
|
+
* successResult() fences its payload as "Generated PinePaper code", which is
|
|
306
|
+
* right for an emitter and wrong for a tool that computes its answer here —
|
|
307
|
+
* a caller reading design tokens should not be handed something that looks
|
|
308
|
+
* like a script to paste.
|
|
309
|
+
*/
|
|
310
|
+
function dataResult(value) {
|
|
311
|
+
return { content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] };
|
|
312
|
+
}
|
|
133
313
|
function executedResult(code, result, screenshot, description, report) {
|
|
134
314
|
const resultObj = { success: true, result };
|
|
135
315
|
if (description)
|
|
@@ -503,7 +683,7 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
|
|
|
503
683
|
// Domain-module dispatch: tools migrated to per-domain handler files
|
|
504
684
|
// short-circuit before the main switch. See DOMAIN_HANDLERS for the
|
|
505
685
|
// current registry.
|
|
506
|
-
for (const domainMap of
|
|
686
|
+
for (const domainMap of getDomainHandlers()) {
|
|
507
687
|
const handler = domainMap[toolName];
|
|
508
688
|
if (handler)
|
|
509
689
|
return await handler(args, options);
|
|
@@ -739,6 +919,28 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
|
|
|
739
919
|
// -----------------------------------------------------------------------
|
|
740
920
|
// BATCH OPERATION TOOLS
|
|
741
921
|
// -----------------------------------------------------------------------
|
|
922
|
+
case 'pinepaper_character': {
|
|
923
|
+
const input = CharacterInputSchema.parse(args);
|
|
924
|
+
const plan = planCharacter(input);
|
|
925
|
+
if (!plan.ok) {
|
|
926
|
+
// A refusal that names what WOULD have worked. The caller cannot see
|
|
927
|
+
// the design graph, and a bare "no" is how a model came to ask for a
|
|
928
|
+
// concept that has never had a drawing.
|
|
929
|
+
return {
|
|
930
|
+
content: [{ type: 'text', text: `pinepaper_character: ${plan.reason}` }],
|
|
931
|
+
isError: true,
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
const code = generateCharacterCode(plan, {
|
|
935
|
+
generateCreateItem: (i) => codeGenerator.generateCreateItem(i),
|
|
936
|
+
generateKeyframeAnimate: (i) => codeGenerator.generateKeyframeAnimate(i),
|
|
937
|
+
}, {
|
|
938
|
+
createItem: (a) => CreateItemInputSchema.parse(a),
|
|
939
|
+
keyframeAnimate: (a) => KeyframeAnimateInputSchema.parse(a),
|
|
940
|
+
});
|
|
941
|
+
const description = `Places ${input.concept} as ${plan.parts.length} parts with ${plan.tracks} keyframe tracks`;
|
|
942
|
+
return executeOrGenerate(code, description, options, 'pinepaper_character');
|
|
943
|
+
}
|
|
742
944
|
case 'pinepaper_batch_create': {
|
|
743
945
|
const input = BatchCreateInputSchema.parse(args);
|
|
744
946
|
const code = codeGenerator.generateBatchCreate(input);
|
|
@@ -947,7 +1149,7 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
|
|
|
947
1149
|
const input = AddRelationInputSchema.parse(args);
|
|
948
1150
|
const code = codeGenerator.generateAddRelation(input);
|
|
949
1151
|
const description = getLocalizedSuccessMessage(i18n, 'relationAdded', {
|
|
950
|
-
relationType: input.relationType
|
|
1152
|
+
relationType: input.relationType ?? `preset:${input.presetId}`,
|
|
951
1153
|
sourceId: input.sourceId,
|
|
952
1154
|
targetId: input.targetId || 'self',
|
|
953
1155
|
});
|
|
@@ -1909,6 +2111,74 @@ You can now start creating new items on a clean canvas.`,
|
|
|
1909
2111
|
}
|
|
1910
2112
|
return batchResult;
|
|
1911
2113
|
}
|
|
2114
|
+
case 'pinepaper_export_store': {
|
|
2115
|
+
const input = ExportStoreInputSchema.parse(args);
|
|
2116
|
+
const controller = options.browserController || getBrowserController();
|
|
2117
|
+
if (!controller.connected) {
|
|
2118
|
+
try {
|
|
2119
|
+
await controller.connect();
|
|
2120
|
+
}
|
|
2121
|
+
catch (connectError) {
|
|
2122
|
+
return errorResult(ErrorCodes.EXECUTION_ERROR, `the export store lives in the studio, and connecting to it failed: ${connectError instanceof Error ? connectError.message : 'unknown error'}`, {}, { toolName: 'pinepaper_export_store' });
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
const held = await controller.executeCode(codeGenerator.generateListExports(), false);
|
|
2126
|
+
if (!held.success) {
|
|
2127
|
+
return errorResult(ErrorCodes.EXECUTION_ERROR, `listing the export store failed: ${held.error || 'unknown error'}`, {}, { toolName: 'pinepaper_export_store' });
|
|
2128
|
+
}
|
|
2129
|
+
const listing = held.result;
|
|
2130
|
+
if (!listing || listing.ok !== true) {
|
|
2131
|
+
return errorResult(ErrorCodes.EXECUTION_ERROR, listing?.reason || 'the studio did not answer with a listing', {}, { toolName: 'pinepaper_export_store' });
|
|
2132
|
+
}
|
|
2133
|
+
const exports = listing.exports ?? [];
|
|
2134
|
+
if (input.action === 'list') {
|
|
2135
|
+
return dataResult({
|
|
2136
|
+
exports,
|
|
2137
|
+
held: exports.length,
|
|
2138
|
+
heldBytes: exports.reduce((n, e) => n + (e.size || 0), 0),
|
|
2139
|
+
note: exports.length > 1
|
|
2140
|
+
? 'A later export evicts the OLDEST held ones to make room. Save or release each before starting the next.'
|
|
2141
|
+
: undefined,
|
|
2142
|
+
});
|
|
2143
|
+
}
|
|
2144
|
+
// Both remaining actions name an id, and an id the store does not have
|
|
2145
|
+
// is the one thing worth saying carefully. The engine records an
|
|
2146
|
+
// eviction durably enough to survive a reload, so a read of that id
|
|
2147
|
+
// will usually say "evicted" — but the record is best-effort browser
|
|
2148
|
+
// storage and a listing cannot consult it at all. Absence of the id
|
|
2149
|
+
// here is therefore "gone, cause unknown", and stating one cause as
|
|
2150
|
+
// fact would be inventing the more interesting half of the answer.
|
|
2151
|
+
const entry = exports.find((e) => e.id === input.exportId);
|
|
2152
|
+
if (!entry) {
|
|
2153
|
+
return errorResult(ErrorCodes.INVALID_PARAMS, `the studio is not holding "${input.exportId}". It was released, evicted to make room for a later export, `
|
|
2154
|
+
+ `or never existed here — a listing cannot tell which. `
|
|
2155
|
+
+ `${exports.length ? `Held right now: ${exports.map((e) => e.id).join(', ')}.` : 'The store is empty.'}`, { exportId: input.exportId, held: exports.map((e) => e.id) }, { toolName: 'pinepaper_export_store' });
|
|
2156
|
+
}
|
|
2157
|
+
if (input.action === 'release') {
|
|
2158
|
+
const dropped = await controller.executeCode(codeGenerator.generateReleaseExport(entry.id), false);
|
|
2159
|
+
const outcome = dropped.result;
|
|
2160
|
+
if (!dropped.success || outcome?.ok !== true) {
|
|
2161
|
+
return errorResult(ErrorCodes.EXECUTION_ERROR, `releasing "${entry.id}" failed: ${dropped.error || outcome?.reason || 'unknown error'}`, { exportId: entry.id }, { toolName: 'pinepaper_export_store' });
|
|
2162
|
+
}
|
|
2163
|
+
return dataResult({ released: entry.id, freedBytes: entry.size });
|
|
2164
|
+
}
|
|
2165
|
+
// save: the same pager agent_export uses, pointed at an id instead of a
|
|
2166
|
+
// fresh render. Reading is idempotent, so a save that failed halfway
|
|
2167
|
+
// can simply be run again — there is no partial state to clean up here.
|
|
2168
|
+
try {
|
|
2169
|
+
const { filePath, fileSize, chunks } = await streamRetainedExportToFile(controller, { exportId: entry.id, size: entry.size, format: entry.format }, input.platform || 'recovered');
|
|
2170
|
+
return {
|
|
2171
|
+
content: [{
|
|
2172
|
+
type: 'text',
|
|
2173
|
+
text: `Export recovered from the studio's store:\n\nFile: ${filePath}\nFormat: ${entry.format}\n`
|
|
2174
|
+
+ `Size: ${(fileSize / 1024).toFixed(1)} KB\nPaged out in ${chunks} chunk${chunks === 1 ? '' : 's'}, then released\n`,
|
|
2175
|
+
}],
|
|
2176
|
+
};
|
|
2177
|
+
}
|
|
2178
|
+
catch (streamError) {
|
|
2179
|
+
return errorResult(ErrorCodes.EXECUTION_ERROR, streamError instanceof Error ? streamError.message : 'paging the export out of the store failed', { exportId: entry.id, size: entry.size }, { toolName: 'pinepaper_export_store' });
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
1912
2182
|
case 'pinepaper_agent_export': {
|
|
1913
2183
|
const input = AgentExportInputSchema.parse(args);
|
|
1914
2184
|
const code = codeGenerator.generateAgentExport(input);
|
|
@@ -1936,24 +2206,94 @@ You can now start creating new items on a clean canvas.`,
|
|
|
1936
2206
|
if (!exportSessionManager.hasActiveJob()) {
|
|
1937
2207
|
exportSessionManager.startJob({ name: 'auto_session', screenshotPolicy: 'on_complete' });
|
|
1938
2208
|
}
|
|
1939
|
-
const exportBrowserResult = await controller.executeCode(code, false
|
|
2209
|
+
const exportBrowserResult = await controller.executeCode(code, false, {
|
|
2210
|
+
governorTimeoutMs: exportGovernorTimeoutMs(),
|
|
2211
|
+
});
|
|
1940
2212
|
if (!exportBrowserResult.success) {
|
|
1941
2213
|
const canvasState = await captureCanvasState(controller);
|
|
1942
2214
|
return errorResult(ErrorCodes.EXECUTION_ERROR, exportBrowserResult.error || 'Export failed', { code }, { toolName: 'pinepaper_agent_export', canvasState: canvasState || undefined });
|
|
1943
2215
|
}
|
|
1944
2216
|
const exportResult = exportBrowserResult.result;
|
|
2217
|
+
// TWO VERDICTS, and only one of them was being read.
|
|
2218
|
+
//
|
|
2219
|
+
// `exportBrowserResult.success` says the CODE RAN. The export's own
|
|
2220
|
+
// verdict is `exportResult.success`, and every emitted branch that
|
|
2221
|
+
// cannot deliver returns `{ success: false, error }` — a studio without
|
|
2222
|
+
// the renderer, an encoder that died, a blob past the inline ceiling.
|
|
2223
|
+
// None of that was checked: the value fell through to the inline return
|
|
2224
|
+
// below, which reports a success and hands back a result object nobody
|
|
2225
|
+
// reads past the first line.
|
|
2226
|
+
//
|
|
2227
|
+
// Measured cost: a five-chunk video where two chunks came back
|
|
2228
|
+
// `success: false, error: "export failed: Failed to write data to data
|
|
2229
|
+
// pipe"` under an outer `"success": true`. The caller shipped a
|
|
2230
|
+
// three-chunk video believing it had five. A silent success is the one
|
|
2231
|
+
// failure mode this surface can never afford, because nothing
|
|
2232
|
+
// downstream has any reason to look again.
|
|
2233
|
+
if (exportResult && exportResult.success === false) {
|
|
2234
|
+
const canvasState = await captureCanvasState(controller);
|
|
2235
|
+
// A "data pipe" failure is the BROWSER TRANSPORT refusing to carry
|
|
2236
|
+
// the result back, not the encoder refusing to make it — the string
|
|
2237
|
+
// exists nowhere in the engine. So the render may well have finished
|
|
2238
|
+
// and the bytes may be sitting in the export store right now. Saying
|
|
2239
|
+
// so is the difference between recovering an export and re-rendering
|
|
2240
|
+
// one, and re-rendering is what evicts an earlier chunk's bytes.
|
|
2241
|
+
const looksLikeTransport = /data pipe/i.test(String(exportResult.error ?? ''));
|
|
2242
|
+
return errorResult(ErrorCodes.EXECUTION_ERROR, `export failed: ${exportResult.error || 'the studio reported failure without naming a reason'}`
|
|
2243
|
+
+ (looksLikeTransport
|
|
2244
|
+
? ' — that is the browser transport failing to carry the result back, not necessarily the render failing.'
|
|
2245
|
+
+ " Call pinepaper_export_store action 'list' BEFORE re-rendering: the bytes may already be held, and a"
|
|
2246
|
+
+ ' fresh export can evict an earlier one to make room.'
|
|
2247
|
+
: ''), { code, format: exportResult.format ?? input.format, result: exportResult }, { toolName: 'pinepaper_agent_export', canvasState: canvasState || undefined });
|
|
2248
|
+
}
|
|
1945
2249
|
const format = exportResult?.format || input.format || 'png';
|
|
1946
2250
|
const data = exportResult?.data;
|
|
2251
|
+
// RETAINED: the studio held the encoded file in its export store rather
|
|
2252
|
+
// than returning bytes, because base64 of a long video is a bigger
|
|
2253
|
+
// string than the video and it would cross the bridge as one value.
|
|
2254
|
+
// Page it into a file here. mp4/webm were always going to be written to
|
|
2255
|
+
// a file anyway (ALWAYS_SAVE_FORMATS), so the agent's result is the
|
|
2256
|
+
// same shape it has always been — a filePath — just reachable at sizes
|
|
2257
|
+
// that used to run out of memory.
|
|
2258
|
+
if (exportResult?.retained === true && typeof exportResult.exportId === 'string') {
|
|
2259
|
+
try {
|
|
2260
|
+
const { filePath, fileSize, chunks } = await streamRetainedExportToFile(controller, {
|
|
2261
|
+
exportId: exportResult.exportId,
|
|
2262
|
+
size: Number(exportResult.size) || 0,
|
|
2263
|
+
format,
|
|
2264
|
+
chunkBytes: Number(exportResult.chunkBytes) || undefined,
|
|
2265
|
+
}, input.platform || 'auto');
|
|
2266
|
+
const cleanResult = { ...exportResult, data: undefined, filePath, fileSize, chunks };
|
|
2267
|
+
return {
|
|
2268
|
+
content: [{
|
|
2269
|
+
type: 'text',
|
|
2270
|
+
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)}`,
|
|
2271
|
+
}],
|
|
2272
|
+
};
|
|
2273
|
+
}
|
|
2274
|
+
catch (streamError) {
|
|
2275
|
+
// The export is still held — the message says so and names the id,
|
|
2276
|
+
// because the bytes are recoverable and re-exporting is not free.
|
|
2277
|
+
const canvasState = await captureCanvasState(controller);
|
|
2278
|
+
return errorResult(ErrorCodes.EXECUTION_ERROR, streamError instanceof Error ? streamError.message : 'Paging the export out of the store failed', { code, exportId: exportResult.exportId, size: exportResult.size }, { toolName: 'pinepaper_agent_export', canvasState: canvasState || undefined });
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
1947
2281
|
const shouldSaveToFile = data && typeof data === 'string' && (ALWAYS_SAVE_FORMATS.has(format) ||
|
|
1948
2282
|
data.length > SAVE_THRESHOLD_BYTES);
|
|
1949
2283
|
if (shouldSaveToFile) {
|
|
1950
2284
|
try {
|
|
1951
2285
|
const { filePath, fileSize } = await saveExportToFile(data, format, input.platform || 'auto');
|
|
1952
2286
|
const cleanResult = { ...exportResult, data: undefined, filePath, fileSize };
|
|
2287
|
+
// A mix that dropped sounds wrote a perfectly good file that is
|
|
2288
|
+
// missing tracks. It rides the SUCCESS path, which is exactly why
|
|
2289
|
+
// it has to be said out loud here rather than left in the JSON.
|
|
2290
|
+
const dropNote = exportResult.incomplete;
|
|
1953
2291
|
return {
|
|
1954
2292
|
content: [{
|
|
1955
2293
|
type: 'text',
|
|
1956
|
-
text: `Export saved to file:\n\nFile: ${filePath}\nFormat: ${format}\nSize: ${(fileSize / 1024).toFixed(1)} KB\nPlatform: ${input.platform}\n
|
|
2294
|
+
text: `Export saved to file:\n\nFile: ${filePath}\nFormat: ${format}\nSize: ${(fileSize / 1024).toFixed(1)} KB\nPlatform: ${input.platform}\n`
|
|
2295
|
+
+ (dropNote ? `DROPPED: ${dropNote}\n` : '')
|
|
2296
|
+
+ `\nResult: ${JSON.stringify(cleanResult, null, 2)}`,
|
|
1957
2297
|
}],
|
|
1958
2298
|
};
|
|
1959
2299
|
}
|
|
@@ -1967,6 +2307,170 @@ You can now start creating new items on a clean canvas.`,
|
|
|
1967
2307
|
// Small export — return inline (existing behavior)
|
|
1968
2308
|
return executedResult(code, exportResult, exportBrowserResult.screenshot, description);
|
|
1969
2309
|
}
|
|
2310
|
+
case 'pinepaper_design_styles':
|
|
2311
|
+
return dataResult({ styles: designSystems.listStyles() });
|
|
2312
|
+
case 'pinepaper_design_systems':
|
|
2313
|
+
case 'pinepaper_design_system': {
|
|
2314
|
+
const rawArgs = { ...args };
|
|
2315
|
+
if (!rawArgs.action && rawArgs.systemName) {
|
|
2316
|
+
if (rawArgs.systemName === 'all') {
|
|
2317
|
+
rawArgs.action = 'list_systems';
|
|
2318
|
+
}
|
|
2319
|
+
else {
|
|
2320
|
+
rawArgs.action = 'get_system';
|
|
2321
|
+
rawArgs.systemId = String(rawArgs.systemName);
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
if (!rawArgs.action && rawArgs.style && !rawArgs.title) {
|
|
2325
|
+
rawArgs.action = 'list_styles';
|
|
2326
|
+
}
|
|
2327
|
+
const input = DesignSystemInputSchema.parse(rawArgs);
|
|
2328
|
+
// Answered HERE, not in the browser — these are decisions over
|
|
2329
|
+
// vendored data, and only `compose` with draw:true needs the canvas.
|
|
2330
|
+
switch (input.action) {
|
|
2331
|
+
case 'list_systems':
|
|
2332
|
+
return dataResult({ systems: designSystems.listSystems() });
|
|
2333
|
+
case 'get_system': {
|
|
2334
|
+
const sys = designSystems.getSystem(input.systemId, input.tokenType);
|
|
2335
|
+
if (!sys) {
|
|
2336
|
+
return errorResult(ErrorCodes.INVALID_PARAMS, `no design system "${input.systemId}" — call action 'list_systems' for the ids`, { systemId: input.systemId }, { toolName: 'pinepaper_design_system' });
|
|
2337
|
+
}
|
|
2338
|
+
return dataResult(sys);
|
|
2339
|
+
}
|
|
2340
|
+
case 'list_motion': {
|
|
2341
|
+
// Curves and durations together, because a motion token is a shape
|
|
2342
|
+
// AND a length. The curves were published and the durations beside
|
|
2343
|
+
// them in the same store were not, so a caller could match a
|
|
2344
|
+
// system's easing and had to invent its timing.
|
|
2345
|
+
const easings = designSystems.listEasings(input.authoredOnly);
|
|
2346
|
+
const durations = designSystems.listDurations(input.authoredOnly);
|
|
2347
|
+
return dataResult({
|
|
2348
|
+
easings, durations,
|
|
2349
|
+
note: 'These are the licensed SYSTEMS\' motion scales. An aesthetic style (bauhaus_geometric, memphis_group) carries no upstream motion data — pair one of these with a style rather than expecting the style to name its own.',
|
|
2350
|
+
});
|
|
2351
|
+
}
|
|
2352
|
+
case 'list_easings':
|
|
2353
|
+
return dataResult({ easings: designSystems.listEasings(input.authoredOnly) });
|
|
2354
|
+
case 'list_styles':
|
|
2355
|
+
return dataResult({ styles: designSystems.listStyles() });
|
|
2356
|
+
case 'compose': {
|
|
2357
|
+
let scene;
|
|
2358
|
+
try {
|
|
2359
|
+
scene = designSystems.compose(input.style, {
|
|
2360
|
+
title: input.title, subtitle: input.subtitle, body: input.body,
|
|
2361
|
+
width: input.width, height: input.height, variant: input.variant,
|
|
2362
|
+
});
|
|
2363
|
+
}
|
|
2364
|
+
catch (composeError) {
|
|
2365
|
+
return errorResult(ErrorCodes.EXECUTION_ERROR, composeError instanceof Error ? composeError.message : 'the style could not compose', { style: input.style }, { toolName: 'pinepaper_design_system' });
|
|
2366
|
+
}
|
|
2367
|
+
if (!scene) {
|
|
2368
|
+
return errorResult(ErrorCodes.INVALID_PARAMS, `"${input.style}" cannot compose — it is a style this surface can name but not build. ` +
|
|
2369
|
+
`Call action 'list_styles' and use one marked composable.`, { style: input.style }, { toolName: 'pinepaper_design_system' });
|
|
2370
|
+
}
|
|
2371
|
+
const ops = designSystems.sceneToOps(scene);
|
|
2372
|
+
if (input.draw === false) {
|
|
2373
|
+
return dataResult({ scene, ops, drawn: false });
|
|
2374
|
+
}
|
|
2375
|
+
const code = codeGenerator.generateDesignCompose(scene, ops, scene.style);
|
|
2376
|
+
return executeOrGenerate(code, `Composed a ${scene.style} scene (${ops.length} items)`, options, 'pinepaper_design_system');
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
break;
|
|
2380
|
+
}
|
|
2381
|
+
case 'pinepaper_stick_figure':
|
|
2382
|
+
case 'pinepaper_stick': {
|
|
2383
|
+
const input = StickInputSchema.parse(args);
|
|
2384
|
+
return executeOrGenerate(codeGenerator.generateStick(input), `Stick ${input.action}`, options, 'pinepaper_stick');
|
|
2385
|
+
}
|
|
2386
|
+
case 'pinepaper_story': {
|
|
2387
|
+
const input = StoryInputSchema.parse(args);
|
|
2388
|
+
return executeOrGenerate(codeGenerator.generateStory(input), `Story: ${input.action}`, options, 'pinepaper_story');
|
|
2389
|
+
}
|
|
2390
|
+
case 'pinepaper_interchange': {
|
|
2391
|
+
const input = InterchangeInputSchema.parse(args);
|
|
2392
|
+
const code = codeGenerator.generateInterchange(input);
|
|
2393
|
+
return executeOrGenerate(code, `Interchange: ${input.action}`, options, 'pinepaper_interchange');
|
|
2394
|
+
}
|
|
2395
|
+
case 'pinepaper_sound': {
|
|
2396
|
+
const input = SoundInputSchema.parse(args);
|
|
2397
|
+
const code = codeGenerator.generateSound(input);
|
|
2398
|
+
const soundDescription = `Sound: ${input.action}`;
|
|
2399
|
+
// A RENDERED SOUNDTRACK IS A FILE, not a response field. 48kHz 16-bit
|
|
2400
|
+
// mono is ~96 KB/s, so a minute is ~5.8 MB of base64 and the ten
|
|
2401
|
+
// minutes the schema allows is ~77 MB — deliverable across the bridge,
|
|
2402
|
+
// useless pasted into a conversation. This mirrors what agent_export
|
|
2403
|
+
// already does for video and PDF, and reuses the same helper rather
|
|
2404
|
+
// than growing a second copy of the save path.
|
|
2405
|
+
if (input.action === 'render_soundtrack') {
|
|
2406
|
+
const execMode = options.executionMode ?? getExecutionMode();
|
|
2407
|
+
if (execMode === 'code' || !options.executeInBrowser) {
|
|
2408
|
+
return executeOrGenerate(code, soundDescription, options, 'pinepaper_sound');
|
|
2409
|
+
}
|
|
2410
|
+
const soundController = options.browserController || getBrowserController();
|
|
2411
|
+
if (!soundController.connected) {
|
|
2412
|
+
try {
|
|
2413
|
+
await soundController.connect();
|
|
2414
|
+
}
|
|
2415
|
+
catch {
|
|
2416
|
+
return executeOrGenerate(code, soundDescription, options, 'pinepaper_sound');
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
const wavResult = await soundController.executeCode(code, false);
|
|
2420
|
+
if (!wavResult.success) {
|
|
2421
|
+
const canvasState = await captureCanvasState(soundController);
|
|
2422
|
+
return errorResult(ErrorCodes.EXECUTION_ERROR, wavResult.error || 'Rendering the soundtrack failed', { code }, { toolName: 'pinepaper_sound', canvasState: canvasState || undefined });
|
|
2423
|
+
}
|
|
2424
|
+
const soundtrack = wavResult.result;
|
|
2425
|
+
// A refusal is a real answer — "no sounds are placed" travels intact
|
|
2426
|
+
// rather than becoming an empty file.
|
|
2427
|
+
if (!soundtrack?.success || !soundtrack.wavBase64) {
|
|
2428
|
+
return executedResult(code, soundtrack, wavResult.screenshot, soundDescription);
|
|
2429
|
+
}
|
|
2430
|
+
try {
|
|
2431
|
+
const { filePath, fileSize } = await saveExportToFile(`data:audio/wav;base64,${soundtrack.wavBase64}`, 'wav', 'soundtrack');
|
|
2432
|
+
const clean = { ...soundtrack, wavBase64: undefined, filePath, fileSize };
|
|
2433
|
+
return {
|
|
2434
|
+
content: [{
|
|
2435
|
+
type: 'text',
|
|
2436
|
+
text: `Soundtrack rendered to file:\n\nFile: ${filePath}\n`
|
|
2437
|
+
+ `Size: ${(fileSize / 1024).toFixed(1)} KB\n`
|
|
2438
|
+
+ `Duration: ${soundtrack.duration}s at ${soundtrack.sampleRate} Hz\n`
|
|
2439
|
+
+ `Mixed: ${soundtrack.placed} sound(s)\n`
|
|
2440
|
+
// The drop count rides on the success path, because a file
|
|
2441
|
+
// that wrote fine while missing sounds is the case nobody
|
|
2442
|
+
// goes looking for.
|
|
2443
|
+
+ (soundtrack.dropped ? `DROPPED: ${soundtrack.incomplete}\n` : '')
|
|
2444
|
+
+ `\nResult: ${JSON.stringify(clean, null, 2)}`,
|
|
2445
|
+
}],
|
|
2446
|
+
};
|
|
2447
|
+
}
|
|
2448
|
+
catch (saveError) {
|
|
2449
|
+
const stripped = {
|
|
2450
|
+
...soundtrack,
|
|
2451
|
+
wavBase64: `[${soundtrack.byteLength} bytes of WAV — file save failed: ${saveError instanceof Error ? saveError.message : 'unknown error'}]`,
|
|
2452
|
+
};
|
|
2453
|
+
return executedResult(code, stripped, wavResult.screenshot, soundDescription);
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
return executeOrGenerate(code, soundDescription, options, 'pinepaper_sound');
|
|
2457
|
+
}
|
|
2458
|
+
case 'pinepaper_motion': {
|
|
2459
|
+
const input = MotionInputSchema.parse(args);
|
|
2460
|
+
const code = codeGenerator.generateMotion(input);
|
|
2461
|
+
const described = input.action === 'list'
|
|
2462
|
+
? 'Listed the generator motion catalogue'
|
|
2463
|
+
: `Applied motion ${input.motion}`;
|
|
2464
|
+
return executeOrGenerate(code, described, options, 'pinepaper_motion');
|
|
2465
|
+
}
|
|
2466
|
+
case 'pinepaper_path': {
|
|
2467
|
+
const input = PathOpInputSchema.parse(args);
|
|
2468
|
+
const code = codeGenerator.generatePathOp(input);
|
|
2469
|
+
const described = input.action === 'boolean'
|
|
2470
|
+
? `Path boolean (${input.op}) over ${input.itemIds?.length ?? 0} items`
|
|
2471
|
+
: `Path ${input.action}${input.itemId ? ` on ${input.itemId}` : ''}`;
|
|
2472
|
+
return executeOrGenerate(code, described, options, 'pinepaper_path');
|
|
2473
|
+
}
|
|
1970
2474
|
case 'pinepaper_agent_analyze': {
|
|
1971
2475
|
const input = AgentAnalyzeInputSchema.parse(args);
|
|
1972
2476
|
const code = codeGenerator.generateAgentAnalyze(input);
|