@pinepaper.studio/mcp-server 1.6.8 → 1.6.10
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 +207 -7
- package/README.pt-BR.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/agent/export-manager.d.ts.map +1 -1
- package/dist/agent/export-manager.js +5 -3
- package/dist/agent/export-manager.js.map +1 -1
- 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 +271 -63
- package/dist/browser/puppeteer-controller.js.map +1 -1
- package/dist/cli.js +12 -0
- package/dist/cli.js.map +1 -1
- package/dist/design/design-systems.d.ts +105 -7
- package/dist/design/design-systems.d.ts.map +1 -1
- package/dist/design/design-systems.js +210 -16
- package/dist/design/design-systems.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +94 -6
- package/dist/tools/definitions.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 +30 -0
- package/dist/tools/engine-surface.d.ts.map +1 -0
- package/dist/tools/engine-surface.js +1188 -0
- package/dist/tools/engine-surface.js.map +1 -0
- package/dist/tools/handlers/font.d.ts.map +1 -1
- package/dist/tools/handlers/font.js +77 -10
- package/dist/tools/handlers/font.js.map +1 -1
- package/dist/tools/handlers.d.ts +16 -0
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +329 -12
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/minimal-descriptions.d.ts.map +1 -1
- package/dist/tools/minimal-descriptions.js +2 -1
- package/dist/tools/minimal-descriptions.js.map +1 -1
- package/dist/tools/toolkits.d.ts.map +1 -1
- package/dist/tools/toolkits.js +1 -0
- package/dist/tools/toolkits.js.map +1 -1
- package/dist/types/code-generator.d.ts +10 -0
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +437 -87
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +119 -74
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +62 -6
- package/dist/types/schemas.js.map +1 -1
- package/manifest.json +5 -1
- package/package.json +5 -2
package/dist/tools/handlers.js
CHANGED
|
@@ -10,7 +10,7 @@ import { CreateItemInputSchema, ModifyItemInputSchema, DeleteItemInputSchema, Ad
|
|
|
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,9 +29,10 @@ 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, appendFile, unlink } from 'node:fs/promises';
|
|
32
|
+
import { writeFile, mkdir, appendFile, unlink, readFile, stat } from 'node:fs/promises';
|
|
33
33
|
import { tmpdir } from 'node:os';
|
|
34
|
-
import { join } from 'node:path';
|
|
34
|
+
import { join, extname } from 'node:path';
|
|
35
|
+
import { fileURLToPath } from 'node:url';
|
|
35
36
|
import { getBrowserController, resetBrowserController, } from '../browser/puppeteer-controller.js';
|
|
36
37
|
import { sameStudioTarget, validateStudioUrl } from '../browser/url-target.js';
|
|
37
38
|
import { getPerformanceTracker } from '../metrics/index.js';
|
|
@@ -84,6 +85,80 @@ function getExportDir() {
|
|
|
84
85
|
// pasted into a response.
|
|
85
86
|
export const ALWAYS_SAVE_FORMATS = new Set(['mp4', 'webm', 'gif', 'pdf', 'wav']);
|
|
86
87
|
const SAVE_THRESHOLD_BYTES = 500_000; // ~500KB base64 ≈ 375KB decoded
|
|
88
|
+
/**
|
|
89
|
+
* Governor budget for the export run itself.
|
|
90
|
+
*
|
|
91
|
+
* FxTool's runGenerated defaults to a 10s async-tail timeout — right for a
|
|
92
|
+
* scene-building script that has no business running longer, and fatal for an
|
|
93
|
+
* export, which renders frame by frame and is capped at 600s of footage. The
|
|
94
|
+
* export died at ten seconds with a PP_TIMEOUT that read like a bug in the
|
|
95
|
+
* scene. This raises the budget rather than bypassing the governor, so the
|
|
96
|
+
* report, the item budget and the seeded PRNG all survive.
|
|
97
|
+
*
|
|
98
|
+
* PINEPAPER_EXPORT_TIMEOUT overrides it. The default matches the floor put on
|
|
99
|
+
* CDP's own per-call timeout (MIN_PROTOCOL_TIMEOUT_MS in
|
|
100
|
+
* puppeteer-controller.ts), so raising this past the default without also
|
|
101
|
+
* raising PINEPAPER_TIMEOUT just moves the execution to a protocol error —
|
|
102
|
+
* which is why that case says so out loud rather than failing obscurely later.
|
|
103
|
+
*/
|
|
104
|
+
const EXPORT_GOVERNOR_DEFAULT_MS = 300_000;
|
|
105
|
+
function exportGovernorTimeoutMs() {
|
|
106
|
+
const raw = Number(process.env.PINEPAPER_EXPORT_TIMEOUT);
|
|
107
|
+
if (!Number.isFinite(raw) || raw <= 0)
|
|
108
|
+
return EXPORT_GOVERNOR_DEFAULT_MS;
|
|
109
|
+
const protocolFloor = Math.max(Number(process.env.PINEPAPER_TIMEOUT) || 0, EXPORT_GOVERNOR_DEFAULT_MS);
|
|
110
|
+
if (raw > protocolFloor) {
|
|
111
|
+
console.error(`[PinePaper] PINEPAPER_EXPORT_TIMEOUT=${raw}ms is above the browser's protocol timeout (${protocolFloor}ms) — ` +
|
|
112
|
+
'raise PINEPAPER_TIMEOUT to match, or an export past that point fails as a protocol error rather than a governor timeout.');
|
|
113
|
+
}
|
|
114
|
+
return raw;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Turn whatever the caller named into something the page can load.
|
|
118
|
+
*
|
|
119
|
+
* http(s) and data URLs pass straight through — the page fetches those itself,
|
|
120
|
+
* and it reports the reason when it cannot. A LOCAL PATH is read here, because
|
|
121
|
+
* a browser page cannot open file:// and refusing it outright left an agent
|
|
122
|
+
* holding images it could not import with nothing to try instead.
|
|
123
|
+
*
|
|
124
|
+
* Bounded deliberately: an image extension, an existing regular file, and a
|
|
125
|
+
* size cap. This reads the user's disk on their instruction, the same as the
|
|
126
|
+
* export side writes to it, and neither should be a wildcard.
|
|
127
|
+
*/
|
|
128
|
+
const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.bmp', '.avif']);
|
|
129
|
+
const MAX_LOCAL_IMAGE_BYTES = 32 * 1024 * 1024;
|
|
130
|
+
const IMAGE_MIME = {
|
|
131
|
+
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif',
|
|
132
|
+
'.webp': 'image/webp', '.svg': 'image/svg+xml', '.bmp': 'image/bmp', '.avif': 'image/avif',
|
|
133
|
+
};
|
|
134
|
+
export async function resolveImageSource(input) {
|
|
135
|
+
const raw = input.trim();
|
|
136
|
+
if (/^(https?:|data:)/i.test(raw))
|
|
137
|
+
return { src: raw };
|
|
138
|
+
const path = raw.startsWith('file://') ? fileURLToPath(raw) : raw;
|
|
139
|
+
const ext = extname(path).toLowerCase();
|
|
140
|
+
if (!IMAGE_EXTENSIONS.has(ext)) {
|
|
141
|
+
return {
|
|
142
|
+
error: `"${input}" is neither an http(s) URL nor a path to an image file. `
|
|
143
|
+
+ `Recognised image extensions: ${[...IMAGE_EXTENSIONS].join(', ')}.`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
let bytes;
|
|
147
|
+
try {
|
|
148
|
+
const info = await stat(path);
|
|
149
|
+
if (!info.isFile())
|
|
150
|
+
return { error: `"${path}" is not a file.` };
|
|
151
|
+
if (info.size > MAX_LOCAL_IMAGE_BYTES) {
|
|
152
|
+
return { error: `"${path}" is ${(info.size / 1024 / 1024).toFixed(1)} MB, above the ${MAX_LOCAL_IMAGE_BYTES / 1024 / 1024} MB import cap. Resize it, or serve it over http.` };
|
|
153
|
+
}
|
|
154
|
+
bytes = await readFile(path);
|
|
155
|
+
}
|
|
156
|
+
catch (e) {
|
|
157
|
+
const why = e instanceof Error ? e.message : 'unknown error';
|
|
158
|
+
return { error: `could not read "${path}": ${why}. Paths are resolved from the server's working directory, so pass an absolute path if in doubt.` };
|
|
159
|
+
}
|
|
160
|
+
return { src: `data:${IMAGE_MIME[ext] ?? 'application/octet-stream'};base64,${bytes.toString('base64')}` };
|
|
161
|
+
}
|
|
87
162
|
export function getFileExtension(format) {
|
|
88
163
|
const extMap = { mp4: 'mp4', webm: 'webm', gif: 'gif', pdf: 'pdf', png: 'png', svg: 'svg', wav: 'wav' };
|
|
89
164
|
return extMap[format] || format;
|
|
@@ -122,6 +197,8 @@ async function saveExportToFile(data, format, platform) {
|
|
|
122
197
|
* recoverable. `releaseExport` runs only once the whole file is written.
|
|
123
198
|
*/
|
|
124
199
|
async function streamRetainedExportToFile(controller, retained, platform) {
|
|
200
|
+
// Reassigned once if the store corrects the size below.
|
|
201
|
+
let corrected = false;
|
|
125
202
|
const exportDir = getExportDir();
|
|
126
203
|
await mkdir(exportDir, { recursive: true });
|
|
127
204
|
const ext = getFileExtension(retained.format);
|
|
@@ -139,18 +216,78 @@ async function streamRetainedExportToFile(controller, retained, platform) {
|
|
|
139
216
|
await unlink(filePath);
|
|
140
217
|
}
|
|
141
218
|
catch { /* nothing written yet */ }
|
|
142
|
-
return new Error(
|
|
143
|
-
|
|
219
|
+
return new Error(
|
|
220
|
+
// The promise now names the call that keeps it. It was true before and
|
|
221
|
+
// unreachable: reading a held export mutates nothing, so "it can be paged
|
|
222
|
+
// again" was always correct and there was no door to page it through.
|
|
223
|
+
`${why} (export "${retained.exportId}" is still held in the studio, ${offset} of ${retained.size} bytes read; ` +
|
|
224
|
+
`page it out with pinepaper_export_store action 'save', exportId "${retained.exportId}" — reading is idempotent, ` +
|
|
225
|
+
'so retrying costs nothing and the export is released only once a whole file is written. Do it BEFORE the next ' +
|
|
226
|
+
'export: a new one evicts the oldest held bytes to make room)');
|
|
144
227
|
};
|
|
228
|
+
// A READ THAT FAILS ON SIZE IS NOT A READ THAT FAILS.
|
|
229
|
+
//
|
|
230
|
+
// A 17 MB memphis chunk died on its FOURTH 4 MB read — three had already
|
|
231
|
+
// landed — with "Failed to write data to data pipe". That is CDP refusing to
|
|
232
|
+
// carry one oversized value across the bridge, not the store refusing to
|
|
233
|
+
// serve it: the render had succeeded and the bytes were sitting right there.
|
|
234
|
+
// The loop abandoned the whole export anyway, and the caller was left with an
|
|
235
|
+
// error that explained the bytes were still held and no way to go get them.
|
|
236
|
+
//
|
|
237
|
+
// So a transport failure halves the request and asks again, down to a floor,
|
|
238
|
+
// before giving up. The offset does not move, so a retry costs one re-read,
|
|
239
|
+
// never a corrupt file. A refusal from the STORE (evicted, out of range) is
|
|
240
|
+
// NOT retried — that answer will not change with a smaller window, and
|
|
241
|
+
// retrying it would only spend the caller's time to reach the same place.
|
|
242
|
+
const MIN_CHUNK_BYTES = 256 * 1024;
|
|
243
|
+
let window = chunkBytes;
|
|
145
244
|
for (;;) {
|
|
146
|
-
const
|
|
245
|
+
const want = Math.min(window, retained.size - offset);
|
|
246
|
+
const code = codeGenerator.generateReadExportChunk(retained.exportId, offset, want);
|
|
247
|
+
// Deliberately governed at the default budget: a chunk read is a slice of a
|
|
248
|
+
// string already in memory. If one of these ever needs ten seconds, the
|
|
249
|
+
// store is wedged and a timeout is the correct answer, not a longer wait.
|
|
147
250
|
const run = await controller.executeCode(code, false);
|
|
148
|
-
if (!run.success)
|
|
149
|
-
|
|
251
|
+
if (!run.success) {
|
|
252
|
+
if (want > MIN_CHUNK_BYTES) {
|
|
253
|
+
window = Math.max(MIN_CHUNK_BYTES, Math.floor(want / 2));
|
|
254
|
+
console.error(`[PinePaper] a ${want}-byte read of "${retained.exportId}" failed (${run.error || 'unknown error'}); `
|
|
255
|
+
+ `retrying the same offset at ${window} bytes`);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
throw await abandon(`reading the export failed even at the ${MIN_CHUNK_BYTES}-byte floor: ${run.error || 'unknown error'}`);
|
|
259
|
+
}
|
|
150
260
|
const chunk = run.result;
|
|
151
261
|
if (!chunk || chunk.ok !== true) {
|
|
262
|
+
// A RANGE REFUSAL IS A CORRECTION, and it carries the real size.
|
|
263
|
+
//
|
|
264
|
+
// readExport refuses an out-of-range read by name rather than clamping —
|
|
265
|
+
// deliberately, so a miscounting caller is told instead of handed a short
|
|
266
|
+
// chunk it treats as the tail, which surfaces later as a truncated video
|
|
267
|
+
// with nothing pointing back at the cause. The size this loop is working
|
|
268
|
+
// from came from the export result, and `save` works from a listing; when
|
|
269
|
+
// either disagrees with the file, the refusal is the authority. Take it
|
|
270
|
+
// ONCE — a second disagreement is not a miscount, it is a moving target.
|
|
271
|
+
// Only when it DISAGREES. Every range refusal quotes the real size,
|
|
272
|
+
// including the ones that are refusing for some other reason — so
|
|
273
|
+
// matching the number alone treated an eviction as a miscount, spent the
|
|
274
|
+
// one correction on a no-op re-read, and hid the actual reason for an
|
|
275
|
+
// iteration.
|
|
276
|
+
const stated = /\(size (\d+)\)/.exec(chunk?.reason ?? '');
|
|
277
|
+
if (stated && !corrected && Number(stated[1]) !== retained.size) {
|
|
278
|
+
corrected = true;
|
|
279
|
+
const was = retained.size;
|
|
280
|
+
retained = { ...retained, size: Number(stated[1]) };
|
|
281
|
+
console.error(`[PinePaper] the store reports "${retained.exportId}" as ${retained.size} bytes, not ${was}; `
|
|
282
|
+
+ "taking the store's number and continuing");
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
152
285
|
// An evicted id is its own failure: a later export reclaimed the space.
|
|
153
|
-
// Re-exporting is the caller's decision, never this loop's.
|
|
286
|
+
// Re-exporting is the caller's decision, never this loop's. The engine
|
|
287
|
+
// records an eviction in browser storage so it survives a reload, but
|
|
288
|
+
// that storage is best-effort — a private window, cleared site data or
|
|
289
|
+
// storage switched off all fall back to memory-only. So `evicted` being
|
|
290
|
+
// ABSENT means unknown, never that the export was not there.
|
|
154
291
|
throw await abandon(chunk?.evicted
|
|
155
292
|
? `the export was evicted mid-read: ${chunk.reason}`
|
|
156
293
|
: `the studio refused the read: ${chunk?.reason || 'no reason given'}`);
|
|
@@ -240,10 +377,25 @@ function executedResult(code, result, screenshot, description, report) {
|
|
|
240
377
|
if (Object.keys(governor).length)
|
|
241
378
|
resultObj.governor = governor;
|
|
242
379
|
}
|
|
380
|
+
// THE CODE ECHO WAS 90% OF THE RESPONSE, AND THE AGENT WROTE THE REQUEST.
|
|
381
|
+
//
|
|
382
|
+
// Measured on a 50-item batch create: 8,740 characters of echoed source
|
|
383
|
+
// against 983 characters of actual result. A tester's cost study found the
|
|
384
|
+
// same thing from outside — response verbosity alone was 86% of the native
|
|
385
|
+
// path's token cost, and it is what makes enumerating 50 creates through
|
|
386
|
+
// tools 33x more expensive than a loop that produces the same picture.
|
|
387
|
+
//
|
|
388
|
+
// On a SUCCESSFUL browser run the code has already done its job and the
|
|
389
|
+
// caller knows what it asked for. It is still returned where it is the
|
|
390
|
+
// deliverable — 'code' execution mode — and where it is the evidence: every
|
|
391
|
+
// error path passes it in `details`. PINEPAPER_ECHO_CODE=1 brings it back
|
|
392
|
+
// for anyone debugging a generator.
|
|
243
393
|
const content = [
|
|
244
394
|
{ type: 'text', text: JSON.stringify(resultObj, null, 2) },
|
|
245
|
-
{ type: 'text', text: `Generated code:\n\`\`\`javascript\n${code}\n\`\`\`` },
|
|
246
395
|
];
|
|
396
|
+
if (process.env.PINEPAPER_ECHO_CODE === '1') {
|
|
397
|
+
content.push({ type: 'text', text: `Generated code:\n\`\`\`javascript\n${code}\n\`\`\`` });
|
|
398
|
+
}
|
|
247
399
|
if (screenshot) {
|
|
248
400
|
content.push({
|
|
249
401
|
type: 'image',
|
|
@@ -493,8 +645,47 @@ ${code}
|
|
|
493
645
|
canvasState: canvasState || undefined,
|
|
494
646
|
});
|
|
495
647
|
}
|
|
648
|
+
// TWO VERDICTS. `result.success` says the CODE RAN. The emitted code has its
|
|
649
|
+
// own verdict, and almost every emitter in this repo returns
|
|
650
|
+
// `{ success: false, error }` when it cannot do the thing — a guard that
|
|
651
|
+
// found a subsystem missing, an engine call that refused, a precondition that
|
|
652
|
+
// was not met. Nothing here read it, so every one of those came back to the
|
|
653
|
+
// agent as a success with an error buried in the payload.
|
|
654
|
+
//
|
|
655
|
+
// Measured twice by a beta tester: two of five video chunks silently lost,
|
|
656
|
+
// and a rigging session where "app.riggingSystem unavailable" rode inside an
|
|
657
|
+
// outer success and the agent kept building on a rig that did not exist.
|
|
658
|
+
// pinepaper_agent_export got this check in 1.6.9; it belongs HERE, where
|
|
659
|
+
// every tool inherits it.
|
|
660
|
+
const inner = innerFailure(result.result);
|
|
661
|
+
if (inner) {
|
|
662
|
+
const canvasState = await captureCanvasState(controller);
|
|
663
|
+
return errorResult(ErrorCodes.EXECUTION_ERROR, inner, { code, result: result.result, governorReport: result.report }, { toolName, canvasState: canvasState || undefined });
|
|
664
|
+
}
|
|
496
665
|
return executedResult(code, result.result, result.screenshot, description, result.report);
|
|
497
666
|
}
|
|
667
|
+
/**
|
|
668
|
+
* The emitted code's own verdict, when it reported one and it was a failure.
|
|
669
|
+
*
|
|
670
|
+
* Deliberately narrow. `success: false` is the shape this repo's emitters use
|
|
671
|
+
* and the only one treated as a failure — a result merely LACKING `success` is
|
|
672
|
+
* not a failure, because plenty of emitters return a bare value (an id, a
|
|
673
|
+
* count, a list) and reading absence as refusal would turn every one of those
|
|
674
|
+
* into an error. `ok: false` is included because the export-store emitters use
|
|
675
|
+
* it, and it carries a `reason` rather than an `error`.
|
|
676
|
+
*/
|
|
677
|
+
export function innerFailure(value) {
|
|
678
|
+
if (!value || typeof value !== 'object')
|
|
679
|
+
return null;
|
|
680
|
+
const r = value;
|
|
681
|
+
const failed = r.success === false || r.ok === false;
|
|
682
|
+
if (!failed)
|
|
683
|
+
return null;
|
|
684
|
+
const why = typeof r.error === 'string' ? r.error
|
|
685
|
+
: typeof r.reason === 'string' ? r.reason
|
|
686
|
+
: 'the studio reported failure without naming a reason';
|
|
687
|
+
return why;
|
|
688
|
+
}
|
|
498
689
|
// =============================================================================
|
|
499
690
|
// I18N HELPER FUNCTIONS
|
|
500
691
|
// =============================================================================
|
|
@@ -2021,6 +2212,74 @@ You can now start creating new items on a clean canvas.`,
|
|
|
2021
2212
|
}
|
|
2022
2213
|
return batchResult;
|
|
2023
2214
|
}
|
|
2215
|
+
case 'pinepaper_export_store': {
|
|
2216
|
+
const input = ExportStoreInputSchema.parse(args);
|
|
2217
|
+
const controller = options.browserController || getBrowserController();
|
|
2218
|
+
if (!controller.connected) {
|
|
2219
|
+
try {
|
|
2220
|
+
await controller.connect();
|
|
2221
|
+
}
|
|
2222
|
+
catch (connectError) {
|
|
2223
|
+
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' });
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
const held = await controller.executeCode(codeGenerator.generateListExports(), false);
|
|
2227
|
+
if (!held.success) {
|
|
2228
|
+
return errorResult(ErrorCodes.EXECUTION_ERROR, `listing the export store failed: ${held.error || 'unknown error'}`, {}, { toolName: 'pinepaper_export_store' });
|
|
2229
|
+
}
|
|
2230
|
+
const listing = held.result;
|
|
2231
|
+
if (!listing || listing.ok !== true) {
|
|
2232
|
+
return errorResult(ErrorCodes.EXECUTION_ERROR, listing?.reason || 'the studio did not answer with a listing', {}, { toolName: 'pinepaper_export_store' });
|
|
2233
|
+
}
|
|
2234
|
+
const exports = listing.exports ?? [];
|
|
2235
|
+
if (input.action === 'list') {
|
|
2236
|
+
return dataResult({
|
|
2237
|
+
exports,
|
|
2238
|
+
held: exports.length,
|
|
2239
|
+
heldBytes: exports.reduce((n, e) => n + (e.size || 0), 0),
|
|
2240
|
+
note: exports.length > 1
|
|
2241
|
+
? 'A later export evicts the OLDEST held ones to make room. Save or release each before starting the next.'
|
|
2242
|
+
: undefined,
|
|
2243
|
+
});
|
|
2244
|
+
}
|
|
2245
|
+
// Both remaining actions name an id, and an id the store does not have
|
|
2246
|
+
// is the one thing worth saying carefully. The engine records an
|
|
2247
|
+
// eviction durably enough to survive a reload, so a read of that id
|
|
2248
|
+
// will usually say "evicted" — but the record is best-effort browser
|
|
2249
|
+
// storage and a listing cannot consult it at all. Absence of the id
|
|
2250
|
+
// here is therefore "gone, cause unknown", and stating one cause as
|
|
2251
|
+
// fact would be inventing the more interesting half of the answer.
|
|
2252
|
+
const entry = exports.find((e) => e.id === input.exportId);
|
|
2253
|
+
if (!entry) {
|
|
2254
|
+
return errorResult(ErrorCodes.INVALID_PARAMS, `the studio is not holding "${input.exportId}". It was released, evicted to make room for a later export, `
|
|
2255
|
+
+ `or never existed here — a listing cannot tell which. `
|
|
2256
|
+
+ `${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' });
|
|
2257
|
+
}
|
|
2258
|
+
if (input.action === 'release') {
|
|
2259
|
+
const dropped = await controller.executeCode(codeGenerator.generateReleaseExport(entry.id), false);
|
|
2260
|
+
const outcome = dropped.result;
|
|
2261
|
+
if (!dropped.success || outcome?.ok !== true) {
|
|
2262
|
+
return errorResult(ErrorCodes.EXECUTION_ERROR, `releasing "${entry.id}" failed: ${dropped.error || outcome?.reason || 'unknown error'}`, { exportId: entry.id }, { toolName: 'pinepaper_export_store' });
|
|
2263
|
+
}
|
|
2264
|
+
return dataResult({ released: entry.id, freedBytes: entry.size });
|
|
2265
|
+
}
|
|
2266
|
+
// save: the same pager agent_export uses, pointed at an id instead of a
|
|
2267
|
+
// fresh render. Reading is idempotent, so a save that failed halfway
|
|
2268
|
+
// can simply be run again — there is no partial state to clean up here.
|
|
2269
|
+
try {
|
|
2270
|
+
const { filePath, fileSize, chunks } = await streamRetainedExportToFile(controller, { exportId: entry.id, size: entry.size, format: entry.format }, input.platform || 'recovered');
|
|
2271
|
+
return {
|
|
2272
|
+
content: [{
|
|
2273
|
+
type: 'text',
|
|
2274
|
+
text: `Export recovered from the studio's store:\n\nFile: ${filePath}\nFormat: ${entry.format}\n`
|
|
2275
|
+
+ `Size: ${(fileSize / 1024).toFixed(1)} KB\nPaged out in ${chunks} chunk${chunks === 1 ? '' : 's'}, then released\n`,
|
|
2276
|
+
}],
|
|
2277
|
+
};
|
|
2278
|
+
}
|
|
2279
|
+
catch (streamError) {
|
|
2280
|
+
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' });
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2024
2283
|
case 'pinepaper_agent_export': {
|
|
2025
2284
|
const input = AgentExportInputSchema.parse(args);
|
|
2026
2285
|
const code = codeGenerator.generateAgentExport(input);
|
|
@@ -2048,12 +2307,46 @@ You can now start creating new items on a clean canvas.`,
|
|
|
2048
2307
|
if (!exportSessionManager.hasActiveJob()) {
|
|
2049
2308
|
exportSessionManager.startJob({ name: 'auto_session', screenshotPolicy: 'on_complete' });
|
|
2050
2309
|
}
|
|
2051
|
-
const exportBrowserResult = await controller.executeCode(code, false
|
|
2310
|
+
const exportBrowserResult = await controller.executeCode(code, false, {
|
|
2311
|
+
governorTimeoutMs: exportGovernorTimeoutMs(),
|
|
2312
|
+
});
|
|
2052
2313
|
if (!exportBrowserResult.success) {
|
|
2053
2314
|
const canvasState = await captureCanvasState(controller);
|
|
2054
2315
|
return errorResult(ErrorCodes.EXECUTION_ERROR, exportBrowserResult.error || 'Export failed', { code }, { toolName: 'pinepaper_agent_export', canvasState: canvasState || undefined });
|
|
2055
2316
|
}
|
|
2056
2317
|
const exportResult = exportBrowserResult.result;
|
|
2318
|
+
// TWO VERDICTS, and only one of them was being read.
|
|
2319
|
+
//
|
|
2320
|
+
// `exportBrowserResult.success` says the CODE RAN. The export's own
|
|
2321
|
+
// verdict is `exportResult.success`, and every emitted branch that
|
|
2322
|
+
// cannot deliver returns `{ success: false, error }` — a studio without
|
|
2323
|
+
// the renderer, an encoder that died, a blob past the inline ceiling.
|
|
2324
|
+
// None of that was checked: the value fell through to the inline return
|
|
2325
|
+
// below, which reports a success and hands back a result object nobody
|
|
2326
|
+
// reads past the first line.
|
|
2327
|
+
//
|
|
2328
|
+
// Measured cost: a five-chunk video where two chunks came back
|
|
2329
|
+
// `success: false, error: "export failed: Failed to write data to data
|
|
2330
|
+
// pipe"` under an outer `"success": true`. The caller shipped a
|
|
2331
|
+
// three-chunk video believing it had five. A silent success is the one
|
|
2332
|
+
// failure mode this surface can never afford, because nothing
|
|
2333
|
+
// downstream has any reason to look again.
|
|
2334
|
+
if (exportResult && exportResult.success === false) {
|
|
2335
|
+
const canvasState = await captureCanvasState(controller);
|
|
2336
|
+
// A "data pipe" failure is the BROWSER TRANSPORT refusing to carry
|
|
2337
|
+
// the result back, not the encoder refusing to make it — the string
|
|
2338
|
+
// exists nowhere in the engine. So the render may well have finished
|
|
2339
|
+
// and the bytes may be sitting in the export store right now. Saying
|
|
2340
|
+
// so is the difference between recovering an export and re-rendering
|
|
2341
|
+
// one, and re-rendering is what evicts an earlier chunk's bytes.
|
|
2342
|
+
const looksLikeTransport = /data pipe/i.test(String(exportResult.error ?? ''));
|
|
2343
|
+
return errorResult(ErrorCodes.EXECUTION_ERROR, `export failed: ${exportResult.error || 'the studio reported failure without naming a reason'}`
|
|
2344
|
+
+ (looksLikeTransport
|
|
2345
|
+
? ' — that is the browser transport failing to carry the result back, not necessarily the render failing.'
|
|
2346
|
+
+ " Call pinepaper_export_store action 'list' BEFORE re-rendering: the bytes may already be held, and a"
|
|
2347
|
+
+ ' fresh export can evict an earlier one to make room.'
|
|
2348
|
+
: ''), { code, format: exportResult.format ?? input.format, result: exportResult }, { toolName: 'pinepaper_agent_export', canvasState: canvasState || undefined });
|
|
2349
|
+
}
|
|
2057
2350
|
const format = exportResult?.format || input.format || 'png';
|
|
2058
2351
|
const data = exportResult?.data;
|
|
2059
2352
|
// RETAINED: the studio held the encoded file in its export store rather
|
|
@@ -2145,6 +2438,18 @@ You can now start creating new items on a clean canvas.`,
|
|
|
2145
2438
|
}
|
|
2146
2439
|
return dataResult(sys);
|
|
2147
2440
|
}
|
|
2441
|
+
case 'list_motion': {
|
|
2442
|
+
// Curves and durations together, because a motion token is a shape
|
|
2443
|
+
// AND a length. The curves were published and the durations beside
|
|
2444
|
+
// them in the same store were not, so a caller could match a
|
|
2445
|
+
// system's easing and had to invent its timing.
|
|
2446
|
+
const easings = designSystems.listEasings(input.authoredOnly);
|
|
2447
|
+
const durations = designSystems.listDurations(input.authoredOnly);
|
|
2448
|
+
return dataResult({
|
|
2449
|
+
easings, durations,
|
|
2450
|
+
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.',
|
|
2451
|
+
});
|
|
2452
|
+
}
|
|
2148
2453
|
case 'list_easings':
|
|
2149
2454
|
return dataResult({ easings: designSystems.listEasings(input.authoredOnly) });
|
|
2150
2455
|
case 'list_styles':
|
|
@@ -2312,7 +2617,19 @@ You can now start creating new items on a clean canvas.`,
|
|
|
2312
2617
|
// -----------------------------------------------------------------------
|
|
2313
2618
|
case 'pinepaper_import_image': {
|
|
2314
2619
|
const parsed = ImportImageInputSchema.parse(args);
|
|
2315
|
-
|
|
2620
|
+
// A LOCAL PATH IS THE COMMON CASE AND WAS THE REJECTED ONE.
|
|
2621
|
+
//
|
|
2622
|
+
// The browser cannot open file:// from a page, so this tool took URLs
|
|
2623
|
+
// only — which leaves an agent that has just generated or downloaded
|
|
2624
|
+
// ten images with no way in, and no alternative named. The server CAN
|
|
2625
|
+
// read the file, so it reads it and hands the page a data URL. Nothing
|
|
2626
|
+
// about the page changes; the bytes simply arrive by a route the
|
|
2627
|
+
// browser is allowed to use.
|
|
2628
|
+
const resolved = await resolveImageSource(parsed.url);
|
|
2629
|
+
if ('error' in resolved) {
|
|
2630
|
+
return errorResult(ErrorCodes.INVALID_PARAMS, resolved.error, { url: parsed.url }, { toolName: 'pinepaper_import_image' });
|
|
2631
|
+
}
|
|
2632
|
+
const code = codeGenerator.generateImportImage({ ...parsed, url: resolved.src });
|
|
2316
2633
|
return executeOrGenerate(code, 'Imports image', options, 'pinepaper_import_image');
|
|
2317
2634
|
}
|
|
2318
2635
|
// -----------------------------------------------------------------------
|